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 = "&";
}
@@ -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