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

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

see #15182 - extract SearchMode and SearchSetting from actions.search.SearchAction to data.osm.search

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