Revision: 993
          http://stripes.svn.sourceforge.net/stripes/?rev=993&view=rev
Author:   bengunter
Date:     2008-10-24 19:47:45 +0000 (Fri, 24 Oct 2008)

Log Message:
-----------
First cut at STS-617. Clean URLs can now match on more than just the prefix. 
Warnings will be logged at startup for detected conflicts among the URL 
bindings. Tags will throw exceptions if an attempt is made to generate a URL 
that maps to more than one ActionBean.

Modified Paths:
--------------
    
trunk/stripes/src/net/sourceforge/stripes/controller/AnnotatedClassActionResolver.java
    trunk/stripes/src/net/sourceforge/stripes/controller/UrlBindingFactory.java
    trunk/stripes/src/net/sourceforge/stripes/util/UrlBuilder.java

Added Paths:
-----------
    
trunk/stripes/src/net/sourceforge/stripes/exception/UrlBindingConflictException.java

Modified: 
trunk/stripes/src/net/sourceforge/stripes/controller/AnnotatedClassActionResolver.java
===================================================================
--- 
trunk/stripes/src/net/sourceforge/stripes/controller/AnnotatedClassActionResolver.java
      2008-10-22 19:18:47 UTC (rev 992)
+++ 
trunk/stripes/src/net/sourceforge/stripes/controller/AnnotatedClassActionResolver.java
      2008-10-24 19:47:45 UTC (rev 993)
@@ -172,7 +172,7 @@
      */
     public String getUrlBindingFromPath(String path) {
         UrlBinding mapping = 
UrlBindingFactory.getInstance().getBindingPrototype(path);
-        return mapping == null ? null : mapping.getPath();
+        return mapping == null ? null : mapping.toString();
     }
 
     /**
@@ -186,7 +186,7 @@
      */
     public String getUrlBinding(Class<? extends ActionBean> clazz) {
         UrlBinding mapping = 
UrlBindingFactory.getInstance().getBindingPrototype(clazz);
-        return mapping == null ? null : mapping.getPath();
+        return mapping == null ? null : mapping.toString();
     }
 
     /**
@@ -275,8 +275,7 @@
      */
     public ActionBean getActionBean(ActionBeanContext context) throws 
