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

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

sonar - squid:S2325 - "private" methods that don't access instance data should be "static"

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