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