TDMParser.java

  1. /* Copyright 2002-2018 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.      * @exception OrekitException if Tracking Date Message cannot be parsed or if file cannot be found
  194.      */
  195.     public TDMFile parse(final String fileName)
  196.         throws OrekitException {
  197.         try (InputStream stream = new FileInputStream(fileName)) {
  198.             return parse(stream, fileName);
  199.         } catch (IOException ioe) {
  200.             throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, fileName);
  201.         }
  202.     }

  203.     /** Parse a CCSDS Tracking Data Message.
  204.      * @param stream stream containing message
  205.      * @return parsed file content in a TDMFile object
  206.      * @exception OrekitException if Tracking Date Message cannot be parsed
  207.      */
  208.     public TDMFile parse(final InputStream stream)
  209.         throws OrekitException {
  210.         return parse(stream, "<unknown>");
  211.     }

  212.     /** Parse a CCSDS Tracking Data Message.
  213.      * @param stream stream containing message
  214.      * @param fileName name of the file containing the message (for error messages)
  215.      * @return parsed file content in a TDMFile object
  216.      * @exception OrekitException if Tracking Date Message cannot be parsed or format is unknown
  217.      */
  218.     public TDMFile parse(final InputStream stream, final String fileName)
  219.         throws  OrekitException {

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

  241.     /** Parse a CCSDS Tracking Data Message with KEYVALUE format.
  242.      * @param stream stream containing message
  243.      * @param fileName name of the file containing the message (for error messages)
  244.      * @return parsed file content in a TDMFile object
  245.      * @exception OrekitException if Tracking Date Message cannot be parsed
  246.      */
  247.     public TDMFile parseKeyValue(final InputStream stream, final String fileName)
  248.         throws  OrekitException {

  249.         final KeyValueHandler handler = new KeyValueHandler(new ParseInfo(this.getMissionReferenceDate(),
  250.                                                                     this.getConventions(),
  251.                                                                     this.isSimpleEOP(),
  252.                                                                     fileName));
  253.         return handler.parse(stream, fileName);
  254.     }



  255.     /** Parse a CCSDS Tracking Data Message with XML format.
  256.      * @param stream stream containing message
  257.      * @param fileName name of the file containing the message (for error messages)
  258.      * @return parsed file content in a TDMFile object
  259.      * @exception OrekitException if Tracking Date Message cannot be parsed
  260.      */
  261.     public TDMFile parseXml(final InputStream stream, final String fileName)
  262.         throws OrekitException {
  263.         try {
  264.             // Create the handler
  265.             final XMLHandler handler = new XMLHandler(new ParseInfo(this.getMissionReferenceDate(),
  266.                                                                     this.getConventions(),
  267.                                                                     this.isSimpleEOP(),
  268.                                                                     fileName));

  269.             // Create the XML SAX parser factory
  270.             final SAXParserFactory factory = SAXParserFactory.newInstance();

  271.             // Build the parser and read the xml file
  272.             final SAXParser parser = factory.newSAXParser();
  273.             parser.parse(stream, handler);

  274.             // Get the content of the file
  275.             final TDMFile tdmFile = handler.parseInfo.tdmFile;

  276.             // Check time systems consistency
  277.             tdmFile.checkTimeSystems();

  278.             return tdmFile;
  279.         } catch (ParserConfigurationException | SAXException | IOException e) {
  280.             // throw caught exception as an OrekitException
  281.             throw new OrekitException(e, new DummyLocalizable(e.getMessage()));
  282.         }
  283.     }

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

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

  290.         /** IERS Conventions. */
  291.         private final  IERSConventions conventions;

  292.         /** Indicator for simple or accurate EOP interpolation. */
  293.         private final  boolean simpleEOP;

  294.         /** Name of the file. */
  295.         private String fileName;

  296.         /** Current Observation Block being parsed. */
  297.         private TDMFile.ObservationsBlock currentObservationsBlock;

  298.         /** Current line number. */
  299.         private int lineNumber;

  300.         /** Current parsed line. */
  301.         private String line;

  302.         /** TDMFile object being filled. */
  303.         private TDMFile tdmFile;

  304.         /** Key value of the current line being read. */
  305.         private KeyValue keyValue;

  306.         /** Temporary stored comments. */
  307.         private List<String> commentTmp;

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

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

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

  334.         /** Parse a meta-data entry.<p>
  335.          * key = value (KEYVALUE file format)<p>
  336.          * <&lt;key>value&lt;/key> (XML file format)
  337.          * @exception OrekitException if error in /parsing dates or if parsing integer or double fails
  338.          */
  339.         private void parseMetaDataEntry() throws OrekitException {

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

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

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

  360.                     case START_TIME:
  361.                         // Set the start time as a String first
  362.                         metaData.setStartTimeString(keyValue.getValue());

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

  368.                     case STOP_TIME:
  369.                         // Set the stop time as a String first
  370.                         metaData.setStopTimeString(keyValue.getValue());

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

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

  381.                         // Add the tuple to the map
  382.                         metaData.addParticipant(participantNumber, keyValue.getValue());
  383.                         break;

  384.                     case MODE:
  385.                         metaData.setMode(keyValue.getValue());
  386.                         break;

  387.                     case PATH:
  388.                         metaData.setPath(keyValue.getValue());
  389.                         break;

  390.                     case PATH_1:
  391.                         metaData.setPath1(keyValue.getValue());
  392.                         break;

  393.                     case PATH_2:
  394.                         metaData.setPath2(keyValue.getValue());
  395.                         break;

  396.                     case TRANSMIT_BAND:
  397.                         metaData.setTransmitBand(keyValue.getValue());
  398.                         break;

  399.                     case RECEIVE_BAND:
  400.                         metaData.setReceiveBand(keyValue.getValue());
  401.                         break;

  402.                     case TURNAROUND_NUMERATOR:
  403.                         metaData.setTurnaroundNumerator(keyValue.getIntegerValue());
  404.                         break;

  405.                     case TURNAROUND_DENOMINATOR:
  406.                         metaData.setTurnaroundDenominator(keyValue.getIntegerValue());
  407.                         break;

  408.                     case TIMETAG_REF:
  409.                         metaData.setTimetagRef(keyValue.getValue());
  410.                         break;

  411.                     case INTEGRATION_INTERVAL:
  412.                         metaData.setIntegrationInterval(keyValue.getDoubleValue());
  413.                         break;

  414.                     case INTEGRATION_REF:
  415.                         metaData.setIntegrationRef(keyValue.getValue());
  416.                         break;

  417.                     case FREQ_OFFSET:
  418.                         metaData.setFreqOffset(keyValue.getDoubleValue());
  419.                         break;

  420.                     case RANGE_MODE:
  421.                         metaData.setRangeMode(keyValue.getValue());
  422.                         break;

  423.                     case RANGE_MODULUS:
  424.                         metaData.setRangeModulus(keyValue.getDoubleValue());
  425.                         break;

  426.                     case RANGE_UNITS:
  427.                         metaData.setRangeUnits(keyValue.getValue());
  428.                         break;

  429.                     case ANGLE_TYPE:
  430.                         metaData.setAngleType(keyValue.getValue());
  431.                         break;

  432.                     case REFERENCE_FRAME:
  433.                         metaData.setReferenceFrameString(keyValue.getValue());
  434.                         metaData.setReferenceFrame(parseCCSDSFrame(keyValue.getValue()).getFrame(this.conventions, this.simpleEOP));
  435.                         break;

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

  441.                         // Add the tuple to the map
  442.                         metaData.addTransmitDelay(participantNumber, keyValue.getDoubleValue());
  443.                         break;

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

  449.                         // Add the tuple to the map
  450.                         metaData.addReceiveDelay(participantNumber, keyValue.getDoubleValue());
  451.                         break;

  452.                     case DATA_QUALITY:
  453.                         metaData.setDataQuality(keyValue.getValue());
  454.                         break;

  455.                     case CORRECTION_ANGLE_1:
  456.                         metaData.setCorrectionAngle1(keyValue.getDoubleValue());
  457.                         break;

  458.                     case CORRECTION_ANGLE_2:
  459.                         metaData.setCorrectionAngle2(keyValue.getDoubleValue());
  460.                         break;

  461.                     case CORRECTION_DOPPLER:
  462.                         metaData.setCorrectionDoppler(keyValue.getDoubleValue());
  463.                         break;

  464.                     case CORRECTION_RANGE:
  465.                         metaData.setCorrectionRange(keyValue.getDoubleValue());
  466.                         break;

  467.                     case CORRECTION_RECEIVE:
  468.                         metaData.setCorrectionReceive(keyValue.getDoubleValue());
  469.                         break;

  470.                     case CORRECTION_TRANSMIT:
  471.                         metaData.setCorrectionTransmit(keyValue.getDoubleValue());
  472.                         break;

  473.                     case CORRECTIONS_APPLIED:
  474.                         metaData.setCorrectionsApplied(keyValue.getValue());
  475.                         break;

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

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

  491.         /** Parse a date.
  492.          * @param date date to parse, as the value of a CCSDS key=value line
  493.          * @param timeSystem time system to use
  494.          * @return parsed date
  495.          * @exception OrekitException if some time scale cannot be retrieved
  496.          */
  497.         private AbsoluteDate parseDate(final String date, final CcsdsTimeScale timeSystem)
  498.                         throws OrekitException {
  499.             return timeSystem.parseDate(date, conventions, missionReferenceDate);
  500.         }
  501.     }

  502.     /** Handler for parsing KEYVALUE file formats. */
  503.     private static class KeyValueHandler {

  504.         /** ParseInfo object. */
  505.         private ParseInfo parseInfo;

  506.         /** Simple constructor.
  507.          * @param parseInfo ParseInfo object
  508.          */
  509.         KeyValueHandler(final ParseInfo parseInfo) {
  510.             this.parseInfo       = parseInfo;
  511.         }

  512.         /**
  513.          * Parse an observation data line and add its content to the Observations Block
  514.          * block.
  515.          *
  516.          * @exception OrekitException if error in parsing dates or inconsistent data lines or in number conversion from String
  517.          */
  518.         private void parseObservationsDataLine()
  519.                         throws OrekitException {

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

  524.             // Check that there are 2 fields in the value of the key
  525.             if (fields.length != 2) {
  526.                 throw new OrekitException(OrekitMessages.CCSDS_TDM_INCONSISTENT_DATA_LINE,
  527.                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  528.             }

  529.             // Convert the date to an AbsoluteDate object (OrekitException if it fails)
  530.             final AbsoluteDate epoch = parseInfo.parseDate(fields[0], parseInfo.currentObservationsBlock.getMetaData().getTimeSystem());
  531.             final double measurement;
  532.             try {
  533.                 // Convert the value to double (NumberFormatException if it fails)
  534.                 measurement = Double.parseDouble(fields[1]);
  535.             } catch (NumberFormatException nfe) {
  536.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  537.                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  538.             }

  539.             // Adds the observation to current observation block
  540.             parseInfo.currentObservationsBlock.addObservation(parseInfo.keyValue.getKeyword().name(),
  541.                                                        epoch,
  542.                                                        measurement);
  543.         }

  544.         /** Parse a CCSDS Tracking Data Message with KEYVALUE format.
  545.          * @param stream stream containing message
  546.          * @param fileName name of the file containing the message (for error messages)
  547.          * @return parsed file content in a TDMFile object
  548.          * @exception OrekitException if stream cannot be read or Tracking Date Message cannot be parsed
  549.          */
  550.         public TDMFile parse(final InputStream stream, final String fileName)
  551.                         throws  OrekitException {
  552.             try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) {
  553.                 try {
  554.                     // Initialize internal TDMFile
  555.                     final TDMFile tdmFile = parseInfo.tdmFile;

  556.                     // Read the file
  557.                     for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  558.                         ++parseInfo.lineNumber;
  559.                         if (line.trim().length() == 0) {
  560.                             continue;
  561.                         }
  562.                         parseInfo.line = line;
  563.                         parseInfo.keyValue = new KeyValue(parseInfo.line, parseInfo.lineNumber, parseInfo.fileName);
  564.                         if (parseInfo.keyValue.getKeyword() == null) {
  565.                             throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD, parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  566.                         }
  567.                         switch (parseInfo.keyValue.getKeyword()) {

  568.                             // Header entries
  569.                             case CCSDS_TDM_VERS:
  570.                                 // Set CCSDS TDM version
  571.                                 tdmFile.setFormatVersion(parseInfo.keyValue.getDoubleValue());
  572.                                 break;

  573.                             case CREATION_DATE:
  574.                                 // Save current comment in header
  575.                                 tdmFile.setHeaderComment(parseInfo.commentTmp);
  576.                                 parseInfo.commentTmp.clear();
  577.                                 // Set creation date
  578.                                 tdmFile.setCreationDate(new AbsoluteDate(parseInfo.keyValue.getValue(), TimeScalesFactory.getUTC()));
  579.                                 break;

  580.                             case ORIGINATOR:
  581.                                 // Set originator
  582.                                 tdmFile.setOriginator(parseInfo.keyValue.getValue());
  583.                                 break;

  584.                                 // Comments
  585.                             case COMMENT:
  586.                                 parseInfo.commentTmp.add(parseInfo.keyValue.getValue());
  587.                                 break;

  588.                                 // Start/Strop keywords
  589.                             case META_START:
  590.                                 // Add an observation block and set the last observation block to the current
  591.                                 tdmFile.addObservationsBlock();
  592.                                 parseInfo.currentObservationsBlock = tdmFile.getObservationsBlocks().get(tdmFile.getObservationsBlocks().size() - 1);
  593.                                 // Indicate the start of meta-data parsing for this block
  594.                                 parseInfo.parsingMetaData = true;
  595.                                 break;

  596.                             case META_STOP:
  597.                                 // Save current comment in current meta-data comment
  598.                                 parseInfo.currentObservationsBlock.getMetaData().setComment(parseInfo.commentTmp);
  599.                                 parseInfo.commentTmp.clear();
  600.                                 // Indicate the end of meta-data parsing for this block
  601.                                 parseInfo.parsingMetaData = false;
  602.                                 break;

  603.                             case DATA_START:
  604.                                 // Indicate the start of data parsing for this block
  605.                                 parseInfo.parsingData = true;
  606.                                 break;

  607.                             case DATA_STOP:
  608.                                 // Save current comment in current Observation Block comment
  609.                                 parseInfo.currentObservationsBlock.setObservationsComment(parseInfo.commentTmp);
  610.                                 parseInfo.commentTmp.clear();
  611.                                 // Indicate the end of data parsing for this block
  612.                                 parseInfo.parsingData = false;
  613.                                 break;

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

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

  645.         /** ParseInfo object. */
  646.         private ParseInfo parseInfo;

  647.         /** Locator used to get current line number. */
  648.         private Locator locator;

  649.         /** Current keyword being read. */
  650.         private Keyword currentKeyword;

  651.         /** Current observation keyword being read. */
  652.         private Keyword currentObservationKeyword;

  653.         /** Current observation epoch being read. */
  654.         private AbsoluteDate currentObservationEpoch;

  655.         /** Current observation measurement being read. */
  656.         private double currentObservationMeasurement;

  657.         /** Simple constructor.
  658.          * @param parseInfo ParseInfo object
  659.          */
  660.         XMLHandler(final ParseInfo parseInfo) {
  661.             this.parseInfo      = parseInfo;
  662.             this.locator        = null;
  663.             this.currentKeyword = null;
  664.             this.currentObservationKeyword      = null;
  665.             this.currentObservationEpoch        = null;
  666.             this.currentObservationMeasurement  = Double.NaN;
  667.         }

  668.         @Override
  669.         public void setDocumentLocator(final Locator documentLocator) {
  670.             this.locator = documentLocator;
  671.         }

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

  696.                     // Scan the keyword
  697.                     switch (currentKeyword) {

  698.                         case CREATION_DATE:
  699.                             // Set creation date
  700.                             parseInfo.tdmFile.setCreationDate(new AbsoluteDate(parseInfo.keyValue.getValue(), TimeScalesFactory.getUTC()));
  701.                             break;

  702.                         case ORIGINATOR:
  703.                             // Set originator
  704.                             parseInfo.tdmFile.setOriginator(parseInfo.keyValue.getValue());
  705.                             break;

  706.                         case COMMENT:
  707.                             // Comments
  708.                             parseInfo.commentTmp.add(parseInfo.keyValue.getValue());
  709.                             break;

  710.                         case tdm: case header: case body: case segment:
  711.                         case metadata: case data:case observation:
  712.                             // Do nothing for this tags
  713.                             break;

  714.                         default:
  715.                             // Parse a line that does not display the previous keywords
  716.                             if ((parseInfo.currentObservationsBlock != null) &&
  717.                                  (parseInfo.parsingData || parseInfo.parsingMetaData)) {
  718.                                 if (parseInfo.parsingMetaData) {
  719.                                     // Call meta-data parsing
  720.                                     parseInfo.parseMetaDataEntry();
  721.                                 } else if (parseInfo.parsingData) {
  722.                                     // Call data parsing
  723.                                     parseObservationDataLine();
  724.                                 }
  725.                             } else {
  726.                                 throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
  727.                                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  728.                             }
  729.                             break;
  730.                     }
  731.                 }
  732.             } catch (OrekitException e) {
  733.                 // Re-throw the exception as a SAXException
  734.                 throw new SAXException(e);
  735.             }
  736.         }

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

  767.                     case observation:
  768.                         // Re-initialize the stored observation's attributes
  769.                         this.currentObservationKeyword     = null;
  770.                         this.currentObservationEpoch       = null;
  771.                         this.currentObservationMeasurement = Double.NaN;
  772.                         break;

  773.                     case segment:
  774.                         // Add an observation block and set the last observation block to the current
  775.                         final TDMFile tdmFile = parseInfo.tdmFile;
  776.                         tdmFile.addObservationsBlock();
  777.                         parseInfo.currentObservationsBlock = tdmFile.getObservationsBlocks().get(tdmFile.getObservationsBlocks().size() - 1);
  778.                         break;

  779.                     case metadata:
  780.                         // Indicate the start of meta-data parsing for this block
  781.                         parseInfo.parsingMetaData = true;
  782.                         break;

  783.                     case data:
  784.                         // Indicate the start of data parsing for this block
  785.                         parseInfo.parsingData = true;
  786.                         break;

  787.                     default:
  788.                         // Ignore the element.
  789.                         break;
  790.                 }
  791.             }
  792.             catch (IllegalArgumentException | OrekitException e)
  793.             {
  794.                 throw new SAXException(e);
  795.             }
  796.         }

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

  826.                     case header:
  827.                         // Save header comment
  828.                         parseInfo.tdmFile.setHeaderComment(parseInfo.commentTmp);
  829.                         parseInfo.commentTmp.clear();
  830.                         break;

  831.                     case observation:
  832.                         // Check that stored observation's attributes were all found
  833.                         if (currentObservationKeyword == null         ||
  834.                             currentObservationEpoch == null           ||
  835.                             Double.isNaN(currentObservationMeasurement)) {
  836.                             throw new OrekitException(OrekitMessages.CCSDS_TDM_XML_INCONSISTENT_DATA_BLOCK,
  837.                                                       locator.getLineNumber(),
  838.                                                       parseInfo.fileName);
  839.                         } else {
  840.                             // Add current observation
  841.                             parseInfo.currentObservationsBlock.addObservation(currentObservationKeyword.name(),
  842.                                                                               currentObservationEpoch,
  843.                                                                               currentObservationMeasurement);
  844.                         }
  845.                         break;

  846.                     case segment:
  847.                         // Do nothing
  848.                         break;

  849.                     case metadata:
  850.                         // Save current comment in current meta-data comment
  851.                         parseInfo.currentObservationsBlock.getMetaData().setComment(parseInfo.commentTmp);
  852.                         parseInfo.commentTmp.clear();
  853.                         // Indicate the end of meta-data parsing for this block
  854.                         parseInfo.parsingMetaData = false;
  855.                         break;

  856.                     case data:
  857.                         // Save current comment in current Observation Block comment
  858.                         parseInfo.currentObservationsBlock.setObservationsComment(parseInfo.commentTmp);
  859.                         parseInfo.commentTmp.clear();
  860.                         // Indicate the end of data parsing for this block
  861.                         parseInfo.parsingData = false;
  862.                         break;

  863.                     default:
  864.                         // Ignore the element.
  865.                 }
  866.             }
  867.             catch (IllegalArgumentException | OrekitException e)
  868.             {
  869.                 throw new SAXException(e);
  870.             }
  871.         }

  872.         @Override
  873.         public InputSource resolveEntity(final String publicId, final String systemId) {
  874.             // disable external entities
  875.             return new InputSource();
  876.         }

  877.         /** Parse a line in an observation data block.
  878.          * @exception OrekitException if error in parsing dates or in conversion of number from String
  879.          */
  880.         private void parseObservationDataLine()
  881.                         throws OrekitException {

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