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.utils.parsing;
18  
19  import java.io.IOException;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.function.Function;
26  import org.hipparchus.exception.LocalizedCoreFormats;
27  import org.orekit.data.DataSource;
28  import org.orekit.errors.OrekitException;
29  import org.orekit.errors.OrekitInternalError;
30  import org.orekit.files.ccsds.utils.FileFormat;
31  import org.orekit.files.ccsds.utils.lexical.LexicalAnalyzerSelector;
32  import org.orekit.files.ccsds.utils.lexical.MessageParser;
33  import org.orekit.files.ccsds.utils.lexical.MessageVersionXmlTokenBuilder;
34  import org.orekit.files.ccsds.utils.lexical.ParseToken;
35  import org.orekit.files.ccsds.utils.lexical.TokenType;
36  import org.orekit.files.ccsds.utils.lexical.XmlTokenBuilder;
37  
38  /** Parser for CCSDS messages.
39   * <p>
40   * Note than starting with Orekit 11.0, CCSDS message parsers are
41   * mutable objects that gather the data being parsed, until the
42   * message is complete and the {@link #parseMessage(org.orekit.data.DataSource)
43   * parseMessage} method has returned. This implies that parsers
44   * should <em>not</em> be used in a multi-thread context. The recommended
45   * way to use parsers is to either dedicate one parser for each message
46   * and drop it afterwards, or to use a single-thread loop.
47   * </p>
48   * @param <T> type of the file
49   * @author Luc Maisonobe
50   * @since 11.0
51   */
52  public abstract class AbstractMessageParser<T> implements MessageParser<T> {
53  
54      /** Comment key.
55       * @since 12.0
56       */
57      private static final String COMMENT = "COMMENT";
58  
59      /** Safety limit for loop over processing states. */
60      private static final int MAX_LOOP = 100;
61  
62      /** Root element for XML files. */
63      private final String root;
64  
65      /** Key for format version. */
66      private final String formatVersionKey;
67  
68      /** Filters for parse tokens. */
69      private final Function<ParseToken, List<ParseToken>>[] filters;
70  
71      /** Anticipated next processing state. */
72      private ProcessingState next;
73  
74      /** Current processing state. */
75      private ProcessingState current;
76  
77      /** Fallback processing state. */
78      private ProcessingState fallback;
79  
80      /** Format of the file ready to be parsed. */
81      private FileFormat format;
82  
83      /** Flag for XML end tag. */
84      private boolean endTagSeen;
85  
86      /** Simple constructor.
87       * @param root root element for XML files
88       * @param formatVersionKey key for format version
89       * @param filters filters to apply to parse tokens
90       * @since 12.0
91       */
92      protected AbstractMessageParser(final String root, final String formatVersionKey,
93                                      final Function<ParseToken, List<ParseToken>>[] filters) {
94          this.root             = root;
95          this.formatVersionKey = formatVersionKey;
96          this.filters          = filters.clone();
97          this.current          = null;
98          setFallback(new ErrorState());
99      }
100 
101     /** Set fallback processing state.
102      * <p>
103      * The fallback processing state is used if anticipated state fails
104      * to parse the token.
105      * </p>
106      * @param fallback processing state to use if anticipated state does not work
107      */
108     public void setFallback(final ProcessingState fallback) {
109         this.fallback = fallback;
110     }
111 
112     /** Reset parser to initial state before parsing.
113      * @param fileFormat format of the file ready to be parsed
114      * @param initialState initial processing state
115      */
116     protected void reset(final FileFormat fileFormat, final ProcessingState initialState) {
117         format     = fileFormat;
118         current    = initialState;
119         endTagSeen = false;
120         anticipateNext(fallback);
121     }
122 
123     /** Set the flag for XML end tag.
124      * @param endTagSeen if true, the XML end tag has been seen
125      */
126     public void setEndTagSeen(final boolean endTagSeen) {
127         this.endTagSeen = endTagSeen;
128     }
129 
130     /** Check if XML end tag has been seen.
131      * @return true if XML end tag has been seen
132      */
133     public boolean wasEndTagSeen() {
134         return endTagSeen;
135     }
136 
137     /** Get the current processing state.
138      * @return current processing state
139      */
140     public ProcessingState getCurrent() {
141         return current;
142     }
143 
144     /** {@inheritDoc} */
145     @Override
146     public FileFormat getFileFormat() {
147         return format;
148     }
149 
150     /** {@inheritDoc} */
151     @Override
152     public T parseMessage(final DataSource source) {
153         try {
154             return LexicalAnalyzerSelector.select(source).accept(this);
155         } catch (IOException ioe) {
156             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE,
157                                       ioe.getLocalizedMessage());
158         }
159     }
160 
161     /** {@inheritDoc} */
162     @Override
163     public String getFormatVersionKey() {
164         return formatVersionKey;
165     }
166 
167     /** {@inheritDoc} */
168     @Override
169     public Map<String, XmlTokenBuilder> getSpecialXmlElementsBuilders() {
170 
171         final HashMap<String, XmlTokenBuilder> builders = new HashMap<>();
172 
173         if (formatVersionKey != null) {
174             // special handling of root tag that contains the format version
175             builders.put(root, new MessageVersionXmlTokenBuilder());
176         }
177 
178         return builders;
179 
180     }
181 
182     /** Anticipate what next processing state should be.
183      * @param anticipated anticipated next processing state
184      */
185     public void anticipateNext(final ProcessingState anticipated) {
186         this.next = anticipated;
187     }
188 
189     /** {@inheritDoc} */
190     @Override
191     public void process(final ParseToken token) {
192 
193         // loop over the filters
194         List<ParseToken> filtered = Collections.singletonList(token);
195         for (Function<ParseToken, List<ParseToken>> filter : filters) {
196             final ArrayList<ParseToken> newFiltered = new ArrayList<>();
197             for (final ParseToken original : filtered) {
198                 newFiltered.addAll(filter.apply(original));
199             }
200             filtered = newFiltered;
201         }
202         if (filtered.isEmpty()) {
203             return;
204         }
205 
206         int remaining = filtered.size();
207         for (final ParseToken filteredToken : filtered) {
208 
209             if (filteredToken.getType() == TokenType.ENTRY &&
210                 !COMMENT.equals(filteredToken.getName()) &&
211                 (filteredToken.getRawContent() == null || filteredToken.getRawContent().isEmpty())) {
212                 // value is empty, token is ignored
213                 if (--remaining == 0) {
214                     // we have processed all filtered tokens
215                     return;
216                 }
217             }
218             else {
219                 // loop over the various states until one really processes the token
220                 for (int i = 0; i < MAX_LOOP; ++i) {
221                     if (current.processToken(filteredToken)) {
222                         // filtered token was properly processed
223                         if (--remaining == 0) {
224                             // we have processed all filtered tokens
225                             return;
226                         }
227                         else {
228                             // we need to continue processing the remaining filtered tokens
229                             break;
230                         }
231                     }
232                     else {
233                         // filtered token was not processed by current processing state, switch to next one
234                         current = next;
235                         next = fallback;
236                     }
237                 }
238             }
239 
240         }
241 
242         // this should never happen
243         throw new OrekitInternalError(null);
244 
245     }
246 
247 }