source: josm/trunk/src/org/openstreetmap/josm/actions/search/SearchCompiler.java@ 12636

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

see #15182 - deprecate Main.map and Main.isDisplayingMapView(). Replacements: gui.MainApplication.getMap() / gui.MainApplication.isDisplayingMapView()

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