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().isPresent() && metadata.getAltCovType().get() == AltCovarianceType.XYZ) {
267                 data = new CdmData(commentsBlock, odParameters, addParameters,
268                                              stateVector, covMatrix, xyzCovMatrix, additionalCovMetadata);
269             } else if (metadata.getAltCovType().isPresent() && metadata.getAltCovType().get() == 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         return new Cdm(header, segments, getConventions(), getDataContext());
308     }
309 
310     /** Add a general comment.
311      * @param comment comment to add
312      * @return always return true
313      */
314     boolean addGeneralComment(final String comment) {
315         return commentsBlock.addComment(comment);
316 
317     }
318 
319     /** Manage relative metadata section.
320      * @param starting if true, parser is entering the section
321      * otherwise it is leaving the section
322      * @return always return true
323      */
324     boolean manageRelativeMetadataSection(final boolean starting) {
325         anticipateNext(starting ? this::processMetadataToken : structureProcessor);
326         return true;
327     }
328 
329     /** Manage relative metadata state vector section.
330      * @param starting if true, parser is entering the section
331      * otherwise it is leaving the section
332      * @return always return true
333      */
334     boolean manageRelativeStateVectorSection(final boolean starting) {
335         anticipateNext(this::processMetadataToken);
336         return true;
337     }
338 
339     /** Manage OD parameters section.
340      * @param starting if true, parser is entering the section
341      * otherwise it is leaving the section
342      * @return always return true
343      */
344     boolean manageODParametersSection(final boolean starting) {
345         commentsBlock.refuseFurtherComments();
346         anticipateNext(starting ? this::processODParamToken : structureProcessor);
347         return true;
348     }
349 
350     /** Manage additional parameters section.
351      * @param starting if true, parser is entering the section
352      * otherwise it is leaving the section
353      * @return always return true
354      */
355     boolean manageAdditionalParametersSection(final boolean starting) {
356         commentsBlock.refuseFurtherComments();
357         anticipateNext(starting ? this::processAdditionalParametersToken : structureProcessor);
358         return true;
359     }
360 
361     /** Manage state vector section.
362      * @param starting if true, parser is entering the section
363      * otherwise it is leaving the section
364      * @return always return true
365      */
366     boolean manageStateVectorSection(final boolean starting) {
367         commentsBlock.refuseFurtherComments();
368         anticipateNext(starting ? this::processStateVectorToken : structureProcessor);
369         return true;
370     }
371 
372     /** Manage general covariance section in XML file.
373      * @param starting if true, parser is entering the section
374      * otherwise it is leaving the section
375      * @return always return true
376      */
377     boolean manageXmlGeneralCovarianceSection(final boolean starting) {
378         commentsBlock.refuseFurtherComments();
379 
380         if (starting) {
381             if (metadata.getAltCovType().isEmpty()) {
382                 anticipateNext(this::processCovMatrixToken);
383             } else {
384                 if (Double.isNaN(covMatrix.getCrr())) {
385                     // First, handle mandatory RTN covariance section
386                     anticipateNext(this::processCovMatrixToken);
387                 } else if ( metadata.getAltCovType().get() == AltCovarianceType.XYZ && xyzCovMatrix == null ||
388                                 metadata.getAltCovType().get() == AltCovarianceType.CSIG3EIGVEC3 && sig3eigvec3 == null ) {
389                     // Second, add the alternate covariance if provided
390                     anticipateNext(this::processAltCovarianceToken);
391                 } else if (additionalCovMetadata == null) {
392                     // Third, process the additional covariance metadata
393                     anticipateNext(this::processAdditionalCovMetadataToken);
394                 }
395             }
396         } else {
397             anticipateNext(structureProcessor);
398         }
399 
400         return true;
401     }
402 
403     /** Manage user-defined parameters section.
404      * @param starting if true, parser is entering the section
405      * otherwise it is leaving the section
406      * @return always return true
407      */
408     boolean manageUserDefinedParametersSection(final boolean starting) {
409         commentsBlock.refuseFurtherComments();
410         if (starting) {
411             if (userDefinedBlock == null) {
412                 // this is the first (and unique) user-defined parameters block, we need to allocate the container
413                 userDefinedBlock = new UserDefined();
414             }
415             anticipateNext(this::processUserDefinedToken);
416         } else {
417             anticipateNext(structureProcessor);
418         }
419         return true;
420     }
421 
422     /** Process one metadata token.
423      * @param token token to process
424      * @return true if token was processed, false otherwise
425      */
426     private boolean processMetadataToken(final ParseToken token) {
427         if (isDatafinished && getFileFormat() != FileFormat.XML) {
428             finalizeData();
429             isDatafinished = false;
430         }
431         if (metadata == null) {
432             // CDM KVN file lack a META_START keyword, hence we can't call prepareMetadata()
433             // automatically before the first metadata token arrives
434             prepareMetadata();
435         }
436         inMetadata();
437 
438         // There can be a COMMENT key at the beginning of relative metadata, but as the relative
439         // metadata are processed in the same try and catch loop than metadata because Orekit is
440         // build to read metadata and then data (and not relative metadata), it would be problematic
441         // to make relative metadata extends comments container(because of the COMMENTS in the middle
442         // of relativemetadata and metadata section. Indeed, as said in {@link
443         // #CommentsContainer} COMMENT should only be at the beginning of sections but in this case
444         // there is a comment at the beginning corresponding to the relative metadata comment
445         // and 1 in the middle for object 1 metadata and one further for object 2 metadata. That
446         // is why this special syntax was used and initializes the relative metadata COMMENT once
447         // at the beginning as relative metadata is not a comment container
448         final String tokenName = token.getName();  // may be null
449         if (COMMENT.equals(tokenName) && doRelativeMetadata ) {
450             if (token.getType() == TokenType.ENTRY) {
451                 relativeMetadata.addComment(token.getContentAsNormalizedString());
452                 return true;
453             }
454         }
455         doRelativeMetadata = false;
456 
457         // null check here to avoid NPE later
458         if (tokenName == null) {
459             return false;
460         }
461         // try...catch structured so that process(...) is not in the try block.
462         // Needed so that IAE indicates enum lookup failure.
463         final CdmRelativeMetadataKey cdmRelativeMetadataKey;
464         try {
465             cdmRelativeMetadataKey = CdmRelativeMetadataKey.valueOf(tokenName);
466         } catch (IllegalArgumentException iaeM) {
467             final MetadataKey metadataKey;
468             try {
469                 metadataKey = MetadataKey.valueOf(tokenName);
470             } catch (IllegalArgumentException iaeD) {
471                 final CdmMetadataKey cdmMetadataKey;
472                 try {
473                     cdmMetadataKey = CdmMetadataKey.valueOf(tokenName);
474                 } catch (IllegalArgumentException iaeC) {
475                     // token has not been recognized
476                     return false;
477                 }
478                 return cdmMetadataKey.process(token, context, metadata);
479             }
480             return metadataKey.process(token, context, metadata);
481         }
482         return cdmRelativeMetadataKey.process(token, context, relativeMetadata);
483     }
484 
485     /** Process one XML data substructure token.
486      * @param token token to process
487      * @return true if token was processed, false otherwise
488      */
489     private boolean processXmlSubStructureToken(final ParseToken token) {
490 
491         // As no relativemetadata token exists in the structure processor and as RelativeMetadata keys are
492         // processed in the same try and catch loop in processMetadatatoken as CdmMetadata keys, if the relativemetadata
493         // token is read it should be as if the token was equal to metadata to start to initialize relative metadata
494         // and metadata and to go in the processMetadataToken try and catch loop. The following relativemetadata
495         // stop should be ignored to stay in the processMetadataToken try and catch loop and the following metadata
496         // start also ignored to stay in the processMetadataToken try and catch loop. Then arrives the end of metadata
497         // so we call structure processor with metadata stop. This distinction of cases is useful for relativemetadata
498         // block followed by metadata block for object 1 and also useful to only close metadata block for object 2.
499         // The metadata start for object 2 is processed by structureProcessor
500         if (METADATA.equals(token.getName()) && TokenType.START.equals(token.getType()) ||
501             RELATIVEMETADATA.equals(token.getName()) && TokenType.STOP.equals(token.getType())) {
502             anticipateNext(this::processMetadataToken);
503             return true;
504 
505         } else if (RELATIVEMETADATA.equals(token.getName()) && TokenType.START.equals(token.getType()) ||
506                    METADATA.equals(token.getName()) && TokenType.STOP.equals(token.getType())) {
507             final ParseToken replaceToken = new ParseToken(token.getType(), METADATA,
508                                       null, token.getUnits(), token.getLineNumber(), token.getFileName());
509 
510             return structureProcessor.processToken(replaceToken);
511 
512         } else {
513 
514             // Relative metadata COMMENT and metadata COMMENT should not be read by XmlSubStructureKey that
515             // is why 2 cases are distinguished here : the COMMENT for relative metadata and the COMMENT
516             // for metadata.
517             if (commentsBlock == null && COMMENT.equals(token.getName())) {
518 
519                 // COMMENT adding for Relative Metadata in XML
520                 if (doRelativeMetadata) {
521                     if (token.getType() == TokenType.ENTRY) {
522                         relativeMetadata.addComment(token.getContentAsNormalizedString());
523                         doRelativeMetadata = false;
524                         return true;
525 
526                     } else {
527                         // if the token Type is still not ENTRY we return true as at the next step
528                         // it will be ENTRY ad we will be able to store the comment (similar treatment
529                         // as OD parameter or Additional parameter or State Vector ... COMMENT treatment.)
530                         return true;
531                     }
532                 }
533 
534                 // COMMENT adding for Metadata in XML
535                 if (!doRelativeMetadata) {
536                     if (token.getType() == TokenType.ENTRY) {
537                         metadata.addComment(token.getContentAsNormalizedString());
538                         return true;
539 
540                     } else {
541                         // same as above
542                         return true;
543                     }
544                 }
545             }
546 
547             // to treat XmlSubStructureKey keys ( OD parameters, relative Metadata ...)
548             try {
549                 return token.getName() != null && !doRelativeMetadata &&
550                        XmlSubStructureKey.valueOf(token.getName()).process(token, this);
551             } catch (IllegalArgumentException iae) {
552                 // token has not been recognized
553                 return false;
554             }
555         }
556     }
557 
558     /** Process one comment token.
559      * @param token token to process
560      * @return true if token was processed, false otherwise
561      */
562     private boolean processGeneralCommentToken(final ParseToken token) {
563         if (commentsBlock == null) {
564             // CDM KVN file lack a META_STOP keyword, hence we can't call finalizeMetadata()
565             // automatically before the first data token arrives
566             finalizeMetadata();
567             // CDM KVN file lack a DATA_START keyword, hence we can't call prepareData()
568             // automatically before the first data token arrives
569             prepareData();
570         }
571         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processODParamToken);
572         if (COMMENT.equals(token.getName()) && commentsBlock.acceptComments()) {
573             if (token.getType() == TokenType.ENTRY) {
574                 commentsBlock.addComment(token.getContentAsNormalizedString());
575             }
576             // in order to be able to differentiate general data comments and next block comment (OD parameters if not empty)
577             // only 1 line comment is allowed for general data comment.
578             commentsBlock.refuseFurtherComments();
579             return true;
580         } else {
581             return false;
582         }
583     }
584 
585     /** Process one od parameter data token.
586      * @param token token to process
587      * @return true if token was processed, false otherwise
588      */
589     private boolean processODParamToken(final ParseToken token) {
590         if (odParameters == null) {
591             odParameters = new ODParameters();
592         }
593         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processAdditionalParametersToken);
594         try {
595             return token.getName() != null &&
596                    ODParametersKey.valueOf(token.getName()).process(token, context, odParameters);
597         } catch (IllegalArgumentException iae) {
598             // token has not been recognized
599             return false;
600         }
601     }
602 
603     /** Process one additional parameter data token.
604      * @param token token to process
605      * @return true if token was processed, false otherwise
606      */
607     private boolean processAdditionalParametersToken(final ParseToken token) {
608         if (addParameters == null) {
609             addParameters = new AdditionalParameters(getFrameMapper());
610         }
611         if (moveCommentsIfEmpty(odParameters, addParameters)) {
612             // get rid of the empty logical block
613             odParameters = null;
614         }
615         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processStateVectorToken);
616         try {
617             return token.getName() != null &&
618                    AdditionalParametersKey.valueOf(token.getName()).process(token, context, addParameters);
619         } catch (IllegalArgumentException iae) {
620             // token has not been recognized
621             return false;
622         }
623     }
624 
625     /** Process one state vector data token.
626      * @param token token to process
627      * @return true if token was processed, false otherwise
628      */
629     private boolean processStateVectorToken(final ParseToken token) {
630         if (moveCommentsIfEmpty(addParameters, stateVector)) {
631             // get rid of the empty logical block
632             addParameters = null;
633         }
634         anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processCovMatrixToken);
635         try {
636             return token.getName() != null &&
637                    StateVectorKey.valueOf(token.getName()).process(token, context, stateVector);
638         } catch (IllegalArgumentException iae) {
639             // token has not been recognized
640             return false;
641         }
642     }
643 
644     /** Process covariance matrix data token.
645      * @param token token to process
646      * @return true if token was processed, false otherwise
647      */
648     private boolean processCovMatrixToken(final ParseToken token) {
649 
650         if (moveCommentsIfEmpty(stateVector, covMatrix)) {
651             // get rid of the empty logical block
652             stateVector = null;
653         }
654 
655         if (metadata.getAltCovType().isEmpty()) {
656             anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processMetadataToken);
657         } else {
658             anticipateNext(getFileFormat() == FileFormat.XML ? this::processXmlSubStructureToken : this::processAltCovarianceToken);
659         }
660 
661         isDatafinished = true;
662         try {
663             return token.getName() != null &&
664                    RTNCovarianceKey.valueOf(token.getName()).process(token, context, covMatrix);
665         } catch (IllegalArgumentException iae) {
666             // token has not been recognized
667             return false;
668         }
669     }
670 
671     /** Process alternate covariance data token.
672      * @param token token to process
673      * @return true if token was processed, false otherwise
674      */
675     private boolean processAltCovarianceToken(final ParseToken token) {
676 
677         // Covariance is provided in XYZ
678         if (metadata.getAltCovType().isPresent()) {
679             if (metadata.getAltCovType().get() == 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().get() == 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().isPresent() && metadata.getAltCovType().get() == AltCovarianceType.XYZ) {
702 
703                 return token.getName() != null &&
704                            XYZCovarianceKey.valueOf(token.getName()).process(token, context, xyzCovMatrix);
705 
706             } else if (metadata.getAltCovType().isPresent() && metadata.getAltCovType().get() == 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