1 /*
2  * Copyright (C) 2010 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.clearsilver.jsilver.data;
18 
19 import com.google.clearsilver.jsilver.resourceloader.ResourceLoader;
20 
21 import java.io.IOException;
22 import java.io.LineNumberReader;
23 import java.io.Reader;
24 import java.util.ArrayList;
25 import java.util.Iterator;
26 import java.util.Stack;
27 
28 /**
29  * Parser for HDF based on the following grammar by Brandon Long.
30  *
31  * COMMAND := (INCLUDE | COMMENT | HDF_SET | HDF_DESCEND | HDF_ASCEND ) INCLUDE := #include
32  * "FILENAME" EOL COMMENT := # .* EOL HDF_DESCEND := HDF_NAME_ATTRS { EOL HDF_ASCEND := } EOL
33  * HDF_SET := (HDF_ASSIGN | HDF_MULTILINE_ASSIGN | HDF_COPY | HDF_LINK) HDF_ASSIGN := HDF_NAME_ATTRS
34  * = .* EOL HDF_MULTILINE_ASSIGN := HDF_NAME_ATTRS << EOM_MARKER EOL (.* EOL)* EOM_MARKER EOL
35  * HDF_COPY := HDF_NAME_ATTRS := HDF_NAME EOL HDF_LINK := HDF_NAME_ATTRS : HDF_NAME EOL
36  * HDF_NAME_ATTRS := (HDF_NAME | HDF_NAME [HDF_ATTRS]) HDF_ATTRS := (HDF_ATTR | HDF_ATTR, HDF_ATTRS)
37  * HDF_ATTR := (HDF_ATTR_KEY | HDF_ATTR_KEY = [^\s,\]]+ | HDF_ATTR_KEY = DQUOTED_STRING)
38  * HDF_ATTR_KEY := [0-9a-zA-Z]+ DQUOTED_STRING := "([^\\"]|\\[ntr]|\\.)*" HDF_NAME := (HDF_SUB_NAME
39  * | HDF_SUB_NAME\.HDF_NAME) HDF_SUB_NAME := [0-9a-zA-Z_]+ EOM_MARKER := \S.*\S EOL := \n
40  */
41 public class NewHdfParser implements Parser {
42 
43   private final StringInternStrategy internStrategy;
44 
45   /**
46    * Special exception used to detect when we unexpectedly run out of characters on the line.
47    */
48   private static class OutOfCharsException extends Exception {}
49 
50   /**
51    * Object used to hold the name and attributes of an HDF node before we are ready to commit it to
52    * the Data object.
53    */
54   private static class HdfNameAttrs {
55     String name;
56     ArrayList<String> attrs = null;
57     int endOfSequence;
58 
reset(String newname)59     void reset(String newname) {
60       // TODO: think about moving interning here instead of parser code
61       this.name = newname;
62       if (attrs != null) {
63         attrs.clear();
64       }
65       endOfSequence = 0;
66     }
67 
addAttribute(String key, String value)68     void addAttribute(String key, String value) {
69       if (attrs == null) {
70         attrs = new ArrayList<String>(10);
71       }
72       attrs.ensureCapacity(attrs.size() + 2);
73       // TODO: think about moving interning here instead of parser code
74       attrs.add(key);
75       attrs.add(value);
76     }
77 
toData(Data data)78     Data toData(Data data) {
79       Data child = data.createChild(name);
80       if (attrs != null) {
81         Iterator<String> it = attrs.iterator();
82         while (it.hasNext()) {
83           String key = it.next();
84           String value = it.next();
85           child.setAttribute(key, value);
86         }
87       }
88       return child;
89     }
90   }
91 
92   static final String UNNAMED_INPUT = "[UNNAMED_INPUT]";
93 
94   /**
95    * State information that we pass through the parse methods. Allows parser to be reentrant as all
96    * the state is passed through method calls.
97    */
98   static class ParseState {
99     final Stack<Data> context = new Stack<Data>();
100     final Data output;
101     final LineNumberReader lineReader;
102     final ErrorHandler errorHandler;
103     final ResourceLoader resourceLoader;
104     final NewHdfParser hdfParser;
105     final boolean ignoreAttributes;
106     final HdfNameAttrs hdfNameAttrs;
107     final UniqueStack<String> includeStack;
108     final String parsedFileName;
109 
110     String line;
111     Data currentNode;
112 
ParseState(Data output, LineNumberReader lineReader, ErrorHandler errorHandler, ResourceLoader resourceLoader, NewHdfParser hdfParser, String parsedFileName, boolean ignoreAttributes, HdfNameAttrs hdfNameAttrs, UniqueStack<String> includeStack)113     private ParseState(Data output, LineNumberReader lineReader, ErrorHandler errorHandler,
114         ResourceLoader resourceLoader, NewHdfParser hdfParser, String parsedFileName,
115         boolean ignoreAttributes, HdfNameAttrs hdfNameAttrs, UniqueStack<String> includeStack) {
116       this.lineReader = lineReader;
117       this.errorHandler = errorHandler;
118       this.output = output;
119       currentNode = output;
120       this.resourceLoader = resourceLoader;
121       this.hdfParser = hdfParser;
122       this.parsedFileName = parsedFileName;
123       this.ignoreAttributes = ignoreAttributes;
124       this.hdfNameAttrs = hdfNameAttrs;
125       this.includeStack = includeStack;
126     }
127 
createNewParseState(Data output, Reader reader, ErrorHandler errorHandler, ResourceLoader resourceLoader, NewHdfParser hdfParser, String parsedFileName, boolean ignoreAttributes)128     public static ParseState createNewParseState(Data output, Reader reader,
129         ErrorHandler errorHandler, ResourceLoader resourceLoader, NewHdfParser hdfParser,
130         String parsedFileName, boolean ignoreAttributes) {
131 
132       if (parsedFileName == null) {
133         parsedFileName = UNNAMED_INPUT;
134       }
135       UniqueStack<String> includeStack = new UniqueStack<String>();
136       includeStack.push(parsedFileName);
137 
138       return new ParseState(output, new LineNumberReader(reader), errorHandler, resourceLoader,
139           hdfParser, parsedFileName, ignoreAttributes, new HdfNameAttrs(), includeStack);
140     }
141 
createParseStateForIncludedFile(ParseState originalState, String includeFileName, Reader includeFileReader)142     public static ParseState createParseStateForIncludedFile(ParseState originalState,
143         String includeFileName, Reader includeFileReader) {
144       return new ParseState(originalState.output, new LineNumberReader(includeFileReader),
145           originalState.errorHandler, originalState.resourceLoader, originalState.hdfParser,
146           originalState.parsedFileName, originalState.ignoreAttributes, new HdfNameAttrs(),
147           originalState.includeStack);
148     }
149   }
150 
151 
152   /**
153    * Constructor for {@link NewHdfParser}.
154    *
155    * @param internPool - {@link StringInternStrategy} instance used to optimize the HDF parsing.
156    */
NewHdfParser(StringInternStrategy internPool)157   public NewHdfParser(StringInternStrategy internPool) {
158     this.internStrategy = internPool;
159   }
160 
161   private static class NewHdfParserFactory implements ParserFactory {
162     private final StringInternStrategy stringInternStrategy;
163 
NewHdfParserFactory(StringInternStrategy stringInternStrategy)164     public NewHdfParserFactory(StringInternStrategy stringInternStrategy) {
165       this.stringInternStrategy = stringInternStrategy;
166     }
167 
168     @Override
newInstance()169     public Parser newInstance() {
170       return new NewHdfParser(stringInternStrategy);
171     }
172   }
173 
174   /**
175    * Creates a {@link ParserFactory} instance.
176    *
177    * <p>
178    * Provided {@code stringInternStrategy} instance will be used by shared all {@link Parser}
179    * objects created by the factory and used to optimize the HDF parsing process by reusing the
180    * String for keys and values.
181    *
182    * @param stringInternStrategy - {@link StringInternStrategy} instance used to optimize the HDF
183    *        parsing.
184    * @return an instance of {@link ParserFactory} implementation.
185    */
newFactory(StringInternStrategy stringInternStrategy)186   public static ParserFactory newFactory(StringInternStrategy stringInternStrategy) {
187     return new NewHdfParserFactory(stringInternStrategy);
188   }
189 
parse(Reader reader, Data output, Parser.ErrorHandler errorHandler, ResourceLoader resourceLoader, String dataFileName, boolean ignoreAttributes)190   public void parse(Reader reader, Data output, Parser.ErrorHandler errorHandler,
191       ResourceLoader resourceLoader, String dataFileName, boolean ignoreAttributes)
192       throws IOException {
193 
194     parse(ParseState.createNewParseState(output, reader, errorHandler, resourceLoader, this,
195         dataFileName, ignoreAttributes));
196   }
197 
parse(ParseState state)198   private void parse(ParseState state) throws IOException {
199     while ((state.line = state.lineReader.readLine()) != null) {
200       String seq = stripWhitespace(state.line);
201       try {
202         parseCommand(seq, state);
203       } catch (OutOfCharsException e) {
204         reportError(state, "End of line was prematurely reached. Parse error.");
205       }
206     }
207   }
208 
209   private static final String INCLUDE_WS = "#include ";
210 
parseCommand(String seq, ParseState state)211   private void parseCommand(String seq, ParseState state) throws IOException, OutOfCharsException {
212     if (seq.length() == 0) {
213       // Empty line.
214       return;
215     }
216     if (charAt(seq, 0) == '#') {
217       // If there isn't a match on include then this is a comment and we do nothing.
218       if (matches(seq, 0, INCLUDE_WS)) {
219         // This is an include command
220         int start = skipLeadingWhitespace(seq, INCLUDE_WS.length());
221         parseInclude(seq, start, state);
222       }
223       return;
224     } else if (charAt(seq, 0) == '}') {
225       if (skipLeadingWhitespace(seq, 1) != seq.length()) {
226         reportError(state, "Extra chars after '}'");
227         return;
228       }
229       handleAscend(state);
230     } else {
231       parseHdfElement(seq, state);
232     }
233   }
234 
parseInclude(String seq, int start, ParseState state)235   private void parseInclude(String seq, int start, ParseState state) throws IOException,
236       OutOfCharsException {
237     int end = seq.length();
238     if (charAt(seq, start) == '"') {
239       if (charAt(seq, end - 1) == '"') {
240         start++;
241         end--;
242       } else {
243         reportError(state, "Missing '\"' at end of include");
244         return;
245       }
246     }
247     handleInclude(seq.substring(start, end), state);
248   }
249 
250   private static final int NO_MATCH = -1;
251 
parseHdfElement(String seq, ParseState state)252   private void parseHdfElement(String seq, ParseState state) throws IOException,
253       OutOfCharsException {
254     // Re-use a single element to avoid repeated allocations/trashing (serious
255     // performance impact, 5% of real service performance)
256     HdfNameAttrs element = state.hdfNameAttrs;
257     if (!parseHdfNameAttrs(element, seq, 0, state)) {
258       return;
259     }
260     int index = skipLeadingWhitespace(seq, element.endOfSequence);
261     switch (charAt(seq, index)) {
262       case '{':
263         // Descend
264         if (index + 1 != seq.length()) {
265           reportError(state, "No characters expected after '{'");
266           return;
267         }
268         handleDescend(state, element);
269         return;
270       case '=':
271         // Assignment
272         index = skipLeadingWhitespace(seq, index + 1);
273         String value = internStrategy.intern(seq.substring(index, seq.length()));
274         handleAssign(state, element, value);
275         return;
276       case ':':
277         if (charAt(seq, index + 1) == '=') {
278           // Copy
279           index = skipLeadingWhitespace(seq, index + 2);
280           String src = parseHdfName(seq, index);
281           if (src == null) {
282             reportError(state, "Invalid HDF name");
283             return;
284           }
285           if (index + src.length() != seq.length()) {
286             reportError(state, "No characters expected after '{'");
287             return;
288           }
289           handleCopy(state, element, src);
290         } else {
291           // Link
292           index = skipLeadingWhitespace(seq, index + 1);
293           String src = parseHdfName(seq, index);
294           if (src == null) {
295             reportError(state, "Invalid HDF name");
296             return;
297           }
298           if (index + src.length() != seq.length()) {
299             reportError(state, "No characters expected after '{'");
300             return;
301           }
302           handleLink(state, element, src);
303         }
304         return;
305       case '<':
306         if (charAt(seq, index + 1) != '<') {
307           reportError(state, "Expected '<<'");
308         }
309         index = skipLeadingWhitespace(seq, index + 2);
310         String eomMarker = seq.substring(index, seq.length());
311         // TODO: think about moving interning to handleAssign()
312         String multilineValue = internStrategy.intern(parseMultilineValue(state, eomMarker));
313         if (multilineValue == null) {
314           return;
315         }
316         handleAssign(state, element, multilineValue);
317         return;
318       default:
319         reportError(state, "No valid operator");
320         return;
321     }
322   }
323 
324   /**
325    * This method parses out an HDF element name and any optional attributes into a caller-supplied
326    * HdfNameAttrs object. It returns a {@code boolean} with whether it succeeded to parse.
327    */
parseHdfNameAttrs(HdfNameAttrs destination, String seq, int index, ParseState state)328   private boolean parseHdfNameAttrs(HdfNameAttrs destination, String seq, int index,
329       ParseState state) throws OutOfCharsException {
330     String hdfName = parseHdfName(seq, index);
331     if (hdfName == null) {
332       reportError(state, "Invalid HDF name");
333       return false;
334     }
335     destination.reset(hdfName);
336     index = skipLeadingWhitespace(seq, index + hdfName.length());
337     int end = parseAttributes(seq, index, state, destination);
338     if (end == NO_MATCH) {
339       // Error already reported below.
340       return false;
341     } else {
342       destination.endOfSequence = end;
343       return true;
344     }
345   }
346 
347   /**
348    * Parses a valid hdf path name.
349    */
parseHdfName(String seq, int index)350   private String parseHdfName(String seq, int index) throws OutOfCharsException {
351     int end = index;
352     while (end < seq.length() && isHdfNameChar(charAt(seq, end))) {
353       end++;
354     }
355     if (end == index) {
356       return null;
357     }
358     return internStrategy.intern(seq.substring(index, end));
359   }
360 
361   /**
362    * Looks for optional attributes and adds them to the HdfNameAttrs object passed into the method.
363    */
parseAttributes(String seq, int index, ParseState state, HdfNameAttrs element)364   private int parseAttributes(String seq, int index, ParseState state, HdfNameAttrs element)
365       throws OutOfCharsException {
366     if (charAt(seq, index) != '[') {
367       // No attributes to parse
368       return index;
369     }
370     index = skipLeadingWhitespace(seq, index + 1);
371 
372     // If we don't care about attributes, just skip over them.
373     if (state.ignoreAttributes) {
374       while (charAt(seq, index) != ']') {
375         index++;
376       }
377       return index + 1;
378     }
379 
380     boolean first = true;
381     do {
382       if (first) {
383         first = false;
384       } else if (charAt(seq, index) == ',') {
385         index = skipLeadingWhitespace(seq, index + 1);
386       } else {
387         reportError(state, "Error parsing attribute list");
388       }
389       index = parseAttribute(seq, index, state, element);
390       if (index == NO_MATCH) {
391         // reportError called by parseAttribute already.
392         return NO_MATCH;
393       }
394       index = skipLeadingWhitespace(seq, index);
395     } while (charAt(seq, index) != ']');
396     return index + 1;
397   }
398 
399   private static final String DEFAULT_ATTR_VALUE = "1";
400 
401   /**
402    * Parse out a single HDF attribute. If there is no explicit value, use default value of "1" like
403    * in C clearsilver. Returns NO_MATCH if it fails to parse an attribute.
404    */
parseAttribute(String seq, int index, ParseState state, HdfNameAttrs element)405   private int parseAttribute(String seq, int index, ParseState state, HdfNameAttrs element)
406       throws OutOfCharsException {
407     int end = parseAttributeKey(seq, index);
408     if (index == end) {
409       reportError(state, "No valid attribute key");
410       return NO_MATCH;
411     }
412     String attrKey = internStrategy.intern(seq.substring(index, end));
413     index = skipLeadingWhitespace(seq, end);
414     if (charAt(seq, index) != '=') {
415       // No value for this attribute key. Use default value of "1"
416       element.addAttribute(attrKey, DEFAULT_ATTR_VALUE);
417       return index;
418     }
419     // We need to parse out the attribute value.
420     index = skipLeadingWhitespace(seq, index + 1);
421     if (charAt(seq, index) == '"') {
422       index++;
423       StringBuilder sb = new StringBuilder();
424       end = parseQuotedAttributeValue(seq, index, sb);
425       if (end == NO_MATCH) {
426         reportError(state, "Unable to parse quoted attribute value");
427         return NO_MATCH;
428       }
429       String attrValue = internStrategy.intern(sb.toString());
430       element.addAttribute(attrKey, attrValue);
431       end++;
432     } else {
433       // Simple attribute that has no whitespace.
434       String attrValue = parseAttributeValue(seq, index, state);
435       if (attrValue == null || attrValue.length() == 0) {
436         reportError(state, "No attribute for key " + attrKey);
437         return NO_MATCH;
438       }
439 
440       attrValue = internStrategy.intern(attrValue);
441       element.addAttribute(attrKey, attrValue);
442       end = index + attrValue.length();
443     }
444     return end;
445   }
446 
447   /**
448    * Returns the range in the sequence starting at start that corresponds to a valid attribute key.
449    */
parseAttributeKey(String seq, int index)450   private int parseAttributeKey(String seq, int index) throws OutOfCharsException {
451     while (isAlphaNumericChar(charAt(seq, index))) {
452       index++;
453     }
454     return index;
455   }
456 
457   /**
458    * Parses a quoted attribute value. Unescapes octal characters and \n, \r, \t, \", etc.
459    */
parseQuotedAttributeValue(String seq, int index, StringBuilder sb)460   private int parseQuotedAttributeValue(String seq, int index, StringBuilder sb)
461       throws OutOfCharsException {
462     char c;
463     while ((c = charAt(seq, index)) != '"') {
464       if (c == '\\') {
465         // Escaped character. Look for 1 to 3 digits in a row as octal or n,t,r.
466         index++;
467         char next = charAt(seq, index);
468         if (isNumericChar(next)) {
469           // Parse the next 1 to 3 characters if they are digits. Treat it as an octal code.
470           int val = next - '0';
471           if (isNumericChar(charAt(seq, index + 1))) {
472             index++;
473             val = val * 8 + (charAt(seq, index) - '0');
474             if (isNumericChar(charAt(seq, index + 1))) {
475               index++;
476               val = val * 8 + (charAt(seq, index) - '0');
477             }
478           }
479           c = (char) val;
480         } else if (next == 'n') {
481           c = '\n';
482         } else if (next == 't') {
483           c = '\t';
484         } else if (next == 'r') {
485           c = '\r';
486         } else {
487           // Regular escaped char like " or /
488           c = next;
489         }
490       }
491       sb.append(c);
492       index++;
493     }
494     return index;
495   }
496 
497   /**
498    * Parses a simple attribute value that cannot have any whitespace or specific punctuation
499    * reserved by the HDF grammar.
500    */
parseAttributeValue(String seq, int index, ParseState state)501   private String parseAttributeValue(String seq, int index, ParseState state)
502       throws OutOfCharsException {
503     int end = index;
504     char c = charAt(seq, end);
505     while (c != ',' && c != ']' && c != '"' && !Character.isWhitespace(c)) {
506       end++;
507       c = charAt(seq, end);
508     }
509     return seq.substring(index, end);
510   }
511 
parseMultilineValue(ParseState state, String eomMarker)512   private String parseMultilineValue(ParseState state, String eomMarker) throws IOException {
513     StringBuilder sb = new StringBuilder(256);
514     String line;
515     while ((line = state.lineReader.readLine()) != null) {
516       if (line.startsWith(eomMarker)
517           && skipLeadingWhitespace(line, eomMarker.length()) == line.length()) {
518         return sb.toString();
519       } else {
520         sb.append(line).append('\n');
521       }
522     }
523     reportError(state, "EOM " + eomMarker + " never found");
524     return null;
525   }
526 
527   // //////////////////////////////////////////////////////////////////////////
528   //
529   // Handlers
530 
handleDescend(ParseState state, HdfNameAttrs element)531   private void handleDescend(ParseState state, HdfNameAttrs element) {
532     Data child = handleNodeCreation(state.currentNode, element);
533     state.context.push(state.currentNode);
534     state.currentNode = child;
535   }
536 
handleNodeCreation(Data node, HdfNameAttrs element)537   private Data handleNodeCreation(Data node, HdfNameAttrs element) {
538     return element.toData(node);
539   }
540 
handleAssign(ParseState state, HdfNameAttrs element, String value)541   private void handleAssign(ParseState state, HdfNameAttrs element, String value) {
542     // TODO: think about moving interning here
543     Data child = handleNodeCreation(state.currentNode, element);
544     child.setValue(value);
545   }
546 
handleCopy(ParseState state, HdfNameAttrs element, String srcName)547   private void handleCopy(ParseState state, HdfNameAttrs element, String srcName) {
548     Data child = handleNodeCreation(state.currentNode, element);
549     Data src = state.output.getChild(srcName);
550     if (src != null) {
551       child.setValue(src.getValue());
552     } else {
553       child.setValue("");
554     }
555   }
556 
handleLink(ParseState state, HdfNameAttrs element, String srcName)557   private void handleLink(ParseState state, HdfNameAttrs element, String srcName) {
558     Data child = handleNodeCreation(state.currentNode, element);
559     child.setSymlink(state.output.createChild(srcName));
560   }
561 
handleAscend(ParseState state)562   private void handleAscend(ParseState state) {
563     if (state.context.isEmpty()) {
564       reportError(state, "Too many '}'");
565       return;
566     }
567     state.currentNode = state.context.pop();
568   }
569 
handleInclude(String seq, ParseState state)570   private void handleInclude(String seq, ParseState state) throws IOException {
571     String includeFileName = internStrategy.intern(seq);
572 
573     // Load the file
574     Reader reader = state.resourceLoader.open(includeFileName);
575     if (reader == null) {
576       reportError(state, "Unable to find file " + includeFileName);
577       return;
578     }
579 
580     // Check whether we are in include loop
581     if (!state.includeStack.push(includeFileName)) {
582       reportError(state, createIncludeStackTraceMessage(state.includeStack, includeFileName));
583       return;
584     }
585 
586     // Parse the file
587     state.hdfParser.parse(ParseState
588         .createParseStateForIncludedFile(state, includeFileName, reader));
589 
590     if (!includeFileName.equals(state.includeStack.pop())) {
591       // Include stack trace is corrupted
592       throw new IllegalStateException("Unable to find on include stack: " + includeFileName);
593     }
594   }
595 
createIncludeStackTraceMessage(UniqueStack<String> includeStack, String includeFileName)596   private String createIncludeStackTraceMessage(UniqueStack<String> includeStack,
597       String includeFileName) {
598     StringBuilder message = new StringBuilder();
599     message.append("File included twice: ");
600     message.append(includeFileName);
601 
602     message.append(" Include stack: ");
603     for (String fileName : includeStack) {
604       message.append(fileName);
605       message.append(" -> ");
606     }
607     message.append(includeFileName);
608     return message.toString();
609   }
610 
611   // /////////////////////////////////////////////////////////////////////////
612   //
613   // Character values
614 
isNumericChar(char c)615   private static boolean isNumericChar(char c) {
616     if ('0' <= c && c <= '9') {
617       return true;
618     } else {
619       return false;
620     }
621   }
622 
isAlphaNumericChar(char c)623   private static boolean isAlphaNumericChar(char c) {
624     if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9')) {
625       return true;
626     } else {
627       return false;
628     }
629   }
630 
isHdfNameChar(char c)631   private static boolean isHdfNameChar(char c) {
632     if (isAlphaNumericChar(c) || c == '_' || c == '.') {
633       return true;
634     } else {
635       return false;
636     }
637   }
638 
stripWhitespace(String seq)639   private static String stripWhitespace(String seq) {
640     int start = skipLeadingWhitespace(seq, 0);
641     int end = seq.length() - 1;
642     while (end > start && Character.isWhitespace(seq.charAt(end))) {
643       --end;
644     }
645     if (start == 0 && end == seq.length() - 1) {
646       return seq;
647     } else {
648       return seq.substring(start, end + 1);
649     }
650   }
651 
skipLeadingWhitespace(String seq, int index)652   private static int skipLeadingWhitespace(String seq, int index) {
653     while (index < seq.length() && Character.isWhitespace(seq.charAt(index))) {
654       index++;
655     }
656     return index;
657   }
658 
659   /**
660    * Determines if a character sequence appears in the given sequence starting at a specified index.
661    *
662    * @param seq the sequence that we want to see if it contains the string match.
663    * @param start the index into seq where we want to check for match
664    * @param match the String we want to look for in the sequence.
665    * @return {@code true} if the string match appears in seq starting at the index start, {@code
666    *         false} otherwise.
667    */
matches(String seq, int start, String match)668   private static boolean matches(String seq, int start, String match) {
669     if (seq.length() - start < match.length()) {
670       return false;
671     }
672     for (int i = 0; i < match.length(); i++) {
673       if (match.charAt(i) != seq.charAt(start + i)) {
674         return false;
675       }
676     }
677     return true;
678   }
679 
680   /**
681    * Reads the character at the specified index in the given String. Throws an exception to be
682    * caught above if the index is out of range.
683    */
charAt(String seq, int index)684   private static char charAt(String seq, int index) throws OutOfCharsException {
685     if (0 <= index && index < seq.length()) {
686       return seq.charAt(index);
687     } else {
688       throw new OutOfCharsException();
689     }
690   }
691 
692 
reportError(ParseState state, String errorMessage)693   private static void reportError(ParseState state, String errorMessage) {
694     if (state.errorHandler != null) {
695       state.errorHandler.error(state.lineReader.getLineNumber(), state.line, state.parsedFileName,
696           errorMessage);
697     } else {
698       throw new RuntimeException("Parse Error on line " + state.lineReader.getLineNumber() + ": "
699           + errorMessage + " : " + state.line);
700     }
701   }
702 }
703