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

Last change on this file since 11288 was 11288, checked in by simon04, 7 years ago

see #13376 - Use TimeUnit instead of combinations of 1000/60/60/24

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