View Javadoc
1   /*
2    * Themis: Java Project Framework
3    * Copyright 2012-2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * 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, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  package io.github.tonywasher.joceanus.themis.lethe.analysis;
18  
19  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
20  import io.github.tonywasher.joceanus.themis.exc.ThemisDataException;
21  
22  import java.util.ArrayDeque;
23  import java.util.Deque;
24  
25  /**
26   * Scanner for headers and trailers.
27   */
28  public class ThemisAnalysisScanner {
29      /**
30       * Terminator processor.
31       */
32      @FunctionalInterface
33      private interface ThemisScannerTerminator {
34          /**
35           * Handle Terminator.
36           *
37           * @param pTerminator the terminator
38           * @throws OceanusException on error
39           */
40          void handle(char pTerminator) throws OceanusException;
41      }
42  
43      /**
44       * The source.
45       */
46      private final ThemisAnalysisSource theSource;
47  
48      /**
49       * The list of result lines.
50       */
51      private Deque<ThemisAnalysisElement> theResults;
52  
53      /**
54       * The current line.
55       */
56      private ThemisAnalysisLine theCurLine;
57  
58      /**
59       * The length of the current line.
60       */
61      private int theLength;
62  
63      /**
64       * The current position in the line.
65       */
66      private int theCurPos;
67  
68      /**
69       * Are we investigating a potential comment?
70       */
71      private boolean maybeComment;
72  
73      /**
74       * Should we skip generics?
75       */
76      private boolean skipGenerics;
77  
78      /**
79       * Constructor.
80       *
81       * @param pSource the source
82       */
83      ThemisAnalysisScanner(final ThemisAnalysisSource pSource) {
84          theSource = pSource;
85          theResults = new ArrayDeque<>();
86      }
87  
88      /**
89       * Set skip generics flag.
90       */
91      void skipGenerics() {
92          skipGenerics = true;
93      }
94  
95      /**
96       * Scan For Terminator.
97       *
98       * @param pLine       the current line
99       * @param pTerminator the terminator
100      * @return the results
101      * @throws OceanusException on error
102      */
103     Deque<ThemisAnalysisElement> scanForTerminator(final ThemisAnalysisLine pLine,
104                                                    final char pTerminator) throws OceanusException {
105         /* Initialise scan */
106         initialiseScan(pLine);
107 
108         /* Loop through the line */
109         while (true) {
110             /* If we have finished the line */
111             if (theCurPos == theLength) {
112                 /* Shift to the next line */
113                 shiftToNextLine(true);
114                 maybeComment = false;
115             }
116 
117             /* Access current character */
118             final char myChar = theCurLine.charAt(theCurPos);
119 
120             /* Check for trailing line comments */
121             if (checkComments(myChar)) {
122                 continue;
123             }
124 
125             /* Break loop if this is a terminator */
126             if (checkForTerminator(myChar, pTerminator, this::handleEOLTerminator)) {
127                 break;
128             }
129         }
130 
131         /* return the results */
132         return theResults;
133     }
134 
135     /**
136      * Scan For Separator.
137      *
138      * @param pSeparator the separator
139      * @return the results
140      * @throws OceanusException on error
141      */
142     Deque<ThemisAnalysisElement> scanForSeparator(final char pSeparator) throws OceanusException {
143         /* Initialise scan */
144         initialiseScan((ThemisAnalysisLine) theSource.popNextLine());
145 
146         /* Loop through the line */
147         while (true) {
148             /* If we have finished the line */
149             if (theCurPos == theLength) {
150                 /* Shift to the next line */
151                 if (shiftToNextLine(false)) {
152                     break;
153                 }
154                 maybeComment = false;
155             }
156 
157             /* Access current character */
158             final char myChar = theCurLine.charAt(theCurPos);
159 
160             /* Check for trailing line comments */
161             if (checkComments(myChar)) {
162                 continue;
163             }
164 
165             /* Break loop if this is a terminator */
166             if (checkForTerminator(myChar, pSeparator, this::handleSeparator)) {
167                 break;
168             }
169         }
170 
171         /* return the results */
172         return theResults;
173     }
174 
175     /**
176      * Check For Separator.
177      *
178      * @param pSeparator the separator
179      * @return the results
180      * @throws OceanusException on error
181      */
182     boolean checkForSeparator(final char pSeparator) throws OceanusException {
183         /* Initialise scan */
184         initialiseScan((ThemisAnalysisLine) theSource.popNextLine());
185 
186         /* Loop through the line */
187         while (true) {
188             /* If we have finished the line */
189             if (theCurPos == theLength) {
190                 /* Shift to the next line */
191                 if (shiftToNextLine(false)) {
192                     restoreStack(pSeparator);
193                     return false;
194                 }
195                 maybeComment = false;
196             }
197 
198             /* Access current character */
199             final char myChar = theCurLine.charAt(theCurPos);
200 
201             /* Check for trailing line comments */
202             if (checkComments(myChar)) {
203                 continue;
204             }
205 
206             /* Break loop if this is a terminator */
207             if (checkForTerminator(myChar, pSeparator, this::restoreStack)) {
208                 return true;
209             }
210         }
211     }
212 
213     /**
214      * Initialise scan.
215      *
216      * @param pLine the current line
217      */
218     private void initialiseScan(final ThemisAnalysisLine pLine) {
219         /* Clear the results array */
220         theResults.clear();
221 
222         /* Access line details */
223         theCurLine = pLine;
224         theLength = theCurLine.getLength();
225 
226         /* reset flags */
227         theCurPos = 0;
228         maybeComment = false;
229     }
230 
231     /**
232      * Check for comment.
233      *
234      * @param pChar the current character
235      * @return reloop true/false
236      * @throws OceanusException on error
237      */
238     private boolean checkComments(final char pChar) throws OceanusException {
239         /* If this is the comment character */
240         if (pChar == ThemisAnalysisChar.COMMENT) {
241             /* Flip flag */
242             maybeComment = !maybeComment;
243 
244             /* If we have double comment */
245             if (!maybeComment) {
246                 /* Strip trailing comments and re-loop */
247                 theCurLine.stripTrailingComments();
248                 theCurPos = theLength;
249                 return true;
250             }
251         } else {
252             maybeComment = false;
253         }
254 
255         /* Continue */
256         return false;
257     }
258 
259     /**
260      * Scan For Terminator.
261      *
262      * @param pChar       the current character
263      * @param pTerminator the terminator
264      * @param pHandler    the handler
265      * @return terminator found true/false
266      * @throws OceanusException on error
267      */
268     private boolean checkForTerminator(final char pChar,
269                                        final char pTerminator,
270                                        final ThemisScannerTerminator pHandler) throws OceanusException {
271         /* If this is a single/double quote */
272         if (pChar == ThemisAnalysisChar.SINGLEQUOTE
273                 || pChar == ThemisAnalysisChar.DOUBLEQUOTE) {
274             /* Find the end of the sequence and skip the quotes */
275             final int myEnd = theCurLine.findEndOfQuotedSequence(theCurPos);
276             theCurPos = myEnd + 1;
277 
278             /* If we have found the terminator */
279         } else if (pChar == pTerminator) {
280             /* Handle the terminator */
281             pHandler.handle(pTerminator);
282             return true;
283 
284             /* If this is a parenthesisOpen character */
285         } else if (pChar == ThemisAnalysisChar.PARENTHESIS_OPEN) {
286             /* Handle the nested sequence */
287             handleNestedSequence(ThemisAnalysisChar.PARENTHESIS_CLOSE);
288 
289             /* If this is a braceOpen character */
290         } else if (pChar == ThemisAnalysisChar.BRACE_OPEN) {
291             /* Handle the nested sequence */
292             handleNestedSequence(ThemisAnalysisChar.BRACE_CLOSE);
293 
294             /* If we should skip Generics and this is a genericOpen character */
295         } else if (skipGenerics && pChar == ThemisAnalysisChar.GENERIC_OPEN) {
296             /* Handle the nested sequence */
297             handleNestedSequence(ThemisAnalysisChar.GENERIC_CLOSE);
298 
299             /* else move to next character */
300         } else {
301             /* Increment position */
302             theCurPos++;
303         }
304 
305         /* not found */
306         return false;
307     }
308 
309     /**
310      * Handle Terminator at End of Line.
311      *
312      * @param pTerminator the terminator
313      * @throws OceanusException on error
314      */
315     private void handleEOLTerminator(final char pTerminator) throws OceanusException {
316         /* Must be at the end of the line */
317         if (theCurPos != theLength - 1) {
318             throw new ThemisDataException("Not at end of line");
319         }
320 
321         /* Strip end character, add to results and break loop */
322         theCurLine.stripEndChar(pTerminator);
323         theResults.add(theCurLine);
324     }
325 
326     /**
327      * Handle Separator (possible mid-line).
328      *
329      * @param pTerminator the terminator
330      */
331     private void handleSeparator(final char pTerminator) {
332         /* If separator is not at end of line */
333         if (theCurPos != theLength - 1) {
334             /* Strip to end character and add to results */
335             final ThemisAnalysisLine myLine = theCurLine.stripUpToPosition(theCurPos - 1);
336             theResults.add(myLine);
337 
338             /* Return a non-blank line to the stack and break loop */
339             theCurLine.stripStartChar(pTerminator);
340             if (!ThemisAnalysisBlank.isBlank(theCurLine)) {
341                 theSource.pushLine(theCurLine);
342             }
343 
344             /* else terminator is at end of line */
345         } else {
346             /* Strip end character, add to results and break loop */
347             theCurLine.stripEndChar(pTerminator);
348             theResults.add(theCurLine);
349         }
350     }
351 
352     /**
353      * Restore the stack.
354      *
355      * @param pTerminator the terminator
356      */
357     private void restoreStack(final char pTerminator) {
358         /* Restore current line */
359         if (theCurLine != null) {
360             theSource.pushLine(theCurLine);
361         }
362 
363         /* Loop through the results */
364         while (!theResults.isEmpty()) {
365             theSource.pushLine(theResults.removeLast());
366         }
367     }
368 
369     /**
370      * Scan For Generic Terminator.
371      *
372      * @param pLine the current line
373      * @return the results
374      * @throws OceanusException on error
375      */
376     Deque<ThemisAnalysisElement> scanForGeneric(final ThemisAnalysisLine pLine) throws OceanusException {
377         return scanForContents(pLine, ThemisAnalysisChar.GENERIC_CLOSE);
378     }
379 
380     /**
381      * Scan For Parenthesis Terminator.
382      *
383      * @param pLine the current line
384      * @return the results
385      * @throws OceanusException on error
386      */
387     Deque<ThemisAnalysisElement> scanForParenthesis(final ThemisAnalysisLine pLine) throws OceanusException {
388         return scanForContents(pLine, ThemisAnalysisChar.PARENTHESIS_CLOSE);
389     }
390 
391     /**
392      * Scan For Array Terminator.
393      *
394      * @param pLine the current line
395      * @return the results
396      * @throws OceanusException on error
397      */
398     Deque<ThemisAnalysisElement> scanForArray(final ThemisAnalysisLine pLine) throws OceanusException {
399         return scanForContents(pLine, ThemisAnalysisChar.ARRAY_CLOSE);
400     }
401 
402     /**
403      * Scan For Terminator across multiple lines.
404      *
405      * @param pLine       the current line
406      * @param pTerminator the terminator
407      * @return the results
408      * @throws OceanusException on error
409      */
410     private Deque<ThemisAnalysisElement> scanForContents(final ThemisAnalysisLine pLine,
411                                                          final char pTerminator) throws OceanusException {
412         /* Allocate array */
413         theResults = new ArrayDeque<>();
414 
415         /* Access line details */
416         theCurLine = pLine;
417         theLength = theCurLine.getLength();
418         theCurPos = 0;
419 
420         /* Locate the end of the sequence */
421         handleNestedSequence(pTerminator);
422 
423         /* Strip to end character and add to results */
424         final ThemisAnalysisLine myLine = theCurLine.stripUpToPosition(theCurPos - 1);
425         theResults.add(myLine);
426 
427         /* Return a non-blank line to the stack and break loop */
428         if (!ThemisAnalysisBlank.isBlank(theCurLine)) {
429             theSource.pushLine(theCurLine);
430         }
431 
432         /* Return the results */
433         return theResults;
434     }
435 
436     /**
437      * Shift to next line.
438      *
439      * @param pErrorOnEmpty throw exception on empty
440      * @return empty true/false
441      * @throws OceanusException on error
442      */
443     private boolean shiftToNextLine(final boolean pErrorOnEmpty) throws OceanusException {
444         /* Add current line to the results */
445         theResults.add(theCurLine);
446         theCurLine = null;
447 
448         /* Check for additional lines */
449         if (!theSource.hasLines()) {
450             /* Throw exception if required, else return empty */
451             if (pErrorOnEmpty) {
452                 throw new ThemisDataException("Did not find terminator");
453             }
454             return true;
455         }
456 
457         /* Access the next line */
458         final ThemisAnalysisElement myEl = theSource.popNextLine();
459         theCurLine = (ThemisAnalysisLine) myEl;
460         theLength = theCurLine.getLength();
461         theCurPos = 0;
462 
463         /* Handle empty lines */
464         return theLength == 0 && shiftToNextLine(pErrorOnEmpty);
465     }
466 
467     /**
468      * Handle nested sequence.
469      *
470      * @param pNestEnd the nest end character
471      * @throws OceanusException on error
472      */
473     private void handleNestedSequence(final char pNestEnd) throws OceanusException {
474         /* Access the nest start character */
475         final char myNestStart = theCurLine.charAt(theCurPos);
476 
477         /* Look for end of nest in this line */
478         int myNested = theCurLine.findEndOfNestedSequence(theCurPos, 0, pNestEnd, myNestStart);
479 
480         /* While the end of the nest is not found */
481         while (myNested < 0) {
482             /* Shift to the next line */
483             shiftToNextLine(true);
484 
485             /* Repeat test for end of nest */
486             myNested = theCurLine.findEndOfNestedSequence(0, myNested, pNestEnd, myNestStart);
487         }
488 
489         /* Adjust position */
490         theCurPos = myNested + 1;
491     }
492 
493     /**
494      * Scanner Source.
495      */
496     public interface ThemisAnalysisSource {
497         /**
498          * Are there more lines to process?
499          *
500          * @return true/false
501          */
502         boolean hasLines();
503 
504         /**
505          * Pop next line from list.
506          *
507          * @return the next line
508          * @throws OceanusException on error
509          */
510         ThemisAnalysisElement popNextLine() throws OceanusException;
511 
512         /**
513          * Push line back onto stack.
514          *
515          * @param pLine to line to push onto stack
516          */
517         void pushLine(ThemisAnalysisElement pLine);
518     }
519 }