Ticket #7178: factory_plugin_search_operators2.patch

File factory_plugin_search_operators2.patch, 34.6 KB (added by joshdoe, 9 years ago)

method closer to what bastik suggested

  • 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

     
    77import java.io.PushbackReader;
    88import java.io.StringReader;
    99import java.text.Normalizer;
    10 import java.util.Collection;
    11 import java.util.Date;
     10import java.util.*;
    1211import java.util.regex.Matcher;
    1312import java.util.regex.Pattern;
    1413import java.util.regex.PatternSyntaxException;
     
    5453    private static String  rxErrorMsg = marktr("The regex \"{0}\" had a parse error at offset {1}, full error:\n\n{2}");
    5554    private static String  rxErrorMsgNoPos = marktr("The regex \"{0}\" had a parse error, full error:\n\n{1}");
    5655    private PushbackTokenizer tokenizer;
     56    private static Map<String, SimpleMatchFactory> simpleMatchFactoryMap = new HashMap<String, SimpleMatchFactory>();
     57    private static Map<String, UnaryMatchFactory> unaryMatchFactoryMap = new HashMap<String, UnaryMatchFactory>();
     58    private static Map<String, BinaryMatchFactory> binaryMatchFactoryMap = new HashMap<String, BinaryMatchFactory>();
    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 factories at first instance, so plugins should
     66         * never be able to generate a NPE
     67         */
     68        if (simpleMatchFactoryMap.isEmpty())
     69            addMatchFactory(new CoreSimpleMatchFactory());
     70        if (unaryMatchFactoryMap.isEmpty())
     71            addMatchFactory(new CoreUnaryMatchFactory());
     72
    6273    }
     74   
     75    /**
     76     * Add (register) MatchFactory with SearchCompiler
     77     * @param factory
     78     */
     79    public static void addMatchFactory(MatchFactory factory) {
     80        for (String keyword : factory.getKeywords()) {
     81            // TODO: check for keyword collisions
     82            if (factory instanceof SimpleMatchFactory)
     83                simpleMatchFactoryMap.put(keyword, (SimpleMatchFactory)factory);
     84            else if (factory instanceof UnaryMatchFactory)
     85                unaryMatchFactoryMap.put(keyword, (UnaryMatchFactory)factory);
     86            else if (factory instanceof BinaryMatchFactory)
     87                binaryMatchFactoryMap.put(keyword, (BinaryMatchFactory)factory);
     88            else
     89                throw new AssertionError("Unknown match factory");
     90        }
     91    }
    6392
     93    public class CoreSimpleMatchFactory implements SimpleMatchFactory {
     94        private Collection<String> keywords = Arrays.asList("id", "version",
     95                "changeset", "nodes", "tags", "areasize", "modified", "selected",
     96                "incomplete", "untagged", "closed", "new", "indownloadarea",
     97                "allindownloadarea", "inview", "allinview", "timestamp");
     98
     99        @Override
     100        public Match get(String keyword, PushbackTokenizer tokenizer) throws ParseError {
     101            if ("id".equals(keyword)) {
     102                return new Id(tokenizer);
     103            } else if ("version".equals(keyword)) {
     104                return new Version(tokenizer);
     105            } else if ("changeset".equals(keyword)) {
     106                return new ChangesetId(tokenizer);
     107            } else if ("nodes".equals(keyword)) {
     108                return new NodeCountRange(tokenizer);
     109            } else if ("tags".equals(keyword)) {
     110                return new TagCountRange(tokenizer);
     111            } else if ("areasize".equals(keyword)) {
     112                return new AreaSize(tokenizer);
     113            } else if ("modified".equals(keyword)) {
     114                return new Modified();
     115            } else if ("selected".equals(keyword)) {
     116                return new Selected();
     117            } else if ("incomplete".equals(keyword)) {
     118                return new Incomplete();
     119            } else if ("untagged".equals(keyword)) {
     120                return new Untagged();
     121            } else if ("closed".equals(keyword)) {
     122                return new Closed();
     123            } else if ("new".equals(keyword)) {
     124                return new New();
     125            } else if ("indownloadedarea".equals(keyword)) {
     126                return new InDataSourceArea(false);
     127            } else if ("allindownloadedarea".equals(keyword)) {
     128                return new InDataSourceArea(true);
     129            } else if ("inview".equals(keyword)) {
     130                return new InView(false);
     131            } else if ("allinview".equals(keyword)) {
     132                return new InView(true);
     133            } else if ("timestamp".equals(keyword)) {
     134                String rangeS = " " + tokenizer.readTextOrNumber() + " "; // add leading/trailing space in order to get expected split (e.g. "a--" => {"a", ""})
     135                String[] rangeA = rangeS.split("/");
     136                if (rangeA.length == 1) {
     137                    return new KeyValue(keyword, rangeS, regexSearch, caseSensitive);
     138                } else if (rangeA.length == 2) {
     139                    String rangeA1 = rangeA[0].trim();
     140                    String rangeA2 = rangeA[1].trim();
     141                    long minDate = DateUtils.fromString(rangeA1.isEmpty() ? "1980" : rangeA1).getTime(); // if min timestap is empty: use lowest possible date
     142                    long maxDate = rangeA2.isEmpty() ? new Date().getTime() : DateUtils.fromString(rangeA2).getTime(); // if max timestamp is empty: use "now"
     143                    return new TimestampRange(minDate, maxDate);
     144                } else {
     145                    /*
     146                     * I18n: Don't translate timestamp keyword
     147                     */ throw new ParseError(tr("Expecting <i>min</i>/<i>max</i> after ''timestamp''"));
     148                }
     149            } else {
     150                return null;
     151            }
     152        }
     153
     154        @Override
     155        public Collection<String> getKeywords() {
     156            return keywords;
     157        }
     158    }
     159   
     160    public static class CoreUnaryMatchFactory implements UnaryMatchFactory {
     161        private static Collection<String> keywords = Arrays.asList("parent", "child");
     162       
     163        @Override
     164        public UnaryMatch get(String keyword, Match matchOperand, PushbackTokenizer tokenizer) {
     165            if ("parent".equals(keyword)) {
     166                return new Parent(matchOperand);
     167            } else if ("child".equals(keyword)) {
     168                return new Child(matchOperand);
     169            }
     170            return null;
     171        }
     172
     173        @Override
     174        public Collection<String> getKeywords() {
     175            return keywords;
     176        }
     177    }
     178
     179    /**
     180     * Classes implementing this interface can provide Match operators.
     181     */
     182    private interface MatchFactory {
     183        public Collection<String> getKeywords();
     184    }
     185   
     186    public interface SimpleMatchFactory extends MatchFactory {
     187        public Match get(String keyword, PushbackTokenizer tokenizer) throws ParseError;
     188    }
     189   
     190    public interface UnaryMatchFactory extends MatchFactory {
     191        public UnaryMatch get(String keyword, Match matchOperand, PushbackTokenizer tokenizer) throws ParseError;
     192    }
     193   
     194    public interface BinaryMatchFactory extends MatchFactory {
     195        public BinaryMatch get(String keyword, Match lhs, Match rhs, PushbackTokenizer tokenizer) throws ParseError;
     196    }
     197
     198    /**
     199     * Base class for all search operators.
     200     */
    64201    abstract public static class Match {
     202
    65203        abstract public boolean match(OsmPrimitive osm);
    66204
    67205        /**
     
    69207         */
    70208        protected boolean existsMatch(Collection<? extends OsmPrimitive> primitives) {
    71209            for (OsmPrimitive p : primitives) {
    72                 if (match(p))
     210                if (match(p)) {
    73211                    return true;
     212                }
    74213            }
    75214            return false;
    76215        }
     
    80219         */
    81220        protected boolean forallMatch(Collection<? extends OsmPrimitive> primitives) {
    82221            for (OsmPrimitive p : primitives) {
    83                 if (!match(p))
     222                if (!match(p)) {
    84223                    return false;
     224                }
    85225            }
    86226            return true;
    87227        }
    88228    }
    89229
     230    /**
     231     * A unary search operator which may take data parameters.
     232     */
     233    abstract public static class UnaryMatch extends Match {
     234
     235        protected final Match match;
     236
     237        public UnaryMatch(Match match) {
     238            if (match == null) {
     239                // "operator" (null) should mean the same as "operator()"
     240                // (Always). I.e. match everything
     241                this.match = new Always();
     242            } else {
     243                this.match = match;
     244            }
     245        }
     246
     247        public Match getOperand() {
     248            return match;
     249        }
     250    }
     251
     252    /**
     253     * A binary search operator which may take data parameters.
     254     */
     255    abstract public static class BinaryMatch extends Match {
     256
     257        protected final Match lhs;
     258        protected final Match rhs;
     259
     260        public BinaryMatch(Match lhs, Match rhs) {
     261            this.lhs = lhs;
     262            this.rhs = rhs;
     263        }
     264
     265        public Match getLhs() {
     266            return lhs;
     267        }
     268
     269        public Match getRhs() {
     270            return rhs;
     271        }
     272    }
     273
     274    /**
     275     * Matches every OsmPrimitive.
     276     */
    90277    public static class Always extends Match {
    91278        public static Always INSTANCE = new Always();
    92279        @Override public boolean match(OsmPrimitive osm) {
     
    94281        }
    95282    }
    96283
     284    /**
     285     * Never matches any OsmPrimitive.
     286     */
    97287    public static class Never extends Match {
    98288        @Override
    99289        public boolean match(OsmPrimitive osm) {
     
    101291        }
    102292    }
    103293
    104     public static class Not extends Match {
    105         private final Match match;
    106         public Not(Match match) {this.match = match;}
     294    /**
     295     * Inverts the match.
     296     */
     297    public static class Not extends UnaryMatch {
     298        public Not(Match match) {super(match);}
    107299        @Override public boolean match(OsmPrimitive osm) {
    108300            return !match.match(osm);
    109301        }
     
    113305        }
    114306    }
    115307
     308    /**
     309     * Matches if the value of the corresponding key is ''yes'', ''true'', ''1'' or ''on''.
     310     */
    116311    private static class BooleanMatch extends Match {
    117312        private final String key;
    118313        private final boolean defaultValue;
     
    131326        }
    132327    }
    133328
    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;}
     329    /**
     330     * Matches if both left and right expressions match.
     331     */
     332    public static class And extends BinaryMatch {
     333    public And(Match lhs, Match rhs) {super(lhs, rhs);}
    138334        @Override public boolean match(OsmPrimitive osm) {
    139335            return lhs.match(osm) && rhs.match(osm);
    140336        }
    141         @Override public String toString() {return lhs+" && "+rhs;}
    142         public Match getLhs() {
    143             return lhs;
     337        @Override public String toString() {
     338            return lhs + " && " + rhs;
    144339        }
    145         public Match getRhs() {
    146             return rhs;
    147         }
    148340    }
    149341
    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;}
     342    /**
     343     * Matches if the left OR the right expression match.
     344     */
     345    public static class Or extends BinaryMatch {
     346    public Or(Match lhs, Match rhs) {super(lhs, rhs);}
    154347        @Override public boolean match(OsmPrimitive osm) {
    155348            return lhs.match(osm) || rhs.match(osm);
    156349        }
    157         @Override public String toString() {return lhs+" || "+rhs;}
    158         public Match getLhs() {
    159             return lhs;
     350        @Override public String toString() {
     351            return lhs + " || " + rhs;
    160352        }
    161         public Match getRhs() {
    162             return rhs;
     353    }
     354
     355    /**
     356     * Matches if the left OR the right expression match, but not both.
     357     */
     358    public static class Xor extends BinaryMatch {
     359    public Xor(Match lhs, Match rhs) {super(lhs, rhs);}
     360        @Override public boolean match(OsmPrimitive osm) {
     361            return lhs.match(osm) ^ rhs.match(osm);
    163362        }
     363        @Override public String toString() {
     364            return lhs + " ^ " + rhs;
     365        }
    164366    }
    165 
     367   
     368    /**
     369     * Matches objects with the given object ID.
     370     */
    166371    private static class Id extends Match {
    167372        private long id;
    168373        public Id(long id) {
    169374            this.id = id;
    170375        }
     376        public Id(PushbackTokenizer tokenizer) throws ParseError {
     377            this(tokenizer.readNumber(tr("Primitive id expected")));
     378        }
    171379        @Override public boolean match(OsmPrimitive osm) {
    172380            return id == 0?osm.isNew():osm.getUniqueId() == id;
    173381        }
    174382        @Override public String toString() {return "id="+id;}
    175383    }
    176384
     385    /**
     386     * Matches objects with the given changeset ID.
     387     */
    177388    private static class ChangesetId extends Match {
    178389        private long changesetid;
    179390        public ChangesetId(long changesetid) {this.changesetid = changesetid;}
     391        public ChangesetId(PushbackTokenizer tokenizer) throws ParseError {
     392            this(tokenizer.readNumber(tr("Changeset id expected")));
     393        }
    180394        @Override public boolean match(OsmPrimitive osm) {
    181395            return osm.getChangesetId() == changesetid;
    182396        }
    183397        @Override public String toString() {return "changeset="+changesetid;}
    184398    }
    185399
     400    /**
     401     * Matches objects with the given version number.
     402     */
    186403    private static class Version extends Match {
    187404        private long version;
    188405        public Version(long version) {this.version = version;}
     406        public Version(PushbackTokenizer tokenizer) throws ParseError {
     407            this(tokenizer.readNumber(tr("Version expected")));
     408        }
    189409        @Override public boolean match(OsmPrimitive osm) {
    190410            return osm.getVersion() == version;
    191411        }
    192412        @Override public String toString() {return "version="+version;}
    193413    }
    194414
     415    /**
     416     * Matches objects with the given key-value pair.
     417     */
    195418    private static class KeyValue extends Match {
    196419        private final String key;
    197420        private final Pattern keyPattern;
     
    286509        @Override public String toString() {return key+"="+value;}
    287510    }
    288511
     512    /**
     513     * Matches objects with the exact given key-value pair.
     514     */
    289515    public static class ExactKeyValue extends Match {
    290516
    291517        private enum Mode {
     
    414640
    415641    }
    416642
     643    /**
     644     * Match a string in any tags (key or value), with optional regex and case insensitivity.
     645     */
    417646    private static class Any extends Match {
    418647        private final String search;
    419648        private final Pattern searchRegex;
     
    477706        }
    478707    }
    479708
     709    // TODO: change how we handle this
    480710    private static class ExactType extends Match {
    481711        private final Class<?> type;
    482712        public ExactType(String type) throws ParseError {
     
    496726        @Override public String toString() {return "type="+type;}
    497727    }
    498728
     729    /**
     730     * Matches objects last changed by the given username.
     731     */
    499732    private static class UserMatch extends Match {
    500733        private String user;
    501734        public UserMatch(String user) {
     
    518751        }
    519752    }
    520753
     754    /**
     755     * Matches objects with the given relation role (i.e. "outer").
     756     */
    521757    private static class RoleMatch extends Match {
    522758        private String role;
    523759        public RoleMatch(String role) {
     
    548784        }
    549785    }
    550786
     787    /**
     788     * Matches objects with properties in a certain range.
     789     */
    551790    private abstract static class CountRange extends Match {
    552791
    553792        private long minCount;
     
    558797            this.maxCount = Math.max(minCount, maxCount);
    559798        }
    560799
     800        public CountRange(Range range) {
     801            this(range.getStart(), range.getEnd());
     802        }
     803
    561804        protected abstract Long getCount(OsmPrimitive osm);
    562805
    563806        protected abstract String getCountString();
     
    578821    }
    579822
    580823
    581 
     824    /**
     825     * Matches ways with a number of nodes in given range
     826     */
    582827    private static class NodeCountRange extends CountRange {
     828        public NodeCountRange(Range range) {
     829            super(range);
     830        }
    583831
    584         public NodeCountRange(long minCount, long maxCount) {
    585             super(minCount, maxCount);
     832        public NodeCountRange(PushbackTokenizer tokenizer) throws ParseError {
     833            this(tokenizer.readRange(tr("Range of numbers expected")));
    586834        }
    587835
    588836        @Override
     
    599847        }
    600848    }
    601849
     850    /**
     851     * Matches objects with a number of tags in given range
     852     */
    602853    private static class TagCountRange extends CountRange {
     854        public TagCountRange(Range range) {
     855            super(range);
     856        }
    603857
    604         public TagCountRange(long minCount, long maxCount) {
    605             super(minCount, maxCount);
     858        public TagCountRange(PushbackTokenizer tokenizer) throws ParseError {
     859            this(tokenizer.readRange(tr("Range of numbers expected")));
    606860        }
    607861
    608862        @Override
     
    616870        }
    617871    }
    618872
     873    /**
     874     * Matches objects with a timestamp in given range
     875     */
    619876    private static class TimestampRange extends CountRange {
    620877
    621878        public TimestampRange(long minCount, long maxCount) {
     
    634891
    635892    }
    636893
     894    /**
     895     * Matches objects that are new (i.e. have not been uploaded to the server)
     896     */
    637897    private static class New extends Match {
    638898        @Override public boolean match(OsmPrimitive osm) {
    639899            return osm.isNew();
     
    643903        }
    644904    }
    645905
     906    /**
     907     * Matches all objects that have been modified, created, or undeleted
     908     */
    646909    private static class Modified extends Match {
    647910        @Override public boolean match(OsmPrimitive osm) {
    648911            return osm.isModified() || osm.isNewOrUndeleted();
     
    650913        @Override public String toString() {return "modified";}
    651914    }
    652915
     916    /**
     917     * Matches all objects currently selected
     918     */
    653919    private static class Selected extends Match {
    654920        @Override public boolean match(OsmPrimitive osm) {
    655921            return Main.main.getCurrentDataSet().isSelected(osm);
     
    657923        @Override public String toString() {return "selected";}
    658924    }
    659925
     926    /**
     927     * Match objects that are incomplete, where only id and type are known.
     928     * Typically some members of a relation are incomplete until they are
     929     * fetched from the server.
     930     */
    660931    private static class Incomplete extends Match {
    661932        @Override public boolean match(OsmPrimitive osm) {
    662933            return osm.isIncomplete();
     
    664935        @Override public String toString() {return "incomplete";}
    665936    }
    666937
     938    /**
     939     * Matches objects that don't have any interesting tags (i.e. only has source,
     940     * FIXME, etc.). The complete list of uninteresting tags can be found here:
     941     * org.openstreetmap.josm.data.osm.OsmPrimitive.getUninterestingKeys()
     942     */
    667943    private static class Untagged extends Match {
    668944        @Override public boolean match(OsmPrimitive osm) {
    669945            return !osm.isTagged() && !osm.isIncomplete();
     
    671947        @Override public String toString() {return "untagged";}
    672948    }
    673949
     950    /**
     951     * Matches ways which are closed (i.e. first and last node are the same)
     952     */
    674953    private static class Closed extends Match {
    675954        @Override public boolean match(OsmPrimitive osm) {
    676955            return osm instanceof Way && ((Way) osm).isClosed();
     
    678957        @Override public String toString() {return "closed";}
    679958    }
    680959
    681     public static class Parent extends Match {
    682         private final Match child;
     960    /**
     961     * Matches objects if they are parents of the expression
     962     */
     963    public static class Parent extends UnaryMatch {
    683964        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             }
     965            super(m);
    691966        }
    692967        @Override public boolean match(OsmPrimitive osm) {
    693968            boolean isParent = false;
    694969
    695970            if (osm instanceof Way) {
    696971                for (Node n : ((Way)osm).getNodes()) {
    697                     isParent |= child.match(n);
     972                    isParent |= match.match(n);
    698973                }
    699974            } else if (osm instanceof Relation) {
    700975                for (RelationMember member : ((Relation)osm).getMembers()) {
    701                     isParent |= child.match(member.getMember());
     976                    isParent |= match.match(member.getMember());
    702977                }
    703978            }
    704979            return isParent;
    705980        }
    706         @Override public String toString() {return "parent(" + child + ")";}
    707         public Match getChild() {
    708             return child;
    709         }
     981        @Override public String toString() {return "parent(" + match + ")";}
    710982    }
    711983
    712     public static class Child extends Match {
    713         private final Match parent;
     984    /**
     985     * Matches objects if they are children of the expression
     986     */
     987    public static class Child extends UnaryMatch {
    714988
    715989        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             }
     990            super(m);
    723991        }
    724992
    725993        @Override public boolean match(OsmPrimitive osm) {
    726994            boolean isChild = false;
    727995            for (OsmPrimitive p : osm.getReferrers()) {
    728                 isChild |= parent.match(p);
     996                isChild |= match.match(p);
    729997            }
    730998            return isChild;
    731999        }
    732         @Override public String toString() {return "child(" + parent + ")";}
    733 
    734         public Match getParent() {
    735             return parent;
    736         }
     1000        @Override public String toString() {return "child(" + match + ")";}
    7371001    }
    7381002
    7391003    /**
    740      * Matches on the area of a closed way.
     1004     * Matches if the size of the area is within the given range
    7411005     *
    7421006     * @author Ole Jørgen Brønner
    7431007     */
    744     private static class Area extends CountRange {
     1008    private static class AreaSize extends CountRange {
    7451009
    746         public Area(long minCount, long maxCount) {
    747             super(minCount, maxCount);
     1010        public AreaSize(Range range) {
     1011            super(range);
    7481012        }
    7491013
     1014        public AreaSize(PushbackTokenizer tokenizer) throws ParseError {
     1015            this(tokenizer.readRange(tr("Range of numbers expected")));
     1016        }
     1017
    7501018        @Override
    7511019        protected Long getCount(OsmPrimitive osm) {
    7521020            if (!(osm instanceof Way && ((Way) osm).isClosed()))
     
    7571025
    7581026        @Override
    7591027        protected String getCountString() {
    760             return "area";
     1028            return "areasize";
    7611029        }
    7621030    }
    7631031
    7641032    /**
    765      * Matches data within bounds.
     1033     * Matches objects within the given bounds.
    7661034     */
    7671035    private abstract static class InArea extends Match {
    7681036
     
    7961064    }
    7971065
    7981066    /**
    799      * Matches data in source area ("downloaded area").
     1067     * Matches objects within source area ("downloaded area").
    8001068     */
    8011069    private static class InDataSourceArea extends InArea {
    8021070
     
    8111079    }
    8121080
    8131081    /**
    814      * Matches data in current map view.
     1082     * Matches objects within current map view.
    8151083     */
    8161084    private static class InView extends InArea {
    8171085
     
    8421110        .parse();
    8431111    }
    8441112
     1113    /**
     1114     * Parse search string.
     1115     *
     1116     * @return match determined by search string
     1117     * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError
     1118     */
    8451119    public Match parse() throws ParseError {
    8461120        Match m = parseExpression();
    8471121        if (!tokenizer.readIfEqual(Token.EOF))
     
    8511125        return m;
    8521126    }
    8531127
     1128    /**
     1129     * Parse expression. This is a recursive method.
     1130     *
     1131     * @return match determined by parsing expression
     1132     * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError
     1133     */
    8541134    private Match parseExpression() throws ParseError {
    8551135        Match factor = parseFactor();
    8561136        if (factor == null)
     1137            // empty search string
    8571138            return null;
    8581139        if (tokenizer.readIfEqual(Token.OR))
    8591140            return new Or(factor, parseExpression(tr("Missing parameter for OR")));
     1141        else if (tokenizer.readIfEqual(Token.XOR))
     1142            return new Xor(factor, parseExpression(tr("Missing parameter for XOR")));
    8601143        else {
    8611144            Match expression = parseExpression();
    8621145            if (expression == null)
     1146                // reached end of search string, no more recursive calls
    8631147                return factor;
    8641148            else
     1149                // the default operator is AND
    8651150                return new And(factor, expression);
    8661151        }
    8671152    }
    8681153
     1154    /**
     1155     * Parse expression, showing the specified error message if parsing fails.
     1156     *
     1157     * @param errorMessage to display if parsing error occurs
     1158     * @return
     1159     * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError
     1160     */
    8691161    private Match parseExpression(String errorMessage) throws ParseError {
    8701162        Match expression = parseExpression();
    8711163        if (expression == null)
     
    8741166            return expression;
    8751167    }
    8761168
     1169    /**
     1170     * Parse next factor (a search operator or search term).
     1171     *
     1172     * @return match determined by parsing factor string
     1173     * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError
     1174     */
    8771175    private Match parseFactor() throws ParseError {
    8781176        if (tokenizer.readIfEqual(Token.LEFT_PARENT)) {
    8791177            Match expression = parseExpression();
    8801178            if (!tokenizer.readIfEqual(Token.RIGHT_PARENT))
    8811179                throw new ParseError(Token.RIGHT_PARENT, tokenizer.nextToken());
    8821180            return expression;
    883         } else if (tokenizer.readIfEqual(Token.NOT))
     1181        } else if (tokenizer.readIfEqual(Token.NOT)) {
    8841182            return new Not(parseFactor(tr("Missing operator for NOT")));
    885         else if (tokenizer.readIfEqual(Token.KEY)) {
     1183        } else if (tokenizer.readIfEqual(Token.KEY)) {
     1184            // factor consists of key:value or key=value
    8861185            String key = tokenizer.getText();
    8871186            if (tokenizer.readIfEqual(Token.EQUALS))
    8881187                return new ExactKeyValue(regexSearch, key, tokenizer.readTextOrNumber());
    8891188            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());
     1189                // see if we have a Match that takes a data parameter
     1190                SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
     1191                if (factory != null)
     1192                    return factory.get(key, tokenizer);
     1193               
     1194                UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
     1195                if (unaryFactory != null)
     1196                    return unaryFactory.get(key, parseFactor(), tokenizer);
     1197
     1198                // key:value form where value is a string (may be OSM key search)
     1199                return parseKV(key, tokenizer.readTextOrNumber());
    9211200            } else if (tokenizer.readIfEqual(Token.QUESTION_MARK))
    9221201                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
     1202            else {
     1203                SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
     1204                if (factory != null)
     1205                    return factory.get(key, null);
     1206               
     1207                UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
     1208                if (unaryFactory != null)
     1209                    return unaryFactory.get(key, parseFactor(), null);
     1210               
     1211                // match string in any key or value
    9481212                return new Any(key, regexSearch, caseSensitive);
     1213            }
    9491214        } else
    9501215            return null;
    9511216    }
  • 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);