Index: trunk/src/org/openstreetmap/josm/actions/search/PushbackTokenizer.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/search/PushbackTokenizer.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/actions/search/PushbackTokenizer.java	(revision 2645)
@@ -3,14 +3,28 @@
 
 import java.io.IOException;
-import java.io.PushbackReader;
-import java.util.LinkedList;
+import java.io.Reader;
+
+import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
 
 public class PushbackTokenizer {
-    private PushbackReader search;
+    private final Reader search;
 
-    private LinkedList<String> pushBackBuf = new LinkedList<String>();
+    private Token currentToken;
+    private String currentText;
+    private int c;
 
-    public PushbackTokenizer(PushbackReader search) {
+    public PushbackTokenizer(Reader search) {
         this.search = search;
+        getChar();
+    }
+
+    public enum Token {NOT, OR, LEFT_PARENT, RIGHT_PARENT, COLON, EQUALS, KEY, EOF}
+
+    private void getChar() {
+        try {
+            c = search.read();
+        } catch (IOException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
     }
 
@@ -23,84 +37,89 @@
      * @return The next token in the stream.
      */
-    public String nextToken() {
-        if (!pushBackBuf.isEmpty()) {
-            return pushBackBuf.removeLast();
+    public Token nextToken() {
+        if (currentToken != null) {
+            Token result = currentToken;
+            currentToken = null;
+            return result;
         }
 
-        try {
-            int next;
-            char c = ' ';
-            while (c == ' ' || c == '\t' || c == '\n') {
-                next = search.read();
-                if (next == -1)
-                    return null;
-                c = (char)next;
+        while (Character.isWhitespace(c)) {
+            getChar();
+        }
+        switch (c) {
+        case -1:
+            getChar();
+            return Token.EOF;
+        case ':':
+            getChar();
+            return Token.COLON;
+        case '=':
+            getChar();
+            return Token.EQUALS;
+        case '-':
+            getChar();
+            return Token.NOT;
+        case '(':
+            getChar();
+            return Token.LEFT_PARENT;
+        case ')':
+            getChar();
+            return Token.RIGHT_PARENT;
+        case '|':
+            getChar();
+            return Token.OR;
+        case '"':
+        {
+            StringBuilder s = new StringBuilder();
+            while (c != -1 && c != '"') {
+                s.append((char)c);
+                getChar();
             }
-            StringBuilder s;
-            switch (c) {
-            case ':':
-            case '=':
-                next = search.read();
-                if (next == -1 || next == ' ' || next == '\t') {
-                    pushBack(" ");
-                } else {
-                    search.unread(next);
-                }
-                return String.valueOf(c);
-            case '-':
-                return "-";
-            case '(':
-                return "(";
-            case ')':
-                return ")";
-            case '|':
-                return "|";
-            case '"':
-                s = new StringBuilder(" ");
-                for (int nc = search.read(); nc != -1 && nc != '"'; nc = search.read())
-                    s.append((char)nc);
-                return s.toString();
-            default:
-                s = new StringBuilder();
-            for (;;) {
-                s.append(c);
-                next = search.read();
-                if (next == -1) {
-                    if (s.toString().equals("OR"))
-                        return "|";
-                    return " "+s.toString();
-                }
-                c = (char)next;
-                if (c == ' ' || c == '\t' || c == '"' || c == ':' || c == '(' || c == ')' || c == '|' || c == '=') {
-                    search.unread(next);
-                    if (s.toString().equals("OR"))
-                        return "|";
-                    return " "+s.toString();
-                }
+            getChar();
+            currentText = s.toString();
+            return Token.KEY;
+        }
+        default:
+        {
+            StringBuilder s = new StringBuilder();
+            while (!(c == -1 || Character.isWhitespace(c) || c == '"'|| c == ':' || c == '(' || c == ')' || c == '|' || c == '=')) {
+                s.append((char)c);
+                getChar();
             }
-            }
-        } catch (IOException e) {
-            throw new RuntimeException(e.getMessage(), e);
+            currentText = s.toString();
+            if ("or".equals(currentText))
+                return Token.OR;
+            else
+                return Token.KEY;
+        }
         }
     }
 
-    public boolean readIfEqual(String tok) {
-        String nextTok = nextToken();
-        if (nextTok == null ? tok == null : nextTok.equals(tok))
+    public boolean readIfEqual(Token token) {
+        Token nextTok = nextToken();
+        if (nextTok == null ? token == null : nextTok == token)
             return true;
-        pushBack(nextTok);
+        currentToken = nextTok;
         return false;
     }
 
     public String readText() {
-        String nextTok = nextToken();
-        if (nextTok != null && nextTok.startsWith(" "))
-            return nextTok.substring(1);
-        pushBack(nextTok);
+        Token nextTok = nextToken();
+        if (nextTok == Token.KEY)
+            return currentText;
+        currentToken = nextTok;
         return null;
     }
 
-    public void pushBack(String tok) {
-        pushBackBuf.addLast(tok);
+    public String readText(String errorMessage) throws ParseError {
+        String text = readText();
+        if (text == null)
+            throw new ParseError(errorMessage);
+        else
+            return text;
+    }
+
+    public String getText() {
+        return currentText;
     }
 }
Index: trunk/src/org/openstreetmap/josm/actions/search/SearchCompiler.java
===================================================================
--- trunk/src/org/openstreetmap/josm/actions/search/SearchCompiler.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/actions/search/SearchCompiler.java	(revision 2645)
@@ -14,4 +14,5 @@
 
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.search.PushbackTokenizer.Token;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -23,6 +24,22 @@
 
 /**
- * Implements a google-like search.
- * @author Imi
+ Implements a google-like search.
+ <br>
+ Grammar:
+<pre>
+expression =
+  fact | expression
+  fact expression
+  fact
+
+fact =
+ ( expression )
+ -fact
+ term=term
+ term:term
+ term
+ </pre>
+
+ @author Imi
  */
 public class SearchCompiler {
@@ -184,5 +201,5 @@
     }
 
-    private static class ExactKeyValue extends Match {
+    public static class ExactKeyValue extends Match {
 
         private enum Mode {
@@ -201,5 +218,5 @@
                 throw new ParseError(tr("Key cannot be empty when tag operator is used. Sample use: key=value"));
             this.key = key;
-            this.value = value;
+            this.value = value == null?"":value;
             if ("".equals(value) && "*".equals(key)) {
                 mode = Mode.NONE;
@@ -527,8 +544,5 @@
             } else if (osm instanceof Relation) {
                 for (RelationMember member : ((Relation)osm).getMembers()) {
-                    if (member.getMember() != null) {
-                        // TODO Nullable member will not be allowed
-                        isParent |= child.match(member.getMember());
-                    }
+                    isParent |= child.match(member.getMember());
                 }
             }
@@ -576,96 +590,79 @@
 
     public Match parse() throws ParseError {
-        Match m = parseJuxta();
-        if (!tokenizer.readIfEqual(null))
+        Match m = parseExpression();
+        if (!tokenizer.readIfEqual(Token.EOF))
             throw new ParseError(tr("Unexpected token: {0}", tokenizer.nextToken()));
         return m;
     }
 
-    private Match parseJuxta() throws ParseError {
-        Match juxta = new Always();
-
-        Match m;
-        while ((m = parseOr()) != null) {
-            juxta = new And(m, juxta);
-        }
-
-        return juxta;
-    }
-
-    private Match parseOr() throws ParseError {
-        Match a = parseNot();
-        if (tokenizer.readIfEqual("|")) {
-            Match b = parseNot();
-            if (a == null || b == null)
-                throw new ParseError(tr("Missing arguments for or."));
-            return new Or(a, b);
-        }
-        return a;
-    }
-
-    private Match parseNot() throws ParseError {
-        if (tokenizer.readIfEqual("-")) {
-            Match m = parseParens();
-            if (m == null)
-                throw new ParseError(tr("Missing argument for not."));
-            return new Not(m);
-        }
-        return parseParens();
-    }
-
-    private Match parseParens() throws ParseError {
-        if (tokenizer.readIfEqual("(")) {
-            Match m = parseJuxta();
-            if (!tokenizer.readIfEqual(")"))
-                throw new ParseError(tr("Expected closing parenthesis."));
-            return m;
-        }
-        return parsePat();
-    }
-
-    private Match parsePat() throws ParseError {
-        String tok = tokenizer.readText();
-
-        if (tokenizer.readIfEqual(":")) {
-            String tok2 = tokenizer.readText();
-            if (tok == null) {
-                tok = "";
-            }
-            if (tok2 == null) {
-                tok2 = "";
-            }
-            return parseKV(tok, tok2);
-        }
-
-        if (tokenizer.readIfEqual("=")) {
-            String tok2 = tokenizer.readText();
-            if (tok == null) {
-                tok = "";
-            }
-            if (tok2 == null) {
-                tok2 = "";
-            }
-            return new ExactKeyValue(regexSearch, tok, tok2);
-        }
-
-        if (tok == null)
+    private Match parseExpression() throws ParseError {
+        Match factor = parseFactor();
+        if (factor == null)
             return null;
-        else if (tok.equals("modified"))
-            return new Modified();
-        else if (tok.equals("incomplete"))
-            return new Incomplete();
-        else if (tok.equals("untagged"))
-            return new Untagged();
-        else if (tok.equals("selected"))
-            return new Selected();
-        else if (tok.equals("child"))
-            return new Child(parseParens());
-        else if (tok.equals("parent"))
-            return new Parent(parseParens());
+        if (tokenizer.readIfEqual(Token.OR))
+            return new Or(factor, parseExpression(tr("Missing parameter for OR")));
+        else {
+            Match expression = parseExpression();
+            if (expression == null)
+                return factor;
+            else
+                return new And(factor, expression);
+        }
+    }
+
+    private Match parseExpression(String errorMessage) throws ParseError {
+        Match expression = parseExpression();
+        if (expression == null)
+            throw new ParseError(errorMessage);
         else
-            return new Any(tok, regexSearch, caseSensitive);
-    }
+            return expression;
+    }
+
+    private Match parseFactor() throws ParseError {
+        if (tokenizer.readIfEqual(Token.LEFT_PARENT)) {
+            Match expression = parseExpression();
+            if (!tokenizer.readIfEqual(Token.RIGHT_PARENT))
+                throw new ParseError(tr("Missing right parent"));
+            return expression;
+        } else if (tokenizer.readIfEqual(Token.NOT))
+            return new Not(parseFactor(tr("Missing operator for NOT")));
+        else if (tokenizer.readIfEqual(Token.KEY)) {
+            String key = tokenizer.getText();
+            if (tokenizer.readIfEqual(Token.EQUALS))
+                return new ExactKeyValue(regexSearch, key, tokenizer.readText());
+            else if (tokenizer.readIfEqual(Token.COLON))
+                return parseKV(key, tokenizer.readText());
+            else if ("modified".equals(key))
+                return new Modified();
+            else if ("incomplete".equals(key))
+                return new Incomplete();
+            else if ("untagged".equals(key))
+                return new Untagged();
+            else if ("selected".equals(key))
+                return new Selected();
+            else if ("child".equals(key))
+                return new Child(parseFactor());
+            else if ("parent".equals(key))
+                return new Parent(parseFactor());
+            else
+                return new Any(key, regexSearch, caseSensitive);
+        } else
+            return null;
+    }
+
+    private Match parseFactor(String errorMessage) throws ParseError {
+        Match fact = parseFactor();
+        if (fact == null)
+            throw new ParseError(errorMessage);
+        else
+            return fact;
+    }
+
+
 
     private Match parseKV(String key, String value) throws ParseError {
+        if (value == null) {
+            value = "";
+        }
         if (key.equals("type"))
             return new ExactType(value);
Index: trunk/src/org/openstreetmap/josm/data/Preferences.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/Preferences.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/data/Preferences.java	(revision 2645)
@@ -670,4 +670,12 @@
     }
 
+    public boolean isCollection(String key, boolean def) {
+        String s = get(key);
+        if (s != null && s.length() != 0)
+            return s.indexOf("\u001e") >= 0 || s.indexOf("§§§") >= 0;
+            else
+                return def;
+    }
+
     synchronized public Collection<String> getCollection(String key, Collection<String> def) {
         String s = get(key);
Index: trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java	(revision 2645)
@@ -21,4 +21,7 @@
 
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.search.SearchCompiler;
+import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
+import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
 import org.openstreetmap.josm.data.osm.visitor.Visitor;
 import org.openstreetmap.josm.gui.mappaint.ElemStyle;
@@ -512,5 +515,5 @@
     private int timestamp;
 
-    private static Collection<String> uninteresting = null;
+    private static volatile Collection<String> uninteresting = null;
     /**
      * Contains a list of "uninteresting" keys that do not make an object
@@ -526,5 +529,5 @@
     }
 
-    private static Collection<String> directionKeys = null;
+    private static volatile Match directionKeys = null;
 
     /**
@@ -533,10 +536,33 @@
      * Initialized by checkDirectionTagged()
      */
-    public static Collection<String> getDirectionKeys() {
+    public static void initDirectionKeys() {
         if(directionKeys == null) {
-            directionKeys = Main.pref.getCollection("tags.direction",
-                    Arrays.asList("oneway","incline","incline_steep","aerialway"));
-        }
-        return directionKeys;
+
+            // Legacy support - convert list of keys to search pattern
+            if (Main.pref.isCollection("tags.direction", false)) {
+                System.out.println("Collection of keys in tags.direction is no longer supported, value will converted to search pattern");
+                Collection<String> keys = Main.pref.getCollection("tags.direction", null);
+                StringBuilder builder = new StringBuilder();
+                for (String key:keys) {
+                    builder.append(key);
+                    builder.append("=* | ");
+                }
+                builder.delete(builder.length() - 3, builder.length());
+                Main.pref.put("tags.direction", builder.toString());
+            }
+
+            String defaultValue = "oneway=* | incline=* | incline_steep=* | aerialway=*";
+            try {
+                directionKeys = SearchCompiler.compile(Main.pref.get("tags.direction", defaultValue), false, false);
+            } catch (ParseError e) {
+                System.err.println("Unable to compile pattern for tags.direction, trying default pattern: " + e.getMessage());
+
+                try {
+                    directionKeys = SearchCompiler.compile(defaultValue, false, false);
+                } catch (ParseError e2) {
+                    throw new AssertionError("Unable to compile default pattern for direction keys: " + e2.getMessage());
+                }
+            }
+        }
     }
 
@@ -1071,14 +1097,10 @@
 
     private void updateHasDirectionKeys() {
-        getDirectionKeys();
-        if (keys != null) {
-            for (Entry<String,String> e : getKeys().entrySet()) {
-                if (directionKeys.contains(e.getKey())) {
-                    flags |= FLAG_HAS_DIRECTIONS;
-                    return;
-                }
-            }
-        }
-        flags &= ~FLAG_HAS_DIRECTIONS;
+        initDirectionKeys();
+        if (directionKeys.match(this)) {
+            flags |= FLAG_HAS_DIRECTIONS;
+        } else {
+            flags &= ~FLAG_HAS_DIRECTIONS;
+        }
     }
     /**
Index: trunk/src/org/openstreetmap/josm/data/osm/event/AbstractDatasetChangedEvent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/event/AbstractDatasetChangedEvent.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/data/osm/event/AbstractDatasetChangedEvent.java	(revision 2645)
@@ -10,4 +10,7 @@
 public abstract class AbstractDatasetChangedEvent {
 
+    public enum DatasetEventType {DATA_CHANGED, NODE_MOVED, PRIMITIVES_ADDED, PRIMITIVES_REMOVED,
+        RELATION_MEMBERS_CHANGED, TAGS_CHANGED, WAY_NODES_CHANGED}
+
     protected final DataSet dataSet;
 
@@ -17,4 +20,14 @@
 
     public abstract void fire(DataSetListener listener);
+
+    /**
+     * Returns list of primitives modified by this event.
+     * <br/>
+     * <strong>WARNING</strong> This value might be incorrect in case
+     * of {@link DataChangedEvent}. It returns all primitives in the dataset
+     * when this method is called (live list), not list of primitives when
+     * the event was created
+     * @return List of modified primitives
+     */
     public abstract List<? extends OsmPrimitive> getPrimitives();
 
@@ -23,3 +36,10 @@
     }
 
+    public abstract DatasetEventType getType();
+
+    @Override
+    public String toString() {
+        return getType().toString();
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/data/osm/event/DataChangedEvent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/event/DataChangedEvent.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/data/osm/event/DataChangedEvent.java	(revision 2645)
@@ -24,3 +24,8 @@
     }
 
+    @Override
+    public DatasetEventType getType() {
+        return DatasetEventType.DATA_CHANGED;
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/data/osm/event/DataSetListenerAdapter.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/event/DataSetListenerAdapter.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/data/osm/event/DataSetListenerAdapter.java	(revision 2645)
@@ -2,4 +2,10 @@
 package org.openstreetmap.josm.data.osm.event;
 
+/**
+ * Classes that do not wish to implement all methods of DataSetListener
+ * may use this class. Implement DatasetListenerAdapter.Listener and
+ * pass this adapter instead of class itself.
+ *
+ */
 public class DataSetListenerAdapter implements DataSetListener {
 
Index: trunk/src/org/openstreetmap/josm/data/osm/event/NodeMovedEvent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/event/NodeMovedEvent.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/data/osm/event/NodeMovedEvent.java	(revision 2645)
@@ -32,3 +32,8 @@
     }
 
+    @Override
+    public DatasetEventType getType() {
+        return DatasetEventType.NODE_MOVED;
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/data/osm/event/PrimitivesAddedEvent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/event/PrimitivesAddedEvent.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/data/osm/event/PrimitivesAddedEvent.java	(revision 2645)
@@ -39,3 +39,8 @@
     }
 
+    @Override
+    public DatasetEventType getType() {
+        return DatasetEventType.PRIMITIVES_ADDED;
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/data/osm/event/PrimitivesRemovedEvent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/event/PrimitivesRemovedEvent.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/data/osm/event/PrimitivesRemovedEvent.java	(revision 2645)
@@ -39,3 +39,8 @@
     }
 
+    @Override
+    public DatasetEventType getType() {
+        return DatasetEventType.PRIMITIVES_REMOVED;
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/data/osm/event/RelationMembersChangedEvent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/event/RelationMembersChangedEvent.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/data/osm/event/RelationMembersChangedEvent.java	(revision 2645)
@@ -32,3 +32,8 @@
     }
 
+    @Override
+    public DatasetEventType getType() {
+        return DatasetEventType.RELATION_MEMBERS_CHANGED;
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/data/osm/event/TagsChangedEvent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/event/TagsChangedEvent.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/data/osm/event/TagsChangedEvent.java	(revision 2645)
@@ -31,3 +31,8 @@
     }
 
+    @Override
+    public DatasetEventType getType() {
+        return DatasetEventType.TAGS_CHANGED;
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/data/osm/event/WayNodesChangedEvent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/osm/event/WayNodesChangedEvent.java	(revision 2644)
+++ trunk/src/org/openstreetmap/josm/data/osm/event/WayNodesChangedEvent.java	(revision 2645)
@@ -32,3 +32,8 @@
     }
 
+    @Override
+    public DatasetEventType getType() {
+        return DatasetEventType.WAY_NODES_CHANGED;
+    }
+
 }
