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

Last change on this file since 8812 was 8812, checked in by simon04, 9 years ago

see #11916 - Search: allow to use MapCSS selectors

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