TDMParser.java

  1. /* Copyright 2002-2019 CS Systèmes d'Information
  2.  * Licensed to CS Systèmes d'Information (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 java.io.BufferedReader;
  19. import java.io.FileInputStream;
  20. import java.io.IOException;
  21. import java.io.InputStream;
  22. import java.io.InputStreamReader;
  23. import java.util.ArrayList;
  24. import java.util.List;

  25. import javax.xml.parsers.ParserConfigurationException;
  26. import javax.xml.parsers.SAXParser;
  27. import javax.xml.parsers.SAXParserFactory;

  28. import org.hipparchus.exception.DummyLocalizable;
  29. import org.orekit.errors.OrekitException;
  30. import org.orekit.errors.OrekitMessages;
  31. import org.orekit.time.AbsoluteDate;
  32. import org.orekit.time.TimeScalesFactory;
  33. import org.orekit.utils.IERSConventions;
  34. import org.xml.sax.Attributes;
  35. import org.xml.sax.InputSource;
  36. import org.xml.sax.Locator;
  37. import org.xml.sax.SAXException;
  38. import org.xml.sax.helpers.DefaultHandler;


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

  66.     /** Enumerate for the format. */
  67.     public enum TDMFileFormat {

  68.         /** Keyvalue (text file with Key = Value lines). */
  69.         KEYVALUE,

  70.         /** XML format. */
  71.         XML,

  72.         /** UKNOWN file format, default format, throw an Orekit Exception if kept this way. */
  73.         UNKNOWN;
  74.     }

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

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

  79.     /** IERS Conventions. */
  80.     private final  IERSConventions conventions;

  81.     /** Indicator for simple or accurate EOP interpolation. */
  82.     private final  boolean simpleEOP;

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

  115.     /** Complete constructor.
  116.      * @param fileFormat The format of the file: KEYVALUE or XML
  117.      * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
  118.      * @param conventions IERS Conventions
  119.      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
  120.      */
  121.     private TDMParser(final TDMFileFormat fileFormat,
  122.                       final AbsoluteDate missionReferenceDate,
  123.                       final IERSConventions conventions,
  124.                       final boolean simpleEOP) {
  125.         this.fileFormat = fileFormat;
  126.         this.missionReferenceDate = missionReferenceDate;
  127.         this.conventions          = conventions;
  128.         this.simpleEOP            = simpleEOP;
  129.     }

  130.     /** Set file format.
  131.      * @param newFileFormat The format of the file: KEYVALUE or XML
  132.      * @return a new instance, with file format set to newFileFormat
  133.      * @see #getFileFormat()
  134.      */
  135.     public TDMParser withFileFormat(final TDMFileFormat newFileFormat) {
  136.         return new TDMParser(newFileFormat, getMissionReferenceDate(), getConventions(), isSimpleEOP());
  137.     }

  138.     /** Get file format.
  139.      * @return the file format
  140.      * @see #withFileFormat(TDMFileFormat)
  141.      */
  142.     public TDMFileFormat getFileFormat() {
  143.         return fileFormat;
  144.     }

  145.     /** Set initial date.
  146.      * @param newMissionReferenceDate mission reference date to use while parsing
  147.      * @return a new instance, with mission reference date replaced
  148.      * @see #getMissionReferenceDate()
  149.      */
  150.     public TDMParser withMissionReferenceDate(final AbsoluteDate newMissionReferenceDate) {
  151.         return new TDMParser(getFileFormat(), newMissionReferenceDate, getConventions(), isSimpleEOP());
  152.     }

  153.     /** Get initial date.
  154.      * @return mission reference date to use while parsing
  155.      * @see #withMissionReferenceDate(AbsoluteDate)
  156.      */
  157.     public AbsoluteDate getMissionReferenceDate() {
  158.         return missionReferenceDate;
  159.     }

  160.     /** Set IERS conventions.
  161.      * @param newConventions IERS conventions to use while parsing
  162.      * @return a new instance, with IERS conventions replaced
  163.      * @see #getConventions()
  164.      */
  165.     public TDMParser withConventions(final IERSConventions newConventions) {
  166.         return new TDMParser(getFileFormat(), getMissionReferenceDate(), newConventions, isSimpleEOP());
  167.     }

  168.     /** Get IERS conventions.
  169.      * @return IERS conventions to use while parsing
  170.      * @see #withConventions(IERSConventions)
  171.      */
  172.     public IERSConventions getConventions() {
  173.         return conventions;
  174.     }

  175.     /** Set EOP interpolation method.
  176.      * @param newSimpleEOP if true, tidal effects are ignored when interpolating EOP
  177.      * @return a new instance, with EOP interpolation method replaced
  178.      * @see #isSimpleEOP()
  179.      */
  180.     public TDMParser withSimpleEOP(final boolean newSimpleEOP) {
  181.         return new TDMParser(getFileFormat(), getMissionReferenceDate(), getConventions(), newSimpleEOP);
  182.     }

  183.     /** Get EOP interpolation method.
  184.      * @return true if tidal effects are ignored when interpolating EOP
  185.      * @see #withSimpleEOP(boolean)
  186.      */
  187.     public boolean isSimpleEOP() {
  188.         return simpleEOP;
  189.     }

  190.     /** Parse a CCSDS Tracking Data Message.
  191.      * @param fileName name of the file containing the message
  192.      * @return parsed file content in a TDMFile object
  193.      */
  194.     public TDMFile parse(final String fileName) {
  195.         try (InputStream stream = new FileInputStream(fileName)) {
  196.             return parse(stream, fileName);
  197.         } catch (IOException ioe) {
  198.             throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, fileName);
  199.         }
  200.     }

  201.     /** Parse a CCSDS Tracking Data Message.
  202.      * @param stream stream containing message
  203.      * @return parsed file content in a TDMFile object
  204.      */
  205.     public TDMFile parse(final InputStream stream) {
  206.         return parse(stream, "<unknown>");
  207.     }

  208.     /** Parse a CCSDS Tracking Data Message.
  209.      * @param stream stream containing message
  210.      * @param fileName name of the file containing the message (for error messages)
  211.      * @return parsed file content in a TDMFile object
  212.      */
  213.     public TDMFile parse(final InputStream stream, final String fileName) {

  214.         // Set the format of the file automatically
  215.         // If it is obvious and was not formerly specified
  216.         // Then, use a different parsing method for each file format
  217.         if (TDMFileFormat.UNKNOWN.equals(fileFormat)) {
  218.             if (fileName.toLowerCase().endsWith(".txt")) {
  219.                 // Keyvalue format case
  220.                 return this.withFileFormat(TDMFileFormat.KEYVALUE).parse(stream, fileName);
  221.             } else if (fileName.toLowerCase().endsWith(".xml")) {
  222.                 // XML format case
  223.                 return this.withFileFormat(TDMFileFormat.XML).parse(stream, fileName);
  224.             } else {
  225.                 throw new OrekitException(OrekitMessages.CCSDS_TDM_UNKNOWN_FORMAT, fileName);
  226.             }
  227.         } else if (this.fileFormat.equals(TDMFileFormat.KEYVALUE)) {
  228.             return parseKeyValue(stream, fileName);
  229.         } else if (this.fileFormat.equals(TDMFileFormat.XML)) {
  230.             return parseXml(stream, fileName);
  231.         } else {
  232.             throw new OrekitException(OrekitMessages.CCSDS_TDM_UNKNOWN_FORMAT, fileName);
  233.         }
  234.     }

  235.     /** Parse a CCSDS Tracking Data Message with KEYVALUE format.
  236.      * @param stream stream containing message
  237.      * @param fileName name of the file containing the message (for error messages)
  238.      * @return parsed file content in a TDMFile object
  239.      */
  240.     public TDMFile parseKeyValue(final InputStream stream, final String fileName) {

  241.         final KeyValueHandler handler = new KeyValueHandler(new ParseInfo(this.getMissionReferenceDate(),
  242.                                                                     this.getConventions(),
  243.                                                                     this.isSimpleEOP(),
  244.                                                                     fileName));
  245.         return handler.parse(stream, fileName);
  246.     }



  247.     /** Parse a CCSDS Tracking Data Message with XML format.
  248.      * @param stream stream containing message
  249.      * @param fileName name of the file containing the message (for error messages)
  250.      * @return parsed file content in a TDMFile object
  251.      */
  252.     public TDMFile parseXml(final InputStream stream, final String fileName) {
  253.         try {
  254.             // Create the handler
  255.             final XMLHandler handler = new XMLHandler(new ParseInfo(this.getMissionReferenceDate(),
  256.                                                                     this.getConventions(),
  257.                                                                     this.isSimpleEOP(),
  258.                                                                     fileName));

  259.             // Create the XML SAX parser factory
  260.             final SAXParserFactory factory = SAXParserFactory.newInstance();

  261.             // Build the parser and read the xml file
  262.             final SAXParser parser = factory.newSAXParser();
  263.             parser.parse(stream, handler);

  264.             // Get the content of the file
  265.             final TDMFile tdmFile = handler.parseInfo.tdmFile;

  266.             // Check time systems consistency
  267.             tdmFile.checkTimeSystems();

  268.             return tdmFile;
  269.         } catch (SAXException se) {
  270.             final OrekitException oe;
  271.             if (se.getException() != null && se.getException() instanceof OrekitException) {
  272.                 oe = (OrekitException) se.getException();
  273.             } else {
  274.                 oe = new OrekitException(se, new DummyLocalizable(se.getMessage()));
  275.             }
  276.             throw oe;
  277.         } catch (ParserConfigurationException | IOException e) {
  278.             // throw caught exception as an OrekitException
  279.             throw new OrekitException(e, new DummyLocalizable(e.getMessage()));
  280.         }
  281.     }

  282.     /** Private class used to stock TDM parsing info.
  283.      * @author sports
  284.      */
  285.     private static class ParseInfo {

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

  288.         /** IERS Conventions. */
  289.         private final  IERSConventions conventions;

  290.         /** Indicator for simple or accurate EOP interpolation. */
  291.         private final  boolean simpleEOP;

  292.         /** Name of the file. */
  293.         private String fileName;

  294.         /** Current Observation Block being parsed. */
  295.         private TDMFile.ObservationsBlock currentObservationsBlock;

  296.         /** Current line number. */
  297.         private int lineNumber;

  298.         /** Current parsed line. */
  299.         private String line;

  300.         /** TDMFile object being filled. */
  301.         private TDMFile tdmFile;

  302.         /** Key value of the current line being read. */
  303.         private KeyValue keyValue;

  304.         /** Temporary stored comments. */
  305.         private List<String> commentTmp;

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

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

  310.         /** Complete constructor.
  311.          * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
  312.          * @param conventions IERS Conventions
  313.          * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
  314.          * @param fileName the name of the file being parsed
  315.          */
  316.         private ParseInfo(final AbsoluteDate missionReferenceDate,
  317.                           final IERSConventions conventions,
  318.                           final boolean simpleEOP,
  319.                           final String fileName) {
  320.             this.missionReferenceDate = missionReferenceDate;
  321.             this.conventions          = conventions;
  322.             this.simpleEOP            = simpleEOP;
  323.             this.fileName             = fileName;
  324.             this.lineNumber = 0;
  325.             this.line = "";
  326.             this.tdmFile = new TDMFile();
  327.             this.commentTmp = new ArrayList<String>();
  328.             this.currentObservationsBlock = null;
  329.             this.parsingMetaData = false;
  330.             this.parsingData     = false;
  331.         }

  332.         /** Parse a meta-data entry.<p>
  333.          * key = value (KEYVALUE file format)<p>
  334.          * <&lt;key>value&lt;/key> (XML file format)
  335.          */
  336.         private void parseMetaDataEntry() {

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

  338.             try {
  339.                 switch (keyValue.getKeyword()) {
  340.                     case TIME_SYSTEM:
  341.                         // Read the time system and ensure that it is supported by Orekit
  342.                         if (!CcsdsTimeScale.contains(keyValue.getValue())) {
  343.                             throw new OrekitException(OrekitMessages.CCSDS_TIME_SYSTEM_NOT_IMPLEMENTED,
  344.                                                       keyValue.getValue());
  345.                         }
  346.                         final CcsdsTimeScale timeSystem =
  347.                                         CcsdsTimeScale.valueOf(keyValue.getValue());
  348.                         metaData.setTimeSystem(timeSystem);

  349.                         // Convert start/stop time to AbsoluteDate if they have been read already
  350.                         if (metaData.getStartTimeString() != null) {
  351.                             metaData.setStartTime(parseDate(metaData.getStartTimeString(), timeSystem));
  352.                         }
  353.                         if (metaData.getStopTimeString() != null) {
  354.                             metaData.setStopTime(parseDate(metaData.getStopTimeString(), timeSystem));
  355.                         }
  356.                         break;

  357.                     case START_TIME:
  358.                         // Set the start time as a String first
  359.                         metaData.setStartTimeString(keyValue.getValue());

  360.                         // If time system has already been defined, convert the start time to an AbsoluteDate
  361.                         if (metaData.getTimeSystem() != null) {
  362.                             metaData.setStartTime(parseDate(keyValue.getValue(), metaData.getTimeSystem()));
  363.                         }
  364.                         break;

  365.                     case STOP_TIME:
  366.                         // Set the stop time as a String first
  367.                         metaData.setStopTimeString(keyValue.getValue());

  368.                         // If time system has already been defined, convert the start time to an AbsoluteDate
  369.                         if (metaData.getTimeSystem() != null) {
  370.                             metaData.setStopTime(parseDate(keyValue.getValue(), metaData.getTimeSystem()));
  371.                         }
  372.                         break;

  373.                     case PARTICIPANT_1: case PARTICIPANT_2: case PARTICIPANT_3:
  374.                     case PARTICIPANT_4: case PARTICIPANT_5:
  375.                         // Get the participant number
  376.                         String key = keyValue.getKey();
  377.                         int participantNumber = Integer.parseInt(key.substring(key.length() - 1));

  378.                         // Add the tuple to the map
  379.                         metaData.addParticipant(participantNumber, keyValue.getValue());
  380.                         break;

  381.                     case MODE:
  382.                         metaData.setMode(keyValue.getValue());
  383.                         break;

  384.                     case PATH:
  385.                         metaData.setPath(keyValue.getValue());
  386.                         break;

  387.                     case PATH_1:
  388.                         metaData.setPath1(keyValue.getValue());
  389.                         break;

  390.                     case PATH_2:
  391.                         metaData.setPath2(keyValue.getValue());
  392.                         break;

  393.                     case TRANSMIT_BAND:
  394.                         metaData.setTransmitBand(keyValue.getValue());
  395.                         break;

  396.                     case RECEIVE_BAND:
  397.                         metaData.setReceiveBand(keyValue.getValue());
  398.                         break;

  399.                     case TURNAROUND_NUMERATOR:
  400.                         metaData.setTurnaroundNumerator(keyValue.getIntegerValue());
  401.                         break;

  402.                     case TURNAROUND_DENOMINATOR:
  403.                         metaData.setTurnaroundDenominator(keyValue.getIntegerValue());
  404.                         break;

  405.                     case TIMETAG_REF:
  406.                         metaData.setTimetagRef(keyValue.getValue());
  407.                         break;

  408.                     case INTEGRATION_INTERVAL:
  409.                         metaData.setIntegrationInterval(keyValue.getDoubleValue());
  410.                         break;

  411.                     case INTEGRATION_REF:
  412.                         metaData.setIntegrationRef(keyValue.getValue());
  413.                         break;

  414.                     case FREQ_OFFSET:
  415.                         metaData.setFreqOffset(keyValue.getDoubleValue());
  416.                         break;

  417.                     case RANGE_MODE:
  418.                         metaData.setRangeMode(keyValue.getValue());
  419.                         break;

  420.                     case RANGE_MODULUS:
  421.                         metaData.setRangeModulus(keyValue.getDoubleValue());
  422.                         break;

  423.                     case RANGE_UNITS:
  424.                         metaData.setRangeUnits(keyValue.getValue());
  425.                         break;

  426.                     case ANGLE_TYPE:
  427.                         metaData.setAngleType(keyValue.getValue());
  428.                         break;

  429.                     case REFERENCE_FRAME:
  430.                         metaData.setReferenceFrameString(keyValue.getValue());
  431.                         metaData.setReferenceFrame(parseCCSDSFrame(keyValue.getValue()).getFrame(this.conventions, this.simpleEOP));
  432.                         break;

  433.                     case TRANSMIT_DELAY_1: case TRANSMIT_DELAY_2: case TRANSMIT_DELAY_3:
  434.                     case TRANSMIT_DELAY_4: case TRANSMIT_DELAY_5:
  435.                         // Get the participant number
  436.                         key = keyValue.getKey();
  437.                         participantNumber = Integer.parseInt(key.substring(key.length() - 1));

  438.                         // Add the tuple to the map
  439.                         metaData.addTransmitDelay(participantNumber, keyValue.getDoubleValue());
  440.                         break;

  441.                     case RECEIVE_DELAY_1: case RECEIVE_DELAY_2: case RECEIVE_DELAY_3:
  442.                     case RECEIVE_DELAY_4: case RECEIVE_DELAY_5:
  443.                         // Get the participant number
  444.                         key = keyValue.getKey();
  445.                         participantNumber = Integer.parseInt(key.substring(key.length() - 1));

  446.                         // Add the tuple to the map
  447.                         metaData.addReceiveDelay(participantNumber, keyValue.getDoubleValue());
  448.                         break;

  449.                     case DATA_QUALITY:
  450.                         metaData.setDataQuality(keyValue.getValue());
  451.                         break;

  452.                     case CORRECTION_ANGLE_1:
  453.                         metaData.setCorrectionAngle1(keyValue.getDoubleValue());
  454.                         break;

  455.                     case CORRECTION_ANGLE_2:
  456.                         metaData.setCorrectionAngle2(keyValue.getDoubleValue());
  457.                         break;

  458.                     case CORRECTION_DOPPLER:
  459.                         metaData.setCorrectionDoppler(keyValue.getDoubleValue());
  460.                         break;

  461.                     case CORRECTION_RANGE:
  462.                         metaData.setCorrectionRange(keyValue.getDoubleValue());
  463.                         break;

  464.                     case CORRECTION_RECEIVE:
  465.                         metaData.setCorrectionReceive(keyValue.getDoubleValue());
  466.                         break;

  467.                     case CORRECTION_TRANSMIT:
  468.                         metaData.setCorrectionTransmit(keyValue.getDoubleValue());
  469.                         break;

  470.                     case CORRECTIONS_APPLIED:
  471.                         metaData.setCorrectionsApplied(keyValue.getValue());
  472.                         break;

  473.                     default:
  474.                         throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD, lineNumber, fileName, line);
  475.                 }
  476.             } catch (NumberFormatException nfe) {
  477.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  478.                                           lineNumber, fileName, line);
  479.             }
  480.         }

  481.         /** Parse a CCSDS frame.
  482.          * @param frameName name of the frame, as the value of a CCSDS key=value line
  483.          * @return CCSDS frame corresponding to the name
  484.          */
  485.         private CCSDSFrame parseCCSDSFrame(final String frameName) {
  486.             return CCSDSFrame.valueOf(frameName.replaceAll("-", ""));
  487.         }

  488.         /** Parse a date.
  489.          * @param date date to parse, as the value of a CCSDS key=value line
  490.          * @param timeSystem time system to use
  491.          * @return parsed date
  492.          */
  493.         private AbsoluteDate parseDate(final String date, final CcsdsTimeScale timeSystem) {
  494.             return timeSystem.parseDate(date, conventions, missionReferenceDate);
  495.         }
  496.     }

  497.     /** Handler for parsing KEYVALUE file formats. */
  498.     private static class KeyValueHandler {

  499.         /** ParseInfo object. */
  500.         private ParseInfo parseInfo;

  501.         /** Simple constructor.
  502.          * @param parseInfo ParseInfo object
  503.          */
  504.         KeyValueHandler(final ParseInfo parseInfo) {
  505.             this.parseInfo       = parseInfo;
  506.         }

  507.         /**
  508.          * Parse an observation data line and add its content to the Observations Block
  509.          * block.
  510.          *
  511.          */
  512.         private void parseObservationsDataLine() {

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

  517.             // Check that there are 2 fields in the value of the key
  518.             if (fields.length != 2) {
  519.                 throw new OrekitException(OrekitMessages.CCSDS_TDM_INCONSISTENT_DATA_LINE,
  520.                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  521.             }

  522.             // Convert the date to an AbsoluteDate object (OrekitException if it fails)
  523.             final AbsoluteDate epoch = parseInfo.parseDate(fields[0], parseInfo.currentObservationsBlock.getMetaData().getTimeSystem());
  524.             final double measurement;
  525.             try {
  526.                 // Convert the value to double (NumberFormatException if it fails)
  527.                 measurement = Double.parseDouble(fields[1]);
  528.             } catch (NumberFormatException nfe) {
  529.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  530.                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  531.             }

  532.             // Adds the observation to current observation block
  533.             parseInfo.currentObservationsBlock.addObservation(parseInfo.keyValue.getKeyword().name(),
  534.                                                        epoch,
  535.                                                        measurement);
  536.         }

  537.         /** Parse a CCSDS Tracking Data Message with KEYVALUE format.
  538.          * @param stream stream containing message
  539.          * @param fileName name of the file containing the message (for error messages)
  540.          * @return parsed file content in a TDMFile object
  541.          */
  542.         public TDMFile parse(final InputStream stream, final String fileName) {
  543.             try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) {
  544.                 try {
  545.                     // Initialize internal TDMFile
  546.                     final TDMFile tdmFile = parseInfo.tdmFile;

  547.                     // Read the file
  548.                     for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  549.                         ++parseInfo.lineNumber;
  550.                         if (line.trim().length() == 0) {
  551.                             continue;
  552.                         }
  553.                         parseInfo.line = line;
  554.                         parseInfo.keyValue = new KeyValue(parseInfo.line, parseInfo.lineNumber, parseInfo.fileName);
  555.                         if (parseInfo.keyValue.getKeyword() == null) {
  556.                             throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD, parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  557.                         }
  558.                         switch (parseInfo.keyValue.getKeyword()) {

  559.                             // Header entries
  560.                             case CCSDS_TDM_VERS:
  561.                                 // Set CCSDS TDM version
  562.                                 tdmFile.setFormatVersion(parseInfo.keyValue.getDoubleValue());
  563.                                 break;

  564.                             case CREATION_DATE:
  565.                                 // Save current comment in header
  566.                                 tdmFile.setHeaderComment(parseInfo.commentTmp);
  567.                                 parseInfo.commentTmp.clear();
  568.                                 // Set creation date
  569.                                 tdmFile.setCreationDate(new AbsoluteDate(parseInfo.keyValue.getValue(), TimeScalesFactory.getUTC()));
  570.                                 break;

  571.                             case ORIGINATOR:
  572.                                 // Set originator
  573.                                 tdmFile.setOriginator(parseInfo.keyValue.getValue());
  574.                                 break;

  575.                                 // Comments
  576.                             case COMMENT:
  577.                                 parseInfo.commentTmp.add(parseInfo.keyValue.getValue());
  578.                                 break;

  579.                                 // Start/Strop keywords
  580.                             case META_START:
  581.                                 // Add an observation block and set the last observation block to the current
  582.                                 tdmFile.addObservationsBlock();
  583.                                 parseInfo.currentObservationsBlock = tdmFile.getObservationsBlocks().get(tdmFile.getObservationsBlocks().size() - 1);
  584.                                 // Indicate the start of meta-data parsing for this block
  585.                                 parseInfo.parsingMetaData = true;
  586.                                 break;

  587.                             case META_STOP:
  588.                                 // Save current comment in current meta-data comment
  589.                                 parseInfo.currentObservationsBlock.getMetaData().setComment(parseInfo.commentTmp);
  590.                                 parseInfo.commentTmp.clear();
  591.                                 // Indicate the end of meta-data parsing for this block
  592.                                 parseInfo.parsingMetaData = false;
  593.                                 break;

  594.                             case DATA_START:
  595.                                 // Indicate the start of data parsing for this block
  596.                                 parseInfo.parsingData = true;
  597.                                 break;

  598.                             case DATA_STOP:
  599.                                 // Save current comment in current Observation Block comment
  600.                                 parseInfo.currentObservationsBlock.setObservationsComment(parseInfo.commentTmp);
  601.                                 parseInfo.commentTmp.clear();
  602.                                 // Indicate the end of data parsing for this block
  603.                                 parseInfo.parsingData = false;
  604.                                 break;

  605.                             default:
  606.                                 // Parse a line that does not display the previous keywords
  607.                                 if ((parseInfo.currentObservationsBlock != null) &&
  608.                                      (parseInfo.parsingData || parseInfo.parsingMetaData)) {
  609.                                     if (parseInfo.parsingMetaData) {
  610.                                         // Parse a meta-data line
  611.                                         parseInfo.parseMetaDataEntry();
  612.                                     } else {
  613.                                         // Parse an observation data line
  614.                                         this.parseObservationsDataLine();
  615.                                     }
  616.                                 } else {
  617.                                     throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
  618.                                                               parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  619.                                 }
  620.                                 break;
  621.                         }
  622.                     }
  623.                     // Check time systems consistency before returning the parsed content
  624.                     tdmFile.checkTimeSystems();
  625.                     return tdmFile;
  626.                 } catch (IOException ioe) {
  627.                     throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
  628.                 }
  629.             } catch (IOException ioe) {
  630.                 throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
  631.             }
  632.         }
  633.     }

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

  636.         /** ParseInfo object. */
  637.         private ParseInfo parseInfo;

  638.         /** Locator used to get current line number. */
  639.         private Locator locator;

  640.         /** Current keyword being read. */
  641.         private Keyword currentKeyword;

  642.         /** Current observation keyword being read. */
  643.         private Keyword currentObservationKeyword;

  644.         /** Current observation epoch being read. */
  645.         private AbsoluteDate currentObservationEpoch;

  646.         /** Current observation measurement being read. */
  647.         private double currentObservationMeasurement;

  648.         /** Simple constructor.
  649.          * @param parseInfo ParseInfo object
  650.          */
  651.         XMLHandler(final ParseInfo parseInfo) {
  652.             this.parseInfo      = parseInfo;
  653.             this.locator        = null;
  654.             this.currentKeyword = null;
  655.             this.currentObservationKeyword      = null;
  656.             this.currentObservationEpoch        = null;
  657.             this.currentObservationMeasurement  = Double.NaN;
  658.         }

  659.         @Override
  660.         public void setDocumentLocator(final Locator documentLocator) {
  661.             this.locator = documentLocator;
  662.         }

  663.         /**
  664.          * Extract the content of an element.
  665.          *
  666.          * @param ch the characters
  667.          * @param start the index of the first character of the desired content
  668.          * @param length the length of the content
  669.          * @throws SAXException in case of an error.
  670.          *
  671.          * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
  672.          */
  673.         @Override
  674.         public void characters(final char[] ch, final int start, final int length) throws SAXException
  675.         {
  676.             try {
  677.                 // currentKeyword is set to null in function endElement every time an end tag is parsed.
  678.                 // Thus only the characters between a start and an end tags are parsed.
  679.                 if (currentKeyword != null) {
  680.                     // Store the info in a KeyValue object so that we can use the common functions of parseInfo
  681.                     // The SAX locator does not allow the retrieving of the line
  682.                     // So a pseudo-line showing the keyword is reconstructed
  683.                     final String value = new String(ch, start, length);
  684.                     parseInfo.line = "<" + currentKeyword.name() + ">" + value + "<" + "/" + currentKeyword.name() + ">";
  685.                     parseInfo.lineNumber = locator.getLineNumber();
  686.                     parseInfo.keyValue = new KeyValue(currentKeyword, value, parseInfo.line, parseInfo.lineNumber, parseInfo.fileName);

  687.                     // Scan the keyword
  688.                     switch (currentKeyword) {

  689.                         case CREATION_DATE:
  690.                             // Set creation date
  691.                             parseInfo.tdmFile.setCreationDate(new AbsoluteDate(parseInfo.keyValue.getValue(), TimeScalesFactory.getUTC()));
  692.                             break;

  693.                         case ORIGINATOR:
  694.                             // Set originator
  695.                             parseInfo.tdmFile.setOriginator(parseInfo.keyValue.getValue());
  696.                             break;

  697.                         case COMMENT:
  698.                             // Comments
  699.                             parseInfo.commentTmp.add(parseInfo.keyValue.getValue());
  700.                             break;

  701.                         case tdm: case header: case body: case segment:
  702.                         case metadata: case data:case observation:
  703.                             // Do nothing for this tags
  704.                             break;

  705.                         default:
  706.                             // Parse a line that does not display the previous keywords
  707.                             if ((parseInfo.currentObservationsBlock != null) &&
  708.                                  (parseInfo.parsingData || parseInfo.parsingMetaData)) {
  709.                                 if (parseInfo.parsingMetaData) {
  710.                                     // Call meta-data parsing
  711.                                     parseInfo.parseMetaDataEntry();
  712.                                 } else if (parseInfo.parsingData) {
  713.                                     // Call data parsing
  714.                                     parseObservationDataLine();
  715.                                 }
  716.                             } else {
  717.                                 throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
  718.                                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  719.                             }
  720.                             break;
  721.                     }
  722.                 }
  723.             } catch (OrekitException e) {
  724.                 // Re-throw the exception as a SAXException
  725.                 throw new SAXException(e);
  726.             }
  727.         }

  728.         /**
  729.          * Detect the beginning of an element.
  730.          *
  731.          * @param uri The Namespace URI, or the empty string if the element has no Namespace URI or if Namespace processing is not being performed.
  732.          * @param localName The local name (without prefix), or the empty string if Namespace processing is not being performed.
  733.          * @param qName The qualified name (with prefix), or the empty string if qualified names are not available.
  734.          * @param attributes The attributes attached to the element. If there are no attributes, it shall be an empty Attributes object.
  735.          * @throws SAXException in case of an error
  736.          *
  737.          * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
  738.          */
  739.         @Override
  740.         public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException
  741.         {
  742.             // Check if the start element belongs to the standard keywords
  743.             try
  744.             {
  745.                 try {
  746.                     this.currentKeyword = Keyword.valueOf(qName);
  747.                 } catch (IllegalArgumentException e) {
  748.                     throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
  749.                                               locator.getLineNumber(),
  750.                                               parseInfo.fileName,
  751.                                               "<" + qName + ">");
  752.                 }
  753.                 switch (currentKeyword) {
  754.                     case tdm:
  755.                         // Get the version number
  756.                         parseInfo.tdmFile.setFormatVersion(Double.parseDouble(attributes.getValue("version")));
  757.                         break;

  758.                     case observation:
  759.                         // Re-initialize the stored observation's attributes
  760.                         this.currentObservationKeyword     = null;
  761.                         this.currentObservationEpoch       = null;
  762.                         this.currentObservationMeasurement = Double.NaN;
  763.                         break;

  764.                     case segment:
  765.                         // Add an observation block and set the last observation block to the current
  766.                         final TDMFile tdmFile = parseInfo.tdmFile;
  767.                         tdmFile.addObservationsBlock();
  768.                         parseInfo.currentObservationsBlock = tdmFile.getObservationsBlocks().get(tdmFile.getObservationsBlocks().size() - 1);
  769.                         break;

  770.                     case metadata:
  771.                         // Indicate the start of meta-data parsing for this block
  772.                         parseInfo.parsingMetaData = true;
  773.                         break;

  774.                     case data:
  775.                         // Indicate the start of data parsing for this block
  776.                         parseInfo.parsingData = true;
  777.                         break;

  778.                     default:
  779.                         // Ignore the element.
  780.                         break;
  781.                 }
  782.             }
  783.             catch (IllegalArgumentException | OrekitException e)
  784.             {
  785.                 throw new SAXException(e);
  786.             }
  787.         }

  788.         /**
  789.          * Detect the end of an element and remove the stored keyword.
  790.          *
  791.          * @param uri The Namespace URI, or the empty string if the element has no Namespace URI or if Namespace processing is not being performed.
  792.          * @param localName The local name (without prefix), or the empty string if Namespace processing is not being performed.
  793.          * @param qName The qualified name (with prefix), or the empty string if qualified names are not available.
  794.          * @throws SAXException in case of an error
  795.          *
  796.          * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
  797.          */
  798.         @Override
  799.         public void endElement(final String uri, final String localName, final String qName) throws SAXException
  800.         {
  801.             // check if the start element belongs to the standard keywords
  802.             try
  803.             {
  804.                 // Set the stored keyword to null
  805.                 currentKeyword = null;
  806.                 // Ending keyword
  807.                 final Keyword endKeyword;
  808.                 try {
  809.                     endKeyword = Keyword.valueOf(qName);
  810.                 } catch (IllegalArgumentException e) {
  811.                     throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
  812.                                               locator.getLineNumber(),
  813.                                               parseInfo.fileName,
  814.                                               "</" + qName + ">");
  815.                 }
  816.                 switch (endKeyword) {

  817.                     case header:
  818.                         // Save header comment
  819.                         parseInfo.tdmFile.setHeaderComment(parseInfo.commentTmp);
  820.                         parseInfo.commentTmp.clear();
  821.                         break;

  822.                     case observation:
  823.                         // Check that stored observation's attributes were all found
  824.                         if (currentObservationKeyword == null         ||
  825.                             currentObservationEpoch == null           ||
  826.                             Double.isNaN(currentObservationMeasurement)) {
  827.                             throw new OrekitException(OrekitMessages.CCSDS_TDM_XML_INCONSISTENT_DATA_BLOCK,
  828.                                                       locator.getLineNumber(),
  829.                                                       parseInfo.fileName);
  830.                         } else {
  831.                             // Add current observation
  832.                             parseInfo.currentObservationsBlock.addObservation(currentObservationKeyword.name(),
  833.                                                                               currentObservationEpoch,
  834.                                                                               currentObservationMeasurement);
  835.                         }
  836.                         break;

  837.                     case segment:
  838.                         // Do nothing
  839.                         break;

  840.                     case metadata:
  841.                         // Save current comment in current meta-data comment
  842.                         parseInfo.currentObservationsBlock.getMetaData().setComment(parseInfo.commentTmp);
  843.                         parseInfo.commentTmp.clear();
  844.                         // Indicate the end of meta-data parsing for this block
  845.                         parseInfo.parsingMetaData = false;
  846.                         break;

  847.                     case data:
  848.                         // Save current comment in current Observation Block comment
  849.                         parseInfo.currentObservationsBlock.setObservationsComment(parseInfo.commentTmp);
  850.                         parseInfo.commentTmp.clear();
  851.                         // Indicate the end of data parsing for this block
  852.                         parseInfo.parsingData = false;
  853.                         break;

  854.                     default:
  855.                         // Ignore the element.
  856.                 }
  857.             }
  858.             catch (IllegalArgumentException | OrekitException e)
  859.             {
  860.                 throw new SAXException(e);
  861.             }
  862.         }

  863.         @Override
  864.         public InputSource resolveEntity(final String publicId, final String systemId) {
  865.             // disable external entities
  866.             return new InputSource();
  867.         }

  868.         /** Parse a line in an observation data block.
  869.          */
  870.         private void parseObservationDataLine() {

  871.             // Parse an observation line
  872.             // An XML observation line should consist in the string "<KEYWORD>value</KEYWORD>
  873.             // Each observation block should display:
  874.             //  - One line with the keyword EPOCH;
  875.             //  - One line with a specific data keyword
  876.             switch(currentKeyword) {
  877.                 case EPOCH:
  878.                     // Convert the date to an AbsoluteDate object (OrekitException if it fails)
  879.                     currentObservationEpoch = parseInfo.parseDate(parseInfo.keyValue.getValue(),
  880.                                                        parseInfo.currentObservationsBlock.getMetaData().getTimeSystem());
  881.                     break;
  882.                 default:
  883.                     try {
  884.                         // Update current observation keyword
  885.                         currentObservationKeyword = currentKeyword;
  886.                         // Convert the value to double (NumberFormatException if it fails)
  887.                         currentObservationMeasurement = Double.parseDouble(parseInfo.keyValue.getValue());
  888.                     } catch (NumberFormatException nfe) {
  889.                         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  890.                                                   parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  891.                     }
  892.                     break;
  893.             }
  894.         }
  895.     }
  896. }