TDMParser.java

  1. /* Copyright 2002-2020 CS Group
  2.  * Licensed to CS Group (CS) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * CS licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *   http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.orekit.files.ccsds;

  18. import javax.xml.parsers.ParserConfigurationException;
  19. import javax.xml.parsers.SAXParser;
  20. import javax.xml.parsers.SAXParserFactory;
  21. import java.io.BufferedReader;
  22. import java.io.FileInputStream;
  23. import java.io.IOException;
  24. import java.io.InputStream;
  25. import java.io.InputStreamReader;
  26. import java.nio.charset.StandardCharsets;
  27. import java.util.ArrayList;
  28. import java.util.List;
  29. import java.util.Locale;

  30. import org.hipparchus.exception.DummyLocalizable;
  31. import org.orekit.annotation.DefaultDataContext;
  32. import org.orekit.data.DataContext;
  33. import org.orekit.errors.OrekitException;
  34. import org.orekit.errors.OrekitMessages;
  35. import org.orekit.time.AbsoluteDate;
  36. import org.orekit.utils.IERSConventions;
  37. import org.xml.sax.Attributes;
  38. import org.xml.sax.InputSource;
  39. import org.xml.sax.Locator;
  40. import org.xml.sax.SAXException;
  41. import org.xml.sax.helpers.DefaultHandler;


  42. /**
  43.  * Class for CCSDS Tracking Data Message parsers.
  44.  *
  45.  * <p> This base class is immutable, and hence thread safe. When parts must be
  46.  * changed, such as reference date for Mission Elapsed Time or Mission Relative
  47.  * Time time systems, or the gravitational coefficient or the IERS conventions,
  48.  * the various {@code withXxx} methods must be called, which create a new
  49.  * immutable instance with the new parameters. This is a combination of the <a
  50.  * href="https://en.wikipedia.org/wiki/Builder_pattern">builder design
  51.  * pattern</a> and a <a href="http://en.wikipedia.org/wiki/Fluent_interface">fluent
  52.  * interface</a>.
  53.  *
  54.  * <p> This class allow the handling of both "keyvalue" and "xml" TDM file formats.
  55.  * Format can be inferred if file names ends respectively with ".txt" or ".xml".
  56.  * Otherwise it must be explicitely set using {@link #withFileFormat(TDMFileFormat)}
  57.  *
  58.  * <p>ParseInfo subclass regroups common parsing functions; and specific handlers were added
  59.  * for both file formats.
  60.  *
  61.  * <p>References:<p>
  62.  *  - <a href="https://public.ccsds.org/Pubs/503x0b1c1.pdf">CCSDS 503.0-B-1 recommended standard</a> ("Tracking Data Message", Blue Book, Issue 1, November 2007).<p>
  63.  *  - <a href="https://public.ccsds.org/Pubs/505x0b1.pdf">CCSDS 505.0-B-1 recommended standard</a> ("XML Specification for Navigation Data Message", Blue Book, Issue 1, December 2010).<p>
  64.  *
  65.  * @author Maxime Journot
  66.  * @since 9.0
  67.  */
  68. public class TDMParser extends DefaultHandler {

  69.     /** Enumerate for the format. */
  70.     public enum TDMFileFormat {

  71.         /** Keyvalue (text file with Key = Value lines). */
  72.         KEYVALUE,

  73.         /** XML format. */
  74.         XML,

  75.         /** UKNOWN file format, default format, throw an Orekit Exception if kept this way. */
  76.         UNKNOWN;
  77.     }

  78.     /** Format of the file to parse: KEYVALUE or XML. */
  79.     private TDMFileFormat fileFormat;

  80.     /** Reference date for Mission Elapsed Time or Mission Relative Time time systems. */
  81.     private final AbsoluteDate missionReferenceDate;

  82.     /** IERS Conventions. */
  83.     private final  IERSConventions conventions;

  84.     /** Indicator for simple or accurate EOP interpolation. */
  85.     private final  boolean simpleEOP;

  86.     /** Data context for frames, time scales, etc. */
  87.     private final DataContext dataContext;

  88.     /** Simple constructor.
  89.      * <p>
  90.      * This class is immutable, and hence thread safe. When parts
  91.      * must be changed, such fiel format or reference date for Mission Elapsed Time or
  92.      * Mission Relative Time time systems, or the IERS conventions,
  93.      * the various {@code withXxx} methods must be called,
  94.      * which create a new immutable instance with the new parameters. This
  95.      * is a combination of the
  96.      * <a href="https://en.wikipedia.org/wiki/Builder_pattern">builder design
  97.      * pattern</a> and a
  98.      * <a href="http://en.wikipedia.org/wiki/Fluent_interface">fluent
  99.      * interface</a>.
  100.      * </p>
  101.      * <p>
  102.      * The initial date for Mission Elapsed Time and Mission Relative Time time systems is not set here.
  103.      * If such time systems are used, it must be initialized before parsing by calling {@link
  104.      * #withMissionReferenceDate(AbsoluteDate)}.
  105.      * </p>
  106.      * <p>
  107.      * The IERS conventions to use is not set here. If it is needed in order to
  108.      * parse some reference frames or UT1 time scale, it must be initialized before
  109.      * parsing by calling {@link #withConventions(IERSConventions)}.
  110.      * </p>
  111.      * <p>
  112.      * The TDM file format to use is not set here. It may be automatically inferred while parsing
  113.      * if the name of the file to parse ends with ".txt" or ".xml".
  114.      * Otherwise it must be initialized before parsing by calling {@link #withFileFormat(TDMFileFormat)}
  115.      * </p>
  116.      *
  117.      * <p>This method uses the {@link DataContext#getDefault() default data context}. See
  118.      * {@link #withDataContext(DataContext)}.
  119.      */
  120.     @DefaultDataContext
  121.     public TDMParser() {
  122.         this(TDMFileFormat.UNKNOWN, AbsoluteDate.FUTURE_INFINITY, null, true,
  123.                 DataContext.getDefault());
  124.     }

  125.     /** Complete constructor.
  126.      * @param fileFormat The format of the file: KEYVALUE or XML
  127.      * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
  128.      * @param conventions IERS Conventions
  129.      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
  130.      * @param dataContext used to retrieve frames, time scales, etc.
  131.      */
  132.     private TDMParser(final TDMFileFormat fileFormat,
  133.                       final AbsoluteDate missionReferenceDate,
  134.                       final IERSConventions conventions,
  135.                       final boolean simpleEOP,
  136.                       final DataContext dataContext) {
  137.         this.fileFormat = fileFormat;
  138.         this.missionReferenceDate = missionReferenceDate;
  139.         this.conventions          = conventions;
  140.         this.simpleEOP            = simpleEOP;
  141.         this.dataContext = dataContext;
  142.     }

  143.     /** Set file format.
  144.      * @param newFileFormat The format of the file: KEYVALUE or XML
  145.      * @return a new instance, with file format set to newFileFormat
  146.      * @see #getFileFormat()
  147.      */
  148.     public TDMParser withFileFormat(final TDMFileFormat newFileFormat) {
  149.         return new TDMParser(newFileFormat, getMissionReferenceDate(), getConventions(),
  150.                 isSimpleEOP(), getDataContext());
  151.     }

  152.     /** Get file format.
  153.      * @return the file format
  154.      * @see #withFileFormat(TDMFileFormat)
  155.      */
  156.     public TDMFileFormat getFileFormat() {
  157.         return fileFormat;
  158.     }

  159.     /** Set initial date.
  160.      * @param newMissionReferenceDate mission reference date to use while parsing
  161.      * @return a new instance, with mission reference date replaced
  162.      * @see #getMissionReferenceDate()
  163.      */
  164.     public TDMParser withMissionReferenceDate(final AbsoluteDate newMissionReferenceDate) {
  165.         return new TDMParser(getFileFormat(), newMissionReferenceDate, getConventions(),
  166.                 isSimpleEOP(), getDataContext());
  167.     }

  168.     /** Get initial date.
  169.      * @return mission reference date to use while parsing
  170.      * @see #withMissionReferenceDate(AbsoluteDate)
  171.      */
  172.     public AbsoluteDate getMissionReferenceDate() {
  173.         return missionReferenceDate;
  174.     }

  175.     /** Set IERS conventions.
  176.      * @param newConventions IERS conventions to use while parsing
  177.      * @return a new instance, with IERS conventions replaced
  178.      * @see #getConventions()
  179.      */
  180.     public TDMParser withConventions(final IERSConventions newConventions) {
  181.         return new TDMParser(getFileFormat(), getMissionReferenceDate(), newConventions,
  182.                 isSimpleEOP(), getDataContext());
  183.     }

  184.     /** Get IERS conventions.
  185.      * @return IERS conventions to use while parsing
  186.      * @see #withConventions(IERSConventions)
  187.      */
  188.     public IERSConventions getConventions() {
  189.         return conventions;
  190.     }

  191.     /** Set EOP interpolation method.
  192.      * @param newSimpleEOP if true, tidal effects are ignored when interpolating EOP
  193.      * @return a new instance, with EOP interpolation method replaced
  194.      * @see #isSimpleEOP()
  195.      */
  196.     public TDMParser withSimpleEOP(final boolean newSimpleEOP) {
  197.         return new TDMParser(getFileFormat(), getMissionReferenceDate(), getConventions(),
  198.                 newSimpleEOP, getDataContext());
  199.     }

  200.     /** Get EOP interpolation method.
  201.      * @return true if tidal effects are ignored when interpolating EOP
  202.      * @see #withSimpleEOP(boolean)
  203.      */
  204.     public boolean isSimpleEOP() {
  205.         return simpleEOP;
  206.     }

  207.     /**
  208.      * Get the data context.
  209.      *
  210.      * @return the data context used for retrieving frames, time scales, etc.
  211.      */
  212.     public DataContext getDataContext() {
  213.         return dataContext;
  214.     }

  215.     /**
  216.      * Set the data context.
  217.      *
  218.      * @param newDataContext used for retrieving frames, time scales, etc.
  219.      * @return a new instance with the data context replaced.
  220.      */
  221.     public TDMParser withDataContext(final DataContext newDataContext) {
  222.         return new TDMParser(getFileFormat(), getMissionReferenceDate(), getConventions(),
  223.                 isSimpleEOP(), newDataContext);
  224.     }

  225.     /** Parse a CCSDS Tracking Data Message.
  226.      * @param fileName name of the file containing the message
  227.      * @return parsed file content in a TDMFile object
  228.      */
  229.     public TDMFile parse(final String fileName) {
  230.         try (InputStream stream = new FileInputStream(fileName)) {
  231.             return parse(stream, fileName);
  232.         } catch (IOException ioe) {
  233.             throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, fileName);
  234.         }
  235.     }

  236.     /** Parse a CCSDS Tracking Data Message.
  237.      * @param stream stream containing message
  238.      * @return parsed file content in a TDMFile object
  239.      */
  240.     public TDMFile parse(final InputStream stream) {
  241.         return parse(stream, "<unknown>");
  242.     }

  243.     /** Parse a CCSDS Tracking Data Message.
  244.      * @param stream stream containing message
  245.      * @param fileName name of the file containing the message (for error messages)
  246.      * @return parsed file content in a TDMFile object
  247.      */
  248.     public TDMFile parse(final InputStream stream, final String fileName) {

  249.         // Set the format of the file automatically
  250.         // If it is obvious and was not formerly specified
  251.         // Then, use a different parsing method for each file format
  252.         if (TDMFileFormat.UNKNOWN.equals(fileFormat)) {
  253.             if (fileName.toLowerCase(Locale.US).endsWith(".txt")) {
  254.                 // Keyvalue format case
  255.                 return this.withFileFormat(TDMFileFormat.KEYVALUE).parse(stream, fileName);
  256.             } else if (fileName.toLowerCase(Locale.US).endsWith(".xml")) {
  257.                 // XML format case
  258.                 return this.withFileFormat(TDMFileFormat.XML).parse(stream, fileName);
  259.             } else {
  260.                 throw new OrekitException(OrekitMessages.CCSDS_TDM_UNKNOWN_FORMAT, fileName);
  261.             }
  262.         } else if (this.fileFormat.equals(TDMFileFormat.KEYVALUE)) {
  263.             return parseKeyValue(stream, fileName);
  264.         } else if (this.fileFormat.equals(TDMFileFormat.XML)) {
  265.             return parseXml(stream, fileName);
  266.         } else {
  267.             throw new OrekitException(OrekitMessages.CCSDS_TDM_UNKNOWN_FORMAT, fileName);
  268.         }
  269.     }

  270.     /** Parse a CCSDS Tracking Data Message with KEYVALUE format.
  271.      * @param stream stream containing message
  272.      * @param fileName name of the file containing the message (for error messages)
  273.      * @return parsed file content in a TDMFile object
  274.      */
  275.     public TDMFile parseKeyValue(final InputStream stream, final String fileName) {

  276.         final KeyValueHandler handler = new KeyValueHandler(new ParseInfo(this.getMissionReferenceDate(),
  277.                                                                     this.getConventions(),
  278.                                                                     this.isSimpleEOP(),
  279.                                                                     fileName,
  280.                                                                     getDataContext()));
  281.         return handler.parse(stream, fileName);
  282.     }



  283.     /** Parse a CCSDS Tracking Data Message with XML format.
  284.      * @param stream stream containing message
  285.      * @param fileName name of the file containing the message (for error messages)
  286.      * @return parsed file content in a TDMFile object
  287.      */
  288.     public TDMFile parseXml(final InputStream stream, final String fileName) {
  289.         try {
  290.             // Create the handler
  291.             final XMLHandler handler = new XMLHandler(new ParseInfo(this.getMissionReferenceDate(),
  292.                                                                     this.getConventions(),
  293.                                                                     this.isSimpleEOP(),
  294.                                                                     fileName,
  295.                                                                     getDataContext()));

  296.             // Create the XML SAX parser factory
  297.             final SAXParserFactory factory = SAXParserFactory.newInstance();

  298.             // Build the parser and read the xml file
  299.             final SAXParser parser = factory.newSAXParser();
  300.             parser.parse(stream, handler);

  301.             // Get the content of the file
  302.             final TDMFile tdmFile = handler.parseInfo.tdmFile;

  303.             // Check time systems consistency
  304.             tdmFile.checkTimeSystems();

  305.             return tdmFile;
  306.         } catch (SAXException se) {
  307.             final OrekitException oe;
  308.             if (se.getException() != null && se.getException() instanceof OrekitException) {
  309.                 oe = (OrekitException) se.getException();
  310.             } else {
  311.                 oe = new OrekitException(se, new DummyLocalizable(se.getMessage()));
  312.             }
  313.             throw oe;
  314.         } catch (ParserConfigurationException | IOException e) {
  315.             // throw caught exception as an OrekitException
  316.             throw new OrekitException(e, new DummyLocalizable(e.getMessage()));
  317.         }
  318.     }

  319.     /** Private class used to stock TDM parsing info.
  320.      * @author sports
  321.      */
  322.     private static class ParseInfo {

  323.         /** Reference date for Mission Elapsed Time or Mission Relative Time time systems. */
  324.         private final AbsoluteDate missionReferenceDate;

  325.         /** IERS Conventions. */
  326.         private final  IERSConventions conventions;

  327.         /** Indicator for simple or accurate EOP interpolation. */
  328.         private final  boolean simpleEOP;

  329.         /** Data context. */
  330.         private final DataContext dataContext;

  331.         /** Name of the file. */
  332.         private String fileName;

  333.         /** Current Observation Block being parsed. */
  334.         private TDMFile.ObservationsBlock currentObservationsBlock;

  335.         /** Current line number. */
  336.         private int lineNumber;

  337.         /** Current parsed line. */
  338.         private String line;

  339.         /** TDMFile object being filled. */
  340.         private TDMFile tdmFile;

  341.         /** Key value of the current line being read. */
  342.         private KeyValue keyValue;

  343.         /** Temporary stored comments. */
  344.         private List<String> commentTmp;

  345.         /** Boolean indicating if the parser is currently parsing a meta-data block. */
  346.         private boolean parsingMetaData;

  347.         /** Boolean indicating if the parser is currently parsing a data block. */
  348.         private boolean parsingData;

  349.         /** Complete constructor.
  350.          * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
  351.          * @param conventions IERS Conventions
  352.          * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
  353.          * @param fileName the name of the file being parsed
  354.          * @param dataContext used to retrieve frames, time scales, etc.
  355.          */
  356.         private ParseInfo(final AbsoluteDate missionReferenceDate,
  357.                           final IERSConventions conventions,
  358.                           final boolean simpleEOP,
  359.                           final String fileName,
  360.                           final DataContext dataContext) {
  361.             this.missionReferenceDate = missionReferenceDate;
  362.             this.conventions          = conventions;
  363.             this.simpleEOP            = simpleEOP;
  364.             this.fileName             = fileName;
  365.             this.dataContext = dataContext;
  366.             this.lineNumber = 0;
  367.             this.line = "";
  368.             this.tdmFile = new TDMFile();
  369.             this.commentTmp = new ArrayList<String>();
  370.             this.currentObservationsBlock = null;
  371.             this.parsingMetaData = false;
  372.             this.parsingData     = false;
  373.         }

  374.         /** Parse a meta-data entry.<p>
  375.          * key = value (KEYVALUE file format)<p>
  376.          * <&lt;key>value&lt;/key> (XML file format)
  377.          */
  378.         private void parseMetaDataEntry() {

  379.             final TDMFile.TDMMetaData metaData = this.currentObservationsBlock.getMetaData();

  380.             try {
  381.                 switch (keyValue.getKeyword()) {
  382.                     case TIME_SYSTEM:
  383.                         // Read the time system and ensure that it is supported by Orekit
  384.                         if (!CcsdsTimeScale.contains(keyValue.getValue())) {
  385.                             throw new OrekitException(OrekitMessages.CCSDS_TIME_SYSTEM_NOT_IMPLEMENTED,
  386.                                                       keyValue.getValue());
  387.                         }
  388.                         final CcsdsTimeScale timeSystem =
  389.                                         CcsdsTimeScale.valueOf(keyValue.getValue());
  390.                         metaData.setTimeSystem(timeSystem);

  391.                         // Convert start/stop time to AbsoluteDate if they have been read already
  392.                         if (metaData.getStartTimeString() != null) {
  393.                             metaData.setStartTime(parseDate(metaData.getStartTimeString(), timeSystem));
  394.                         }
  395.                         if (metaData.getStopTimeString() != null) {
  396.                             metaData.setStopTime(parseDate(metaData.getStopTimeString(), timeSystem));
  397.                         }
  398.                         break;

  399.                     case START_TIME:
  400.                         // Set the start time as a String first
  401.                         metaData.setStartTimeString(keyValue.getValue());

  402.                         // If time system has already been defined, convert the start time to an AbsoluteDate
  403.                         if (metaData.getTimeSystem() != null) {
  404.                             metaData.setStartTime(parseDate(keyValue.getValue(), metaData.getTimeSystem()));
  405.                         }
  406.                         break;

  407.                     case STOP_TIME:
  408.                         // Set the stop time as a String first
  409.                         metaData.setStopTimeString(keyValue.getValue());

  410.                         // If time system has already been defined, convert the start time to an AbsoluteDate
  411.                         if (metaData.getTimeSystem() != null) {
  412.                             metaData.setStopTime(parseDate(keyValue.getValue(), metaData.getTimeSystem()));
  413.                         }
  414.                         break;

  415.                     case PARTICIPANT_1: case PARTICIPANT_2: case PARTICIPANT_3:
  416.                     case PARTICIPANT_4: case PARTICIPANT_5:
  417.                         // Get the participant number
  418.                         String key = keyValue.getKey();
  419.                         int participantNumber = Integer.parseInt(key.substring(key.length() - 1));

  420.                         // Add the tuple to the map
  421.                         metaData.addParticipant(participantNumber, keyValue.getValue());
  422.                         break;

  423.                     case MODE:
  424.                         metaData.setMode(keyValue.getValue());
  425.                         break;

  426.                     case PATH:
  427.                         metaData.setPath(keyValue.getValue());
  428.                         break;

  429.                     case PATH_1:
  430.                         metaData.setPath1(keyValue.getValue());
  431.                         break;

  432.                     case PATH_2:
  433.                         metaData.setPath2(keyValue.getValue());
  434.                         break;

  435.                     case TRANSMIT_BAND:
  436.                         metaData.setTransmitBand(keyValue.getValue());
  437.                         break;

  438.                     case RECEIVE_BAND:
  439.                         metaData.setReceiveBand(keyValue.getValue());
  440.                         break;

  441.                     case TURNAROUND_NUMERATOR:
  442.                         metaData.setTurnaroundNumerator(keyValue.getIntegerValue());
  443.                         break;

  444.                     case TURNAROUND_DENOMINATOR:
  445.                         metaData.setTurnaroundDenominator(keyValue.getIntegerValue());
  446.                         break;

  447.                     case TIMETAG_REF:
  448.                         metaData.setTimetagRef(keyValue.getValue());
  449.                         break;

  450.                     case INTEGRATION_INTERVAL:
  451.                         metaData.setIntegrationInterval(keyValue.getDoubleValue());
  452.                         break;

  453.                     case INTEGRATION_REF:
  454.                         metaData.setIntegrationRef(keyValue.getValue());
  455.                         break;

  456.                     case FREQ_OFFSET:
  457.                         metaData.setFreqOffset(keyValue.getDoubleValue());
  458.                         break;

  459.                     case RANGE_MODE:
  460.                         metaData.setRangeMode(keyValue.getValue());
  461.                         break;

  462.                     case RANGE_MODULUS:
  463.                         metaData.setRangeModulus(keyValue.getDoubleValue());
  464.                         break;

  465.                     case RANGE_UNITS:
  466.                         metaData.setRangeUnits(keyValue.getValue());
  467.                         break;

  468.                     case ANGLE_TYPE:
  469.                         metaData.setAngleType(keyValue.getValue());
  470.                         break;

  471.                     case REFERENCE_FRAME:
  472.                         metaData.setReferenceFrameString(keyValue.getValue());
  473.                         metaData.setReferenceFrame(parseCCSDSFrame(keyValue.getValue())
  474.                                 .getFrame(this.conventions, this.simpleEOP, dataContext));
  475.                         break;

  476.                     case TRANSMIT_DELAY_1: case TRANSMIT_DELAY_2: case TRANSMIT_DELAY_3:
  477.                     case TRANSMIT_DELAY_4: case TRANSMIT_DELAY_5:
  478.                         // Get the participant number
  479.                         key = keyValue.getKey();
  480.                         participantNumber = Integer.parseInt(key.substring(key.length() - 1));

  481.                         // Add the tuple to the map
  482.                         metaData.addTransmitDelay(participantNumber, keyValue.getDoubleValue());
  483.                         break;

  484.                     case RECEIVE_DELAY_1: case RECEIVE_DELAY_2: case RECEIVE_DELAY_3:
  485.                     case RECEIVE_DELAY_4: case RECEIVE_DELAY_5:
  486.                         // Get the participant number
  487.                         key = keyValue.getKey();
  488.                         participantNumber = Integer.parseInt(key.substring(key.length() - 1));

  489.                         // Add the tuple to the map
  490.                         metaData.addReceiveDelay(participantNumber, keyValue.getDoubleValue());
  491.                         break;

  492.                     case DATA_QUALITY:
  493.                         metaData.setDataQuality(keyValue.getValue());
  494.                         break;

  495.                     case CORRECTION_ANGLE_1:
  496.                         metaData.setCorrectionAngle1(keyValue.getDoubleValue());
  497.                         break;

  498.                     case CORRECTION_ANGLE_2:
  499.                         metaData.setCorrectionAngle2(keyValue.getDoubleValue());
  500.                         break;

  501.                     case CORRECTION_DOPPLER:
  502.                         metaData.setCorrectionDoppler(keyValue.getDoubleValue());
  503.                         break;

  504.                     case CORRECTION_RANGE:
  505.                         metaData.setCorrectionRange(keyValue.getDoubleValue());
  506.                         break;

  507.                     case CORRECTION_RECEIVE:
  508.                         metaData.setCorrectionReceive(keyValue.getDoubleValue());
  509.                         break;

  510.                     case CORRECTION_TRANSMIT:
  511.                         metaData.setCorrectionTransmit(keyValue.getDoubleValue());
  512.                         break;

  513.                     case CORRECTIONS_APPLIED:
  514.                         metaData.setCorrectionsApplied(keyValue.getValue());
  515.                         break;

  516.                     default:
  517.                         throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD, lineNumber, fileName, line);
  518.                 }
  519.             } catch (NumberFormatException nfe) {
  520.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  521.                                           lineNumber, fileName, line);
  522.             }
  523.         }

  524.         /** Parse a CCSDS frame.
  525.          * @param frameName name of the frame, as the value of a CCSDS key=value line
  526.          * @return CCSDS frame corresponding to the name
  527.          */
  528.         private CCSDSFrame parseCCSDSFrame(final String frameName) {
  529.             return CCSDSFrame.valueOf(frameName.replaceAll("-", ""));
  530.         }

  531.         /** Parse a date.
  532.          * @param date date to parse, as the value of a CCSDS key=value line
  533.          * @param timeSystem time system to use
  534.          * @return parsed date
  535.          */
  536.         private AbsoluteDate parseDate(final String date, final CcsdsTimeScale timeSystem) {
  537.             return timeSystem.parseDate(date, conventions, missionReferenceDate,
  538.                     dataContext.getTimeScales());
  539.         }
  540.     }

  541.     /** Handler for parsing KEYVALUE file formats. */
  542.     private static class KeyValueHandler {

  543.         /** ParseInfo object. */
  544.         private ParseInfo parseInfo;

  545.         /** Simple constructor.
  546.          * @param parseInfo ParseInfo object
  547.          */
  548.         KeyValueHandler(final ParseInfo parseInfo) {
  549.             this.parseInfo       = parseInfo;
  550.         }

  551.         /**
  552.          * Parse an observation data line and add its content to the Observations Block
  553.          * block.
  554.          *
  555.          */
  556.         private void parseObservationsDataLine() {

  557.             // Parse an observation line
  558.             // An observation line should consist in the string "keyword = epoch value"
  559.             // parseInfo.keyValue.getValue() should return the string "epoch value"
  560.             final String[] fields = parseInfo.keyValue.getValue().split("\\s+");

  561.             // Check that there are 2 fields in the value of the key
  562.             if (fields.length != 2) {
  563.                 throw new OrekitException(OrekitMessages.CCSDS_TDM_INCONSISTENT_DATA_LINE,
  564.                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  565.             }

  566.             // Convert the date to an AbsoluteDate object (OrekitException if it fails)
  567.             final AbsoluteDate epoch = parseInfo.parseDate(fields[0], parseInfo.currentObservationsBlock.getMetaData().getTimeSystem());
  568.             final double measurement;
  569.             try {
  570.                 // Convert the value to double (NumberFormatException if it fails)
  571.                 measurement = Double.parseDouble(fields[1]);
  572.             } catch (NumberFormatException nfe) {
  573.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  574.                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  575.             }

  576.             // Adds the observation to current observation block
  577.             parseInfo.currentObservationsBlock.addObservation(parseInfo.keyValue.getKeyword().name(),
  578.                                                        epoch,
  579.                                                        measurement);
  580.         }

  581.         /** Parse a CCSDS Tracking Data Message with KEYVALUE format.
  582.          * @param stream stream containing message
  583.          * @param fileName name of the file containing the message (for error messages)
  584.          * @return parsed file content in a TDMFile object
  585.          */
  586.         public TDMFile parse(final InputStream stream, final String fileName) {
  587.             try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
  588.                 try {
  589.                     // Initialize internal TDMFile
  590.                     final TDMFile tdmFile = parseInfo.tdmFile;

  591.                     // Read the file
  592.                     for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  593.                         ++parseInfo.lineNumber;
  594.                         if (line.trim().length() == 0) {
  595.                             continue;
  596.                         }
  597.                         parseInfo.line = line;
  598.                         parseInfo.keyValue = new KeyValue(parseInfo.line, parseInfo.lineNumber, parseInfo.fileName);
  599.                         if (parseInfo.keyValue.getKeyword() == null) {
  600.                             throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD, parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  601.                         }
  602.                         switch (parseInfo.keyValue.getKeyword()) {

  603.                             // Header entries
  604.                             case CCSDS_TDM_VERS:
  605.                                 // Set CCSDS TDM version
  606.                                 tdmFile.setFormatVersion(parseInfo.keyValue.getDoubleValue());
  607.                                 break;

  608.                             case CREATION_DATE:
  609.                                 // Save current comment in header
  610.                                 tdmFile.setHeaderComment(parseInfo.commentTmp);
  611.                                 parseInfo.commentTmp.clear();
  612.                                 // Set creation date
  613.                                 tdmFile.setCreationDate(new AbsoluteDate(
  614.                                         parseInfo.keyValue.getValue(),
  615.                                         parseInfo.dataContext.getTimeScales().getUTC()));
  616.                                 break;

  617.                             case ORIGINATOR:
  618.                                 // Set originator
  619.                                 tdmFile.setOriginator(parseInfo.keyValue.getValue());
  620.                                 break;

  621.                                 // Comments
  622.                             case COMMENT:
  623.                                 parseInfo.commentTmp.add(parseInfo.keyValue.getValue());
  624.                                 break;

  625.                                 // Start/Strop keywords
  626.                             case META_START:
  627.                                 // Add an observation block and set the last observation block to the current
  628.                                 tdmFile.addObservationsBlock();
  629.                                 parseInfo.currentObservationsBlock = tdmFile.getObservationsBlocks().get(tdmFile.getObservationsBlocks().size() - 1);
  630.                                 // Indicate the start of meta-data parsing for this block
  631.                                 parseInfo.parsingMetaData = true;
  632.                                 break;

  633.                             case META_STOP:
  634.                                 // Save current comment in current meta-data comment
  635.                                 parseInfo.currentObservationsBlock.getMetaData().setComment(parseInfo.commentTmp);
  636.                                 parseInfo.commentTmp.clear();
  637.                                 // Indicate the end of meta-data parsing for this block
  638.                                 parseInfo.parsingMetaData = false;
  639.                                 break;

  640.                             case DATA_START:
  641.                                 // Indicate the start of data parsing for this block
  642.                                 parseInfo.parsingData = true;
  643.                                 break;

  644.                             case DATA_STOP:
  645.                                 // Save current comment in current Observation Block comment
  646.                                 parseInfo.currentObservationsBlock.setObservationsComment(parseInfo.commentTmp);
  647.                                 parseInfo.commentTmp.clear();
  648.                                 // Indicate the end of data parsing for this block
  649.                                 parseInfo.parsingData = false;
  650.                                 break;

  651.                             default:
  652.                                 // Parse a line that does not display the previous keywords
  653.                                 if ((parseInfo.currentObservationsBlock != null) &&
  654.                                      (parseInfo.parsingData || parseInfo.parsingMetaData)) {
  655.                                     if (parseInfo.parsingMetaData) {
  656.                                         // Parse a meta-data line
  657.                                         parseInfo.parseMetaDataEntry();
  658.                                     } else {
  659.                                         // Parse an observation data line
  660.                                         this.parseObservationsDataLine();
  661.                                     }
  662.                                 } else {
  663.                                     throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
  664.                                                               parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  665.                                 }
  666.                                 break;
  667.                         }
  668.                     }
  669.                     // Check time systems consistency before returning the parsed content
  670.                     tdmFile.checkTimeSystems();
  671.                     return tdmFile;
  672.                 } catch (IOException ioe) {
  673.                     throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
  674.                 }
  675.             } catch (IOException ioe) {
  676.                 throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
  677.             }
  678.         }
  679.     }

  680.     /** Handler for parsing XML file formats. */
  681.     private static class XMLHandler extends DefaultHandler {

  682.         /** ParseInfo object. */
  683.         private ParseInfo parseInfo;

  684.         /** Locator used to get current line number. */
  685.         private Locator locator;

  686.         /** Current keyword being read. */
  687.         private Keyword currentKeyword;

  688.         /** Current observation keyword being read. */
  689.         private Keyword currentObservationKeyword;

  690.         /** Current observation epoch being read. */
  691.         private AbsoluteDate currentObservationEpoch;

  692.         /** Current observation measurement being read. */
  693.         private double currentObservationMeasurement;

  694.         /** Simple constructor.
  695.          * @param parseInfo ParseInfo object
  696.          */
  697.         XMLHandler(final ParseInfo parseInfo) {
  698.             this.parseInfo      = parseInfo;
  699.             this.locator        = null;
  700.             this.currentKeyword = null;
  701.             this.currentObservationKeyword      = null;
  702.             this.currentObservationEpoch        = null;
  703.             this.currentObservationMeasurement  = Double.NaN;
  704.         }

  705.         @Override
  706.         public void setDocumentLocator(final Locator documentLocator) {
  707.             this.locator = documentLocator;
  708.         }

  709.         /**
  710.          * Extract the content of an element.
  711.          *
  712.          * @param ch the characters
  713.          * @param start the index of the first character of the desired content
  714.          * @param length the length of the content
  715.          * @throws SAXException in case of an error.
  716.          *
  717.          * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
  718.          */
  719.         @Override
  720.         public void characters(final char[] ch, final int start, final int length) throws SAXException
  721.         {
  722.             try {
  723.                 // currentKeyword is set to null in function endElement every time an end tag is parsed.
  724.                 // Thus only the characters between a start and an end tags are parsed.
  725.                 if (currentKeyword != null) {
  726.                     // Store the info in a KeyValue object so that we can use the common functions of parseInfo
  727.                     // The SAX locator does not allow the retrieving of the line
  728.                     // So a pseudo-line showing the keyword is reconstructed
  729.                     final String value = new String(ch, start, length);
  730.                     parseInfo.line = "<" + currentKeyword.name() + ">" + value + "<" + "/" + currentKeyword.name() + ">";
  731.                     parseInfo.lineNumber = locator.getLineNumber();
  732.                     parseInfo.keyValue = new KeyValue(currentKeyword, value, parseInfo.line, parseInfo.lineNumber, parseInfo.fileName);

  733.                     // Scan the keyword
  734.                     switch (currentKeyword) {

  735.                         case CREATION_DATE:
  736.                             // Set creation date
  737.                             parseInfo.tdmFile.setCreationDate(new AbsoluteDate(
  738.                                     parseInfo.keyValue.getValue(),
  739.                                     parseInfo.dataContext.getTimeScales().getUTC()));
  740.                             break;

  741.                         case ORIGINATOR:
  742.                             // Set originator
  743.                             parseInfo.tdmFile.setOriginator(parseInfo.keyValue.getValue());
  744.                             break;

  745.                         case COMMENT:
  746.                             // Comments
  747.                             parseInfo.commentTmp.add(parseInfo.keyValue.getValue());
  748.                             break;

  749.                         case tdm: case header: case body: case segment:
  750.                         case metadata: case data:case observation:
  751.                             // Do nothing for this tags
  752.                             break;

  753.                         default:
  754.                             // Parse a line that does not display the previous keywords
  755.                             if ((parseInfo.currentObservationsBlock != null) &&
  756.                                  (parseInfo.parsingData || parseInfo.parsingMetaData)) {
  757.                                 if (parseInfo.parsingMetaData) {
  758.                                     // Call meta-data parsing
  759.                                     parseInfo.parseMetaDataEntry();
  760.                                 } else if (parseInfo.parsingData) {
  761.                                     // Call data parsing
  762.                                     parseObservationDataLine();
  763.                                 }
  764.                             } else {
  765.                                 throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
  766.                                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  767.                             }
  768.                             break;
  769.                     }
  770.                 }
  771.             } catch (OrekitException e) {
  772.                 // Re-throw the exception as a SAXException
  773.                 throw new SAXException(e);
  774.             }
  775.         }

  776.         /**
  777.          * Detect the beginning of an element.
  778.          *
  779.          * @param uri The Namespace URI, or the empty string if the element has no Namespace URI or if Namespace processing is not being performed.
  780.          * @param localName The local name (without prefix), or the empty string if Namespace processing is not being performed.
  781.          * @param qName The qualified name (with prefix), or the empty string if qualified names are not available.
  782.          * @param attributes The attributes attached to the element. If there are no attributes, it shall be an empty Attributes object.
  783.          * @throws SAXException in case of an error
  784.          *
  785.          * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
  786.          */
  787.         @Override
  788.         public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException
  789.         {
  790.             // Check if the start element belongs to the standard keywords
  791.             try
  792.             {
  793.                 try {
  794.                     this.currentKeyword = Keyword.valueOf(qName);
  795.                 } catch (IllegalArgumentException e) {
  796.                     throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
  797.                                               locator.getLineNumber(),
  798.                                               parseInfo.fileName,
  799.                                               "<" + qName + ">");
  800.                 }
  801.                 switch (currentKeyword) {
  802.                     case tdm:
  803.                         // Get the version number
  804.                         parseInfo.tdmFile.setFormatVersion(Double.parseDouble(attributes.getValue("version")));
  805.                         break;

  806.                     case observation:
  807.                         // Re-initialize the stored observation's attributes
  808.                         this.currentObservationKeyword     = null;
  809.                         this.currentObservationEpoch       = null;
  810.                         this.currentObservationMeasurement = Double.NaN;
  811.                         break;

  812.                     case segment:
  813.                         // Add an observation block and set the last observation block to the current
  814.                         final TDMFile tdmFile = parseInfo.tdmFile;
  815.                         tdmFile.addObservationsBlock();
  816.                         parseInfo.currentObservationsBlock = tdmFile.getObservationsBlocks().get(tdmFile.getObservationsBlocks().size() - 1);
  817.                         break;

  818.                     case metadata:
  819.                         // Indicate the start of meta-data parsing for this block
  820.                         parseInfo.parsingMetaData = true;
  821.                         break;

  822.                     case data:
  823.                         // Indicate the start of data parsing for this block
  824.                         parseInfo.parsingData = true;
  825.                         break;

  826.                     default:
  827.                         // Ignore the element.
  828.                         break;
  829.                 }
  830.             }
  831.             catch (IllegalArgumentException | OrekitException e)
  832.             {
  833.                 throw new SAXException(e);
  834.             }
  835.         }

  836.         /**
  837.          * Detect the end of an element and remove the stored keyword.
  838.          *
  839.          * @param uri The Namespace URI, or the empty string if the element has no Namespace URI or if Namespace processing is not being performed.
  840.          * @param localName The local name (without prefix), or the empty string if Namespace processing is not being performed.
  841.          * @param qName The qualified name (with prefix), or the empty string if qualified names are not available.
  842.          * @throws SAXException in case of an error
  843.          *
  844.          * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
  845.          */
  846.         @Override
  847.         public void endElement(final String uri, final String localName, final String qName) throws SAXException
  848.         {
  849.             // check if the start element belongs to the standard keywords
  850.             try
  851.             {
  852.                 // Set the stored keyword to null
  853.                 currentKeyword = null;
  854.                 // Ending keyword
  855.                 final Keyword endKeyword;
  856.                 try {
  857.                     endKeyword = Keyword.valueOf(qName);
  858.                 } catch (IllegalArgumentException e) {
  859.                     throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
  860.                                               locator.getLineNumber(),
  861.                                               parseInfo.fileName,
  862.                                               "</" + qName + ">");
  863.                 }
  864.                 switch (endKeyword) {

  865.                     case header:
  866.                         // Save header comment
  867.                         parseInfo.tdmFile.setHeaderComment(parseInfo.commentTmp);
  868.                         parseInfo.commentTmp.clear();
  869.                         break;

  870.                     case observation:
  871.                         // Check that stored observation's attributes were all found
  872.                         if (currentObservationKeyword == null         ||
  873.                             currentObservationEpoch == null           ||
  874.                             Double.isNaN(currentObservationMeasurement)) {
  875.                             throw new OrekitException(OrekitMessages.CCSDS_TDM_XML_INCONSISTENT_DATA_BLOCK,
  876.                                                       locator.getLineNumber(),
  877.                                                       parseInfo.fileName);
  878.                         } else {
  879.                             // Add current observation
  880.                             parseInfo.currentObservationsBlock.addObservation(currentObservationKeyword.name(),
  881.                                                                               currentObservationEpoch,
  882.                                                                               currentObservationMeasurement);
  883.                         }
  884.                         break;

  885.                     case segment:
  886.                         // Do nothing
  887.                         break;

  888.                     case metadata:
  889.                         // Save current comment in current meta-data comment
  890.                         parseInfo.currentObservationsBlock.getMetaData().setComment(parseInfo.commentTmp);
  891.                         parseInfo.commentTmp.clear();
  892.                         // Indicate the end of meta-data parsing for this block
  893.                         parseInfo.parsingMetaData = false;
  894.                         break;

  895.                     case data:
  896.                         // Save current comment in current Observation Block comment
  897.                         parseInfo.currentObservationsBlock.setObservationsComment(parseInfo.commentTmp);
  898.                         parseInfo.commentTmp.clear();
  899.                         // Indicate the end of data parsing for this block
  900.                         parseInfo.parsingData = false;
  901.                         break;

  902.                     default:
  903.                         // Ignore the element.
  904.                 }
  905.             }
  906.             catch (IllegalArgumentException | OrekitException e)
  907.             {
  908.                 throw new SAXException(e);
  909.             }
  910.         }

  911.         @Override
  912.         public InputSource resolveEntity(final String publicId, final String systemId) {
  913.             // disable external entities
  914.             return new InputSource();
  915.         }

  916.         /** Parse a line in an observation data block.
  917.          */
  918.         private void parseObservationDataLine() {

  919.             // Parse an observation line
  920.             // An XML observation line should consist in the string "<KEYWORD>value</KEYWORD>
  921.             // Each observation block should display:
  922.             //  - One line with the keyword EPOCH;
  923.             //  - One line with a specific data keyword
  924.             switch(currentKeyword) {
  925.                 case EPOCH:
  926.                     // Convert the date to an AbsoluteDate object (OrekitException if it fails)
  927.                     currentObservationEpoch = parseInfo.parseDate(parseInfo.keyValue.getValue(),
  928.                                                        parseInfo.currentObservationsBlock.getMetaData().getTimeSystem());
  929.                     break;
  930.                 default:
  931.                     try {
  932.                         // Update current observation keyword
  933.                         currentObservationKeyword = currentKeyword;
  934.                         // Convert the value to double (NumberFormatException if it fails)
  935.                         currentObservationMeasurement = Double.parseDouble(parseInfo.keyValue.getValue());
  936.                     } catch (NumberFormatException nfe) {
  937.                         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  938.                                                   parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  939.                     }
  940.                     break;
  941.             }
  942.         }
  943.     }
  944. }