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

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

global use of Utils.isEmpty/isBlank

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