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

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

fix #11150 - False validator warning on ford if node is outside the downloaded area and highway or waterway is not loaded

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