001
002 /*
003 * Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
004 *
005 * $Id: ParseContext.java 115 2011-08-20 23:24:18Z archie.cobbs $
006 */
007
008 package org.dellroad.stuff.string;
009
010 import java.util.regex.Matcher;
011 import java.util.regex.Pattern;
012
013 /**
014 * Utility class supporting parsing of strings.
015 *
016 * <p>
017 * Instances of this class are not thread safe.
018 */
019 public class ParseContext implements Cloneable {
020
021 private static final int MAX_REJECT_QUOTE = 15;
022
023 private final String input;
024
025 private int index;
026
027 /**
028 * Constructor.
029 *
030 * @param input the input string to parse
031 */
032 public ParseContext(String input) {
033 this.input = input;
034 }
035
036 /**
037 * Get the original input string as passed to the constructor.
038 *
039 * @return original input string
040 */
041 public String getOriginalInput() {
042 return this.input;
043 }
044
045 /**
046 * Get the current input.
047 *
048 * @return substring of the original input string starting at the current parse position
049 */
050 public String getInput() {
051 return this.input.substring(this.index);
052 }
053
054 /**
055 * Get the current index into the original input string.
056 *
057 * @return current parse position
058 * @see #setIndex
059 */
060 public int getIndex() {
061 return this.index;
062 }
063
064 /**
065 * Set the current index into the original input string.
066 *
067 * @param index new parse position
068 * @throws IllegalArgumentException if {@code index} is greater than the original string length
069 * @see #getIndex
070 */
071 public void setIndex(int index) {
072 this.index = index;
073 }
074
075 /**
076 * Reset this instance. This instance will return to the state it was in
077 * immediately after construction.
078 *
079 * <p>
080 * This method just invokes:
081 * <blockquote>
082 * <code>setIndex(0)</code>
083 * </blockquote>
084 */
085 public void reset() {
086 this.setIndex(0);
087 }
088
089 /**
090 * Match the current input against the given regular expression and advance past it.
091 *
092 * @param regex regular expression to match against the current input
093 * @throws IllegalArgumentException if the current input does not match
094 */
095 public Matcher matchPrefix(String regex) {
096 return this.matchPrefix(Pattern.compile(regex));
097 }
098
099 /**
100 * Match the current input against the given regular expression and advance past it.
101 *
102 * @param regex regular expression to match against the current input
103 * @throws IllegalArgumentException if the current input does not match
104 */
105 public Matcher matchPrefix(Pattern regex) {
106 String s = this.getInput();
107 Matcher matcher = regex.matcher(s);
108 if (!matcher.lookingAt())
109 throw buildException("expected input matching pattern `" + regex + "'");
110 this.index += matcher.end();
111 return matcher;
112 }
113
114 /**
115 * Determine if the current input starts with the given literal prefix.
116 * If so, advance past it. If not, do not advance.
117 *
118 * @param prefix literal string to try to match against the current input
119 * @return whether the current input matched {@code prefix}
120 */
121 public boolean tryLiteral(String prefix) {
122 boolean match = this.input.startsWith(prefix, this.index);
123 if (match)
124 this.index += prefix.length();
125 return match;
126 }
127
128 /**
129 * Determine if we are at the end of the input.
130 */
131 public boolean isEOF() {
132 return this.index >= this.input.length();
133 }
134
135 /**
136 * Read and advance past the next character.
137 *
138 * @return the next character of input
139 * @throws IllegalArgumentException if there are no more characters
140 */
141 public char read() {
142 char ch = this.peek();
143 this.index++;
144 return ch;
145 }
146
147 /**
148 * Read, but do not advance past, the next character.
149 *
150 * @return the next character of input
151 * @throws IllegalArgumentException if there are no more characters
152 */
153 public char peek() {
154 try {
155 return this.input.charAt(this.index);
156 } catch (StringIndexOutOfBoundsException e) {
157 throw this.buildException("truncated input");
158 }
159 }
160
161 /**
162 * Push back the previously read character.
163 *
164 * @throws IllegalStateException if the beginning of the original string has been reached
165 */
166 public void unread() {
167 if (this.index == 0)
168 throw new IllegalStateException();
169 this.index--;
170 }
171
172 /**
173 * Read and advance past the next character, which must match {@code ch}.
174 *
175 * @param ch the expected next character of input
176 * @throws IllegalArgumentException if there are no more characters or the
177 * next character read is not {@code ch}
178 */
179 public void expect(char ch) {
180 if (this.read() != ch) {
181 this.unread();
182 throw buildException("expected `" + ch + "'");
183 }
184 }
185
186 /**
187 * Skip leading whitespace, if any.
188 *
189 * @see Character#isWhitespace
190 */
191 public void skipWhitespace() {
192 while (this.index < this.input.length() && Character.isWhitespace(this.input.charAt(this.index)))
193 this.index++;
194 }
195
196 /**
197 * Clone this instance.
198 */
199 public ParseContext clone() {
200 try {
201 return (ParseContext)super.clone();
202 } catch (CloneNotSupportedException e) {
203 throw new RuntimeException(e);
204 }
205 }
206
207 /**
208 * Create a generic exception for rejecting the current input.
209 */
210 public IllegalArgumentException buildException() {
211 return this.buildException(null);
212 }
213
214 /**
215 * Create an exception for rejecting the current input.
216 *
217 * @param message problem description, or {@code null} for none
218 */
219 public IllegalArgumentException buildException(String message) {
220 String text = "parse error ";
221 String bogus = getInput();
222 if (bogus.length() == 0)
223 text += "at end of input";
224 else {
225 if (bogus.length() > MAX_REJECT_QUOTE)
226 bogus = bogus.substring(0, MAX_REJECT_QUOTE - 3) + "...";
227 text += "staring with `" + bogus + "'";
228 }
229 if (message != null)
230 text += ": " + message;
231 return new IllegalArgumentException(text);
232 }
233 }
234