Ticket #7178: SearchCompiler.java

File SearchCompiler.java, 49.1 KB (added by joshdoe, 14 years ago)

SearchCompiler using factories/reflection

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