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

Last change on this file since 9397 was 9397, checked in by simon04, 8 years ago

see #12325 - Do not exclude the test for non-downloaded datasets, fix unit test

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