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

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

see #19334 - javadoc fixes + protected constructors for abstract classes

  • Property svn:eol-style set to native
File size: 71.0 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 timestamp 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 protected 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 protected 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 return Objects.hash(lhs, rhs);
425 }
426
427 @Override
428 public boolean equals(Object obj) {
429 if (this == obj)
430 return true;
431 if (obj == null || getClass() != obj.getClass())
432 return false;
433 AbstractBinaryMatch other = (AbstractBinaryMatch) obj;
434 if (lhs == null) {
435 if (other.lhs != null)
436 return false;
437 } else if (!lhs.equals(other.lhs))
438 return false;
439 if (rhs == null) {
440 if (other.rhs != null)
441 return false;
442 } else if (!rhs.equals(other.rhs))
443 return false;
444 return true;
445 }
446 }
447
448 /**
449 * Matches every OsmPrimitive.
450 */
451 public static class Always extends TaggedMatch {
452 /** The unique instance/ */
453 public static final Always INSTANCE = new Always();
454 @Override
455 public boolean match(Tagged osm) {
456 return true;
457 }
458 }
459
460 /**
461 * Never matches any OsmPrimitive.
462 */
463 public static class Never extends TaggedMatch {
464 /** The unique instance/ */
465 public static final Never INSTANCE = new Never();
466 @Override
467 public boolean match(Tagged osm) {
468 return false;
469 }
470 }
471
472 /**
473 * Inverts the match.
474 */
475 public static class Not extends UnaryMatch {
476 public Not(Match match) {
477 super(match);
478 }
479
480 @Override
481 public boolean match(OsmPrimitive osm) {
482 return !match.match(osm);
483 }
484
485 @Override
486 public boolean match(Tagged osm) {
487 return !match.match(osm);
488 }
489
490 @Override
491 public String toString() {
492 return '!' + match.toString();
493 }
494
495 public Match getMatch() {
496 return match;
497 }
498 }
499
500 /**
501 * Matches if the value of the corresponding key is ''yes'', ''true'', ''1'' or ''on''.
502 */
503 public static class BooleanMatch extends TaggedMatch {
504 private final String key;
505 private final boolean defaultValue;
506
507 BooleanMatch(String key, boolean defaultValue) {
508 this.key = key;
509 this.defaultValue = defaultValue;
510 }
511
512 public String getKey() {
513 return key;
514 }
515
516 @Override
517 public boolean match(Tagged osm) {
518 return Optional.ofNullable(OsmUtils.getOsmBoolean(osm.get(key))).orElse(defaultValue);
519 }
520
521 @Override
522 public String toString() {
523 return key + '?';
524 }
525
526 @Override
527 public int hashCode() {
528 return Objects.hash(defaultValue, key);
529 }
530
531 @Override
532 public boolean equals(Object obj) {
533 if (this == obj)
534 return true;
535 if (obj == null || getClass() != obj.getClass())
536 return false;
537 BooleanMatch other = (BooleanMatch) obj;
538 if (defaultValue != other.defaultValue)
539 return false;
540 if (key == null) {
541 if (other.key != null)
542 return false;
543 } else if (!key.equals(other.key))
544 return false;
545 return true;
546 }
547 }
548
549 /**
550 * Matches if both left and right expressions match.
551 */
552 public static class And extends AbstractBinaryMatch {
553 /**
554 * Constructs a new {@code And} match.
555 * @param lhs left hand side
556 * @param rhs right hand side
557 */
558 public And(Match lhs, Match rhs) {
559 super(lhs, rhs);
560 }
561
562 @Override
563 public boolean match(OsmPrimitive osm) {
564 return lhs.match(osm) && rhs.match(osm);
565 }
566
567 @Override
568 public boolean match(Tagged osm) {
569 return lhs.match(osm) && rhs.match(osm);
570 }
571
572 @Override
573 public String toString() {
574 return map(m -> m instanceof AbstractBinaryMatch && !(m instanceof And) ? parenthesis(m) : m, (s1, s2) -> s1 + " && " + s2);
575 }
576 }
577
578 /**
579 * Matches if the left OR the right expression match.
580 */
581 public static class Or extends AbstractBinaryMatch {
582 /**
583 * Constructs a new {@code Or} match.
584 * @param lhs left hand side
585 * @param rhs right hand side
586 */
587 public Or(Match lhs, Match rhs) {
588 super(lhs, rhs);
589 }
590
591 @Override
592 public boolean match(OsmPrimitive osm) {
593 return lhs.match(osm) || rhs.match(osm);
594 }
595
596 @Override
597 public boolean match(Tagged osm) {
598 return lhs.match(osm) || rhs.match(osm);
599 }
600
601 @Override
602 public String toString() {
603 return map(m -> m instanceof AbstractBinaryMatch && !(m instanceof Or) ? parenthesis(m) : m, (s1, s2) -> s1 + " || " + s2);
604 }
605 }
606
607 /**
608 * Matches if the left OR the right expression match, but not both.
609 */
610 public static class Xor extends AbstractBinaryMatch {
611 /**
612 * Constructs a new {@code Xor} match.
613 * @param lhs left hand side
614 * @param rhs right hand side
615 */
616 public Xor(Match lhs, Match rhs) {
617 super(lhs, rhs);
618 }
619
620 @Override
621 public boolean match(OsmPrimitive osm) {
622 return lhs.match(osm) ^ rhs.match(osm);
623 }
624
625 @Override
626 public boolean match(Tagged osm) {
627 return lhs.match(osm) ^ rhs.match(osm);
628 }
629
630 @Override
631 public String toString() {
632 return map(m -> m instanceof AbstractBinaryMatch && !(m instanceof Xor) ? parenthesis(m) : m, (s1, s2) -> s1 + " ^ " + s2);
633 }
634 }
635
636 /**
637 * Matches objects with ID in the given range.
638 */
639 private static class Id extends RangeMatch {
640 Id(Range range) {
641 super(range);
642 }
643
644 Id(PushbackTokenizer tokenizer) throws SearchParseError {
645 this(tokenizer.readRange(tr("Range of primitive ids expected")));
646 }
647
648 @Override
649 protected Long getNumber(OsmPrimitive osm) {
650 return osm.isNew() ? 0 : osm.getUniqueId();
651 }
652
653 @Override
654 protected String getString() {
655 return "id";
656 }
657 }
658
659 /**
660 * Matches objects with a changeset ID in the given range.
661 */
662 private static class ChangesetId extends RangeMatch {
663 ChangesetId(Range range) {
664 super(range);
665 }
666
667 ChangesetId(PushbackTokenizer tokenizer) throws SearchParseError {
668 this(tokenizer.readRange(tr("Range of changeset ids expected")));
669 }
670
671 @Override
672 protected Long getNumber(OsmPrimitive osm) {
673 return (long) osm.getChangesetId();
674 }
675
676 @Override
677 protected String getString() {
678 return "changeset";
679 }
680 }
681
682 /**
683 * Matches objects with a version number in the given range.
684 */
685 private static class Version extends RangeMatch {
686 Version(Range range) {
687 super(range);
688 }
689
690 Version(PushbackTokenizer tokenizer) throws SearchParseError {
691 this(tokenizer.readRange(tr("Range of versions expected")));
692 }
693
694 @Override
695 protected Long getNumber(OsmPrimitive osm) {
696 return (long) osm.getVersion();
697 }
698
699 @Override
700 protected String getString() {
701 return "version";
702 }
703 }
704
705 /**
706 * Matches objects with the given key-value pair.
707 */
708 public static class KeyValue extends TaggedMatch {
709 private final String key;
710 private final Pattern keyPattern;
711 private final String value;
712 private final Pattern valuePattern;
713 private final boolean caseSensitive;
714
715 KeyValue(String key, String value, boolean regexSearch, boolean caseSensitive) throws SearchParseError {
716 this.caseSensitive = caseSensitive;
717 if (regexSearch) {
718 int searchFlags = regexFlags(caseSensitive);
719 this.keyPattern = compilePattern(key, searchFlags);
720 this.valuePattern = compilePattern(value, searchFlags);
721 this.key = key;
722 this.value = value;
723 } else {
724 this.key = key;
725 this.value = value;
726 this.keyPattern = null;
727 this.valuePattern = null;
728 }
729 }
730
731 @Override
732 public boolean match(Tagged osm) {
733 if (keyPattern != null) {
734 if (osm.hasKeys()) {
735 // The string search will just get a key like 'highway' and look that up as osm.get(key).
736 // But since we're doing a regex match we'll have to loop over all the keys to see if they match our regex,
737 // and only then try to match against the value
738 return osm.keySet().stream()
739 .anyMatch(k -> keyPattern.matcher(k).find() && valuePattern.matcher(osm.get(k)).find());
740 }
741 } else {
742 String mv = getMv(osm);
743 if (mv != null) {
744 String v1 = Normalizer.normalize(caseSensitive ? mv : mv.toLowerCase(Locale.ENGLISH), Normalizer.Form.NFC);
745 String v2 = Normalizer.normalize(caseSensitive ? value : value.toLowerCase(Locale.ENGLISH), Normalizer.Form.NFC);
746 return v1.indexOf(v2) != -1;
747 }
748 }
749 return false;
750 }
751
752 private String getMv(Tagged osm) {
753 String mv;
754 if ("timestamp".equals(key) && osm instanceof OsmPrimitive) {
755 mv = DateUtils.fromTimestamp(((OsmPrimitive) osm).getRawTimestamp());
756 } else {
757 mv = osm.get(key);
758 if (!caseSensitive && mv == null) {
759 mv = osm.keySet().stream().filter(key::equalsIgnoreCase).findFirst().map(osm::get).orElse(null);
760 }
761 }
762 return mv;
763 }
764
765 public String getKey() {
766 return key;
767 }
768
769 public String getValue() {
770 return value;
771 }
772
773 @Override
774 public String toString() {
775 return key + '=' + value;
776 }
777
778 @Override
779 public int hashCode() {
780 return Objects.hash(caseSensitive, key, keyPattern, value, valuePattern);
781 }
782
783 @Override
784 public boolean equals(Object obj) {
785 if (this == obj)
786 return true;
787 if (obj == null || getClass() != obj.getClass())
788 return false;
789 KeyValue other = (KeyValue) obj;
790 return caseSensitive == other.caseSensitive
791 && Objects.equals(key, other.key)
792 && Objects.equals(keyPattern, other.keyPattern)
793 && Objects.equals(value, other.value)
794 && Objects.equals(valuePattern, other.valuePattern);
795 }
796 }
797
798 public static class ValueComparison extends TaggedMatch {
799 private final String key;
800 private final String referenceValue;
801 private final Double referenceNumber;
802 private final int compareMode;
803 private static final Pattern ISO8601 = Pattern.compile("\\d+-\\d+-\\d+");
804
805 public ValueComparison(String key, String referenceValue, int compareMode) {
806 this.key = key;
807 this.referenceValue = referenceValue;
808 Double v = null;
809 try {
810 if (referenceValue != null) {
811 v = Double.valueOf(referenceValue);
812 }
813 } catch (NumberFormatException ignore) {
814 Logging.trace(ignore);
815 }
816 this.referenceNumber = v;
817 this.compareMode = compareMode;
818 }
819
820 @Override
821 public boolean match(Tagged osm) {
822 final String currentValue = osm.get(key);
823 final int compareResult;
824 if (currentValue == null) {
825 return false;
826 } else if (ISO8601.matcher(currentValue).matches() || ISO8601.matcher(referenceValue).matches()) {
827 compareResult = currentValue.compareTo(referenceValue);
828 } else if (referenceNumber != null) {
829 try {
830 compareResult = Double.compare(Double.parseDouble(currentValue), referenceNumber);
831 } catch (NumberFormatException ignore) {
832 return false;
833 }
834 } else {
835 compareResult = AlphanumComparator.getInstance().compare(currentValue, referenceValue);
836 }
837 return compareMode < 0 ? compareResult < 0 : compareMode > 0 ? compareResult > 0 : compareResult == 0;
838 }
839
840 @Override
841 public String toString() {
842 return key + (compareMode == -1 ? "<" : compareMode == +1 ? ">" : "") + referenceValue;
843 }
844
845 @Override
846 public int hashCode() {
847 return Objects.hash(compareMode, key, referenceNumber, referenceValue);
848 }
849
850 @Override
851 public boolean equals(Object obj) {
852 if (this == obj)
853 return true;
854 if (obj == null || getClass() != obj.getClass())
855 return false;
856 ValueComparison other = (ValueComparison) obj;
857 if (compareMode != other.compareMode)
858 return false;
859 if (key == null) {
860 if (other.key != null)
861 return false;
862 } else if (!key.equals(other.key))
863 return false;
864 if (referenceNumber == null) {
865 if (other.referenceNumber != null)
866 return false;
867 } else if (!referenceNumber.equals(other.referenceNumber))
868 return false;
869 if (referenceValue == null) {
870 if (other.referenceValue != null)
871 return false;
872 } else if (!referenceValue.equals(other.referenceValue))
873 return false;
874 return true;
875 }
876 }
877
878 /**
879 * Matches objects with the exact given key-value pair.
880 */
881 public static class ExactKeyValue extends TaggedMatch {
882
883 public enum Mode {
884 ANY, ANY_KEY, ANY_VALUE, EXACT, NONE, MISSING_KEY,
885 ANY_KEY_REGEXP, ANY_VALUE_REGEXP, EXACT_REGEXP, MISSING_KEY_REGEXP;
886 }
887
888 private final String key;
889 private final String value;
890 private final Pattern keyPattern;
891 private final Pattern valuePattern;
892 private final Mode mode;
893
894 /**
895 * Constructs a new {@code ExactKeyValue}.
896 * @param regexp regular expression
897 * @param key key
898 * @param value value
899 * @throws SearchParseError if a parse error occurs
900 */
901 public ExactKeyValue(boolean regexp, String key, String value) throws SearchParseError {
902 if ("".equals(key))
903 throw new SearchParseError(tr("Key cannot be empty when tag operator is used. Sample use: key=value"));
904 this.key = key;
905 this.value = value == null ? "" : value;
906 if ("".equals(this.value) && "*".equals(key)) {
907 mode = Mode.NONE;
908 } else if ("".equals(this.value)) {
909 if (regexp) {
910 mode = Mode.MISSING_KEY_REGEXP;
911 } else {
912 mode = Mode.MISSING_KEY;
913 }
914 } else if ("*".equals(key) && "*".equals(this.value)) {
915 mode = Mode.ANY;
916 } else if ("*".equals(key)) {
917 if (regexp) {
918 mode = Mode.ANY_KEY_REGEXP;
919 } else {
920 mode = Mode.ANY_KEY;
921 }
922 } else if ("*".equals(this.value)) {
923 if (regexp) {
924 mode = Mode.ANY_VALUE_REGEXP;
925 } else {
926 mode = Mode.ANY_VALUE;
927 }
928 } else {
929 if (regexp) {
930 mode = Mode.EXACT_REGEXP;
931 } else {
932 mode = Mode.EXACT;
933 }
934 }
935
936 if (regexp && !key.isEmpty() && !"*".equals(key)) {
937 keyPattern = compilePattern(key, regexFlags(false));
938 } else {
939 keyPattern = null;
940 }
941 if (regexp && !this.value.isEmpty() && !"*".equals(this.value)) {
942 valuePattern = compilePattern(this.value, regexFlags(false));
943 } else {
944 valuePattern = null;
945 }
946 }
947
948 @Override
949 public boolean match(Tagged osm) {
950
951 if (!osm.hasKeys())
952 return mode == Mode.NONE;
953
954 switch (mode) {
955 case NONE:
956 return false;
957 case MISSING_KEY:
958 return !osm.hasTag(key);
959 case ANY:
960 return true;
961 case ANY_VALUE:
962 return osm.hasTag(key);
963 case ANY_KEY:
964 return osm.getKeys().values().stream().anyMatch(v -> v.equals(value));
965 case EXACT:
966 return value.equals(osm.get(key));
967 case ANY_KEY_REGEXP:
968 return osm.getKeys().values().stream().anyMatch(v -> valuePattern.matcher(v).matches());
969 case ANY_VALUE_REGEXP:
970 case EXACT_REGEXP:
971 return osm.keySet().stream().anyMatch(k -> keyPattern.matcher(k).matches()
972 && (mode == Mode.ANY_VALUE_REGEXP || valuePattern.matcher(osm.get(k)).matches()));
973 case MISSING_KEY_REGEXP:
974 return osm.keySet().stream().noneMatch(k -> keyPattern.matcher(k).matches());
975 }
976 throw new AssertionError("Missed state");
977 }
978
979 public String getKey() {
980 return key;
981 }
982
983 public String getValue() {
984 return value;
985 }
986
987 public Mode getMode() {
988 return mode;
989 }
990
991 @Override
992 public String toString() {
993 return key + '=' + value;
994 }
995
996 @Override
997 public int hashCode() {
998 return Objects.hash(key, keyPattern, mode, value, valuePattern);
999 }
1000
1001 @Override
1002 public boolean equals(Object obj) {
1003 if (this == obj)
1004 return true;
1005 if (obj == null || getClass() != obj.getClass())
1006 return false;
1007 ExactKeyValue other = (ExactKeyValue) obj;
1008 if (key == null) {
1009 if (other.key != null)
1010 return false;
1011 } else if (!key.equals(other.key))
1012 return false;
1013 if (keyPattern == null) {
1014 if (other.keyPattern != null)
1015 return false;
1016 } else if (!keyPattern.equals(other.keyPattern))
1017 return false;
1018 if (mode != other.mode)
1019 return false;
1020 if (value == null) {
1021 if (other.value != null)
1022 return false;
1023 } else if (!value.equals(other.value))
1024 return false;
1025 if (valuePattern == null) {
1026 if (other.valuePattern != null)
1027 return false;
1028 } else if (!valuePattern.equals(other.valuePattern))
1029 return false;
1030 return true;
1031 }
1032 }
1033
1034 /**
1035 * Match a string in any tags (key or value), with optional regex and case insensitivity.
1036 */
1037 private static class Any extends TaggedMatch {
1038 private final String search;
1039 private final Pattern searchRegex;
1040 private final boolean caseSensitive;
1041
1042 Any(String s, boolean regexSearch, boolean caseSensitive) throws SearchParseError {
1043 s = Normalizer.normalize(s, Normalizer.Form.NFC);
1044 this.caseSensitive = caseSensitive;
1045 if (regexSearch) {
1046 this.searchRegex = compilePattern(s, regexFlags(caseSensitive));
1047 this.search = s;
1048 } else if (caseSensitive) {
1049 this.search = s;
1050 this.searchRegex = null;
1051 } else {
1052 this.search = s.toLowerCase(Locale.ENGLISH);
1053 this.searchRegex = null;
1054 }
1055 }
1056
1057 @Override
1058 public boolean match(Tagged osm) {
1059 if (!osm.hasKeys())
1060 return search.isEmpty();
1061
1062 for (String key: osm.keySet()) {
1063 String value = osm.get(key);
1064 if (searchRegex != null) {
1065
1066 value = Normalizer.normalize(value, Normalizer.Form.NFC);
1067
1068 Matcher keyMatcher = searchRegex.matcher(key);
1069 Matcher valMatcher = searchRegex.matcher(value);
1070
1071 boolean keyMatchFound = keyMatcher.find();
1072 boolean valMatchFound = valMatcher.find();
1073
1074 if (keyMatchFound || valMatchFound)
1075 return true;
1076 } else {
1077 if (!caseSensitive) {
1078 key = key.toLowerCase(Locale.ENGLISH);
1079 value = value.toLowerCase(Locale.ENGLISH);
1080 }
1081
1082 value = Normalizer.normalize(value, Normalizer.Form.NFC);
1083
1084 if (key.indexOf(search) != -1 || value.indexOf(search) != -1)
1085 return true;
1086 }
1087 }
1088 return false;
1089 }
1090
1091 @Override
1092 public String toString() {
1093 return search;
1094 }
1095
1096 @Override
1097 public int hashCode() {
1098 return Objects.hash(caseSensitive, search, searchRegex);
1099 }
1100
1101 @Override
1102 public boolean equals(Object obj) {
1103 if (this == obj)
1104 return true;
1105 if (obj == null || getClass() != obj.getClass())
1106 return false;
1107 Any other = (Any) obj;
1108 if (caseSensitive != other.caseSensitive)
1109 return false;
1110 if (search == null) {
1111 if (other.search != null)
1112 return false;
1113 } else if (!search.equals(other.search))
1114 return false;
1115 if (searchRegex == null) {
1116 if (other.searchRegex != null)
1117 return false;
1118 } else if (!searchRegex.equals(other.searchRegex))
1119 return false;
1120 return true;
1121 }
1122 }
1123
1124 public static class ExactType extends Match {
1125 private final OsmPrimitiveType type;
1126
1127 ExactType(String type) throws SearchParseError {
1128 this.type = OsmPrimitiveType.from(type);
1129 if (this.type == null)
1130 throw new SearchParseError(tr("Unknown primitive type: {0}. Allowed values are node, way or relation", type));
1131 }
1132
1133 public OsmPrimitiveType getType() {
1134 return type;
1135 }
1136
1137 @Override
1138 public boolean match(OsmPrimitive osm) {
1139 return type == osm.getType();
1140 }
1141
1142 @Override
1143 public String toString() {
1144 return "type=" + type;
1145 }
1146
1147 @Override
1148 public int hashCode() {
1149 return 31 + ((type == null) ? 0 : type.hashCode());
1150 }
1151
1152 @Override
1153 public boolean equals(Object obj) {
1154 if (this == obj)
1155 return true;
1156 if (obj == null || getClass() != obj.getClass())
1157 return false;
1158 ExactType other = (ExactType) obj;
1159 return type == other.type;
1160 }
1161 }
1162
1163 /**
1164 * Matches objects last changed by the given username.
1165 */
1166 public static class UserMatch extends Match {
1167 private String user;
1168
1169 UserMatch(String user) {
1170 if ("anonymous".equals(user)) {
1171 this.user = null;
1172 } else {
1173 this.user = user;
1174 }
1175 }
1176
1177 public String getUser() {
1178 return user;
1179 }
1180
1181 @Override
1182 public boolean match(OsmPrimitive osm) {
1183 if (osm.getUser() == null)
1184 return user == null;
1185 else
1186 return osm.getUser().hasName(user);
1187 }
1188
1189 @Override
1190 public String toString() {
1191 return "user=" + (user == null ? "" : user);
1192 }
1193
1194 @Override
1195 public int hashCode() {
1196 return 31 + ((user == null) ? 0 : user.hashCode());
1197 }
1198
1199 @Override
1200 public boolean equals(Object obj) {
1201 if (this == obj)
1202 return true;
1203 if (obj == null || getClass() != obj.getClass())
1204 return false;
1205 UserMatch other = (UserMatch) obj;
1206 if (user == null) {
1207 if (other.user != null)
1208 return false;
1209 } else if (!user.equals(other.user))
1210 return false;
1211 return true;
1212 }
1213 }
1214
1215 /**
1216 * Matches objects with the given relation role (i.e. "outer").
1217 */
1218 private static class RoleMatch extends Match {
1219 private String role;
1220
1221 RoleMatch(String role) {
1222 if (role == null) {
1223 this.role = "";
1224 } else {
1225 this.role = role;
1226 }
1227 }
1228
1229 @Override
1230 public boolean match(OsmPrimitive osm) {
1231 return osm.referrers(Relation.class)
1232 .filter(ref -> !ref.isIncomplete() && !ref.isDeleted())
1233 .flatMap(ref -> ref.getMembers().stream()).filter(m -> m.getMember() == osm)
1234 .map(RelationMember::getRole)
1235 .anyMatch(testRole -> role.equals(testRole == null ? "" : testRole));
1236 }
1237
1238 @Override
1239 public String toString() {
1240 return "role=" + role;
1241 }
1242
1243 @Override
1244 public int hashCode() {
1245 return 31 + ((role == null) ? 0 : role.hashCode());
1246 }
1247
1248 @Override
1249 public boolean equals(Object obj) {
1250 if (this == obj)
1251 return true;
1252 if (obj == null || getClass() != obj.getClass())
1253 return false;
1254 RoleMatch other = (RoleMatch) obj;
1255 if (role == null) {
1256 if (other.role != null)
1257 return false;
1258 } else if (!role.equals(other.role))
1259 return false;
1260 return true;
1261 }
1262 }
1263
1264 /**
1265 * Matches the n-th object of a relation and/or the n-th node of a way.
1266 */
1267 private static class Nth extends Match {
1268
1269 private final int nth;
1270 private final boolean modulo;
1271
1272 Nth(PushbackTokenizer tokenizer, boolean modulo) throws SearchParseError {
1273 this((int) tokenizer.readNumber(tr("Positive integer expected")), modulo);
1274 }
1275
1276 private Nth(int nth, boolean modulo) {
1277 this.nth = nth;
1278 this.modulo = modulo;
1279 }
1280
1281 @Override
1282 public boolean match(OsmPrimitive osm) {
1283 for (OsmPrimitive p : osm.getReferrers()) {
1284 final int idx;
1285 final int maxIndex;
1286 if (p instanceof Way) {
1287 Way w = (Way) p;
1288 idx = w.getNodes().indexOf(osm);
1289 maxIndex = w.getNodesCount();
1290 } else if (p instanceof Relation) {
1291 Relation r = (Relation) p;
1292 idx = r.getMemberPrimitivesList().indexOf(osm);
1293 maxIndex = r.getMembersCount();
1294 } else {
1295 continue;
1296 }
1297 if (nth < 0 && idx - maxIndex == nth) {
1298 return true;
1299 } else if (idx == nth || (modulo && idx % nth == 0))
1300 return true;
1301 }
1302 return false;
1303 }
1304
1305 @Override
1306 public String toString() {
1307 return "Nth{nth=" + nth + ", modulo=" + modulo + '}';
1308 }
1309
1310 @Override
1311 public int hashCode() {
1312 return Objects.hash(modulo, nth);
1313 }
1314
1315 @Override
1316 public boolean equals(Object obj) {
1317 if (this == obj)
1318 return true;
1319 if (obj == null || getClass() != obj.getClass())
1320 return false;
1321 Nth other = (Nth) obj;
1322 return modulo == other.modulo
1323 && nth == other.nth;
1324 }
1325 }
1326
1327 /**
1328 * Matches objects with properties in a certain range.
1329 */
1330 private abstract static class RangeMatch extends Match {
1331
1332 private final long min;
1333 private final long max;
1334
1335 RangeMatch(long min, long max) {
1336 this.min = Math.min(min, max);
1337 this.max = Math.max(min, max);
1338 }
1339
1340 RangeMatch(Range range) {
1341 this(range.getStart(), range.getEnd());
1342 }
1343
1344 protected abstract Long getNumber(OsmPrimitive osm);
1345
1346 protected abstract String getString();
1347
1348 @Override
1349 public boolean match(OsmPrimitive osm) {
1350 Long num = getNumber(osm);
1351 if (num == null)
1352 return false;
1353 else
1354 return (num >= min) && (num <= max);
1355 }
1356
1357 @Override
1358 public String toString() {
1359 return getString() + '=' + min + '-' + max;
1360 }
1361
1362 @Override
1363 public int hashCode() {
1364 return Objects.hash(max, min);
1365 }
1366
1367 @Override
1368 public boolean equals(Object obj) {
1369 if (this == obj)
1370 return true;
1371 if (obj == null || getClass() != obj.getClass())
1372 return false;
1373 RangeMatch other = (RangeMatch) obj;
1374 return max == other.max
1375 && min == other.min;
1376 }
1377 }
1378
1379 /**
1380 * Matches ways with a number of nodes in given range
1381 */
1382 private static class NodeCountRange extends RangeMatch {
1383 NodeCountRange(Range range) {
1384 super(range);
1385 }
1386
1387 NodeCountRange(PushbackTokenizer tokenizer) throws SearchParseError {
1388 this(tokenizer.readRange(tr("Range of numbers expected")));
1389 }
1390
1391 @Override
1392 protected Long getNumber(OsmPrimitive osm) {
1393 if (osm instanceof Way) {
1394 return (long) ((Way) osm).getRealNodesCount();
1395 } else if (osm instanceof Relation) {
1396 return (long) ((Relation) osm).getMemberPrimitives(Node.class).size();
1397 } else {
1398 return null;
1399 }
1400 }
1401
1402 @Override
1403 protected String getString() {
1404 return "nodes";
1405 }
1406 }
1407
1408 /**
1409 * Matches objects with the number of referring/contained ways in the given range
1410 */
1411 private static class WayCountRange extends RangeMatch {
1412 WayCountRange(Range range) {
1413 super(range);
1414 }
1415
1416 WayCountRange(PushbackTokenizer tokenizer) throws SearchParseError {
1417 this(tokenizer.readRange(tr("Range of numbers expected")));
1418 }
1419
1420 @Override
1421 protected Long getNumber(OsmPrimitive osm) {
1422 if (osm instanceof Node) {
1423 return osm.referrers(Way.class).count();
1424 } else if (osm instanceof Relation) {
1425 return (long) ((Relation) osm).getMemberPrimitives(Way.class).size();
1426 } else {
1427 return null;
1428 }
1429 }
1430
1431 @Override
1432 protected String getString() {
1433 return "ways";
1434 }
1435 }
1436
1437 /**
1438 * Matches objects with a number of tags in given range
1439 */
1440 private static class TagCountRange extends RangeMatch {
1441 TagCountRange(Range range) {
1442 super(range);
1443 }
1444
1445 TagCountRange(PushbackTokenizer tokenizer) throws SearchParseError {
1446 this(tokenizer.readRange(tr("Range of numbers expected")));
1447 }
1448
1449 @Override
1450 protected Long getNumber(OsmPrimitive osm) {
1451 return (long) osm.getKeys().size();
1452 }
1453
1454 @Override
1455 protected String getString() {
1456 return "tags";
1457 }
1458 }
1459
1460 /**
1461 * Matches objects with a timestamp in given range
1462 */
1463 private static class TimestampRange extends RangeMatch {
1464
1465 TimestampRange(long minCount, long maxCount) {
1466 super(minCount, maxCount);
1467 }
1468
1469 @Override
1470 protected Long getNumber(OsmPrimitive osm) {
1471 return osm.getTimestamp().getTime();
1472 }
1473
1474 @Override
1475 protected String getString() {
1476 return "timestamp";
1477 }
1478 }
1479
1480 /**
1481 * Matches relations with a member of the given role
1482 */
1483 private static class HasRole extends Match {
1484 private final String role;
1485
1486 HasRole(PushbackTokenizer tokenizer) {
1487 role = tokenizer.readTextOrNumber();
1488 }
1489
1490 @Override
1491 public boolean match(OsmPrimitive osm) {
1492 return osm instanceof Relation && ((Relation) osm).getMemberRoles().contains(role);
1493 }
1494
1495 @Override
1496 public int hashCode() {
1497 return 31 + ((role == null) ? 0 : role.hashCode());
1498 }
1499
1500 @Override
1501 public boolean equals(Object obj) {
1502 if (this == obj)
1503 return true;
1504 if (obj == null || getClass() != obj.getClass())
1505 return false;
1506 HasRole other = (HasRole) obj;
1507 if (role == null) {
1508 if (other.role != null)
1509 return false;
1510 } else if (!role.equals(other.role))
1511 return false;
1512 return true;
1513 }
1514 }
1515
1516 /**
1517 * Matches objects that are new (i.e. have not been uploaded to the server)
1518 */
1519 private static class New extends Match {
1520 @Override
1521 public boolean match(OsmPrimitive osm) {
1522 return osm.isNew();
1523 }
1524
1525 @Override
1526 public String toString() {
1527 return "new";
1528 }
1529 }
1530
1531 /**
1532 * Matches all objects that have been modified, created, or undeleted
1533 */
1534 private static class Modified extends Match {
1535 @Override
1536 public boolean match(OsmPrimitive osm) {
1537 return osm.isModified() || osm.isNewOrUndeleted();
1538 }
1539
1540 @Override
1541 public String toString() {
1542 return "modified";
1543 }
1544 }
1545
1546 /**
1547 * Matches all objects that have been deleted
1548 */
1549 private static class Deleted extends Match {
1550 @Override
1551 public boolean match(OsmPrimitive osm) {
1552 return osm.isDeleted();
1553 }
1554
1555 @Override
1556 public String toString() {
1557 return "deleted";
1558 }
1559 }
1560
1561 /**
1562 * Matches all objects currently selected
1563 */
1564 private static class Selected extends Match {
1565 @Override
1566 public boolean match(OsmPrimitive osm) {
1567 return osm.getDataSet().isSelected(osm);
1568 }
1569
1570 @Override
1571 public String toString() {
1572 return "selected";
1573 }
1574 }
1575
1576 /**
1577 * Match objects that are incomplete, where only id and type are known.
1578 * Typically some members of a relation are incomplete until they are
1579 * fetched from the server.
1580 */
1581 private static class Incomplete extends Match {
1582 @Override
1583 public boolean match(OsmPrimitive osm) {
1584 return osm.isIncomplete() || (osm instanceof Relation && ((Relation) osm).hasIncompleteMembers());
1585 }
1586
1587 @Override
1588 public String toString() {
1589 return "incomplete";
1590 }
1591 }
1592
1593 /**
1594 * Matches objects that don't have any interesting tags (i.e. only has source,
1595 * FIXME, etc.). The complete list of uninteresting tags can be found here:
1596 * org.openstreetmap.josm.data.osm.OsmPrimitive.getUninterestingKeys()
1597 */
1598 private static class Untagged extends Match {
1599 @Override
1600 public boolean match(OsmPrimitive osm) {
1601 return !osm.isTagged() && !osm.isIncomplete();
1602 }
1603
1604 @Override
1605 public String toString() {
1606 return "untagged";
1607 }
1608 }
1609
1610 /**
1611 * Matches ways which are closed (i.e. first and last node are the same)
1612 */
1613 private static class Closed extends Match {
1614 @Override
1615 public boolean match(OsmPrimitive osm) {
1616 return osm instanceof Way && ((Way) osm).isClosed();
1617 }
1618
1619 @Override
1620 public String toString() {
1621 return "closed";
1622 }
1623 }
1624
1625 /**
1626 * Matches objects if they are parents of the expression
1627 */
1628 public static class Parent extends UnaryMatch {
1629 public Parent(Match m) {
1630 super(m);
1631 }
1632
1633 @Override
1634 public boolean match(OsmPrimitive osm) {
1635 if (osm instanceof Way) {
1636 return ((Way) osm).getNodes().stream().anyMatch(match::match);
1637 } else if (osm instanceof Relation) {
1638 return ((Relation) osm).getMembers().stream().anyMatch(member -> match.match(member.getMember()));
1639 } else {
1640 return false;
1641 }
1642 }
1643
1644 @Override
1645 public String toString() {
1646 return "parent(" + match + ')';
1647 }
1648 }
1649
1650 /**
1651 * Matches objects if they are children of the expression
1652 */
1653 public static class Child extends UnaryMatch {
1654
1655 public Child(Match m) {
1656 super(m);
1657 }
1658
1659 @Override
1660 public boolean match(OsmPrimitive osm) {
1661 return osm.getReferrers().stream().anyMatch(match::match);
1662 }
1663
1664 @Override
1665 public String toString() {
1666 return "child(" + match + ')';
1667 }
1668 }
1669
1670 /**
1671 * Matches if the size of the area is within the given range
1672 *
1673 * @author Ole Jørgen Brønner
1674 */
1675 private static class AreaSize extends RangeMatch {
1676
1677 AreaSize(Range range) {
1678 super(range);
1679 }
1680
1681 AreaSize(PushbackTokenizer tokenizer) throws SearchParseError {
1682 this(tokenizer.readRange(tr("Range of numbers expected")));
1683 }
1684
1685 @Override
1686 protected Long getNumber(OsmPrimitive osm) {
1687 final Double area = Geometry.computeArea(osm);
1688 return area == null ? null : area.longValue();
1689 }
1690
1691 @Override
1692 protected String getString() {
1693 return "areasize";
1694 }
1695 }
1696
1697 /**
1698 * Matches if the length of a way is within the given range
1699 */
1700 private static class WayLength extends RangeMatch {
1701
1702 WayLength(Range range) {
1703 super(range);
1704 }
1705
1706 WayLength(PushbackTokenizer tokenizer) throws SearchParseError {
1707 this(tokenizer.readRange(tr("Range of numbers expected")));
1708 }
1709
1710 @Override
1711 protected Long getNumber(OsmPrimitive osm) {
1712 if (!(osm instanceof Way))
1713 return null;
1714 Way way = (Way) osm;
1715 return (long) way.getLength();
1716 }
1717
1718 @Override
1719 protected String getString() {
1720 return "waylength";
1721 }
1722 }
1723
1724 /**
1725 * Matches objects within the given bounds.
1726 */
1727 public abstract static class InArea extends Match {
1728
1729 protected final boolean all;
1730
1731 /**
1732 * @param all if true, all way nodes or relation members have to be within source area;if false, one suffices.
1733 */
1734 protected InArea(boolean all) {
1735 this.all = all;
1736 }
1737
1738 protected abstract Collection<Bounds> getBounds(OsmPrimitive primitive);
1739
1740 @Override
1741 public boolean match(OsmPrimitive osm) {
1742 if (!osm.isUsable())
1743 return false;
1744 else if (osm instanceof Node) {
1745 LatLon coordinate = ((Node) osm).getCoor();
1746 Collection<Bounds> allBounds = getBounds(osm);
1747 return coordinate != null && allBounds != null && allBounds.stream().anyMatch(bounds -> bounds.contains(coordinate));
1748 } else if (osm instanceof Way) {
1749 Collection<Node> nodes = ((Way) osm).getNodes();
1750 return all ? nodes.stream().allMatch(this) : nodes.stream().anyMatch(this);
1751 } else if (osm instanceof Relation) {
1752 Collection<OsmPrimitive> primitives = ((Relation) osm).getMemberPrimitivesList();
1753 return all ? primitives.stream().allMatch(this) : primitives.stream().anyMatch(this);
1754 } else
1755 return false;
1756 }
1757
1758 @Override
1759 public int hashCode() {
1760 return 31 + (all ? 1231 : 1237);
1761 }
1762
1763 @Override
1764 public boolean equals(Object obj) {
1765 if (this == obj)
1766 return true;
1767 if (obj == null || getClass() != obj.getClass())
1768 return false;
1769 InArea other = (InArea) obj;
1770 return all == other.all;
1771 }
1772 }
1773
1774 /**
1775 * Matches objects within source area ("downloaded area").
1776 */
1777 public static class InDataSourceArea extends InArea {
1778
1779 /**
1780 * Constructs a new {@code InDataSourceArea}.
1781 * @param all if true, all way nodes or relation members have to be within source area; if false, one suffices.
1782 */
1783 public InDataSourceArea(boolean all) {
1784 super(all);
1785 }
1786
1787 @Override
1788 protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
1789 return primitive.getDataSet() != null ? primitive.getDataSet().getDataSourceBounds() : null;
1790 }
1791
1792 @Override
1793 public String toString() {
1794 return all ? "allindownloadedarea" : "indownloadedarea";
1795 }
1796 }
1797
1798 /**
1799 * Matches objects which are not outside the source area ("downloaded area").
1800 * Unlike {@link InDataSourceArea} this matches also if no source area is set (e.g., for new layers).
1801 */
1802 public static class NotOutsideDataSourceArea extends InDataSourceArea {
1803
1804 /**
1805 * Constructs a new {@code NotOutsideDataSourceArea}.
1806 */
1807 public NotOutsideDataSourceArea() {
1808 super(false);
1809 }
1810
1811 @Override
1812 protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
1813 final Collection<Bounds> bounds = super.getBounds(primitive);
1814 return bounds == null || bounds.isEmpty() ?
1815 Collections.singleton(ProjectionRegistry.getProjection().getWorldBoundsLatLon()) : bounds;
1816 }
1817
1818 @Override
1819 public String toString() {
1820 return "NotOutsideDataSourceArea";
1821 }
1822 }
1823
1824 /**
1825 * Matches presets.
1826 * @since 12464
1827 */
1828 private static class Preset extends Match {
1829 private final List<TaggingPreset> presets;
1830
1831 Preset(String presetName) throws SearchParseError {
1832
1833 if (presetName == null || presetName.isEmpty()) {
1834 throw new SearchParseError("The name of the preset is required");
1835 }
1836
1837 int wildCardIdx = presetName.lastIndexOf('*');
1838 int length = presetName.length() - 1;
1839
1840 /*
1841 * Match strictly (simply comparing the names) if there is no '*' symbol
1842 * at the end of the name or '*' is a part of the preset name.
1843 */
1844 boolean matchStrictly = wildCardIdx == -1 || wildCardIdx != length;
1845
1846 this.presets = TaggingPresets.getTaggingPresets()
1847 .stream()
1848 .filter(preset -> !(preset instanceof TaggingPresetMenu || preset instanceof TaggingPresetSeparator))
1849 .filter(preset -> presetNameMatch(presetName, preset, matchStrictly))
1850 .collect(Collectors.toList());
1851
1852 if (this.presets.isEmpty()) {
1853 throw new SearchParseError(tr("Unknown preset name: ") + presetName);
1854 }
1855 }
1856
1857 @Override
1858 public boolean match(OsmPrimitive osm) {
1859 return this.presets.stream().anyMatch(preset -> preset.test(osm));
1860 }
1861
1862 private static boolean presetNameMatch(String name, TaggingPreset preset, boolean matchStrictly) {
1863 if (matchStrictly) {
1864 return name.equalsIgnoreCase(preset.getRawName());
1865 }
1866
1867 try {
1868 String groupSuffix = name.substring(0, name.length() - 2); // try to remove '/*'
1869 TaggingPresetMenu group = preset.group;
1870
1871 return group != null && groupSuffix.equalsIgnoreCase(group.getRawName());
1872 } catch (StringIndexOutOfBoundsException ex) {
1873 Logging.trace(ex);
1874 return false;
1875 }
1876 }
1877
1878 @Override
1879 public int hashCode() {
1880 return 31 + ((presets == null) ? 0 : presets.hashCode());
1881 }
1882
1883 @Override
1884 public boolean equals(Object obj) {
1885 if (this == obj)
1886 return true;
1887 if (obj == null || getClass() != obj.getClass())
1888 return false;
1889 Preset other = (Preset) obj;
1890 if (presets == null) {
1891 if (other.presets != null)
1892 return false;
1893 } else if (!presets.equals(other.presets))
1894 return false;
1895 return true;
1896 }
1897 }
1898
1899 /**
1900 * Compiles the search expression.
1901 * @param searchStr the search expression
1902 * @return a {@link Match} object for the expression
1903 * @throws SearchParseError if an error has been encountered while compiling
1904 * @see #compile(SearchSetting)
1905 */
1906 public static Match compile(String searchStr) throws SearchParseError {
1907 return new SearchCompiler(false, false,
1908 new PushbackTokenizer(
1909 new PushbackReader(new StringReader(searchStr))))
1910 .parse();
1911 }
1912
1913 /**
1914 * Compiles the search expression.
1915 * @param setting the settings to use
1916 * @return a {@link Match} object for the expression
1917 * @throws SearchParseError if an error has been encountered while compiling
1918 * @see #compile(String)
1919 */
1920 public static Match compile(SearchSetting setting) throws SearchParseError {
1921 if (setting instanceof MatchSupplier) {
1922 return ((MatchSupplier) setting).get();
1923 }
1924 if (setting.mapCSSSearch) {
1925 return compileMapCSS(setting.text);
1926 }
1927 return new SearchCompiler(setting.caseSensitive, setting.regexSearch,
1928 new PushbackTokenizer(
1929 new PushbackReader(new StringReader(setting.text))))
1930 .parse();
1931 }
1932
1933 static Match compileMapCSS(String mapCSS) throws SearchParseError {
1934 try {
1935 final List<Selector> selectors = new MapCSSParser(new StringReader(mapCSS)).selectors_for_search();
1936 return new MapCSSMatch(selectors);
1937 } catch (ParseException | IllegalArgumentException e) {
1938 throw new SearchParseError(tr("Failed to parse MapCSS selector"), e);
1939 }
1940 }
1941
1942 private static class MapCSSMatch extends Match {
1943 private final List<Selector> selectors;
1944
1945 MapCSSMatch(List<Selector> selectors) {
1946 this.selectors = selectors;
1947 }
1948
1949 @Override
1950 public boolean match(OsmPrimitive osm) {
1951 return selectors.stream()
1952 .anyMatch(selector -> selector.matches(new Environment(osm)));
1953 }
1954
1955 @Override
1956 public boolean equals(Object o) {
1957 if (this == o) return true;
1958 if (o == null || getClass() != o.getClass()) return false;
1959 MapCSSMatch that = (MapCSSMatch) o;
1960 return Objects.equals(selectors, that.selectors);
1961 }
1962
1963 @Override
1964 public int hashCode() {
1965 return Objects.hash(selectors);
1966 }
1967 }
1968
1969 /**
1970 * Parse search string.
1971 *
1972 * @return match determined by search string
1973 * @throws org.openstreetmap.josm.data.osm.search.SearchParseError if search expression cannot be parsed
1974 */
1975 public Match parse() throws SearchParseError {
1976 Match m = Optional.ofNullable(parseExpression()).orElse(Always.INSTANCE);
1977 if (!tokenizer.readIfEqual(Token.EOF))
1978 throw new SearchParseError(tr("Unexpected token: {0}", tokenizer.nextToken()));
1979 Logging.debug("Parsed search expression is {0}", m);
1980 return m;
1981 }
1982
1983 /**
1984 * Parse expression.
1985 *
1986 * @return match determined by parsing expression
1987 * @throws SearchParseError if search expression cannot be parsed
1988 */
1989 private Match parseExpression() throws SearchParseError {
1990 // Step 1: parse the whole expression and build a list of factors and logical tokens
1991 List<Object> list = parseExpressionStep1();
1992 // Step 2: iterate the list in reverse order to build the logical expression
1993 // This iterative approach avoids StackOverflowError for long expressions (see #14217)
1994 return parseExpressionStep2(list);
1995 }
1996
1997 private List<Object> parseExpressionStep1() throws SearchParseError {
1998 Match factor;
1999 String token = null;
2000 String errorMessage = null;
2001 List<Object> list = new ArrayList<>();
2002 do {
2003 factor = parseFactor();
2004 if (factor != null) {
2005 if (token != null) {
2006 list.add(token);
2007 }
2008 list.add(factor);
2009 if (tokenizer.readIfEqual(Token.OR)) {
2010 token = "OR";
2011 errorMessage = tr("Missing parameter for OR");
2012 } else if (tokenizer.readIfEqual(Token.XOR)) {
2013 token = "XOR";
2014 errorMessage = tr("Missing parameter for XOR");
2015 } else {
2016 token = "AND";
2017 errorMessage = null;
2018 }
2019 } else if (errorMessage != null) {
2020 throw new SearchParseError(errorMessage);
2021 }
2022 } while (factor != null);
2023 return list;
2024 }
2025
2026 private static Match parseExpressionStep2(List<Object> list) {
2027 Match result = null;
2028 for (int i = list.size() - 1; i >= 0; i--) {
2029 Object o = list.get(i);
2030 if (o instanceof Match && result == null) {
2031 result = (Match) o;
2032 } else if (o instanceof String && i > 0) {
2033 Match factor = (Match) list.get(i-1);
2034 switch ((String) o) {
2035 case "OR":
2036 result = new Or(factor, result);
2037 break;
2038 case "XOR":
2039 result = new Xor(factor, result);
2040 break;
2041 case "AND":
2042 result = new And(factor, result);
2043 break;
2044 default: throw new IllegalStateException(tr("Unexpected token: {0}", o));
2045 }
2046 i--;
2047 } else {
2048 throw new IllegalStateException("i=" + i + "; o=" + o);
2049 }
2050 }
2051 return result;
2052 }
2053
2054 /**
2055 * Parse next factor (a search operator or search term).
2056 *
2057 * @return match determined by parsing factor string
2058 * @throws SearchParseError if search expression cannot be parsed
2059 */
2060 private Match parseFactor() throws SearchParseError {
2061 if (tokenizer.readIfEqual(Token.LEFT_PARENT)) {
2062 Match expression = parseExpression();
2063 if (!tokenizer.readIfEqual(Token.RIGHT_PARENT))
2064 throw new SearchParseError(Token.RIGHT_PARENT, tokenizer.nextToken());
2065 return expression;
2066 } else if (tokenizer.readIfEqual(Token.NOT)) {
2067 return new Not(parseFactor(tr("Missing operator for NOT")));
2068 } else if (tokenizer.readIfEqual(Token.KEY)) {
2069 // factor consists of key:value or key=value
2070 String key = tokenizer.getText();
2071 if (tokenizer.readIfEqual(Token.EQUALS)) {
2072 return new ExactKeyValue(regexSearch, key, tokenizer.readTextOrNumber());
2073 } else if (tokenizer.readIfEqual(Token.TILDE)) {
2074 return new ExactKeyValue(true, key, tokenizer.readTextOrNumber());
2075 } else if (tokenizer.readIfEqual(Token.LESS_THAN)) {
2076 return new ValueComparison(key, tokenizer.readTextOrNumber(), -1);
2077 } else if (tokenizer.readIfEqual(Token.GREATER_THAN)) {
2078 return new ValueComparison(key, tokenizer.readTextOrNumber(), +1);
2079 } else if (tokenizer.readIfEqual(Token.COLON)) {
2080 // see if we have a Match that takes a data parameter
2081 SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
2082 if (factory != null)
2083 return factory.get(key, caseSensitive, regexSearch, tokenizer);
2084
2085 UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
2086 if (unaryFactory != null)
2087 return unaryFactory.get(key, parseFactor(), tokenizer);
2088
2089 // key:value form where value is a string (may be OSM key search)
2090 final String value = tokenizer.readTextOrNumber();
2091 return new KeyValue(key, value != null ? value : "", regexSearch, caseSensitive);
2092 } else if (tokenizer.readIfEqual(Token.QUESTION_MARK))
2093 return new BooleanMatch(key, false);
2094 else {
2095 SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
2096 if (factory != null)
2097 return factory.get(key, caseSensitive, regexSearch, null);
2098
2099 UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
2100 if (unaryFactory != null)
2101 return unaryFactory.get(key, parseFactor(), null);
2102
2103 // match string in any key or value
2104 return new Any(key, regexSearch, caseSensitive);
2105 }
2106 } else
2107 return null;
2108 }
2109
2110 private Match parseFactor(String errorMessage) throws SearchParseError {
2111 return Optional.ofNullable(parseFactor()).orElseThrow(() -> new SearchParseError(errorMessage));
2112 }
2113
2114 private static int regexFlags(boolean caseSensitive) {
2115 int searchFlags = 0;
2116
2117 // Enables canonical Unicode equivalence so that e.g. the two
2118 // forms of "\u00e9gal" and "e\u0301gal" will match.
2119 //
2120 // It makes sense to match no matter how the character
2121 // happened to be constructed.
2122 searchFlags |= Pattern.CANON_EQ;
2123
2124 // Make "." match any character including newline (/s in Perl)
2125 searchFlags |= Pattern.DOTALL;
2126
2127 // CASE_INSENSITIVE by itself only matches US-ASCII case
2128 // insensitively, but the OSM data is in Unicode. With
2129 // UNICODE_CASE casefolding is made Unicode-aware.
2130 if (!caseSensitive) {
2131 searchFlags |= (Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
2132 }
2133
2134 return searchFlags;
2135 }
2136
2137 static String escapeStringForSearch(String s) {
2138 return s.replace("\\", "\\\\").replace("\"", "\\\"");
2139 }
2140
2141 /**
2142 * Builds a search string for the given tag. If value is empty, the existence of the key is checked.
2143 *
2144 * @param key the tag key
2145 * @param value the tag value
2146 * @return a search string for the given tag
2147 */
2148 public static String buildSearchStringForTag(String key, String value) {
2149 final String forKey = '"' + escapeStringForSearch(key) + '"' + '=';
2150 if (value == null || value.isEmpty()) {
2151 return forKey + '*';
2152 } else {
2153 return forKey + '"' + escapeStringForSearch(value) + '"';
2154 }
2155 }
2156}
Note: See TracBrowser for help on using the repository browser.