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 }