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;
18  
19  import java.lang.reflect.Array;
20  import java.util.List;
21  import java.util.function.Function;
22  
23  import org.orekit.annotation.DefaultDataContext;
24  import org.orekit.data.DataContext;
25  import org.orekit.files.ccsds.ndm.adm.acm.AcmParser;
26  import org.orekit.files.ccsds.ndm.adm.aem.AemParser;
27  import org.orekit.files.ccsds.ndm.adm.apm.ApmParser;
28  import org.orekit.files.ccsds.ndm.cdm.CdmParser;
29  import org.orekit.files.ccsds.ndm.odm.ocm.OcmParser;
30  import org.orekit.files.ccsds.ndm.odm.oem.OemParser;
31  import org.orekit.files.ccsds.ndm.odm.omm.OmmParser;
32  import org.orekit.files.ccsds.ndm.odm.opm.OpmParser;
33  import org.orekit.files.ccsds.ndm.tdm.IdentityConverter;
34  import org.orekit.files.ccsds.ndm.tdm.RangeUnits;
35  import org.orekit.files.ccsds.ndm.tdm.RangeUnitsConverter;
36  import org.orekit.files.ccsds.ndm.tdm.TdmParser;
37  import org.orekit.files.ccsds.utils.lexical.ParseToken;
38  import org.orekit.time.AbsoluteDate;
39  import org.orekit.utils.IERSConventions;
40  
41  /** Builder for all {@link NdmConstituent CCSDS Message} files parsers.
42   * <p>
43   * This builder can be used for building all CCSDS Messages parsers types.
44   * It is particularly useful in multi-threaded context as parsers cannot
45   * be shared between threads and thus several independent parsers must be
46   * built in this case.
47   * </p>
48   * @author Luc Maisonobe
49   * @since 11.0
50   */
51  public class ParserBuilder extends AbstractBuilder<ParserBuilder> {
52  
53      /** Indicator for simple or accurate EOP interpolation. */
54      private final  boolean simpleEOP;
55  
56      /** Gravitational coefficient. */
57      private final double mu;
58  
59      /** Default mass. */
60      private final double defaultMass;
61  
62      /** Default interpolation degree. */
63      private final int defaultInterpolationDegree;
64  
65      /** Behavior adopted for units that have been parsed from a CCSDS message. */
66      private final ParsedUnitsBehavior parsedUnitsBehavior;
67  
68      /** Filters for parse tokens.
69       * @since 12.0
70       */
71      private final Function<ParseToken, List<ParseToken>>[] filters;
72  
73      /**
74       * Simple constructor.
75       * <p>
76       * This constructor creates a builder with
77       * <ul>
78       *   <li>{@link #getConventions() IERS conventions} set to {@link IERSConventions#IERS_2010}</li>
79       *   <li>{@link #isSimpleEOP() simple EOP} set to {@code true}</li>
80       *   <li>{@link #getDataContext() data context} set to {@link DataContext#getDefault() default context}</li>
81       *   <li>{@link #getMissionReferenceDate() mission reference date} set to {@code null}</li>
82       *   <li>{@link #getMu() gravitational coefficient} set to {@code Double.NaN}</li>
83       *   <li>{@link #getEquatorialRadius() central body equatorial radius} set to {@code Double.NaN}</li>
84       *   <li>{@link #getFlattening() central body flattening} set to {@code Double.NaN}</li>
85       *   <li>{@link #getDefaultMass() default mass} set to {@code Double.NaN}</li>
86       *   <li>{@link #getDefaultInterpolationDegree() default interpolation degree} set to {@code 1}</li>
87       *   <li>{@link #getParsedUnitsBehavior() parsed unit behavior} set to {@link ParsedUnitsBehavior#CONVERT_COMPATIBLE}</li>
88       *   <li>{@link #getRangeUnitsConverter() converter for range units} set to {@link IdentityConverter}</li>
89       * </ul>
90       */
91      @DefaultDataContext
92      public ParserBuilder() {
93          this(DataContext.getDefault());
94      }
95  
96      /**
97       * Simple constructor.
98       * <p>
99       * This constructor creates a builder with
100      * <ul>
101      *   <li>{@link #getConventions() IERS conventions} set to {@link IERSConventions#IERS_2010}</li>
102      *   <li>{@link #isSimpleEOP() simple EOP} set to {@code true}</li>
103      *   <li>{@link #getMissionReferenceDate() mission reference date} set to {@code null}</li>
104      *   <li>{@link #getMu() gravitational coefficient} set to {@code Double.NaN}</li>
105      *   <li>{@link #getEquatorialRadius() central body equatorial radius} set to {@code Double.NaN}</li>
106      *   <li>{@link #getFlattening() central body flattening} set to {@code Double.NaN}</li>
107      *   <li>{@link #getDefaultMass() default mass} set to {@code Double.NaN}</li>
108      *   <li>{@link #getDefaultInterpolationDegree() default interpolation degree} set to {@code 1}</li>
109      *   <li>{@link #getParsedUnitsBehavior() parsed unit behavior} set to {@link ParsedUnitsBehavior#CONVERT_COMPATIBLE}</li>
110      *   <li>{@link #getRangeUnitsConverter() converter for range units} set to {@link IdentityConverter}</li>
111      * </ul>
112      * @param dataContext data context used to retrieve frames, time scales, etc.
113      */
114     @SuppressWarnings("unchecked")
115     public ParserBuilder(final DataContext dataContext) {
116         this(IERSConventions.IERS_2010, Double.NaN, Double.NaN, dataContext,
117              null, new IdentityConverter(), true, Double.NaN, Double.NaN,
118              1, ParsedUnitsBehavior.CONVERT_COMPATIBLE,
119              (Function<ParseToken, List<ParseToken>>[]) Array.newInstance(Function.class, 0));
120     }
121 
122     /** Complete constructor.
123      * @param conventions IERS Conventions
124      * @param equatorialRadius central body equatorial radius
125      * @param flattening central body flattening
126      * @param dataContext used to retrieve frames, time scales, etc.
127      * @param missionReferenceDate reference date for Mission Elapsed Time or Mission Relative Time time systems
128      * @param rangeUnitsConverter converter for {@link RangeUnits#RU Range Units}
129      * @param simpleEOP if true, tidal effects are ignored when interpolating EOP
130      * @param mu gravitational coefficient
131      * @param defaultMass default mass
132      * @param defaultInterpolationDegree default interpolation degree
133      * @param parsedUnitsBehavior behavior to adopt for handling parsed units
134      * @param filters filters to apply to parse tokens
135      * @since 12.0
136      */
137     private ParserBuilder(final IERSConventions conventions,
138                           final double equatorialRadius, final double flattening,
139                           final DataContext dataContext, final AbsoluteDate missionReferenceDate,
140                           final RangeUnitsConverter rangeUnitsConverter,
141                           final boolean simpleEOP, final double mu,
142                           final double defaultMass, final int defaultInterpolationDegree,
143                           final ParsedUnitsBehavior parsedUnitsBehavior,
144                           final Function<ParseToken, List<ParseToken>>[] filters) {
145         super(conventions, equatorialRadius, flattening, dataContext, missionReferenceDate, rangeUnitsConverter);
146         this.simpleEOP                  = simpleEOP;
147         this.mu                         = mu;
148         this.defaultMass                = defaultMass;
149         this.defaultInterpolationDegree = defaultInterpolationDegree;
150         this.parsedUnitsBehavior        = parsedUnitsBehavior;
151         this.filters                    = filters.clone();
152     }
153 
154     /** {@inheritDoc} */
155     @Override
156     protected ParserBuilder create(final IERSConventions newConventions,
157                                    final double newEquatorialRadius, final double newFlattening,
158                                    final DataContext newDataContext,
159                                    final AbsoluteDate newMissionReferenceDate, final RangeUnitsConverter newRangeUnitsConverter) {
160         return new ParserBuilder(newConventions, newEquatorialRadius, newFlattening, newDataContext,
161                                  newMissionReferenceDate, newRangeUnitsConverter, simpleEOP, mu,
162                                  defaultMass, defaultInterpolationDegree, parsedUnitsBehavior, filters);
163     }
164 
165     /** Set up flag for ignoring tidal effects when interpolating EOP.
166      * @param newSimpleEOP true if tidal effects are ignored when interpolating EOP
167      * @return a new builder with updated configuration (the instance is not changed)
168      */
169     public ParserBuilder withSimpleEOP(final boolean newSimpleEOP) {
170         return new ParserBuilder(getConventions(), getEquatorialRadius(), getFlattening(), getDataContext(),
171                                  getMissionReferenceDate(), getRangeUnitsConverter(), newSimpleEOP, getMu(), getDefaultMass(),
172                                  getDefaultInterpolationDegree(), getParsedUnitsBehavior(), getFilters());
173     }
174 
175     /** Check if tidal effects are ignored when interpolating EOP.
176      * @return true if tidal effects are ignored when interpolating EOP
177      */
178     public boolean isSimpleEOP() {
179         return simpleEOP;
180     }
181 
182     /** Set up the gravitational coefficient.
183      * @param newMu gravitational coefficient
184      * @return a new builder with updated configuration (the instance is not changed)
185      */
186     public ParserBuilder withMu(final double newMu) {
187         return new ParserBuilder(getConventions(), getEquatorialRadius(), getFlattening(), getDataContext(),
188                                  getMissionReferenceDate(), getRangeUnitsConverter(), isSimpleEOP(), newMu, getDefaultMass(),
189                                  getDefaultInterpolationDegree(), getParsedUnitsBehavior(), getFilters());
190     }
191 
192     /** Get the gravitational coefficient.
193      * @return gravitational coefficient
194      */
195     public double getMu() {
196         return mu;
197     }
198 
199     /** Set up the default mass.
200      * <p>
201      * The default mass is used only by {@link OpmParser}.
202      * </p>
203      * @param newDefaultMass default mass
204      * @return a new builder with updated configuration (the instance is not changed)
205      */
206     public ParserBuilder withDefaultMass(final double newDefaultMass) {
207         return new ParserBuilder(getConventions(), getEquatorialRadius(), getFlattening(), getDataContext(),
208                                  getMissionReferenceDate(), getRangeUnitsConverter(), isSimpleEOP(), getMu(), newDefaultMass,
209                                  getDefaultInterpolationDegree(), getParsedUnitsBehavior(), getFilters());
210     }
211 
212     /** Get the default mass.
213      * @return default mass
214      */
215     public double getDefaultMass() {
216         return defaultMass;
217     }
218 
219     /** Set up the default interpolation degree.
220      * <p>
221      * The default interpolation degree is used only by {@link AemParser}
222      * and {@link OemParser}.
223      * </p>
224      * @param newDefaultInterpolationDegree default interpolation degree
225      * @return a new builder with updated configuration (the instance is not changed)
226      */
227     public ParserBuilder withDefaultInterpolationDegree(final int newDefaultInterpolationDegree) {
228         return new ParserBuilder(getConventions(), getEquatorialRadius(), getFlattening(), getDataContext(),
229                                  getMissionReferenceDate(), getRangeUnitsConverter(), isSimpleEOP(), getMu(), getDefaultMass(),
230                                  newDefaultInterpolationDegree, getParsedUnitsBehavior(), getFilters());
231     }
232 
233     /** Get the default interpolation degree.
234      * @return default interpolation degree
235      */
236     public int getDefaultInterpolationDegree() {
237         return defaultInterpolationDegree;
238     }
239 
240     /** Set up the behavior to adopt for handling parsed units.
241      * @param newParsedUnitsBehavior behavior to adopt for handling parsed units
242      * @return a new builder with updated configuration (the instance is not changed)
243      */
244     public ParserBuilder withParsedUnitsBehavior(final ParsedUnitsBehavior newParsedUnitsBehavior) {
245         return new ParserBuilder(getConventions(), getEquatorialRadius(), getFlattening(), getDataContext(),
246                                  getMissionReferenceDate(), getRangeUnitsConverter(), isSimpleEOP(), getMu(), getDefaultMass(),
247                                  getDefaultInterpolationDegree(), newParsedUnitsBehavior, getFilters());
248     }
249 
250     /** Get the behavior to adopt for handling parsed units.
251      * @return behavior to adopt for handling parsed units
252      */
253     public ParsedUnitsBehavior getParsedUnitsBehavior() {
254         return parsedUnitsBehavior;
255     }
256 
257     /** Add a filter for parsed tokens.
258      * <p>
259      * This filter allows to change parsed tokens. This method can be called several times,
260      * once for each filter to set up. The filters are always applied in the order they were set.
261      * There are several use cases for this feature.
262      * </p>
263      * <p>
264      * The first use case is to allow parsing malformed CCSDS messages with some known
265      * discrepancies that can be fixed. One real life example (the one that motivated the
266      * development of this feature) is OMM files in XML format that add an empty
267      * OBJECT_ID. This could be fixed by setting a filter as follows:
268      * </p>
269      * <pre>{@code
270      * Omm omm = new ParserBuilder().
271      *           withFilter(token -> {
272      *                          if ("OBJECT_ID".equals(token.getName()) &&
273      *                              (token.getRawContent() == null || token.getRawContent().isEmpty())) {
274      *                              // replace null/empty entries with "unknown"
275      *                              return Collections.singletonList(new ParseToken(token.getType(), token.getName(),
276      *                                                                              "unknown", token.getUnits(),
277      *                                                                              token.getLineNumber(), token.getFileName()));
278      *                          } else {
279      *                              return Collections.singletonList(token);
280      *                          }
281      *                     }).
282      *           buildOmmParser().
283      *           parseMessage(message);
284      * }</pre>
285      * <p>
286      * A second use case is to remove unwanted data. For example in order to remove all user-defined data
287      * one could use:
288      * </p>
289      * <pre>{@code
290      * Omm omm = new ParserBuilder().
291      *           withFilter(token -> {
292      *                          if (token.getName().startsWith("USER_DEFINED")) {
293      *                              return Collections.emptyList();
294      *                          } else {
295      *                              return Collections.singletonList(token);
296      *                          }
297      *                     }).
298      *           buildOmmmParser().
299      *           parseMessage(message);
300      * }</pre>
301      * <p>
302      * A third use case is to add data not originally present in the file. For example in order
303      * to add a generated ODM V3 message id to an ODM V2 message that lacks it, one could do:
304      * </p>
305      * <pre>{@code
306      * final String myMessageId = ...; // this could be computed from a counter, or a SHA256 digest, or some metadata
307      * Omm omm = new ParserBuilder()
308      *           withFilter(token -> {
309      *                          if ("CCSDS_OMM_VERS".equals(token.getName())) {
310      *                              // enforce ODM V3
311      *                              return Collections.singletonList(new ParseToken(token.getType(), token.getName(),
312      *                                                                              "3.0", token.getUnits(),
313      *                                                                              token.getLineNumber(), token.getFileName()));
314      *                          } else {
315      *                              return Collections.singletonList(token);
316      *                          }
317      *                      }).
318      *           withFilter(token -> {
319      *                          if ("ORIGINATOR".equals(token.getName())) {
320      *                              // add generated message ID after ORIGINATOR entry
321      *                              return Arrays.asList(token,
322      *                                                   new ParseToken(TokenType.ENTRY, "MESSAGE_ID",
323      *                                                                  myMessageId, null,
324      *                                                                  -1, token.getFileName()));
325      *                          } else {
326      *                              return Collections.singletonList(token);
327      *                          }
328      *                      }).
329      *           buildOmmmParser().
330      *           parseMessage(message);
331      * }</pre>
332      * @param filter token filter to add
333      * @return a new builder with updated configuration (the instance is not changed)
334      * @since 12.0
335      */
336     public ParserBuilder withFilter(final Function<ParseToken, List<ParseToken>> filter) {
337 
338         // populate new filters array
339         @SuppressWarnings("unchecked")
340         final Function<ParseToken, List<ParseToken>>[] newFilters =
341                         (Function<ParseToken, List<ParseToken>>[]) Array.newInstance(Function.class, filters.length + 1);
342         System.arraycopy(filters, 0, newFilters, 0, filters.length);
343         newFilters[filters.length] = filter;
344 
345         return new ParserBuilder(getConventions(), getEquatorialRadius(), getFlattening(), getDataContext(),
346                                  getMissionReferenceDate(), getRangeUnitsConverter(), isSimpleEOP(), getMu(), getDefaultMass(),
347                                  getDefaultInterpolationDegree(), getParsedUnitsBehavior(),
348                                  newFilters);
349 
350     }
351 
352     /** Get the filters to apply to parse tokens.
353      * @return filters to apply to parse tokens
354      * @since 12.0
355      */
356     public Function<ParseToken, List<ParseToken>>[] getFilters() {
357         return filters.clone();
358     }
359 
360     /** Build a parser for {@link org.orekit.files.ccsds.ndm.Ndm Navigation Data Messages}.
361      * @return a new parser
362      */
363     public NdmParser buildNdmParser() {
364         return new NdmParser(this, getFilters());
365     }
366 
367     /** Build a parser for {@link org.orekit.files.ccsds.ndm.odm.opm.Opm Orbit Parameters Messages}.
368      * @return a new parser
369      */
370     public OpmParser buildOpmParser() {
371         return new OpmParser(getConventions(), isSimpleEOP(), getDataContext(), getMissionReferenceDate(),
372                              getMu(), getDefaultMass(), getParsedUnitsBehavior(), getFilters());
373     }
374 
375     /** Build a parser for {@link org.orekit.files.ccsds.ndm.odm.omm.Omm Orbit Mean elements Messages}.
376      * @return a new parser
377      */
378     public OmmParser buildOmmParser() {
379         return new OmmParser(getConventions(), isSimpleEOP(), getDataContext(), getMissionReferenceDate(),
380                              getMu(), getDefaultMass(), getParsedUnitsBehavior(), getFilters());
381     }
382 
383     /** Build a parser for {@link org.orekit.files.ccsds.ndm.odm.oem.Oem Orbit Ephemeris Messages}.
384      * @return a new parser
385      */
386     public OemParser buildOemParser() {
387         return new OemParser(getConventions(), isSimpleEOP(), getDataContext(), getMissionReferenceDate(),
388                              getMu(), getDefaultInterpolationDegree(), getParsedUnitsBehavior(), getFilters());
389     }
390 
391     /** Build a parser for {@link org.orekit.files.ccsds.ndm.odm.ocm.Ocm Orbit Comprehensive Messages}.
392      * @return a new parser
393      */
394     public OcmParser buildOcmParser() {
395         return new OcmParser(getConventions(), getEquatorialRadius(), getFlattening(),
396                              isSimpleEOP(), getDataContext(), getMu(),
397                              getParsedUnitsBehavior(), getFilters());
398     }
399 
400     /** Build a parser for {@link org.orekit.files.ccsds.ndm.adm.apm.Apm Attitude Parameters Messages}.
401      * @return a new parser
402      */
403     public ApmParser buildApmParser() {
404         return new ApmParser(getConventions(), isSimpleEOP(), getDataContext(),
405                              getMissionReferenceDate(), getParsedUnitsBehavior(), getFilters());
406     }
407 
408     /** Build a parser for {@link org.orekit.files.ccsds.ndm.adm.aem.Aem Attitude Ephemeris Messages}.
409      * @return a new parser
410      */
411     public AemParser buildAemParser() {
412         return new AemParser(getConventions(), isSimpleEOP(), getDataContext(), getMissionReferenceDate(),
413                              getDefaultInterpolationDegree(), getParsedUnitsBehavior(), getFilters());
414     }
415 
416     /** Build a parser for {@link org.orekit.files.ccsds.ndm.adm.acm.Acm Attitude Comprehensive Messages}.
417      * @return a new parser
418      * @since 12.0
419      */
420     public AcmParser buildAcmParser() {
421         return new AcmParser(getConventions(), isSimpleEOP(), getDataContext(),
422                              getParsedUnitsBehavior(), getFilters());
423     }
424 
425     /** Build a parser for {@link org.orekit.files.ccsds.ndm.tdm.Tdm Tracking Data Messages}.
426      * @return a new parser
427      */
428     public TdmParser buildTdmParser() {
429         return new TdmParser(getConventions(), isSimpleEOP(), getDataContext(),
430                              getParsedUnitsBehavior(), getRangeUnitsConverter(), getFilters());
431     }
432 
433     /** Build a parser for {@link org.orekit.files.ccsds.ndm.cdm.Cdm Conjunction Data Messages}.
434      * @return a new parser
435      */
436     public CdmParser buildCdmParser() {
437         return new CdmParser(getConventions(), isSimpleEOP(), getDataContext(),
438                              getParsedUnitsBehavior(), getFilters());
439     }
440 
441 }