source: josm/trunk/src/org/openstreetmap/josm/actions/search/SearchCompiler.java@ 1389

Last change on this file since 1389 was 1389, checked in by stoecker, 15 years ago

close #2132. patch by xeen

  • Property svn:eol-style set to native
File size: 15.5 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions.search;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.io.PushbackReader;
8import java.io.StringReader;
9import java.util.Map.Entry;
10import java.util.regex.Matcher;
11import java.util.regex.Pattern;
12import java.util.regex.PatternSyntaxException;
13
14import org.openstreetmap.josm.data.osm.Node;
15import org.openstreetmap.josm.data.osm.OsmPrimitive;
16import org.openstreetmap.josm.data.osm.Relation;
17import org.openstreetmap.josm.data.osm.User;
18import org.openstreetmap.josm.data.osm.Way;
19
20/**
21 * Implements a google-like search.
22 * @author Imi
23 */
24public class SearchCompiler {
25
26 private boolean caseSensitive = false;
27 private boolean regexSearch = false;
28 private String rxErrorMsg = marktr("The regex \"{0}\" had a parse error at offset {1}, full error:\n\n{2}");
29 private PushbackTokenizer tokenizer;
30
31 public SearchCompiler(boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) {
32 this.caseSensitive = caseSensitive;
33 this.regexSearch = regexSearch;
34 this.tokenizer = tokenizer;
35 }
36
37 abstract public static class Match {
38 abstract public boolean match(OsmPrimitive osm) throws ParseError;
39 }
40
41 private static class Always extends Match {
42 @Override public boolean match(OsmPrimitive osm) {
43 return true;
44 }
45 }
46
47 private static class Not extends Match {
48 private final Match match;
49 public Not(Match match) {this.match = match;}
50 @Override public boolean match(OsmPrimitive osm) throws ParseError {
51 return !match.match(osm);
52 }
53 @Override public String toString() {return "!"+match;}
54 }
55
56 private static class And extends Match {
57 private Match lhs;
58 private Match rhs;
59 public And(Match lhs, Match rhs) {this.lhs = lhs; this.rhs = rhs;}
60 @Override public boolean match(OsmPrimitive osm) throws ParseError {
61 return lhs.match(osm) && rhs.match(osm);
62 }
63 @Override public String toString() {return lhs+" && "+rhs;}
64 }
65
66 private static class Or extends Match {
67 private Match lhs;
68 private Match rhs;
69 public Or(Match lhs, Match rhs) {this.lhs = lhs; this.rhs = rhs;}
70 @Override public boolean match(OsmPrimitive osm) throws ParseError {
71 return lhs.match(osm) || rhs.match(osm);
72 }
73 @Override public String toString() {return lhs+" || "+rhs;}
74 }
75
76 private static class Id extends Match {
77 private long id;
78 public Id(long id) {this.id = id;}
79 @Override public boolean match(OsmPrimitive osm) {
80 return osm.id == id;
81 }
82 @Override public String toString() {return "id="+id;}
83 }
84
85 private class KeyValue extends Match {
86 private String key;
87 private String value;
88 public KeyValue(String key, String value) {this.key = key; this.value = value; }
89 @Override public boolean match(OsmPrimitive osm) throws ParseError {
90
91 if (regexSearch) {
92 if (osm.keys == null)
93 return false;
94
95 /* The string search will just get a key like
96 * 'highway' and look that up as osm.get(key). But
97 * since we're doing a regex match we'll have to loop
98 * over all the keys to see if they match our regex,
99 * and only then try to match against the value
100 */
101
102 Pattern searchKey = null;
103 Pattern searchValue = null;
104
105 if (caseSensitive) {
106 try {
107 searchKey = Pattern.compile(key);
108 } catch (PatternSyntaxException e) {
109 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()));
110 }
111 try {
112 searchValue = Pattern.compile(value);
113 } catch (PatternSyntaxException e) {
114 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()));
115 }
116 } else {
117 try {
118 searchKey = Pattern.compile(key, Pattern.CASE_INSENSITIVE);
119 } catch (PatternSyntaxException e) {
120 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()));
121 }
122 try {
123 searchValue = Pattern.compile(value, Pattern.CASE_INSENSITIVE);
124 } catch (PatternSyntaxException e) {
125 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()));
126 }
127 }
128
129 for (Entry<String, String> e : osm.keys.entrySet()) {
130 String k = e.getKey();
131 String v = e.getValue();
132
133 Matcher matcherKey = searchKey.matcher(k);
134 boolean matchedKey = matcherKey.find();
135
136 if (matchedKey) {
137 Matcher matcherValue = searchValue.matcher(v);
138 boolean matchedValue = matcherValue.find();
139
140 if (matchedValue)
141 return true;
142 }
143 }
144 } else {
145 String value = null;
146
147 if (key.equals("timestamp"))
148 value = osm.getTimeStr();
149 else
150 value = osm.get(key);
151
152 if (value == null)
153 return false;
154
155 String v1 = caseSensitive ? value : value.toLowerCase();
156 String v2 = caseSensitive ? this.value : this.value.toLowerCase();
157
158 // is not Java 1.5
159 //v1 = java.text.Normalizer.normalize(v1, java.text.Normalizer.Form.NFC);
160 //v2 = java.text.Normalizer.normalize(v2, java.text.Normalizer.Form.NFC);
161 return v1.indexOf(v2) != -1;
162 }
163
164 return false;
165 }
166 @Override public String toString() {return key+"="+value;}
167 }
168
169 private class Any extends Match {
170 private String s;
171 public Any(String s) {this.s = s;}
172 @Override public boolean match(OsmPrimitive osm) throws ParseError {
173 if (osm.keys == null)
174 return s.equals("");
175
176 String search;
177 Pattern searchRegex = null;
178
179 if (regexSearch) {
180 search = s;
181 if (caseSensitive) {
182 try {
183 searchRegex = Pattern.compile(search);
184 } catch (PatternSyntaxException e) {
185 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()));
186 }
187 } else {
188 try {
189 searchRegex = Pattern.compile(search, Pattern.CASE_INSENSITIVE);
190 } catch (PatternSyntaxException e) {
191 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()));
192 }
193 }
194 } else {
195 search = caseSensitive ? s : s.toLowerCase();
196 }
197
198 // is not Java 1.5
199 //search = java.text.Normalizer.normalize(search, java.text.Normalizer.Form.NFC);
200 for (Entry<String, String> e : osm.keys.entrySet()) {
201 if (regexSearch) {
202 String key = e.getKey();
203 String value = e.getValue();
204
205 // is not Java 1.5
206 //value = java.text.Normalizer.normalize(value, java.text.Normalizer.Form.NFC);
207
208 Matcher keyMatcher = searchRegex.matcher(key);
209 Matcher valMatcher = searchRegex.matcher(value);
210
211 boolean keyMatchFound = keyMatcher.find();
212 boolean valMatchFound = valMatcher.find();
213
214 if (keyMatchFound || valMatchFound)
215 return true;
216 } else {
217 String key = caseSensitive ? e.getKey() : e.getKey().toLowerCase();
218 String value = caseSensitive ? e.getValue() : e.getValue().toLowerCase();
219
220 // is not Java 1.5
221 //value = java.text.Normalizer.normalize(value, java.text.Normalizer.Form.NFC);
222
223 if (key.indexOf(search) != -1 || value.indexOf(search) != -1)
224 return true;
225 }
226 }
227 if (osm.user != null) {
228 String name = osm.user.name;
229 // is not Java 1.5
230 //String name = java.text.Normalizer.normalize(name, java.text.Normalizer.Form.NFC);
231 if (!caseSensitive)
232 name = name.toLowerCase();
233 if (name.indexOf(search) != -1)
234 return true;
235 }
236 return false;
237 }
238 @Override public String toString() {return s;}
239 }
240
241 private static class ExactType extends Match {
242 private String type;
243 public ExactType(String type) {this.type = type;}
244 @Override public boolean match(OsmPrimitive osm) {
245 if (osm instanceof Node)
246 return type.equals("node");
247 if (osm instanceof Way)
248 return type.equals("way");
249 if (osm instanceof Relation)
250 return type.equals("relation");
251 throw new IllegalStateException("unknown class "+osm.getClass());
252 }
253 @Override public String toString() {return "type="+type;}
254 }
255
256 private static class UserMatch extends Match {
257 private User user;
258 public UserMatch(String user) { this.user = User.get(user); }
259 @Override public boolean match(OsmPrimitive osm) {
260 return osm.user == user;
261 }
262 @Override public String toString() { return "user=" + user.name; }
263 }
264
265 private static class NodeCount extends Match {
266 private int count;
267 public NodeCount(int count) {this.count = count;}
268 @Override public boolean match(OsmPrimitive osm) {
269 return osm instanceof Way && ((Way) osm).nodes.size() == count;
270 }
271 @Override public String toString() {return "nodes="+count;}
272 }
273
274 private static class NodeCountRange extends Match {
275 private int minCount;
276 private int maxCount;
277 public NodeCountRange(int minCount, int maxCount) {
278 if(maxCount < minCount) {
279 this.minCount = maxCount;
280 this.maxCount = minCount;
281 } else {
282 this.minCount = minCount;
283 this.maxCount = maxCount;
284 }
285 }
286 @Override public boolean match(OsmPrimitive osm) {
287 if(!(osm instanceof Way)) return false;
288 int size = ((Way)osm).nodes.size();
289 return (size >= minCount) && (size <= maxCount);
290 }
291 @Override public String toString() {return "nodes="+minCount+"-"+maxCount;}
292 }
293
294 private static class Modified extends Match {
295 @Override public boolean match(OsmPrimitive osm) {
296 return osm.modified || osm.id == 0;
297 }
298 @Override public String toString() {return "modified";}
299 }
300
301 private static class Selected extends Match {
302 @Override public boolean match(OsmPrimitive osm) {
303 return osm.selected;
304 }
305 @Override public String toString() {return "selected";}
306 }
307
308 private static class Incomplete extends Match {
309 @Override public boolean match(OsmPrimitive osm) {
310 return osm.incomplete;
311 }
312 @Override public String toString() {return "incomplete";}
313 }
314
315 private static class Untagged extends Match {
316 @Override public boolean match(OsmPrimitive osm) {
317 return !osm.tagged;
318 }
319 @Override public String toString() {return "untagged";}
320 }
321
322 public static class ParseError extends Exception {
323 public ParseError(String msg) {
324 super(msg);
325 }
326 }
327
328 public static Match compile(String searchStr, boolean caseSensitive, boolean regexSearch)
329 throws ParseError {
330 return new SearchCompiler(caseSensitive, regexSearch,
331 new PushbackTokenizer(
332 new PushbackReader(new StringReader(searchStr))))
333 .parse();
334 }
335
336 public Match parse() throws ParseError {
337 Match m = parseJuxta();
338 if (!tokenizer.readIfEqual(null)) {
339 throw new ParseError("Unexpected token: " + tokenizer.nextToken());
340 }
341 return m;
342 }
343
344 private Match parseJuxta() throws ParseError {
345 Match juxta = new Always();
346
347 Match m;
348 while ((m = parseOr()) != null) {
349 juxta = new And(m, juxta);
350 }
351
352 return juxta;
353 }
354
355 private Match parseOr() throws ParseError {
356 Match a = parseNot();
357 if (tokenizer.readIfEqual("|")) {
358 Match b = parseNot();
359 if (a == null || b == null) {
360 throw new ParseError(tr("Missing arguments for or."));
361 }
362 return new Or(a, b);
363 }
364 return a;
365 }
366
367 private Match parseNot() throws ParseError {
368 if (tokenizer.readIfEqual("-")) {
369 Match m = parseParens();
370 if (m == null) {
371 throw new ParseError(tr("Missing argument for not."));
372 }
373 return new Not(m);
374 }
375 return parseParens();
376 }
377
378 private Match parseParens() throws ParseError {
379 if (tokenizer.readIfEqual("(")) {
380 Match m = parseJuxta();
381 if (!tokenizer.readIfEqual(")")) {
382 throw new ParseError(tr("Expected closing parenthesis."));
383 }
384 return m;
385 }
386 return parsePat();
387 }
388
389 private Match parsePat() {
390 String tok = tokenizer.readText();
391
392 if (tokenizer.readIfEqual(":")) {
393 String tok2 = tokenizer.readText();
394 if (tok == null) tok = "";
395 if (tok2 == null) tok2 = "";
396 return parseKV(tok, tok2);
397 }
398
399 if (tok == null) {
400 return null;
401 } else if (tok.equals("modified")) {
402 return new Modified();
403 } else if (tok.equals("incomplete")) {
404 return new Incomplete();
405 } else if (tok.equals("untagged")) {
406 return new Untagged();
407 } else if (tok.equals("selected")) {
408 return new Selected();
409 } else {
410 return new Any(tok);
411 }
412 }
413
414 private Match parseKV(String key, String value) {
415 if (key.equals("type")) {
416 return new ExactType(value);
417 } else if (key.equals("user")) {
418 return new UserMatch(value);
419 } else if (key.equals("nodes")) {
420 try {
421 return new NodeCount(Integer.parseInt(value));
422 } catch(Exception x) {}
423
424 try {
425 String[] range = value.split("-", 2);
426 return new NodeCountRange(Integer.parseInt(range[0]), Integer.parseInt(range[1]));
427 } catch(Exception x) {}
428
429 return new NodeCount(0);
430 } else if (key.equals("id")) {
431 try {
432 return new Id(Long.parseLong(value));
433 } catch (NumberFormatException x) {
434 return new Id(0);
435 }
436 } else {
437 return new KeyValue(key, value);
438 }
439 }
440}
Note: See TracBrowser for help on using the repository browser.