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