// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.actions.search; import static org.openstreetmap.josm.tools.I18n.marktr; import static org.openstreetmap.josm.tools.I18n.tr; import java.io.PushbackReader; import java.io.StringReader; import java.text.Normalizer; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.search.PushbackTokenizer.Range; import org.openstreetmap.josm.actions.search.PushbackTokenizer.Token; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.OsmPrimitiveType; import org.openstreetmap.josm.data.osm.OsmUtils; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.RelationMember; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.tools.Geometry; import org.openstreetmap.josm.tools.Predicate; import org.openstreetmap.josm.tools.Utils; import org.openstreetmap.josm.tools.date.DateUtils; /** Implements a google-like search.
Grammar:
expression =
  fact | expression
  fact expression
  fact

fact =
 ( expression )
 -fact
 term?
 term=term
 term:term
 term
 
@author Imi */ public class SearchCompiler { private boolean caseSensitive = false; private boolean regexSearch = false; private static String rxErrorMsg = marktr("The regex \"{0}\" had a parse error at offset {1}, full error:\n\n{2}"); private static String rxErrorMsgNoPos = marktr("The regex \"{0}\" had a parse error, full error:\n\n{1}"); private PushbackTokenizer tokenizer; private static Map simpleMatchFactoryMap = new HashMap<>(); private static Map unaryMatchFactoryMap = new HashMap<>(); private static Map binaryMatchFactoryMap = new HashMap<>(); public SearchCompiler(boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) { this.caseSensitive = caseSensitive; this.regexSearch = regexSearch; this.tokenizer = tokenizer; /* register core match factories at first instance, so plugins should * never be able to generate a NPE */ if (simpleMatchFactoryMap.isEmpty()) { addMatchFactory(new CoreSimpleMatchFactory()); } if (unaryMatchFactoryMap.isEmpty()) { addMatchFactory(new CoreUnaryMatchFactory()); } } /** * Add (register) MatchFactory with SearchCompiler * @param factory */ public static void addMatchFactory(MatchFactory factory) { for (String keyword : factory.getKeywords()) { // TODO: check for keyword collisions if (factory instanceof SimpleMatchFactory) { simpleMatchFactoryMap.put(keyword, (SimpleMatchFactory)factory); } else if (factory instanceof UnaryMatchFactory) { unaryMatchFactoryMap.put(keyword, (UnaryMatchFactory)factory); } else if (factory instanceof BinaryMatchFactory) { binaryMatchFactoryMap.put(keyword, (BinaryMatchFactory)factory); } else throw new AssertionError("Unknown match factory"); } } public class CoreSimpleMatchFactory implements SimpleMatchFactory { private Collection keywords = Arrays.asList("id", "version", "changeset", "nodes", "ways", "tags", "areasize", "waylength", "modified", "selected", "incomplete", "untagged", "closed", "new", "indownloadedarea", "allindownloadedarea", "inview", "allinview", "timestamp", "nth", "nth%"); @Override public Match get(String keyword, PushbackTokenizer tokenizer) throws ParseError { switch(keyword) { case "modified": return new Modified(); case "selected": return new Selected(); case "incomplete": return new Incomplete(); case "untagged": return new Untagged(); case "closed": return new Closed(); case "new": return new New(); case "indownloadedarea": return new InDataSourceArea(false); case "allindownloadedarea": return new InDataSourceArea(true); case "inview": return new InView(false); case "allinview": return new InView(true); default: if (tokenizer != null) { switch (keyword) { case "id": return new Id(tokenizer); case "version": return new Version(tokenizer); case "changeset": return new ChangesetId(tokenizer); case "nodes": return new NodeCountRange(tokenizer); case "ways": return new WayCountRange(tokenizer); case "tags": return new TagCountRange(tokenizer); case "areasize": return new AreaSize(tokenizer); case "waylength": return new WayLength(tokenizer); case "nth": return new Nth(tokenizer, false); case "nth%": return new Nth(tokenizer, true); case "timestamp": // add leading/trailing space in order to get expected split (e.g. "a--" => {"a", ""}) String rangeS = " " + tokenizer.readTextOrNumber() + " "; String[] rangeA = rangeS.split("/"); if (rangeA.length == 1) { return new KeyValue(keyword, rangeS.trim(), regexSearch, caseSensitive); } else if (rangeA.length == 2) { String rangeA1 = rangeA[0].trim(); String rangeA2 = rangeA[1].trim(); // if min timestap is empty: use lowest possible date long minDate = DateUtils.fromString(rangeA1.isEmpty() ? "1980" : rangeA1).getTime(); // if max timestamp is empty: use "now" long maxDate = rangeA2.isEmpty() ? System.currentTimeMillis() : DateUtils.fromString(rangeA2).getTime(); return new TimestampRange(minDate, maxDate); } else { // I18n: Don't translate timestamp keyword throw new ParseError(tr("Expecting min/max after ''timestamp''")); } } } } return null; } @Override public Collection getKeywords() { return keywords; } } public static class CoreUnaryMatchFactory implements UnaryMatchFactory { private static Collection keywords = Arrays.asList("parent", "child"); @Override public UnaryMatch get(String keyword, Match matchOperand, PushbackTokenizer tokenizer) { if ("parent".equals(keyword)) return new Parent(matchOperand); else if ("child".equals(keyword)) return new Child(matchOperand); return null; } @Override public Collection getKeywords() { return keywords; } } /** * Classes implementing this interface can provide Match operators. */ private interface MatchFactory { public Collection getKeywords(); } public interface SimpleMatchFactory extends MatchFactory { public Match get(String keyword, PushbackTokenizer tokenizer) throws ParseError; } public interface UnaryMatchFactory extends MatchFactory { public UnaryMatch get(String keyword, Match matchOperand, PushbackTokenizer tokenizer) throws ParseError; } public interface BinaryMatchFactory extends MatchFactory { public BinaryMatch get(String keyword, Match lhs, Match rhs, PushbackTokenizer tokenizer) throws ParseError; } /** * Base class for all search operators. */ public abstract static class Match implements Predicate { public abstract boolean match(OsmPrimitive osm); /** * Tests whether one of the primitives matches. */ protected boolean existsMatch(Collection primitives) { for (OsmPrimitive p : primitives) { if (match(p)) return true; } return false; } /** * Tests whether all primitives match. */ protected boolean forallMatch(Collection primitives) { for (OsmPrimitive p : primitives) { if (!match(p)) return false; } return true; } @Override public final boolean evaluate(OsmPrimitive object) { return match(object); } } /** * A unary search operator which may take data parameters. */ public abstract static class UnaryMatch extends Match { protected final Match match; public UnaryMatch(Match match) { if (match == null) { // "operator" (null) should mean the same as "operator()" // (Always). I.e. match everything this.match = new Always(); } else { this.match = match; } } public Match getOperand() { return match; } } /** * A binary search operator which may take data parameters. */ public abstract static class BinaryMatch extends Match { protected final Match lhs; protected final Match rhs; public BinaryMatch(Match lhs, Match rhs) { this.lhs = lhs; this.rhs = rhs; } public Match getLhs() { return lhs; } public Match getRhs() { return rhs; } } /** * Matches every OsmPrimitive. */ public static class Always extends Match { /** The unique instance/ */ public static final Always INSTANCE = new Always(); @Override public boolean match(OsmPrimitive osm) { return true; } } /** * Never matches any OsmPrimitive. */ public static class Never extends Match { @Override public boolean match(OsmPrimitive osm) { return false; } } /** * Inverts the match. */ public static class Not extends UnaryMatch { public Not(Match match) {super(match);} @Override public boolean match(OsmPrimitive osm) { return !match.match(osm); } @Override public String toString() {return "!"+match;} public Match getMatch() { return match; } } /** * Matches if the value of the corresponding key is ''yes'', ''true'', ''1'' or ''on''. */ private static class BooleanMatch extends Match { private final String key; private final boolean defaultValue; public BooleanMatch(String key, boolean defaultValue) { this.key = key; this.defaultValue = defaultValue; } @Override public boolean match(OsmPrimitive osm) { Boolean ret = OsmUtils.getOsmBoolean(osm.get(key)); if (ret == null) return defaultValue; else return ret; } } /** * Matches if both left and right expressions match. */ public static class And extends BinaryMatch { public And(Match lhs, Match rhs) {super(lhs, rhs);} @Override public boolean match(OsmPrimitive osm) { return lhs.match(osm) && rhs.match(osm); } @Override public String toString() { return lhs + " && " + rhs; } } /** * Matches if the left OR the right expression match. */ public static class Or extends BinaryMatch { public Or(Match lhs, Match rhs) {super(lhs, rhs);} @Override public boolean match(OsmPrimitive osm) { return lhs.match(osm) || rhs.match(osm); } @Override public String toString() { return lhs + " || " + rhs; } } /** * Matches if the left OR the right expression match, but not both. */ public static class Xor extends BinaryMatch { public Xor(Match lhs, Match rhs) {super(lhs, rhs);} @Override public boolean match(OsmPrimitive osm) { return lhs.match(osm) ^ rhs.match(osm); } @Override public String toString() { return lhs + " ^ " + rhs; } } /** * Matches objects with ID in the given range. */ private static class Id extends RangeMatch { public Id(Range range) {super(range);} public Id(PushbackTokenizer tokenizer) throws ParseError { this(tokenizer.readRange(tr("Range of primitive ids expected"))); } @Override protected Long getNumber(OsmPrimitive osm) { return osm.isNew() ? 0 : osm.getUniqueId(); } @Override protected String getString() { return "id"; } } /** * Matches objects with a changeset ID in the given range. */ private static class ChangesetId extends RangeMatch { public ChangesetId(Range range) {super(range);} public ChangesetId(PushbackTokenizer tokenizer) throws ParseError { this(tokenizer.readRange(tr("Range of changeset ids expected"))); } @Override protected Long getNumber(OsmPrimitive osm) { return (long) osm.getChangesetId(); } @Override protected String getString() { return "changeset"; } } /** * Matches objects with a version number in the given range. */ private static class Version extends RangeMatch { public Version(Range range) {super(range);} public Version(PushbackTokenizer tokenizer) throws ParseError { this(tokenizer.readRange(tr("Range of versions expected"))); } @Override protected Long getNumber(OsmPrimitive osm) { return (long) osm.getVersion(); } @Override protected String getString() { return "version"; } } /** * Matches objects with the given key-value pair. */ private static class KeyValue extends Match { private final String key; private final Pattern keyPattern; private final String value; private final Pattern valuePattern; private final boolean caseSensitive; public KeyValue(String key, String value, boolean regexSearch, boolean caseSensitive) throws ParseError { this.caseSensitive = caseSensitive; if (regexSearch) { int searchFlags = regexFlags(caseSensitive); try { this.keyPattern = Pattern.compile(key, searchFlags); } catch (PatternSyntaxException e) { throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e); } catch (Exception e) { throw new ParseError(tr(rxErrorMsgNoPos, key, e.getMessage()), e); } try { this.valuePattern = Pattern.compile(value, searchFlags); } catch (PatternSyntaxException e) { throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e); } catch (Exception e) { throw new ParseError(tr(rxErrorMsgNoPos, value, e.getMessage()), e); } this.key = key; this.value = value; } else if (caseSensitive) { this.key = key; this.value = value; this.keyPattern = null; this.valuePattern = null; } else { this.key = key.toLowerCase(); this.value = value; this.keyPattern = null; this.valuePattern = null; } } @Override public boolean match(OsmPrimitive osm) { if (keyPattern != null) { if (!osm.hasKeys()) return false; /* The string search will just get a key like * 'highway' and look that up as osm.get(key). But * since we're doing a regex match we'll have to loop * over all the keys to see if they match our regex, * and only then try to match against the value */ for (String k: osm.keySet()) { String v = osm.get(k); Matcher matcherKey = keyPattern.matcher(k); boolean matchedKey = matcherKey.find(); if (matchedKey) { Matcher matcherValue = valuePattern.matcher(v); boolean matchedValue = matcherValue.find(); if (matchedValue) return true; } } } else { String mv = null; if ("timestamp".equals(key)) { mv = DateUtils.fromDate(osm.getTimestamp()); } else { mv = osm.get(key); } if (mv == null) return false; String v1 = caseSensitive ? mv : mv.toLowerCase(); String v2 = caseSensitive ? value : value.toLowerCase(); v1 = Normalizer.normalize(v1, Normalizer.Form.NFC); v2 = Normalizer.normalize(v2, Normalizer.Form.NFC); return v1.indexOf(v2) != -1; } return false; } @Override public String toString() {return key+"="+value;} } public static class ValueComparison extends Match { private final String key; private final String referenceValue; private final int compareMode; public ValueComparison(String key, String referenceValue, int compareMode) { this.key = key; this.referenceValue = referenceValue; this.compareMode = compareMode; } @Override public boolean match(OsmPrimitive osm) { int compareResult; String currentValue = osm.get(key); if (currentValue == null) return false; try { compareResult = Double.compare( Double.parseDouble(currentValue), Double.parseDouble(referenceValue) ); } catch (NumberFormatException ignore) { compareResult = osm.get(key).compareTo(referenceValue); } return compareMode < 0 ? compareResult < 0 : compareMode > 0 ? compareResult > 0 : compareResult == 0; } } /** * Matches objects with the exact given key-value pair. */ public static class ExactKeyValue extends Match { private enum Mode { ANY, ANY_KEY, ANY_VALUE, EXACT, NONE, MISSING_KEY, ANY_KEY_REGEXP, ANY_VALUE_REGEXP, EXACT_REGEXP, MISSING_KEY_REGEXP; } private final String key; private final String value; private final Pattern keyPattern; private final Pattern valuePattern; private final Mode mode; public ExactKeyValue(boolean regexp, String key, String value) throws ParseError { if ("".equals(key)) throw new ParseError(tr("Key cannot be empty when tag operator is used. Sample use: key=value")); this.key = key; this.value = value == null?"":value; if ("".equals(this.value) && "*".equals(key)) { mode = Mode.NONE; } else if ("".equals(this.value)) { if (regexp) { mode = Mode.MISSING_KEY_REGEXP; } else { mode = Mode.MISSING_KEY; } } else if ("*".equals(key) && "*".equals(this.value)) { mode = Mode.ANY; } else if ("*".equals(key)) { if (regexp) { mode = Mode.ANY_KEY_REGEXP; } else { mode = Mode.ANY_KEY; } } else if ("*".equals(this.value)) { if (regexp) { mode = Mode.ANY_VALUE_REGEXP; } else { mode = Mode.ANY_VALUE; } } else { if (regexp) { mode = Mode.EXACT_REGEXP; } else { mode = Mode.EXACT; } } if (regexp && key.length() > 0 && !"*".equals(key)) { try { keyPattern = Pattern.compile(key, regexFlags(false)); } catch (PatternSyntaxException e) { throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage())); } catch (Exception e) { throw new ParseError(tr(rxErrorMsgNoPos, key, e.getMessage())); } } else { keyPattern = null; } if (regexp && this.value.length() > 0 && !"*".equals(this.value)) { try { valuePattern = Pattern.compile(this.value, regexFlags(false)); } catch (PatternSyntaxException e) { throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage())); } catch (Exception e) { throw new ParseError(tr(rxErrorMsgNoPos, value, e.getMessage())); } } else { valuePattern = null; } } @Override public boolean match(OsmPrimitive osm) { if (!osm.hasKeys()) return mode == Mode.NONE; switch (mode) { case NONE: return false; case MISSING_KEY: return osm.get(key) == null; case ANY: return true; case ANY_VALUE: return osm.get(key) != null; case ANY_KEY: for (String v:osm.getKeys().values()) { if (v.equals(value)) return true; } return false; case EXACT: return value.equals(osm.get(key)); case ANY_KEY_REGEXP: for (String v:osm.getKeys().values()) { if (valuePattern.matcher(v).matches()) return true; } return false; case ANY_VALUE_REGEXP: case EXACT_REGEXP: for (String key: osm.keySet()) { if (keyPattern.matcher(key).matches()) { if (mode == Mode.ANY_VALUE_REGEXP || valuePattern.matcher(osm.get(key)).matches()) return true; } } return false; case MISSING_KEY_REGEXP: for (String k:osm.keySet()) { if (keyPattern.matcher(k).matches()) return false; } return true; } throw new AssertionError("Missed state"); } @Override public String toString() { return key + '=' + value; } } /** * Match a string in any tags (key or value), with optional regex and case insensitivity. */ private static class Any extends Match { private final String search; private final Pattern searchRegex; private final boolean caseSensitive; public Any(String s, boolean regexSearch, boolean caseSensitive) throws ParseError { s = Normalizer.normalize(s, Normalizer.Form.NFC); this.caseSensitive = caseSensitive; if (regexSearch) { try { this.searchRegex = Pattern.compile(s, regexFlags(caseSensitive)); } catch (PatternSyntaxException e) { throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e); } catch (Exception e) { throw new ParseError(tr(rxErrorMsgNoPos, s, e.getMessage()), e); } this.search = s; } else if (caseSensitive) { this.search = s; this.searchRegex = null; } else { this.search = s.toLowerCase(); this.searchRegex = null; } } @Override public boolean match(OsmPrimitive osm) { if (!osm.hasKeys() && osm.getUser() == null) return search.isEmpty(); for (String key: osm.keySet()) { String value = osm.get(key); if (searchRegex != null) { value = Normalizer.normalize(value, Normalizer.Form.NFC); Matcher keyMatcher = searchRegex.matcher(key); Matcher valMatcher = searchRegex.matcher(value); boolean keyMatchFound = keyMatcher.find(); boolean valMatchFound = valMatcher.find(); if (keyMatchFound || valMatchFound) return true; } else { if (!caseSensitive) { key = key.toLowerCase(); value = value.toLowerCase(); } value = Normalizer.normalize(value, Normalizer.Form.NFC); if (key.indexOf(search) != -1 || value.indexOf(search) != -1) return true; } } return false; } @Override public String toString() { return search; } } private static class ExactType extends Match { private final OsmPrimitiveType type; public ExactType(String type) throws ParseError { this.type = OsmPrimitiveType.from(type); if (this.type == null) throw new ParseError(tr("Unknown primitive type: {0}. Allowed values are node, way or relation", type)); } @Override public boolean match(OsmPrimitive osm) { return type.equals(osm.getType()); } @Override public String toString() {return "type="+type;} } /** * Matches objects last changed by the given username. */ private static class UserMatch extends Match { private String user; public UserMatch(String user) { if ("anonymous".equals(user)) { this.user = null; } else { this.user = user; } } @Override public boolean match(OsmPrimitive osm) { if (osm.getUser() == null) return user == null; else return osm.getUser().hasName(user); } @Override public String toString() { return "user=" + (user == null ? "" : user); } } /** * Matches objects with the given relation role (i.e. "outer"). */ private static class RoleMatch extends Match { private String role; public RoleMatch(String role) { if (role == null) { this.role = ""; } else { this.role = role; } } @Override public boolean match(OsmPrimitive osm) { for (OsmPrimitive ref: osm.getReferrers()) { if (ref instanceof Relation && !ref.isIncomplete() && !ref.isDeleted()) { for (RelationMember m : ((Relation) ref).getMembers()) { if (m.getMember() == osm) { String testRole = m.getRole(); if(role.equals(testRole == null ? "" : testRole)) return true; } } } } return false; } @Override public String toString() { return "role=" + role; } } /** * Matches the n-th object of a relation and/or the n-th node of a way. */ private static class Nth extends Match { private final int nth; private final boolean modulo; public Nth(PushbackTokenizer tokenizer, boolean modulo) throws ParseError { this((int) tokenizer.readNumber(tr("Positive integer expected")), modulo); } private Nth(int nth, boolean modulo) throws ParseError { if (nth <= 0) { throw new ParseError(tr("Positive integer expected")); } this.nth = nth; this.modulo = modulo; } @Override public boolean match(OsmPrimitive osm) { for (OsmPrimitive p : osm.getReferrers()) { Integer idx = null; if (p instanceof Way) { Way w = (Way) p; idx = w.getNodes().indexOf(osm); } else if (p instanceof Relation) { Relation r = (Relation) p; idx = r.getMemberPrimitivesList().indexOf(osm); } if (idx != null) { if (idx.intValue() == nth || (modulo && idx.intValue() % nth == 0)) { return true; } } } return false; } } /** * Matches objects with properties in a certain range. */ private abstract static class RangeMatch extends Match { private final long min; private final long max; public RangeMatch(long min, long max) { this.min = Math.min(min, max); this.max = Math.max(min, max); } public RangeMatch(Range range) { this(range.getStart(), range.getEnd()); } protected abstract Long getNumber(OsmPrimitive osm); protected abstract String getString(); @Override public boolean match(OsmPrimitive osm) { Long num = getNumber(osm); if (num == null) return false; else return (num >= min) && (num <= max); } @Override public String toString() { return getString() + "=" + min + "-" + max; } } /** * Matches ways with a number of nodes in given range */ private static class NodeCountRange extends RangeMatch { public NodeCountRange(Range range) { super(range); } public NodeCountRange(PushbackTokenizer tokenizer) throws ParseError { this(tokenizer.readRange(tr("Range of numbers expected"))); } @Override protected Long getNumber(OsmPrimitive osm) { if (osm instanceof Way) { return (long) ((Way) osm).getRealNodesCount(); } else if (osm instanceof Relation) { return (long) ((Relation) osm).getMemberPrimitives(Node.class).size(); } else { return null; } } @Override protected String getString() { return "nodes"; } } /** * Matches objects with the number of referring/contained ways in the given range */ private static class WayCountRange extends RangeMatch { public WayCountRange(Range range) { super(range); } public WayCountRange(PushbackTokenizer tokenizer) throws ParseError { this(tokenizer.readRange(tr("Range of numbers expected"))); } @Override protected Long getNumber(OsmPrimitive osm) { if (osm instanceof Node) { return (long) Utils.filteredCollection(osm.getReferrers(), Way.class).size(); } else if (osm instanceof Relation) { return (long) ((Relation) osm).getMemberPrimitives(Way.class).size(); } else { return null; } } @Override protected String getString() { return "ways"; } } /** * Matches objects with a number of tags in given range */ private static class TagCountRange extends RangeMatch { public TagCountRange(Range range) { super(range); } public TagCountRange(PushbackTokenizer tokenizer) throws ParseError { this(tokenizer.readRange(tr("Range of numbers expected"))); } @Override protected Long getNumber(OsmPrimitive osm) { return (long) osm.getKeys().size(); } @Override protected String getString() { return "tags"; } } /** * Matches objects with a timestamp in given range */ private static class TimestampRange extends RangeMatch { public TimestampRange(long minCount, long maxCount) { super(minCount, maxCount); } @Override protected Long getNumber(OsmPrimitive osm) { return osm.getTimestamp().getTime(); } @Override protected String getString() { return "timestamp"; } } /** * Matches objects that are new (i.e. have not been uploaded to the server) */ private static class New extends Match { @Override public boolean match(OsmPrimitive osm) { return osm.isNew(); } @Override public String toString() { return "new"; } } /** * Matches all objects that have been modified, created, or undeleted */ private static class Modified extends Match { @Override public boolean match(OsmPrimitive osm) { return osm.isModified() || osm.isNewOrUndeleted(); } @Override public String toString() {return "modified";} } /** * Matches all objects currently selected */ private static class Selected extends Match { @Override public boolean match(OsmPrimitive osm) { return Main.main.getCurrentDataSet().isSelected(osm); } @Override public String toString() {return "selected";} } /** * Match objects that are incomplete, where only id and type are known. * Typically some members of a relation are incomplete until they are * fetched from the server. */ private static class Incomplete extends Match { @Override public boolean match(OsmPrimitive osm) { return osm.isIncomplete(); } @Override public String toString() {return "incomplete";} } /** * Matches objects that don't have any interesting tags (i.e. only has source, * FIXME, etc.). The complete list of uninteresting tags can be found here: * org.openstreetmap.josm.data.osm.OsmPrimitive.getUninterestingKeys() */ private static class Untagged extends Match { @Override public boolean match(OsmPrimitive osm) { return !osm.isTagged() && !osm.isIncomplete(); } @Override public String toString() {return "untagged";} } /** * Matches ways which are closed (i.e. first and last node are the same) */ private static class Closed extends Match { @Override public boolean match(OsmPrimitive osm) { return osm instanceof Way && ((Way) osm).isClosed(); } @Override public String toString() {return "closed";} } /** * Matches objects if they are parents of the expression */ public static class Parent extends UnaryMatch { public Parent(Match m) { super(m); } @Override public boolean match(OsmPrimitive osm) { boolean isParent = false; if (osm instanceof Way) { for (Node n : ((Way)osm).getNodes()) { isParent |= match.match(n); } } else if (osm instanceof Relation) { for (RelationMember member : ((Relation)osm).getMembers()) { isParent |= match.match(member.getMember()); } } return isParent; } @Override public String toString() {return "parent(" + match + ")";} } /** * Matches objects if they are children of the expression */ public static class Child extends UnaryMatch { public Child(Match m) { super(m); } @Override public boolean match(OsmPrimitive osm) { boolean isChild = false; for (OsmPrimitive p : osm.getReferrers()) { isChild |= match.match(p); } return isChild; } @Override public String toString() {return "child(" + match + ")";} } /** * Matches if the size of the area is within the given range * * @author Ole Jørgen Brønner */ private static class AreaSize extends RangeMatch { public AreaSize(Range range) { super(range); } public AreaSize(PushbackTokenizer tokenizer) throws ParseError { this(tokenizer.readRange(tr("Range of numbers expected"))); } @Override protected Long getNumber(OsmPrimitive osm) { if (!(osm instanceof Way && ((Way) osm).isClosed())) return null; Way way = (Way) osm; return (long) Geometry.closedWayArea(way); } @Override protected String getString() { return "areasize"; } } /** * Matches if the length of a way is within the given range */ private static class WayLength extends RangeMatch { public WayLength(Range range) { super(range); } public WayLength(PushbackTokenizer tokenizer) throws ParseError { this(tokenizer.readRange(tr("Range of numbers expected"))); } @Override protected Long getNumber(OsmPrimitive osm) { if (!(osm instanceof Way)) return null; Way way = (Way) osm; return (long) way.getLength(); } @Override protected String getString() { return "waylength"; } } /** * Matches objects within the given bounds. */ private abstract static class InArea extends Match { protected abstract Bounds getBounds(); protected final boolean all; /** * @param all if true, all way nodes or relation members have to be within source area;if false, one suffices. */ public InArea(boolean all) { this.all = all; } @Override public boolean match(OsmPrimitive osm) { if (!osm.isUsable()) return false; else if (osm instanceof Node) { Bounds bounds = getBounds(); return bounds != null && bounds.contains(((Node) osm).getCoor()); } else if (osm instanceof Way) { Collection nodes = ((Way) osm).getNodes(); return all ? forallMatch(nodes) : existsMatch(nodes); } else if (osm instanceof Relation) { Collection primitives = ((Relation) osm).getMemberPrimitives(); return all ? forallMatch(primitives) : existsMatch(primitives); } else return false; } } /** * Matches objects within source area ("downloaded area"). */ private static class InDataSourceArea extends InArea { public InDataSourceArea(boolean all) { super(all); } @Override protected Bounds getBounds() { return new Bounds(Main.main.getCurrentDataSet().getDataSourceArea().getBounds2D()); } } /** * Matches objects within current map view. */ private static class InView extends InArea { public InView(boolean all) { super(all); } @Override protected Bounds getBounds() { if (!Main.isDisplayingMapView()) { return null; } return Main.map.mapView.getRealBounds(); } } public static class ParseError extends Exception { public ParseError(String msg) { super(msg); } public ParseError(String msg, Throwable cause) { super(msg, cause); } public ParseError(Token expected, Token found) { this(tr("Unexpected token. Expected {0}, found {1}", expected, found)); } } public static Match compile(String searchStr, boolean caseSensitive, boolean regexSearch) throws ParseError { return new SearchCompiler(caseSensitive, regexSearch, new PushbackTokenizer( new PushbackReader(new StringReader(searchStr)))) .parse(); } /** * Parse search string. * * @return match determined by search string * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError */ public Match parse() throws ParseError { Match m = parseExpression(); if (!tokenizer.readIfEqual(Token.EOF)) throw new ParseError(tr("Unexpected token: {0}", tokenizer.nextToken())); if (m == null) return new Always(); return m; } /** * Parse expression. This is a recursive method. * * @return match determined by parsing expression * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError */ private Match parseExpression() throws ParseError { Match factor = parseFactor(); if (factor == null) // empty search string return null; if (tokenizer.readIfEqual(Token.OR)) return new Or(factor, parseExpression(tr("Missing parameter for OR"))); else if (tokenizer.readIfEqual(Token.XOR)) return new Xor(factor, parseExpression(tr("Missing parameter for XOR"))); else { Match expression = parseExpression(); if (expression == null) // reached end of search string, no more recursive calls return factor; else // the default operator is AND return new And(factor, expression); } } /** * Parse expression, showing the specified error message if parsing fails. * * @param errorMessage to display if parsing error occurs * @return match determined by parsing expression * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError * @see #parseExpression() */ private Match parseExpression(String errorMessage) throws ParseError { Match expression = parseExpression(); if (expression == null) throw new ParseError(errorMessage); else return expression; } /** * Parse next factor (a search operator or search term). * * @return match determined by parsing factor string * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError */ private Match parseFactor() throws ParseError { if (tokenizer.readIfEqual(Token.LEFT_PARENT)) { Match expression = parseExpression(); if (!tokenizer.readIfEqual(Token.RIGHT_PARENT)) throw new ParseError(Token.RIGHT_PARENT, tokenizer.nextToken()); return expression; } else if (tokenizer.readIfEqual(Token.NOT)) { return new Not(parseFactor(tr("Missing operator for NOT"))); } else if (tokenizer.readIfEqual(Token.KEY)) { // factor consists of key:value or key=value String key = tokenizer.getText(); if (tokenizer.readIfEqual(Token.EQUALS)) { return new ExactKeyValue(regexSearch, key, tokenizer.readTextOrNumber()); } else if (tokenizer.readIfEqual(Token.LESS_THAN)) { return new ValueComparison(key, tokenizer.readTextOrNumber(), -1); } else if (tokenizer.readIfEqual(Token.GREATER_THAN)) { return new ValueComparison(key, tokenizer.readTextOrNumber(), +1); } else if (tokenizer.readIfEqual(Token.COLON)) { // see if we have a Match that takes a data parameter SimpleMatchFactory factory = simpleMatchFactoryMap.get(key); if (factory != null) return factory.get(key, tokenizer); UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key); if (unaryFactory != null) return unaryFactory.get(key, parseFactor(), tokenizer); // key:value form where value is a string (may be OSM key search) return parseKV(key, tokenizer.readTextOrNumber()); } else if (tokenizer.readIfEqual(Token.QUESTION_MARK)) return new BooleanMatch(key, false); else { SimpleMatchFactory factory = simpleMatchFactoryMap.get(key); if (factory != null) return factory.get(key, null); UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key); if (unaryFactory != null) return unaryFactory.get(key, parseFactor(), null); // match string in any key or value return new Any(key, regexSearch, caseSensitive); } } else return null; } private Match parseFactor(String errorMessage) throws ParseError { Match fact = parseFactor(); if (fact == null) throw new ParseError(errorMessage); else return fact; } private Match parseKV(String key, String value) throws ParseError { if (value == null) { value = ""; } switch(key) { case "type": return new ExactType(value); case "user": return new UserMatch(value); case "role": return new RoleMatch(value); default: return new KeyValue(key, value, regexSearch, caseSensitive); } } private static int regexFlags(boolean caseSensitive) { int searchFlags = 0; // Enables canonical Unicode equivalence so that e.g. the two // forms of "\u00e9gal" and "e\u0301gal" will match. // // It makes sense to match no matter how the character // happened to be constructed. searchFlags |= Pattern.CANON_EQ; // Make "." match any character including newline (/s in Perl) searchFlags |= Pattern.DOTALL; // CASE_INSENSITIVE by itself only matches US-ASCII case // insensitively, but the OSM data is in Unicode. With // UNICODE_CASE casefolding is made Unicode-aware. if (!caseSensitive) { searchFlags |= (Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); } return searchFlags; } }