]> source.dussan.org Git - vaadin-framework.git/commitdiff
Add VaadinLocator multielement queries (#13016, #13017, #13018, #13019)
authorPatrik Lindström <patrik@vaadin.com>
Thu, 28 Nov 2013 19:22:22 +0000 (21:22 +0200)
committerTeemu Suo-Anttila <teemusa@vaadin.com>
Wed, 11 Dec 2013 13:33:14 +0000 (15:33 +0200)
Change-Id: If6faa939953023761dccaf256001c6ece018d5e8

client/src/com/vaadin/client/ApplicationConfiguration.java
client/src/com/vaadin/client/ApplicationConnection.java
client/src/com/vaadin/client/componentlocator/ComponentLocator.java
client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java
client/src/com/vaadin/client/componentlocator/LocatorStrategy.java
client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java

index a8ae47385a90cd18f0b97de9d0def3fe53e95224..1a5b0d836ff3fabc2b2fa1dd017ae25792cbb778 100644 (file)
@@ -512,6 +512,30 @@ public class ApplicationConfiguration implements EntryPoint {
         }
     }
 
+    /**
+     * Returns all tags for given class. Tags are used in
+     * {@link ApplicationConfiguration} to keep track of different classes and
+     * their hierarchy
+     * 
+     * @since 7.2
+     * @param classname
+     *            name of class which tags we want
+     * @return Integer array of tags pointing to this classname
+     */
+    public Integer[] getTagsForServerSideClassName(String classname) {
+        List<Integer> tags = new ArrayList<Integer>();
+
+        for (Map.Entry<Integer, String> entry : tagToServerSideClassName
+                .entrySet()) {
+            if (classname.equals(entry.getValue())) {
+                tags.add(entry.getKey());
+            }
+        }
+
+        Integer[] out = new Integer[tags.size()];
+        return tags.toArray(out);
+    }
+
     public Integer getParentTag(int tag) {
         return componentInheritanceMap.get(tag);
     }
@@ -762,5 +786,4 @@ public class ApplicationConfiguration implements EntryPoint {
     private static final Logger getLogger() {
         return Logger.getLogger(ApplicationConfiguration.class.getName());
     }
-
 }
