1   /* Copyright 2002-2026 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.ndm.cdm;
18  
19  import java.util.ArrayList;
20  import java.util.List;
21  import java.util.function.Function;
22  
23  import org.orekit.data.DataContext;
24  import org.orekit.files.ccsds.definitions.CcsdsFrameMapper;
25  import org.orekit.files.ccsds.definitions.TimeSystem;
26  import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
27  import org.orekit.files.ccsds.ndm.odm.UserDefined;
28  import org.orekit.files.ccsds.section.CommentsContainer;
29  import org.orekit.files.ccsds.section.KvnStructureProcessingState;
30  import org.orekit.files.ccsds.section.MetadataKey;
31  import org.orekit.files.ccsds.section.XmlStructureProcessingState;
32  import org.orekit.files.ccsds.utils.ContextBinding;
33  import org.orekit.files.ccsds.utils.FileFormat;
34  import org.orekit.files.ccsds.utils.lexical.ParseToken;
35  import org.orekit.files.ccsds.utils.lexical.TokenType;
36  import org.orekit.files.ccsds.utils.parsing.AbstractConstituentParser;
37  import org.orekit.files.ccsds.utils.parsing.ProcessingState;
38  import org.orekit.frames.Frame;
39  import org.orekit.utils.IERSConventions;
40  
41  /**
42   * Base class for Conjunction Data Message parsers.
43   * <p>
44   * Note than starting with Orekit 11.0, CCSDS message parsers are
45   * mutable objects that gather the data being parsed, until the
46   * message is complete and the {@link #parseMessage(org.orekit.data.DataSource)
47   * parseMessage} method has returned. This implies that parsers
48   * should <em>not</em> be used in a multi-thread context. The recommended
49   * way to use parsers is to either dedicate one parser for each message
50   * and drop it afterwards, or to use a single-thread loop.
51   * </p>
52   * @author Melina Vanel
53   * @since 11.2
54   */
55  public class CdmParser extends AbstractConstituentParser<CdmHeader, Cdm, CdmParser> {
56  
57      /** Comment key. */
58      private static final String COMMENT = "COMMENT";
59  
60      /** XML relative metadata key. */
61      private static final String RELATIVEMETADATA = "relativeMetadataData";
62  
63      /** XML metadata key. */
64      private static final String METADATA = "metadata";
65  
66      /** File header. */
67      private CdmHeader header;
68  
69      /** File segments. */
70      private List<CdmSegment> segments;
71  
72      /** CDM metadata being read. */
73      private CdmMetadata metadata;
74  
75      /** CDM relative metadata being read. */
76      private CdmRelativeMetadata relativeMetadata;
77  
78      /** Context binding valid for current metadata. */
79      private ContextBinding context;
80  
81      /** CDM general data comments block being read. */
82      private CommentsContainer commentsBlock;
83  
84      /** CDM OD parameters logical block being read. */
85      private ODParameters odParameters;
86  
87      /** CDM additional parameters logical block being read. */
88      private AdditionalParameters addParameters;
89  
90      /** CDM state vector logical block being read. */
91      private StateVector stateVector;
92  
93      /** CDM covariance matrix logical block being read. */
94      private RTNCovariance covMatrix;
95  
96      /** CDM XYZ covariance matrix logical block being read. */
97      private XYZCovariance xyzCovMatrix;
98  
99      /** CDM Sigma/Eigenvectors covariance logical block being read. */
100     private SigmaEigenvectorsCovariance sig3eigvec3;
101 
102     /** CDM additional covariance metadata logical block being read. */
103     private AdditionalCovarianceMetadata additionalCovMetadata;
104 
105     /** Processor for global message structure. */
106     private ProcessingState structureProcessor;
107 
108     /** Flag to only compute once relative metadata. */
109     private boolean doRelativeMetadata;
110 
111     /** Flag to indicate that data block parsing is finished. */
112     private boolean isDatafinished;
113 
114     /** CDM user defined logical block being read. */
115     private UserDefined userDefinedBlock;
116 
117     /** Complete constructor.
118      * <p>
119      * Calling this constructor directly is not recommended. Users should rather use
120      * {@link org.orekit.files.ccsds.ndm.ParserBuilder#buildCdmParser()
121      * parserBuilder.buildCdmParser()}.
122      * </p>
123      * @param conventions IERS Conventions
124      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
125      * @param dataContext used to retrieve frames, time scales, etc.
126      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
127      * @param filters filters to apply to parse tokens
128      * @param frameMapper for creating an Orekit {@link Frame}.
129      * @since 13.1.5
130      */
131     public CdmParser(final IERSConventions conventions, final boolean simpleEOP, final DataContext dataContext,
132                      final ParsedUnitsBehavior parsedUnitsBehavior,
133                      final Function<ParseToken, List<ParseToken>>[] filters,
134                      final CcsdsFrameMapper frameMapper) {
135         super(Cdm.ROOT, Cdm.FORMAT_VERSION_KEY, conventions, simpleEOP,
136                 dataContext, parsedUnitsBehavior, filters, frameMapper);
137         this.doRelativeMetadata = true;
138         this.isDatafinished = false;
139     }
140 
141     /** {@inheritDoc} */
142     @Override
143     public CdmHeader getHeader() {
144         return header;
145     }
146 
147     /** {@inheritDoc} */
148     @Override
149     public void reset(final FileFormat fileFormat) {
150         header                    = new CdmHeader();
151         segments                  = new ArrayList<>();
152         metadata                  = null;
153         relativeMetadata          = null;
154         context                   = null;
155         odParameters              = null;
156         addParameters             = null;
157         stateVector               = null;
158         covMatrix                 = null;
159         xyzCovMatrix              = null;
160         sig3eigvec3               = null;
161         additionalCovMetadata     = null;
162         userDefinedBlock          = null;
163         commentsBlock             = null;
164         if (fileFormat == FileFormat.XML) {
165             structureProcessor = new XmlStructureProcessingState(Cdm.ROOT, this);
166             reset(fileFormat, structureProcessor);
167         } else {
168             structureProcessor = new KvnStructureProcessingState(this);
169             reset(fileFormat, new CdmHeaderProcessingState(this));
170         }
171     }
172 
173     /** {@inheritDoc} */
174     @Override
175     public boolean prepareHeader() {
176         anticipateNext(new CdmHeaderProcessingState(this));
177         return true;
178     }
179 
180     /** {@inheritDoc} */
181     @Override
182     public boolean inHeader() {
183         anticipateNext(getFileFormat() == FileFormat.XML ? structureProcessor : this::processMetadataToken);
184         return true;
185     }
186 
187     /** {@inheritDoc} */
188     @Override
189     public boolean finalizeHeader() {
190         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : structureProcessor);
191         header.validate(header.getFormatVersion());
192         return true;
193     }
194 
195     /** {@inheritDoc} */
196     @Override
197     public boolean prepareMetadata() {
198         if (metadata != null) {
199             return false;
200         }
201         if (doRelativeMetadata) {
202             // if parser is just after header it is time to create / read relative metadata,
203             // their are only initialized once and then shared between metadata for object 1 and 2
204             relativeMetadata = new CdmRelativeMetadata();
205             relativeMetadata.setTimeSystem(TimeSystem.UTC);
206         }
207         metadata  = new CdmMetadata(getDataContext(), getFrameMapper());
208         metadata.setRelativeMetadata(relativeMetadata);
209 
210         // As no time system is defined in CDM because all dates are given in UTC,
211         // time system is set here to UTC, we use relative metadata and not metadata
212         // because setting time system on metadata implies refusingfurthercomments
213         // witch would be a problem as metadata comments have not been read yet.
214         context   = new ContextBinding(this::getConventions, this::isSimpleEOP,
215                                        this::getDataContext, this::getParsedUnitsBehavior,
216                                        () -> null, relativeMetadata::getTimeSystem,
217                                        () -> 0.0, () -> 1.0);
218         anticipateNext(this::processMetadataToken);
219         return true;
220     }
221 
222     /** {@inheritDoc} */
223     @Override
224     public boolean inMetadata() {
225         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processGeneralCommentToken);
226         return true;
227     }
228 
229     /** {@inheritDoc} */
230     @Override
231     public boolean finalizeMetadata() {
232         metadata.validate(header.getFormatVersion());
233         relativeMetadata.validate();
234         anticipateNext(structureProcessor);
235         return true;
236     }
237 
238     /** {@inheritDoc} */
239     @Override
240     public boolean prepareData() {
241         // stateVector and RTNCovariance blocks are 2 mandatory data blocks
242         stateVector = new StateVector();
243         covMatrix = new RTNCovariance();
244 
245         // initialize comments block for general data comments
246         commentsBlock = new CommentsContainer();
247         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processGeneralCommentToken);
248         return true;
249     }
250 
251     /** {@inheritDoc} */
252     @Override
253     public boolean inData() {
254         return true;
255     }
256 
257     /** {@inheritDoc} */
258     @Override
259     public boolean finalizeData() {
260         // call at the and of data block for object 1 or 2
261         if (metadata != null) {
262 
263             CdmData data = new CdmData(commentsBlock, odParameters, addParameters,
264                                              stateVector, covMatrix, additionalCovMetadata);
265 
266             if (metadata.getAltCovType() != null && metadata.getAltCovType() == AltCovarianceType.XYZ) {
267                 data = new CdmData(commentsBlock, odParameters, addParameters,
268                                              stateVector, covMatrix, xyzCovMatrix, additionalCovMetadata);
269             } else if (metadata.getAltCovType() != null && metadata.getAltCovType() == AltCovarianceType.CSIG3EIGVEC3) {
270                 data = new CdmData(commentsBlock, odParameters, addParameters,
271                                              stateVector, covMatrix, sig3eigvec3, additionalCovMetadata);
272             }
273 
274             data.validate(header.getFormatVersion());
275             segments.add(new CdmSegment(metadata, data));
276 
277             // Add the user defined block to both Objects data sections
278             if (userDefinedBlock != null && !userDefinedBlock.getParameters().isEmpty()) {
279                 for (CdmSegment segment : segments) {
280                     segment.getData().setUserDefinedBlock(userDefinedBlock);
281                 }
282             }
283         }
284         metadata                  = null;
285         context                   = null;
286         odParameters              = null;
287         addParameters             = null;
288         stateVector               = null;
289         covMatrix                 = null;
290         xyzCovMatrix              = null;
291         sig3eigvec3               = null;
292         additionalCovMetadata     = null;
293         userDefinedBlock          = null;
294         commentsBlock             = null;
295         return true;
296     }
297 
298     /** {@inheritDoc} */
299     @Override
300     public Cdm build() {
301         // CDM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
302         // automatically before the end of the file
303         finalizeData();
304         if (userDefinedBlock != null && userDefinedBlock.getParameters().isEmpty()) {
305             userDefinedBlock = null;
306         }
307         final Cdm file = new Cdm(header, segments, getConventions(), getDataContext());
308         return file;
309     }
310 
311     /** Add a general comment.
312      * @param comment comment to add
313      * @return always return true
314      */
315     boolean addGeneralComment(final String comment) {
316         return commentsBlock.addComment(comment);
317 
318     }
319 
320     /** Manage relative metadata section.
321      * @param starting if true, parser is entering the section
322      * otherwise it is leaving the section
323      * @return always return true
324      */
325     boolean manageRelativeMetadataSection(final boolean starting) {
326         anticipateNext(starting ? this::processMetadataToken : structureProcessor);
327         return true;
328     }
329 
330     /** Manage relative metadata state vector section.
331      * @param starting if true, parser is entering the section
332      * otherwise it is leaving the section
333      * @return always return true
334      */
335     boolean manageRelativeStateVectorSection(final boolean starting) {
336         anticipateNext(this::processMetadataToken);
337         return true;
338     }
339 
340     /** Manage OD parameters section.
341      * @param starting if true, parser is entering the section
342      * otherwise it is leaving the section
343      * @return always return true
344      */
345     boolean manageODParametersSection(final boolean starting) {
346         commentsBlock.refuseFurtherComments();
347         anticipateNext(starting ? this::processODParamToken : structureProcessor);
348         return true;
349     }
350 
351     /** Manage additional parameters section.
352      * @param starting if true, parser is entering the section
353      * otherwise it is leaving the section
354      * @return always return true
355      */
356     boolean manageAdditionalParametersSection(final boolean starting) {
357         commentsBlock.refuseFurtherComments();
358         anticipateNext(starting ? this::processAdditionalParametersToken : structureProcessor);
359         return true;
360     }
361 
362     /** Manage state vector section.
363      * @param starting if true, parser is entering the section
364      * otherwise it is leaving the section
365      * @return always return true
366      */
367     boolean manageStateVectorSection(final boolean starting) {
368         commentsBlock.refuseFurtherComments();
369         anticipateNext(starting ? this::processStateVectorToken : structureProcessor);
370         return true;
371     }
372 
373     /** Manage general covariance section in XML file.
374      * @param starting if true, parser is entering the section
375      * otherwise it is leaving the section
376      * @return always return true
377      */
378     boolean manageXmlGeneralCovarianceSection(final boolean starting) {
379         commentsBlock.refuseFurtherComments();
380 
381         if (starting) {
382             if (metadata.getAltCovType() == null) {
383                 anticipateNext(this::processCovMatrixToken);
384             } else {
385                 if (Double.isNaN(covMatrix.getCrr())) {
386                     // First, handle mandatory RTN covariance section
387                     anticipateNext(this::processCovMatrixToken);
388                 } else if ( metadata.getAltCovType() == AltCovarianceType.XYZ && xyzCovMatrix == null ||
389                                 metadata.getAltCovType() == AltCovarianceType.CSIG3EIGVEC3 && sig3eigvec3 == null ) {
390                     // Second, add the alternate covariance if provided
391                     anticipateNext(this::processAltCovarianceToken);
392                 } else if (additionalCovMetadata == null) {
393                     // Third, process the additional covariance metadata
394                     anticipateNext(this::processAdditionalCovMetadataToken);
395                 }
396             }
397         } else {
398             anticipateNext(structureProcessor);
399         }
400 
401         return true;
402     }
403 
404     /** Manage user-defined parameters section.
405      * @param starting if true, parser is entering the section
406      * otherwise it is leaving the section
407      * @return always return true
408      */
409     boolean manageUserDefinedParametersSection(final boolean starting) {
410         commentsBlock.refuseFurtherComments();
411         if (starting) {
412             if (userDefinedBlock == null) {
413                 // this is the first (and unique) user-defined parameters block, we need to allocate the container
414                 userDefinedBlock = new UserDefined();
415             }
416             anticipateNext(this::processUserDefinedToken);
417         } else {
418             anticipateNext(structureProcessor);
419         }
420         return true;
421     }
422 
423     /** Process one metadata token.
424      * @param token token to process
425      * @return true if token was processed, false otherwise
426      */
427     private boolean processMetadataToken(final ParseToken token) {
428         if (isDatafinished && getFileFormat() != FileFormat.XML) {
429             finalizeData();
430             isDatafinished = false;
431         }
432         if (metadata == null) {
433             // CDM KVN file lack a META_START keyword, hence we can't call prepareMetadata()
434             // automatically before the first metadata token arrives
435             prepareMetadata();
436         }
437         inMetadata();
438 
439         // There can be a COMMENT key at the beginning of relative metadata, but as the relative
440         // metadata are processed in the same try and catch loop than metadata because Orekit is
441         // build to read metadata and then data (and not relative metadata), it would be problematic
442         // to make relative metadata extends comments container(because of the COMMENTS in the middle
443         // of relativemetadata and metadata section. Indeed, as said in {@link
444         // #CommentsContainer} COMMENT should only be at the beginning of sections but in this case
445         // there is a comment at the beginning corresponding to the relative metadata comment
446         // and 1 in the middle for object 1 metadata and one further for object 2 metadata. That
447         // is why this special syntax was used and initializes the relative metadata COMMENT once
448         // at the beginning as relative metadata is not a comment container
449         final String tokenName = token.getName();  // may be null
450         if (COMMENT.equals(tokenName) && doRelativeMetadata ) {
451             if (token.getType() == TokenType.ENTRY) {
452                 relativeMetadata.addComment(token.getContentAsNormalizedString());
453                 return true;
454             }
455         }
456         doRelativeMetadata = false;
457 
458         // null check here to avoid NPE later
459         if (tokenName == null) {
460             return false;
461         }
462         // try...catch structured so that process(...) is not in the try block.
463         // Needed so that IAE indicates enum lookup failure.
464         final CdmRelativeMetadataKey cdmRelativeMetadataKey;
465         try {
466             cdmRelativeMetadataKey = CdmRelativeMetadataKey.valueOf(tokenName);
467         } catch (IllegalArgumentException iaeM) {
468             final MetadataKey metadataKey;
469             try {
470                 metadataKey = MetadataKey.valueOf(tokenName);
471             } catch (IllegalArgumentException iaeD) {
472                 final CdmMetadataKey cdmMetadataKey;
473                 try {
474                     cdmMetadataKey = CdmMetadataKey.valueOf(tokenName);
475                 } catch (IllegalArgumentException iaeC) {
476                     // token has not been recognized
477                     return false;
478                 }
479                 return cdmMetadataKey.process(token, context, metadata);
480             }
481             return metadataKey.process(token, context, metadata);
482         }
483         return cdmRelativeMetadataKey.process(token, context, relativeMetadata);
484     }
485 
486     /** Process one XML data substructure token.
487      * @param token token to process
488      * @return true if token was processed, false otherwise
489      */
490     private boolean processXmlSubStructureToken(final ParseToken token) {
491 
492         // As no relativemetadata token exists in the structure processor and as RelativeMetadata keys are
493         // processed in the same try and catch loop in processMetadatatoken as CdmMetadata keys, if the relativemetadata
494         // token is read it should be as if the token was equal to metadata to start to initialize relative metadata
495         // and metadata and to go in the processMetadataToken try and catch loop. The following relativemetadata
496         // stop should be ignored to stay in the processMetadataToken try and catch loop and the following metadata
497         // start also ignored to stay in the processMetadataToken try and catch loop. Then arrives the end of metadata
498         // so we call structure processor with metadata stop. This distinction of cases is useful for relativemetadata
499         // block followed by metadata block for object 1 and also useful to only close metadata block for object 2.
500         // The metadata start for object 2 is processed by structureProcessor
501         if (METADATA.equals(token.getName()) && TokenType.START.equals(token.getType()) ||
502             RELATIVEMETADATA.equals(token.getName()) && TokenType.STOP.equals(token.getType())) {
503             anticipateNext(this::processMetadataToken);
504             return true;
505 
506         } else if (RELATIVEMETADATA.equals(token.getName()) && TokenType.START.equals(token.getType()) ||
507                    METADATA.equals(token.getName()) && TokenType.STOP.equals(token.getType())) {
508             final ParseToken replaceToken = new ParseToken(token.getType(), METADATA,
509                                       null, token.getUnits(), token.getLineNumber(), token.getFileName());
510 
511             return structureProcessor.processToken(replaceToken);
512 
513         } else {
514 
515             // Relative metadata COMMENT and metadata COMMENT should not be read by XmlSubStructureKey that
516             // is why 2 cases are distinguished here : the COMMENT for relative metadata and the COMMENT
517             // for metadata.
518             if (commentsBlock == null && COMMENT.equals(token.getName())) {
519 
520                 // COMMENT adding for Relative Metadata in XML
521                 if (doRelativeMetadata) {
522                     if (token.getType() == TokenType.ENTRY) {
523                         relativeMetadata.addComment(token.getContentAsNormalizedString());
524                         doRelativeMetadata = false;
525                         return true;
526 
527                     } else {
528                         // if the token Type is still not ENTRY we return true as at the next step
529                         // it will be ENTRY ad we will be able to store the comment (similar treatment
530                         // as OD parameter or Additional parameter or State Vector ... COMMENT treatment.)
531                         return true;
532                     }
533                 }
534 
535                 // COMMENT adding for Metadata in XML
536                 if (!doRelativeMetadata) {
537                     if (token.getType() == TokenType.ENTRY) {
538                         metadata.addComment(token.getContentAsNormalizedString());
539                         return true;
540 
541                     } else {
542                         // same as above
543                         return true;
544                     }
545                 }
546             }
547 
548             // to treat XmlSubStructureKey keys ( OD parameters, relative Metadata ...)
549             try {
550                 return token.getName() != null && !doRelativeMetadata &&
551                        XmlSubStructureKey.valueOf(token.getName()).process(token, this);
552             } catch (IllegalArgumentException iae) {
553                 // token has not been recognized
554                 return false;
555             }
556         }
557     }
558 
559     /** Process one comment token.
560      * @param token token to process
561      * @return true if token was processed, false otherwise
562      */
563     private boolean processGeneralCommentToken(final ParseToken token) {
564         if (commentsBlock == null) {
565             // CDM KVN file lack a META_STOP keyword, hence we can't call finalizeMetadata()
566             // automatically before the first data token arrives
567             finalizeMetadata();
568             // CDM KVN file lack a DATA_START keyword, hence we can't call prepareData()
569             // automatically before the first data token arrives
570             prepareData();
571         }
572         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processODParamToken);
573         if (COMMENT.equals(token.getName()) && commentsBlock.acceptComments()) {
574             if (token.getType() == TokenType.ENTRY) {
575                 commentsBlock.addComment(token.getContentAsNormalizedString());
576             }
577             // in order to be able to differentiate general data comments and next block comment (OD parameters if not empty)
578             // only 1 line comment is allowed for general data comment.
579             commentsBlock.refuseFurtherComments();
580             return true;
581         } else {
582             return false;
583         }
584     }
585 
586     /** Process one od parameter data token.
587      * @param token token to process
588      * @return true if token was processed, false otherwise
589      */
590     private boolean processODParamToken(final ParseToken token) {
591         if (odParameters == null) {
592             odParameters = new ODParameters();
593         }
594         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processAdditionalParametersToken);
595         try {
596             return token.getName() != null &&
597                    ODParametersKey.valueOf(token.getName()).process(token, context, odParameters);
598         } catch (IllegalArgumentException iae) {
599             // token has not been recognized
600             return false;
601         }
602     }
603 
604     /** Process one additional parameter data token.
605      * @param token token to process
606      * @return true if token was processed, false otherwise
607      */
608     private boolean processAdditionalParametersToken(final ParseToken token) {
609         if (addParameters == null) {
610             addParameters = new AdditionalParameters(getFrameMapper());
611         }
612         if (moveCommentsIfEmpty(odParameters, addParameters)) {
613             // get rid of the empty logical block
614             odParameters = null;
615         }
616         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processStateVectorToken);
617         try {
618             return token.getName() != null &&
619                    AdditionalParametersKey.valueOf(token.getName()).process(token, context, addParameters);
620         } catch (IllegalArgumentException iae) {
621             // token has not been recognized
622             return false;
623         }
624     }
625 
626     /** Process one state vector data token.
627      * @param token token to process
628      * @return true if token was processed, false otherwise
629      */
630     private boolean processStateVectorToken(final ParseToken token) {
631         if (moveCommentsIfEmpty(addParameters, stateVector)) {
632             // get rid of the empty logical block
633             addParameters = null;
634         }
635         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processCovMatrixToken);
636         try {
637             return token.getName() != null &&
638                    StateVectorKey.valueOf(token.getName()).process(token, context, stateVector);
639         } catch (IllegalArgumentException iae) {
640             // token has not been recognized
641             return false;
642         }
643     }
644 
645     /** Process covariance matrix data token.
646      * @param token token to process
647      * @return true if token was processed, false otherwise
648      */
649     private boolean processCovMatrixToken(final ParseToken token) {
650 
651         if (moveCommentsIfEmpty(stateVector, covMatrix)) {
652             // get rid of the empty logical block
653             stateVector = null;
654         }
655 
656         if (metadata.getAltCovType() == null) {
657             anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processMetadataToken);
658         } else {
659             anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processAltCovarianceToken);
660         }
661 
662         isDatafinished = true;
663         try {
664             return token.getName() != null &&
665                    RTNCovarianceKey.valueOf(token.getName()).process(token, context, covMatrix);
666         } catch (IllegalArgumentException iae) {
667             // token has not been recognized
668             return false;
669         }
670     }
671 
672     /** Process alternate covariance data token.
673      * @param token token to process
674      * @return true if token was processed, false otherwise
675      */
676     private boolean processAltCovarianceToken(final ParseToken token) {
677 
678         // Covariance is provided in XYZ
679         if (metadata.getAltCovType() == AltCovarianceType.XYZ && xyzCovMatrix == null) {
680             xyzCovMatrix = new XYZCovariance(true);
681 
682             if (moveCommentsIfEmpty(covMatrix, xyzCovMatrix)) {
683                 // get rid of the empty logical block
684                 covMatrix = null;
685             }
686         }
687         // Covariance is provided in CSIG3EIGVEC3 format
688         if (metadata.getAltCovType() == AltCovarianceType.CSIG3EIGVEC3 && sig3eigvec3 == null) {
689             sig3eigvec3 = new SigmaEigenvectorsCovariance(true);
690 
691             if (moveCommentsIfEmpty(covMatrix, sig3eigvec3)) {
692                 // get rid of the empty logical block
693                 covMatrix = null;
694             }
695         }
696 
697 
698         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processAdditionalCovMetadataToken);
699         try {
700 
701             if (metadata.getAltCovType() != null && metadata.getAltCovType() == AltCovarianceType.XYZ) {
702 
703                 return token.getName() != null &&
704                            XYZCovarianceKey.valueOf(token.getName()).process(token, context, xyzCovMatrix);
705 
706             } else if (metadata.getAltCovType() != null && metadata.getAltCovType() == AltCovarianceType.CSIG3EIGVEC3) {
707 
708                 return token.getName() != null &&
709                            SigmaEigenvectorsCovarianceKey.valueOf(token.getName()).process(token, context, sig3eigvec3);
710 
711             } else {
712 
713                 // token has not been recognized
714                 return false;
715 
716             }
717 
718         } catch (IllegalArgumentException iae) {
719             // token has not been recognized
720             return false;
721         }
722     }
723 
724     /** Process additional covariance metadata token.
725      * @param token token to process
726      * @return true if token was processed, false otherwise
727      */
728     private boolean processAdditionalCovMetadataToken(final ParseToken token) {
729 
730         // Additional covariance metadata
731         if ( additionalCovMetadata == null) {
732             additionalCovMetadata = new AdditionalCovarianceMetadata();
733         }
734 
735         if (moveCommentsIfEmpty(xyzCovMatrix, additionalCovMetadata)) {
736             // get rid of the empty logical block
737             xyzCovMatrix = null;
738         } else if (moveCommentsIfEmpty(sig3eigvec3, additionalCovMetadata)) {
739             // get rid of the empty logical block
740             sig3eigvec3 = null;
741         }
742 
743         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processUserDefinedToken);
744         try {
745             return token.getName() != null &&
746                            AdditionalCovarianceMetadataKey.valueOf(token.getName()).process(token, context, additionalCovMetadata);
747         } catch (IllegalArgumentException iae) {
748             // token has not been recognized
749             return false;
750         }
751     }
752 
753     /** Process one user-defined parameter data token.
754      * @param token token to process
755      * @return true if token was processed, false otherwise
756      */
757     private boolean processUserDefinedToken(final ParseToken token) {
758 
759         if (userDefinedBlock == null) {
760             userDefinedBlock = new UserDefined();
761         }
762 
763         if (moveCommentsIfEmpty(additionalCovMetadata, userDefinedBlock)) {
764             // get rid of the empty logical block
765             additionalCovMetadata = null;
766         }
767 
768         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processMetadataToken);
769 
770         if (COMMENT.equals(token.getName())) {
771             return token.getType() == TokenType.ENTRY ? userDefinedBlock.addComment(token.getContentAsNormalizedString()) : true;
772         } else if (token.getName().startsWith(UserDefined.USER_DEFINED_PREFIX)) {
773             if (token.getType() == TokenType.ENTRY) {
774                 userDefinedBlock.addEntry(token.getName().substring(UserDefined.USER_DEFINED_PREFIX.length()),
775                                           token.getContentAsNormalizedString());
776             }
777             return true;
778         } else {
779             // the token was not processed
780             return false;
781         }
782     }
783 
784 
785     /** Move comments from one empty logical block to another logical block.
786      * @param origin origin block
787      * @param destination destination block
788      * @return true if origin block was empty
789      */
790     private boolean moveCommentsIfEmpty(final CommentsContainer origin, final CommentsContainer destination) {
791         if (origin != null && origin.acceptComments()) {
792             // origin block is empty, move the existing comments
793             for (final String comment : origin.getComments()) {
794                 destination.addComment(comment);
795             }
796             return true;
797         } else {
798             return false;
799         }
800     }
801 
802 }
803