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

Last change on this file since 11809 was 11613, 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: 59.2 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 private 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 public ExactKeyValue(boolean regexp, String key, String value) throws ParseError {
760 if ("".equals(key))
761 throw new ParseError(tr("Key cannot be empty when tag operator is used. Sample use: key=value"));
762 this.key = key;
763 this.value = value == null ? "" : value;
764 if ("".equals(this.value) && "*".equals(key)) {
765 mode = Mode.NONE;
766 } else if ("".equals(this.value)) {
767 if (regexp) {
768 mode = Mode.MISSING_KEY_REGEXP;
769 } else {
770 mode = Mode.MISSING_KEY;
771 }
772 } else if ("*".equals(key) && "*".equals(this.value)) {
773 mode = Mode.ANY;
774 } else if ("*".equals(key)) {
775 if (regexp) {
776 mode = Mode.ANY_KEY_REGEXP;
777 } else {
778 mode = Mode.ANY_KEY;
779 }
780 } else if ("*".equals(this.value)) {
781 if (regexp) {
782 mode = Mode.ANY_VALUE_REGEXP;
783 } else {
784 mode = Mode.ANY_VALUE;
785 }
786 } else {
787 if (regexp) {
788 mode = Mode.EXACT_REGEXP;
789 } else {
790 mode = Mode.EXACT;
791 }
792 }
793
794 if (regexp && !key.isEmpty() && !"*".equals(key)) {
795 try {
796 keyPattern = Pattern.compile(key, regexFlags(false));
797 } catch (PatternSyntaxException e) {
798 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
799 } catch (IllegalArgumentException e) {
800 throw new ParseError(tr(rxErrorMsgNoPos, key, e.getMessage()), e);
801 }
802 } else {
803 keyPattern = null;
804 }
805 if (regexp && !this.value.isEmpty() && !"*".equals(this.value)) {
806 try {
807 valuePattern = Pattern.compile(this.value, regexFlags(false));
808 } catch (PatternSyntaxException e) {
809 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
810 } catch (IllegalArgumentException e) {
811 throw new ParseError(tr(rxErrorMsgNoPos, value, e.getMessage()), e);
812 }
813 } else {
814 valuePattern = null;
815 }
816 }
817
818 @Override
819 public boolean match(Tagged osm) {
820
821 if (!osm.hasKeys())
822 return mode == Mode.NONE;
823
824 switch (mode) {
825 case NONE:
826 return false;
827 case MISSING_KEY:
828 return osm.get(key) == null;
829 case ANY:
830 return true;
831 case ANY_VALUE:
832 return osm.get(key) != null;
833 case ANY_KEY:
834 for (String v:osm.getKeys().values()) {
835 if (v.equals(value))
836 return true;
837 }
838 return false;
839 case EXACT:
840 return value.equals(osm.get(key));
841 case ANY_KEY_REGEXP:
842 for (String v:osm.getKeys().values()) {
843 if (valuePattern.matcher(v).matches())
844 return true;
845 }
846 return false;
847 case ANY_VALUE_REGEXP:
848 case EXACT_REGEXP:
849 for (String k : osm.keySet()) {
850 if (keyPattern.matcher(k).matches()
851 && (mode == Mode.ANY_VALUE_REGEXP || valuePattern.matcher(osm.get(k)).matches()))
852 return true;
853 }
854 return false;
855 case MISSING_KEY_REGEXP:
856 for (String k:osm.keySet()) {
857 if (keyPattern.matcher(k).matches())
858 return false;
859 }
860 return true;
861 }
862 throw new AssertionError("Missed state");
863 }
864
865 @Override
866 public String toString() {
867 return key + '=' + value;
868 }
869 }
870
871 /**
872 * Match a string in any tags (key or value), with optional regex and case insensitivity.
873 */
874 private static class Any extends TaggedMatch {
875 private final String search;
876 private final Pattern searchRegex;
877 private final boolean caseSensitive;
878
879 Any(String s, boolean regexSearch, boolean caseSensitive) throws ParseError {
880 s = Normalizer.normalize(s, Normalizer.Form.NFC);
881 this.caseSensitive = caseSensitive;
882 if (regexSearch) {
883 try {
884 this.searchRegex = Pattern.compile(s, regexFlags(caseSensitive));
885 } catch (PatternSyntaxException e) {
886 throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
887 } catch (IllegalArgumentException | StringIndexOutOfBoundsException e) {
888 // StringIndexOutOfBoundsException catched because of https://bugs.openjdk.java.net/browse/JI-9044959
889 // See #13870: To remove after we switch to a version of Java which resolves this bug
890 throw new ParseError(tr(rxErrorMsgNoPos, s, e.getMessage()), e);
891 }
892 this.search = s;
893 } else if (caseSensitive) {
894 this.search = s;
895 this.searchRegex = null;
896 } else {
897 this.search = s.toLowerCase(Locale.ENGLISH);
898 this.searchRegex = null;
899 }
900 }
901
902 @Override
903 public boolean match(Tagged osm) {
904 if (!osm.hasKeys())
905 return search.isEmpty();
906
907 for (String key: osm.keySet()) {
908 String value = osm.get(key);
909 if (searchRegex != null) {
910
911 value = Normalizer.normalize(value, Normalizer.Form.NFC);
912
913 Matcher keyMatcher = searchRegex.matcher(key);
914 Matcher valMatcher = searchRegex.matcher(value);
915
916 boolean keyMatchFound = keyMatcher.find();
917 boolean valMatchFound = valMatcher.find();
918
919 if (keyMatchFound || valMatchFound)
920 return true;
921 } else {
922 if (!caseSensitive) {
923 key = key.toLowerCase(Locale.ENGLISH);
924 value = value.toLowerCase(Locale.ENGLISH);
925 }
926
927 value = Normalizer.normalize(value, Normalizer.Form.NFC);
928
929 if (key.indexOf(search) != -1 || value.indexOf(search) != -1)
930 return true;
931 }
932 }
933 return false;
934 }
935
936 @Override
937 public String toString() {
938 return search;
939 }
940 }
941
942 private static class ExactType extends Match {
943 private final OsmPrimitiveType type;
944
945 ExactType(String type) throws ParseError {
946 this.type = OsmPrimitiveType.from(type);
947 if (this.type == null)
948 throw new ParseError(tr("Unknown primitive type: {0}. Allowed values are node, way or relation", type));
949 }
950
951 @Override
952 public boolean match(OsmPrimitive osm) {
953 return type.equals(osm.getType());
954 }
955
956 @Override
957 public String toString() {
958 return "type=" + type;
959 }
960 }
961
962 /**
963 * Matches objects last changed by the given username.
964 */
965 private static class UserMatch extends Match {
966 private String user;
967
968 UserMatch(String user) {
969 if ("anonymous".equals(user)) {
970 this.user = null;
971 } else {
972 this.user = user;
973 }
974 }
975
976 @Override
977 public boolean match(OsmPrimitive osm) {
978 if (osm.getUser() == null)
979 return user == null;
980 else
981 return osm.getUser().hasName(user);
982 }
983
984 @Override
985 public String toString() {
986 return "user=" + (user == null ? "" : user);
987 }
988 }
989
990 /**
991 * Matches objects with the given relation role (i.e. "outer").
992 */
993 private static class RoleMatch extends Match {
994 private String role;
995
996 RoleMatch(String role) {
997 if (role == null) {
998 this.role = "";
999 } else {
1000 this.role = role;
1001 }
1002 }
1003
1004 @Override
1005 public boolean match(OsmPrimitive osm) {
1006 for (OsmPrimitive ref: osm.getReferrers()) {
1007 if (ref instanceof Relation && !ref.isIncomplete() && !ref.isDeleted()) {
1008 for (RelationMember m : ((Relation) ref).getMembers()) {
1009 if (m.getMember() == osm) {
1010 String testRole = m.getRole();
1011 if (role.equals(testRole == null ? "" : testRole))
1012 return true;
1013 }
1014 }
1015 }
1016 }
1017 return false;
1018 }
1019
1020 @Override
1021 public String toString() {
1022 return "role=" + role;
1023 }
1024 }
1025
1026 /**
1027 * Matches the n-th object of a relation and/or the n-th node of a way.
1028 */
1029 private static class Nth extends Match {
1030
1031 private final int nth;
1032 private final boolean modulo;
1033
1034 Nth(PushbackTokenizer tokenizer, boolean modulo) throws ParseError {
1035 this((int) tokenizer.readNumber(tr("Positive integer expected")), modulo);
1036 }
1037
1038 private Nth(int nth, boolean modulo) {
1039 this.nth = nth;
1040 this.modulo = modulo;
1041 }
1042
1043 @Override
1044 public boolean match(OsmPrimitive osm) {
1045 for (OsmPrimitive p : osm.getReferrers()) {
1046 final int idx;
1047 final int maxIndex;
1048 if (p instanceof Way) {
1049 Way w = (Way) p;
1050 idx = w.getNodes().indexOf(osm);
1051 maxIndex = w.getNodesCount();
1052 } else if (p instanceof Relation) {
1053 Relation r = (Relation) p;
1054 idx = r.getMemberPrimitivesList().indexOf(osm);
1055 maxIndex = r.getMembersCount();
1056 } else {
1057 continue;
1058 }
1059 if (nth < 0 && idx - maxIndex == nth) {
1060 return true;
1061 } else if (idx == nth || (modulo && idx % nth == 0))
1062 return true;
1063 }
1064 return false;
1065 }
1066
1067 @Override
1068 public String toString() {
1069 return "Nth{nth=" + nth + ", modulo=" + modulo + '}';
1070 }
1071 }
1072
1073 /**
1074 * Matches objects with properties in a certain range.
1075 */
1076 private abstract static class RangeMatch extends Match {
1077
1078 private final long min;
1079 private final long max;
1080
1081 RangeMatch(long min, long max) {
1082 this.min = Math.min(min, max);
1083 this.max = Math.max(min, max);
1084 }
1085
1086 RangeMatch(Range range) {
1087 this(range.getStart(), range.getEnd());
1088 }
1089
1090 protected abstract Long getNumber(OsmPrimitive osm);
1091
1092 protected abstract String getString();
1093
1094 @Override
1095 public boolean match(OsmPrimitive osm) {
1096 Long num = getNumber(osm);
1097 if (num == null)
1098 return false;
1099 else
1100 return (num >= min) && (num <= max);
1101 }
1102
1103 @Override
1104 public String toString() {
1105 return getString() + '=' + min + '-' + max;
1106 }
1107 }
1108
1109 /**
1110 * Matches ways with a number of nodes in given range
1111 */
1112 private static class NodeCountRange extends RangeMatch {
1113 NodeCountRange(Range range) {
1114 super(range);
1115 }
1116
1117 NodeCountRange(PushbackTokenizer tokenizer) throws ParseError {
1118 this(tokenizer.readRange(tr("Range of numbers expected")));
1119 }
1120
1121 @Override
1122 protected Long getNumber(OsmPrimitive osm) {
1123 if (osm instanceof Way) {
1124 return (long) ((Way) osm).getRealNodesCount();
1125 } else if (osm instanceof Relation) {
1126 return (long) ((Relation) osm).getMemberPrimitives(Node.class).size();
1127 } else {
1128 return null;
1129 }
1130 }
1131
1132 @Override
1133 protected String getString() {
1134 return "nodes";
1135 }
1136 }
1137
1138 /**
1139 * Matches objects with the number of referring/contained ways in the given range
1140 */
1141 private static class WayCountRange extends RangeMatch {
1142 WayCountRange(Range range) {
1143 super(range);
1144 }
1145
1146 WayCountRange(PushbackTokenizer tokenizer) throws ParseError {
1147 this(tokenizer.readRange(tr("Range of numbers expected")));
1148 }
1149
1150 @Override
1151 protected Long getNumber(OsmPrimitive osm) {
1152 if (osm instanceof Node) {
1153 return (long) Utils.filteredCollection(osm.getReferrers(), Way.class).size();
1154 } else if (osm instanceof Relation) {
1155 return (long) ((Relation) osm).getMemberPrimitives(Way.class).size();
1156 } else {
1157 return null;
1158 }
1159 }
1160
1161 @Override
1162 protected String getString() {
1163 return "ways";
1164 }
1165 }
1166
1167 /**
1168 * Matches objects with a number of tags in given range
1169 */
1170 private static class TagCountRange extends RangeMatch {
1171 TagCountRange(Range range) {
1172 super(range);
1173 }
1174
1175 TagCountRange(PushbackTokenizer tokenizer) throws ParseError {
1176 this(tokenizer.readRange(tr("Range of numbers expected")));
1177 }
1178
1179 @Override
1180 protected Long getNumber(OsmPrimitive osm) {
1181 return (long) osm.getKeys().size();
1182 }
1183
1184 @Override
1185 protected String getString() {
1186 return "tags";
1187 }
1188 }
1189
1190 /**
1191 * Matches objects with a timestamp in given range
1192 */
1193 private static class TimestampRange extends RangeMatch {
1194
1195 TimestampRange(long minCount, long maxCount) {
1196 super(minCount, maxCount);
1197 }
1198
1199 @Override
1200 protected Long getNumber(OsmPrimitive osm) {
1201 return osm.getTimestamp().getTime();
1202 }
1203
1204 @Override
1205 protected String getString() {
1206 return "timestamp";
1207 }
1208 }
1209
1210 /**
1211 * Matches relations with a member of the given role
1212 */
1213 private static class HasRole extends Match {
1214 private final String role;
1215
1216 HasRole(PushbackTokenizer tokenizer) {
1217 role = tokenizer.readTextOrNumber();
1218 }
1219
1220 @Override
1221 public boolean match(OsmPrimitive osm) {
1222 return osm instanceof Relation && ((Relation) osm).getMemberRoles().contains(role);
1223 }
1224 }
1225
1226 /**
1227 * Matches objects that are new (i.e. have not been uploaded to the server)
1228 */
1229 private static class New extends Match {
1230 @Override
1231 public boolean match(OsmPrimitive osm) {
1232 return osm.isNew();
1233 }
1234
1235 @Override
1236 public String toString() {
1237 return "new";
1238 }
1239 }
1240
1241 /**
1242 * Matches all objects that have been modified, created, or undeleted
1243 */
1244 private static class Modified extends Match {
1245 @Override
1246 public boolean match(OsmPrimitive osm) {
1247 return osm.isModified() || osm.isNewOrUndeleted();
1248 }
1249
1250 @Override
1251 public String toString() {
1252 return "modified";
1253 }
1254 }
1255
1256 /**
1257 * Matches all objects that have been deleted
1258 */
1259 private static class Deleted extends Match {
1260 @Override
1261 public boolean match(OsmPrimitive osm) {
1262 return osm.isDeleted();
1263 }
1264
1265 @Override
1266 public String toString() {
1267 return "deleted";
1268 }
1269 }
1270
1271 /**
1272 * Matches all objects currently selected
1273 */
1274 private static class Selected extends Match {
1275 @Override
1276 public boolean match(OsmPrimitive osm) {
1277 return osm.getDataSet().isSelected(osm);
1278 }
1279
1280 @Override
1281 public String toString() {
1282 return "selected";
1283 }
1284 }
1285
1286 /**
1287 * Match objects that are incomplete, where only id and type are known.
1288 * Typically some members of a relation are incomplete until they are
1289 * fetched from the server.
1290 */
1291 private static class Incomplete extends Match {
1292 @Override
1293 public boolean match(OsmPrimitive osm) {
1294 return osm.isIncomplete() || (osm instanceof Relation && ((Relation) osm).hasIncompleteMembers());
1295 }
1296
1297 @Override
1298 public String toString() {
1299 return "incomplete";
1300 }
1301 }
1302
1303 /**
1304 * Matches objects that don't have any interesting tags (i.e. only has source,
1305 * FIXME, etc.). The complete list of uninteresting tags can be found here:
1306 * org.openstreetmap.josm.data.osm.OsmPrimitive.getUninterestingKeys()
1307 */
1308 private static class Untagged extends Match {
1309 @Override
1310 public boolean match(OsmPrimitive osm) {
1311 return !osm.isTagged() && !osm.isIncomplete();
1312 }
1313
1314 @Override
1315 public String toString() {
1316 return "untagged";
1317 }
1318 }
1319
1320 /**
1321 * Matches ways which are closed (i.e. first and last node are the same)
1322 */
1323 private static class Closed extends Match {
1324 @Override
1325 public boolean match(OsmPrimitive osm) {
1326 return osm instanceof Way && ((Way) osm).isClosed();
1327 }
1328
1329 @Override
1330 public String toString() {
1331 return "closed";
1332 }
1333 }
1334
1335 /**
1336 * Matches objects if they are parents of the expression
1337 */
1338 public static class Parent extends UnaryMatch {
1339 public Parent(Match m) {
1340 super(m);
1341 }
1342
1343 @Override
1344 public boolean match(OsmPrimitive osm) {
1345 boolean isParent = false;
1346
1347 if (osm instanceof Way) {
1348 for (Node n : ((Way) osm).getNodes()) {
1349 isParent |= match.match(n);
1350 }
1351 } else if (osm instanceof Relation) {
1352 for (RelationMember member : ((Relation) osm).getMembers()) {
1353 isParent |= match.match(member.getMember());
1354 }
1355 }
1356 return isParent;
1357 }
1358
1359 @Override
1360 public String toString() {
1361 return "parent(" + match + ')';
1362 }
1363 }
1364
1365 /**
1366 * Matches objects if they are children of the expression
1367 */
1368 public static class Child extends UnaryMatch {
1369
1370 public Child(Match m) {
1371 super(m);
1372 }
1373
1374 @Override
1375 public boolean match(OsmPrimitive osm) {
1376 boolean isChild = false;
1377 for (OsmPrimitive p : osm.getReferrers()) {
1378 isChild |= match.match(p);
1379 }
1380 return isChild;
1381 }
1382
1383 @Override
1384 public String toString() {
1385 return "child(" + match + ')';
1386 }
1387 }
1388
1389 /**
1390 * Matches if the size of the area is within the given range
1391 *
1392 * @author Ole Jørgen Brønner
1393 */
1394 private static class AreaSize extends RangeMatch {
1395
1396 AreaSize(Range range) {
1397 super(range);
1398 }
1399
1400 AreaSize(PushbackTokenizer tokenizer) throws ParseError {
1401 this(tokenizer.readRange(tr("Range of numbers expected")));
1402 }
1403
1404 @Override
1405 protected Long getNumber(OsmPrimitive osm) {
1406 final Double area = Geometry.computeArea(osm);
1407 return area == null ? null : area.longValue();
1408 }
1409
1410 @Override
1411 protected String getString() {
1412 return "areasize";
1413 }
1414 }
1415
1416 /**
1417 * Matches if the length of a way is within the given range
1418 */
1419 private static class WayLength extends RangeMatch {
1420
1421 WayLength(Range range) {
1422 super(range);
1423 }
1424
1425 WayLength(PushbackTokenizer tokenizer) throws ParseError {
1426 this(tokenizer.readRange(tr("Range of numbers expected")));
1427 }
1428
1429 @Override
1430 protected Long getNumber(OsmPrimitive osm) {
1431 if (!(osm instanceof Way))
1432 return null;
1433 Way way = (Way) osm;
1434 return (long) way.getLength();
1435 }
1436
1437 @Override
1438 protected String getString() {
1439 return "waylength";
1440 }
1441 }
1442
1443 /**
1444 * Matches objects within the given bounds.
1445 */
1446 private abstract static class InArea extends Match {
1447
1448 protected final boolean all;
1449
1450 /**
1451 * @param all if true, all way nodes or relation members have to be within source area;if false, one suffices.
1452 */
1453 InArea(boolean all) {
1454 this.all = all;
1455 }
1456
1457 protected abstract Collection<Bounds> getBounds(OsmPrimitive primitive);
1458
1459 @Override
1460 public boolean match(OsmPrimitive osm) {
1461 if (!osm.isUsable())
1462 return false;
1463 else if (osm instanceof Node) {
1464 LatLon coordinate = ((Node) osm).getCoor();
1465 Collection<Bounds> allBounds = getBounds(osm);
1466 return coordinate != null && allBounds != null && allBounds.stream().anyMatch(bounds -> bounds.contains(coordinate));
1467 } else if (osm instanceof Way) {
1468 Collection<Node> nodes = ((Way) osm).getNodes();
1469 return all ? nodes.stream().allMatch(this) : nodes.stream().anyMatch(this);
1470 } else if (osm instanceof Relation) {
1471 Collection<OsmPrimitive> primitives = ((Relation) osm).getMemberPrimitivesList();
1472 return all ? primitives.stream().allMatch(this) : primitives.stream().anyMatch(this);
1473 } else
1474 return false;
1475 }
1476 }
1477
1478 /**
1479 * Matches objects within source area ("downloaded area").
1480 */
1481 public static class InDataSourceArea extends InArea {
1482
1483 /**
1484 * Constructs a new {@code InDataSourceArea}.
1485 * @param all if true, all way nodes or relation members have to be within source area; if false, one suffices.
1486 */
1487 public InDataSourceArea(boolean all) {
1488 super(all);
1489 }
1490
1491 @Override
1492 protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
1493 return primitive.getDataSet() != null ? primitive.getDataSet().getDataSourceBounds() : null;
1494 }
1495
1496 @Override
1497 public String toString() {
1498 return all ? "allindownloadedarea" : "indownloadedarea";
1499 }
1500 }
1501
1502 /**
1503 * Matches objects which are not outside the source area ("downloaded area").
1504 * Unlike {@link InDataSourceArea} this matches also if no source area is set (e.g., for new layers).
1505 */
1506 public static class NotOutsideDataSourceArea extends InDataSourceArea {
1507
1508 /**
1509 * Constructs a new {@code NotOutsideDataSourceArea}.
1510 */
1511 public NotOutsideDataSourceArea() {
1512 super(false);
1513 }
1514
1515 @Override
1516 protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
1517 final Collection<Bounds> bounds = super.getBounds(primitive);
1518 return bounds == null || bounds.isEmpty() ? Collections.singleton(Main.getProjection().getWorldBoundsLatLon()) : bounds;
1519 }
1520
1521 @Override
1522 public String toString() {
1523 return "NotOutsideDataSourceArea";
1524 }
1525 }
1526
1527 /**
1528 * Matches objects within current map view.
1529 */
1530 private static class InView extends InArea {
1531
1532 InView(boolean all) {
1533 super(all);
1534 }
1535
1536 @Override
1537 protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
1538 if (!Main.isDisplayingMapView()) {
1539 return null;
1540 }
1541 return Collections.singleton(Main.map.mapView.getRealBounds());
1542 }
1543
1544 @Override
1545 public String toString() {
1546 return all ? "allinview" : "inview";
1547 }
1548 }
1549
1550 public static class ParseError extends Exception {
1551 public ParseError(String msg) {
1552 super(msg);
1553 }
1554
1555 public ParseError(String msg, Throwable cause) {
1556 super(msg, cause);
1557 }
1558
1559 public ParseError(Token expected, Token found) {
1560 this(tr("Unexpected token. Expected {0}, found {1}", expected, found));
1561 }
1562 }
1563
1564 /**
1565 * Compiles the search expression.
1566 * @param searchStr the search expression
1567 * @return a {@link Match} object for the expression
1568 * @throws ParseError if an error has been encountered while compiling
1569 * @see #compile(org.openstreetmap.josm.actions.search.SearchAction.SearchSetting)
1570 */
1571 public static Match compile(String searchStr) throws ParseError {
1572 return new SearchCompiler(false, false,
1573 new PushbackTokenizer(
1574 new PushbackReader(new StringReader(searchStr))))
1575 .parse();
1576 }
1577
1578 /**
1579 * Compiles the search expression.
1580 * @param setting the settings to use
1581 * @return a {@link Match} object for the expression
1582 * @throws ParseError if an error has been encountered while compiling
1583 * @see #compile(String)
1584 */
1585 public static Match compile(SearchAction.SearchSetting setting) throws ParseError {
1586 if (setting.mapCSSSearch) {
1587 return compileMapCSS(setting.text);
1588 }
1589 return new SearchCompiler(setting.caseSensitive, setting.regexSearch,
1590 new PushbackTokenizer(
1591 new PushbackReader(new StringReader(setting.text))))
1592 .parse();
1593 }
1594
1595 static Match compileMapCSS(String mapCSS) throws ParseError {
1596 try {
1597 final List<Selector> selectors = new MapCSSParser(new StringReader(mapCSS)).selectors();
1598 return new Match() {
1599 @Override
1600 public boolean match(OsmPrimitive osm) {
1601 for (Selector selector : selectors) {
1602 if (selector.matches(new Environment(osm))) {
1603 return true;
1604 }
1605 }
1606 return false;
1607 }
1608 };
1609 } catch (ParseException e) {
1610 throw new ParseError(tr("Failed to parse MapCSS selector"), e);
1611 }
1612 }
1613
1614 /**
1615 * Parse search string.
1616 *
1617 * @return match determined by search string
1618 * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError if search expression cannot be parsed
1619 */
1620 public Match parse() throws ParseError {
1621 Match m = Optional.ofNullable(parseExpression()).orElse(Always.INSTANCE);
1622 if (!tokenizer.readIfEqual(Token.EOF))
1623 throw new ParseError(tr("Unexpected token: {0}", tokenizer.nextToken()));
1624 Main.debug("Parsed search expression is {0}", m);
1625 return m;
1626 }
1627
1628 /**
1629 * Parse expression.
1630 *
1631 * @return match determined by parsing expression
1632 * @throws ParseError if search expression cannot be parsed
1633 */
1634 private Match parseExpression() throws ParseError {
1635 // Step 1: parse the whole expression and build a list of factors and logical tokens
1636 List<Object> list = parseExpressionStep1();
1637 // Step 2: iterate the list in reverse order to build the logical expression
1638 // This iterative approach avoids StackOverflowError for long expressions (see #14217)
1639 return parseExpressionStep2(list);
1640 }
1641
1642 private List<Object> parseExpressionStep1() throws ParseError {
1643 Match factor;
1644 String token = null;
1645 String errorMessage = null;
1646 List<Object> list = new ArrayList<>();
1647 do {
1648 factor = parseFactor();
1649 if (factor != null) {
1650 if (token != null) {
1651 list.add(token);
1652 }
1653 list.add(factor);
1654 if (tokenizer.readIfEqual(Token.OR)) {
1655 token = "OR";
1656 errorMessage = tr("Missing parameter for OR");
1657 } else if (tokenizer.readIfEqual(Token.XOR)) {
1658 token = "XOR";
1659 errorMessage = tr("Missing parameter for XOR");
1660 } else {
1661 token = "AND";
1662 errorMessage = null;
1663 }
1664 } else if (errorMessage != null) {
1665 throw new ParseError(errorMessage);
1666 }
1667 } while (factor != null);
1668 return list;
1669 }
1670
1671 private static Match parseExpressionStep2(List<Object> list) {
1672 Match result = null;
1673 for (int i = list.size() - 1; i >= 0; i--) {
1674 Object o = list.get(i);
1675 if (o instanceof Match && result == null) {
1676 result = (Match) o;
1677 } else if (o instanceof String && i > 0) {
1678 Match factor = (Match) list.get(i-1);
1679 switch ((String) o) {
1680 case "OR":
1681 result = new Or(factor, result);
1682 break;
1683 case "XOR":
1684 result = new Xor(factor, result);
1685 break;
1686 case "AND":
1687 result = new And(factor, result);
1688 break;
1689 default: throw new IllegalStateException(tr("Unexpected token: {0}", o));
1690 }
1691 i--;
1692 } else {
1693 throw new IllegalStateException("i=" + i + "; o=" + o);
1694 }
1695 }
1696 return result;
1697 }
1698
1699 /**
1700 * Parse next factor (a search operator or search term).
1701 *
1702 * @return match determined by parsing factor string
1703 * @throws ParseError if search expression cannot be parsed
1704 */
1705 private Match parseFactor() throws ParseError {
1706 if (tokenizer.readIfEqual(Token.LEFT_PARENT)) {
1707 Match expression = parseExpression();
1708 if (!tokenizer.readIfEqual(Token.RIGHT_PARENT))
1709 throw new ParseError(Token.RIGHT_PARENT, tokenizer.nextToken());
1710 return expression;
1711 } else if (tokenizer.readIfEqual(Token.NOT)) {
1712 return new Not(parseFactor(tr("Missing operator for NOT")));
1713 } else if (tokenizer.readIfEqual(Token.KEY)) {
1714 // factor consists of key:value or key=value
1715 String key = tokenizer.getText();
1716 if (tokenizer.readIfEqual(Token.EQUALS)) {
1717 return new ExactKeyValue(regexSearch, key, tokenizer.readTextOrNumber());
1718 } else if (tokenizer.readIfEqual(Token.LESS_THAN)) {
1719 return new ValueComparison(key, tokenizer.readTextOrNumber(), -1);
1720 } else if (tokenizer.readIfEqual(Token.GREATER_THAN)) {
1721 return new ValueComparison(key, tokenizer.readTextOrNumber(), +1);
1722 } else if (tokenizer.readIfEqual(Token.COLON)) {
1723 // see if we have a Match that takes a data parameter
1724 SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
1725 if (factory != null)
1726 return factory.get(key, tokenizer);
1727
1728 UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
1729 if (unaryFactory != null)
1730 return unaryFactory.get(key, parseFactor(), tokenizer);
1731
1732 // key:value form where value is a string (may be OSM key search)
1733 final String value = tokenizer.readTextOrNumber();
1734 return new KeyValue(key, value != null ? value : "", regexSearch, caseSensitive);
1735 } else if (tokenizer.readIfEqual(Token.QUESTION_MARK))
1736 return new BooleanMatch(key, false);
1737 else {
1738 SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
1739 if (factory != null)
1740 return factory.get(key, null);
1741
1742 UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
1743 if (unaryFactory != null)
1744 return unaryFactory.get(key, parseFactor(), null);
1745
1746 // match string in any key or value
1747 return new Any(key, regexSearch, caseSensitive);
1748 }
1749 } else
1750 return null;
1751 }
1752
1753 private Match parseFactor(String errorMessage) throws ParseError {
1754 return Optional.ofNullable(parseFactor()).orElseThrow(() -> new ParseError(errorMessage));
1755 }
1756
1757 private static int regexFlags(boolean caseSensitive) {
1758 int searchFlags = 0;
1759
1760 // Enables canonical Unicode equivalence so that e.g. the two
1761 // forms of "\u00e9gal" and "e\u0301gal" will match.
1762 //
1763 // It makes sense to match no matter how the character
1764 // happened to be constructed.
1765 searchFlags |= Pattern.CANON_EQ;
1766
1767 // Make "." match any character including newline (/s in Perl)
1768 searchFlags |= Pattern.DOTALL;
1769
1770 // CASE_INSENSITIVE by itself only matches US-ASCII case
1771 // insensitively, but the OSM data is in Unicode. With
1772 // UNICODE_CASE casefolding is made Unicode-aware.
1773 if (!caseSensitive) {
1774 searchFlags |= (Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
1775 }
1776
1777 return searchFlags;
1778 }
1779
1780 static String escapeStringForSearch(String s) {
1781 return s.replace("\\", "\\\\").replace("\"", "\\\"");
1782 }
1783
1784 /**
1785 * Builds a search string for the given tag. If value is empty, the existence of the key is checked.
1786 *
1787 * @param key the tag key
1788 * @param value the tag value
1789 * @return a search string for the given tag
1790 */
1791 public static String buildSearchStringForTag(String key, String value) {
1792 final String forKey = '"' + escapeStringForSearch(key) + '"' + '=';
1793 if (value == null || value.isEmpty()) {
1794 return forKey + '*';
1795 } else {
1796 return forKey + '"' + escapeStringForSearch(value) + '"';
1797 }
1798 }
1799}
Note: See TracBrowser for help on using the repository browser.