1   /* Copyright 2002-2019 CS Systèmes d'Information
2    * Licensed to CS Systèmes d'Information (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.files.ccsds;
18  
19  import java.io.BufferedReader;
20  import java.io.FileInputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.util.ArrayList;
25  import java.util.List;
26  
27  import javax.xml.parsers.ParserConfigurationException;
28  import javax.xml.parsers.SAXParser;
29  import javax.xml.parsers.SAXParserFactory;
30  
31  import org.hipparchus.exception.DummyLocalizable;
32  import org.orekit.errors.OrekitException;
33  import org.orekit.errors.OrekitMessages;
34  import org.orekit.time.AbsoluteDate;
35  import org.orekit.time.TimeScalesFactory;
36  import org.orekit.utils.IERSConventions;
37  import org.xml.sax.Attributes;
38  import org.xml.sax.InputSource;
39  import org.xml.sax.Locator;
40  import org.xml.sax.SAXException;
41  import org.xml.sax.helpers.DefaultHandler;
42  
43  
44  /**
45   * Class for CCSDS Tracking Data Message parsers.
46   *
47   * <p> This base class is immutable, and hence thread safe. When parts must be
48   * changed, such as reference date for Mission Elapsed Time or Mission Relative
49   * Time time systems, or the gravitational coefficient or the IERS conventions,
50   * the various {@code withXxx} methods must be called, which create a new
51   * immutable instance with the new parameters. This is a combination of the <a
52   * href="https://en.wikipedia.org/wiki/Builder_pattern">builder design
53   * pattern</a> and a <a href="http://en.wikipedia.org/wiki/Fluent_interface">fluent
54   * interface</a>.
55   *
56   * <p> This class allow the handling of both "keyvalue" and "xml" TDM file formats.
57   * Format can be inferred if file names ends respectively with ".txt" or ".xml".
58   * Otherwise it must be explicitely set using {@link #withFileFormat(TDMFileFormat)}
59   *
60   * <p>ParseInfo subclass regroups common parsing functions; and specific handlers were added
61   * for both file formats.
62   *
63   * <p>References:<p>
64   *  - <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>
65   *  - <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>
66   *
67   * @author Maxime Journot
68   * @since 9.0
69   */
70  public class TDMParser extends DefaultHandler {
71  
72      /** Enumerate for the format. */
73      public enum TDMFileFormat {
74  
75          /** Keyvalue (text file with Key = Value lines). */
76          KEYVALUE,
77  
78          /** XML format. */
79          XML,
80  
81          /** UKNOWN file format, default format, throw an Orekit Exception if kept this way. */
82          UNKNOWN;
83      }
84  
85      /** Format of the file to parse: KEYVALUE or XML. */
86      private TDMFileFormat fileFormat;
87  
88      /** Reference date for Mission Elapsed Time or Mission Relative Time time systems. */
89      private final AbsoluteDate missionReferenceDate;
90  
91      /** IERS Conventions. */
92      private final  IERSConventions conventions;
93  
94      /** Indicator for simple or accurate EOP interpolation. */
95      private final  boolean simpleEOP;
96  
97      /** Simple constructor.
98       * <p>
99       * This class is immutable, and hence thread safe. When parts
100      * must be changed, such fiel format or reference date for Mission Elapsed Time or
101      * Mission Relative Time time systems, or the IERS conventions,
102      * the various {@code withXxx} methods must be called,
103      * which create a new immutable instance with the new parameters. This
104      * is a combination of the
105      * <a href="https://en.wikipedia.org/wiki/Builder_pattern">builder design
106      * pattern</a> and a
107      * <a href="http://en.wikipedia.org/wiki/Fluent_interface">fluent
108      * interface</a>.
109      * </p>
110      * <p>
111      * The initial date for Mission Elapsed Time and Mission Relative Time time systems is not set here.
112      * If such time systems are used, it must be initialized before parsing by calling {@link
113      * #withMissionReferenceDate(AbsoluteDate)}.
114      * </p>
115      * <p>
116      * The IERS conventions to use is not set here. If it is needed in order to
117      * parse some reference frames or UT1 time scale, it must be initialized before
118      * parsing by calling {@link #withConventions(IERSConventions)}.
119      * </p>
120      * <p>
121      * The TDM file format to use is not set here. It may be automatically inferred while parsing
122      * if the name of the file to parse ends with ".txt" or ".xml".
123      * Otherwise it must be initialized before parsing by calling {@link #withFileFormat(TDMFileFormat)}
124      * </p>
125      */
126     public TDMParser() {
127         this(TDMFileFormat.UNKNOWN, AbsoluteDate.FUTURE_INFINITY, null, true);
128     }
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      */
136     private TDMParser(final TDMFileFormat fileFormat,
137                       final AbsoluteDate missionReferenceDate,
138                       final IERSConventions conventions,
139                       final boolean simpleEOP) {
140         this.fileFormat = fileFormat;
141         this.missionReferenceDate = missionReferenceDate;
142         this.conventions          = conventions;
143         this.simpleEOP            = simpleEOP;
144     }
145 
146     /** Set file format.
147      * @param newFileFormat The format of the file: KEYVALUE or XML
148      * @return a new instance, with file format set to newFileFormat
149      * @see #getFileFormat()
150      */
151     public TDMParser withFileFormat(final TDMFileFormat newFileFormat) {
152         return new TDMParser(newFileFormat, getMissionReferenceDate(), getConventions(), isSimpleEOP());
153     }
154 
155     /** Get file format.
156      * @return the file format
157      * @see #withFileFormat(TDMFileFormat)
158      */
159     public TDMFileFormat getFileFormat() {
160         return fileFormat;
161     }
162 
163     /** Set initial date.
164      * @param newMissionReferenceDate mission reference date to use while parsing
165      * @return a new instance, with mission reference date replaced
166      * @see #getMissionReferenceDate()
167      */
168     public TDMParser withMissionReferenceDate(final AbsoluteDate newMissionReferenceDate) {
169         return new TDMParser(getFileFormat(), newMissionReferenceDate, getConventions(), isSimpleEOP());
170     }
171 
172     /** Get initial date.
173      * @return mission reference date to use while parsing
174      * @see #withMissionReferenceDate(AbsoluteDate)
175      */
176     public AbsoluteDate getMissionReferenceDate() {
177         return missionReferenceDate;
178     }
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, isSimpleEOP());
187     }
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 
197     /** Set EOP interpolation method.
198      * @param newSimpleEOP if true, tidal effects are ignored when interpolating EOP
199      * @return a new instance, with EOP interpolation method replaced
200      * @see #isSimpleEOP()
201      */
202     public TDMParser withSimpleEOP(final boolean newSimpleEOP) {
203         return new TDMParser(getFileFormat(), getMissionReferenceDate(), getConventions(), newSimpleEOP);
204     }
205 
206     /** Get EOP interpolation method.
207      * @return true if tidal effects are ignored when interpolating EOP
208      * @see #withSimpleEOP(boolean)
209      */
210     public boolean isSimpleEOP() {
211         return simpleEOP;
212     }
213 
214     /** Parse a CCSDS Tracking Data Message.
215      * @param fileName name of the file containing the message
216      * @return parsed file content in a TDMFile object
217      */
218     public TDMFile parse(final String fileName) {
219         try (InputStream stream = new FileInputStream(fileName)) {
220             return parse(stream, fileName);
221         } catch (IOException ioe) {
222             throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, fileName);
223         }
224     }
225 
226     /** Parse a CCSDS Tracking Data Message.
227      * @param stream stream containing message
228      * @return parsed file content in a TDMFile object
229      */
230     public TDMFile parse(final InputStream stream) {
231         return parse(stream, "<unknown>");
232     }
233 
234     /** Parse a CCSDS Tracking Data Message.
235      * @param stream stream containing message
236      * @param fileName name of the file containing the message (for error messages)
237      * @return parsed file content in a TDMFile object
238      */
239     public TDMFile parse(final InputStream stream, final String fileName) {
240 
241         // Set the format of the file automatically
242         // If it is obvious and was not formerly specified
243         // Then, use a different parsing method for each file format
244         if (TDMFileFormat.UNKNOWN.equals(fileFormat)) {
245             if (fileName.toLowerCase().endsWith(".txt")) {
246                 // Keyvalue format case
247                 return this.withFileFormat(TDMFileFormat.KEYVALUE).parse(stream, fileName);
248             } else if (fileName.toLowerCase().endsWith(".xml")) {
249                 // XML format case
250                 return this.withFileFormat(TDMFileFormat.XML).parse(stream, fileName);
251             } else {
252                 throw new OrekitException(OrekitMessages.CCSDS_TDM_UNKNOWN_FORMAT, fileName);
253             }
254         } else if (this.fileFormat.equals(TDMFileFormat.KEYVALUE)) {
255             return parseKeyValue(stream, fileName);
256         } else if (this.fileFormat.equals(TDMFileFormat.XML)) {
257             return parseXml(stream, fileName);
258         } else {
259             throw new OrekitException(OrekitMessages.CCSDS_TDM_UNKNOWN_FORMAT, fileName);
260         }
261     }
262 
263     /** Parse a CCSDS Tracking Data Message with KEYVALUE format.
264      * @param stream stream containing message
265      * @param fileName name of the file containing the message (for error messages)
266      * @return parsed file content in a TDMFile object
267      */
268     public TDMFile parseKeyValue(final InputStream stream, final String fileName) {
269 
270         final KeyValueHandler handler = new KeyValueHandler(new ParseInfo(this.getMissionReferenceDate(),
271                                                                     this.getConventions(),
272                                                                     this.isSimpleEOP(),
273                                                                     fileName));
274         return handler.parse(stream, fileName);
275     }
276 
277 
278 
279     /** Parse a CCSDS Tracking Data Message with XML format.
280      * @param stream stream containing message
281      * @param fileName name of the file containing the message (for error messages)
282      * @return parsed file content in a TDMFile object
283      */
284     public TDMFile parseXml(final InputStream stream, final String fileName) {
285         try {
286             // Create the handler
287             final XMLHandler handler = new XMLHandler(new ParseInfo(this.getMissionReferenceDate(),
288                                                                     this.getConventions(),
289                                                                     this.isSimpleEOP(),
290                                                                     fileName));
291 
292             // Create the XML SAX parser factory
293             final SAXParserFactory factory = SAXParserFactory.newInstance();
294 
295             // Build the parser and read the xml file
296             final SAXParser parser = factory.newSAXParser();
297             parser.parse(stream, handler);
298 
299             // Get the content of the file
300             final TDMFile tdmFile = handler.parseInfo.tdmFile;
301 
302             // Check time systems consistency
303             tdmFile.checkTimeSystems();
304 
305             return tdmFile;
306         } catch (SAXException se) {
307             final OrekitException oe;
308             if (se.getException() != null && se.getException() instanceof OrekitException) {
309                 oe = (OrekitException) se.getException();
310             } else {
311                 oe = new OrekitException(se, new DummyLocalizable(se.getMessage()));
312             }
313             throw oe;
314         } catch (ParserConfigurationException | IOException e) {
315             // throw caught exception as an OrekitException
316             throw new OrekitException(e, new DummyLocalizable(e.getMessage()));
317         }
318     }
319 
320     /** Private class used to stock TDM parsing info.
321      * @author sports
322      */
323     private static class ParseInfo {
324 
325         /** Reference date for Mission Elapsed Time or Mission Relative Time time systems. */
326         private final AbsoluteDate missionReferenceDate;
327 
328         /** IERS Conventions. */
329         private final  IERSConventions conventions;
330 
331         /** Indicator for simple or accurate EOP interpolation. */
332         private final  boolean simpleEOP;
333 
334         /** Name of the file. */
335         private String fileName;
336 
337         /** Current Observation Block being parsed. */
338         private TDMFile.ObservationsBlock currentObservationsBlock;
339 
340         /** Current line number. */
341         private int lineNumber;
342 
343         /** Current parsed line. */
344         private String line;
345 
346         /** TDMFile object being filled. */
347         private TDMFile tdmFile;
348 
349         /** Key value of the current line being read. */
350         private KeyValue keyValue;
351 
352         /** Temporary stored comments. */
353         private List<String> commentTmp;
354 
355         /** Boolean indicating if the parser is currently parsing a meta-data block. */
356         private boolean parsingMetaData;
357 
358         /** Boolean indicating if the parser is currently parsing a data block. */
359         private boolean parsingData;
360 
361         /** Complete constructor.
362          * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
363          * @param conventions IERS Conventions
364          * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
365          * @param fileName the name of the file being parsed
366          */
367         private ParseInfo(final AbsoluteDate missionReferenceDate,
368                           final IERSConventions conventions,
369                           final boolean simpleEOP,
370                           final String fileName) {
371             this.missionReferenceDate = missionReferenceDate;
372             this.conventions          = conventions;
373             this.simpleEOP            = simpleEOP;
374             this.fileName             = fileName;
375             this.lineNumber = 0;
376             this.line = "";
377             this.tdmFile = new TDMFile();
378             this.commentTmp = new ArrayList<String>();
379             this.currentObservationsBlock = null;
380             this.parsingMetaData = false;
381             this.parsingData     = false;
382         }
383 
384         /** Parse a meta-data entry.<p>
385          * key = value (KEYVALUE file format)<p>
386          * <&lt;key>value&lt;/key> (XML file format)
387          */
388         private void parseMetaDataEntry() {
389 
390             final TDMFile.TDMMetaData metaData = this.currentObservationsBlock.getMetaData();
391 
392             try {
393                 switch (keyValue.getKeyword()) {
394                     case TIME_SYSTEM:
395                         // Read the time system and ensure that it is supported by Orekit
396                         if (!CcsdsTimeScale.contains(keyValue.getValue())) {
397                             throw new OrekitException(OrekitMessages.CCSDS_TIME_SYSTEM_NOT_IMPLEMENTED,
398                                                       keyValue.getValue());
399                         }
400                         final CcsdsTimeScale timeSystem =
401                                         CcsdsTimeScale.valueOf(keyValue.getValue());
402                         metaData.setTimeSystem(timeSystem);
403 
404                         // Convert start/stop time to AbsoluteDate if they have been read already
405                         if (metaData.getStartTimeString() != null) {
406                             metaData.setStartTime(parseDate(metaData.getStartTimeString(), timeSystem));
407                         }
408                         if (metaData.getStopTimeString() != null) {
409                             metaData.setStopTime(parseDate(metaData.getStopTimeString(), timeSystem));
410                         }
411                         break;
412 
413                     case START_TIME:
414                         // Set the start time as a String first
415                         metaData.setStartTimeString(keyValue.getValue());
416 
417                         // If time system has already been defined, convert the start time to an AbsoluteDate
418                         if (metaData.getTimeSystem() != null) {
419                             metaData.setStartTime(parseDate(keyValue.getValue(), metaData.getTimeSystem()));
420                         }
421                         break;
422 
423                     case STOP_TIME:
424                         // Set the stop time as a String first
425                         metaData.setStopTimeString(keyValue.getValue());
426 
427                         // If time system has already been defined, convert the start time to an AbsoluteDate
428                         if (metaData.getTimeSystem() != null) {
429                             metaData.setStopTime(parseDate(keyValue.getValue(), metaData.getTimeSystem()));
430                         }
431                         break;
432 
433                     case PARTICIPANT_1: case PARTICIPANT_2: case PARTICIPANT_3:
434                     case PARTICIPANT_4: case PARTICIPANT_5:
435                         // Get the participant number
436                         String key = keyValue.getKey();
437                         int participantNumber = Integer.parseInt(key.substring(key.length() - 1));
438 
439                         // Add the tuple to the map
440                         metaData.addParticipant(participantNumber, keyValue.getValue());
441                         break;
442 
443                     case MODE:
444                         metaData.setMode(keyValue.getValue());
445                         break;
446 
447                     case PATH:
448                         metaData.setPath(keyValue.getValue());
449                         break;
450 
451                     case PATH_1:
452                         metaData.setPath1(keyValue.getValue());
453                         break;
454 
455                     case PATH_2:
456                         metaData.setPath2(keyValue.getValue());
457                         break;
458 
459                     case TRANSMIT_BAND:
460                         metaData.setTransmitBand(keyValue.getValue());
461                         break;
462 
463                     case RECEIVE_BAND:
464                         metaData.setReceiveBand(keyValue.getValue());
465                         break;
466 
467                     case TURNAROUND_NUMERATOR:
468                         metaData.setTurnaroundNumerator(keyValue.getIntegerValue());
469                         break;
470 
471                     case TURNAROUND_DENOMINATOR:
472                         metaData.setTurnaroundDenominator(keyValue.getIntegerValue());
473                         break;
474 
475                     case TIMETAG_REF:
476                         metaData.setTimetagRef(keyValue.getValue());
477                         break;
478 
479                     case INTEGRATION_INTERVAL:
480                         metaData.setIntegrationInterval(keyValue.getDoubleValue());
481                         break;
482 
483                     case INTEGRATION_REF:
484                         metaData.setIntegrationRef(keyValue.getValue());
485                         break;
486 
487                     case FREQ_OFFSET:
488                         metaData.setFreqOffset(keyValue.getDoubleValue());
489                         break;
490 
491                     case RANGE_MODE:
492                         metaData.setRangeMode(keyValue.getValue());
493                         break;
494 
495                     case RANGE_MODULUS:
496                         metaData.setRangeModulus(keyValue.getDoubleValue());
497                         break;
498 
499                     case RANGE_UNITS:
500                         metaData.setRangeUnits(keyValue.getValue());
501                         break;
502 
503                     case ANGLE_TYPE:
504                         metaData.setAngleType(keyValue.getValue());
505                         break;
506 
507                     case REFERENCE_FRAME:
508                         metaData.setReferenceFrameString(keyValue.getValue());
509                         metaData.setReferenceFrame(parseCCSDSFrame(keyValue.getValue()).getFrame(this.conventions, this.simpleEOP));
510                         break;
511 
512                     case TRANSMIT_DELAY_1: case TRANSMIT_DELAY_2: case TRANSMIT_DELAY_3:
513                     case TRANSMIT_DELAY_4: case TRANSMIT_DELAY_5:
514                         // Get the participant number
515                         key = keyValue.getKey();
516                         participantNumber = Integer.parseInt(key.substring(key.length() - 1));
517 
518                         // Add the tuple to the map
519                         metaData.addTransmitDelay(participantNumber, keyValue.getDoubleValue());
520                         break;
521 
522                     case RECEIVE_DELAY_1: case RECEIVE_DELAY_2: case RECEIVE_DELAY_3:
523                     case RECEIVE_DELAY_4: case RECEIVE_DELAY_5:
524                         // Get the participant number
525                         key = keyValue.getKey();
526                         participantNumber = Integer.parseInt(key.substring(key.length() - 1));
527 
528                         // Add the tuple to the map
529                         metaData.addReceiveDelay(participantNumber, keyValue.getDoubleValue());
530                         break;
531 
532                     case DATA_QUALITY:
533                         metaData.setDataQuality(keyValue.getValue());
534                         break;
535 
536                     case CORRECTION_ANGLE_1:
537                         metaData.setCorrectionAngle1(keyValue.getDoubleValue());
538                         break;
539 
540                     case CORRECTION_ANGLE_2:
541                         metaData.setCorrectionAngle2(keyValue.getDoubleValue());
542                         break;
543 
544                     case CORRECTION_DOPPLER:
545                         metaData.setCorrectionDoppler(keyValue.getDoubleValue());
546                         break;
547 
548                     case CORRECTION_RANGE:
549                         metaData.setCorrectionRange(keyValue.getDoubleValue());
550                         break;
551 
552                     case CORRECTION_RECEIVE:
553                         metaData.setCorrectionReceive(keyValue.getDoubleValue());
554                         break;
555 
556                     case CORRECTION_TRANSMIT:
557                         metaData.setCorrectionTransmit(keyValue.getDoubleValue());
558                         break;
559 
560                     case CORRECTIONS_APPLIED:
561                         metaData.setCorrectionsApplied(keyValue.getValue());
562                         break;
563 
564                     default:
565                         throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD, lineNumber, fileName, line);
566                 }
567             } catch (NumberFormatException nfe) {
568                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
569                                           lineNumber, fileName, line);
570             }
571         }
572 
573         /** Parse a CCSDS frame.
574          * @param frameName name of the frame, as the value of a CCSDS key=value line
575          * @return CCSDS frame corresponding to the name
576          */
577         private CCSDSFrame parseCCSDSFrame(final String frameName) {
578             return CCSDSFrame.valueOf(frameName.replaceAll("-", ""));
579         }
580 
581         /** Parse a date.
582          * @param date date to parse, as the value of a CCSDS key=value line
583          * @param timeSystem time system to use
584          * @return parsed date
585          */
586         private AbsoluteDate parseDate(final String date, final CcsdsTimeScale timeSystem) {
587             return timeSystem.parseDate(date, conventions, missionReferenceDate);
588         }
589     }
590 
591     /** Handler for parsing KEYVALUE file formats. */
592     private static class KeyValueHandler {
593 
594         /** ParseInfo object. */
595         private ParseInfo parseInfo;
596 
597         /** Simple constructor.
598          * @param parseInfo ParseInfo object
599          */
600         KeyValueHandler(final ParseInfo parseInfo) {
601             this.parseInfo       = parseInfo;
602         }
603 
604         /**
605          * Parse an observation data line and add its content to the Observations Block
606          * block.
607          *
608          */
609         private void parseObservationsDataLine() {
610 
611             // Parse an observation line
612             // An observation line should consist in the string "keyword = epoch value"
613             // parseInfo.keyValue.getValue() should return the string "epoch value"
614             final String[] fields = parseInfo.keyValue.getValue().split("\\s+");
615 
616             // Check that there are 2 fields in the value of the key
617             if (fields.length != 2) {
618                 throw new OrekitException(OrekitMessages.CCSDS_TDM_INCONSISTENT_DATA_LINE,
619                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
620             }
621 
622             // Convert the date to an AbsoluteDate object (OrekitException if it fails)
623             final AbsoluteDate epoch = parseInfo.parseDate(fields[0], parseInfo.currentObservationsBlock.getMetaData().getTimeSystem());
624             final double measurement;
625             try {
626                 // Convert the value to double (NumberFormatException if it fails)
627                 measurement = Double.parseDouble(fields[1]);
628             } catch (NumberFormatException nfe) {
629                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
630                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
631             }
632 
633             // Adds the observation to current observation block
634             parseInfo.currentObservationsBlock.addObservation(parseInfo.keyValue.getKeyword().name(),
635                                                        epoch,
636                                                        measurement);
637         }
638 
639         /** Parse a CCSDS Tracking Data Message with KEYVALUE format.
640          * @param stream stream containing message
641          * @param fileName name of the file containing the message (for error messages)
642          * @return parsed file content in a TDMFile object
643          */
644         public TDMFile parse(final InputStream stream, final String fileName) {
645             try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) {
646                 try {
647                     // Initialize internal TDMFile
648                     final TDMFile tdmFile = parseInfo.tdmFile;
649 
650                     // Read the file
651                     for (String line = reader.readLine(); line != null; line = reader.readLine()) {
652                         ++parseInfo.lineNumber;
653                         if (line.trim().length() == 0) {
654                             continue;
655                         }
656                         parseInfo.line = line;
657                         parseInfo.keyValue = new KeyValue(parseInfo.line, parseInfo.lineNumber, parseInfo.fileName);
658                         if (parseInfo.keyValue.getKeyword() == null) {
659                             throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD, parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
660                         }
661                         switch (parseInfo.keyValue.getKeyword()) {
662 
663                             // Header entries
664                             case CCSDS_TDM_VERS:
665                                 // Set CCSDS TDM version
666                                 tdmFile.setFormatVersion(parseInfo.keyValue.getDoubleValue());
667                                 break;
668 
669                             case CREATION_DATE:
670                                 // Save current comment in header
671                                 tdmFile.setHeaderComment(parseInfo.commentTmp);
672                                 parseInfo.commentTmp.clear();
673                                 // Set creation date
674                                 tdmFile.setCreationDate(new AbsoluteDate(parseInfo.keyValue.getValue(), TimeScalesFactory.getUTC()));
675                                 break;
676 
677                             case ORIGINATOR:
678                                 // Set originator
679                                 tdmFile.setOriginator(parseInfo.keyValue.getValue());
680                                 break;
681 
682                                 // Comments
683                             case COMMENT:
684                                 parseInfo.commentTmp.add(parseInfo.keyValue.getValue());
685                                 break;
686 
687                                 // Start/Strop keywords
688                             case META_START:
689                                 // Add an observation block and set the last observation block to the current
690                                 tdmFile.addObservationsBlock();
691                                 parseInfo.currentObservationsBlock = tdmFile.getObservationsBlocks().get(tdmFile.getObservationsBlocks().size() - 1);
692                                 // Indicate the start of meta-data parsing for this block
693                                 parseInfo.parsingMetaData = true;
694                                 break;
695 
696                             case META_STOP:
697                                 // Save current comment in current meta-data comment
698                                 parseInfo.currentObservationsBlock.getMetaData().setComment(parseInfo.commentTmp);
699                                 parseInfo.commentTmp.clear();
700                                 // Indicate the end of meta-data parsing for this block
701                                 parseInfo.parsingMetaData = false;
702                                 break;
703 
704                             case DATA_START:
705                                 // Indicate the start of data parsing for this block
706                                 parseInfo.parsingData = true;
707                                 break;
708 
709                             case DATA_STOP:
710                                 // Save current comment in current Observation Block comment
711                                 parseInfo.currentObservationsBlock.setObservationsComment(parseInfo.commentTmp);
712                                 parseInfo.commentTmp.clear();
713                                 // Indicate the end of data parsing for this block
714                                 parseInfo.parsingData = false;
715                                 break;
716 
717                             default:
718                                 // Parse a line that does not display the previous keywords
719                                 if ((parseInfo.currentObservationsBlock != null) &&
720                                      (parseInfo.parsingData || parseInfo.parsingMetaData)) {
721                                     if (parseInfo.parsingMetaData) {
722                                         // Parse a meta-data line
723                                         parseInfo.parseMetaDataEntry();
724                                     } else {
725                                         // Parse an observation data line
726                                         this.parseObservationsDataLine();
727                                     }
728                                 } else {
729                                     throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
730                                                               parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
731                                 }
732                                 break;
733                         }
734                     }
735                     // Check time systems consistency before returning the parsed content
736                     tdmFile.checkTimeSystems();
737                     return tdmFile;
738                 } catch (IOException ioe) {
739                     throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
740                 }
741             } catch (IOException ioe) {
742                 throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
743             }
744         }
745     }
746 
747     /** Handler for parsing XML file formats. */
748     private static class XMLHandler extends DefaultHandler {
749 
750         /** ParseInfo object. */
751         private ParseInfo parseInfo;
752 
753         /** Locator used to get current line number. */
754         private Locator locator;
755 
756         /** Current keyword being read. */
757         private Keyword currentKeyword;
758 
759         /** Current observation keyword being read. */
760         private Keyword currentObservationKeyword;
761 
762         /** Current observation epoch being read. */
763         private AbsoluteDate currentObservationEpoch;
764 
765         /** Current observation measurement being read. */
766         private double currentObservationMeasurement;
767 
768         /** Simple constructor.
769          * @param parseInfo ParseInfo object
770          */
771         XMLHandler(final ParseInfo parseInfo) {
772             this.parseInfo      = parseInfo;
773             this.locator        = null;
774             this.currentKeyword = null;
775             this.currentObservationKeyword      = null;
776             this.currentObservationEpoch        = null;
777             this.currentObservationMeasurement  = Double.NaN;
778         }
779 
780         @Override
781         public void setDocumentLocator(final Locator documentLocator) {
782             this.locator = documentLocator;
783         }
784 
785         /**
786          * Extract the content of an element.
787          *
788          * @param ch the characters
789          * @param start the index of the first character of the desired content
790          * @param length the length of the content
791          * @throws SAXException in case of an error.
792          *
793          * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
794          */
795         @Override
796         public void characters(final char[] ch, final int start, final int length) throws SAXException
797         {
798             try {
799                 // currentKeyword is set to null in function endElement every time an end tag is parsed.
800                 // Thus only the characters between a start and an end tags are parsed.
801                 if (currentKeyword != null) {
802                     // Store the info in a KeyValue object so that we can use the common functions of parseInfo
803                     // The SAX locator does not allow the retrieving of the line
804                     // So a pseudo-line showing the keyword is reconstructed
805                     final String value = new String(ch, start, length);
806                     parseInfo.line = "<" + currentKeyword.name() + ">" + value + "<" + "/" + currentKeyword.name() + ">";
807                     parseInfo.lineNumber = locator.getLineNumber();
808                     parseInfo.keyValue = new KeyValue(currentKeyword, value, parseInfo.line, parseInfo.lineNumber, parseInfo.fileName);
809 
810                     // Scan the keyword
811                     switch (currentKeyword) {
812 
813                         case CREATION_DATE:
814                             // Set creation date
815                             parseInfo.tdmFile.setCreationDate(new AbsoluteDate(parseInfo.keyValue.getValue(), TimeScalesFactory.getUTC()));
816                             break;
817 
818                         case ORIGINATOR:
819                             // Set originator
820                             parseInfo.tdmFile.setOriginator(parseInfo.keyValue.getValue());
821                             break;
822 
823                         case COMMENT:
824                             // Comments
825                             parseInfo.commentTmp.add(parseInfo.keyValue.getValue());
826                             break;
827 
828                         case tdm: case header: case body: case segment:
829                         case metadata: case data:case observation:
830                             // Do nothing for this tags
831                             break;
832 
833                         default:
834                             // Parse a line that does not display the previous keywords
835                             if ((parseInfo.currentObservationsBlock != null) &&
836                                  (parseInfo.parsingData || parseInfo.parsingMetaData)) {
837                                 if (parseInfo.parsingMetaData) {
838                                     // Call meta-data parsing
839                                     parseInfo.parseMetaDataEntry();
840                                 } else if (parseInfo.parsingData) {
841                                     // Call data parsing
842                                     parseObservationDataLine();
843                                 }
844                             } else {
845                                 throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
846                                                           parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
847                             }
848                             break;
849                     }
850                 }
851             } catch (OrekitException e) {
852                 // Re-throw the exception as a SAXException
853                 throw new SAXException(e);
854             }
855         }
856 
857         /**
858          * Detect the beginning of an element.
859          *
860          * @param uri The Namespace URI, or the empty string if the element has no Namespace URI or if Namespace processing is not being performed.
861          * @param localName The local name (without prefix), or the empty string if Namespace processing is not being performed.
862          * @param qName The qualified name (with prefix), or the empty string if qualified names are not available.
863          * @param attributes The attributes attached to the element. If there are no attributes, it shall be an empty Attributes object.
864          * @throws SAXException in case of an error
865          *
866          * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
867          */
868         @Override
869         public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) throws SAXException
870         {
871             // Check if the start element belongs to the standard keywords
872             try
873             {
874                 try {
875                     this.currentKeyword = Keyword.valueOf(qName);
876                 } catch (IllegalArgumentException e) {
877                     throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
878                                               locator.getLineNumber(),
879                                               parseInfo.fileName,
880                                               "<" + qName + ">");
881                 }
882                 switch (currentKeyword) {
883                     case tdm:
884                         // Get the version number
885                         parseInfo.tdmFile.setFormatVersion(Double.parseDouble(attributes.getValue("version")));
886                         break;
887 
888                     case observation:
889                         // Re-initialize the stored observation's attributes
890                         this.currentObservationKeyword     = null;
891                         this.currentObservationEpoch       = null;
892                         this.currentObservationMeasurement = Double.NaN;
893                         break;
894 
895                     case segment:
896                         // Add an observation block and set the last observation block to the current
897                         final TDMFile tdmFile = parseInfo.tdmFile;
898                         tdmFile.addObservationsBlock();
899                         parseInfo.currentObservationsBlock = tdmFile.getObservationsBlocks().get(tdmFile.getObservationsBlocks().size() - 1);
900                         break;
901 
902                     case metadata:
903                         // Indicate the start of meta-data parsing for this block
904                         parseInfo.parsingMetaData = true;
905                         break;
906 
907                     case data:
908                         // Indicate the start of data parsing for this block
909                         parseInfo.parsingData = true;
910                         break;
911 
912                     default:
913                         // Ignore the element.
914                         break;
915                 }
916             }
917             catch (IllegalArgumentException | OrekitException e)
918             {
919                 throw new SAXException(e);
920             }
921         }
922 
923         /**
924          * Detect the end of an element and remove the stored keyword.
925          *
926          * @param uri The Namespace URI, or the empty string if the element has no Namespace URI or if Namespace processing is not being performed.
927          * @param localName The local name (without prefix), or the empty string if Namespace processing is not being performed.
928          * @param qName The qualified name (with prefix), or the empty string if qualified names are not available.
929          * @throws SAXException in case of an error
930          *
931          * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
932          */
933         @Override
934         public void endElement(final String uri, final String localName, final String qName) throws SAXException
935         {
936             // check if the start element belongs to the standard keywords
937             try
938             {
939                 // Set the stored keyword to null
940                 currentKeyword = null;
941                 // Ending keyword
942                 final Keyword endKeyword;
943                 try {
944                     endKeyword = Keyword.valueOf(qName);
945                 } catch (IllegalArgumentException e) {
946                     throw new OrekitException(OrekitMessages.CCSDS_UNEXPECTED_KEYWORD,
947                                               locator.getLineNumber(),
948                                               parseInfo.fileName,
949                                               "</" + qName + ">");
950                 }
951                 switch (endKeyword) {
952 
953                     case header:
954                         // Save header comment
955                         parseInfo.tdmFile.setHeaderComment(parseInfo.commentTmp);
956                         parseInfo.commentTmp.clear();
957                         break;
958 
959                     case observation:
960                         // Check that stored observation's attributes were all found
961                         if (currentObservationKeyword == null         ||
962                             currentObservationEpoch == null           ||
963                             Double.isNaN(currentObservationMeasurement)) {
964                             throw new OrekitException(OrekitMessages.CCSDS_TDM_XML_INCONSISTENT_DATA_BLOCK,
965                                                       locator.getLineNumber(),
966                                                       parseInfo.fileName);
967                         } else {
968                             // Add current observation
969                             parseInfo.currentObservationsBlock.addObservation(currentObservationKeyword.name(),
970                                                                               currentObservationEpoch,
971                                                                               currentObservationMeasurement);
972                         }
973                         break;
974 
975                     case segment:
976                         // Do nothing
977                         break;
978 
979                     case metadata:
980                         // Save current comment in current meta-data comment
981                         parseInfo.currentObservationsBlock.getMetaData().setComment(parseInfo.commentTmp);
982                         parseInfo.commentTmp.clear();
983                         // Indicate the end of meta-data parsing for this block
984                         parseInfo.parsingMetaData = false;
985                         break;
986 
987                     case data:
988                         // Save current comment in current Observation Block comment
989                         parseInfo.currentObservationsBlock.setObservationsComment(parseInfo.commentTmp);
990                         parseInfo.commentTmp.clear();
991                         // Indicate the end of data parsing for this block
992                         parseInfo.parsingData = false;
993                         break;
994 
995                     default:
996                         // Ignore the element.
997                 }
998             }
999             catch (IllegalArgumentException | OrekitException e)
1000             {
1001                 throw new SAXException(e);
1002             }
1003         }
1004 
1005         @Override
1006         public InputSource resolveEntity(final String publicId, final String systemId) {
1007             // disable external entities
1008             return new InputSource();
1009         }
1010 
1011         /** Parse a line in an observation data block.
1012          */
1013         private void parseObservationDataLine() {
1014 
1015             // Parse an observation line
1016             // An XML observation line should consist in the string "<KEYWORD>value</KEYWORD>
1017             // Each observation block should display:
1018             //  - One line with the keyword EPOCH;
1019             //  - One line with a specific data keyword
1020             switch(currentKeyword) {
1021                 case EPOCH:
1022                     // Convert the date to an AbsoluteDate object (OrekitException if it fails)
1023                     currentObservationEpoch = parseInfo.parseDate(parseInfo.keyValue.getValue(),
1024                                                        parseInfo.currentObservationsBlock.getMetaData().getTimeSystem());
1025                     break;
1026                 default:
1027                     try {
1028                         // Update current observation keyword
1029                         currentObservationKeyword = currentKeyword;
1030                         // Convert the value to double (NumberFormatException if it fails)
1031                         currentObservationMeasurement = Double.parseDouble(parseInfo.keyValue.getValue());
1032                     } catch (NumberFormatException nfe) {
1033                         throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
1034                                                   parseInfo.lineNumber, parseInfo.fileName, parseInfo.line);
1035                     }
1036                     break;
1037             }
1038         }
1039     }
1040 }