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

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

improve coverage and javadoc of enum classes for package actions

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