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

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

see #15182 - fix javadoc warning

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