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

Last change on this file since 4817 was 4817, checked in by bastiK, 12 years ago

applied #7178 - Allow plugins to register search operators (patch by joshdoe)

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