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

Last change on this file since 18852 was 18852, checked in by taylor.smock, 9 months ago

Fix regression from r18847: Current tests expect a search for access= to match access=null and access="" (has key, but no value)

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