source: josm/trunk/src/org/openstreetmap/josm/data/osm/search/SearchCompiler.java@ 14129

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

code refactoring

  • Property svn:eol-style set to native
File size: 73.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm.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.ArrayList;
11import java.util.Arrays;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.HashMap;
15import java.util.List;
16import java.util.Locale;
17import java.util.Map;
18import java.util.Optional;
19import java.util.function.Predicate;
20import java.util.regex.Matcher;
21import java.util.regex.Pattern;
22import java.util.regex.PatternSyntaxException;
23import java.util.stream.Collectors;
24
25import org.openstreetmap.josm.data.Bounds;
26import org.openstreetmap.josm.data.coor.LatLon;
27import org.openstreetmap.josm.data.osm.Node;
28import org.openstreetmap.josm.data.osm.OsmPrimitive;
29import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
30import org.openstreetmap.josm.data.osm.OsmUtils;
31import org.openstreetmap.josm.data.osm.Relation;
32import org.openstreetmap.josm.data.osm.RelationMember;
33import org.openstreetmap.josm.data.osm.Tagged;
34import org.openstreetmap.josm.data.osm.Way;
35import org.openstreetmap.josm.data.osm.search.PushbackTokenizer.Range;
36import org.openstreetmap.josm.data.osm.search.PushbackTokenizer.Token;
37import org.openstreetmap.josm.data.projection.ProjectionRegistry;
38import org.openstreetmap.josm.gui.mappaint.Environment;
39import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
40import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
41import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
42import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
43import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetMenu;
44import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSeparator;
45import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
46import org.openstreetmap.josm.tools.AlphanumComparator;
47import org.openstreetmap.josm.tools.Geometry;
48import org.openstreetmap.josm.tools.Logging;
49import org.openstreetmap.josm.tools.UncheckedParseException;
50import org.openstreetmap.josm.tools.Utils;
51import org.openstreetmap.josm.tools.date.DateUtils;
52
53/**
54 * Implements a google-like search.
55 * <br>
56 * Grammar:
57 * <pre>
58 * expression =
59 * fact | expression
60 * fact expression
61 * fact
62 *
63 * fact =
64 * ( expression )
65 * -fact
66 * term?
67 * term=term
68 * term:term
69 * term
70 * </pre>
71 *
72 * @author Imi
73 * @since 12656 (moved from actions.search package)
74 */
75public class SearchCompiler {
76
77 private final boolean caseSensitive;
78 private final boolean regexSearch;
79 private static String rxErrorMsg = marktr("The regex \"{0}\" had a parse error at offset {1}, full error:\n\n{2}");
80 private static String rxErrorMsgNoPos = marktr("The regex \"{0}\" had a parse error, full error:\n\n{1}");
81 private final PushbackTokenizer tokenizer;
82 private static Map<String, SimpleMatchFactory> simpleMatchFactoryMap = new HashMap<>();
83 private static Map<String, UnaryMatchFactory> unaryMatchFactoryMap = new HashMap<>();
84 private static Map<String, BinaryMatchFactory> binaryMatchFactoryMap = new HashMap<>();
85
86 static {
87 addMatchFactory(new CoreSimpleMatchFactory());
88 addMatchFactory(new CoreUnaryMatchFactory());
89 }
90
91 /**
92 * Constructs a new {@code SearchCompiler}.
93 * @param caseSensitive {@code true} to perform a case-sensitive search
94 * @param regexSearch {@code true} to perform a regex-based search
95 * @param tokenizer to split the search string into tokens
96 */
97 public SearchCompiler(boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) {
98 this.caseSensitive = caseSensitive;
99 this.regexSearch = regexSearch;
100 this.tokenizer = tokenizer;
101 }
102
103 /**
104 * Add (register) MatchFactory with SearchCompiler
105 * @param factory match factory
106 */
107 public static void addMatchFactory(MatchFactory factory) {
108 for (String keyword : factory.getKeywords()) {
109 final MatchFactory existing;
110 if (factory instanceof SimpleMatchFactory) {
111 existing = simpleMatchFactoryMap.put(keyword, (SimpleMatchFactory) factory);
112 } else if (factory instanceof UnaryMatchFactory) {
113 existing = unaryMatchFactoryMap.put(keyword, (UnaryMatchFactory) factory);
114 } else if (factory instanceof BinaryMatchFactory) {
115 existing = binaryMatchFactoryMap.put(keyword, (BinaryMatchFactory) factory);
116 } else
117 throw new AssertionError("Unknown match factory");
118 if (existing != null) {
119 Logging.warn("SearchCompiler: for key ''{0}'', overriding match factory ''{1}'' with ''{2}''", keyword, existing, factory);
120 }
121 }
122 }
123
124 public static class CoreSimpleMatchFactory implements SimpleMatchFactory {
125 private final Collection<String> keywords = Arrays.asList("id", "version", "type", "user", "role",
126 "changeset", "nodes", "ways", "tags", "areasize", "waylength", "modified", "deleted", "selected",
127 "incomplete", "untagged", "closed", "new", "indownloadedarea",
128 "allindownloadedarea", "timestamp", "nth", "nth%", "hasRole", "preset");
129
130 @Override
131 public Match get(String keyword, boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) throws SearchParseError {
132 switch(keyword) {
133 case "modified":
134 return new Modified();
135 case "deleted":
136 return new Deleted();
137 case "selected":
138 return new Selected();
139 case "incomplete":
140 return new Incomplete();
141 case "untagged":
142 return new Untagged();
143 case "closed":
144 return new Closed();
145 case "new":
146 return new New();
147 case "indownloadedarea":
148 return new InDataSourceArea(false);
149 case "allindownloadedarea":
150 return new InDataSourceArea(true);
151 default:
152 if (tokenizer != null) {
153 switch (keyword) {
154 case "id":
155 return new Id(tokenizer);
156 case "version":
157 return new Version(tokenizer);
158 case "type":
159 return new ExactType(tokenizer.readTextOrNumber());
160 case "preset":
161 return new Preset(tokenizer.readTextOrNumber());
162 case "user":
163 return new UserMatch(tokenizer.readTextOrNumber());
164 case "role":
165 return new RoleMatch(tokenizer.readTextOrNumber());
166 case "changeset":
167 return new ChangesetId(tokenizer);
168 case "nodes":
169 return new NodeCountRange(tokenizer);
170 case "ways":
171 return new WayCountRange(tokenizer);
172 case "tags":
173 return new TagCountRange(tokenizer);
174 case "areasize":
175 return new AreaSize(tokenizer);
176 case "waylength":
177 return new WayLength(tokenizer);
178 case "nth":
179 return new Nth(tokenizer, false);
180 case "nth%":
181 return new Nth(tokenizer, true);
182 case "hasRole":
183 return new HasRole(tokenizer);
184 case "timestamp":
185 // add leading/trailing space in order to get expected split (e.g. "a--" => {"a", ""})
186 String rangeS = ' ' + tokenizer.readTextOrNumber() + ' ';
187 String[] rangeA = rangeS.split("/");
188 if (rangeA.length == 1) {
189 return new KeyValue(keyword, rangeS.trim(), regexSearch, caseSensitive);
190 } else if (rangeA.length == 2) {
191 String rangeA1 = rangeA[0].trim();
192 String rangeA2 = rangeA[1].trim();
193 final long minDate;
194 final long maxDate;
195 try {
196 // if min timestap is empty: use lowest possible date
197 minDate = DateUtils.fromString(rangeA1.isEmpty() ? "1980" : rangeA1).getTime();
198 } catch (UncheckedParseException ex) {
199 throw new SearchParseError(tr("Cannot parse timestamp ''{0}''", rangeA1), ex);
200 }
201 try {
202 // if max timestamp is empty: use "now"
203 maxDate = rangeA2.isEmpty() ? System.currentTimeMillis() : DateUtils.fromString(rangeA2).getTime();
204 } catch (UncheckedParseException ex) {
205 throw new SearchParseError(tr("Cannot parse timestamp ''{0}''", rangeA2), ex);
206 }
207 return new TimestampRange(minDate, maxDate);
208 } else {
209 throw new SearchParseError("<html>" + tr("Expecting {0} after {1}", "<i>min</i>/<i>max</i>", "<i>timestamp</i>")
210 + "</html>");
211 }
212 }
213 } else {
214 throw new SearchParseError("<html>" + tr("Expecting {0} after {1}", "<code>:</code>", "<i>" + keyword + "</i>") + "</html>");
215 }
216 }
217 throw new IllegalStateException("Not expecting keyword " + keyword);
218 }
219
220 @Override
221 public Collection<String> getKeywords() {
222 return keywords;
223 }
224 }
225
226 public static class CoreUnaryMatchFactory implements UnaryMatchFactory {
227 private static Collection<String> keywords = Arrays.asList("parent", "child");
228
229 @Override
230 public UnaryMatch get(String keyword, Match matchOperand, PushbackTokenizer tokenizer) {
231 if ("parent".equals(keyword))
232 return new Parent(matchOperand);
233 else if ("child".equals(keyword))
234 return new Child(matchOperand);
235 return null;
236 }
237
238 @Override
239 public Collection<String> getKeywords() {
240 return keywords;
241 }
242 }
243
244 /**
245 * Classes implementing this interface can provide Match operators.
246 * @since 10600 (functional interface)
247 */
248 @FunctionalInterface
249 private interface MatchFactory {
250 Collection<String> getKeywords();
251 }
252
253 public interface SimpleMatchFactory extends MatchFactory {
254 Match get(String keyword, boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) throws SearchParseError;
255 }
256
257 public interface UnaryMatchFactory extends MatchFactory {
258 UnaryMatch get(String keyword, Match matchOperand, PushbackTokenizer tokenizer) throws SearchParseError;
259 }
260
261 public interface BinaryMatchFactory extends MatchFactory {
262 AbstractBinaryMatch get(String keyword, Match lhs, Match rhs, PushbackTokenizer tokenizer) throws SearchParseError;
263 }
264
265 /**
266 * Base class for all search criteria. If the criterion only depends on an object's tags,
267 * inherit from {@link org.openstreetmap.josm.data.osm.search.SearchCompiler.TaggedMatch}.
268 */
269 public abstract static class Match implements Predicate<OsmPrimitive> {
270
271 /**
272 * Tests whether the primitive matches this criterion.
273 * @param osm the primitive to test
274 * @return true if the primitive matches this criterion
275 */
276 public abstract boolean match(OsmPrimitive osm);
277
278 /**
279 * Tests whether the tagged object matches this criterion.
280 * @param tagged the tagged object to test
281 * @return true if the tagged object matches this criterion
282 */
283 public boolean match(Tagged tagged) {
284 return tagged instanceof OsmPrimitive && match((OsmPrimitive) tagged);
285 }
286
287 @Override
288 public final boolean test(OsmPrimitive object) {
289 return match(object);
290 }
291 }
292
293 public abstract static class TaggedMatch extends Match {
294
295 @Override
296 public abstract boolean match(Tagged tags);
297
298 @Override
299 public final boolean match(OsmPrimitive osm) {
300 return match((Tagged) osm);
301 }
302
303 protected static Pattern compilePattern(String regex, int flags) throws SearchParseError {
304 try {
305 return Pattern.compile(regex, flags);
306 } catch (PatternSyntaxException e) {
307 throw new SearchParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
308 } catch (IllegalArgumentException | StringIndexOutOfBoundsException e) {
309 // StringIndexOutOfBoundsException catched because of https://bugs.openjdk.java.net/browse/JI-9044959
310 // See #13870: To remove after we switch to a version of Java which resolves this bug
311 throw new SearchParseError(tr(rxErrorMsgNoPos, regex, e.getMessage()), e);
312 }
313 }
314 }
315
316 /**
317 * A unary search operator which may take data parameters.
318 */
319 public abstract static class UnaryMatch extends Match {
320
321 protected final Match match;
322
323 public UnaryMatch(Match match) {
324 if (match == null) {
325 // "operator" (null) should mean the same as "operator()"
326 // (Always). I.e. match everything
327 this.match = Always.INSTANCE;
328 } else {
329 this.match = match;
330 }
331 }
332
333 public Match getOperand() {
334 return match;
335 }
336
337 @Override
338 public int hashCode() {
339 return 31 + ((match == null) ? 0 : match.hashCode());
340 }
341
342 @Override
343 public boolean equals(Object obj) {
344 if (this == obj)
345 return true;
346 if (obj == null || getClass() != obj.getClass())
347 return false;
348 UnaryMatch other = (UnaryMatch) obj;
349 if (match == null) {
350 if (other.match != null)
351 return false;
352 } else if (!match.equals(other.match))
353 return false;
354 return true;
355 }
356 }
357
358 /**
359 * A binary search operator which may take data parameters.
360 */
361 public abstract static class AbstractBinaryMatch extends Match {
362
363 protected final Match lhs;
364 protected final Match rhs;
365
366 /**
367 * Constructs a new {@code BinaryMatch}.
368 * @param lhs Left hand side
369 * @param rhs Right hand side
370 */
371 public AbstractBinaryMatch(Match lhs, Match rhs) {
372 this.lhs = lhs;
373 this.rhs = rhs;
374 }
375
376 /**
377 * Returns left hand side.
378 * @return left hand side
379 */
380 public final Match getLhs() {
381 return lhs;
382 }
383
384 /**
385 * Returns right hand side.
386 * @return right hand side
387 */
388 public final Match getRhs() {
389 return rhs;
390 }
391
392 protected static String parenthesis(Match m) {
393 return '(' + m.toString() + ')';
394 }
395
396 @Override
397 public int hashCode() {
398 final int prime = 31;
399 int result = 1;
400 result = prime * result + ((lhs == null) ? 0 : lhs.hashCode());
401 result = prime * result + ((rhs == null) ? 0 : rhs.hashCode());
402 return result;
403 }
404
405 @Override
406 public boolean equals(Object obj) {
407 if (this == obj)
408 return true;
409 if (obj == null || getClass() != obj.getClass())
410 return false;
411 AbstractBinaryMatch other = (AbstractBinaryMatch) obj;
412 if (lhs == null) {
413 if (other.lhs != null)
414 return false;
415 } else if (!lhs.equals(other.lhs))
416 return false;
417 if (rhs == null) {
418 if (other.rhs != null)
419 return false;
420 } else if (!rhs.equals(other.rhs))
421 return false;
422 return true;
423 }
424 }
425
426 /**
427 * Matches every OsmPrimitive.
428 */
429 public static class Always extends TaggedMatch {
430 /** The unique instance/ */
431 public static final Always INSTANCE = new Always();
432 @Override
433 public boolean match(Tagged osm) {
434 return true;
435 }
436 }
437
438 /**
439 * Never matches any OsmPrimitive.
440 */
441 public static class Never extends TaggedMatch {
442 /** The unique instance/ */
443 public static final Never INSTANCE = new Never();
444 @Override
445 public boolean match(Tagged osm) {
446 return false;
447 }
448 }
449
450 /**
451 * Inverts the match.
452 */
453 public static class Not extends UnaryMatch {
454 public Not(Match match) {
455 super(match);
456 }
457
458 @Override
459 public boolean match(OsmPrimitive osm) {
460 return !match.match(osm);
461 }
462
463 @Override
464 public boolean match(Tagged osm) {
465 return !match.match(osm);
466 }
467
468 @Override
469 public String toString() {
470 return '!' + match.toString();
471 }
472
473 public Match getMatch() {
474 return match;
475 }
476 }
477
478 /**
479 * Matches if the value of the corresponding key is ''yes'', ''true'', ''1'' or ''on''.
480 */
481 private static class BooleanMatch extends TaggedMatch {
482 private final String key;
483 private final boolean defaultValue;
484
485 BooleanMatch(String key, boolean defaultValue) {
486 this.key = key;
487 this.defaultValue = defaultValue;
488 }
489
490 @Override
491 public boolean match(Tagged osm) {
492 return Optional.ofNullable(OsmUtils.getOsmBoolean(osm.get(key))).orElse(defaultValue);
493 }
494
495 @Override
496 public String toString() {
497 return key + '?';
498 }
499
500 @Override
501 public int hashCode() {
502 final int prime = 31;
503 int result = 1;
504 result = prime * result + (defaultValue ? 1231 : 1237);
505 result = prime * result + ((key == null) ? 0 : key.hashCode());
506 return result;
507 }
508
509 @Override
510 public boolean equals(Object obj) {
511 if (this == obj)
512 return true;
513 if (obj == null || getClass() != obj.getClass())
514 return false;
515 BooleanMatch other = (BooleanMatch) obj;
516 if (defaultValue != other.defaultValue)
517 return false;
518 if (key == null) {
519 if (other.key != null)
520 return false;
521 } else if (!key.equals(other.key))
522 return false;
523 return true;
524 }
525 }
526
527 /**
528 * Matches if both left and right expressions match.
529 */
530 public static class And extends AbstractBinaryMatch {
531 /**
532 * Constructs a new {@code And} match.
533 * @param lhs left hand side
534 * @param rhs right hand side
535 */
536 public And(Match lhs, Match rhs) {
537 super(lhs, rhs);
538 }
539
540 @Override
541 public boolean match(OsmPrimitive osm) {
542 return lhs.match(osm) && rhs.match(osm);
543 }
544
545 @Override
546 public boolean match(Tagged osm) {
547 return lhs.match(osm) && rhs.match(osm);
548 }
549
550 @Override
551 public String toString() {
552 return (lhs instanceof AbstractBinaryMatch && !(lhs instanceof And) ? parenthesis(lhs) : lhs) + " && "
553 + (rhs instanceof AbstractBinaryMatch && !(rhs instanceof And) ? parenthesis(rhs) : rhs);
554 }
555 }
556
557 /**
558 * Matches if the left OR the right expression match.
559 */
560 public static class Or extends AbstractBinaryMatch {
561 /**
562 * Constructs a new {@code Or} match.
563 * @param lhs left hand side
564 * @param rhs right hand side
565 */
566 public Or(Match lhs, Match rhs) {
567 super(lhs, rhs);
568 }
569
570 @Override
571 public boolean match(OsmPrimitive osm) {
572 return lhs.match(osm) || rhs.match(osm);
573 }
574
575 @Override
576 public boolean match(Tagged osm) {
577 return lhs.match(osm) || rhs.match(osm);
578 }
579
580 @Override
581 public String toString() {
582 return (lhs instanceof AbstractBinaryMatch && !(lhs instanceof Or) ? parenthesis(lhs) : lhs) + " || "
583 + (rhs instanceof AbstractBinaryMatch && !(rhs instanceof Or) ? parenthesis(rhs) : rhs);
584 }
585 }
586
587 /**
588 * Matches if the left OR the right expression match, but not both.
589 */
590 public static class Xor extends AbstractBinaryMatch {
591 /**
592 * Constructs a new {@code Xor} match.
593 * @param lhs left hand side
594 * @param rhs right hand side
595 */
596 public Xor(Match lhs, Match rhs) {
597 super(lhs, rhs);
598 }
599
600 @Override
601 public boolean match(OsmPrimitive osm) {
602 return lhs.match(osm) ^ rhs.match(osm);
603 }
604
605 @Override
606 public boolean match(Tagged osm) {
607 return lhs.match(osm) ^ rhs.match(osm);
608 }
609
610 @Override
611 public String toString() {
612 return (lhs instanceof AbstractBinaryMatch && !(lhs instanceof Xor) ? parenthesis(lhs) : lhs) + " ^ "
613 + (rhs instanceof AbstractBinaryMatch && !(rhs instanceof Xor) ? parenthesis(rhs) : rhs);
614 }
615 }
616
617 /**
618 * Matches objects with ID in the given range.
619 */
620 private static class Id extends RangeMatch {
621 Id(Range range) {
622 super(range);
623 }
624
625 Id(PushbackTokenizer tokenizer) throws SearchParseError {
626 this(tokenizer.readRange(tr("Range of primitive ids expected")));
627 }
628
629 @Override
630 protected Long getNumber(OsmPrimitive osm) {
631 return osm.isNew() ? 0 : osm.getUniqueId();
632 }
633
634 @Override
635 protected String getString() {
636 return "id";
637 }
638 }
639
640 /**
641 * Matches objects with a changeset ID in the given range.
642 */
643 private static class ChangesetId extends RangeMatch {
644 ChangesetId(Range range) {
645 super(range);
646 }
647
648 ChangesetId(PushbackTokenizer tokenizer) throws SearchParseError {
649 this(tokenizer.readRange(tr("Range of changeset ids expected")));
650 }
651
652 @Override
653 protected Long getNumber(OsmPrimitive osm) {
654 return (long) osm.getChangesetId();
655 }
656
657 @Override
658 protected String getString() {
659 return "changeset";
660 }
661 }
662
663 /**
664 * Matches objects with a version number in the given range.
665 */
666 private static class Version extends RangeMatch {
667 Version(Range range) {
668 super(range);
669 }
670
671 Version(PushbackTokenizer tokenizer) throws SearchParseError {
672 this(tokenizer.readRange(tr("Range of versions expected")));
673 }
674
675 @Override
676 protected Long getNumber(OsmPrimitive osm) {
677 return (long) osm.getVersion();
678 }
679
680 @Override
681 protected String getString() {
682 return "version";
683 }
684 }
685
686 /**
687 * Matches objects with the given key-value pair.
688 */
689 private static class KeyValue extends TaggedMatch {
690 private final String key;
691 private final Pattern keyPattern;
692 private final String value;
693 private final Pattern valuePattern;
694 private final boolean caseSensitive;
695
696 KeyValue(String key, String value, boolean regexSearch, boolean caseSensitive) throws SearchParseError {
697 this.caseSensitive = caseSensitive;
698 if (regexSearch) {
699 int searchFlags = regexFlags(caseSensitive);
700 this.keyPattern = compilePattern(key, searchFlags);
701 this.valuePattern = compilePattern(value, searchFlags);
702 this.key = key;
703 this.value = value;
704 } else {
705 this.key = key;
706 this.value = value;
707 this.keyPattern = null;
708 this.valuePattern = null;
709 }
710 }
711
712 @Override
713 public boolean match(Tagged osm) {
714
715 if (keyPattern != null) {
716 if (!osm.hasKeys())
717 return false;
718
719 /* The string search will just get a key like
720 * 'highway' and look that up as osm.get(key). But
721 * since we're doing a regex match we'll have to loop
722 * over all the keys to see if they match our regex,
723 * and only then try to match against the value
724 */
725
726 for (String k: osm.keySet()) {
727 String v = osm.get(k);
728
729 Matcher matcherKey = keyPattern.matcher(k);
730 boolean matchedKey = matcherKey.find();
731
732 if (matchedKey) {
733 Matcher matcherValue = valuePattern.matcher(v);
734 boolean matchedValue = matcherValue.find();
735
736 if (matchedValue)
737 return true;
738 }
739 }
740 } else {
741 String mv;
742
743 if ("timestamp".equals(key) && osm instanceof OsmPrimitive) {
744 mv = DateUtils.fromTimestamp(((OsmPrimitive) osm).getRawTimestamp());
745 } else {
746 mv = osm.get(key);
747 if (!caseSensitive && mv == null) {
748 for (String k: osm.keySet()) {
749 if (key.equalsIgnoreCase(k)) {
750 mv = osm.get(k);
751 break;
752 }
753 }
754 }
755 }
756
757 if (mv == null)
758 return false;
759
760 String v1 = caseSensitive ? mv : mv.toLowerCase(Locale.ENGLISH);
761 String v2 = caseSensitive ? value : value.toLowerCase(Locale.ENGLISH);
762
763 v1 = Normalizer.normalize(v1, Normalizer.Form.NFC);
764 v2 = Normalizer.normalize(v2, Normalizer.Form.NFC);
765 return v1.indexOf(v2) != -1;
766 }
767
768 return false;
769 }
770
771 @Override
772 public String toString() {
773 return key + '=' + value;
774 }
775
776 @Override
777 public int hashCode() {
778 final int prime = 31;
779 int result = 1;
780 result = prime * result + (caseSensitive ? 1231 : 1237);
781 result = prime * result + ((key == null) ? 0 : key.hashCode());
782 result = prime * result + ((keyPattern == null) ? 0 : keyPattern.hashCode());
783 result = prime * result + ((value == null) ? 0 : value.hashCode());
784 result = prime * result + ((valuePattern == null) ? 0 : valuePattern.hashCode());
785 return result;
786 }
787
788 @Override
789 public boolean equals(Object obj) {
790 if (this == obj)
791 return true;
792 if (obj == null || getClass() != obj.getClass())
793 return false;
794 KeyValue other = (KeyValue) obj;
795 if (caseSensitive != other.caseSensitive)
796 return false;
797 if (key == null) {
798 if (other.key != null)
799 return false;
800 } else if (!key.equals(other.key))
801 return false;
802 if (keyPattern == null) {
803 if (other.keyPattern != null)
804 return false;
805 } else if (!keyPattern.equals(other.keyPattern))
806 return false;
807 if (value == null) {
808 if (other.value != null)
809 return false;
810 } else if (!value.equals(other.value))
811 return false;
812 if (valuePattern == null) {
813 if (other.valuePattern != null)
814 return false;
815 } else if (!valuePattern.equals(other.valuePattern))
816 return false;
817 return true;
818 }
819 }
820
821 public static class ValueComparison extends TaggedMatch {
822 private final String key;
823 private final String referenceValue;
824 private final Double referenceNumber;
825 private final int compareMode;
826 private static final Pattern ISO8601 = Pattern.compile("\\d+-\\d+-\\d+");
827
828 public ValueComparison(String key, String referenceValue, int compareMode) {
829 this.key = key;
830 this.referenceValue = referenceValue;
831 Double v = null;
832 try {
833 if (referenceValue != null) {
834 v = Double.valueOf(referenceValue);
835 }
836 } catch (NumberFormatException ignore) {
837 Logging.trace(ignore);
838 }
839 this.referenceNumber = v;
840 this.compareMode = compareMode;
841 }
842
843 @Override
844 public boolean match(Tagged osm) {
845 final String currentValue = osm.get(key);
846 final int compareResult;
847 if (currentValue == null) {
848 return false;
849 } else if (ISO8601.matcher(currentValue).matches() || ISO8601.matcher(referenceValue).matches()) {
850 compareResult = currentValue.compareTo(referenceValue);
851 } else if (referenceNumber != null) {
852 try {
853 compareResult = Double.compare(Double.parseDouble(currentValue), referenceNumber);
854 } catch (NumberFormatException ignore) {
855 return false;
856 }
857 } else {
858 compareResult = AlphanumComparator.getInstance().compare(currentValue, referenceValue);
859 }
860 return compareMode < 0 ? compareResult < 0 : compareMode > 0 ? compareResult > 0 : compareResult == 0;
861 }
862
863 @Override
864 public String toString() {
865 return key + (compareMode == -1 ? "<" : compareMode == +1 ? ">" : "") + referenceValue;
866 }
867
868 @Override
869 public int hashCode() {
870 final int prime = 31;
871 int result = 1;
872 result = prime * result + compareMode;
873 result = prime * result + ((key == null) ? 0 : key.hashCode());
874 result = prime * result + ((referenceNumber == null) ? 0 : referenceNumber.hashCode());
875 result = prime * result + ((referenceValue == null) ? 0 : referenceValue.hashCode());
876 return result;
877 }
878
879 @Override
880 public boolean equals(Object obj) {
881 if (this == obj)
882 return true;
883 if (obj == null || getClass() != obj.getClass())
884 return false;
885 ValueComparison other = (ValueComparison) obj;
886 if (compareMode != other.compareMode)
887 return false;
888 if (key == null) {
889 if (other.key != null)
890 return false;
891 } else if (!key.equals(other.key))
892 return false;
893 if (referenceNumber == null) {
894 if (other.referenceNumber != null)
895 return false;
896 } else if (!referenceNumber.equals(other.referenceNumber))
897 return false;
898 if (referenceValue == null) {
899 if (other.referenceValue != null)
900 return false;
901 } else if (!referenceValue.equals(other.referenceValue))
902 return false;
903 return true;
904 }
905 }
906
907 /**
908 * Matches objects with the exact given key-value pair.
909 */
910 public static class ExactKeyValue extends TaggedMatch {
911
912 enum Mode {
913 ANY, ANY_KEY, ANY_VALUE, EXACT, NONE, MISSING_KEY,
914 ANY_KEY_REGEXP, ANY_VALUE_REGEXP, EXACT_REGEXP, MISSING_KEY_REGEXP;
915 }
916
917 private final String key;
918 private final String value;
919 private final Pattern keyPattern;
920 private final Pattern valuePattern;
921 private final Mode mode;
922
923 /**
924 * Constructs a new {@code ExactKeyValue}.
925 * @param regexp regular expression
926 * @param key key
927 * @param value value
928 * @throws SearchParseError if a parse error occurs
929 */
930 public ExactKeyValue(boolean regexp, String key, String value) throws SearchParseError {
931 if ("".equals(key))
932 throw new SearchParseError(tr("Key cannot be empty when tag operator is used. Sample use: key=value"));
933 this.key = key;
934 this.value = value == null ? "" : value;
935 if ("".equals(this.value) && "*".equals(key)) {
936 mode = Mode.NONE;
937 } else if ("".equals(this.value)) {
938 if (regexp) {
939 mode = Mode.MISSING_KEY_REGEXP;
940 } else {
941 mode = Mode.MISSING_KEY;
942 }
943 } else if ("*".equals(key) && "*".equals(this.value)) {
944 mode = Mode.ANY;
945 } else if ("*".equals(key)) {
946 if (regexp) {
947 mode = Mode.ANY_KEY_REGEXP;
948 } else {
949 mode = Mode.ANY_KEY;
950 }
951 } else if ("*".equals(this.value)) {
952 if (regexp) {
953 mode = Mode.ANY_VALUE_REGEXP;
954 } else {
955 mode = Mode.ANY_VALUE;
956 }
957 } else {
958 if (regexp) {
959 mode = Mode.EXACT_REGEXP;
960 } else {
961 mode = Mode.EXACT;
962 }
963 }
964
965 if (regexp && !key.isEmpty() && !"*".equals(key)) {
966 keyPattern = compilePattern(key, regexFlags(false));
967 } else {
968 keyPattern = null;
969 }
970 if (regexp && !this.value.isEmpty() && !"*".equals(this.value)) {
971 valuePattern = compilePattern(this.value, regexFlags(false));
972 } else {
973 valuePattern = null;
974 }
975 }
976
977 @Override
978 public boolean match(Tagged osm) {
979
980 if (!osm.hasKeys())
981 return mode == Mode.NONE;
982
983 switch (mode) {
984 case NONE:
985 return false;
986 case MISSING_KEY:
987 return !osm.hasTag(key);
988 case ANY:
989 return true;
990 case ANY_VALUE:
991 return osm.hasTag(key);
992 case ANY_KEY:
993 for (String v:osm.getKeys().values()) {
994 if (v.equals(value))
995 return true;
996 }
997 return false;
998 case EXACT:
999 return value.equals(osm.get(key));
1000 case ANY_KEY_REGEXP:
1001 for (String v:osm.getKeys().values()) {
1002 if (valuePattern.matcher(v).matches())
1003 return true;
1004 }
1005 return false;
1006 case ANY_VALUE_REGEXP:
1007 case EXACT_REGEXP:
1008 for (String k : osm.keySet()) {
1009 if (keyPattern.matcher(k).matches()
1010 && (mode == Mode.ANY_VALUE_REGEXP || valuePattern.matcher(osm.get(k)).matches()))
1011 return true;
1012 }
1013 return false;
1014 case MISSING_KEY_REGEXP:
1015 for (String k:osm.keySet()) {
1016 if (keyPattern.matcher(k).matches())
1017 return false;
1018 }
1019 return true;
1020 }
1021 throw new AssertionError("Missed state");
1022 }
1023
1024 @Override
1025 public String toString() {
1026 return key + '=' + value;
1027 }
1028
1029 @Override
1030 public int hashCode() {
1031 final int prime = 31;
1032 int result = 1;
1033 result = prime * result + ((key == null) ? 0 : key.hashCode());
1034 result = prime * result + ((keyPattern == null) ? 0 : keyPattern.hashCode());
1035 result = prime * result + ((mode == null) ? 0 : mode.hashCode());
1036 result = prime * result + ((value == null) ? 0 : value.hashCode());
1037 result = prime * result + ((valuePattern == null) ? 0 : valuePattern.hashCode());
1038 return result;
1039 }
1040
1041 @Override
1042 public boolean equals(Object obj) {
1043 if (this == obj)
1044 return true;
1045 if (obj == null || getClass() != obj.getClass())
1046 return false;
1047 ExactKeyValue other = (ExactKeyValue) obj;
1048 if (key == null) {
1049 if (other.key != null)
1050 return false;
1051 } else if (!key.equals(other.key))
1052 return false;
1053 if (keyPattern == null) {
1054 if (other.keyPattern != null)
1055 return false;
1056 } else if (!keyPattern.equals(other.keyPattern))
1057 return false;
1058 if (mode != other.mode)
1059 return false;
1060 if (value == null) {
1061 if (other.value != null)
1062 return false;
1063 } else if (!value.equals(other.value))
1064 return false;
1065 if (valuePattern == null) {
1066 if (other.valuePattern != null)
1067 return false;
1068 } else if (!valuePattern.equals(other.valuePattern))
1069 return false;
1070 return true;
1071 }
1072 }
1073
1074 /**
1075 * Match a string in any tags (key or value), with optional regex and case insensitivity.
1076 */
1077 private static class Any extends TaggedMatch {
1078 private final String search;
1079 private final Pattern searchRegex;
1080 private final boolean caseSensitive;
1081
1082 Any(String s, boolean regexSearch, boolean caseSensitive) throws SearchParseError {
1083 s = Normalizer.normalize(s, Normalizer.Form.NFC);
1084 this.caseSensitive = caseSensitive;
1085 if (regexSearch) {
1086 this.searchRegex = compilePattern(s, regexFlags(caseSensitive));
1087 this.search = s;
1088 } else if (caseSensitive) {
1089 this.search = s;
1090 this.searchRegex = null;
1091 } else {
1092 this.search = s.toLowerCase(Locale.ENGLISH);
1093 this.searchRegex = null;
1094 }
1095 }
1096
1097 @Override
1098 public boolean match(Tagged osm) {
1099 if (!osm.hasKeys())
1100 return search.isEmpty();
1101
1102 for (String key: osm.keySet()) {
1103 String value = osm.get(key);
1104 if (searchRegex != null) {
1105
1106 value = Normalizer.normalize(value, Normalizer.Form.NFC);
1107
1108 Matcher keyMatcher = searchRegex.matcher(key);
1109 Matcher valMatcher = searchRegex.matcher(value);
1110
1111 boolean keyMatchFound = keyMatcher.find();
1112 boolean valMatchFound = valMatcher.find();
1113
1114 if (keyMatchFound || valMatchFound)
1115 return true;
1116 } else {
1117 if (!caseSensitive) {
1118 key = key.toLowerCase(Locale.ENGLISH);
1119 value = value.toLowerCase(Locale.ENGLISH);
1120 }
1121
1122 value = Normalizer.normalize(value, Normalizer.Form.NFC);
1123
1124 if (key.indexOf(search) != -1 || value.indexOf(search) != -1)
1125 return true;
1126 }
1127 }
1128 return false;
1129 }
1130
1131 @Override
1132 public String toString() {
1133 return search;
1134 }
1135
1136 @Override
1137 public int hashCode() {
1138 final int prime = 31;
1139 int result = 1;
1140 result = prime * result + (caseSensitive ? 1231 : 1237);
1141 result = prime * result + ((search == null) ? 0 : search.hashCode());
1142 result = prime * result + ((searchRegex == null) ? 0 : searchRegex.hashCode());
1143 return result;
1144 }
1145
1146 @Override
1147 public boolean equals(Object obj) {
1148 if (this == obj)
1149 return true;
1150 if (obj == null || getClass() != obj.getClass())
1151 return false;
1152 Any other = (Any) obj;
1153 if (caseSensitive != other.caseSensitive)
1154 return false;
1155 if (search == null) {
1156 if (other.search != null)
1157 return false;
1158 } else if (!search.equals(other.search))
1159 return false;
1160 if (searchRegex == null) {
1161 if (other.searchRegex != null)
1162 return false;
1163 } else if (!searchRegex.equals(other.searchRegex))
1164 return false;
1165 return true;
1166 }
1167 }
1168
1169 private static class ExactType extends Match {
1170 private final OsmPrimitiveType type;
1171
1172 ExactType(String type) throws SearchParseError {
1173 this.type = OsmPrimitiveType.from(type);
1174 if (this.type == null)
1175 throw new SearchParseError(tr("Unknown primitive type: {0}. Allowed values are node, way or relation", type));
1176 }
1177
1178 @Override
1179 public boolean match(OsmPrimitive osm) {
1180 return type.equals(osm.getType());
1181 }
1182
1183 @Override
1184 public String toString() {
1185 return "type=" + type;
1186 }
1187
1188 @Override
1189 public int hashCode() {
1190 return 31 + ((type == null) ? 0 : type.hashCode());
1191 }
1192
1193 @Override
1194 public boolean equals(Object obj) {
1195 if (this == obj)
1196 return true;
1197 if (obj == null || getClass() != obj.getClass())
1198 return false;
1199 ExactType other = (ExactType) obj;
1200 return type == other.type;
1201 }
1202 }
1203
1204 /**
1205 * Matches objects last changed by the given username.
1206 */
1207 private static class UserMatch extends Match {
1208 private String user;
1209
1210 UserMatch(String user) {
1211 if ("anonymous".equals(user)) {
1212 this.user = null;
1213 } else {
1214 this.user = user;
1215 }
1216 }
1217
1218 @Override
1219 public boolean match(OsmPrimitive osm) {
1220 if (osm.getUser() == null)
1221 return user == null;
1222 else
1223 return osm.getUser().hasName(user);
1224 }
1225
1226 @Override
1227 public String toString() {
1228 return "user=" + (user == null ? "" : user);
1229 }
1230
1231 @Override
1232 public int hashCode() {
1233 return 31 + ((user == null) ? 0 : user.hashCode());
1234 }
1235
1236 @Override
1237 public boolean equals(Object obj) {
1238 if (this == obj)
1239 return true;
1240 if (obj == null || getClass() != obj.getClass())
1241 return false;
1242 UserMatch other = (UserMatch) obj;
1243 if (user == null) {
1244 if (other.user != null)
1245 return false;
1246 } else if (!user.equals(other.user))
1247 return false;
1248 return true;
1249 }
1250 }
1251
1252 /**
1253 * Matches objects with the given relation role (i.e. "outer").
1254 */
1255 private static class RoleMatch extends Match {
1256 private String role;
1257
1258 RoleMatch(String role) {
1259 if (role == null) {
1260 this.role = "";
1261 } else {
1262 this.role = role;
1263 }
1264 }
1265
1266 @Override
1267 public boolean match(OsmPrimitive osm) {
1268 for (OsmPrimitive ref: osm.getReferrers()) {
1269 if (ref instanceof Relation && !ref.isIncomplete() && !ref.isDeleted()) {
1270 for (RelationMember m : ((Relation) ref).getMembers()) {
1271 if (m.getMember() == osm) {
1272 String testRole = m.getRole();
1273 if (role.equals(testRole == null ? "" : testRole))
1274 return true;
1275 }
1276 }
1277 }
1278 }
1279 return false;
1280 }
1281
1282 @Override
1283 public String toString() {
1284 return "role=" + role;
1285 }
1286
1287 @Override
1288 public int hashCode() {
1289 return 31 + ((role == null) ? 0 : role.hashCode());
1290 }
1291
1292 @Override
1293 public boolean equals(Object obj) {
1294 if (this == obj)
1295 return true;
1296 if (obj == null || getClass() != obj.getClass())
1297 return false;
1298 RoleMatch other = (RoleMatch) obj;
1299 if (role == null) {
1300 if (other.role != null)
1301 return false;
1302 } else if (!role.equals(other.role))
1303 return false;
1304 return true;
1305 }
1306 }
1307
1308 /**
1309 * Matches the n-th object of a relation and/or the n-th node of a way.
1310 */
1311 private static class Nth extends Match {
1312
1313 private final int nth;
1314 private final boolean modulo;
1315
1316 Nth(PushbackTokenizer tokenizer, boolean modulo) throws SearchParseError {
1317 this((int) tokenizer.readNumber(tr("Positive integer expected")), modulo);
1318 }
1319
1320 private Nth(int nth, boolean modulo) {
1321 this.nth = nth;
1322 this.modulo = modulo;
1323 }
1324
1325 @Override
1326 public boolean match(OsmPrimitive osm) {
1327 for (OsmPrimitive p : osm.getReferrers()) {
1328 final int idx;
1329 final int maxIndex;
1330 if (p instanceof Way) {
1331 Way w = (Way) p;
1332 idx = w.getNodes().indexOf(osm);
1333 maxIndex = w.getNodesCount();
1334 } else if (p instanceof Relation) {
1335 Relation r = (Relation) p;
1336 idx = r.getMemberPrimitivesList().indexOf(osm);
1337 maxIndex = r.getMembersCount();
1338 } else {
1339 continue;
1340 }
1341 if (nth < 0 && idx - maxIndex == nth) {
1342 return true;
1343 } else if (idx == nth || (modulo && idx % nth == 0))
1344 return true;
1345 }
1346 return false;
1347 }
1348
1349 @Override
1350 public String toString() {
1351 return "Nth{nth=" + nth + ", modulo=" + modulo + '}';
1352 }
1353
1354 @Override
1355 public int hashCode() {
1356 final int prime = 31;
1357 int result = 1;
1358 result = prime * result + (modulo ? 1231 : 1237);
1359 result = prime * result + nth;
1360 return result;
1361 }
1362
1363 @Override
1364 public boolean equals(Object obj) {
1365 if (this == obj)
1366 return true;
1367 if (obj == null || getClass() != obj.getClass())
1368 return false;
1369 Nth other = (Nth) obj;
1370 return modulo == other.modulo
1371 && nth == other.nth;
1372 }
1373 }
1374
1375 /**
1376 * Matches objects with properties in a certain range.
1377 */
1378 private abstract static class RangeMatch extends Match {
1379
1380 private final long min;
1381 private final long max;
1382
1383 RangeMatch(long min, long max) {
1384 this.min = Math.min(min, max);
1385 this.max = Math.max(min, max);
1386 }
1387
1388 RangeMatch(Range range) {
1389 this(range.getStart(), range.getEnd());
1390 }
1391
1392 protected abstract Long getNumber(OsmPrimitive osm);
1393
1394 protected abstract String getString();
1395
1396 @Override
1397 public boolean match(OsmPrimitive osm) {
1398 Long num = getNumber(osm);
1399 if (num == null)
1400 return false;
1401 else
1402 return (num >= min) && (num <= max);
1403 }
1404
1405 @Override
1406 public String toString() {
1407 return getString() + '=' + min + '-' + max;
1408 }
1409
1410 @Override
1411 public int hashCode() {
1412 final int prime = 31;
1413 int result = 1;
1414 result = prime * result + (int) (max ^ (max >>> 32));
1415 result = prime * result + (int) (min ^ (min >>> 32));
1416 return result;
1417 }
1418
1419 @Override
1420 public boolean equals(Object obj) {
1421 if (this == obj)
1422 return true;
1423 if (obj == null || getClass() != obj.getClass())
1424 return false;
1425 RangeMatch other = (RangeMatch) obj;
1426 return max == other.max
1427 && min == other.min;
1428 }
1429 }
1430
1431 /**
1432 * Matches ways with a number of nodes in given range
1433 */
1434 private static class NodeCountRange extends RangeMatch {
1435 NodeCountRange(Range range) {
1436 super(range);
1437 }
1438
1439 NodeCountRange(PushbackTokenizer tokenizer) throws SearchParseError {
1440 this(tokenizer.readRange(tr("Range of numbers expected")));
1441 }
1442
1443 @Override
1444 protected Long getNumber(OsmPrimitive osm) {
1445 if (osm instanceof Way) {
1446 return (long) ((Way) osm).getRealNodesCount();
1447 } else if (osm instanceof Relation) {
1448 return (long) ((Relation) osm).getMemberPrimitives(Node.class).size();
1449 } else {
1450 return null;
1451 }
1452 }
1453
1454 @Override
1455 protected String getString() {
1456 return "nodes";
1457 }
1458 }
1459
1460 /**
1461 * Matches objects with the number of referring/contained ways in the given range
1462 */
1463 private static class WayCountRange extends RangeMatch {
1464 WayCountRange(Range range) {
1465 super(range);
1466 }
1467
1468 WayCountRange(PushbackTokenizer tokenizer) throws SearchParseError {
1469 this(tokenizer.readRange(tr("Range of numbers expected")));
1470 }
1471
1472 @Override
1473 protected Long getNumber(OsmPrimitive osm) {
1474 if (osm instanceof Node) {
1475 return (long) Utils.filteredCollection(osm.getReferrers(), Way.class).size();
1476 } else if (osm instanceof Relation) {
1477 return (long) ((Relation) osm).getMemberPrimitives(Way.class).size();
1478 } else {
1479 return null;
1480 }
1481 }
1482
1483 @Override
1484 protected String getString() {
1485 return "ways";
1486 }
1487 }
1488
1489 /**
1490 * Matches objects with a number of tags in given range
1491 */
1492 private static class TagCountRange extends RangeMatch {
1493 TagCountRange(Range range) {
1494 super(range);
1495 }
1496
1497 TagCountRange(PushbackTokenizer tokenizer) throws SearchParseError {
1498 this(tokenizer.readRange(tr("Range of numbers expected")));
1499 }
1500
1501 @Override
1502 protected Long getNumber(OsmPrimitive osm) {
1503 return (long) osm.getKeys().size();
1504 }
1505
1506 @Override
1507 protected String getString() {
1508 return "tags";
1509 }
1510 }
1511
1512 /**
1513 * Matches objects with a timestamp in given range
1514 */
1515 private static class TimestampRange extends RangeMatch {
1516
1517 TimestampRange(long minCount, long maxCount) {
1518 super(minCount, maxCount);
1519 }
1520
1521 @Override
1522 protected Long getNumber(OsmPrimitive osm) {
1523 return osm.getTimestamp().getTime();
1524 }
1525
1526 @Override
1527 protected String getString() {
1528 return "timestamp";
1529 }
1530 }
1531
1532 /**
1533 * Matches relations with a member of the given role
1534 */
1535 private static class HasRole extends Match {
1536 private final String role;
1537
1538 HasRole(PushbackTokenizer tokenizer) {
1539 role = tokenizer.readTextOrNumber();
1540 }
1541
1542 @Override
1543 public boolean match(OsmPrimitive osm) {
1544 return osm instanceof Relation && ((Relation) osm).getMemberRoles().contains(role);
1545 }
1546
1547 @Override
1548 public int hashCode() {
1549 return 31 + ((role == null) ? 0 : role.hashCode());
1550 }
1551
1552 @Override
1553 public boolean equals(Object obj) {
1554 if (this == obj)
1555 return true;
1556 if (obj == null || getClass() != obj.getClass())
1557 return false;
1558 HasRole other = (HasRole) obj;
1559 if (role == null) {
1560 if (other.role != null)
1561 return false;
1562 } else if (!role.equals(other.role))
1563 return false;
1564 return true;
1565 }
1566 }
1567
1568 /**
1569 * Matches objects that are new (i.e. have not been uploaded to the server)
1570 */
1571 private static class New extends Match {
1572 @Override
1573 public boolean match(OsmPrimitive osm) {
1574 return osm.isNew();
1575 }
1576
1577 @Override
1578 public String toString() {
1579 return "new";
1580 }
1581 }
1582
1583 /**
1584 * Matches all objects that have been modified, created, or undeleted
1585 */
1586 private static class Modified extends Match {
1587 @Override
1588 public boolean match(OsmPrimitive osm) {
1589 return osm.isModified() || osm.isNewOrUndeleted();
1590 }
1591
1592 @Override
1593 public String toString() {
1594 return "modified";
1595 }
1596 }
1597
1598 /**
1599 * Matches all objects that have been deleted
1600 */
1601 private static class Deleted extends Match {
1602 @Override
1603 public boolean match(OsmPrimitive osm) {
1604 return osm.isDeleted();
1605 }
1606
1607 @Override
1608 public String toString() {
1609 return "deleted";
1610 }
1611 }
1612
1613 /**
1614 * Matches all objects currently selected
1615 */
1616 private static class Selected extends Match {
1617 @Override
1618 public boolean match(OsmPrimitive osm) {
1619 return osm.getDataSet().isSelected(osm);
1620 }
1621
1622 @Override
1623 public String toString() {
1624 return "selected";
1625 }
1626 }
1627
1628 /**
1629 * Match objects that are incomplete, where only id and type are known.
1630 * Typically some members of a relation are incomplete until they are
1631 * fetched from the server.
1632 */
1633 private static class Incomplete extends Match {
1634 @Override
1635 public boolean match(OsmPrimitive osm) {
1636 return osm.isIncomplete() || (osm instanceof Relation && ((Relation) osm).hasIncompleteMembers());
1637 }
1638
1639 @Override
1640 public String toString() {
1641 return "incomplete";
1642 }
1643 }
1644
1645 /**
1646 * Matches objects that don't have any interesting tags (i.e. only has source,
1647 * FIXME, etc.). The complete list of uninteresting tags can be found here:
1648 * org.openstreetmap.josm.data.osm.OsmPrimitive.getUninterestingKeys()
1649 */
1650 private static class Untagged extends Match {
1651 @Override
1652 public boolean match(OsmPrimitive osm) {
1653 return !osm.isTagged() && !osm.isIncomplete();
1654 }
1655
1656 @Override
1657 public String toString() {
1658 return "untagged";
1659 }
1660 }
1661
1662 /**
1663 * Matches ways which are closed (i.e. first and last node are the same)
1664 */
1665 private static class Closed extends Match {
1666 @Override
1667 public boolean match(OsmPrimitive osm) {
1668 return osm instanceof Way && ((Way) osm).isClosed();
1669 }
1670
1671 @Override
1672 public String toString() {
1673 return "closed";
1674 }
1675 }
1676
1677 /**
1678 * Matches objects if they are parents of the expression
1679 */
1680 public static class Parent extends UnaryMatch {
1681 public Parent(Match m) {
1682 super(m);
1683 }
1684
1685 @Override
1686 public boolean match(OsmPrimitive osm) {
1687 boolean isParent = false;
1688
1689 if (osm instanceof Way) {
1690 for (Node n : ((Way) osm).getNodes()) {
1691 isParent |= match.match(n);
1692 }
1693 } else if (osm instanceof Relation) {
1694 for (RelationMember member : ((Relation) osm).getMembers()) {
1695 isParent |= match.match(member.getMember());
1696 }
1697 }
1698 return isParent;
1699 }
1700
1701 @Override
1702 public String toString() {
1703 return "parent(" + match + ')';
1704 }
1705 }
1706
1707 /**
1708 * Matches objects if they are children of the expression
1709 */
1710 public static class Child extends UnaryMatch {
1711
1712 public Child(Match m) {
1713 super(m);
1714 }
1715
1716 @Override
1717 public boolean match(OsmPrimitive osm) {
1718 boolean isChild = false;
1719 for (OsmPrimitive p : osm.getReferrers()) {
1720 isChild |= match.match(p);
1721 }
1722 return isChild;
1723 }
1724
1725 @Override
1726 public String toString() {
1727 return "child(" + match + ')';
1728 }
1729 }
1730
1731 /**
1732 * Matches if the size of the area is within the given range
1733 *
1734 * @author Ole Jørgen Brønner
1735 */
1736 private static class AreaSize extends RangeMatch {
1737
1738 AreaSize(Range range) {
1739 super(range);
1740 }
1741
1742 AreaSize(PushbackTokenizer tokenizer) throws SearchParseError {
1743 this(tokenizer.readRange(tr("Range of numbers expected")));
1744 }
1745
1746 @Override
1747 protected Long getNumber(OsmPrimitive osm) {
1748 final Double area = Geometry.computeArea(osm);
1749 return area == null ? null : area.longValue();
1750 }
1751
1752 @Override
1753 protected String getString() {
1754 return "areasize";
1755 }
1756 }
1757
1758 /**
1759 * Matches if the length of a way is within the given range
1760 */
1761 private static class WayLength extends RangeMatch {
1762
1763 WayLength(Range range) {
1764 super(range);
1765 }
1766
1767 WayLength(PushbackTokenizer tokenizer) throws SearchParseError {
1768 this(tokenizer.readRange(tr("Range of numbers expected")));
1769 }
1770
1771 @Override
1772 protected Long getNumber(OsmPrimitive osm) {
1773 if (!(osm instanceof Way))
1774 return null;
1775 Way way = (Way) osm;
1776 return (long) way.getLength();
1777 }
1778
1779 @Override
1780 protected String getString() {
1781 return "waylength";
1782 }
1783 }
1784
1785 /**
1786 * Matches objects within the given bounds.
1787 */
1788 public abstract static class InArea extends Match {
1789
1790 protected final boolean all;
1791
1792 /**
1793 * @param all if true, all way nodes or relation members have to be within source area;if false, one suffices.
1794 */
1795 protected InArea(boolean all) {
1796 this.all = all;
1797 }
1798
1799 protected abstract Collection<Bounds> getBounds(OsmPrimitive primitive);
1800
1801 @Override
1802 public boolean match(OsmPrimitive osm) {
1803 if (!osm.isUsable())
1804 return false;
1805 else if (osm instanceof Node) {
1806 LatLon coordinate = ((Node) osm).getCoor();
1807 Collection<Bounds> allBounds = getBounds(osm);
1808 return coordinate != null && allBounds != null && allBounds.stream().anyMatch(bounds -> bounds.contains(coordinate));
1809 } else if (osm instanceof Way) {
1810 Collection<Node> nodes = ((Way) osm).getNodes();
1811 return all ? nodes.stream().allMatch(this) : nodes.stream().anyMatch(this);
1812 } else if (osm instanceof Relation) {
1813 Collection<OsmPrimitive> primitives = ((Relation) osm).getMemberPrimitivesList();
1814 return all ? primitives.stream().allMatch(this) : primitives.stream().anyMatch(this);
1815 } else
1816 return false;
1817 }
1818
1819 @Override
1820 public int hashCode() {
1821 return 31 + (all ? 1231 : 1237);
1822 }
1823
1824 @Override
1825 public boolean equals(Object obj) {
1826 if (this == obj)
1827 return true;
1828 if (obj == null || getClass() != obj.getClass())
1829 return false;
1830 InArea other = (InArea) obj;
1831 return all == other.all;
1832 }
1833 }
1834
1835 /**
1836 * Matches objects within source area ("downloaded area").
1837 */
1838 public static class InDataSourceArea extends InArea {
1839
1840 /**
1841 * Constructs a new {@code InDataSourceArea}.
1842 * @param all if true, all way nodes or relation members have to be within source area; if false, one suffices.
1843 */
1844 public InDataSourceArea(boolean all) {
1845 super(all);
1846 }
1847
1848 @Override
1849 protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
1850 return primitive.getDataSet() != null ? primitive.getDataSet().getDataSourceBounds() : null;
1851 }
1852
1853 @Override
1854 public String toString() {
1855 return all ? "allindownloadedarea" : "indownloadedarea";
1856 }
1857 }
1858
1859 /**
1860 * Matches objects which are not outside the source area ("downloaded area").
1861 * Unlike {@link InDataSourceArea} this matches also if no source area is set (e.g., for new layers).
1862 */
1863 public static class NotOutsideDataSourceArea extends InDataSourceArea {
1864
1865 /**
1866 * Constructs a new {@code NotOutsideDataSourceArea}.
1867 */
1868 public NotOutsideDataSourceArea() {
1869 super(false);
1870 }
1871
1872 @Override
1873 protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
1874 final Collection<Bounds> bounds = super.getBounds(primitive);
1875 return bounds == null || bounds.isEmpty() ?
1876 Collections.singleton(ProjectionRegistry.getProjection().getWorldBoundsLatLon()) : bounds;
1877 }
1878
1879 @Override
1880 public String toString() {
1881 return "NotOutsideDataSourceArea";
1882 }
1883 }
1884
1885 /**
1886 * Matches presets.
1887 * @since 12464
1888 */
1889 private static class Preset extends Match {
1890 private final List<TaggingPreset> presets;
1891
1892 Preset(String presetName) throws SearchParseError {
1893
1894 if (presetName == null || presetName.isEmpty()) {
1895 throw new SearchParseError("The name of the preset is required");
1896 }
1897
1898 int wildCardIdx = presetName.lastIndexOf('*');
1899 int length = presetName.length() - 1;
1900
1901 /*
1902 * Match strictly (simply comparing the names) if there is no '*' symbol
1903 * at the end of the name or '*' is a part of the preset name.
1904 */
1905 boolean matchStrictly = wildCardIdx == -1 || wildCardIdx != length;
1906
1907 this.presets = TaggingPresets.getTaggingPresets()
1908 .stream()
1909 .filter(preset -> !(preset instanceof TaggingPresetMenu || preset instanceof TaggingPresetSeparator))
1910 .filter(preset -> presetNameMatch(presetName, preset, matchStrictly))
1911 .collect(Collectors.toList());
1912
1913 if (this.presets.isEmpty()) {
1914 throw new SearchParseError(tr("Unknown preset name: ") + presetName);
1915 }
1916 }
1917
1918 @Override
1919 public boolean match(OsmPrimitive osm) {
1920 for (TaggingPreset preset : this.presets) {
1921 if (preset.test(osm)) {
1922 return true;
1923 }
1924 }
1925
1926 return false;
1927 }
1928
1929 private static boolean presetNameMatch(String name, TaggingPreset preset, boolean matchStrictly) {
1930 if (matchStrictly) {
1931 return name.equalsIgnoreCase(preset.getRawName());
1932 }
1933
1934 try {
1935 String groupSuffix = name.substring(0, name.length() - 2); // try to remove '/*'
1936 TaggingPresetMenu group = preset.group;
1937
1938 return group != null && groupSuffix.equalsIgnoreCase(group.getRawName());
1939 } catch (StringIndexOutOfBoundsException ex) {
1940 Logging.trace(ex);
1941 return false;
1942 }
1943 }
1944
1945 @Override
1946 public int hashCode() {
1947 return 31 + ((presets == null) ? 0 : presets.hashCode());
1948 }
1949
1950 @Override
1951 public boolean equals(Object obj) {
1952 if (this == obj)
1953 return true;
1954 if (obj == null || getClass() != obj.getClass())
1955 return false;
1956 Preset other = (Preset) obj;
1957 if (presets == null) {
1958 if (other.presets != null)
1959 return false;
1960 } else if (!presets.equals(other.presets))
1961 return false;
1962 return true;
1963 }
1964 }
1965
1966 /**
1967 * Compiles the search expression.
1968 * @param searchStr the search expression
1969 * @return a {@link Match} object for the expression
1970 * @throws SearchParseError if an error has been encountered while compiling
1971 * @see #compile(SearchSetting)
1972 */
1973 public static Match compile(String searchStr) throws SearchParseError {
1974 return new SearchCompiler(false, false,
1975 new PushbackTokenizer(
1976 new PushbackReader(new StringReader(searchStr))))
1977 .parse();
1978 }
1979
1980 /**
1981 * Compiles the search expression.
1982 * @param setting the settings to use
1983 * @return a {@link Match} object for the expression
1984 * @throws SearchParseError if an error has been encountered while compiling
1985 * @see #compile(String)
1986 */
1987 public static Match compile(SearchSetting setting) throws SearchParseError {
1988 if (setting.mapCSSSearch) {
1989 return compileMapCSS(setting.text);
1990 }
1991 return new SearchCompiler(setting.caseSensitive, setting.regexSearch,
1992 new PushbackTokenizer(
1993 new PushbackReader(new StringReader(setting.text))))
1994 .parse();
1995 }
1996
1997 static Match compileMapCSS(String mapCSS) throws SearchParseError {
1998 try {
1999 final List<Selector> selectors = new MapCSSParser(new StringReader(mapCSS)).selectors();
2000 return new Match() {
2001 @Override
2002 public boolean match(OsmPrimitive osm) {
2003 for (Selector selector : selectors) {
2004 if (selector.matches(new Environment(osm))) {
2005 return true;
2006 }
2007 }
2008 return false;
2009 }
2010 };
2011 } catch (ParseException e) {
2012 throw new SearchParseError(tr("Failed to parse MapCSS selector"), e);
2013 }
2014 }
2015
2016 /**
2017 * Parse search string.
2018 *
2019 * @return match determined by search string
2020 * @throws org.openstreetmap.josm.data.osm.search.SearchParseError if search expression cannot be parsed
2021 */
2022 public Match parse() throws SearchParseError {
2023 Match m = Optional.ofNullable(parseExpression()).orElse(Always.INSTANCE);
2024 if (!tokenizer.readIfEqual(Token.EOF))
2025 throw new SearchParseError(tr("Unexpected token: {0}", tokenizer.nextToken()));
2026 Logging.debug("Parsed search expression is {0}", m);
2027 return m;
2028 }
2029
2030 /**
2031 * Parse expression.
2032 *
2033 * @return match determined by parsing expression
2034 * @throws SearchParseError if search expression cannot be parsed
2035 */
2036 private Match parseExpression() throws SearchParseError {
2037 // Step 1: parse the whole expression and build a list of factors and logical tokens
2038 List<Object> list = parseExpressionStep1();
2039 // Step 2: iterate the list in reverse order to build the logical expression
2040 // This iterative approach avoids StackOverflowError for long expressions (see #14217)
2041 return parseExpressionStep2(list);
2042 }
2043
2044 private List<Object> parseExpressionStep1() throws SearchParseError {
2045 Match factor;
2046 String token = null;
2047 String errorMessage = null;
2048 List<Object> list = new ArrayList<>();
2049 do {
2050 factor = parseFactor();
2051 if (factor != null) {
2052 if (token != null) {
2053 list.add(token);
2054 }
2055 list.add(factor);
2056 if (tokenizer.readIfEqual(Token.OR)) {
2057 token = "OR";
2058 errorMessage = tr("Missing parameter for OR");
2059 } else if (tokenizer.readIfEqual(Token.XOR)) {
2060 token = "XOR";
2061 errorMessage = tr("Missing parameter for XOR");
2062 } else {
2063 token = "AND";
2064 errorMessage = null;
2065 }
2066 } else if (errorMessage != null) {
2067 throw new SearchParseError(errorMessage);
2068 }
2069 } while (factor != null);
2070 return list;
2071 }
2072
2073 private static Match parseExpressionStep2(List<Object> list) {
2074 Match result = null;
2075 for (int i = list.size() - 1; i >= 0; i--) {
2076 Object o = list.get(i);
2077 if (o instanceof Match && result == null) {
2078 result = (Match) o;
2079 } else if (o instanceof String && i > 0) {
2080 Match factor = (Match) list.get(i-1);
2081 switch ((String) o) {
2082 case "OR":
2083 result = new Or(factor, result);
2084 break;
2085 case "XOR":
2086 result = new Xor(factor, result);
2087 break;
2088 case "AND":
2089 result = new And(factor, result);
2090 break;
2091 default: throw new IllegalStateException(tr("Unexpected token: {0}", o));
2092 }
2093 i--;
2094 } else {
2095 throw new IllegalStateException("i=" + i + "; o=" + o);
2096 }
2097 }
2098 return result;
2099 }
2100
2101 /**
2102 * Parse next factor (a search operator or search term).
2103 *
2104 * @return match determined by parsing factor string
2105 * @throws SearchParseError if search expression cannot be parsed
2106 */
2107 private Match parseFactor() throws SearchParseError {
2108 if (tokenizer.readIfEqual(Token.LEFT_PARENT)) {
2109 Match expression = parseExpression();
2110 if (!tokenizer.readIfEqual(Token.RIGHT_PARENT))
2111 throw new SearchParseError(Token.RIGHT_PARENT, tokenizer.nextToken());
2112 return expression;
2113 } else if (tokenizer.readIfEqual(Token.NOT)) {
2114 return new Not(parseFactor(tr("Missing operator for NOT")));
2115 } else if (tokenizer.readIfEqual(Token.KEY)) {
2116 // factor consists of key:value or key=value
2117 String key = tokenizer.getText();
2118 if (tokenizer.readIfEqual(Token.EQUALS)) {
2119 return new ExactKeyValue(regexSearch, key, tokenizer.readTextOrNumber());
2120 } else if (tokenizer.readIfEqual(Token.LESS_THAN)) {
2121 return new ValueComparison(key, tokenizer.readTextOrNumber(), -1);
2122 } else if (tokenizer.readIfEqual(Token.GREATER_THAN)) {
2123 return new ValueComparison(key, tokenizer.readTextOrNumber(), +1);
2124 } else if (tokenizer.readIfEqual(Token.COLON)) {
2125 // see if we have a Match that takes a data parameter
2126 SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
2127 if (factory != null)
2128 return factory.get(key, caseSensitive, regexSearch, tokenizer);
2129
2130 UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
2131 if (unaryFactory != null)
2132 return unaryFactory.get(key, parseFactor(), tokenizer);
2133
2134 // key:value form where value is a string (may be OSM key search)
2135 final String value = tokenizer.readTextOrNumber();
2136 return new KeyValue(key, value != null ? value : "", regexSearch, caseSensitive);
2137 } else if (tokenizer.readIfEqual(Token.QUESTION_MARK))
2138 return new BooleanMatch(key, false);
2139 else {
2140 SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
2141 if (factory != null)
2142 return factory.get(key, caseSensitive, regexSearch, null);
2143
2144 UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
2145 if (unaryFactory != null)
2146 return unaryFactory.get(key, parseFactor(), null);
2147
2148 // match string in any key or value
2149 return new Any(key, regexSearch, caseSensitive);
2150 }
2151 } else
2152 return null;
2153 }
2154
2155 private Match parseFactor(String errorMessage) throws SearchParseError {
2156 return Optional.ofNullable(parseFactor()).orElseThrow(() -> new SearchParseError(errorMessage));
2157 }
2158
2159 private static int regexFlags(boolean caseSensitive) {
2160 int searchFlags = 0;
2161
2162 // Enables canonical Unicode equivalence so that e.g. the two
2163 // forms of "\u00e9gal" and "e\u0301gal" will match.
2164 //
2165 // It makes sense to match no matter how the character
2166 // happened to be constructed.
2167 searchFlags |= Pattern.CANON_EQ;
2168
2169 // Make "." match any character including newline (/s in Perl)
2170 searchFlags |= Pattern.DOTALL;
2171
2172 // CASE_INSENSITIVE by itself only matches US-ASCII case
2173 // insensitively, but the OSM data is in Unicode. With
2174 // UNICODE_CASE casefolding is made Unicode-aware.
2175 if (!caseSensitive) {
2176 searchFlags |= (Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
2177 }
2178
2179 return searchFlags;
2180 }
2181
2182 static String escapeStringForSearch(String s) {
2183 return s.replace("\\", "\\\\").replace("\"", "\\\"");
2184 }
2185
2186 /**
2187 * Builds a search string for the given tag. If value is empty, the existence of the key is checked.
2188 *
2189 * @param key the tag key
2190 * @param value the tag value
2191 * @return a search string for the given tag
2192 */
2193 public static String buildSearchStringForTag(String key, String value) {
2194 final String forKey = '"' + escapeStringForSearch(key) + '"' + '=';
2195 if (value == null || value.isEmpty()) {
2196 return forKey + '*';
2197 } else {
2198 return forKey + '"' + escapeStringForSearch(value) + '"';
2199 }
2200 }
2201}
Note: See TracBrowser for help on using the repository browser.