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

Last change on this file since 11446 was 11446, checked in by stoecker, 7 years ago

add search option to find deleted objects (e.g. to purge them)

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