TDMParser.java

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

  18. import 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.nio.charset.StandardCharsets;
  24. import java.util.ArrayList;
  25. import java.util.List;
  26. import java.util.Locale;
  27. import java.util.regex.Pattern;

  28. import javax.xml.parsers.ParserConfigurationException;
  29. import javax.xml.parsers.SAXParser;
  30. import javax.xml.parsers.SAXParserFactory;

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


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

  70.     /** Pattern for dash. */
  71.     private static final Pattern DASH = Pattern.compile("-");

  72.     /** Pattern for delimiting regular expressions. */
  73.     private static final Pattern SEPARATOR = Pattern.compile("\\s+");

  74.     /** Enumerate for the format. */
  75.     public enum TDMFileFormat {

  76.         /** Keyvalue (text file with Key = Value lines). */
  77.         KEYVALUE,

  78.         /** XML format. */
  79.         XML,

  80.         /** UKNOWN file format, default format, throw an Orekit Exception if kept this way. */
  81.         UNKNOWN;
  82.     }

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

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

  87.     /** IERS Conventions. */
  88.     private final  IERSConventions conventions;

  89.     /** Indicator for simple or accurate EOP interpolation. */
  90.     private final  boolean simpleEOP;

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

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

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

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

  157.     /** Get file format.
  158.      * @return the file format
  159.      * @see #withFileFormat(TDMFileFormat)
  160.      */
  161.     public TDMFileFormat getFileFormat() {
  162.         return fileFormat;
  163.     }

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

  173.     /** Get initial date.
  174.      * @return mission reference date to use while parsing
  175.      * @see #withMissionReferenceDate(AbsoluteDate)
  176.      */
  177.     public AbsoluteDate getMissionReferenceDate() {
  178.         return missionReferenceDate;
  179.     }

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

  189.     /** Get IERS conventions.
  190.      * @return IERS conventions to use while parsing
  191.      * @see #withConventions(IERSConventions)
  192.      */
  193.     public IERSConventions getConventions() {
  194.         return conventions;
  195.     }

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

  205.     /** Get EOP interpolation method.
  206.      * @return true if tidal effects are ignored when interpolating EOP
  207.      * @see #withSimpleEOP(boolean)
  208.      */
  209.     public boolean isSimpleEOP() {
  210.         return simpleEOP;
  211.     }

  212.     /**
  213.      * Get the data context.
  214.      *
  215.      * @return the data context used for retrieving frames, time scales, etc.
  216.      */
  217.     public DataContext getDataContext() {
  218.         return dataContext;
  219.     }

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

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

  241.     /** Parse a CCSDS Tracking Data Message.
  242.      * @param stream stream containing message
  243.      * @return parsed file content in a TDMFile object
  244.      */
  245.     public TDMFile parse(final InputStream stream) {
  246.         return parse(stream, "<unknown>");
  247.     }

  248.     /** Parse a CCSDS Tracking Data Message.
  249.      * @param stream stream containing message
  250.      * @param fileName name of the file containing the message (for error messages)
  251.      * @return parsed file content in a TDMFile object
  252.      */
  253.     public TDMFile parse(final InputStream stream, final String fileName) {

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

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

  281.         final KeyValueHandler handler = new KeyValueHandler(new ParseInfo(this.getMissionReferenceDate(),
  282.                                                                     this.getConventions(),
  283.                                                                     this.isSimpleEOP(),
  284.                                                                     fileName,
  285.                                                                     getDataContext()));
  286.         return handler.parse(stream, fileName);
  287.     }



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

  301.             // Create the XML SAX parser factory
  302.             final SAXParserFactory factory = SAXParserFactory.newInstance();

  303.             // Build the parser and read the xml file
  304.             final SAXParser parser = factory.newSAXParser();
  305.             parser.parse(stream, handler);

  306.             // Get the content of the file
  307.             final TDMFile tdmFile = handler.parseInfo.tdmFile;

  308.             // Check time systems consistency
  309.             tdmFile.checkTimeSystems();

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

  324.     /** Private class used to stock TDM parsing info.
  325.      * @author sports
  326.      */
  327.     private static class ParseInfo {

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

  330.         /** IERS Conventions. */
  331.         private final  IERSConventions conventions;

  332.         /** Indicator for simple or accurate EOP interpolation. */
  333.         private final  boolean simpleEOP;

  334.         /** Data context. */
  335.         private final DataContext dataContext;

  336.         /** Name of the file. */
  337.         private String fileName;

  338.         /** Current Observation Block being parsed. */
  339.         private TDMFile.ObservationsBlock currentObservationsBlock;

  340.         /** Current line number. */
  341.         private int lineNumber;

  342.         /** Current parsed line. */
  343.         private String line;

  344.         /** TDMFile object being filled. */
  345.         private TDMFile tdmFile;

  346.         /** Key value of the current line being read. */
  347.         private KeyValue keyValue;

  348.         /** Temporary stored comments. */
  349.         private List<String> commentTmp;

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

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

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

  379.         /** Parse a meta-data entry.<p>
  380.          * key = value (KEYVALUE file format)<p>
  381.          * <&lt;key>value&lt;/key> (XML file format)
  382.          */
  383.         private void parseMetaDataEntry() {

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

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

  396.                         // Convert start/stop time to AbsoluteDate if they have been read already
  397.                         if (metaData.getStartTimeString() != null) {
  398.                             metaData.setStartTime(parseDate(metaData.getStartTimeString(), timeSystem));
  399.                         }
  400.                         if (metaData.getStopTimeString() != null) {
  401.                             metaData.setStopTime(parseDate(metaData.getStopTimeString(), timeSystem));
  402.                         }
  403.                         break;

  404.                     case START_TIME:
  405.                         // Set the start time as a String first
  406.                         metaData.setStartTimeString(keyValue.getValue());

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

  412.                     case STOP_TIME:
  413.                         // Set the stop time as a String first
  414.                         metaData.setStopTimeString(keyValue.getValue());

  415.                         // If time system has already been defined, convert the start time to an AbsoluteDate
  416.                         if (metaData.getTimeSystem() != null) {
  417.                             metaData.setStopTime(parseDate(keyValue.getValue(), metaData.getTimeSystem()));
  418.                         }
  419.                         break;

  420.                     case PARTICIPANT_1: case PARTICIPANT_2: case PARTICIPANT_3:
  421.                     case PARTICIPANT_4: case PARTICIPANT_5:
  422.                         // Get the participant number
  423.                         String key = keyValue.getKey();
  424.                         int participantNumber = Integer.parseInt(key.substring(key.length() - 1));

  425.                         // Add the tuple to the map
  426.                         metaData.addParticipant(participantNumber, keyValue.getValue());
  427.                         break;

  428.                     case MODE:
  429.                         metaData.setMode(keyValue.getValue());
  430.                         break;

  431.                     case PATH:
  432.                         metaData.setPath(keyValue.getValue());
  433.                         break;

  434.                     case PATH_1:
  435.                         metaData.setPath1(keyValue.getValue());
  436.                         break;

  437.                     case PATH_2:
  438.                         metaData.setPath2(keyValue.getValue());
  439.                         break;

  440.                     case TRANSMIT_BAND:
  441.                         metaData.setTransmitBand(keyValue.getValue());
  442.                         break;

  443.                     case RECEIVE_BAND:
  444.                         metaData.setReceiveBand(keyValue.getValue());
  445.                         break;

  446.                     case TURNAROUND_NUMERATOR:
  447.                         metaData.setTurnaroundNumerator(keyValue.getIntegerValue());
  448.                         break;

  449.                     case TURNAROUND_DENOMINATOR:
  450.                         metaData.setTurnaroundDenominator(keyValue.getIntegerValue());
  451.                         break;

  452.                     case TIMETAG_REF:
  453.                         metaData.setTimetagRef(keyValue.getValue());
  454.                         break;

  455.                     case INTEGRATION_INTERVAL:
  456.                         metaData.setIntegrationInterval(keyValue.getDoubleValue());
  457.                         break;

  458.                     case INTEGRATION_REF:
  459.                         metaData.setIntegrationRef(keyValue.getValue());
  460.                         break;

  461.                     case FREQ_OFFSET:
  462.                         metaData.setFreqOffset(keyValue.getDoubleValue());
  463.                         break;

  464.                     case RANGE_MODE:
  465.                         metaData.setRangeMode(keyValue.getValue());
  466.                         break;

  467.                     case RANGE_MODULUS:
  468.                         metaData.setRangeModulus(keyValue.getDoubleValue());
  469.                         break;

  470.                     case RANGE_UNITS:
  471.                         metaData.setRangeUnits(keyValue.getValue());
  472.                         break;

  473.                     case ANGLE_TYPE:
  474.                         metaData.setAngleType(keyValue.getValue());
  475.                         break;

  476.                     case REFERENCE_FRAME:
  477.                         metaData.setReferenceFrameString(keyValue.getValue());
  478.                         metaData.setReferenceFrame(parseCCSDSFrame(keyValue.getValue())
  479.                                 .getFrame(this.conventions, this.simpleEOP, dataContext));
  480.                         break;

  481.                     case TRANSMIT_DELAY_1: case TRANSMIT_DELAY_2: case TRANSMIT_DELAY_3:
  482.                     case TRANSMIT_DELAY_4: case TRANSMIT_DELAY_5:
  483.                         // Get the participant number
  484.                         key = keyValue.getKey();
  485.                         participantNumber = Integer.parseInt(key.substring(key.length() - 1));

  486.                         // Add the tuple to the map
  487.                         metaData.addTransmitDelay(participantNumber, keyValue.getDoubleValue());
  488.                         break;

  489.                     case RECEIVE_DELAY_1: case RECEIVE_DELAY_2: case RECEIVE_DELAY_3:
  490.                     case RECEIVE_DELAY_4: case RECEIVE_DELAY_5:
  491.                         // Get the participant number
  492.                         key = keyValue.getKey();
  493.                         participantNumber = Integer.parseInt(key.substring(key.length() - 1));

  494.                         // Add the tuple to the map
  495.                         metaData.addReceiveDelay(participantNumber, keyValue.getDoubleValue());
  496.                         break;

  497.                     case DATA_QUALITY:
  498.                         metaData.setDataQuality(keyValue.getValue());
  499.                         break;

  500.                     case CORRECTION_ANGLE_1:
  501.                         metaData.setCorrectionAngle1(keyValue.getDoubleValue());
  502.                         break;

  503.                     case CORRECTION_ANGLE_2:
  504.                         metaData.setCorrectionAngle2(keyValue.getDoubleValue());
  505.                         break;

  506.                     case CORRECTION_DOPPLER:
  507.                         metaData.setCorrectionDoppler(keyValue.getDoubleValue());
  508.                         break;

  509.                     case CORRECTION_RANGE:
  510.                         metaData.setCorrectionRange(keyValue.getDoubleValue());
  511.                         break;

  512.                     case CORRECTION_RECEIVE:
  513.                         metaData.setCorrectionReceive(keyValue.getDoubleValue());
  514.                         break;

  515.                     case CORRECTION_TRANSMIT:
  516.                         metaData.setCorrectionTransmit(keyValue.getDoubleValue());
  517.                         break;

  518.                     case CORRECTIONS_APPLIED:
  519.                         metaData.setCorrectionsApplied(keyValue.getValue());
  520.                         break;

  521.                     default:
  522.                         throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD, lineNumber, fileName, line);
  523.                 }
  524.             } catch (NumberFormatException nfe) {
  525.                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  526.                                           lineNumber, fileName, line);
  527.             }
  528.         }

  529.         /** Parse a CCSDS frame.
  530.          * @param frameName name of the frame, as the value of a CCSDS key=value line
  531.          * @return CCSDS frame corresponding to the name
  532.          */
  533.         private CCSDSFrame parseCCSDSFrame(final String frameName) {
  534.             return CCSDSFrame.valueOf(DASH.matcher(frameName).replaceAll(""));
  535.         }

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

  546.     /** Handler for parsing KEYVALUE file formats. */
  547.     private static class KeyValueHandler {

  548.         /** ParseInfo object. */
  549.         private ParseInfo parseInfo;

  550.         /** Simple constructor.
  551.          * @param parseInfo ParseInfo object
  552.          */
  553.         KeyValueHandler(final ParseInfo parseInfo) {
  554.             this.parseInfo       = parseInfo;
  555.         }

  556.         /**
  557.          * Parse an observation data line and add its content to the Observations Block
  558.          * block.
  559.          *
  560.          */
  561.         private void parseObservationsDataLine() {

  562.             // Parse an observation line
  563.             // An observation line should consist in the string "keyword = epoch value"
  564.             // parseInfo.keyValue.getValue() should return the string "epoch value"
  565.             final String[] fields = SEPARATOR.split(parseInfo.keyValue.getValue());

  566.             // Check that there are 2 fields in the value of the key
  567.             if (fields.length != 2) {
  568.                 throw new OrekitException(OrekitMessages.CCSDS_TDM_INCONSISTENT_DATA_LINE,
  569.                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
  570.             }

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

  581.             // Adds the observation to current observation block
  582.             parseInfo.currentObservationsBlock.addObservation(parseInfo.keyValue.getKeyword().name(),
  583.                                                        epoch,
  584.                                                        measurement);
  585.         }

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

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

  608.                             // Header entries
  609.                             case CCSDS_TDM_VERS:
  610.                                 // Set CCSDS TDM version
  611.                                 tdmFile.setFormatVersion(parseInfo.keyValue.getDoubleValue());
  612.                                 break;

  613.                             case CREATION_DATE:
  614.                                 // Save current comment in header
  615.                                 tdmFile.setHeaderComment(parseInfo.commentTmp);
  616.                                 parseInfo.commentTmp.clear();
  617.                                 // Set creation date
  618.                                 tdmFile.setCreationDate(new AbsoluteDate(
  619.                                         parseInfo.keyValue.getValue(),
  620.                                         parseInfo.dataContext.getTimeScales().getUTC()));
  621.                                 break;

  622.                             case ORIGINATOR:
  623.                                 // Set originator
  624.                                 tdmFile.setOriginator(parseInfo.keyValue.getValue());
  625.                                 break;

  626.                                 // Comments
  627.                             case COMMENT:
  628.                                 parseInfo.commentTmp.add(parseInfo.keyValue.getValue());
  629.                                 break;

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

  638.                             case META_STOP:
  639.                                 // Save current comment in current meta-data comment
  640.                                 parseInfo.currentObservationsBlock.getMetaData().setComment(parseInfo.commentTmp);
  641.                                 parseInfo.commentTmp.clear();
  642.                                 // Indicate the end of meta-data parsing for this block
  643.                                 parseInfo.parsingMetaData = false;
  644.                                 break;

  645.                             case DATA_START:
  646.                                 // Indicate the start of data parsing for this block
  647.                                 parseInfo.parsingData = true;
  648.                                 break;

  649.                             case DATA_STOP:
  650.                                 // Save current comment in current Observation Block comment
  651.                                 parseInfo.currentObservationsBlock.setObservationsComment(parseInfo.commentTmp);
  652.                                 parseInfo.commentTmp.clear();
  653.                                 // Indicate the end of data parsing for this block
  654.                                 parseInfo.parsingData = false;
  655.                                 break;

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

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

  687.         /** ParseInfo object. */
  688.         private ParseInfo parseInfo;

  689.         /** Locator used to get current line number. */
  690.         private Locator locator;

  691.         /** Current keyword being read. */
  692.         private Keyword currentKeyword;

  693.         /** Current observation keyword being read. */
  694.         private Keyword currentObservationKeyword;

  695.         /** Current observation epoch being read. */
  696.         private AbsoluteDate currentObservationEpoch;

  697.         /** Current observation measurement being read. */
  698.         private double currentObservationMeasurement;

  699.         /** Simple constructor.
  700.          * @param parseInfo ParseInfo object
  701.          */
  702.         XMLHandler(final ParseInfo parseInfo) {
  703.             this.parseInfo      = parseInfo;
  704.             this.locator        = null;
  705.             this.currentKeyword = null;
  706.             this.currentObservationKeyword      = null;
  707.             this.currentObservationEpoch        = null;
  708.             this.currentObservationMeasurement  = Double.NaN;
  709.         }

  710.         @Override
  711.         public void setDocumentLocator(final Locator documentLocator) {
  712.             this.locator = documentLocator;
  713.         }

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

  738.                     // Scan the keyword
  739.                     switch (currentKeyword) {

  740.                         case CREATION_DATE:
  741.                             // Set creation date
  742.                             parseInfo.tdmFile.setCreationDate(new AbsoluteDate(
  743.                                     parseInfo.keyValue.getValue(),
  744.                                     parseInfo.dataContext.getTimeScales().getUTC()));
  745.                             break;

  746.                         case ORIGINATOR:
  747.                             // Set originator
  748.                             parseInfo.tdmFile.setOriginator(parseInfo.keyValue.getValue());
  749.                             break;

  750.                         case COMMENT:
  751.                             // Comments
  752.                             parseInfo.commentTmp.add(parseInfo.keyValue.getValue());
  753.                             break;

  754.                         case tdm: case header: case body: case segment:
  755.                         case metadata: case data:case observation:
  756.                             // Do nothing for this tags
  757.                             break;

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

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

  811.                     case observation:
  812.                         // Re-initialize the stored observation's attributes
  813.                         this.currentObservationKeyword     = null;
  814.                         this.currentObservationEpoch       = null;
  815.                         this.currentObservationMeasurement = Double.NaN;
  816.                         break;

  817.                     case segment:
  818.                         // Add an observation block and set the last observation block to the current
  819.                         final TDMFile tdmFile = parseInfo.tdmFile;
  820.                         tdmFile.addObservationsBlock();
  821.                         parseInfo.currentObservationsBlock = tdmFile.getObservationsBlocks().get(tdmFile.getObservationsBlocks().size() - 1);
  822.                         break;

  823.                     case metadata:
  824.                         // Indicate the start of meta-data parsing for this block
  825.                         parseInfo.parsingMetaData = true;
  826.                         break;

  827.                     case data:
  828.                         // Indicate the start of data parsing for this block
  829.                         parseInfo.parsingData = true;
  830.                         break;

  831.                     default:
  832.                         // Ignore the element.
  833.                         break;
  834.                 }
  835.             }
  836.             catch (IllegalArgumentException | OrekitException e)
  837.             {
  838.                 throw new SAXException(e);
  839.             }
  840.         }

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

  870.                     case header:
  871.                         // Save header comment
  872.                         parseInfo.tdmFile.setHeaderComment(parseInfo.commentTmp);
  873.                         parseInfo.commentTmp.clear();
  874.                         break;

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

  890.                     case segment:
  891.                         // Do nothing
  892.                         break;

  893.                     case metadata:
  894.                         // Save current comment in current meta-data comment
  895.                         parseInfo.currentObservationsBlock.getMetaData().setComment(parseInfo.commentTmp);
  896.                         parseInfo.commentTmp.clear();
  897.                         // Indicate the end of meta-data parsing for this block
  898.                         parseInfo.parsingMetaData = false;
  899.                         break;

  900.                     case data:
  901.                         // Save current comment in current Observation Block comment
  902.                         parseInfo.currentObservationsBlock.setObservationsComment(parseInfo.commentTmp);
  903.                         parseInfo.commentTmp.clear();
  904.                         // Indicate the end of data parsing for this block
  905.                         parseInfo.parsingData = false;
  906.                         break;

  907.                     default:
  908.                         // Ignore the element.
  909.                 }
  910.             }
  911.             catch (IllegalArgumentException | OrekitException e)
  912.             {
  913.                 throw new SAXException(e);
  914.             }
  915.         }

  916.         @Override
  917.         public InputSource resolveEntity(final String publicId, final String systemId) {
  918.             // disable external entities
  919.             return new InputSource();
  920.         }

  921.         /** Parse a line in an observation data block.
  922.          */
  923.         private void parseObservationDataLine() {

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