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

Last change on this file since 8404 was 8404, checked in by Don-vip, 9 years ago

When doing a String.toLowerCase()/toUpperCase() call, use a Locale. This avoids problems with certain locales, i.e. Lithuanian or Turkish. See PMD UseLocaleWithCaseConversions rule and String.toLowerCase() javadoc.

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