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

Last change on this file since 16262 was 16262, checked in by simon04, 4 years ago

fix #18164 - Migrate OverpassTurboQueryWizard to Java

The new OverpassTurboQueryWizard first invokes SearchCompiler, and then turns the AST into an Overpass QL.

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