index 841e1df96d9a01361c5b80469783a055ee57614c..68fb57b8c004afcbb4e1a915af05f5bc9ddce28a 100644 (file)
@@ -1,4 +1,4 @@
-/*
+/* 
  * Copyright 2000-2013 Vaadin Ltd.
  * 
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
@@ -64,7 +64,6 @@ import com.google.gwt.user.client.Window.ClosingHandler;
 import com.google.gwt.user.client.ui.HasWidgets;
 import com.google.gwt.user.client.ui.Widget;
 import com.vaadin.client.ApplicationConfiguration.ErrorMessage;
-import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent;
 import com.vaadin.client.ResourceLoader.ResourceLoadEvent;
 import com.vaadin.client.ResourceLoader.ResourceLoadListener;
 import com.vaadin.client.communication.HasJavaScriptConnectorHelper;
@@ -577,6 +576,12 @@ public class ApplicationConnection {
         client.getElementByPathStartingAt = $entry(function(id, element) {
             return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementByPathStartingAt(Ljava/lang/String;Lcom/google/gwt/user/client/Element;)(id, element);
         });
+        client.getElementsByPath = $entry(function(id) {
+            return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementsByPath(Ljava/lang/String;)(id);
+        });
+        client.getElementsByPathStartingAt = $entry(function(id, element) {
+            return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementsByPathStartingAt(Ljava/lang/String;Lcom/google/gwt/user/client/Element;)(id, element);
+        });
         client.getPathForElement = $entry(function(element) {
             return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element);
         });
index 6f6e52c0e1e4b41fd7562ef4fc5c04a3fa966310..aa841ce5b05bda793e5789c0ecac1761167e1f99 100644 (file)
@@ -18,6 +18,8 @@ package com.vaadin.client.componentlocator;
 import java.util.Arrays;
 import java.util.List;
 
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.Element;
 import com.vaadin.client.ApplicationConnection;
 
@@ -94,14 +96,74 @@ public class ComponentLocator {
      */
     public Element getElementByPath(String path) {
         for (LocatorStrategy strategy : locatorStrategies) {
-            Element element = strategy.getElementByPath(path);
-            if (null != element) {
-                return element;
+            if (strategy.validatePath(path)) {
+                Element element = strategy.getElementByPath(path);
+                if (null != element) {
+                    return element;
+                }
             }
         }
         return null;
     }
 
+    /**
+     * Locates elements using a String locator (path) which identifies DOM
+     * elements.
+     * 
+     * @since 7.2
+     * @param path
+     *            The String locator which identifies target elements.
+     * @return The JavaScriptArray of DOM elements identified by {@code path} or
+     *         empty array if elements could not be located.
+     */
+    public JsArray<Element> getElementsByPath(String path) {
+        JsArray<Element> jsElements = JavaScriptObject.createArray().cast();
+        for (LocatorStrategy strategy : locatorStrategies) {
+            if (strategy.validatePath(path)) {
+                List<Element> elements = strategy.getElementsByPath(path);
+                if (elements.size() > 0) {
+                    for (Element e : elements) {
+                        jsElements.push(e);
+                    }
+                    return jsElements;
+                }
+            }
+        }
+        return jsElements;
+    }
+
+    /**
+     * Locates elements using a String locator (path) which identifies DOM
+     * elements. The path starts from the specified root element.
+     * 
+     * @see #getElementByPath(String)
+     * 
+     * @since 7.2
+     * @param path
+     *            The path of elements to be found
+     * @param root
+     *            The root element where the path is anchored
+     * @return The JavaScriptArray of DOM elements identified by {@code path} or
+     *         empty array if elements could not be located.
+     */
+    public JsArray<Element> getElementsByPathStartingAt(String path,
+            Element root) {
+        JsArray<Element> jsElements = JavaScriptObject.createArray().cast();
+        for (LocatorStrategy strategy : locatorStrategies) {
+            if (strategy.validatePath(path)) {
+                List<Element> elements = strategy.getElementsByPathStartingAt(
+                        path, root);
+                if (elements.size() > 0) {
+                    for (Element e : elements) {
+                        jsElements.push(e);
+                    }
+                    return jsElements;
+                }
+            }
+        }
+        return jsElements;
+    }
+
     /**
      * Locates an element using a String locator (path) which identifies a DOM
      * element. The path starts from the specified root element.
@@ -117,9 +179,12 @@ public class ComponentLocator {
      */
     public Element getElementByPathStartingAt(String path, Element root) {
         for (LocatorStrategy strategy : locatorStrategies) {
-            Element element = strategy.getElementByPathStartingAt(path, root);
-            if (null != element) {
-                return element;
+            if (strategy.validatePath(path)) {
+                Element element = strategy.getElementByPathStartingAt(path,
+                        root);
+                if (null != element) {
+                    return element;
+                }
             }
         }
         return null;
@@ -135,4 +200,5 @@ public class ComponentLocator {
     public ApplicationConnection getClient() {
         return client;
     }
+
 }
index 5123a57e5dd4ba1c5817b9ec045c0d76c825899d..4a1b100213b6e05ea46960826efd48d2694b10d1 100644 (file)
@@ -20,6 +20,7 @@ import java.util.Iterator;
 import java.util.List;
 
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.regexp.shared.RegExp;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.ui.HasWidgets;
@@ -73,10 +74,18 @@ public class LegacyLocatorStrategy implements LocatorStrategy {
 
     private final ApplicationConnection client;
 
+    private static final RegExp validSyntax = RegExp
+            .compile("^((\\w+::)?(PID_S\\w+)?)?(/[a-zA-Z0-9]+\\[\\d+\\])*$");
+
     public LegacyLocatorStrategy(ApplicationConnection clientConnection) {
         client = clientConnection;
     }
 
+    @Override
+    public boolean validatePath(String path) {
+        return validSyntax.test(path);
+    }
+
     @Override
     public String getPathForElement(Element targetElement) {
         ComponentConnector connector = Util
@@ -178,11 +187,17 @@ public class LegacyLocatorStrategy implements LocatorStrategy {
         }
     }
 
+    /**
+     * {@inheritDoc}
+     */
     @Override
     public Element getElementByPath(String path) {
         return getElementByPathStartingAt(path, null);
     }
 
+    /**
+     * {@inheritDoc}
+     */
     @Override
     public Element getElementByPathStartingAt(String path, Element baseElement) {
         /*
@@ -219,6 +234,34 @@ public class LegacyLocatorStrategy implements LocatorStrategy {
         return null;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public List<Element> getElementsByPath(String path) {
+        // This type of search is not supported in LegacyLocator
+        List<Element> array = new ArrayList<Element>();
+        Element e = getElementByPath(path);
+        if (e != null) {
+            array.add(e);
+        }
+        return array;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public List<Element> getElementsByPathStartingAt(String path, Element root) {
+        // This type of search is not supported in LegacyLocator
+        List<Element> array = new ArrayList<Element>();
+        Element e = getElementByPathStartingAt(path, root);
+        if (e != null) {
+            array.add(e);
+        }
+        return array;
+    }
+
     /**
      * Finds the first widget in the hierarchy (moving upwards) that implements
      * SubPartAware. Returns the SubPartAware implementor or null if none is
@@ -672,4 +715,5 @@ public class LegacyLocatorStrategy implements LocatorStrategy {
 
         return null;
     }
+
 }
index 56ed396609793979653fee9759eb530621f1f2c0..e892f43d76612f8be196d155dc5d9cf443994b5a 100644 (file)
@@ -15,6 +15,8 @@
  */
 package com.vaadin.client.componentlocator;
 
+import java.util.List;
+
 import com.google.gwt.user.client.Element;
 
 /**
@@ -28,6 +30,18 @@ import com.google.gwt.user.client.Element;
  * @author Vaadin Ltd
  */
 public interface LocatorStrategy {
+
+    /**
+     * Test the given input path for formatting errors. If a given path can not
+     * be validated, the locator strategy will not be attempted.
+     * 
+     * @param path
+     *            a locator path expression
+     * @return true, if the implementing class can process the given path,
+     *         otherwise false
+     */
+    boolean validatePath(String path);
+
     /**
      * Generates a String locator which uniquely identifies the target element.
      * The {@link #getElementByPath(String)} method can be used for the inverse
@@ -74,4 +88,35 @@ public interface LocatorStrategy {
      *         could not be located.
      */
     Element getElementByPathStartingAt(String path, Element root);
+
+    /**
+     * Locates all elements that match a String locator (path) which identifies
+     * DOM elements.
+     * 
+     * This functionality is limited in {@link LegacyLocatorStrategy}.
+     * 
+     * @param path
+     *            The String locator which identifies target elements.
+     * @return List that contains all matched elements. Empty list if none
+     *         found.
+     */
+    List<Element> getElementsByPath(String path);
+
+    /**
+     * Locates all elements that match a String locator (path) which identifies
+     * DOM elements. The path starts from the specified root element.
+     * 
+     * This functionality is limited in {@link LegacyLocatorStrategy}.
+     * 
+     * @see #getElementsByPath(String)
+     * 
+     * @param path
+     *            The String locator which identifies target elements.
+     * @param root
+     *            The element that is at the root of the path.
+     * @return List that contains all matched elements. Empty list if none
+     *         found.
+     */
+
+    List<Element> getElementsByPathStartingAt(String path, Element root);
 }
index 95b2745bf80d07be205390dfae6c394124e9c1df..6337ca7e8c3455fa5c3e9232baaf8c5cbf6dea9e 100644 (file)
 package com.vaadin.client.componentlocator;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
+import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.ui.RootPanel;
 import com.google.gwt.user.client.ui.Widget;
 import com.vaadin.client.ApplicationConnection;
 import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.FastStringSet;
+import com.vaadin.client.HasComponentsConnector;
 import com.vaadin.client.Util;
 import com.vaadin.client.metadata.NoDataException;
 import com.vaadin.client.metadata.Property;
+import com.vaadin.client.metadata.TypeDataStore;
 import com.vaadin.client.ui.AbstractConnector;
-import com.vaadin.client.ui.AbstractHasComponentsConnector;
 import com.vaadin.client.ui.SubPartAware;
 import com.vaadin.client.ui.VNotification;
 import com.vaadin.shared.AbstractComponentState;
@@ -55,6 +59,18 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
 
     private final ApplicationConnection client;
 
+    /**
+     * Internal container/descriptor for search predicates
+     * 
+     * @author Vaadin Ltd
+     */
+    private static final class Predicate {
+        private String name = "";
+        private String value = "";
+        private boolean wildcard = false;
+        private int index = -1;
+    }
+
     public VaadinFinderLocatorStrategy(ApplicationConnection clientConnection) {
         client = clientConnection;
     }
@@ -71,18 +87,145 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
         return null;
     }
 
+    private boolean isNotificationExpression(String path) {
+        String[] starts = { "//", "/" };
+
+        String[] frags = { "com.vaadin.ui.Notification.class",
+                "com.vaadin.ui.Notification", "VNotification.class",
+                "VNotification", "Notification.class", "Notification" };
+
+        String[] ends = { "/", "[" };
+
+        for (String s : starts) {
+            for (String f : frags) {
+                if (path.equals(s + f)) {
+                    return true;
+                }
+
+                for (String e : ends) {
+                    if (path.startsWith(s + f + e)) {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public List<Element> getElementsByPath(String path) {
+
+        if (isNotificationExpression(path)) {
+            List<Element> elements = new ArrayList<Element>();
+
+            for (VNotification n : findNotificationsByPath(path)) {
+                elements.add(n.getElement());
+            }
+
+            return elements;
+        }
+
+        List<Element> elems = eliminateDuplicates(getElementsByPathStartingAtConnector(
+                path, client.getUIConnector()));
+
+        return elems;
+    }
+
     /**
      * {@inheritDoc}
      */
     @Override
     public Element getElementByPath(String path) {
-        if (path.startsWith("//VNotification")) {
-            return findNotificationByPath(path);
+        if (isNotificationExpression(path)) {
+            return findNotificationsByPath(path).get(0).getElement();
         }
         return getElementByPathStartingAtConnector(path,
                 client.getUIConnector());
     }
 
+    /**
+     * Generate a list of predicates from a single predicate string
+     * 
+     * @param str
+     *            a comma separated string of predicates
+     * @return a List of Predicate objects
+     */
+    private List<Predicate> extractPredicates(String path) {
+        List<Predicate> predicates = new ArrayList<Predicate>();
+
+        String str = extractPredicateString(path);
+        if (null == str || str.length() == 0) {
+            return predicates;
+        }
+
+        // Extract input strings
+        List<String> input = new ArrayList<String>();
+        {
+            int idx = indexOfIgnoringQuotes(str, ',', 0), p = 0;
+            if (idx == -1) {
+                input.add(str);
+            } else {
+                do {
+                    input.add(str.substring(p, idx));
+                    p = idx + 1;
+                    idx = indexOfIgnoringQuotes(str, ',', p);
+                } while (idx > -1);
+                input.add(str.substring(p));
+            }
+        }
+
+        // Process each predicate into proper predicate descriptor
+        for (String s : input) {
+            Predicate p = new Predicate();
+            s = s.trim();
+
+            try {
+                // If we can parse out the predicate as a pure index argument,
+                // stop processing here.
+                p.index = Integer.parseInt(s);
+                predicates.add(p);
+
+                continue;
+            } catch (Exception e) {
+                p.index = -1;
+            }
+
+            int idx = indexOfIgnoringQuotes(s, '=');
+            if (idx < 0) {
+                continue;
+            }
+            p.name = s.substring(0, idx);
+            p.value = s.substring(idx + 1);
+
+            if (p.value.equals("?")) {
+                p.wildcard = true;
+                p.value = null;
+            } else {
+                // Only unquote predicate value once we're sure it's a proper
+                // value...
+
+                p.value = unquote(p.value);
+            }
+
+            predicates.add(p);
+        }
+
+        // Move any (and all) index predicates to last place in the list.
+        for (int i = 0, l = predicates.size(); i < l - 1; ++i) {
+            if (predicates.get(i).index > -1) {
+                predicates.add(predicates.remove(i));
+                --i;
+                --l;
+            }
+        }
+
+        return predicates;
+    }
+
     /**
      * Special case for finding notifications as they have no connectors and are
      * directly attached to {@link RootPanel}.
@@ -93,19 +236,29 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
      *            brackets.
      * @return the notification element or null if not found.
      */
-    private Element findNotificationByPath(String path) {
-        ArrayList<VNotification> notifications = new ArrayList<VNotification>();
+    private List<VNotification> findNotificationsByPath(String path) {
+
+        List<VNotification> notifications = new ArrayList<VNotification>();
         for (Widget w : RootPanel.get()) {
             if (w instanceof VNotification) {
                 notifications.add((VNotification) w);
             }
         }
-        String indexStr = extractPredicateString(path);
-        int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
-        if (index >= 0 && index < notifications.size()) {
-            return notifications.get(index).getElement();
+
+        List<Predicate> predicates = extractPredicates(path);
+        for (Predicate p : predicates) {
+
+            if (p.index > -1) {
+                VNotification n = notifications.get(p.index);
+                notifications.clear();
+                if (n != null) {
+                    notifications.add(n);
+                }
+            }
+
         }
-        return null;
+
+        return eliminateDuplicates(notifications);
     }
 
     /**
@@ -117,6 +270,16 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
                 Util.findPaintable(client, root));
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public List<Element> getElementsByPathStartingAt(String path, Element root) {
+        List<Element> elements = getElementsByPathStartingAtConnector(path,
+                Util.findPaintable(client, root));
+        return elements;
+    }
+
     /**
      * Finds an element by the specified path, starting traversal of the
      * connector hierarchy from the specified root.
@@ -130,8 +293,12 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
     private Element getElementByPathStartingAtConnector(String path,
             ComponentConnector root) {
         String[] pathComponents = path.split(SUBPART_SEPARATOR);
-        ComponentConnector connector = findConnectorByPath(pathComponents[0],
-                root);
+        ComponentConnector connector;
+        if (pathComponents[0].length() > 0) {
+            connector = findConnectorByPath(pathComponents[0], root);
+        } else {
+            connector = root;
+        }
         if (connector != null) {
             if (pathComponents.length > 1) {
                 // We have subparts
@@ -147,6 +314,47 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
         return null;
     }
 
+    /**
+     * Finds a list of elements by the specified path, starting traversal of the
+     * connector hierarchy from the specified root.
+     * 
+     * @param path
+     *            the locator path
+     * @param root
+     *            the root connector
+     * @return the list of elements identified by path or empty list if not
+     *         found.
+     */
+    private List<Element> getElementsByPathStartingAtConnector(String path,
+            ComponentConnector root) {
+        String[] pathComponents = path.split(SUBPART_SEPARATOR);
+        List<ComponentConnector> connectors;
+        if (pathComponents[0].length() > 0) {
+            connectors = findConnectorsByPath(pathComponents[0],
+                    Arrays.asList(root));
+        } else {
+            connectors = Arrays.asList(root);
+        }
+
+        List<Element> output = new ArrayList<Element>();
+        if (null != connectors && !connectors.isEmpty()) {
+            if (pathComponents.length > 1) {
+                // We have subparts
+                for (ComponentConnector connector : connectors) {
+                    if (connector.getWidget() instanceof SubPartAware) {
+                        output.add(((SubPartAware) connector.getWidget())
+                                .getSubPartElement(pathComponents[1]));
+                    }
+                }
+            } else {
+                for (ComponentConnector connector : connectors) {
+                    output.add(connector.getWidget().getElement());
+                }
+            }
+        }
+        return eliminateDuplicates(output);
+    }
+
     /**
      * Recursively finds a connector for the element identified by the provided
      * path by traversing the connector hierarchy starting from the
@@ -156,7 +364,7 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
      *            The path identifying an element.
      * @param parent
      *            The connector to start traversing from.
-     * @return The connector identified by {@code path} or null if it no such
+     * @return The connector identified by {@code path} or null if no such
      *         connector could be found.
      */
     private ComponentConnector findConnectorByPath(String path,
@@ -168,18 +376,55 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
         String[] fragments = splitFirstFragmentFromTheRest(path);
         List<ComponentConnector> potentialMatches = collectPotentialMatches(
                 parent, fragments[0], findRecursively);
-        ComponentConnector connector = filterPotentialMatches(potentialMatches,
-                extractPredicateString(fragments[0]));
-        if (connector != null) {
+
+        List<ComponentConnector> connectors = filterMatches(potentialMatches,
+                extractPredicates(fragments[0]));
+
+        if (!connectors.isEmpty()) {
             if (fragments.length > 1) {
-                return findConnectorByPath(fragments[1], connector);
+                return findConnectorByPath(fragments[1], connectors.get(0));
             } else {
-                return connector;
+                return connectors.get(0);
             }
         }
         return null;
     }
 
+    /**
+     * Recursively finds connectors for the elements identified by the provided
+     * path by traversing the connector hierarchy starting from {@code parents}
+     * connectors.
+     * 
+     * @param path
+     *            The path identifying elements.
+     * @param parents
+     *            The list of connectors to start traversing from.
+     * @return The list of connectors identified by {@code path} or empty list
+     *         if no such connectors could be found.
+     */
+    private List<ComponentConnector> findConnectorsByPath(String path,
+            List<ComponentConnector> parents) {
+        boolean findRecursively = path.startsWith("//");
+        // Strip away the one or two slashes from the beginning of the path
+        path = path.substring(findRecursively ? 2 : 1);
+
+        String[] fragments = splitFirstFragmentFromTheRest(path);
+
+        List<ComponentConnector> potentialMatches = new ArrayList<ComponentConnector>();
+        for (ComponentConnector parent : parents) {
+            potentialMatches.addAll(collectPotentialMatches(parent,
+                    fragments[0], findRecursively));
+        }
+
+        List<ComponentConnector> connectors = filterMatches(potentialMatches,
+                extractPredicates(fragments[0]));
+
+        if (!connectors.isEmpty() && fragments.length > 1) {
+            return (findConnectorsByPath(fragments[1], connectors));
+        }
+        return eliminateDuplicates(connectors);
+    }
+
     /**
      * Returns the predicate string, i.e. the string between the brackets in a
      * path fragment. Examples: <code>
@@ -189,7 +434,8 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
      * 
      * @param pathFragment
      *            The path fragment from which to extract the predicate string.
-     * @return The predicate string for the path fragment or null if none.
+     * @return The predicate string for the path fragment or empty string if not
+     *         found.
      */
     private String extractPredicateString(String pathFragment) {
         int ixOpenBracket = indexOfIgnoringQuotes(pathFragment, '[');
@@ -198,60 +444,63 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
                     ixOpenBracket);
             return pathFragment.substring(ixOpenBracket + 1, ixCloseBracket);
         }
-        return null;
+        return "";
     }
 
     /**
-     * Returns the first ComponentConnector that matches the predicate string
-     * from a list of potential matches. If {@code predicateString} is null, the
-     * first element in the {@code potentialMatches} list is returned.
+     * Go through a list of potentially matching components, modifying that list
+     * until all elements that remain in that list match the complete list of
+     * predicates.
      * 
      * @param potentialMatches
-     *            A list of potential matches to check.
-     * @param predicateString
-     *            The predicate that should match. Can be an index or a property
-     *            name, value pair or null.
-     * @return A {@link ComponentConnector} from the {@code potentialMatches}
-     *         list, which matches the {@code predicateString} or null if no
-     *         matches are found.
+     *            a list of component connectors. Will be changed.
+     * @param predicates
+     *            an immutable list of predicates
+     * @return filtered list of component connectors.
      */
-    private ComponentConnector filterPotentialMatches(
-            List<ComponentConnector> potentialMatches, String predicateString) {
-
-        if (potentialMatches.isEmpty()) {
-            return null;
-        }
-
-        if (predicateString != null) {
+    private List<ComponentConnector> filterMatches(
+            List<ComponentConnector> potentialMatches,
+            List<Predicate> predicates) {
+
+        for (Predicate p : predicates) {
+
+            if (p.index > -1) {
+                try {
+                    ComponentConnector v = potentialMatches.get(p.index);
+                    potentialMatches.clear();
+                    potentialMatches.add(v);
+                } catch (IndexOutOfBoundsException e) {
+                    potentialMatches.clear();
+                }
 
-            int split_idx = predicateString.indexOf('=');
+                continue;
+            }
 
-            if (split_idx != -1) {
+            for (int i = 0, l = potentialMatches.size(); i < l; ++i) {
 
-                String propertyName = predicateString.substring(0, split_idx)
-                        .trim();
-                String value = unquote(predicateString.substring(split_idx + 1)
-                        .trim());
+                ComponentConnector c = potentialMatches.get(i);
+                Property property = AbstractConnector.getStateType(c)
+                        .getProperty(p.name);
 
-                for (ComponentConnector connector : potentialMatches) {
-                    Property property = AbstractConnector.getStateType(
-                            connector).getProperty(propertyName);
-                    if (valueEqualsPropertyValue(value, property,
-                            connector.getState())) {
-                        return connector;
-                    }
+                Object propData;
+                try {
+                    propData = property.getValue(c.getState());
+                } catch (NoDataException e) {
+                    propData = null;
                 }
 
-                return null;
-
-            } else {
-                int index = Integer.valueOf(predicateString);
-                return index < potentialMatches.size() ? potentialMatches
-                        .get(index) : null;
+                if ((p.wildcard && propData == null)
+                        || (!p.wildcard && !valueEqualsPropertyValue(p.value,
+                                property, c.getState()))) {
+                    potentialMatches.remove(i);
+                    --l;
+                    --i;
+                }
             }
+
         }
 
-        return potentialMatches.get(0);
+        return eliminateDuplicates(potentialMatches);
     }
 
     /**
@@ -270,7 +519,7 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
             AbstractComponentState state) {
         try {
             return value.equals(property.getValue(state));
-        } catch (NoDataException e) {
+        } catch (Exception e) {
             // The property doesn't exist in the state object, so they aren't
             // equal.
             return false;
@@ -315,8 +564,8 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
             ComponentConnector parent, String pathFragment,
             boolean collectRecursively) {
         ArrayList<ComponentConnector> potentialMatches = new ArrayList<ComponentConnector>();
-        if (parent instanceof AbstractHasComponentsConnector) {
-            List<ComponentConnector> children = ((AbstractHasComponentsConnector) parent)
+        if (parent instanceof HasComponentsConnector) {
+            List<ComponentConnector> children = ((HasComponentsConnector) parent)
                     .getChildComponents();
             for (ComponentConnector child : children) {
                 String widgetName = getWidgetName(pathFragment);
@@ -329,7 +578,7 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
                 }
             }
         }
-        return potentialMatches;
+        return eliminateDuplicates(potentialMatches);
     }
 
     /**
@@ -346,7 +595,60 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
      */
     private boolean connectorMatchesPathFragment(ComponentConnector connector,
             String widgetName) {
-        return widgetName.equals(Util.getSimpleName(connector.getWidget()));
+        Class<?> connectorClass = connector.getClass();
+        List<String> ids = new ArrayList<String>();
+
+        FastStringSet identifiers = TypeDataStore.get().findIdentifiersFor(
+                connectorClass);
+        JsArrayString str = identifiers.dump();
+
+        for (int j = 0; j < str.length(); ++j) {
+            ids.add(str.get(j));
+        }
+
+        Integer[] widgetTags = client.getConfiguration()
+                .getTagsForServerSideClassName(getFullClassName(widgetName));
+        if (widgetTags.length == 0) {
+            widgetTags = client.getConfiguration()
+                    .getTagsForServerSideClassName(
+                            getFullClassName("com.vaadin.ui." + widgetName));
+        }
+
+        for (int i = 0, l = ids.size(); i < l; ++i) {
+
+            // Fuzz the connector name, so that the client can provide (for
+            // example: /Button, /Button.class, /com.vaadin.ui.Button,
+            // /com.vaadin.ui.Button.class, etc)
+
+            String name = ids.get(i);
+            final String simpleName = getSimpleClassName(name);
+            final String fullName = getFullClassName(name);
+
+            if (widgetTags.length > 0) {
+                Integer[] foundTags = client.getConfiguration()
+                        .getTagsForServerSideClassName(fullName);
+                for (int tag : foundTags) {
+                    if (tagsMatch(widgetTags, tag)) {
+                        return true;
+                    }
+                }
+            }
+
+            // Fallback if something failed before.
+            if (widgetName.equals(fullName + ".class")
+                    || widgetName.equals(fullName)
+                    || widgetName.equals(simpleName + ".class")
+                    || widgetName.equals(simpleName) || widgetName.equals(name)) {
+                return true;
+            }
+        }
+
+        // If the server-side class name didn't match, fall back to testing for
+        // the explicit widget name
+        String widget = Util.getSimpleName(connector.getWidget());
+        return widgetName.equals(widget)
+                || widgetName.equals(widget + ".class");
+
     }
 
     /**
@@ -412,4 +714,75 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy {
         return -1;
     }
 
+    private String getSimpleClassName(String s) {
+        String[] parts = s.split("\\.");
+        if (s.endsWith(".class")) {
+            return parts[parts.length - 2];
+        }
+        return parts.length > 0 ? parts[parts.length - 1] : s;
+    }
+
+    private String getFullClassName(String s) {
+        if (s.endsWith(".class")) {
+            return s.substring(0, s.lastIndexOf(".class"));
+        }
+        return s;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * com.vaadin.client.componentlocator.LocatorStrategy#validatePath(java.
+     * lang.String)
+     */
+    @Override
+    public boolean validatePath(String path) {
+        // This syntax is so difficult to regexp properly, that we'll just try
+        // to find something with it regardless of the correctness of the
+        // syntax...
+        return true;
+    }
+
+    /**
+     * Go through a list, removing all duplicate elements from it. This method
+     * is used to avoid accumulation of duplicate entries in result lists
+     * resulting from low-context recursion.
+     * 
+     * Preserves first entry in list, removes others. Preserves list order.
+     * 
+     * @return list passed as parameter, after modification
+     */
+    private final <T> List<T> eliminateDuplicates(List<T> list) {
+
+        int l = list.size();
+        for (int j = 0; j < l; ++j) {
+            T ref = list.get(j);
+
+            for (int i = j + 1; i < l; ++i) {
+                if (list.get(i) == ref) {
+                    list.remove(i);
+                    --i;
+                    --l;
+                }
+            }
+        }
+
+        return list;
+    }
+
+    private boolean tagsMatch(Integer[] targets, Integer tag) {
+        for (int i = 0; i < targets.length; ++i) {
+            if (targets[i].equals(tag)) {
+                return true;
+            }
+        }
+
+        try {
+            return tagsMatch(targets,
+                    client.getConfiguration().getParentTag(tag));
+        } catch (Exception e) {
+            return false;
+        }
+    }
 }