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

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

fix #11351 - Search/ways:: make implementation comply with description (… nodes with specified number of referring ways …)

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