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.odm.ocm;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.function.Function;
24  import java.util.regex.Pattern;
25  
26  import org.orekit.bodies.OneAxisEllipsoid;
27  import org.orekit.data.DataContext;
28  import org.orekit.data.DataSource;
29  import org.orekit.errors.OrekitException;
30  import org.orekit.errors.OrekitIllegalArgumentException;
31  import org.orekit.errors.OrekitMessages;
32  import org.orekit.files.ccsds.definitions.CcsdsFrameMapper;
33  import org.orekit.files.ccsds.ndm.ParsedUnitsBehavior;
34  import org.orekit.files.ccsds.ndm.odm.OdmHeader;
35  import org.orekit.files.ccsds.ndm.odm.OdmMetadataKey;
36  import org.orekit.files.ccsds.ndm.odm.OdmParser;
37  import org.orekit.files.ccsds.ndm.odm.UserDefined;
38  import org.orekit.files.ccsds.section.HeaderProcessingState;
39  import org.orekit.files.ccsds.section.KvnStructureProcessingState;
40  import org.orekit.files.ccsds.section.MetadataKey;
41  import org.orekit.files.ccsds.section.Segment;
42  import org.orekit.files.ccsds.section.XmlStructureProcessingState;
43  import org.orekit.files.ccsds.utils.ContextBinding;
44  import org.orekit.files.ccsds.utils.FileFormat;
45  import org.orekit.files.ccsds.utils.lexical.ParseToken;
46  import org.orekit.files.ccsds.utils.lexical.TokenType;
47  import org.orekit.files.ccsds.utils.lexical.UserDefinedXmlTokenBuilder;
48  import org.orekit.files.ccsds.utils.lexical.XmlTokenBuilder;
49  import org.orekit.files.ccsds.utils.parsing.ProcessingState;
50  import org.orekit.files.general.EphemerisFileParser;
51  import org.orekit.frames.Frame;
52  import org.orekit.time.AbsoluteDate;
53  import org.orekit.utils.IERSConventions;
54  import org.orekit.utils.units.Unit;
55  
56  /** A parser for the CCSDS OCM (Orbit Comprehensive Message).
57   * <p>
58   * Note than starting with Orekit 11.0, CCSDS message parsers are
59   * mutable objects that gather the data being parsed, until the
60   * message is complete and the {@link #parseMessage(org.orekit.data.DataSource)
61   * parseMessage} method has returned. This implies that parsers
62   * should <em>not</em> be used in a multi-thread context. The recommended
63   * way to use parsers is to either dedicate one parser for each message
64   * and drop it afterwards, or to use a single-thread loop.
65   * </p>
66   * @author Luc Maisonobe
67   * @since 11.0
68   */
69  public class OcmParser extends OdmParser<Ocm, OcmParser> implements EphemerisFileParser<Ocm> {
70  
71      /** Pattern for splitting strings at blanks. */
72      private static final Pattern SPLIT_AT_BLANKS = Pattern.compile("\\s+");
73  
74      /** File header. */
75      private OdmHeader header;
76  
77      /** Metadata for current observation block. */
78      private OcmMetadata metadata;
79  
80      /** Central body equatorial radius.
81       * @since 12.0
82       */
83      private final double equatorialRadius;
84  
85      /** Central body flattening.
86       * @since 12.0
87       */
88      private final double flattening;
89  
90      /** Context binding valid for current metadata. */
91      private ContextBinding context;
92  
93      /** Trajectory state histories logical blocks. */
94      private List<TrajectoryStateHistory> trajectoryBlocks;
95  
96      /** Current trajectory state metadata. */
97      private TrajectoryStateHistoryMetadata currentTrajectoryStateHistoryMetadata;
98  
99      /** Current trajectory state time history being read. */
100     private List<TrajectoryState> currentTrajectoryStateHistory;
101 
102     /** Physical properties logical block. */
103     private OrbitPhysicalProperties physicBlock;
104 
105     /** Covariance logical blocks. */
106     private List<OrbitCovarianceHistory> covarianceBlocks;
107 
108     /** Current covariance metadata. */
109     private OrbitCovarianceHistoryMetadata currentCovarianceHistoryMetadata;
110 
111     /** Current covariance history being read. */
112     private List<OrbitCovariance> currentCovarianceHistory;
113 
114     /** Maneuver logical blocks. */
115     private List<OrbitManeuverHistory> maneuverBlocks;
116 
117     /** Current maneuver metadata. */
118     private OrbitManeuverHistoryMetadata currentManeuverHistoryMetadata;
119 
120     /** Current maneuver history being read. */
121     private List<OrbitManeuver> currentManeuverHistory;
122 
123     /** Perturbations logical block. */
124     private Perturbations perturbationsBlock;
125 
126     /** Orbit determination logical block. */
127     private OrbitDetermination orbitDeterminationBlock;
128 
129     /** User defined parameters logical block. */
130     private UserDefined userDefinedBlock;
131 
132     /** Processor for global message structure. */
133     private ProcessingState structureProcessor;
134 
135     /**
136      * Complete constructor.
137      * <p>
138      * Calling this constructor directly is not recommended. Users should rather use
139      * {@link org.orekit.files.ccsds.ndm.ParserBuilder#buildOcmParser()
140      * parserBuilder.buildOcmParser()}.
141      * </p>
142      * @param conventions IERS Conventions
143      * @param equatorialRadius central body equatorial radius
144      * @param flattening central body flattening
145      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
146      * @param dataContext used to retrieve frames, time scales, etc.
147      * @param mu gravitational coefficient
148      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
149      * @param filters filters to apply to parse tokens
150      * @param frameMapper for creating a {@link Frame}.
151      * @since 13.1.5
152      */
153     public OcmParser(final IERSConventions conventions,
154                      final double equatorialRadius, final double flattening,
155                      final boolean simpleEOP, final DataContext dataContext,
156                      final double mu, final ParsedUnitsBehavior parsedUnitsBehavior,
157                      final Function<ParseToken, List<ParseToken>>[] filters,
158                      final CcsdsFrameMapper frameMapper) {
159         super(Ocm.ROOT, Ocm.FORMAT_VERSION_KEY, conventions, simpleEOP, dataContext, null,
160               mu, parsedUnitsBehavior, filters, frameMapper);
161         this.equatorialRadius = equatorialRadius;
162         this.flattening       = flattening;
163     }
164 
165     /** {@inheritDoc} */
166     @Override
167     public Map<String, XmlTokenBuilder> getSpecialXmlElementsBuilders() {
168 
169         final Map<String, XmlTokenBuilder> builders = super.getSpecialXmlElementsBuilders();
170 
171         // special handling of user-defined parameters
172         builders.put(UserDefined.USER_DEFINED_XML_TAG, new UserDefinedXmlTokenBuilder());
173 
174         return builders;
175 
176     }
177 
178     /** {@inheritDoc} */
179     @Override
180     public Ocm parse(final DataSource source) {
181         return parseMessage(source);
182     }
183 
184     /** {@inheritDoc} */
185     @Override
186     public OdmHeader getHeader() {
187         return header;
188     }
189 
190     /** {@inheritDoc} */
191     @Override
192     public void reset(final FileFormat fileFormat) {
193         header                  = new OdmHeader();
194         metadata                = null;
195         context                 = null;
196         trajectoryBlocks        = null;
197         physicBlock             = null;
198         covarianceBlocks        = null;
199         maneuverBlocks          = null;
200         perturbationsBlock      = null;
201         orbitDeterminationBlock = null;
202         userDefinedBlock        = null;
203         if (fileFormat == FileFormat.XML) {
204             structureProcessor = new XmlStructureProcessingState(Ocm.ROOT, this);
205             reset(fileFormat, structureProcessor);
206         } else {
207             structureProcessor = new KvnStructureProcessingState(this);
208             reset(fileFormat, new HeaderProcessingState(this));
209         }
210     }
211 
212     /** {@inheritDoc} */
213     @Override
214     public boolean prepareHeader() {
215         anticipateNext(new HeaderProcessingState(this));
216         return true;
217     }
218 
219     /** {@inheritDoc} */
220     @Override
221     public boolean inHeader() {
222         anticipateNext(structureProcessor);
223         return true;
224     }
225 
226     /** {@inheritDoc} */
227     @Override
228     public boolean finalizeHeader() {
229         header.validate(header.getFormatVersion());
230         return true;
231     }
232 
233     /** {@inheritDoc} */
234     @Override
235     public boolean prepareMetadata() {
236         if (metadata != null) {
237             return false;
238         }
239         metadata  = new OcmMetadata(getDataContext(), getFrameMapper());
240         context   = new ContextBinding(this::getConventions, this::isSimpleEOP, this::getDataContext,
241                                        this::getParsedUnitsBehavior, metadata::getEpochT0, metadata::getTimeSystem,
242                                        metadata::getSclkOffsetAtEpoch, metadata::getSclkSecPerSISec);
243         anticipateNext(this::processMetadataToken);
244         return true;
245     }
246 
247     /** {@inheritDoc} */
248     @Override
249     public boolean inMetadata() {
250         anticipateNext(structureProcessor);
251         return true;
252     }
253 
254     /** {@inheritDoc} */
255     @Override
256     public boolean finalizeMetadata() {
257         metadata.validate(header.getFormatVersion());
258         anticipateNext(this::processDataSubStructureToken);
259         return true;
260     }
261 
262     /** {@inheritDoc} */
263     @Override
264     public boolean prepareData() {
265         anticipateNext(this::processDataSubStructureToken);
266         return true;
267     }
268 
269     /** {@inheritDoc} */
270     @Override
271     public boolean inData() {
272         return true;
273     }
274 
275     /** {@inheritDoc} */
276     @Override
277     public boolean finalizeData() {
278         final List<TrajectoryStateHistory> old = trajectoryBlocks;
279         if (old != null) {
280             final OneAxisEllipsoid body =
281                             currentTrajectoryStateHistoryMetadata.getTrajType() == OrbitElementsType.GEODETIC ?
282                             new OneAxisEllipsoid(equatorialRadius, flattening,
283                                                  currentTrajectoryStateHistoryMetadata.getFrame()) :
284                             null;
285             trajectoryBlocks = new ArrayList<>(old.size());
286             for (final TrajectoryStateHistory osh : old) {
287                 trajectoryBlocks.add(new TrajectoryStateHistory(osh.getMetadata(), osh.getTrajectoryStates(),
288                                                                 body, getSelectedMu()));
289             }
290         }
291         return true;
292     }
293 
294     /** Manage trajectory state history section.
295      * @param starting if true, parser is entering the section
296      * otherwise it is leaving the section
297      * @return always return true
298      */
299     boolean manageTrajectoryStateSection(final boolean starting) {
300         if (starting) {
301             if (trajectoryBlocks == null) {
302                 // this is the first trajectory block, we need to allocate the container
303                 trajectoryBlocks = new ArrayList<>();
304             }
305             currentTrajectoryStateHistoryMetadata = new TrajectoryStateHistoryMetadata(
306                     metadata.getEpochT0(),
307                     getDataContext(),
308                     getFrameMapper());
309             currentTrajectoryStateHistory         = new ArrayList<>();
310             anticipateNext(this::processTrajectoryStateToken);
311         } else {
312             final OneAxisEllipsoid body =
313                             currentTrajectoryStateHistoryMetadata.getTrajType() == OrbitElementsType.GEODETIC ?
314                             new OneAxisEllipsoid(equatorialRadius, flattening,
315                                                  currentTrajectoryStateHistoryMetadata.getFrame()) :
316                             null;
317             anticipateNext(structureProcessor);
318             if (currentTrajectoryStateHistoryMetadata.getCenter().getBody() != null) {
319                 setMuCreated(currentTrajectoryStateHistoryMetadata.getCenter().getBody().getGM());
320             }
321             // we temporarily set gravitational parameter to NaN,
322             // as we may get a proper one in the perturbations section
323             trajectoryBlocks.add(new TrajectoryStateHistory(currentTrajectoryStateHistoryMetadata,
324                                                             currentTrajectoryStateHistory,
325                                                             body, Double.NaN));
326         }
327         return true;
328     }
329 
330     /** Manage physical properties 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 managePhysicalPropertiesSection(final boolean starting) {
336         if (starting) {
337             if (physicBlock == null) {
338                 // this is the first (and unique) physical properties block, we need to allocate the container
339                 physicBlock = new OrbitPhysicalProperties(
340                         metadata.getEpochT0(),
341                         metadata.getFrameMapper());
342             }
343             anticipateNext(this::processPhysicalPropertyToken);
344         } else {
345             anticipateNext(structureProcessor);
346         }
347         return true;
348     }
349 
350     /** Manage covariance history 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 manageCovarianceHistorySection(final boolean starting) {
356         if (starting) {
357             if (covarianceBlocks == null) {
358                 // this is the first covariance block, we need to allocate the container
359                 covarianceBlocks = new ArrayList<>();
360             }
361             currentCovarianceHistoryMetadata = new OrbitCovarianceHistoryMetadata(
362                     metadata.getEpochT0(),
363                     metadata.getFrameMapper());
364             currentCovarianceHistory         = new ArrayList<>();
365             anticipateNext(this::processCovarianceToken);
366         } else {
367             anticipateNext(structureProcessor);
368             covarianceBlocks.add(new OrbitCovarianceHistory(currentCovarianceHistoryMetadata,
369                                                             currentCovarianceHistory));
370             currentCovarianceHistoryMetadata = null;
371             currentCovarianceHistory         = null;
372         }
373         return true;
374     }
375 
376     /** Manage maneuvers section.
377      * @param starting if true, parser is entering the section
378      * otherwise it is leaving the section
379      * @return always return true
380      */
381     boolean manageManeuversSection(final boolean starting) {
382         if (starting) {
383             if (maneuverBlocks == null) {
384                 // this is the first maneuver block, we need to allocate the container
385                 maneuverBlocks = new ArrayList<>();
386             }
387             currentManeuverHistoryMetadata = new OrbitManeuverHistoryMetadata(
388                     metadata.getEpochT0(),
389                     metadata.getFrameMapper());
390             currentManeuverHistory         = new ArrayList<>();
391             anticipateNext(this::processManeuverToken);
392         } else {
393             anticipateNext(structureProcessor);
394             maneuverBlocks.add(new OrbitManeuverHistory(currentManeuverHistoryMetadata,
395                                                         currentManeuverHistory));
396             currentManeuverHistoryMetadata = null;
397             currentManeuverHistory         = null;
398         }
399         return true;
400     }
401 
402     /** Manage perturbation parameters section.
403      * @param starting if true, parser is entering the section
404      * otherwise it is leaving the section
405      * @return always return true
406      */
407     boolean managePerturbationParametersSection(final boolean starting) {
408         if (starting) {
409             if (perturbationsBlock == null) {
410                 // this is the first (and unique) perturbations parameters block, we need to allocate the container
411                 perturbationsBlock = new Perturbations(context.getDataContext().getCelestialBodies());
412             }
413             anticipateNext(this::processPerturbationToken);
414         } else {
415             anticipateNext(structureProcessor);
416         }
417         return true;
418     }
419 
420     /** Manage orbit determination section.
421      * @param starting if true, parser is entering the section
422      * otherwise it is leaving the section
423      * @return always return true
424      */
425     boolean manageOrbitDeterminationSection(final boolean starting) {
426         if (starting) {
427             if (orbitDeterminationBlock == null) {
428                 // this is the first (and unique) orbit determination block, we need to allocate the container
429                 orbitDeterminationBlock = new OrbitDetermination();
430             }
431             anticipateNext(this::processOrbitDeterminationToken);
432         } else {
433             anticipateNext(structureProcessor);
434         }
435         return true;
436     }
437 
438     /** Manage user-defined parameters section.
439      * @param starting if true, parser is entering the section
440      * otherwise it is leaving the section
441      * @return always return true
442      */
443     boolean manageUserDefinedParametersSection(final boolean starting) {
444         if (starting) {
445             if (userDefinedBlock == null) {
446                 // this is the first (and unique) user-defined parameters block, we need to allocate the container
447                 userDefinedBlock = new UserDefined();
448             }
449             anticipateNext(this::processUserDefinedToken);
450         } else {
451             anticipateNext(structureProcessor);
452         }
453         return true;
454     }
455 
456     /** {@inheritDoc} */
457     @Override
458     public Ocm build() {
459 
460         // OCM KVN file lack a DATA_STOP keyword, hence we can't call finalizeData()
461         // automatically before the end of the file
462         finalizeData();
463         if (userDefinedBlock != null && userDefinedBlock.getParameters().isEmpty()) {
464             userDefinedBlock = null;
465         }
466 
467         // the mu is needed only if there are trajectories
468         final double mu;
469         if (trajectoryBlocks == null) {
470             mu = Double.NaN;
471         } else {
472             if (perturbationsBlock != null) {
473                 // this may be Double.NaN, but it will be handled correctly
474                 setMuParsed(perturbationsBlock.getGm());
475             }
476             mu = getSelectedMu();
477         }
478 
479         final OcmData data = new OcmData(trajectoryBlocks, physicBlock, covarianceBlocks,
480                                          maneuverBlocks, perturbationsBlock,
481                                          orbitDeterminationBlock, userDefinedBlock);
482         data.validate(header.getFormatVersion());
483 
484         return new Ocm(header, Collections.singletonList(new Segment<>(metadata, data)),
485                            getConventions(), getDataContext(), mu);
486 
487     }
488 
489     /** Process one metadata token.
490      * @param token token to process
491      * @return true if token was processed, false otherwise
492      */
493     private boolean processMetadataToken(final ParseToken token) {
494         inMetadata();
495         try {
496             return token.getName() != null &&
497                    MetadataKey.valueOf(token.getName()).process(token, context, metadata);
498         } catch (IllegalArgumentException iaeM) {
499             try {
500                 return OdmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
501             } catch (IllegalArgumentException iaeD) {
502                 try {
503                     return OcmMetadataKey.valueOf(token.getName()).process(token, context, metadata);
504                 } catch (IllegalArgumentException iaeC) {
505                     // token has not been recognized
506                     return false;
507                 }
508             }
509         }
510     }
511 
512     /** Process one data substructure token.
513      * @param token token to process
514      * @return true if token was processed, false otherwise
515      */
516     private boolean processDataSubStructureToken(final ParseToken token) {
517         try {
518             return token.getName() != null &&
519                    OcmDataSubStructureKey.valueOf(token.getName()).process(token, this);
520         } catch (IllegalArgumentException iae) {
521             // token has not been recognized
522             return false;
523         }
524     }
525 
526     /** Process one trajectory state history data token.
527      * @param token token to process
528      * @return true if token was processed, false otherwise
529      */
530     private boolean processTrajectoryStateToken(final ParseToken token) {
531         if (token.getName() != null && !token.getName().equals(Ocm.TRAJ_LINE)) {
532             // we are in the section metadata part
533             try {
534                 return TrajectoryStateHistoryMetadataKey.valueOf(token.getName()).
535                        process(token, context, currentTrajectoryStateHistoryMetadata);
536             } catch (IllegalArgumentException iae) {
537                 // token has not been recognized
538                 return false;
539             }
540         } else {
541             // we are in the section data part
542             if (currentTrajectoryStateHistory.isEmpty()) {
543                 // we are starting the real data section, we can now check metadata is complete
544                 currentTrajectoryStateHistoryMetadata.validate(header.getFormatVersion());
545                 anticipateNext(this::processDataSubStructureToken);
546             }
547             if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
548                 return true;
549             }
550             try {
551                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
552                 // as TRAJ_UNITS is optional and indeed MUST match type, get them directly from type
553                 final List<Unit> units = currentTrajectoryStateHistoryMetadata.getTrajType().getUnits();
554                 if (fields.length != units.size() + 1) {
555                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
556                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
557                 }
558                 final AbsoluteDate epoch = context.getTimeSystem().getConverter(context).parse(fields[0]);
559                 return currentTrajectoryStateHistory.add(new TrajectoryState(currentTrajectoryStateHistoryMetadata.getTrajType(),
560                                                                              epoch, fields, 1, units));
561             } catch (NumberFormatException | OrekitIllegalArgumentException e) {
562                 throw new OrekitException(e, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
563                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
564             }
565         }
566     }
567 
568     /** Process one physical property data token.
569      * @param token token to process
570      * @return true if token was processed, false otherwise
571      */
572     private boolean processPhysicalPropertyToken(final ParseToken token) {
573         if (physicBlock == null) {
574             physicBlock = new OrbitPhysicalProperties(
575                     metadata.getEpochT0(),
576                     metadata.getFrameMapper());
577         }
578         anticipateNext(this::processDataSubStructureToken);
579         try {
580             return token.getName() != null &&
581                    OrbitPhysicalPropertiesKey.valueOf(token.getName()).process(token, context, physicBlock);
582         } catch (IllegalArgumentException iae) {
583             // token has not been recognized
584             return false;
585         }
586     }
587 
588     /** Process one covariance history history data token.
589      * @param token token to process
590      * @return true if token was processed, false otherwise
591      */
592     private boolean processCovarianceToken(final ParseToken token) {
593         if (token.getName() != null && !token.getName().equals(Ocm.COV_LINE)) {
594             // we are in the section metadata part
595             try {
596                 return OrbitCovarianceHistoryMetadataKey.valueOf(token.getName()).
597                        process(token, context, currentCovarianceHistoryMetadata);
598             } catch (IllegalArgumentException iae) {
599                 // token has not been recognized
600                 return false;
601             }
602         } else {
603             // we are in the section data part
604             if (currentCovarianceHistory.isEmpty()) {
605                 // we are starting the real data section, we can now check metadata is complete
606                 currentCovarianceHistoryMetadata.validate(header.getFormatVersion());
607                 anticipateNext(this::processDataSubStructureToken);
608             }
609             if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
610                 return true;
611             }
612             try {
613                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
614                 final int n = currentCovarianceHistoryMetadata.getCovType().getUnits().size();
615                 if (fields.length - 1 != currentCovarianceHistoryMetadata.getCovOrdering().nbElements(n)) {
616                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
617                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
618                 }
619                 currentCovarianceHistory.add(new OrbitCovariance(currentCovarianceHistoryMetadata.getCovType(),
620                                                             currentCovarianceHistoryMetadata.getCovOrdering(),
621                                                             context.getTimeSystem().getConverter(context).parse(fields[0]),
622                                                             fields, 1));
623                 return true;
624             } catch (NumberFormatException nfe) {
625                 throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
626                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
627             }
628         }
629     }
630 
631     /** Process one maneuver data token.
632      * @param token token to process
633      * @return true if token was processed, false otherwise
634      */
635     private boolean processManeuverToken(final ParseToken token) {
636         if (token.getName() != null && !token.getName().equals(Ocm.MAN_LINE)) {
637             // we are in the section metadata part
638             try {
639                 return OrbitManeuverHistoryMetadataKey.valueOf(token.getName()).
640                        process(token, context, currentManeuverHistoryMetadata);
641             } catch (IllegalArgumentException iae) {
642                 // token has not been recognized
643                 return false;
644             }
645         } else {
646             // we are in the section data part
647             if (currentManeuverHistory.isEmpty()) {
648                 // we are starting the real data section, we can now check metadata is complete
649                 currentManeuverHistoryMetadata.validate(header.getFormatVersion());
650                 anticipateNext(this::processDataSubStructureToken);
651             }
652             if (token.getType() == TokenType.START || token.getType() == TokenType.STOP) {
653                 return true;
654             }
655             try {
656                 final String[] fields = SPLIT_AT_BLANKS.split(token.getRawContent().trim());
657                 final List<ManeuverFieldType> types = currentManeuverHistoryMetadata.getManComposition();
658                 if (fields.length != types.size()) {
659                     throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
660                                               token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
661                 }
662                 final OrbitManeuver maneuver = new OrbitManeuver();
663                 for (int i = 0; i < fields.length; ++i) {
664                     types.get(i).process(fields[i], context, maneuver, token.getLineNumber(), token.getFileName());
665                 }
666                 currentManeuverHistory.add(maneuver);
667                 return true;
668             } catch (NumberFormatException nfe) {
669                 throw new OrekitException(nfe, OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
670                                           token.getLineNumber(), token.getFileName(), token.getContentAsNormalizedString());
671             }
672         }
673     }
674 
675     /** Process one perturbation parameter data token.
676      * @param token token to process
677      * @return true if token was processed, false otherwise
678      */
679     private boolean processPerturbationToken(final ParseToken token) {
680         anticipateNext(this::processDataSubStructureToken);
681         try {
682             return token.getName() != null &&
683                    PerturbationsKey.valueOf(token.getName()).process(token, context, perturbationsBlock);
684         } catch (IllegalArgumentException iae) {
685             // token has not been recognized
686             return false;
687         }
688     }
689 
690     /** Process one orbit determination data token.
691      * @param token token to process
692      * @return true if token was processed, false otherwise
693      */
694     private boolean processOrbitDeterminationToken(final ParseToken token) {
695         if (orbitDeterminationBlock == null) {
696             orbitDeterminationBlock = new OrbitDetermination();
697         }
698         anticipateNext(this::processDataSubStructureToken);
699         try {
700             return token.getName() != null &&
701                    OrbitDeterminationKey.valueOf(token.getName()).process(token, context, orbitDeterminationBlock);
702         } catch (IllegalArgumentException iae) {
703             // token has not been recognized
704             return false;
705         }
706     }
707 
708     /** Process one user-defined parameter data token.
709      * @param token token to process
710      * @return true if token was processed, false otherwise
711      */
712     private boolean processUserDefinedToken(final ParseToken token) {
713         if (userDefinedBlock == null) {
714             userDefinedBlock = new UserDefined();
715         }
716         anticipateNext(this::processDataSubStructureToken);
717         if ("COMMENT".equals(token.getName())) {
718             return token.getType() == TokenType.ENTRY ? userDefinedBlock.addComment(token.getContentAsNormalizedString()) : true;
719         } else if (token.getName().startsWith(UserDefined.USER_DEFINED_PREFIX)) {
720             if (token.getType() == TokenType.ENTRY) {
721                 userDefinedBlock.addEntry(token.getName().substring(UserDefined.USER_DEFINED_PREFIX.length()),
722                                           token.getContentAsNormalizedString());
723             }
724             return true;
725         } else {
726             // the token was not processed
727             return false;
728         }
729     }
730 
731 }