source: josm/trunk/src/org/openstreetmap/josm/data/osm/search/PushbackTokenizer.java@ 16260

Last change on this file since 16260 was 16260, checked in by simon04, 4 years ago

fix #19070 - SearchCompiler: regexp comparison using <tilde>

  • Property svn:eol-style set to native
File size: 9.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm.search;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.io.IOException;
8import java.io.Reader;
9import java.util.Arrays;
10import java.util.List;
11import java.util.Objects;
12
13import org.openstreetmap.josm.tools.JosmRuntimeException;
14
15/**
16 * This class is used to parse a search string and split it into tokens.
17 * It provides methods to parse numbers and extract strings.
18 * @since 12656 (moved from actions.search package)
19 */
20public class PushbackTokenizer {
21
22 /**
23 * A range of long numbers. Immutable
24 */
25 public static class Range {
26 private final long start;
27 private final long end;
28
29 /**
30 * Create a new range
31 * @param start The start
32 * @param end The end (inclusive)
33 */
34 public Range(long start, long end) {
35 this.start = start;
36 this.end = end;
37 }
38
39 /**
40 * @return The start
41 */
42 public long getStart() {
43 return start;
44 }
45
46 /**
47 * @return The end (inclusive)
48 */
49 public long getEnd() {
50 return end;
51 }
52
53 @Override
54 public String toString() {
55 return "Range [start=" + start + ", end=" + end + ']';
56 }
57 }
58
59 private final Reader search;
60
61 private Token currentToken;
62 private String currentText;
63 private Long currentNumber;
64 private Long currentRange;
65 private int c;
66 private boolean isRange;
67
68 /**
69 * Creates a new {@link PushbackTokenizer}
70 * @param search The search string reader to read the tokens from
71 */
72 public PushbackTokenizer(Reader search) {
73 this.search = search;
74 getChar();
75 }
76
77 /**
78 * The token types that may be read
79 */
80 public enum Token {
81 /**
82 * Not token (-)
83 */
84 NOT(marktr("<not>")),
85 /**
86 * Or token (or) (|)
87 */
88 OR(marktr("<or>")),
89 /**
90 * Xor token (xor) (^)
91 */
92 XOR(marktr("<xor>")),
93 /**
94 * opening parentheses token (
95 */
96 LEFT_PARENT(marktr("<left parent>")),
97 /**
98 * closing parentheses token )
99 */
100 RIGHT_PARENT(marktr("<right parent>")),
101 /**
102 * Colon :
103 */
104 COLON(marktr("<colon>")),
105 /**
106 * The equals sign (=)
107 */
108 EQUALS(marktr("<equals>")),
109 /**
110 * The tilde sign (~)
111 */
112 TILDE(marktr("<tilde>")),
113 /**
114 * A text
115 */
116 KEY(marktr("<key>")),
117 /**
118 * A question mark (?)
119 */
120 QUESTION_MARK(marktr("<question mark>")),
121 /**
122 * Marks the end of the input
123 */
124 EOF(marktr("<end-of-file>")),
125 /**
126 * Less than sign (&lt;)
127 */
128 LESS_THAN("<less-than>"),
129 /**
130 * Greater than sign (&gt;)
131 */
132 GREATER_THAN("<greater-than>");
133
134 Token(String name) {
135 this.name = name;
136 }
137
138 private final String name;
139
140 @Override
141 public String toString() {
142 return tr(name);
143 }
144 }
145
146 private void getChar() {
147 try {
148 c = search.read();
149 } catch (IOException e) {
150 throw new JosmRuntimeException(e.getMessage(), e);
151 }
152 }
153
154 private static final List<Character> SPECIAL_CHARS = Arrays.asList('"', ':', '(', ')', '|', '^', '=', '~', '?', '<', '>');
155 private static final List<Character> SPECIAL_CHARS_QUOTED = Arrays.asList('"');
156
157 private String getString(boolean quoted) {
158 List<Character> sChars = quoted ? SPECIAL_CHARS_QUOTED : SPECIAL_CHARS;
159 StringBuilder s = new StringBuilder();
160 boolean escape = false;
161 while (c != -1 && (escape || (!sChars.contains((char) c) && (quoted || !Character.isWhitespace(c))))) {
162 if (c == '\\' && !escape) {
163 escape = true;
164 } else {
165 s.append((char) c);
166 escape = false;
167 }
168 getChar();
169 }
170 return s.toString();
171 }
172
173 private String getString() {
174 return getString(false);
175 }
176
177 /**
178 * The token returned is <code>null</code> or starts with an identifier character:
179 * - for an '-'. This will be the only character
180 * : for an key. The value is the next token
181 * | for "OR"
182 * ^ for "XOR"
183 * ' ' for anything else.
184 * @return The next token in the stream.
185 */
186 public Token nextToken() {
187 if (currentToken != null) {
188 Token result = currentToken;
189 currentToken = null;
190 return result;
191 }
192
193 while (Character.isWhitespace(c)) {
194 getChar();
195 }
196 switch (c) {
197 case -1:
198 getChar();
199 return Token.EOF;
200 case ':':
201 getChar();
202 return Token.COLON;
203 case '=':
204 getChar();
205 return Token.EQUALS;
206 case '~':
207 getChar();
208 return Token.TILDE;
209 case '<':
210 getChar();
211 return Token.LESS_THAN;
212 case '>':
213 getChar();
214 return Token.GREATER_THAN;
215 case '(':
216 getChar();
217 return Token.LEFT_PARENT;
218 case ')':
219 getChar();
220 return Token.RIGHT_PARENT;
221 case '|':
222 getChar();
223 return Token.OR;
224 case '^':
225 getChar();
226 return Token.XOR;
227 case '&':
228 getChar();
229 return nextToken();
230 case '?':
231 getChar();
232 return Token.QUESTION_MARK;
233 case '"':
234 getChar();
235 currentText = getString(true);
236 getChar();
237 return Token.KEY;
238 default:
239 String prefix = "";
240 if (c == '-') {
241 getChar();
242 if (!Character.isDigit(c))
243 return Token.NOT;
244 prefix = "-";
245 }
246 currentText = prefix + getString();
247 if ("or".equalsIgnoreCase(currentText))
248 return Token.OR;
249 else if ("xor".equalsIgnoreCase(currentText))
250 return Token.XOR;
251 else if ("and".equalsIgnoreCase(currentText))
252 return nextToken();
253 // try parsing number
254 try {
255 currentNumber = Long.valueOf(currentText);
256 } catch (NumberFormatException e) {
257 currentNumber = null;
258 }
259 // if text contains "-", try parsing a range
260 int pos = currentText.indexOf('-', 1);
261 isRange = pos > 0;
262 if (isRange) {
263 try {
264 currentNumber = Long.valueOf(currentText.substring(0, pos));
265 } catch (NumberFormatException e) {
266 currentNumber = null;
267 }
268 try {
269 currentRange = Long.valueOf(currentText.substring(pos + 1));
270 } catch (NumberFormatException e) {
271 currentRange = null;
272 }
273 } else {
274 currentRange = null;
275 }
276 return Token.KEY;
277 }
278 }
279
280 /**
281 * Reads the next token if it is equal to the given, suggested token
282 * @param token The token the next one should be equal to
283 * @return <code>true</code> if it has been read
284 */
285 public boolean readIfEqual(Token token) {
286 Token nextTok = nextToken();
287 if (Objects.equals(nextTok, token))
288 return true;
289 currentToken = nextTok;
290 return false;
291 }
292
293 /**
294 * Reads the next token. If it is a text, return that text. If not, advance
295 * @return the text or <code>null</code> if the reader was advanced
296 */
297 public String readTextOrNumber() {
298 Token nextTok = nextToken();
299 if (nextTok == Token.KEY)
300 return currentText;
301 currentToken = nextTok;
302 return null;
303 }
304
305 /**
306 * Reads a number
307 * @param errorMessage The error if the number cannot be read
308 * @return The number that was found
309 * @throws SearchParseError if there is no number
310 */
311 public long readNumber(String errorMessage) throws SearchParseError {
312 if ((nextToken() == Token.KEY) && (currentNumber != null))
313 return currentNumber;
314 else
315 throw new SearchParseError(errorMessage);
316 }
317
318 /**
319 * Gets the last number that was read
320 * @return The last number
321 */
322 public long getReadNumber() {
323 return (currentNumber != null) ? currentNumber : 0;
324 }
325
326 /**
327 * Reads a range of numbers
328 * @param errorMessage The error if the input is malformed
329 * @return The range that was found
330 * @throws SearchParseError If the input is not as expected for a range
331 */
332 public Range readRange(String errorMessage) throws SearchParseError {
333 if (nextToken() != Token.KEY || (currentNumber == null && currentRange == null)) {
334 throw new SearchParseError(errorMessage);
335 } else if (!isRange && currentNumber != null) {
336 if (currentNumber >= 0) {
337 return new Range(currentNumber, currentNumber);
338 } else {
339 return new Range(0, Math.abs(currentNumber));
340 }
341 } else if (isRange && currentRange == null) {
342 return new Range(currentNumber, Long.MAX_VALUE);
343 } else if (currentNumber != null && currentRange != null) {
344 return new Range(currentNumber, currentRange);
345 } else {
346 throw new SearchParseError(errorMessage);
347 }
348 }
349
350 /**
351 * Gets the last text that was found
352 * @return The text
353 */
354 public String getText() {
355 return currentText;
356 }
357}
Note: See TracBrowser for help on using the repository browser.