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

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

sonar - Performance - Method passes constant String of length 1 to character overridden method + add unit tests/javadoc

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