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 * <<key>value</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 }