Revision: 574
          http://svn.sourceforge.net/stripes/?rev=574&view=rev
Author:   bengunter
Date:     2007-06-12 22:20:46 -0700 (Tue, 12 Jun 2007)

Log Message:
-----------
Initial version of clean URL support. See STS-262 for more information.

Modified Paths:
--------------
    trunk/stripes/src/net/sourceforge/stripes/action/OnwardResolution.java
    
trunk/stripes/src/net/sourceforge/stripes/controller/AnnotatedClassActionResolver.java
    trunk/stripes/src/net/sourceforge/stripes/controller/StripesConstants.java
    trunk/stripes/src/net/sourceforge/stripes/controller/StripesFilter.java
    trunk/stripes/src/net/sourceforge/stripes/tag/LinkTagSupport.java
    trunk/stripes/src/net/sourceforge/stripes/util/UrlBuilder.java

Added Paths:
-----------
    trunk/stripes/src/net/sourceforge/stripes/controller/UrlBinding.java
    trunk/stripes/src/net/sourceforge/stripes/controller/UrlBindingFactory.java
    
trunk/stripes/src/net/sourceforge/stripes/controller/UrlBindingParameter.java

Modified: trunk/stripes/src/net/sourceforge/stripes/action/OnwardResolution.java
===================================================================
--- trunk/stripes/src/net/sourceforge/stripes/action/OnwardResolution.java      
2007-06-11 13:48:14 UTC (rev 573)
+++ trunk/stripes/src/net/sourceforge/stripes/action/OnwardResolution.java      
2007-06-13 05:20:46 UTC (rev 574)
@@ -36,6 +36,7 @@
  * @author Tim Fennell
  */
 public abstract class OnwardResolution<T extends OnwardResolution<T>> {
+    private Class<? extends ActionBean> beanType;
     private String path;
     private Map<String,Object> parameters = new HashMap<String,Object>();
 
@@ -55,6 +56,7 @@
      */
     public OnwardResolution(Class<? extends ActionBean> beanType) {
         
this(StripesFilter.getConfiguration().getActionResolver().getUrlBinding(beanType));
+        this.beanType = beanType;
     }
 
     /**
@@ -86,9 +88,12 @@
      */
     @Override
     public String toString() {
-        return getClass().getSimpleName() + "{" +
-            "path='" + path + "'" +
-            "}";
+        if (beanType == null) {
+            return getClass().getSimpleName() + "{path='" + path + "'}";
+        }
+        else {
+            return getClass().getSimpleName() + "{beanType='" + 
beanType.getName() + "'}";
+        }
     }
 
     /**
@@ -172,7 +177,13 @@
      * @param locale the locale to be used by [EMAIL PROTECTED] Formatter}s 
when formatting parameters
      */
     public String getUrl(Locale locale) {
-        UrlBuilder builder = new UrlBuilder(locale, path, false);
+        UrlBuilder builder;
+        if (beanType == null) {
+            builder = new UrlBuilder(locale, path, false);
+        }
+        else {
+            builder = new UrlBuilder(locale, beanType, false);
+        }
         builder.addParameters(this.parameters);
         return builder.toString();
     }

Modified: 
trunk/stripes/src/net/sourceforge/stripes/controller/AnnotatedClassActionResolver.java
===================================================================
--- 
trunk/stripes/src/net/sourceforge/stripes/controller/AnnotatedClassActionResolver.java
      2007-06-11 13:48:14 UTC (rev 573)
+++ 
trunk/stripes/src/net/sourceforge/stripes/controller/AnnotatedClassActionResolver.java
      2007-06-13 05:20:46 UTC (rev 574)
@@ -19,7 +19,6 @@
 import net.sourceforge.stripes.action.DefaultHandler;
 import net.sourceforge.stripes.action.HandlesEvent;
 import net.sourceforge.stripes.action.SessionScope;
-import net.sourceforge.stripes.action.UrlBinding;
 import net.sourceforge.stripes.config.BootstrapPropertyResolver;
 import net.sourceforge.stripes.config.Configuration;
 import net.sourceforge.stripes.exception.StripesRuntimeException;
@@ -77,10 +76,6 @@
     /** Handle to the configuration. */
     private Configuration configuration;
 
-    /** Map of form names to Class objects representing subclasses of 
ActionBean. */
-    private Map<String,Class<? extends ActionBean>> formBeans =
-            new HashMap<String,Class<? extends ActionBean>>();
-
     /**
      * Map used to resolve the methods handling events within form beans. Maps 
the class
      * representing a subclass of ActionBean to a Map of event names to Method 
objects.
@@ -117,11 +112,17 @@
      */
     protected void addActionBean(Class<? extends ActionBean> clazz) {
         String binding = getUrlBinding(clazz);
+        if (binding == null)
+            return;
 
+        // make sure mapping exists in cache
+        UrlBinding proto = 
UrlBindingFactory.getInstance().getBindingPrototype(clazz);
+        if (proto == null) {
+            UrlBindingFactory.getInstance().addBinding(clazz, new 
UrlBinding(clazz, binding));
+        }
+
         // Only process the class if it's properly annotated
         if (binding != null) {
-            this.formBeans.put(binding, clazz);
-
             // Construct the mapping of event->method for the class
             Map<String, Method> classMappings = new HashMap<String, Method>();
             processMethods(clazz, classMappings);
@@ -130,13 +131,13 @@
             this.eventMappings.put(clazz, classMappings);
 
             // Print out the event mappings nicely
-            for (Map.Entry<String,Method> entry : classMappings.entrySet()) {
-                String event   = entry.getKey();
+            for (Map.Entry<String, Method> entry : classMappings.entrySet()) {
+                String event = entry.getKey();
                 Method handler = entry.getValue();
                 boolean isDefault = DEFAULT_HANDLER_KEY.equals(event);
 
                 log.debug("Bound: ", clazz.getSimpleName(), ".", 
handler.getName(), "() ==> ",
-                          binding, isDefault ? "" : "?" + event);
+                        binding, isDefault ? "" : "?" + event);
             }
         }
     }
@@ -152,23 +153,8 @@
      *         supplied cannot be mapped to an ActionBean.
      */
     public String getUrlBindingFromPath(String path) {
-        String binding = null;
-        while (binding == null && path != null) {
-            if (this.formBeans.containsKey(path)) {
-                binding = path;
-            }
-            else {
-                int lastSlash = path.lastIndexOf("/");
-                if (lastSlash > 0) {
-                    path = path.substring(0, lastSlash);
-                }
-                else {
-                    path = null;
-                }
-            }
-        }
-
-        return binding;
+        UrlBinding mapping = 
UrlBindingFactory.getInstance().getBindingPrototype(path);
+        return mapping == null ? null : mapping.getPath();
     }
 
     /**
@@ -181,13 +167,8 @@
      * @return the UrlBinding or null if none can be determined
      */
     public String getUrlBinding(Class<? extends ActionBean> clazz) {
-        UrlBinding binding = clazz.getAnnotation(UrlBinding.class);
-        if (binding != null) {
-            return binding.value();
-        }
-        else {
-            return null;
-        }
+        UrlBinding mapping = 
UrlBindingFactory.getInstance().getBindingPrototype(clazz);
+        return mapping == null ? null : mapping.getPath();
     }
 
     /**
@@ -247,7 +228,8 @@
      *         is made using the path specified or null if no ActionBean 
matches.
      */
     public Class<? extends ActionBean> getActionBeanType(String path) {
-        return this.formBeans.get(getUrlBindingFromPath(path));
+        UrlBinding binding = 
UrlBindingFactory.getInstance().getBindingPrototype(path);
+        return binding == null ? null : binding.getBeanType();
     }
 
     /**
@@ -259,9 +241,15 @@
      * @return the name of the form to be used for this request
      */
     public ActionBean getActionBean(ActionBeanContext context) throws 
StripesServletException {
-        // Defensively construct the URL that was used to hit the dispatcher
-        HttpServletRequest request = context.getRequest();
-        String path = getRequestedPath(request);
+        String path;
+        StripesRequestWrapper request = (StripesRequestWrapper) 
context.getRequest();
+        UrlBinding binding = 
UrlBindingFactory.getInstance().getBindingPrototype(request);
+        if (binding == null) {
+            path = getRequestedPath(request);
+        }
+        else {
+            path = binding.getPath();
+        }
 
         ActionBean bean = getActionBean(context, path);
         request.setAttribute(RESOLVED_ACTION, getUrlBindingFromPath(path));
@@ -306,9 +294,7 @@
      */
     public ActionBean getActionBean(ActionBeanContext context, String path)
             throws StripesServletException {
-
-        String urlBinding = getUrlBindingFromPath(path);
-        Class<? extends ActionBean> beanClass = this.formBeans.get(urlBinding);
+        Class<? extends ActionBean> beanClass = getActionBeanType(path);
         ActionBean bean;
 
         if (beanClass == null) {
@@ -316,7 +302,7 @@
                     "Could not locate an ActionBean that is bound to the URL 
[" + path +
                             "]. Commons reasons for this include mis-matched 
URLs and forgetting " +
                             "to implement ActionBean in your class. Registered 
ActionBeans are: " +
-                            this.formBeans);
+                            UrlBindingFactory.getInstance());
         }
 
         try {

Modified: 
trunk/stripes/src/net/sourceforge/stripes/controller/StripesConstants.java
===================================================================
--- trunk/stripes/src/net/sourceforge/stripes/controller/StripesConstants.java  
2007-06-11 13:48:14 UTC (rev 573)
+++ trunk/stripes/src/net/sourceforge/stripes/controller/StripesConstants.java  
2007-06-13 05:20:46 UTC (rev 574)
@@ -100,6 +100,12 @@
     String REQ_ATTR_EVENT_NAME = "__stripes_event_name";
 
     /**
+     * The name of the request attribute that is set when a request is 
rewritten and forwarded so
+     * that the request will not be rewritten again.
+     */
+    String REQ_ATTR_BYPASS_REWRITE = "__stripes_bypass_rewrite";
+
+    /**
      * Request attribute key defined by the servlet spec for storing the 
included servlet
      * path when processing a server side include.
      */

Modified: 
trunk/stripes/src/net/sourceforge/stripes/controller/StripesFilter.java
===================================================================
--- trunk/stripes/src/net/sourceforge/stripes/controller/StripesFilter.java     
2007-06-11 13:48:14 UTC (rev 573)
+++ trunk/stripes/src/net/sourceforge/stripes/controller/StripesFilter.java     
2007-06-13 05:20:46 UTC (rev 574)
@@ -32,6 +32,7 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
+import java.net.URLEncoder;
 import java.util.Locale;
 import java.util.Map;
 
@@ -196,7 +197,9 @@
 
             // Execute the rest of the chain
             flashInbound(request);
-            filterChain.doFilter(request, servletResponse);
+            if (!rewriteRequest(httpRequest, httpResponse)) {
+                filterChain.doFilter(request, servletResponse);
+            }
         }
         catch (Throwable t) {
             this.configuration.getExceptionHandler().handle(t, httpRequest, 
httpResponse);
@@ -266,6 +269,62 @@
         }
     }
 
+    /**
+     * Uses the [EMAIL PROTECTED] UrlBindingFactory} (if any) to determine if 
this URL should be rewritten. If
+     * necessary, the request will be rewritten and forwarded.
+     * 
+     * @param request servlet request
+     * @param response servlet response
+     * @return True if the request was forwarded. False if it was not.
+     * @throws ServletException
+     * @throws IOException
+     */
+    protected boolean rewriteRequest(HttpServletRequest request, 
HttpServletResponse response)
+            throws ServletException, IOException {
+        // if bypass flag not set, then check for rewrite
+        boolean rewrite = false;
+        UrlBinding binding = null;
+        if (request.getAttribute(StripesConstants.REQ_ATTR_BYPASS_REWRITE) == 
null) {
+            binding = UrlBindingFactory.getInstance().getBinding(request);
+            rewrite = binding != null && binding.getParameters().size() > 0;
+        }
+
+        // if all that worked out to be true, then the URL needs to be 
rewritten
+        if (rewrite) {
+            // get request URI sans the context path
+            StringBuilder url;
+            int contextLength = request.getContextPath().length();
+            if (contextLength > 1)
+                url = new 
StringBuilder(request.getRequestURI().substring(contextLength));
+            else
+                url = new StringBuilder(request.getRequestURI());
+
+            // append the binding parameters to the query string
+            char separator = '?';
+            for (UrlBindingParameter p : binding.getParameters()) {
+                String name = p.getName();
+                if (name != null) {
+                    String value = p.getValue();
+                    if (value != null) {
+                        name = URLEncoder.encode(name, "UTF-8");
+                        value = URLEncoder.encode(value, "UTF-8");
+                        
url.append(separator).append(name).append('=').append(value);
+                        separator = '&';
+                    }
+                }
+            }
+
+            // forward to rewritten request
+            request.setAttribute(StripesConstants.REQ_ATTR_BYPASS_REWRITE, 
Boolean.TRUE);
+            request.getRequestDispatcher(url.toString()).forward(request, 
response);
+        }
+        else {
+            request.removeAttribute(StripesConstants.REQ_ATTR_BYPASS_REWRITE);
+        }
+
+        return rewrite;
+    }
+
     /** Does nothing. */
     public void destroy() {
         // Do nothing

Added: trunk/stripes/src/net/sourceforge/stripes/controller/UrlBinding.java
===================================================================
--- trunk/stripes/src/net/sourceforge/stripes/controller/UrlBinding.java        
                        (rev 0)
+++ trunk/stripes/src/net/sourceforge/stripes/controller/UrlBinding.java        
2007-06-13 05:20:46 UTC (rev 574)
@@ -0,0 +1,116 @@
+/* Copyright 2007 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.controller;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import net.sourceforge.stripes.action.ActionBean;
+
+/**
+ * Represents a URL binding as declared by a [EMAIL PROTECTED] 
net.sourceforge.stripes.action.UrlBinding}
+ * annotation on an [EMAIL PROTECTED] ActionBean} class.
+ * 
+ * @author Ben Gunter
+ * @since Stripes 1.5
+ */
+public class UrlBinding {
+    protected Class<? extends ActionBean> beanType;
+    protected String path;
+    protected List<Object> components;
+    protected List<UrlBindingParameter> parameters;
+
+    /**
+     * Create a new instance with all its members. Collections passed in will 
be made immutable.
+     * 
+     * @param beanType the [EMAIL PROTECTED] ActionBean} class to which this 
binding applies
+     * @param path the path to which the action is mapped
+     * @param components list of literal strings that separate the parameters
+     */
+    public UrlBinding(Class<? extends ActionBean> beanType, String path, 
List<Object> components) {
+        this.beanType = beanType;
+        this.path = path;
+        if (components != null)
+            this.components = Collections.unmodifiableList(components);
+
+        this.parameters = new 
ArrayList<UrlBindingParameter>(this.components.size());
+        for (Object component : components) {
+            if (component instanceof UrlBindingParameter) {
+                this.parameters.add((UrlBindingParameter) component);
+            }
+        }
+    }
+
+    /**
+     * Create a new instance that takes no parameters.
+     * 
+     * @param beanType
+     * @param path
+     */
+    public UrlBinding(Class<? extends ActionBean> beanType, String path) {
+        this.beanType = beanType;
+        this.path = path;
+        this.components = Collections.emptyList();
+    }
+
+    /**
+     * Get the [EMAIL PROTECTED] ActionBean} class to which this binding 
applies.
+     */
+    public Class<? extends ActionBean> getBeanType() {
+        return beanType;
+    }
+
+    /**
+     * Get the list of components that comprise this binding. The components 
are returned in the
+     * order in which they appear in the binding definition.
+     */
+    public List<Object> getComponents() {
+        return components;
+    }
+
+    /**
+     * Get the list of parameters for this binding.
+     */
+    public List<UrlBindingParameter> getParameters() {
+        return parameters;
+    }
+
+    /**
+     * Get the path for this binding. The path is the string of literal 
characters in the pattern up
+     * to the first parameter definition.
+     */
+    public String getPath() {
+        return path;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buf = new StringBuilder(getPath());
+        for (Object component : getComponents()) {
+            if (component instanceof String) {
+                buf.append(component);
+            }
+            else if (component instanceof UrlBindingParameter) {
+                UrlBindingParameter parameter = (UrlBindingParameter) 
component;
+                buf.append('{').append(parameter.getName());
+                if (parameter.getDefaultValue() != null)
+                    buf.append('=').append(parameter.getDefaultValue());
+                buf.append('}');
+            }
+        }
+        return buf.toString();
+    }
+}

Added: 
trunk/stripes/src/net/sourceforge/stripes/controller/UrlBindingFactory.java
===================================================================
--- trunk/stripes/src/net/sourceforge/stripes/controller/UrlBindingFactory.java 
                        (rev 0)
+++ trunk/stripes/src/net/sourceforge/stripes/controller/UrlBindingFactory.java 
2007-06-13 05:20:46 UTC (rev 574)
@@ -0,0 +1,389 @@
+/**
+ * $Id$
+ * $Name$
+ *
+ * Created on Jun 8, 2007 at 10:18:03 AM by Ben Gunter.
+ * 
+ * Copyright 2007 Cpons.com, Inc. All rights reserved.
+ */
+package net.sourceforge.stripes.controller;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.servlet.http.HttpServletRequest;
+
+import net.sourceforge.stripes.action.ActionBean;
+import net.sourceforge.stripes.exception.StripesRuntimeException;
+import net.sourceforge.stripes.util.bean.ParseException;
+
+/**
+ * <p>
+ * Provides access to [EMAIL PROTECTED] UrlBinding} objects. Bindings are used 
in two contexts:
+ * <ul>
+ * <li><strong>As a prototype:</strong> Binding prototypes provide static 
information about the
+ * binding, such as the URI path, string literals, parameter names and default 
values. However, the
+ * parameters associated with a prototype do not have a value since they are 
not evaluated against a
+ * live request.</li>
+ * <li><strong>"Live":</strong> Bindings that have been evaluated against a 
live servlet request
+ * or request URI are exactly like their prototypes except that the parameter 
values associated with
+ * them contain the values (if any) that were extracted from the URI.</li>
+ * </ul>
+ * </p>
+ * 
+ * @author Ben Gunter
+ * @since Stripes 1.5
+ * @see UrlBinding
+ * @see UrlBindingParameter
+ */
+public class UrlBindingFactory {
+    /** Singleton instance */
+    private static final UrlBindingFactory instance = new UrlBindingFactory();
+
+    /**
+     * Get the singleton instance.
+     * 
+     * @return an instance of this class
+     */
+    public static UrlBindingFactory getInstance() {
+        return instance;
+    }
+
+    /** Maps [EMAIL PROTECTED] ActionBean} classes to [EMAIL PROTECTED] 
UrlBinding}s */
+    private final Map<Class<? extends ActionBean>, UrlBinding> classCache = 
new HashMap<Class<? extends ActionBean>, UrlBinding>();
+
+    /** Maps simple paths to [EMAIL PROTECTED] UrlBinding}s */
+    private final Map<String, UrlBinding> pathCache = new HashMap<String, 
UrlBinding>();
+
+    /** Holds the set of paths that are cached, sorted from longest to 
shortest */
+    private final Set<String> pathSet = new TreeSet<String>(new 
Comparator<String>() {
+        public int compare(String a, String b) {
+            int cmp = b.length() - a.length();
+            return cmp == 0 ? a.compareTo(b) : cmp;
+        }
+    });
+
+    /** Don't want the constructor to be public */
+    protected UrlBindingFactory() {
+        // do nothing
+    }
+
+    /**
+     * Get the [EMAIL PROTECTED] UrlBinding} prototype associated with the 
given [EMAIL PROTECTED] ActionBean} type. This
+     * method may return null if no binding is associated with the given type.
+     * 
+     * @param type a class that implements [EMAIL PROTECTED] ActionBean}
+     * @return a binding object if one is defined or null if not
+     */
+    public UrlBinding getBindingPrototype(Class<? extends ActionBean> type) {
+        UrlBinding binding = classCache.get(type);
+        if (binding != null)
+            return binding;
+
+        binding = parseUrlBinding(type);
+        if (binding != null)
+            addBinding(type, binding);
+        return binding;
+    }
+
+    /**
+     * Examines a URI (as might be returned by [EMAIL PROTECTED] 
HttpServletRequest#getRequestURI()}) and
+     * returns the associated binding prototype, if any. No attempt is made to 
extract parameter
+     * values from the URI. This is intended as a fast means to get static 
information associated
+     * with a given request URI.
+     * 
+     * @param uri a request URI
+     * @return a binding prototype, or null if the URI does not match
+     */
+    public UrlBinding getBindingPrototype(String uri) {
+        // look up as a path first
+        UrlBinding prototype = pathCache.get(uri);
+        if (prototype != null)
+            return prototype;
+
+        // if not found, then find longest matching path
+        for (String path : pathSet) {
+            if (uri.startsWith(path)) {
+                prototype = pathCache.get(path);
+                break;
+            }
+        }
+
+        return prototype;
+    }
+
+    /**
+     * Examines a servlet request and returns the associated binding 
prototype, if any. No attempt
+     * is made to extract parameter values from the URI. This is intended as a 
fast means to get
+     * static information associated with a given request.
+     * 
+     * @param request a servlet request
+     * @return a binding prototype, or null if the request URI does not match
+     */
+    public UrlBinding getBindingPrototype(HttpServletRequest request) {
+        return getBindingPrototype(request.getRequestURI());
+    }
+
+    /**
+     * Examines a URI (as might be returned by [EMAIL PROTECTED] 
HttpServletRequest#getRequestURI()}) and
+     * returns the associated binding, if any. Parameters will be extracted 
from the URI, and the
+     * [EMAIL PROTECTED] UrlBindingParameter} objects returned by [EMAIL 
PROTECTED] UrlBinding#getParameters()} will
+     * contain the values that are present in the URI.
+     * 
+     * @param uri a request URI
+     * @return a binding prototype, or null if the URI does not match
+     */
+    public UrlBinding getBinding(String uri) {
+        UrlBinding prototype = getBindingPrototype(uri);
+        if (prototype == null)
+            return null;
+
+        // extract the request parameters and add to new binding object
+        ArrayList<Object> components = new 
ArrayList<Object>(prototype.getComponents().size());
+        int index = prototype.getPath().length();
+        UrlBindingParameter current = null;
+        String value = null;
+        Iterator<Object> iter = prototype.getComponents().iterator();
+        while (index < uri.length() && iter.hasNext()) {
+            Object component = iter.next();
+            if (component instanceof String) {
+                // extract the parameter value from the URI
+                String literal = (String) component;
+                int end = uri.indexOf(literal, index);
+                if (end >= 0) {
+                    value = uri.substring(index, end);
+                    index = end + literal.length();
+                }
+                else {
+                    value = uri.substring(index);
+                    index = uri.length();
+                }
+
+                // add to the binding
+                if (current != null && value != null && value.length() > 0) {
+                    components.add(new UrlBindingParameter(current, value));
+                    components.add(component);
+                    current = null;
+                    value = null;
+                }
+            }
+            else if (component instanceof UrlBindingParameter) {
+                current = (UrlBindingParameter) component;
+            }
+        }
+
+        // if component iterator ended before end of string, then grab 
remainder of string
+        if (index < uri.length()) {
+            value = uri.substring(index);
+        }
+
+        // parameter was last component in list
+        if (current != null && value != null && value.length() > 0) {
+            components.add(new UrlBindingParameter(current, value));
+        }
+
+        // ensure all components are included so default parameter values are 
available
+        while (iter.hasNext()) {
+            Object component = iter.next();
+            if (component instanceof UrlBindingParameter) {
+                components.add(new UrlBindingParameter((UrlBindingParameter) 
component));
+            }
+            else {
+                components.add(component);
+            }
+        }
+
+        return new UrlBinding(prototype.getBeanType(), prototype.getPath(), 
components);
+    }
+
+    /**
+     * Examines a servlet request and returns the associated binding, if any. 
Parameters will be
+     * extracted from the request, and the [EMAIL PROTECTED] 
UrlBindingParameter} objects returned by
+     * [EMAIL PROTECTED] UrlBinding#getParameters()} will contain the values 
that are present in the request.
+     * 
+     * @param request a servlet request
+     * @return if the request matches a defined binding, then this method 
should return that
+     *         binding. Otherwise, this method should return null.
+     */
+    public UrlBinding getBinding(HttpServletRequest request) {
+        try {
+            // get character encoding
+            String charset = request.getCharacterEncoding();
+            if (charset == null)
+                charset = "UTF-8";
+
+            // trim and decode the request URI
+            String uri = request.getRequestURI();
+            String contextPath = request.getContextPath();
+            if (contextPath != null && contextPath.length() > 0)
+                uri = uri.substring(contextPath.length());
+            uri = URLDecoder.decode(uri, charset);
+
+            // look up the binding by the URI
+            return getBinding(uri);
+        }
+        catch (UnsupportedEncodingException e) {
+            throw new StripesRuntimeException(e);
+        }
+    }
+
+    /**
+     * Map an [EMAIL PROTECTED] ActionBean} to a URL.
+     * 
+     * @param beanType the [EMAIL PROTECTED] ActionBean} class
+     * @param binding the URL binding
+     */
+    public void addBinding(Class<? extends ActionBean> beanType, UrlBinding 
binding) {
+        pathCache.put(binding.getPath(), binding);
+        pathSet.add(binding.getPath());
+        classCache.put(beanType, 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
+     * @return a [EMAIL PROTECTED] UrlBinding}
+     * @throws ParseException if the pattern cannot be parsed
+     */
+    protected static UrlBinding parseUrlBinding(Class<? extends ActionBean> 
beanType) {
+        // check that class is annotated
+        net.sourceforge.stripes.action.UrlBinding annotation = beanType
+                
.getAnnotation(net.sourceforge.stripes.action.UrlBinding.class);
+        if (annotation == null)
+            return null;
+
+        // check that value is not null or empty
+        String pattern = annotation.value();
+        if (pattern == null || pattern.length() < 1)
+            return null;
+
+        // parse the pattern
+        String path = null;
+        List<Object> components = new ArrayList<Object>();
+        int braceLevel = 0;
+        boolean escape = false;
+        char[] chars = pattern.toCharArray();
+        StringBuilder buf = new StringBuilder(pattern.length());
+        char c = 0;
+        for (int i = 0; i < chars.length; i++) {
+            c = chars[i];
+            if (!escape) {
+                switch (c) {
+                case '{':
+                    ++braceLevel;
+                    if (braceLevel == 1) {
+                        if (path == null) {
+                            // extract trailing non-alphanum chars as a 
literal to trim the path
+                            int end = buf.length() - 1;
+                            while (end >= 0 && 
!Character.isJavaIdentifierPart(buf.charAt(end)))
+                                --end;
+                            if (end < 0) {
+                                path = buf.toString();
+                            }
+                            else {
+                                ++end;
+                                path = buf.substring(0, end);
+                                components.add(buf.substring(end));
+                            }
+                        }
+                        else {
+                            components.add(buf.toString());
+                        }
+                        buf.setLength(0);
+                        continue;
+                    }
+                    break;
+                case '}':
+                    if (braceLevel > 0) {
+                        --braceLevel;
+                    }
+                    if (braceLevel == 0) {
+                        
components.add(parseUrlBindingParameter(buf.toString()));
+                        buf.setLength(0);
+                        continue;
+                    }
+                    break;
+                case '\\':
+                    escape = true;
+                    continue;
+                }
+            }
+
+            // append the char
+            buf.append(c);
+            escape = false;
+        }
+
+        // handle whatever is left
+        if (buf.length() > 0) {
+            if (escape)
+                throw new ParseException("Expression must not end with escape 
character", pattern);
+            else if (braceLevel > 0)
+                throw new ParseException("Unterminated left brace ('{') in 
expression", pattern);
+            else if (path == null)
+                path = buf.toString();
+            else if (c == '}')
+                components.add(parseUrlBindingParameter(buf.toString()));
+            else
+                components.add(buf.toString());
+        }
+
+        return new UrlBinding(beanType, path, components);
+    }
+
+    /**
+     * Parses a parameter specification into name and default value and 
returns a
+     * [EMAIL PROTECTED] UrlBindingParameter} with the corresponding name and 
default value properties set
+     * accordingly.
+     * 
+     * @param string the parameter string
+     * @return a parameter object
+     */
+    protected static UrlBindingParameter parseUrlBindingParameter(String 
string) {
+        char[] chars = string.toCharArray();
+        char c = 0;
+        boolean escape = false;
+        StringBuilder name = new StringBuilder();
+        StringBuilder defaultValue = new StringBuilder();
+        StringBuilder current = name;
+        for (int i = 0; i < chars.length; i++) {
+            c = chars[i];
+            if (!escape) {
+                switch (c) {
+                case '\\':
+                    escape = true;
+                    continue;
+                case '=':
+                    current = defaultValue;
+                    continue;
+                }
+            }
+
+            current.append(c);
+            escape = false;
+        }
+
+        String dflt = defaultValue.length() < 1 ? null : 
defaultValue.toString();
+        return new UrlBindingParameter(name.toString(), null, dflt) {
+            @Override
+            public String getValue() {
+                throw new UnsupportedOperationException(
+                        "getValue() is not implemented for URL parameter 
prototypes");
+            }
+        };
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(classCache);
+    }
+}

Added: 
trunk/stripes/src/net/sourceforge/stripes/controller/UrlBindingParameter.java
===================================================================
--- 
trunk/stripes/src/net/sourceforge/stripes/controller/UrlBindingParameter.java   
                            (rev 0)
+++ 
trunk/stripes/src/net/sourceforge/stripes/controller/UrlBindingParameter.java   
    2007-06-13 05:20:46 UTC (rev 574)
@@ -0,0 +1,123 @@
+/* Copyright 2007 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.controller;
+
+/**
+ * A parameter to a clean URL.
+ * 
+ * @author Ben Gunter
+ * @since Stripes 1.5
+ */
+public class UrlBindingParameter {
+    protected String name;
+    protected String value;
+    protected String defaultValue;
+
+    /**
+     * Create a new [EMAIL PROTECTED] UrlBindingParameter} with the given name 
and value. The
+     * [EMAIL PROTECTED] #defaultValue} will be null.
+     * 
+     * @param name parameter name
+     * @param value parameter value
+     */
+    public UrlBindingParameter(String name, String value) {
+        this.name = name;
+        this.value = value;
+    }
+
+    /**
+     * Create a new [EMAIL PROTECTED] UrlBindingParameter} with the given 
name, value and default value.
+     * 
+     * @param name parameter name
+     * @param value parameter value
+     * @param defaultValue default value to use if value is null
+     */
+    public UrlBindingParameter(String name, String value, String defaultValue) 
{
+        super();
+        this.name = name;
+        this.value = value;
+        this.defaultValue = defaultValue;
+    }
+
+    /**
+     * Make an exact copy of the given [EMAIL PROTECTED] UrlBindingParameter}.
+     * 
+     * @param prototype a parameter
+     */
+    public UrlBindingParameter(UrlBindingParameter prototype) {
+        this(prototype.name, prototype.value, prototype.defaultValue);
+    }
+
+    /**
+     * Make a copy of the given [EMAIL PROTECTED] UrlBindingParameter} except 
that the parameter's value will
+     * be set to <code>value</code>.
+     * 
+     * @param prototype a parameter
+     * @param value the new parameter value
+     */
+    public UrlBindingParameter(UrlBindingParameter prototype, String value) {
+        this(prototype.name, value, prototype.defaultValue);
+    }
+
+    /**
+     * Get the parameter's default value. This value will be returned by 
[EMAIL PROTECTED] #getValue()} if the
+     * parameter's actual value is null. The default value may be null.
+     * 
+     * @return the default value
+     */
+    public String getDefaultValue() {
+        return defaultValue;
+    }
+
+    /**
+     * Get the parameter name.
+     * 
+     * @return parameter name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Return the parameter value that was extracted from a URI. If the value 
is null, then the
+     * default value will be returned.
+     * 
+     * @return parameter value
+     */
+    public String getValue() {
+        return value == null ? defaultValue : value;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof UrlBindingParameter))
+            return false;
+        UrlBindingParameter that = (UrlBindingParameter) o;
+        return this.value == that.value || ((this.value != null) && 
this.value.equals(that.value));
+    }
+
+    @Override
+    public int hashCode() {
+        return getValue().hashCode();
+    }
+
+    @Override
+    public String toString() {
+        if (defaultValue == null)
+            return name;
+        else
+            return name + "=" + defaultValue;
+    }
+}

Modified: trunk/stripes/src/net/sourceforge/stripes/tag/LinkTagSupport.java
===================================================================
--- trunk/stripes/src/net/sourceforge/stripes/tag/LinkTagSupport.java   
2007-06-11 13:48:14 UTC (rev 573)
+++ trunk/stripes/src/net/sourceforge/stripes/tag/LinkTagSupport.java   
2007-06-13 05:20:46 UTC (rev 574)
@@ -16,6 +16,7 @@
 
 import net.sourceforge.stripes.exception.StripesJspException;
 import net.sourceforge.stripes.util.UrlBuilder;
+import net.sourceforge.stripes.action.ActionBean;
 import net.sourceforge.stripes.controller.StripesConstants;
 
 import javax.servlet.http.HttpServletRequest;
@@ -153,18 +154,18 @@
         HttpServletRequest request = (HttpServletRequest) 
getPageContext().getRequest();
         HttpServletResponse response = (HttpServletResponse) 
getPageContext().getResponse();
 
-        String base = getPreferredBaseUrl();
-        String contextPath = request.getContextPath();
-
-        // Append the context path, but only if the user didn't already
-        if (base.startsWith("/") && !"/".equals(contextPath)
-                && !base.contains(contextPath + "/")) {
-            base = contextPath + base;
+        UrlBuilder builder;
+        Class<? extends ActionBean> beanclass = 
getActionBeanType(this.beanclass);
+        if (beanclass == null) {
+            String base = getPreferredBaseUrl();
+            builder = new UrlBuilder(pageContext.getRequest().getLocale(), 
base, false);
         }
+        else {
+            builder = new UrlBuilder(pageContext.getRequest().getLocale(), 
beanclass, false);
+        }
 
         // Add all the parameters and reset the href attribute; pass to false 
here because
         // the HtmlTagSupport will HtmlEncode the ampersands for us
-        UrlBuilder builder = new 
UrlBuilder(pageContext.getRequest().getLocale(), base, false);
         if (this.event != null) {
             builder.addParameter(this.event);
         }
@@ -173,6 +174,13 @@
         }
         builder.addParameters(this.parameters);
 
-        return response.encodeURL(builder.toString());
+        // Prepend the context path, but only if the user didn't already
+        String url = builder.toString();
+        String contextPath = request.getContextPath();
+        if (!"/".equals(contextPath) && !url.startsWith(contextPath + "/")) {
+            url = contextPath + url;
+        }
+
+        return response.encodeURL(url);
     }
 }

Modified: trunk/stripes/src/net/sourceforge/stripes/util/UrlBuilder.java
===================================================================
--- trunk/stripes/src/net/sourceforge/stripes/util/UrlBuilder.java      
2007-06-11 13:48:14 UTC (rev 573)
+++ trunk/stripes/src/net/sourceforge/stripes/util/UrlBuilder.java      
2007-06-13 05:20:46 UTC (rev 574)
@@ -16,12 +16,19 @@
 
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
+import net.sourceforge.stripes.action.ActionBean;
 import net.sourceforge.stripes.config.Configuration;
 import net.sourceforge.stripes.controller.StripesFilter;
+import net.sourceforge.stripes.controller.UrlBinding;
+import net.sourceforge.stripes.controller.UrlBindingFactory;
+import net.sourceforge.stripes.controller.UrlBindingParameter;
 import net.sourceforge.stripes.exception.StripesRuntimeException;
 import net.sourceforge.stripes.format.Formatter;
 import net.sourceforge.stripes.format.FormatterFactory;
@@ -41,11 +48,27 @@
  * @since Stripes 1.1.2
  */
 public class UrlBuilder {
+    /**
+     * Holds the name and value of a parameter to be appended to the URL.
+     */
+    private static class Parameter {
+        String name;
+        Object value;
+        boolean skip;
+
+        Parameter(String name, Object value) {
+            this.name = name;
+            this.value = value;
+        }
+    }
+
+    private Class<? extends ActionBean> beanType;
+    private String path;
+    private String anchor;
     private Locale locale;
-    private StringBuilder url = new StringBuilder(256);
-    boolean seenQuestionMark = false;
     private String parameterSeparator;
-    private String anchor;
+    private List<Parameter> parameters = new ArrayList<Parameter>();
+    private String url;
 
     /**
      * Constructs a UrlBuilder with the path to a resource. Parameters can be 
added
@@ -76,19 +99,45 @@
      *        tag), false if for some other purpose.
      */
     public UrlBuilder(Locale locale, String url, boolean isForPage) {
-        this.locale = locale;
+        this(locale, isForPage);
         if (url != null) {
             // Check to see if there is an embedded anchor, and strip it out 
for later
             int index = url.indexOf('#');
             if (index != -1) {
-                this.anchor = url.substring(index+1);
+                if (index < url.length() - 1) {
+                    this.anchor = url.substring(index + 1);
+                }
                 url = url.substring(0, index);
             }
 
-            this.url.append(url);
-            this.seenQuestionMark = this.url.indexOf("?") != -1;
+            this.path = url;
         }
+    }
 
+    /**
+     * Constructs a UrlBuilder that references an [EMAIL PROTECTED] 
ActionBean}. Parameters can be added later
+     * using addParameter(). If the link is to be used in a page then the 
ampersand character
+     * usually used to separate parameters will be escaped using the XML 
entity for ampersand.
+     * 
+     * @param locale the locale to use when formatting parameters with a 
[EMAIL PROTECTED] Formatter}
+     * @param beanType [EMAIL PROTECTED] ActionBean} class for which the URL 
will be built
+     * @param isForPage true if the URL is to be embedded in a page (e.g. in 
an anchor of img tag),
+     *            false if for some other purpose.
+     */
+    public UrlBuilder(Locale locale, Class<? extends ActionBean> beanType, 
boolean isForPage) {
+        this(locale, isForPage);
+        this.beanType = beanType;
+    }
+
+    /**
+     * Sets the locale and sets the parameter separator based on the value of 
<code>isForPage</code>.
+     * 
+     * @param locale the locale to use when formatting parameters with a 
[EMAIL PROTECTED] Formatter}
+     * @param isForPage true if the URL is to be embedded in a page (e.g. in 
an anchor of img tag),
+     *            false if for some other purpose.
+     */
+    protected UrlBuilder(Locale locale, boolean isForPage) {
+        this.locale = locale;
         if (isForPage) {
             this.parameterSeparator = "&amp;";
         }
@@ -127,41 +176,24 @@
      * @param values one or more values for the parameter supplied
      */
     public void addParameter(String name, Object... values) {
-        try {
-            // If values is null or empty, then simply sub in a single empty 
string
-            if (values == null || values.length == 0) {
-                values = Literal.array("");
-            }
+        // If values is null or empty, then simply sub in a single empty string
+        if (values == null || values.length == 0) {
+            values = Literal.array("");
+        }
 
-            for (Object v : values) {
-                // Special case: recurse for nested collections and arrays!
-                if (v instanceof Collection) {
-                    addParameter(name, ((Collection) v).toArray());
-                }
-                else if (v != null && v.getClass().isArray()) {
-                    addParameter(name, CollectionUtil.asObjectArray(v));
-                }
-                else {
-                    // Figure out whether we already have params or not
-                    if (!this.seenQuestionMark) {
-                        this.url.append('?');
-                        this.seenQuestionMark = true;
-                    }
-                    else {
-                        this.url.append(this.parameterSeparator);
-                    }
-
-                    this.url.append(name);
-                    this.url.append('=');
-                    if (v != null) {
-                        this.url.append( URLEncoder.encode(format(v), "UTF-8") 
);
-                    }
-                }
+        for (Object v : values) {
+            // Special case: recurse for nested collections and arrays!
+            if (v instanceof Collection) {
+                addParameter(name, ((Collection) v).toArray());
             }
+            else if (v != null && v.getClass().isArray()) {
+                addParameter(name, CollectionUtil.asObjectArray(v));
+            }
+            else {
+                parameters.add(new Parameter(name, v));
+                url = null;
+            }
         }
-        catch (UnsupportedEncodingException uee) {
-            throw new StripesRuntimeException("Unsupported encoding?  UTF-8?  
That's unpossible.");
-        }
     }
 
     /**
@@ -224,11 +256,14 @@
      */
     @Override
     public String toString() {
-        if (this.anchor != null && !"".equals(this.anchor)) {
-            return this.url.toString() + "#" + this.anchor;
+        if (url == null) {
+            url = build();
         }
+        if (this.anchor != null && this.anchor.length() > 0) {
+            return url + "#" + this.anchor;
+        }
         else {
-            return this.url.toString();
+            return url;
         }
     }
 
@@ -275,4 +310,99 @@
 
         return factory.getFormatter(value.getClass(), locale, null, null);
     }
+
+    /**
+     * Build and return the URL
+     */
+    protected String build() {
+        try {
+            StringBuilder buffer = new StringBuilder(256);
+            buffer.append(getBaseURL());
+            boolean seenQuestionMark = buffer.indexOf("?") != -1;
+            for (Parameter pair : parameters) {
+                if (pair.skip)
+                    continue;
+
+                // Figure out whether we already have params or not
+                if (!seenQuestionMark) {
+                    buffer.append('?');
+                    seenQuestionMark = true;
+                }
+                else {
+                    buffer.append(getParameterSeparator());
+                }
+                buffer.append(pair.name);
+                buffer.append('=');
+                if (pair.value != null) {
+                    buffer.append(URLEncoder.encode(format(pair.value), 
"UTF-8"));
+                }
+            }
+            return buffer.toString();
+        }
+        catch (UnsupportedEncodingException uee) {
+            throw new StripesRuntimeException("Unsupported encoding?  UTF-8?  
That's unpossible.");
+        }
+    }
+
+    /**
+     * Get the base URL (without a query string). If an [EMAIL PROTECTED] 
ActionBean} class was passed to
+     * [EMAIL PROTECTED] #UrlBuilder(Locale, Class, boolean)}, then this 
method will return the URL binding
+     * that is mapped to that class, including any URI parameters that are 
available. Otherwise, it
+     * returns the URL string with which this object was initialized.
+     * 
+     * @return the base URL, without a query string
+     * @see #UrlBuilder(Locale, Class, boolean)
+     * @see #UrlBuilder(Locale, String, boolean)
+     */
+    protected String getBaseURL() {
+        if (beanType == null)
+            return path;
+
+        UrlBinding binding = 
UrlBindingFactory.getInstance().getBindingPrototype(beanType);
+        if (binding == null) {
+            return 
StripesFilter.getConfiguration().getActionResolver().getUrlBinding(beanType);
+        }
+
+        Map<String, Parameter> map = new HashMap<String, Parameter>();
+        for (Parameter p : parameters) {
+            p.skip = false;
+            if (!map.containsKey(p.name))
+                map.put(p.name, p);
+        }
+
+        StringBuilder buf = new StringBuilder(256);
+        buf.append(binding.getPath());
+
+        String nextLiteral = null;
+        for (Object component : binding.getComponents()) {
+            if (component instanceof String) {
+                nextLiteral = (String) component;
+            }
+            else if (component instanceof UrlBindingParameter) {
+                UrlBindingParameter parameter = (UrlBindingParameter) 
component;
+                boolean ok = false;
+                if (map.containsKey(parameter.getName())) {
+                    Parameter assigned = map.get(parameter.getName());
+                    String value = format(assigned.value);
+                    if (value != null && value.length() > 0) {
+                        if (nextLiteral != null) {
+                            buf.append(nextLiteral);
+                        }
+
+                        buf.append(value);
+                        assigned.skip = true;
+                        ok = true;
+                    }
+                }
+                nextLiteral = null;
+                if (!ok)
+                    break;
+            }
+        }
+        if (nextLiteral != null) {
+            buf.append(nextLiteral);
+        }
+
+        return buf.toString();
+    }
 }


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 DB2 Express
Download DB2 Express C - the FREE version of DB2 express and take
control of your XML. No limits. Just data. Click to get it now.
http://sourceforge.net/powerbar/db2/
_______________________________________________
Stripes-development mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/stripes-development

Reply via email to