Ticket #7178: factory_plugin_search_operators.patch

File factory_plugin_search_operators.patch, 35.8 KB (added by joshdoe, 9 years ago)

add ability for plugins to register search match operators using a factory pattern

  • src/org/openstreetmap/josm/actions/search/PushbackTokenizer.java

     
    4747    }
    4848
    4949    public enum Token {
    50         NOT(marktr("<not>")), OR(marktr("<or>")), LEFT_PARENT(marktr("<left parent>")),
     50        NOT(marktr("<not>")), OR(marktr("<or>")), XOR(marktr("<xor>")), LEFT_PARENT(marktr("<left parent>")),
    5151        RIGHT_PARENT(marktr("<right parent>")), COLON(marktr("<colon>")), EQUALS(marktr("<equals>")),
    5252        KEY(marktr("<key>")), QUESTION_MARK(marktr("<question mark>")),
    5353        EOF(marktr("<end-of-file>"));
     
    7373        }
    7474    }
    7575
    76     private static final List<Character> specialChars = Arrays.asList(new Character[] {'"', ':', '(', ')', '|', '=', '?'});
     76    private static final List<Character> specialChars = Arrays.asList(new Character[] {'"', ':', '(', ')', '|', '^', '=', '?'});
    7777    private static final List<Character> specialCharsQuoted = Arrays.asList(new Character[] {'"'});
    7878
    7979    private String getString(boolean quoted) {
     
    101101     * - for an '-'. This will be the only character
    102102     * : for an key. The value is the next token
    103103     * | for "OR"
     104     * ^ for "XOR"
    104105     * ' ' for anything else.
    105106     * @return The next token in the stream.
    106107     */
     
    133134        case '|':
    134135            getChar();
    135136            return Token.OR;
     137        case '^':
     138            getChar();
     139            return Token.XOR;
    136140        case '&':
    137141            getChar();
    138142            return nextToken();
     
    155159            currentText = prefix + getString();
    156160            if ("or".equalsIgnoreCase(currentText))
    157161                return Token.OR;
    158             if ("and".equalsIgnoreCase(currentText))
     162            else if ("xor".equalsIgnoreCase(currentText))
     163                return Token.XOR;
     164            else if ("and".equalsIgnoreCase(currentText))
    159165                return nextToken();
    160166            // try parsing number
    161167            try {
  • src/org/openstreetmap/josm/actions/search/SearchCompiler.java

     
    99import java.text.Normalizer;
    1010import java.util.Collection;
    1111import java.util.Date;
     12import java.util.Vector;
    1213import java.util.regex.Matcher;
    1314import java.util.regex.Pattern;
    1415import java.util.regex.PatternSyntaxException;
     
    5455    private static String  rxErrorMsg = marktr("The regex \"{0}\" had a parse error at offset {1}, full error:\n\n{2}");
    5556    private static String  rxErrorMsgNoPos = marktr("The regex \"{0}\" had a parse error, full error:\n\n{1}");
    5657    private PushbackTokenizer tokenizer;
     58    private static MasterMatchFactory matchFactory = null;
    5759
    5860    public SearchCompiler(boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) {
    5961        this.caseSensitive = caseSensitive;
    6062        this.regexSearch = regexSearch;
    6163        this.tokenizer = tokenizer;
     64
     65        /* register core match factory at first instance, so plugins should
     66         * never be able to generate a NPE
     67         */
     68        if (matchFactory == null) {
     69                matchFactory = new MasterMatchFactory();
     70                matchFactory.add(new CoreMatchFactory());
     71        }
     72
    6273    }
     74   
     75    /**
     76     * Add (register) MatchFactory with SearchCompiler
     77     * @param factory
     78     */
     79    public static void addMatchFactory(MatchFactory factory) {
     80        matchFactory.add(factory);
     81    }
     82   
     83    public static class MasterMatchFactory implements MatchFactory {
    6384
     85        private static Collection<MatchFactory> matchFactories = new Vector<MatchFactory>();
     86
     87        public void add(MatchFactory factory) {
     88            matchFactories.add(factory);
     89        }
     90
     91        @Override
     92        public Match getSimpleMatch(String keyword, PushbackTokenizer tokenizer) throws ParseError {
     93            for (MatchFactory factory : matchFactories) {
     94                Match match = factory.getSimpleMatch(keyword, tokenizer);
     95                if (match != null) {
     96                    return match;
     97                }
     98            }
     99            return null;
     100        }
     101
     102        @Override
     103        public UnaryMatch getUnaryMatch(String keyword, Match matchOperand, PushbackTokenizer tokenizer) throws ParseError {
     104            for (MatchFactory factory : matchFactories) {
     105                UnaryMatch match = factory.getUnaryMatch(keyword, matchOperand, tokenizer);
     106                if (match != null) {
     107                    return match;
     108                }
     109            }
     110            return null;
     111        }
     112
     113        @Override
     114        public BinaryMatch getBinaryMatch(String keyword, Match lhs, Match rhs, PushbackTokenizer tokenizer) throws ParseError {
     115            for (MatchFactory factory : matchFactories) {
     116                BinaryMatch match = factory.getBinaryMatch(keyword, lhs, rhs, tokenizer);
     117                if (match != null) {
     118                    return match;
     119                }
     120            }
     121            return null;
     122        }
     123
     124        @Override
     125        public Collection<String> getKeywords() {
     126            throw new UnsupportedOperationException("Not supported yet.");
     127        }
     128
     129        @Override
     130        public boolean isUnaryMatch(String keyword) {
     131            for (MatchFactory factory : matchFactories) {
     132                if (factory.isUnaryMatch(keyword)) {
     133                    return true;
     134                }
     135            }
     136            return false;
     137        }
     138
     139        @Override
     140        public boolean isBinaryMatch(String keyword) {
     141            for (MatchFactory factory : matchFactories) {
     142                if (factory.isBinaryMatch(keyword)) {
     143                    return true;
     144                }
     145            }
     146            return false;
     147        }
     148    }
     149
     150    public class CoreMatchFactory implements MatchFactory {
     151
     152        @Override
     153        public Match getSimpleMatch(String keyword, PushbackTokenizer tokenizer) throws ParseError {
     154            if ("id".equals(keyword)) {
     155                return new Id(tokenizer);
     156            } else if ("version".equals(keyword)) {
     157                return new Version(tokenizer);
     158            } else if ("changeset".equals(keyword)) {
     159                return new ChangesetId(tokenizer);
     160            } else if ("nodes".equals(keyword)) {
     161                return new NodeCountRange(tokenizer);
     162            } else if ("tags".equals(keyword)) {
     163                return new TagCountRange(tokenizer);
     164            } else if ("areasize".equals(keyword)) {
     165                return new AreaSize(tokenizer);
     166            } else if ("modified".equals(keyword)) {
     167                return new Modified();
     168            } else if ("selected".equals(keyword)) {
     169                return new Selected();
     170            } else if ("incomplete".equals(keyword)) {
     171                return new Incomplete();
     172            } else if ("untagged".equals(keyword)) {
     173                return new Untagged();
     174            } else if ("closed".equals(keyword)) {
     175                return new Closed();
     176            } else if ("new".equals(keyword)) {
     177                return new New();
     178            } else if ("indownloadedarea".equals(keyword)) {
     179                return new InDataSourceArea(false);
     180            } else if ("allindownloadedarea".equals(keyword)) {
     181                return new InDataSourceArea(true);
     182            } else if ("inview".equals(keyword)) {
     183                return new InView(false);
     184            } else if ("allinview".equals(keyword)) {
     185                return new InView(true);
     186            } else if ("timestamp".equals(keyword)) {
     187                // TODO: how to handle this? symptom of inadequate search grammar?
     188                String rangeS = " " + tokenizer.readTextOrNumber() + " "; // add leading/trailing space in order to get expected split (e.g. "a--" => {"a", ""})
     189                String[] rangeA = rangeS.split("/");
     190                if (rangeA.length == 1) {
     191                    return new KeyValue(keyword, rangeS, regexSearch, caseSensitive);
     192                } else if (rangeA.length == 2) {
     193                    String rangeA1 = rangeA[0].trim();
     194                    String rangeA2 = rangeA[1].trim();
     195                    long minDate = DateUtils.fromString(rangeA1.isEmpty() ? "1980" : rangeA1).getTime(); // if min timestap is empty: use lowest possible date
     196                    long maxDate = rangeA2.isEmpty() ? new Date().getTime() : DateUtils.fromString(rangeA2).getTime(); // if max timestamp is empty: use "now"
     197                    return new TimestampRange(minDate, maxDate);
     198                } else {
     199                    /*
     200                     * I18n: Don't translate timestamp keyword
     201                     */ throw new ParseError(tr("Expecting <i>min</i>/<i>max</i> after ''timestamp''"));
     202                }
     203            } else {
     204                return null;
     205            }
     206        }
     207
     208        @Override
     209        public UnaryMatch getUnaryMatch(String keyword, Match matchOperand, PushbackTokenizer tokenizer) {
     210            if ("parent".equals(keyword)) {
     211                return new Parent(matchOperand);
     212            } else if ("child".equals(keyword)) {
     213                return new Child(matchOperand);
     214            }
     215            return null;
     216        }
     217
     218        @Override
     219        public BinaryMatch getBinaryMatch(String keyword, Match lhs, Match rhs, PushbackTokenizer tokenizer) {
     220            throw new UnsupportedOperationException("Not supported yet.");
     221        }
     222
     223        @Override
     224        public Collection<String> getKeywords() {
     225            throw new UnsupportedOperationException("Not supported yet.");
     226        }
     227
     228        @Override
     229        public boolean isUnaryMatch(String keyword) {
     230            if ("parent".equals(keyword) || "child".equals(keyword)) {
     231                return true;
     232            } else {
     233                return false;
     234            }
     235        }
     236
     237        @Override
     238        public boolean isBinaryMatch(String keyword) {
     239            return false;
     240        }
     241    }
     242
     243    /**
     244     * Classes implementing this interface can provide Match operators.
     245     */
     246    public interface MatchFactory {
     247
     248        public Match getSimpleMatch(String keyword, PushbackTokenizer tokenizer) throws ParseError;
     249
     250        public UnaryMatch getUnaryMatch(String keyword, Match matchOperand, PushbackTokenizer tokenizer) throws ParseError;
     251
     252        public BinaryMatch getBinaryMatch(String keyword, Match lhs, Match rhs, PushbackTokenizer tokenizer) throws ParseError;
     253
     254        public Collection<String> getKeywords();
     255
     256        public boolean isUnaryMatch(String keyword);
     257
     258        public boolean isBinaryMatch(String keyword);
     259    }
     260
     261    /**
     262     * Base class for all search operators.
     263     */
    64264    abstract public static class Match {
     265
    65266        abstract public boolean match(OsmPrimitive osm);
    66267
    67268        /**
     
    69270         */
    70271        protected boolean existsMatch(Collection<? extends OsmPrimitive> primitives) {
    71272            for (OsmPrimitive p : primitives) {
    72                 if (match(p))
     273                if (match(p)) {
    73274                    return true;
     275                }
    74276            }
    75277            return false;
    76278        }
     
    80282         */
    81283        protected boolean forallMatch(Collection<? extends OsmPrimitive> primitives) {
    82284            for (OsmPrimitive p : primitives) {
    83                 if (!match(p))
     285                if (!match(p)) {
    84286                    return false;
     287                }
    85288            }
    86289            return true;
    87290        }
    88291    }
    89292
     293    /**
     294     * A unary search operator which may take data parameters.
     295     */
     296    abstract public static class UnaryMatch extends Match {
     297
     298        protected final Match match;
     299
     300        public UnaryMatch(Match match) {
     301            if (match == null) {
     302                // "operator" (null) should mean the same as "operator()"
     303                // (Always). I.e. match everything
     304                this.match = new Always();
     305            } else {
     306                this.match = match;
     307            }
     308        }
     309
     310        public Match getOperand() {
     311            return match;
     312        }
     313    }
     314
     315    /**
     316     * A binary search operator which may take data parameters.
     317     */
     318    abstract public static class BinaryMatch extends Match {
     319
     320        protected final Match lhs;
     321        protected final Match rhs;
     322
     323        public BinaryMatch(Match lhs, Match rhs) {
     324            this.lhs = lhs;
     325            this.rhs = rhs;
     326        }
     327
     328        public Match getLhs() {
     329            return lhs;
     330        }
     331
     332        public Match getRhs() {
     333            return rhs;
     334        }
     335    }
     336
     337    /**
     338     * Matches every OsmPrimitive.
     339     */
    90340    public static class Always extends Match {
    91341        public static Always INSTANCE = new Always();
    92342        @Override public boolean match(OsmPrimitive osm) {
     
    94344        }
    95345    }
    96346
     347    /**
     348     * Never matches any OsmPrimitive.
     349     */
    97350    public static class Never extends Match {
    98351        @Override
    99352        public boolean match(OsmPrimitive osm) {
     
    101354        }
    102355    }
    103356
    104     public static class Not extends Match {
    105         private final Match match;
    106         public Not(Match match) {this.match = match;}
     357    /**
     358     * Inverts the match.
     359     */
     360    public static class Not extends UnaryMatch {
     361        public Not(Match match) {super(match);}
    107362        @Override public boolean match(OsmPrimitive osm) {
    108363            return !match.match(osm);
    109364        }
     
    113368        }
    114369    }
    115370
     371    /**
     372     * Matches if the value of the corresponding key is ''yes'', ''true'', ''1'' or ''on''.
     373     */
    116374    private static class BooleanMatch extends Match {
    117375        private final String key;
    118376        private final boolean defaultValue;
     
    131389        }
    132390    }
    133391
    134     public static class And extends Match {
    135         private final Match lhs;
    136         private final Match rhs;
    137         public And(Match lhs, Match rhs) {this.lhs = lhs; this.rhs = rhs;}
     392    /**
     393     * Matches if both left and right expressions match.
     394     */
     395    public static class And extends BinaryMatch {
     396    public And(Match lhs, Match rhs) {super(lhs, rhs);}
    138397        @Override public boolean match(OsmPrimitive osm) {
    139398            return lhs.match(osm) && rhs.match(osm);
    140399        }
    141         @Override public String toString() {return lhs+" && "+rhs;}
    142         public Match getLhs() {
    143             return lhs;
     400        @Override public String toString() {
     401            return lhs + " && " + rhs;
    144402        }
    145         public Match getRhs() {
    146             return rhs;
    147         }
    148403    }
    149404
    150     public static class Or extends Match {
    151         private final Match lhs;
    152         private final Match rhs;
    153         public Or(Match lhs, Match rhs) {this.lhs = lhs; this.rhs = rhs;}
     405    /**
     406     * Matches if the left OR the right expression match.
     407     */
     408    public static class Or extends BinaryMatch {
     409    public Or(Match lhs, Match rhs) {super(lhs, rhs);}
    154410        @Override public boolean match(OsmPrimitive osm) {
    155411            return lhs.match(osm) || rhs.match(osm);
    156412        }
    157         @Override public String toString() {return lhs+" || "+rhs;}
    158         public Match getLhs() {
    159             return lhs;
     413        @Override public String toString() {
     414            return lhs + " || " + rhs;
    160415        }
    161         public Match getRhs() {
    162             return rhs;
     416    }
     417
     418    /**
     419     * Matches if the left OR the right expression match, but not both.
     420     */
     421    public static class Xor extends BinaryMatch {
     422    public Xor(Match lhs, Match rhs) {super(lhs, rhs);}
     423        @Override public boolean match(OsmPrimitive osm) {
     424            return lhs.match(osm) ^ rhs.match(osm);
    163425        }
     426        @Override public String toString() {
     427            return lhs + " ^ " + rhs;
     428        }
    164429    }
    165 
     430   
     431    /**
     432     * Matches objects with the given object ID.
     433     */
    166434    private static class Id extends Match {
    167435        private long id;
    168436        public Id(long id) {
    169437            this.id = id;
    170438        }
     439        public Id(PushbackTokenizer tokenizer) throws ParseError {
     440            this(tokenizer.readNumber(tr("Primitive id expected")));
     441        }
    171442        @Override public boolean match(OsmPrimitive osm) {
    172443            return id == 0?osm.isNew():osm.getUniqueId() == id;
    173444        }
    174445        @Override public String toString() {return "id="+id;}
    175446    }
    176447
     448    /**
     449     * Matches objects with the given changeset ID.
     450     */
    177451    private static class ChangesetId extends Match {
    178452        private long changesetid;
    179453        public ChangesetId(long changesetid) {this.changesetid = changesetid;}
     454        public ChangesetId(PushbackTokenizer tokenizer) throws ParseError {
     455            this(tokenizer.readNumber(tr("Changeset id expected")));
     456        }
    180457        @Override public boolean match(OsmPrimitive osm) {
    181458            return osm.getChangesetId() == changesetid;
    182459        }
    183460        @Override public String toString() {return "changeset="+changesetid;}
    184461    }
    185462
     463    /**
     464     * Matches objects with the given version number.
     465     */
    186466    private static class Version extends Match {
    187467        private long version;
    188468        public Version(long version) {this.version = version;}
     469        public Version(PushbackTokenizer tokenizer) throws ParseError {
     470            this(tokenizer.readNumber(tr("Version expected")));
     471        }
    189472        @Override public boolean match(OsmPrimitive osm) {
    190473            return osm.getVersion() == version;
    191474        }
    192475        @Override public String toString() {return "version="+version;}
    193476    }
    194477
     478    /**
     479     * Matches objects with the given key-value pair.
     480     */
    195481    private static class KeyValue extends Match {
    196482        private final String key;
    197483        private final Pattern keyPattern;
     
    286572        @Override public String toString() {return key+"="+value;}
    287573    }
    288574
     575    /**
     576     * Matches objects with the exact given key-value pair.
     577     */
    289578    public static class ExactKeyValue extends Match {
    290579
    291580        private enum Mode {
     
    414703
    415704    }
    416705
     706    /**
     707     * Match a string in any tags (key or value), with optional regex and case insensitivity.
     708     */
    417709    private static class Any extends Match {
    418710        private final String search;
    419711        private final Pattern searchRegex;
     
    477769        }
    478770    }
    479771
     772    // TODO: change how we handle this
    480773    private static class ExactType extends Match {
    481774        private final Class<?> type;
    482775        public ExactType(String type) throws ParseError {
     
    496789        @Override public String toString() {return "type="+type;}
    497790    }
    498791
     792    /**
     793     * Matches objects last changed by the given username.
     794     */
    499795    private static class UserMatch extends Match {
    500796        private String user;
    501797        public UserMatch(String user) {
     
    518814        }
    519815    }
    520816
     817    /**
     818     * Matches objects with the given relation role (i.e. "outer").
     819     */
    521820    private static class RoleMatch extends Match {
    522821        private String role;
    523822        public RoleMatch(String role) {
     
    548847        }
    549848    }
    550849
     850    /**
     851     * Matches objects with properties in a certain range.
     852     */
    551853    private abstract static class CountRange extends Match {
    552854
    553855        private long minCount;
     
    558860            this.maxCount = Math.max(minCount, maxCount);
    559861        }
    560862
     863        public CountRange(Range range) {
     864            this(range.getStart(), range.getEnd());
     865        }
     866
    561867        protected abstract Long getCount(OsmPrimitive osm);
    562868
    563869        protected abstract String getCountString();
     
    578884    }
    579885
    580886
    581 
     887    /**
     888     * Matches ways with a number of nodes in given range
     889     */
    582890    private static class NodeCountRange extends CountRange {
     891        public NodeCountRange(Range range) {
     892            super(range);
     893        }
    583894
    584         public NodeCountRange(long minCount, long maxCount) {
    585             super(minCount, maxCount);
     895        public NodeCountRange(PushbackTokenizer tokenizer) throws ParseError {
     896            this(tokenizer.readRange(tr("Range of numbers expected")));
    586897        }
    587898
    588899        @Override
     
    599910        }
    600911    }
    601912
     913    /**
     914     * Matches objects with a number of tags in given range
     915     */
    602916    private static class TagCountRange extends CountRange {
     917        public TagCountRange(Range range) {
     918            super(range);
     919        }
    603920
    604         public TagCountRange(long minCount, long maxCount) {
    605             super(minCount, maxCount);
     921        public TagCountRange(PushbackTokenizer tokenizer) throws ParseError {
     922            this(tokenizer.readRange(tr("Range of numbers expected")));
    606923        }
    607924
    608925        @Override
     
    616933        }
    617934    }
    618935
     936    /**
     937     * Matches objects with a timestamp in given range
     938     */
    619939    private static class TimestampRange extends CountRange {
    620940
    621941        public TimestampRange(long minCount, long maxCount) {
     
    634954
    635955    }
    636956
     957    /**
     958     * Matches objects that are new (i.e. have not been uploaded to the server)
     959     */
    637960    private static class New extends Match {
    638961        @Override public boolean match(OsmPrimitive osm) {
    639962            return osm.isNew();
     
    643966        }
    644967    }
    645968
     969    /**
     970     * Matches all objects that have been modified, created, or undeleted
     971     */
    646972    private static class Modified extends Match {
    647973        @Override public boolean match(OsmPrimitive osm) {
    648974            return osm.isModified() || osm.isNewOrUndeleted();
     
    650976        @Override public String toString() {return "modified";}
    651977    }
    652978
     979    /**
     980     * Matches all objects currently selected
     981     */
    653982    private static class Selected extends Match {
    654983        @Override public boolean match(OsmPrimitive osm) {
    655984            return Main.main.getCurrentDataSet().isSelected(osm);
     
    657986        @Override public String toString() {return "selected";}
    658987    }
    659988
     989    /**
     990     * Match objects that are incomplete, where only id and type are known.
     991     * Typically some members of a relation are incomplete until they are
     992     * fetched from the server.
     993     */
    660994    private static class Incomplete extends Match {
    661995        @Override public boolean match(OsmPrimitive osm) {
    662996            return osm.isIncomplete();
     
    664998        @Override public String toString() {return "incomplete";}
    665999    }
    6661000
     1001    /**
     1002     * Matches objects that don't have any interesting tags (i.e. only has source,
     1003     * FIXME, etc.). The complete list of uninteresting tags can be found here:
     1004     * org.openstreetmap.josm.data.osm.OsmPrimitive.getUninterestingKeys()
     1005     */
    6671006    private static class Untagged extends Match {
    6681007        @Override public boolean match(OsmPrimitive osm) {
    6691008            return !osm.isTagged() && !osm.isIncomplete();
     
    6711010        @Override public String toString() {return "untagged";}
    6721011    }
    6731012
     1013    /**
     1014     * Matches ways which are closed (i.e. first and last node are the same)
     1015     */
    6741016    private static class Closed extends Match {
    6751017        @Override public boolean match(OsmPrimitive osm) {
    6761018            return osm instanceof Way && ((Way) osm).isClosed();
     
    6781020        @Override public String toString() {return "closed";}
    6791021    }
    6801022
    681     public static class Parent extends Match {
    682         private final Match child;
     1023    /**
     1024     * Matches objects if they are parents of the expression
     1025     */
     1026    public static class Parent extends UnaryMatch {
    6831027        public Parent(Match m) {
    684             if (m == null) {
    685                 // "parent" (null) should mean the same as "parent()"
    686                 // (Always). I.e. match everything
    687                 child = new Always();
    688             } else {
    689                 child = m;
    690             }
     1028            super(m);
    6911029        }
    6921030        @Override public boolean match(OsmPrimitive osm) {
    6931031            boolean isParent = false;
    6941032
    6951033            if (osm instanceof Way) {
    6961034                for (Node n : ((Way)osm).getNodes()) {
    697                     isParent |= child.match(n);
     1035                    isParent |= match.match(n);
    6981036                }
    6991037            } else if (osm instanceof Relation) {
    7001038                for (RelationMember member : ((Relation)osm).getMembers()) {
    701                     isParent |= child.match(member.getMember());
     1039                    isParent |= match.match(member.getMember());
    7021040                }
    7031041            }
    7041042            return isParent;
    7051043        }
    706         @Override public String toString() {return "parent(" + child + ")";}
    707         public Match getChild() {
    708             return child;
    709         }
     1044        @Override public String toString() {return "parent(" + match + ")";}
    7101045    }
    7111046
    712     public static class Child extends Match {
    713         private final Match parent;
     1047    /**
     1048     * Matches objects if they are children of the expression
     1049     */
     1050    public static class Child extends UnaryMatch {
    7141051
    7151052        public Child(Match m) {
    716             // "child" (null) should mean the same as "child()"
    717             // (Always). I.e. match everything
    718             if (m == null) {
    719                 parent = new Always();
    720             } else {
    721                 parent = m;
    722             }
     1053            super(m);
    7231054        }
    7241055
    7251056        @Override public boolean match(OsmPrimitive osm) {
    7261057            boolean isChild = false;
    7271058            for (OsmPrimitive p : osm.getReferrers()) {
    728                 isChild |= parent.match(p);
     1059                isChild |= match.match(p);
    7291060            }
    7301061            return isChild;
    7311062        }
    732         @Override public String toString() {return "child(" + parent + ")";}
    733 
    734         public Match getParent() {
    735             return parent;
    736         }
     1063        @Override public String toString() {return "child(" + match + ")";}
    7371064    }
    7381065
    7391066    /**
    740      * Matches on the area of a closed way.
     1067     * Matches if the size of the area is within the given range
    7411068     *
    7421069     * @author Ole Jørgen Brønner
    7431070     */
    744     private static class Area extends CountRange {
     1071    private static class AreaSize extends CountRange {
    7451072
    746         public Area(long minCount, long maxCount) {
    747             super(minCount, maxCount);
     1073        public AreaSize(Range range) {
     1074            super(range);
    7481075        }
    7491076
     1077        public AreaSize(PushbackTokenizer tokenizer) throws ParseError {
     1078            this(tokenizer.readRange(tr("Range of numbers expected")));
     1079        }
     1080
    7501081        @Override
    7511082        protected Long getCount(OsmPrimitive osm) {
    7521083            if (!(osm instanceof Way && ((Way) osm).isClosed()))
     
    7571088
    7581089        @Override
    7591090        protected String getCountString() {
    760             return "area";
     1091            return "areasize";
    7611092        }
    7621093    }
    7631094
    7641095    /**
    765      * Matches data within bounds.
     1096     * Matches objects within the given bounds.
    7661097     */
    7671098    private abstract static class InArea extends Match {
    7681099
     
    7961127    }
    7971128
    7981129    /**
    799      * Matches data in source area ("downloaded area").
     1130     * Matches objects within source area ("downloaded area").
    8001131     */
    8011132    private static class InDataSourceArea extends InArea {
    8021133
     
    8111142    }
    8121143
    8131144    /**
    814      * Matches data in current map view.
     1145     * Matches objects within current map view.
    8151146     */
    8161147    private static class InView extends InArea {
    8171148
     
    8421173        .parse();
    8431174    }
    8441175
     1176    /**
     1177     * Parse search string.
     1178     *
     1179     * @return match determined by search string
     1180     * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError
     1181     */
    8451182    public Match parse() throws ParseError {
    8461183        Match m = parseExpression();
    8471184        if (!tokenizer.readIfEqual(Token.EOF))
     
    8511188        return m;
    8521189    }
    8531190
     1191    /**
     1192     * Parse expression. This is a recursive method.
     1193     *
     1194     * @return match determined by parsing expression
     1195     * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError
     1196     */
    8541197    private Match parseExpression() throws ParseError {
    8551198        Match factor = parseFactor();
    8561199        if (factor == null)
     1200            // empty search string
    8571201            return null;
    8581202        if (tokenizer.readIfEqual(Token.OR))
    8591203            return new Or(factor, parseExpression(tr("Missing parameter for OR")));
     1204        else if (tokenizer.readIfEqual(Token.XOR))
     1205            return new Xor(factor, parseExpression(tr("Missing parameter for XOR")));
    8601206        else {
    8611207            Match expression = parseExpression();
    8621208            if (expression == null)
     1209                // reached end of search string, no more recursive calls
    8631210                return factor;
    8641211            else
     1212                // the default operator is AND
    8651213                return new And(factor, expression);
    8661214        }
    8671215    }
    8681216
     1217    /**
     1218     * Parse expression, showing the specified error message if parsing fails.
     1219     *
     1220     * @param errorMessage to display if parsing error occurs
     1221     * @return
     1222     * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError
     1223     */
    8691224    private Match parseExpression(String errorMessage) throws ParseError {
    8701225        Match expression = parseExpression();
    8711226        if (expression == null)
     
    8741229            return expression;
    8751230    }
    8761231
     1232    /**
     1233     * Parse next factor (a search operator or search term).
     1234     *
     1235     * @return match determined by parsing factor string
     1236     * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError
     1237     */
    8771238    private Match parseFactor() throws ParseError {
    8781239        if (tokenizer.readIfEqual(Token.LEFT_PARENT)) {
    8791240            Match expression = parseExpression();
    8801241            if (!tokenizer.readIfEqual(Token.RIGHT_PARENT))
    8811242                throw new ParseError(Token.RIGHT_PARENT, tokenizer.nextToken());
    8821243            return expression;
    883         } else if (tokenizer.readIfEqual(Token.NOT))
     1244        } else if (tokenizer.readIfEqual(Token.NOT)) {
    8841245            return new Not(parseFactor(tr("Missing operator for NOT")));
    885         else if (tokenizer.readIfEqual(Token.KEY)) {
     1246        } else if (tokenizer.readIfEqual(Token.KEY)) {
     1247            // factor consists of key:value or key=value
    8861248            String key = tokenizer.getText();
    8871249            if (tokenizer.readIfEqual(Token.EQUALS))
    8881250                return new ExactKeyValue(regexSearch, key, tokenizer.readTextOrNumber());
    8891251            else if (tokenizer.readIfEqual(Token.COLON)) {
    890                 if ("id".equals(key))
    891                     return new Id(tokenizer.readNumber(tr("Primitive id expected")));
    892                 else if ("tags".equals(key)) {
    893                     Range range = tokenizer.readRange(tr("Range of numbers expected"));
    894                     return new TagCountRange(range.getStart(), range.getEnd());
    895                 } else if ("nodes".equals(key)) {
    896                     Range range = tokenizer.readRange(tr("Range of numbers expected"));
    897                     return new NodeCountRange(range.getStart(), range.getEnd());
    898                 } else if ("areasize".equals(key)) {
    899                     Range range = tokenizer.readRange(tr("Range of numbers expected"));
    900                     return new Area(range.getStart(), range.getEnd());
    901                 } else if ("timestamp".equals(key)) {
    902                     String rangeS = " " + tokenizer.readTextOrNumber() + " "; // add leading/trailing space in order to get expected split (e.g. "a--" => {"a", ""})
    903                     String[] rangeA = rangeS.split("/");
    904                     if (rangeA.length == 1) {
    905                         return new KeyValue(key, rangeS, regexSearch, caseSensitive);
    906                     } else if (rangeA.length == 2) {
    907                         String rangeA1 = rangeA[0].trim();
    908                         String rangeA2 = rangeA[1].trim();
    909                         long minDate = DateUtils.fromString(rangeA1.isEmpty() ? "1980" : rangeA1).getTime(); // if min timestap is empty: use lowest possible date
    910                         long maxDate = rangeA2.isEmpty() ? new Date().getTime() : DateUtils.fromString(rangeA2).getTime(); // if max timestamp is empty: use "now"
    911                         return new TimestampRange(minDate, maxDate);
    912                     } else {
    913                         /* I18n: Don't translate timestamp keyword */ throw new ParseError(tr("Expecting <i>min</i>/<i>max</i> after ''timestamp''"));
    914                     }
    915                 } else if ("changeset".equals(key))
    916                     return new ChangesetId(tokenizer.readNumber(tr("Changeset id expected")));
    917                 else if ("version".equals(key))
    918                     return new Version(tokenizer.readNumber(tr("Version expected")));
    919                 else
    920                     return parseKV(key, tokenizer.readTextOrNumber());
     1252                // see if we have a Match that takes a data parameter
     1253                Match match = matchFactory.getSimpleMatch(key, tokenizer);
     1254                if (match != null) {
     1255                    return match;
     1256                }
     1257               
     1258                if (matchFactory.isUnaryMatch(key))
     1259                    return matchFactory.getUnaryMatch(key, parseFactor(), tokenizer);
     1260
     1261                // key:value form where value is a string (may be OSM key search)
     1262                return parseKV(key, tokenizer.readTextOrNumber());
    9211263            } else if (tokenizer.readIfEqual(Token.QUESTION_MARK))
    9221264                return new BooleanMatch(key, false);
    923             else if ("new".equals(key))
    924                 return new New();
    925             else if ("modified".equals(key))
    926                 return new Modified();
    927             else if ("incomplete".equals(key))
    928                 return new Incomplete();
    929             else if ("untagged".equals(key))
    930                 return new Untagged();
    931             else if ("selected".equals(key))
    932                 return new Selected();
    933             else if ("closed".equals(key))
    934                 return new Closed();
    935             else if ("child".equals(key))
    936                 return new Child(parseFactor());
    937             else if ("parent".equals(key))
    938                 return new Parent(parseFactor());
    939             else if ("indownloadedarea".equals(key))
    940                 return new InDataSourceArea(false);
    941             else if ("allindownloadedarea".equals(key))
    942                 return new InDataSourceArea(true);
    943             else if ("inview".equals(key))
    944                 return new InView(false);
    945             else if ("allinview".equals(key))
    946                 return new InView(true);
    947             else
     1265            else {
     1266                Match match = matchFactory.getSimpleMatch(key, null);
     1267                if (match != null) {
     1268                    return match;
     1269                }
     1270               
     1271                if (matchFactory.isUnaryMatch(key))
     1272                    return matchFactory.getUnaryMatch(key, parseFactor(), tokenizer);
     1273               
     1274                // match string in any key or value
    9481275                return new Any(key, regexSearch, caseSensitive);
     1276            }
    9491277        } else
    9501278            return null;
    9511279    }
  • src/org/openstreetmap/josm/tools/template_engine/ContextSwitchTemplate.java

     
    183183
    184184    private Match transform(Match m, int searchExpressionPosition) throws ParseError {
    185185        if (m instanceof Parent) {
    186             Match child = transform(((Parent) m).getChild(), searchExpressionPosition);
     186            Match child = transform(((Parent) m).getOperand(), searchExpressionPosition);
    187187            return new ParentSet(child);
    188188        } else if (m instanceof Child) {
    189             Match parent = transform(((Child) m).getParent(), searchExpressionPosition);
     189            Match parent = transform(((Child) m).getOperand(), searchExpressionPosition);
    190190            return new ChildSet(parent);
    191191        } else if (m instanceof And) {
    192192            Match lhs = transform(((And) m).getLhs(), searchExpressionPosition);