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

Last change on this file since 6106 was 6102, checked in by Don-vip, 11 years ago

see #8902 - Small performance enhancements / coding style (patch by shinigami):

  • effective double comparison
  • new Date().getTime() => System.currentTimeMillis()
  • Property svn:eol-style set to native
File size: 43.9 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.text.Normalizer;
10import java.util.Arrays;
11import java.util.Collection;
12import java.util.HashMap;
13import java.util.Map;
14import java.util.regex.Matcher;
15import java.util.regex.Pattern;
16import java.util.regex.PatternSyntaxException;
17
18import org.openstreetmap.josm.Main;
19import org.openstreetmap.josm.actions.search.PushbackTokenizer.Range;
20import org.openstreetmap.josm.actions.search.PushbackTokenizer.Token;
21import org.openstreetmap.josm.data.Bounds;
22import org.openstreetmap.josm.data.osm.Node;
23import org.openstreetmap.josm.data.osm.OsmPrimitive;
24import org.openstreetmap.josm.data.osm.OsmUtils;
25import org.openstreetmap.josm.data.osm.Relation;
26import org.openstreetmap.josm.data.osm.RelationMember;
27import org.openstreetmap.josm.data.osm.Way;
28import org.openstreetmap.josm.tools.DateUtils;
29import org.openstreetmap.josm.tools.Geometry;
30
31/**
32 Implements a google-like search.
33 <br>
34 Grammar:
35<pre>
36expression =
37 fact | expression
38 fact expression
39 fact
40
41fact =
42 ( expression )
43 -fact
44 term?
45 term=term
46 term:term
47 term
48 </pre>
49
50 @author Imi
51 */
52public class SearchCompiler {
53
54 private boolean caseSensitive = false;
55 private boolean regexSearch = false;
56 private static String rxErrorMsg = marktr("The regex \"{0}\" had a parse error at offset {1}, full error:\n\n{2}");
57 private static String rxErrorMsgNoPos = marktr("The regex \"{0}\" had a parse error, full error:\n\n{1}");
58 private PushbackTokenizer tokenizer;
59 private static Map<String, SimpleMatchFactory> simpleMatchFactoryMap = new HashMap<String, SimpleMatchFactory>();
60 private static Map<String, UnaryMatchFactory> unaryMatchFactoryMap = new HashMap<String, UnaryMatchFactory>();
61 private static Map<String, BinaryMatchFactory> binaryMatchFactoryMap = new HashMap<String, BinaryMatchFactory>();
62
63 public SearchCompiler(boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) {
64 this.caseSensitive = caseSensitive;
65 this.regexSearch = regexSearch;
66 this.tokenizer = tokenizer;
67
68 /* register core match factories at first instance, so plugins should
69 * never be able to generate a NPE
70 */
71 if (simpleMatchFactoryMap.isEmpty()) {
72 addMatchFactory(new CoreSimpleMatchFactory());
73 }
74 if (unaryMatchFactoryMap.isEmpty()) {
75 addMatchFactory(new CoreUnaryMatchFactory());
76 }
77
78 }
79
80 /**
81 * Add (register) MatchFactory with SearchCompiler
82 * @param factory
83 */
84 public static void addMatchFactory(MatchFactory factory) {
85 for (String keyword : factory.getKeywords()) {
86 // TODO: check for keyword collisions
87 if (factory instanceof SimpleMatchFactory) {
88 simpleMatchFactoryMap.put(keyword, (SimpleMatchFactory)factory);
89 } else if (factory instanceof UnaryMatchFactory) {
90 unaryMatchFactoryMap.put(keyword, (UnaryMatchFactory)factory);
91 } else if (factory instanceof BinaryMatchFactory) {
92 binaryMatchFactoryMap.put(keyword, (BinaryMatchFactory)factory);
93 } else
94 throw new AssertionError("Unknown match factory");
95 }
96 }
97
98 public class CoreSimpleMatchFactory implements SimpleMatchFactory {
99 private Collection<String> keywords = Arrays.asList("id", "version",
100 "changeset", "nodes", "tags", "areasize", "modified", "selected",
101 "incomplete", "untagged", "closed", "new", "indownloadedarea",
102 "allindownloadedarea", "inview", "allinview", "timestamp", "nth", "nth%");
103
104 @Override
105 public Match get(String keyword, PushbackTokenizer tokenizer) throws ParseError {
106 if ("modified".equals(keyword))
107 return new Modified();
108 else if ("selected".equals(keyword))
109 return new Selected();
110 else if ("incomplete".equals(keyword))
111 return new Incomplete();
112 else if ("untagged".equals(keyword))
113 return new Untagged();
114 else if ("closed".equals(keyword))
115 return new Closed();
116 else if ("new".equals(keyword))
117 return new New();
118 else if ("indownloadedarea".equals(keyword))
119 return new InDataSourceArea(false);
120 else if ("allindownloadedarea".equals(keyword))
121 return new InDataSourceArea(true);
122 else if ("inview".equals(keyword))
123 return new InView(false);
124 else if ("allinview".equals(keyword))
125 return new InView(true);
126 else if (tokenizer != null) {
127 if ("id".equals(keyword))
128 return new Id(tokenizer);
129 else if ("version".equals(keyword))
130 return new Version(tokenizer);
131 else if ("changeset".equals(keyword))
132 return new ChangesetId(tokenizer);
133 else if ("nodes".equals(keyword))
134 return new NodeCountRange(tokenizer);
135 else if ("tags".equals(keyword))
136 return new TagCountRange(tokenizer);
137 else if ("areasize".equals(keyword))
138 return new AreaSize(tokenizer);
139 else if ("nth".equals(keyword))
140 return new Nth(tokenizer, false);
141 else if ("nth%".equals(keyword))
142 return new Nth(tokenizer, true);
143 else if ("timestamp".equals(keyword)) {
144 String rangeS = " " + tokenizer.readTextOrNumber() + " "; // add leading/trailing space in order to get expected split (e.g. "a--" => {"a", ""})
145 String[] rangeA = rangeS.split("/");
146 if (rangeA.length == 1)
147 return new KeyValue(keyword, rangeS.trim(), regexSearch, caseSensitive);
148 else if (rangeA.length == 2) {
149 String rangeA1 = rangeA[0].trim();
150 String rangeA2 = rangeA[1].trim();
151 long minDate = DateUtils.fromString(rangeA1.isEmpty() ? "1980" : rangeA1).getTime(); // if min timestap is empty: use lowest possible date
152 long maxDate = rangeA2.isEmpty() ? System.currentTimeMillis() : DateUtils.fromString(rangeA2).getTime(); // if max timestamp is empty: use "now"
153 return new TimestampRange(minDate, maxDate);
154 } else
155 /*
156 * I18n: Don't translate timestamp keyword
157 */ throw new ParseError(tr("Expecting <i>min</i>/<i>max</i> after ''timestamp''"));
158 }
159 }
160 return null;
161 }
162
163 @Override
164 public Collection<String> getKeywords() {
165 return keywords;
166 }
167 }
168
169 public static class CoreUnaryMatchFactory implements UnaryMatchFactory {
170 private static Collection<String> keywords = Arrays.asList("parent", "child");
171
172 @Override
173 public UnaryMatch get(String keyword, Match matchOperand, PushbackTokenizer tokenizer) {
174 if ("parent".equals(keyword))
175 return new Parent(matchOperand);
176 else if ("child".equals(keyword))
177 return new Child(matchOperand);
178 return null;
179 }
180
181 @Override
182 public Collection<String> getKeywords() {
183 return keywords;
184 }
185 }
186
187 /**
188 * Classes implementing this interface can provide Match operators.
189 */
190 private interface MatchFactory {
191 public Collection<String> getKeywords();
192 }
193
194 public interface SimpleMatchFactory extends MatchFactory {
195 public Match get(String keyword, PushbackTokenizer tokenizer) throws ParseError;
196 }
197
198 public interface UnaryMatchFactory extends MatchFactory {
199 public UnaryMatch get(String keyword, Match matchOperand, PushbackTokenizer tokenizer) throws ParseError;
200 }
201
202 public interface BinaryMatchFactory extends MatchFactory {
203 public BinaryMatch get(String keyword, Match lhs, Match rhs, PushbackTokenizer tokenizer) throws ParseError;
204 }
205
206 /**
207 * Base class for all search operators.
208 */
209 abstract public static class Match {
210
211 abstract public boolean match(OsmPrimitive osm);
212
213 /**
214 * Tests whether one of the primitives matches.
215 */
216 protected boolean existsMatch(Collection<? extends OsmPrimitive> primitives) {
217 for (OsmPrimitive p : primitives) {
218 if (match(p))
219 return true;
220 }
221 return false;
222 }
223
224 /**
225 * Tests whether all primitives match.
226 */
227 protected boolean forallMatch(Collection<? extends OsmPrimitive> primitives) {
228 for (OsmPrimitive p : primitives) {
229 if (!match(p))
230 return false;
231 }
232 return true;
233 }
234 }
235
236 /**
237 * A unary search operator which may take data parameters.
238 */
239 abstract public static class UnaryMatch extends Match {
240
241 protected final Match match;
242
243 public UnaryMatch(Match match) {
244 if (match == null) {
245 // "operator" (null) should mean the same as "operator()"
246 // (Always). I.e. match everything
247 this.match = new Always();
248 } else {
249 this.match = match;
250 }
251 }
252
253 public Match getOperand() {
254 return match;
255 }
256 }
257
258 /**
259 * A binary search operator which may take data parameters.
260 */
261 abstract public static class BinaryMatch extends Match {
262
263 protected final Match lhs;
264 protected final Match rhs;
265
266 public BinaryMatch(Match lhs, Match rhs) {
267 this.lhs = lhs;
268 this.rhs = rhs;
269 }
270
271 public Match getLhs() {
272 return lhs;
273 }
274
275 public Match getRhs() {
276 return rhs;
277 }
278 }
279
280 /**
281 * Matches every OsmPrimitive.
282 */
283 public static class Always extends Match {
284 public static Always INSTANCE = new Always();
285 @Override public boolean match(OsmPrimitive osm) {
286 return true;
287 }
288 }
289
290 /**
291 * Never matches any OsmPrimitive.
292 */
293 public static class Never extends Match {
294 @Override
295 public boolean match(OsmPrimitive osm) {
296 return false;
297 }
298 }
299
300 /**
301 * Inverts the match.
302 */
303 public static class Not extends UnaryMatch {
304 public Not(Match match) {super(match);}
305 @Override public boolean match(OsmPrimitive osm) {
306 return !match.match(osm);
307 }
308 @Override public String toString() {return "!"+match;}
309 public Match getMatch() {
310 return match;
311 }
312 }
313
314 /**
315 * Matches if the value of the corresponding key is ''yes'', ''true'', ''1'' or ''on''.
316 */
317 private static class BooleanMatch extends Match {
318 private final String key;
319 private final boolean defaultValue;
320
321 public BooleanMatch(String key, boolean defaultValue) {
322 this.key = key;
323 this.defaultValue = defaultValue;
324 }
325 @Override
326 public boolean match(OsmPrimitive osm) {
327 Boolean ret = OsmUtils.getOsmBoolean(osm.get(key));
328 if (ret == null)
329 return defaultValue;
330 else
331 return ret;
332 }
333 }
334
335 /**
336 * Matches if both left and right expressions match.
337 */
338 public static class And extends BinaryMatch {
339 public And(Match lhs, Match rhs) {super(lhs, rhs);}
340 @Override public boolean match(OsmPrimitive osm) {
341 return lhs.match(osm) && rhs.match(osm);
342 }
343 @Override public String toString() {
344 return lhs + " && " + rhs;
345 }
346 }
347
348 /**
349 * Matches if the left OR the right expression match.
350 */
351 public static class Or extends BinaryMatch {
352 public Or(Match lhs, Match rhs) {super(lhs, rhs);}
353 @Override public boolean match(OsmPrimitive osm) {
354 return lhs.match(osm) || rhs.match(osm);
355 }
356 @Override public String toString() {
357 return lhs + " || " + rhs;
358 }
359 }
360
361 /**
362 * Matches if the left OR the right expression match, but not both.
363 */
364 public static class Xor extends BinaryMatch {
365 public Xor(Match lhs, Match rhs) {super(lhs, rhs);}
366 @Override public boolean match(OsmPrimitive osm) {
367 return lhs.match(osm) ^ rhs.match(osm);
368 }
369 @Override public String toString() {
370 return lhs + " ^ " + rhs;
371 }
372 }
373
374 /**
375 * Matches objects with ID in the given range.
376 */
377 private static class Id extends RangeMatch {
378 public Id(Range range) {super(range);}
379 public Id(PushbackTokenizer tokenizer) throws ParseError {
380 this(tokenizer.readRange(tr("Range of primitive ids expected")));
381 }
382 @Override protected Long getNumber(OsmPrimitive osm) {
383 return osm.isNew() ? 0 : osm.getUniqueId();
384 }
385 @Override protected String getString() {
386 return "id";
387 }
388 }
389
390 /**
391 * Matches objects with a changeset ID in the given range.
392 */
393 private static class ChangesetId extends RangeMatch {
394 public ChangesetId(Range range) {super(range);}
395 public ChangesetId(PushbackTokenizer tokenizer) throws ParseError {
396 this(tokenizer.readRange(tr("Range of changeset ids expected")));
397 }
398 @Override protected Long getNumber(OsmPrimitive osm) {
399 return (long) osm.getChangesetId();
400 }
401 @Override protected String getString() {
402 return "changeset";
403 }
404 }
405
406 /**
407 * Matches objects with a version number in the given range.
408 */
409 private static class Version extends RangeMatch {
410 public Version(Range range) {super(range);}
411 public Version(PushbackTokenizer tokenizer) throws ParseError {
412 this(tokenizer.readRange(tr("Range of versions expected")));
413 }
414 @Override protected Long getNumber(OsmPrimitive osm) {
415 return (long) osm.getVersion();
416 }
417 @Override protected String getString() {
418 return "version";
419 }
420 }
421
422 /**
423 * Matches objects with the given key-value pair.
424 */
425 private static class KeyValue extends Match {
426 private final String key;
427 private final Pattern keyPattern;
428 private final String value;
429 private final Pattern valuePattern;
430 private final boolean caseSensitive;
431
432 public KeyValue(String key, String value, boolean regexSearch, boolean caseSensitive) throws ParseError {
433 this.caseSensitive = caseSensitive;
434 if (regexSearch) {
435 int searchFlags = regexFlags(caseSensitive);
436
437 try {
438 this.keyPattern = Pattern.compile(key, searchFlags);
439 } catch (PatternSyntaxException e) {
440 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()));
441 } catch (Exception e) {
442 throw new ParseError(tr(rxErrorMsgNoPos, key, e.getMessage()));
443 }
444 try {
445 this.valuePattern = Pattern.compile(value, searchFlags);
446 } catch (PatternSyntaxException e) {
447 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()));
448 } catch (Exception e) {
449 throw new ParseError(tr(rxErrorMsgNoPos, value, e.getMessage()));
450 }
451 this.key = key;
452 this.value = value;
453
454 } else if (caseSensitive) {
455 this.key = key;
456 this.value = value;
457 this.keyPattern = null;
458 this.valuePattern = null;
459 } else {
460 this.key = key.toLowerCase();
461 this.value = value;
462 this.keyPattern = null;
463 this.valuePattern = null;
464 }
465 }
466
467 @Override public boolean match(OsmPrimitive osm) {
468
469 if (keyPattern != null) {
470 if (!osm.hasKeys())
471 return false;
472
473 /* The string search will just get a key like
474 * 'highway' and look that up as osm.get(key). But
475 * since we're doing a regex match we'll have to loop
476 * over all the keys to see if they match our regex,
477 * and only then try to match against the value
478 */
479
480 for (String k: osm.keySet()) {
481 String v = osm.get(k);
482
483 Matcher matcherKey = keyPattern.matcher(k);
484 boolean matchedKey = matcherKey.find();
485
486 if (matchedKey) {
487 Matcher matcherValue = valuePattern.matcher(v);
488 boolean matchedValue = matcherValue.find();
489
490 if (matchedValue)
491 return true;
492 }
493 }
494 } else {
495 String mv = null;
496
497 if (key.equals("timestamp")) {
498 mv = DateUtils.fromDate(osm.getTimestamp());
499 } else {
500 mv = osm.get(key);
501 }
502
503 if (mv == null)
504 return false;
505
506 String v1 = caseSensitive ? mv : mv.toLowerCase();
507 String v2 = caseSensitive ? value : value.toLowerCase();
508
509 v1 = Normalizer.normalize(v1, Normalizer.Form.NFC);
510 v2 = Normalizer.normalize(v2, Normalizer.Form.NFC);
511 return v1.indexOf(v2) != -1;
512 }
513
514 return false;
515 }
516 @Override public String toString() {return key+"="+value;}
517 }
518
519 /**
520 * Matches objects with the exact given key-value pair.
521 */
522 public static class ExactKeyValue extends Match {
523
524 private enum Mode {
525 ANY, ANY_KEY, ANY_VALUE, EXACT, NONE, MISSING_KEY,
526 ANY_KEY_REGEXP, ANY_VALUE_REGEXP, EXACT_REGEXP, MISSING_KEY_REGEXP;
527 }
528
529 private final String key;
530 private final String value;
531 private final Pattern keyPattern;
532 private final Pattern valuePattern;
533 private final Mode mode;
534
535 public ExactKeyValue(boolean regexp, String key, String value) throws ParseError {
536 if ("".equals(key))
537 throw new ParseError(tr("Key cannot be empty when tag operator is used. Sample use: key=value"));
538 this.key = key;
539 this.value = value == null?"":value;
540 if ("".equals(this.value) && "*".equals(key)) {
541 mode = Mode.NONE;
542 } else if ("".equals(this.value)) {
543 if (regexp) {
544 mode = Mode.MISSING_KEY_REGEXP;
545 } else {
546 mode = Mode.MISSING_KEY;
547 }
548 } else if ("*".equals(key) && "*".equals(this.value)) {
549 mode = Mode.ANY;
550 } else if ("*".equals(key)) {
551 if (regexp) {
552 mode = Mode.ANY_KEY_REGEXP;
553 } else {
554 mode = Mode.ANY_KEY;
555 }
556 } else if ("*".equals(this.value)) {
557 if (regexp) {
558 mode = Mode.ANY_VALUE_REGEXP;
559 } else {
560 mode = Mode.ANY_VALUE;
561 }
562 } else {
563 if (regexp) {
564 mode = Mode.EXACT_REGEXP;
565 } else {
566 mode = Mode.EXACT;
567 }
568 }
569
570 if (regexp && key.length() > 0 && !key.equals("*")) {
571 try {
572 keyPattern = Pattern.compile(key, regexFlags(false));
573 } catch (PatternSyntaxException e) {
574 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()));
575 } catch (Exception e) {
576 throw new ParseError(tr(rxErrorMsgNoPos, key, e.getMessage()));
577 }
578 } else {
579 keyPattern = null;
580 }
581 if (regexp && this.value.length() > 0 && !this.value.equals("*")) {
582 try {
583 valuePattern = Pattern.compile(this.value, regexFlags(false));
584 } catch (PatternSyntaxException e) {
585 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()));
586 } catch (Exception e) {
587 throw new ParseError(tr(rxErrorMsgNoPos, value, e.getMessage()));
588 }
589 } else {
590 valuePattern = null;
591 }
592 }
593
594 @Override
595 public boolean match(OsmPrimitive osm) {
596
597 if (!osm.hasKeys())
598 return mode == Mode.NONE;
599
600 switch (mode) {
601 case NONE:
602 return false;
603 case MISSING_KEY:
604 return osm.get(key) == null;
605 case ANY:
606 return true;
607 case ANY_VALUE:
608 return osm.get(key) != null;
609 case ANY_KEY:
610 for (String v:osm.getKeys().values()) {
611 if (v.equals(value))
612 return true;
613 }
614 return false;
615 case EXACT:
616 return value.equals(osm.get(key));
617 case ANY_KEY_REGEXP:
618 for (String v:osm.getKeys().values()) {
619 if (valuePattern.matcher(v).matches())
620 return true;
621 }
622 return false;
623 case ANY_VALUE_REGEXP:
624 case EXACT_REGEXP:
625 for (String key: osm.keySet()) {
626 if (keyPattern.matcher(key).matches()) {
627 if (mode == Mode.ANY_VALUE_REGEXP
628 || valuePattern.matcher(osm.get(key)).matches())
629 return true;
630 }
631 }
632 return false;
633 case MISSING_KEY_REGEXP:
634 for (String k:osm.keySet()) {
635 if (keyPattern.matcher(k).matches())
636 return false;
637 }
638 return true;
639 }
640 throw new AssertionError("Missed state");
641 }
642
643 @Override
644 public String toString() {
645 return key + '=' + value;
646 }
647
648 }
649
650 /**
651 * Match a string in any tags (key or value), with optional regex and case insensitivity.
652 */
653 private static class Any extends Match {
654 private final String search;
655 private final Pattern searchRegex;
656 private final boolean caseSensitive;
657
658 public Any(String s, boolean regexSearch, boolean caseSensitive) throws ParseError {
659 s = Normalizer.normalize(s, Normalizer.Form.NFC);
660 this.caseSensitive = caseSensitive;
661 if (regexSearch) {
662 try {
663 this.searchRegex = Pattern.compile(s, regexFlags(caseSensitive));
664 } catch (PatternSyntaxException e) {
665 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()));
666 } catch (Exception e) {
667 throw new ParseError(tr(rxErrorMsgNoPos, s, e.getMessage()));
668 }
669 this.search = s;
670 } else if (caseSensitive) {
671 this.search = s;
672 this.searchRegex = null;
673 } else {
674 this.search = s.toLowerCase();
675 this.searchRegex = null;
676 }
677 }
678
679 @Override public boolean match(OsmPrimitive osm) {
680 if (!osm.hasKeys() && osm.getUser() == null)
681 return search.isEmpty();
682
683 for (String key: osm.keySet()) {
684 String value = osm.get(key);
685 if (searchRegex != null) {
686
687 value = Normalizer.normalize(value, Normalizer.Form.NFC);
688
689 Matcher keyMatcher = searchRegex.matcher(key);
690 Matcher valMatcher = searchRegex.matcher(value);
691
692 boolean keyMatchFound = keyMatcher.find();
693 boolean valMatchFound = valMatcher.find();
694
695 if (keyMatchFound || valMatchFound)
696 return true;
697 } else {
698 if (!caseSensitive) {
699 key = key.toLowerCase();
700 value = value.toLowerCase();
701 }
702
703 value = Normalizer.normalize(value, Normalizer.Form.NFC);
704
705 if (key.indexOf(search) != -1 || value.indexOf(search) != -1)
706 return true;
707 }
708 }
709 return false;
710 }
711 @Override public String toString() {
712 return search;
713 }
714 }
715
716 // TODO: change how we handle this
717 private static class ExactType extends Match {
718 private final Class<?> type;
719 public ExactType(String type) throws ParseError {
720 if ("node".equals(type)) {
721 this.type = Node.class;
722 } else if ("way".equals(type)) {
723 this.type = Way.class;
724 } else if ("relation".equals(type)) {
725 this.type = Relation.class;
726 } else
727 throw new ParseError(tr("Unknown primitive type: {0}. Allowed values are node, way or relation",
728 type));
729 }
730 @Override public boolean match(OsmPrimitive osm) {
731 return osm.getClass() == type;
732 }
733 @Override public String toString() {return "type="+type;}
734 }
735
736 /**
737 * Matches objects last changed by the given username.
738 */
739 private static class UserMatch extends Match {
740 private String user;
741 public UserMatch(String user) {
742 if (user.equals("anonymous")) {
743 this.user = null;
744 } else {
745 this.user = user;
746 }
747 }
748
749 @Override public boolean match(OsmPrimitive osm) {
750 if (osm.getUser() == null)
751 return user == null;
752 else
753 return osm.getUser().hasName(user);
754 }
755
756 @Override public String toString() {
757 return "user=" + user == null ? "" : user;
758 }
759 }
760
761 /**
762 * Matches objects with the given relation role (i.e. "outer").
763 */
764 private static class RoleMatch extends Match {
765 private String role;
766 public RoleMatch(String role) {
767 if (role == null) {
768 this.role = "";
769 } else {
770 this.role = role;
771 }
772 }
773
774 @Override public boolean match(OsmPrimitive osm) {
775 for (OsmPrimitive ref: osm.getReferrers()) {
776 if (ref instanceof Relation && !ref.isIncomplete() && !ref.isDeleted()) {
777 for (RelationMember m : ((Relation) ref).getMembers()) {
778 if (m.getMember() == osm) {
779 String testRole = m.getRole();
780 if(role.equals(testRole == null ? "" : testRole))
781 return true;
782 }
783 }
784 }
785 }
786 return false;
787 }
788
789 @Override public String toString() {
790 return "role=" + role;
791 }
792 }
793
794 /**
795 * Matches the n-th object of a relation and/or the n-th node of a way.
796 */
797 private static class Nth extends Match {
798
799 private final int nth;
800 private final boolean modulo;
801
802 public Nth(PushbackTokenizer tokenizer, boolean modulo) throws ParseError {
803 this((int) tokenizer.readNumber(tr("Primitive id expected")), modulo);
804 }
805
806 private Nth(int nth, boolean modulo) {
807 this.nth = nth;
808 this.modulo = modulo;
809 }
810
811 @Override
812 public boolean match(OsmPrimitive osm) {
813 for (OsmPrimitive p : osm.getReferrers()) {
814 Integer idx = null;
815 if (p instanceof Way) {
816 Way w = (Way) p;
817 idx = w.getNodes().indexOf(osm);
818 } else if (p instanceof Relation) {
819 Relation r = (Relation) p;
820 idx = r.getMemberPrimitivesList().indexOf(osm);
821 }
822 if (idx != null) {
823 if (idx.intValue() == nth || (modulo && idx.intValue() % nth == 0)) {
824 return true;
825 }
826 }
827 }
828 return false;
829 }
830 }
831
832 /**
833 * Matches objects with properties in a certain range.
834 */
835 private abstract static class RangeMatch extends Match {
836
837 private final long min;
838 private final long max;
839
840 public RangeMatch(long min, long max) {
841 this.min = Math.min(min, max);
842 this.max = Math.max(min, max);
843 }
844
845 public RangeMatch(Range range) {
846 this(range.getStart(), range.getEnd());
847 }
848
849 protected abstract Long getNumber(OsmPrimitive osm);
850
851 protected abstract String getString();
852
853 @Override
854 public boolean match(OsmPrimitive osm) {
855 Long num = getNumber(osm);
856 if (num == null)
857 return false;
858 else
859 return (num >= min) && (num <= max);
860 }
861
862 @Override
863 public String toString() {
864 return getString() + "=" + min + "-" + max;
865 }
866 }
867
868
869 /**
870 * Matches ways with a number of nodes in given range
871 */
872 private static class NodeCountRange extends RangeMatch {
873 public NodeCountRange(Range range) {
874 super(range);
875 }
876
877 public NodeCountRange(PushbackTokenizer tokenizer) throws ParseError {
878 this(tokenizer.readRange(tr("Range of numbers expected")));
879 }
880
881 @Override
882 protected Long getNumber(OsmPrimitive osm) {
883 if (!(osm instanceof Way))
884 return null;
885 else
886 return (long) ((Way) osm).getRealNodesCount();
887 }
888
889 @Override
890 protected String getString() {
891 return "nodes";
892 }
893 }
894
895 /**
896 * Matches objects with a number of tags in given range
897 */
898 private static class TagCountRange extends RangeMatch {
899 public TagCountRange(Range range) {
900 super(range);
901 }
902
903 public TagCountRange(PushbackTokenizer tokenizer) throws ParseError {
904 this(tokenizer.readRange(tr("Range of numbers expected")));
905 }
906
907 @Override
908 protected Long getNumber(OsmPrimitive osm) {
909 return (long) osm.getKeys().size();
910 }
911
912 @Override
913 protected String getString() {
914 return "tags";
915 }
916 }
917
918 /**
919 * Matches objects with a timestamp in given range
920 */
921 private static class TimestampRange extends RangeMatch {
922
923 public TimestampRange(long minCount, long maxCount) {
924 super(minCount, maxCount);
925 }
926
927 @Override
928 protected Long getNumber(OsmPrimitive osm) {
929 return osm.getTimestamp().getTime();
930 }
931
932 @Override
933 protected String getString() {
934 return "timestamp";
935 }
936
937 }
938
939 /**
940 * Matches objects that are new (i.e. have not been uploaded to the server)
941 */
942 private static class New extends Match {
943 @Override public boolean match(OsmPrimitive osm) {
944 return osm.isNew();
945 }
946 @Override public String toString() {
947 return "new";
948 }
949 }
950
951 /**
952 * Matches all objects that have been modified, created, or undeleted
953 */
954 private static class Modified extends Match {
955 @Override public boolean match(OsmPrimitive osm) {
956 return osm.isModified() || osm.isNewOrUndeleted();
957 }
958 @Override public String toString() {return "modified";}
959 }
960
961 /**
962 * Matches all objects currently selected
963 */
964 private static class Selected extends Match {
965 @Override public boolean match(OsmPrimitive osm) {
966 return Main.main.getCurrentDataSet().isSelected(osm);
967 }
968 @Override public String toString() {return "selected";}
969 }
970
971 /**
972 * Match objects that are incomplete, where only id and type are known.
973 * Typically some members of a relation are incomplete until they are
974 * fetched from the server.
975 */
976 private static class Incomplete extends Match {
977 @Override public boolean match(OsmPrimitive osm) {
978 return osm.isIncomplete();
979 }
980 @Override public String toString() {return "incomplete";}
981 }
982
983 /**
984 * Matches objects that don't have any interesting tags (i.e. only has source,
985 * FIXME, etc.). The complete list of uninteresting tags can be found here:
986 * org.openstreetmap.josm.data.osm.OsmPrimitive.getUninterestingKeys()
987 */
988 private static class Untagged extends Match {
989 @Override public boolean match(OsmPrimitive osm) {
990 return !osm.isTagged() && !osm.isIncomplete();
991 }
992 @Override public String toString() {return "untagged";}
993 }
994
995 /**
996 * Matches ways which are closed (i.e. first and last node are the same)
997 */
998 private static class Closed extends Match {
999 @Override public boolean match(OsmPrimitive osm) {
1000 return osm instanceof Way && ((Way) osm).isClosed();
1001 }
1002 @Override public String toString() {return "closed";}
1003 }
1004
1005 /**
1006 * Matches objects if they are parents of the expression
1007 */
1008 public static class Parent extends UnaryMatch {
1009 public Parent(Match m) {
1010 super(m);
1011 }
1012 @Override public boolean match(OsmPrimitive osm) {
1013 boolean isParent = false;
1014
1015 if (osm instanceof Way) {
1016 for (Node n : ((Way)osm).getNodes()) {
1017 isParent |= match.match(n);
1018 }
1019 } else if (osm instanceof Relation) {
1020 for (RelationMember member : ((Relation)osm).getMembers()) {
1021 isParent |= match.match(member.getMember());
1022 }
1023 }
1024 return isParent;
1025 }
1026 @Override public String toString() {return "parent(" + match + ")";}
1027 }
1028
1029 /**
1030 * Matches objects if they are children of the expression
1031 */
1032 public static class Child extends UnaryMatch {
1033
1034 public Child(Match m) {
1035 super(m);
1036 }
1037
1038 @Override public boolean match(OsmPrimitive osm) {
1039 boolean isChild = false;
1040 for (OsmPrimitive p : osm.getReferrers()) {
1041 isChild |= match.match(p);
1042 }
1043 return isChild;
1044 }
1045 @Override public String toString() {return "child(" + match + ")";}
1046 }
1047
1048 /**
1049 * Matches if the size of the area is within the given range
1050 *
1051 * @author Ole Jørgen Brønner
1052 */
1053 private static class AreaSize extends RangeMatch {
1054
1055 public AreaSize(Range range) {
1056 super(range);
1057 }
1058
1059 public AreaSize(PushbackTokenizer tokenizer) throws ParseError {
1060 this(tokenizer.readRange(tr("Range of numbers expected")));
1061 }
1062
1063 @Override
1064 protected Long getNumber(OsmPrimitive osm) {
1065 if (!(osm instanceof Way && ((Way) osm).isClosed()))
1066 return null;
1067 Way way = (Way) osm;
1068 return (long) Geometry.closedWayArea(way);
1069 }
1070
1071 @Override
1072 protected String getString() {
1073 return "areasize";
1074 }
1075 }
1076
1077 /**
1078 * Matches objects within the given bounds.
1079 */
1080 private abstract static class InArea extends Match {
1081
1082 protected abstract Bounds getBounds();
1083 protected final boolean all;
1084
1085 /**
1086 * @param all if true, all way nodes or relation members have to be within source area;if false, one suffices.
1087 */
1088 public InArea(boolean all) {
1089 this.all = all;
1090 }
1091
1092 @Override
1093 public boolean match(OsmPrimitive osm) {
1094 if (!osm.isUsable())
1095 return false;
1096 else if (osm instanceof Node) {
1097 Bounds bounds = getBounds();
1098 return bounds != null && bounds.contains(((Node) osm).getCoor());
1099 } else if (osm instanceof Way) {
1100 Collection<Node> nodes = ((Way) osm).getNodes();
1101 return all ? forallMatch(nodes) : existsMatch(nodes);
1102 } else if (osm instanceof Relation) {
1103 Collection<OsmPrimitive> primitives = ((Relation) osm).getMemberPrimitives();
1104 return all ? forallMatch(primitives) : existsMatch(primitives);
1105 } else
1106 return false;
1107 }
1108 }
1109
1110 /**
1111 * Matches objects within source area ("downloaded area").
1112 */
1113 private static class InDataSourceArea extends InArea {
1114
1115 public InDataSourceArea(boolean all) {
1116 super(all);
1117 }
1118
1119 @Override
1120 protected Bounds getBounds() {
1121 return new Bounds(Main.main.getCurrentDataSet().getDataSourceArea().getBounds2D());
1122 }
1123 }
1124
1125 /**
1126 * Matches objects within current map view.
1127 */
1128 private static class InView extends InArea {
1129
1130 public InView(boolean all) {
1131 super(all);
1132 }
1133
1134 @Override
1135 protected Bounds getBounds() {
1136 if (Main.map == null || Main.map.mapView == null) {
1137 return null;
1138 }
1139 return Main.map.mapView.getRealBounds();
1140 }
1141 }
1142
1143 public static class ParseError extends Exception {
1144 public ParseError(String msg) {
1145 super(msg);
1146 }
1147 public ParseError(Token expected, Token found) {
1148 this(tr("Unexpected token. Expected {0}, found {1}", expected, found));
1149 }
1150 }
1151
1152 public static Match compile(String searchStr, boolean caseSensitive, boolean regexSearch)
1153 throws ParseError {
1154 return new SearchCompiler(caseSensitive, regexSearch,
1155 new PushbackTokenizer(
1156 new PushbackReader(new StringReader(searchStr))))
1157 .parse();
1158 }
1159
1160 /**
1161 * Parse search string.
1162 *
1163 * @return match determined by search string
1164 * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError
1165 */
1166 public Match parse() throws ParseError {
1167 Match m = parseExpression();
1168 if (!tokenizer.readIfEqual(Token.EOF))
1169 throw new ParseError(tr("Unexpected token: {0}", tokenizer.nextToken()));
1170 if (m == null)
1171 return new Always();
1172 return m;
1173 }
1174
1175 /**
1176 * Parse expression. This is a recursive method.
1177 *
1178 * @return match determined by parsing expression
1179 * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError
1180 */
1181 private Match parseExpression() throws ParseError {
1182 Match factor = parseFactor();
1183 if (factor == null)
1184 // empty search string
1185 return null;
1186 if (tokenizer.readIfEqual(Token.OR))
1187 return new Or(factor, parseExpression(tr("Missing parameter for OR")));
1188 else if (tokenizer.readIfEqual(Token.XOR))
1189 return new Xor(factor, parseExpression(tr("Missing parameter for XOR")));
1190 else {
1191 Match expression = parseExpression();
1192 if (expression == null)
1193 // reached end of search string, no more recursive calls
1194 return factor;
1195 else
1196 // the default operator is AND
1197 return new And(factor, expression);
1198 }
1199 }
1200
1201 /**
1202 * Parse expression, showing the specified error message if parsing fails.
1203 *
1204 * @param errorMessage to display if parsing error occurs
1205 * @return match determined by parsing expression
1206 * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError
1207 * @see #parseExpression()
1208 */
1209 private Match parseExpression(String errorMessage) throws ParseError {
1210 Match expression = parseExpression();
1211 if (expression == null)
1212 throw new ParseError(errorMessage);
1213 else
1214 return expression;
1215 }
1216
1217 /**
1218 * Parse next factor (a search operator or search term).
1219 *
1220 * @return match determined by parsing factor string
1221 * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError
1222 */
1223 private Match parseFactor() throws ParseError {
1224 if (tokenizer.readIfEqual(Token.LEFT_PARENT)) {
1225 Match expression = parseExpression();
1226 if (!tokenizer.readIfEqual(Token.RIGHT_PARENT))
1227 throw new ParseError(Token.RIGHT_PARENT, tokenizer.nextToken());
1228 return expression;
1229 } else if (tokenizer.readIfEqual(Token.NOT)) {
1230 return new Not(parseFactor(tr("Missing operator for NOT")));
1231 } else if (tokenizer.readIfEqual(Token.KEY)) {
1232 // factor consists of key:value or key=value
1233 String key = tokenizer.getText();
1234 if (tokenizer.readIfEqual(Token.EQUALS))
1235 return new ExactKeyValue(regexSearch, key, tokenizer.readTextOrNumber());
1236 else if (tokenizer.readIfEqual(Token.COLON)) {
1237 // see if we have a Match that takes a data parameter
1238 SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
1239 if (factory != null)
1240 return factory.get(key, tokenizer);
1241
1242 UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
1243 if (unaryFactory != null)
1244 return unaryFactory.get(key, parseFactor(), tokenizer);
1245
1246 // key:value form where value is a string (may be OSM key search)
1247 return parseKV(key, tokenizer.readTextOrNumber());
1248 } else if (tokenizer.readIfEqual(Token.QUESTION_MARK))
1249 return new BooleanMatch(key, false);
1250 else {
1251 SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
1252 if (factory != null)
1253 return factory.get(key, null);
1254
1255 UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
1256 if (unaryFactory != null)
1257 return unaryFactory.get(key, parseFactor(), null);
1258
1259 // match string in any key or value
1260 return new Any(key, regexSearch, caseSensitive);
1261 }
1262 } else
1263 return null;
1264 }
1265
1266 private Match parseFactor(String errorMessage) throws ParseError {
1267 Match fact = parseFactor();
1268 if (fact == null)
1269 throw new ParseError(errorMessage);
1270 else
1271 return fact;
1272 }
1273
1274 private Match parseKV(String key, String value) throws ParseError {
1275 if (value == null) {
1276 value = "";
1277 }
1278 if (key.equals("type"))
1279 return new ExactType(value);
1280 else if (key.equals("user"))
1281 return new UserMatch(value);
1282 else if (key.equals("role"))
1283 return new RoleMatch(value);
1284 else
1285 return new KeyValue(key, value, regexSearch, caseSensitive);
1286 }
1287
1288 private static int regexFlags(boolean caseSensitive) {
1289 int searchFlags = 0;
1290
1291 // Enables canonical Unicode equivalence so that e.g. the two
1292 // forms of "\u00e9gal" and "e\u0301gal" will match.
1293 //
1294 // It makes sense to match no matter how the character
1295 // happened to be constructed.
1296 searchFlags |= Pattern.CANON_EQ;
1297
1298 // Make "." match any character including newline (/s in Perl)
1299 searchFlags |= Pattern.DOTALL;
1300
1301 // CASE_INSENSITIVE by itself only matches US-ASCII case
1302 // insensitively, but the OSM data is in Unicode. With
1303 // UNICODE_CASE casefolding is made Unicode-aware.
1304 if (!caseSensitive) {
1305 searchFlags |= (Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
1306 }
1307
1308 return searchFlags;
1309 }
1310}
Note: See TracBrowser for help on using the repository browser.