Ticket #7178: factory_plugin_search_operators.patch

File factory_plugin_search_operators.patch, 35.8 KB (added by joshdoe, 16 months 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);