StripesServletException {
         HttpServletRequest request = context.getRequest();
-        UrlBinding binding = 
UrlBindingFactory.getInstance().getBindingPrototype(request);
-        String path = binding == null ? HttpUtil.getRequestedPath(request) : 
binding.getPath();
+        String path = HttpUtil.getRequestedPath(request);
         ActionBean bean = getActionBean(context, path);
         request.setAttribute(RESOLVED_ACTION, getUrlBindingFromPath(path));
         return bean;
@@ -524,7 +523,8 @@
                                           ActionBeanContext context) {
         Map<String,Method> mappings = this.eventMappings.get(bean);
         String path = HttpUtil.getRequestedPath(context.getRequest());
-        String binding = getUrlBindingFromPath(path);
+        UrlBinding prototype = 
UrlBindingFactory.getInstance().getBindingPrototype(path);
+        String binding = prototype == null ? null : prototype.getPath();
 
         if (binding != null && path.length() != binding.length()) {
             String extra = path.substring(binding.length() + 1);

Modified: 
trunk/stripes/src/net/sourceforge/stripes/controller/UrlBindingFactory.java
===================================================================
--- trunk/stripes/src/net/sourceforge/stripes/controller/UrlBindingFactory.java 
2008-10-22 19:18:47 UTC (rev 992)
+++ trunk/stripes/src/net/sourceforge/stripes/controller/UrlBindingFactory.java 
2008-10-24 19:47:45 UTC (rev 993)
@@ -24,14 +24,18 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.Map.Entry;
 
 import javax.servlet.http.HttpServletRequest;
 
 import net.sourceforge.stripes.action.ActionBean;
 import net.sourceforge.stripes.exception.StripesRuntimeException;
+import net.sourceforge.stripes.exception.UrlBindingConflictException;
 import net.sourceforge.stripes.util.HttpUtil;
+import net.sourceforge.stripes.util.Log;
 import net.sourceforge.stripes.util.bean.ParseException;
 
 /**
@@ -54,6 +58,8 @@
  * @see UrlBindingParameter
  */
 public class UrlBindingFactory {
+    private static final Log log = Log.getInstance(UrlBindingFactory.class);
+
     /** Singleton instance */
     private static final UrlBindingFactory instance = new UrlBindingFactory();
 
@@ -72,8 +78,11 @@
     /** Maps simple paths to [EMAIL PROTECTED] UrlBinding}s */
     private final Map<String, UrlBinding> pathCache = new HashMap<String, 
UrlBinding>();
 
+    /** Keeps a list of all the paths that could not be cached due to 
conflicts between URL bindings */
+    private final Map<String, List<String>> pathConflicts = new 
HashMap<String, List<String>>();
+
     /** Holds the set of paths that are cached, sorted from longest to 
shortest */
-    private final Map<String, UrlBinding> prefixCache = new TreeMap<String, 
UrlBinding>(
+    private final Map<String, Set<UrlBinding>> prefixCache = new 
TreeMap<String, Set<UrlBinding>>(
             new Comparator<String>() {
                 public int compare(String a, String b) {
                     int cmp = b.length() - a.length();
@@ -125,15 +134,77 @@
         UrlBinding prototype = pathCache.get(uri);
         if (prototype != null)
             return prototype;
+        else if (pathConflicts.containsKey(uri))
+            throw new UrlBindingConflictException(uri, pathConflicts.get(uri));
 
-        // Then look for a matching prefix
-        for (Entry<String, UrlBinding> entry : prefixCache.entrySet()) {
+        // Get all the bindings whose prefix matches the URI
+        Set<UrlBinding> candidates = null;
+        for (Entry<String, Set<UrlBinding>> entry : prefixCache.entrySet()) {
             if (uri.startsWith(entry.getKey())) {
-                prototype = entry.getValue();
+                candidates = entry.getValue();
                 break;
             }
         }
 
+        // If none matched or exactly one matched then return now
+        if (candidates == null) {
+            log.debug("No URL binding matches ", uri);
+            return null;
+        }
+        else if (candidates.size() == 1) {
+            log.debug("Matched ", uri, " to ", candidates);
+            return candidates.iterator().next();
+        }
+
+        // Now find the one that matches deepest into the URI with the fewest 
components
+        int maxIndex = 0, minComponents = Integer.MAX_VALUE;
+        List<String> conflicts = null;
+        for (UrlBinding binding : candidates) {
+            int idx = binding.getPath().length();
+            List<Object> components = binding.getComponents();
+            int componentCount = components.size();
+
+            for (Object component : components) {
+                if (!(component instanceof String))
+                    continue;
+
+                int at = uri.indexOf((String) component, idx);
+                if (at >= 0) {
+                    idx = at + ((String) component).length();
+                }
+                else {
+                    break;
+                }
+            }
+
+            if (idx == maxIndex) {
+                if (componentCount < minComponents) {
+                    conflicts = null;
+                    minComponents = componentCount;
+                    prototype = binding;
+                }
+                else if (componentCount == minComponents) {
+                    if (conflicts == null) {
+                        conflicts = new ArrayList<String>(candidates.size());
+                        conflicts.add(prototype.toString());
+                    }
+                    conflicts.add(binding.toString());
+                    prototype = null;
+                }
+            }
+            else if (idx > maxIndex) {
+                conflicts = null;
+                minComponents = componentCount;
+                prototype = binding;
+                maxIndex = idx;
+            }
+        }
+
+        log.debug("Matched @", maxIndex, " ", uri, " to ", prototype);
+        if (prototype == null) {
+            throw new UrlBindingConflictException(uri, conflicts);
+        }
+
         return prototype;
     }
 
@@ -265,17 +336,58 @@
      * @param binding the URL binding
      */
     public void addBinding(Class<? extends ActionBean> beanType, UrlBinding 
binding) {
-        pathCache.put(binding.getPath(), binding);
+        cachePath(binding.getPath(), binding);
         if (binding.getSuffix() != null)
-                       pathCache.put(binding.getPath() + binding.getSuffix(), 
binding);
-        prefixCache.put(binding.getPath() + '/', binding);
+            cachePath(binding.getPath() + binding.getSuffix(), binding);
+        if (!binding.toString().equals(binding.getPath()))
+            cachePath(binding.toString(), binding);
+
+        Set<UrlBinding> bindings = prefixCache.get(binding.getPath() + '/');
+        if (bindings == null) {
+            bindings = new TreeSet<UrlBinding>(new Comparator<UrlBinding>() {
+                public int compare(UrlBinding o1, UrlBinding o2) {
+                    int cmp = o1.getComponents().size() - 
o2.getComponents().size();
+                    if (cmp == 0)
+                        cmp = o1.toString().compareTo(o2.toString());
+                    return cmp;
+                }
+            });
+        }
+        bindings.add(binding);
+
+        prefixCache.put(binding.getPath() + '/', bindings);
         List<Object> components = binding.getComponents();
         if (components != null && !components.isEmpty() && components.get(0) 
instanceof String)
-            prefixCache.put(binding.getPath() + components.get(0), binding);
+            prefixCache.put(binding.getPath() + components.get(0), bindings);
+
         classCache.put(beanType, binding);
     }
 
     /**
+     * Map a path directly to a binding. If the path matches more than one 
binding, then a warning
+     * will be logged indicating such a condition, and the path will not be 
cached for any binding.
+     * 
+     * @param path The path to cache
+     * @param binding The binding to which the path should map
+     */
+    protected void cachePath(String path, UrlBinding binding) {
+        if (pathCache.containsKey(path)) {
+            UrlBinding conflict = pathCache.put(path, null);
+            List<String> list = pathConflicts.get(path);
+            if (list == null) {
+                list = new ArrayList<String>();
+                list.add(conflict.toString());
+                pathConflicts.put(path, list);
+            }
+            log.warn("The path ", path, " for binding ", binding, " conflicts 
with ", list);
+            list.add(binding.toString());
+        }
+        else {
+            pathCache.put(path, binding);
+        }
+    }
+
+    /**
      * Parse a binding pattern and create a [EMAIL PROTECTED] UrlBinding} 
object.
      * 
      * @param beanType the [EMAIL PROTECTED] ActionBean} type whose binding is 
to be parsed

Added: 
trunk/stripes/src/net/sourceforge/stripes/exception/UrlBindingConflictException.java
===================================================================
--- 
trunk/stripes/src/net/sourceforge/stripes/exception/UrlBindingConflictException.java
                                (rev 0)
+++ 
trunk/stripes/src/net/sourceforge/stripes/exception/UrlBindingConflictException.java
        2008-10-24 19:47:45 UTC (rev 993)
@@ -0,0 +1,130 @@
+/* Copyright 2008 Ben Gunter
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sourceforge.stripes.exception;
+
+import java.util.Collection;
+
+import net.sourceforge.stripes.action.ActionBean;
+
+/**
+ * <p>
+ * This exception indicates that a URL does not contain enough information to 
map it to a single
+ * [EMAIL PROTECTED] ActionBean} class. In some cases, a URL may match more 
than one URL binding.
+ * </p>
+ * <p>
+ * For example, suppose you have two ActionBeans with the URL bindings 
<code>/foo/{param}/bar</code>
+ * and <code>/foo/{param}/blah</code>. The paths [EMAIL PROTECTED] /foo} and 
[EMAIL PROTECTED] /foo/X} -- while legal,
+ * since any number of parameters or literals may be omitted from the end of a 
clean URL -- match
+ * both of the URL bindings. Since Stripes cannot determine from the URL the 
ActionBean to which to
+ * dispatch the request, it throws this exception to indicate the conflict.
+ * </p>
+ * 
+ * @author Ben Gunter
+ * @since Stripes 1.5.1
+ */
+public class UrlBindingConflictException extends StripesRuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    /** Generate the message to pass to the superclass constructor */
+    protected static String getMessage(Class<? extends ActionBean> 
targetClass, String path,
+            Collection<String> matches) {
+        return (targetClass == null ? "" : "Failure generating URL for " + 
targetClass + ". ")
+                + "The path " + path + " cannot be mapped to a single 
ActionBean because multiple "
+                + "URL bindings match it. The matching URL bindings are " + 
matches + ". If you "
+                + "generated the URL using the Stripes tag library 
(stripes:link, stripes:url, "
+                + "stripes:form, etc.) then you must embed enough 
stripes:param tags within the "
+                + "parent tag to produce a URL that maps to exactly one of the 
indicated matches. "
+                + "If you generated the URL by some other means, then you must 
embed enough "
+                + "information in the URL to achieve the same end.";
+    }
+
+    private String path;
+    private Collection<String> matches;
+    private Class<? extends ActionBean> targetClass;
+
+    /**
+     * New exception indicating that the [EMAIL PROTECTED] path} does not map 
to a single ActionBean because it
+     * potentially matches all the URL bindings in the [EMAIL PROTECTED] 
matches} collection.
+     * 
+     * @param message An informative message about what went wrong
+     * @param path The offending path
+     * @param matches A collection of all the potentially matching URL bindings
+     */
+    public UrlBindingConflictException(String message, Class<? extends 
ActionBean> targetClass,
+            String path, Collection<String> matches) {
+        super(message);
+        this.targetClass = targetClass;
+        this.path = path;
+        this.matches = matches;
+    }
+
+    /**
+     * New exception indicating that the [EMAIL PROTECTED] path} does not map 
to a single ActionBean because it
+     * potentially matches all the URL bindings in the [EMAIL PROTECTED] 
matches} collection.
+     * 
+     * @param message An informative message about what went wrong
+     * @param path The offending path
+     * @param matches A collection of all the potentially matching URL bindings
+     */
+    public UrlBindingConflictException(Class<? extends ActionBean> 
targetClass, String path,
+            Collection<String> matches) {
+        this(getMessage(targetClass, path, matches), targetClass, path, 
matches);
+    }
+
+    /**
+     * New exception indicating that the [EMAIL PROTECTED] path} does not map 
to a single ActionBean because it
+     * potentially matches all the URL bindings in the [EMAIL PROTECTED] 
matches} collection.
+     * 
+     * @param message An informative message about what went wrong
+     * @param path The offending path
+     * @param matches A collection of all the potentially matching URL bindings
+     */
+    public UrlBindingConflictException(String message, String path, 
Collection<String> matches) {
+        this(message, null, path, matches);
+    }
+
+    /**
+     * New exception indicating that the [EMAIL PROTECTED] path} does not map 
to a single ActionBean because it
+     * potentially matches all the URL bindings in the [EMAIL PROTECTED] 
matches} collection.
+     * 
+     * @param message An informative message about what went wrong
+     * @param path The offending path
+     * @param matches A collection of all the potentially matching URL bindings
+     */
+    public UrlBindingConflictException(String path, Collection<String> 
matches) {
+        this(getMessage(null, path, matches), path, matches);
+    }
+
+    /** Get the path that failed to map to a single ActionBean */
+    public String getPath() {
+        return path;
+    }
+
+    /** Get all the URL bindings on existing ActionBeans that match the path */
+    public Collection<String> getMatches() {
+        return matches;
+    }
+
+    /**
+     * Get the [EMAIL PROTECTED] ActionBean} class for which a URL was being 
generated when this exception was
+     * thrown. If the exception occurred while dispatching a request, then 
this property will be
+     * null since the path cannot be associated with an ActionBean class. 
However, if it is thrown
+     * while generating a URL that is intended to point to an ActionBean, then 
this property will
+     * indicate the class that was being targeted.
+     */
+    public Class<? extends ActionBean> getTargetClass() {
+        return targetClass;
+    }
+}

Modified: trunk/stripes/src/net/sourceforge/stripes/util/UrlBuilder.java
===================================================================
--- trunk/stripes/src/net/sourceforge/stripes/util/UrlBuilder.java      
2008-10-22 19:18:47 UTC (rev 992)
+++ trunk/stripes/src/net/sourceforge/stripes/util/UrlBuilder.java      
2008-10-24 19:47:45 UTC (rev 993)
@@ -30,6 +30,7 @@
 import net.sourceforge.stripes.controller.UrlBindingFactory;
 import net.sourceforge.stripes.controller.UrlBindingParameter;
 import net.sourceforge.stripes.exception.StripesRuntimeException;
+import net.sourceforge.stripes.exception.UrlBindingConflictException;
 import net.sourceforge.stripes.format.Formatter;
 import net.sourceforge.stripes.format.FormatterFactory;
 import net.sourceforge.stripes.validation.ValidationMetadata;
@@ -448,6 +449,11 @@
             return baseUrl;
         }
 
+        // if we have a parameterized binding then we need to trim it down to 
the path
+        if (baseUrl.equals(binding.toString())) {
+            baseUrl = binding.getPath();
+        }
+
         // if any extra path info is present then do not add URI parameters
         if (binding.getPath().length() < baseUrl.length()) {
             return baseUrl;
@@ -520,6 +526,20 @@
             buf.append(binding.getSuffix());
         }
 
-        return buf.toString();
+        // Test the URL to make sure it won't throw an exception when Stripes 
tries to dispatch it
+        String url = buf.toString();
+        try {
+            
StripesFilter.getConfiguration().getActionResolver().getActionBeanType(url);
+        }
+        catch (UrlBindingConflictException e) {
+            if (binding != null) {
+                UrlBindingConflictException tmp = new 
UrlBindingConflictException(binding
+                        .getBeanType(), e.getPath(), e.getMatches());
+                tmp.setStackTrace(e.getStackTrace());
+                e = tmp;
+            }
+            throw e;
+        }
+        return url;
     }
 }


This was sent by the SourceForge.net collaborative development platform, the 
world's largest Open Source development site.

-------------------------------------------------------------------------
This SF.Net email is sponsored by the Moblin Your Move Developer's challenge
Build the coolest Linux based applications with Moblin SDK & win great prizes
Grand prize is a trip for two to an Open Source event anywhere in the world
http://moblin-contest.org/redirect.php?banner_id=100&url=/
_______________________________________________
Stripes-development mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/stripes-development

Reply via email to