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

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

sonar - squid:S1854 - Dead stores should be removed

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