001 // License: GPL. Copyright 2007 by Immanuel Scholz and others
002 package org.openstreetmap.josm.actions.search;
003
004 import static org.openstreetmap.josm.tools.I18n.marktr;
005 import static org.openstreetmap.josm.tools.I18n.tr;
006 import static org.openstreetmap.josm.tools.Utils.equal;
007
008 import java.io.IOException;
009 import java.io.Reader;
010 import java.util.Arrays;
011 import java.util.List;
012
013 import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
014
015 public class PushbackTokenizer {
016
017 public static class Range {
018 private final long start;
019 private final long end;
020
021 public Range(long start, long end) {
022 this.start = start;
023 this.end = end;
024 }
025
026 public long getStart() {
027 return start;
028 }
029
030 public long getEnd() {
031 return end;
032 }
033 }
034
035 private final Reader search;
036
037 private Token currentToken;
038 private String currentText;
039 private Long currentNumber;
040 private Long currentRange;
041 private int c;
042 private boolean isRange;
043
044 public PushbackTokenizer(Reader search) {
045 this.search = search;
046 getChar();
047 }
048
049 public enum Token {
050 NOT(marktr("<not>")), OR(marktr("<or>")), XOR(marktr("<xor>")), LEFT_PARENT(marktr("<left parent>")),
051 RIGHT_PARENT(marktr("<right parent>")), COLON(marktr("<colon>")), EQUALS(marktr("<equals>")),
052 KEY(marktr("<key>")), QUESTION_MARK(marktr("<question mark>")),
053 EOF(marktr("<end-of-file>"));
054
055 private Token(String name) {
056 this.name = name;
057 }
058
059 private final String name;
060
061 @Override
062 public String toString() {
063 return tr(name);
064 }
065 }
066
067
068 private void getChar() {
069 try {
070 c = search.read();
071 } catch (IOException e) {
072 throw new RuntimeException(e.getMessage(), e);
073 }
074 }
075
076 private static final List<Character> specialChars = Arrays.asList(new Character[] {'"', ':', '(', ')', '|', '^', '=', '?'});
077 private static final List<Character> specialCharsQuoted = Arrays.asList(new Character[] {'"'});
078
079 private String getString(boolean quoted) {
080 List<Character> sChars = quoted ? specialCharsQuoted : specialChars;
081 StringBuilder s = new StringBuilder();
082 boolean escape = false;
083 while (c != -1 && (escape || (!sChars.contains((char)c) && (quoted || !Character.isWhitespace(c))))) {
084 if (c == '\\' && !escape) {
085 escape = true;
086 } else {
087 s.append((char)c);
088 escape = false;
089 }
090 getChar();
091 }
092 return s.toString();
093 }
094
095 private String getString() {
096 return getString(false);
097 }
098
099 /**
100 * The token returned is <code>null</code> or starts with an identifier character:
101 * - for an '-'. This will be the only character
102 * : for an key. The value is the next token
103 * | for "OR"
104 * ^ for "XOR"
105 * ' ' for anything else.
106 * @return The next token in the stream.
107 */
108 public Token nextToken() {
109 if (currentToken != null) {
110 Token result = currentToken;
111 currentToken = null;
112 return result;
113 }
114
115 while (Character.isWhitespace(c)) {
116 getChar();
117 }
118 switch (c) {
119 case -1:
120 getChar();
121 return Token.EOF;
122 case ':':
123 getChar();
124 return Token.COLON;
125 case '=':
126 getChar();
127 return Token.EQUALS;
128 case '(':
129 getChar();
130 return Token.LEFT_PARENT;
131 case ')':
132 getChar();
133 return Token.RIGHT_PARENT;
134 case '|':
135 getChar();
136 return Token.OR;
137 case '^':
138 getChar();
139 return Token.XOR;
140 case '&':
141 getChar();
142 return nextToken();
143 case '?':
144 getChar();
145 return Token.QUESTION_MARK;
146 case '"':
147 getChar();
148 currentText = getString(true);
149 getChar();
150 return Token.KEY;
151 default:
152 String prefix = "";
153 if (c == '-') {
154 getChar();
155 if (!Character.isDigit(c))
156 return Token.NOT;
157 prefix = "-";
158 }
159 currentText = prefix + getString();
160 if ("or".equalsIgnoreCase(currentText))
161 return Token.OR;
162 else if ("xor".equalsIgnoreCase(currentText))
163 return Token.XOR;
164 else if ("and".equalsIgnoreCase(currentText))
165 return nextToken();
166 // try parsing number
167 try {
168 currentNumber = Long.parseLong(currentText);
169 } catch (NumberFormatException e) {
170 currentNumber = null;
171 }
172 // if text contains "-", try parsing a range
173 int pos = currentText.indexOf('-', 1);
174 isRange = pos > 0;
175 if (isRange) {
176 try {
177 currentNumber = Long.parseLong(currentText.substring(0, pos));
178 } catch (NumberFormatException e) {
179 currentNumber = null;
180 }
181 try {
182 currentRange = Long.parseLong(currentText.substring(pos + 1));
183 } catch (NumberFormatException e) {
184 currentRange = null;
185 }
186 } else {
187 currentRange = null;
188 }
189 return Token.KEY;
190 }
191 }
192
193 public boolean readIfEqual(Token token) {
194 Token nextTok = nextToken();
195 if (equal(nextTok, token))
196 return true;
197 currentToken = nextTok;
198 return false;
199 }
200
201 public String readTextOrNumber() {
202 Token nextTok = nextToken();
203 if (nextTok == Token.KEY)
204 return currentText;
205 currentToken = nextTok;
206 return null;
207 }
208
209 public long readNumber(String errorMessage) throws ParseError {
210 if ((nextToken() == Token.KEY) && (currentNumber != null))
211 return currentNumber;
212 else
213 throw new ParseError(errorMessage);
214 }
215
216 public long getReadNumber() {
217 return (currentNumber != null) ? currentNumber : 0;
218 }
219
220 public Range readRange(String errorMessage) throws ParseError {
221 if (nextToken() != Token.KEY || (currentNumber == null && currentRange == null)) {
222 throw new ParseError(errorMessage);
223 } else if (!isRange && currentNumber != null) {
224 if (currentNumber >= 0) {
225 return new Range(currentNumber, currentNumber);
226 } else {
227 return new Range(0, Math.abs(currentNumber));
228 }
229 } else if (isRange && currentRange == null) {
230 return new Range(currentNumber, Integer.MAX_VALUE);
231 } else if (currentNumber != null && currentRange != null) {
232 return new Range(currentNumber, currentRange);
233 } else {
234 throw new ParseError(errorMessage);
235 }
236 }
237
238 public String getText() {
239 return currentText;
240 }
241 }