Author: mrdon Date: Sat Oct 20 07:48:27 2007 New Revision: 586735 URL: http://svn.apache.org/viewvc?rev=586735&view=rev Log: Redesigned rest plugin to fit in better with Struts 2 apps and work with the codebehind plugin
Added: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerSelector.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultRestInfo.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ResourceClasspathPackageProvider.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestInfo.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/ContentTypeHandler.java struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java Removed: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/BasicRestful.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionProxy.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/Restful.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/MimeTypeHandler.java struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/RestActionInvoicationTest.java Modified: struts/sandbox/trunk/struts2-rest-plugin/pom.xml struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionInvocation.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionMapper.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionProxyFactory.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/HtmlHandler.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamJsonHandler.java struts/sandbox/trunk/struts2-rest-plugin/src/main/resources/struts-plugin.xml Modified: struts/sandbox/trunk/struts2-rest-plugin/pom.xml URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/pom.xml?rev=586735&r1=586734&r2=586735&view=diff ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/pom.xml (original) +++ struts/sandbox/trunk/struts2-rest-plugin/pom.xml Sat Oct 20 07:48:27 2007 @@ -2,18 +2,21 @@ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> - + <parent> + <groupId>org.apache.struts</groupId> + <artifactId>struts2-plugins</artifactId> + <version>2.1.0-SNAPSHOT</version> + </parent> <groupId>org.apache.struts</groupId> <artifactId>struts2-rest-plugin</artifactId> - <version>1.0-SNAPSHOT</version> + <version>2.1.0-SNAPSHOT</version> <name>Struts 2 Plugin</name> <dependencies> - <dependency> <groupId>org.apache.struts</groupId> - <artifactId>struts2-core</artifactId> - <version>2.0.8</version> + <artifactId>struts2-codebehind-plugin</artifactId> + <version>${pom.version}</version> </dependency> <dependency> <groupId>com.thoughtworks.xstream</groupId> @@ -42,28 +45,7 @@ <version>3.8.1</version> <scope>test</scope> </dependency> - <dependency> - <groupId>mockobjects</groupId> - <artifactId>mockobjects-core</artifactId> - <version>0.09</version> - <scope>test</scope> - </dependency> </dependencies> - - <build> - <defaultGoal>install</defaultGoal> - <pluginManagement> - <plugins> - <plugin> - <artifactId>maven-compiler-plugin</artifactId> - <configuration> - <source>1.5</source> - <target>1.5</target> - </configuration> - </plugin> - </plugins> - </pluginManagement> - </build> </project> Added: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerSelector.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerSelector.java?rev=586735&view=auto ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerSelector.java (added) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeHandlerSelector.java Sat Oct 20 07:48:27 2007 @@ -0,0 +1,49 @@ +package org.apache.struts2.rest; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.struts2.rest.handler.ContentTypeHandler; + +import com.opensymphony.xwork2.inject.Container; +import com.opensymphony.xwork2.inject.Inject; + +public class ContentTypeHandlerSelector { + + private Map<String,ContentTypeHandler> handlers = new HashMap<String,ContentTypeHandler>(); + private String defaultHandlerName; + + @Inject("struts.rest.defaultHandlerName") + public void setDefaultHandlerName(String name) { + this.defaultHandlerName = name; + } + + @Inject + public void setContainer(Container container) { + Set<String> names = container.getInstanceNames(ContentTypeHandler.class); + for (String name : names) { + ContentTypeHandler handler = container.getInstance(ContentTypeHandler.class, name); + this.handlers.put(handler.getExtension(), handler); + } + } + + public ContentTypeHandler getHandlerForRequest(HttpServletRequest req) { + String extension = findExtension(req.getRequestURI()); + if (extension == null) { + extension = defaultHandlerName; + } + return handlers.get(extension); + } + + protected String findExtension(String url) { + int dotPos = url.lastIndexOf('.'); + int slashPos = url.lastIndexOf('/'); + if (dotPos > slashPos && dotPos > -1) { + return url.substring(dotPos+1); + } + return null; + } +} Added: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java?rev=586735&view=auto ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java (added) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java Sat Oct 20 07:48:27 2007 @@ -0,0 +1,41 @@ +package org.apache.struts2.rest; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.struts2.ServletActionContext; +import org.apache.struts2.rest.handler.ContentTypeHandler; + +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.ModelDriven; +import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.interceptor.Interceptor; + +public class ContentTypeInterceptor implements Interceptor { + + ContentTypeHandlerSelector selector; + + @Inject + public void setContentTypeHandlerSelector(ContentTypeHandlerSelector sel) { + this.selector = sel; + } + + public void destroy() {} + + public void init() {} + + public String intercept(ActionInvocation invocation) throws Exception { + HttpServletRequest request = ServletActionContext.getRequest(); + ContentTypeHandler handler = selector.getHandlerForRequest(request); + + Object target = invocation.getAction(); + if (target instanceof ModelDriven) { + target = ((ModelDriven)target).getModel(); + } + + if (request.getContentLength() > 0) { + handler.toObject(request.getInputStream(), target); + } + return invocation.invoke(); + } + +} Added: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultRestInfo.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultRestInfo.java?rev=586735&view=auto ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultRestInfo.java (added) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultRestInfo.java Sat Oct 20 07:48:27 2007 @@ -0,0 +1,88 @@ +package org.apache.struts2.rest; + +import static javax.servlet.http.HttpServletResponse.*; + +import java.util.Date; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class DefaultRestInfo implements RestInfo { + String resultCode; + int status = SC_OK; + Object etag; + Object locationId; + String location; + boolean disableCaching; + Date lastModified; + + public DefaultRestInfo renderResult(String code) { + this.resultCode = code; + return this; + } + + public DefaultRestInfo withStatus(int code) { + this.status = code; + return this; + } + + public DefaultRestInfo withETag(Object etag) { + this.etag = etag; + return this; + } + + public DefaultRestInfo setLocationId(Object id) { + this.locationId = id; + return this; + } + + public DefaultRestInfo setLocation(String loc) { + this.location = loc; + return this; + } + + public DefaultRestInfo lastModified(Date date) { + this.lastModified = date; + return this; + } + + public DefaultRestInfo disableCaching() { + this.disableCaching = true; + return this; + } + + /* (non-Javadoc) + * @see org.apache.struts2.rest.RestInfo#apply(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object) + */ + public String apply(HttpServletRequest request, HttpServletResponse response, Object target) { + response.setStatus(status); + if (disableCaching) { + response.setHeader("Cache-Control", "no-cache"); + } else if (lastModified != null) { + response.setDateHeader("LastModified", lastModified.getTime()); + } else { + if (etag == null) { + etag = String.valueOf(target.hashCode()); + } + response.setHeader("ETag", etag.toString()); + } + if (locationId != null) { + String url = request.getRequestURL().toString(); + int lastSlash = url.lastIndexOf("/"); + int lastDot = url.lastIndexOf("."); + if (lastDot > lastSlash && lastDot > -1) { + url = url.substring(0, lastDot)+locationId+url.substring(lastDot); + } else { + url += locationId; + } + response.setHeader("Location", url); + } else if (location != null) { + response.setHeader("Location", location); + } + return resultCode; + } + + + + +} Added: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ResourceClasspathPackageProvider.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ResourceClasspathPackageProvider.java?rev=586735&view=auto ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ResourceClasspathPackageProvider.java (added) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ResourceClasspathPackageProvider.java Sat Oct 20 07:48:27 2007 @@ -0,0 +1,27 @@ +package org.apache.struts2.rest; + +import org.apache.struts2.config.ClasspathPackageProvider; + +import com.opensymphony.xwork2.util.ResolverUtil.ClassTest; + +/** + * Checks for actions ending in Resource indicating a Rest resource + */ +public class ResourceClasspathPackageProvider extends ClasspathPackageProvider { + + @Override + protected ClassTest createActionClassTest() { + return new ClassTest() { + // Match Action implementations and classes ending with "Resource" + public boolean matches(Class type) { + return (type.getSimpleName().endsWith("Resource")); + } + }; + } + + @Override + protected String getClassSuffix() { + return "Resource"; + } + +} Modified: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionInvocation.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionInvocation.java?rev=586735&r1=586734&r2=586735&view=diff ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionInvocation.java (original) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionInvocation.java Sat Oct 20 07:48:27 2007 @@ -25,6 +25,7 @@ import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.ActionProxy; import com.opensymphony.xwork2.DefaultActionInvocation; +import com.opensymphony.xwork2.ModelDriven; import com.opensymphony.xwork2.ObjectFactory; import com.opensymphony.xwork2.Result; import com.opensymphony.xwork2.UnknownHandler; @@ -32,20 +33,21 @@ import com.opensymphony.xwork2.config.entities.ActionConfig; import com.opensymphony.xwork2.config.entities.InterceptorMapping; import com.opensymphony.xwork2.config.entities.ResultConfig; +import com.opensymphony.xwork2.conversion.impl.XWorkConverter; import com.opensymphony.xwork2.inject.Container; import com.opensymphony.xwork2.inject.Inject; import com.opensymphony.xwork2.interceptor.PreResultListener; import com.opensymphony.xwork2.util.ValueStack; import com.opensymphony.xwork2.util.ValueStackFactory; -import com.opensymphony.xwork2.util.XWorkConverter; +import com.opensymphony.xwork2.util.logging.LoggerFactory; import com.opensymphony.xwork2.util.profiling.UtilTimerStack; +import com.opensymphony.xwork2.util.logging.Logger; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.apache.struts2.ServletActionContext; -import org.apache.struts2.rest.handler.MimeTypeHandler; +import org.apache.struts2.rest.handler.ContentTypeHandler; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -75,52 +77,24 @@ */ public class RestActionInvocation extends DefaultActionInvocation { - private final Log LOG = LogFactory.getLog(RestActionInvocation.class); + private static final long serialVersionUID = 3485701178946428716L; + + private static final Logger LOG = LoggerFactory.getLogger(RestActionInvocation.class); - private XWorkConverter converter; - private Map<String,MimeTypeHandler> handlers = new HashMap<String,MimeTypeHandler>(); - private String defaultHandlerName; - - protected RestActionInvocation(ObjectFactory objectFactory, UnknownHandler handler, ActionProxy proxy, Map extraContext, boolean pushAction, ActionEventListener actionEventListener) throws Exception { - super(objectFactory, handler, proxy, extraContext, pushAction, - actionEventListener); - } + private ContentTypeHandlerSelector handlerSelector; - protected RestActionInvocation(ObjectFactory objectFactory, UnknownHandler handler, ActionProxy proxy, Map extraContext, boolean pushAction) throws Exception { - super(objectFactory, handler, proxy, extraContext, pushAction); + protected RestActionInvocation(Map extraContext, boolean pushAction) throws Exception { + super(extraContext, pushAction); } - protected RestActionInvocation(ObjectFactory objectFactory, UnknownHandler handler, ActionProxy proxy, Map extraContext) throws Exception { - super(objectFactory, handler, proxy, extraContext); - } - - public void setXWorkConverter(XWorkConverter conv) { - this.converter = conv; - } - - public void setDefaultHandlerName(String name) { - this.defaultHandlerName = name; + @Inject + public void setMimeTypeHandlerSelector(ContentTypeHandlerSelector sel) { + this.handlerSelector = sel; } - public void addMimeTypeHandler(String name, MimeTypeHandler handler) { - this.handlers.put(name, handler); - } - protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception { - - HttpServletRequest req = ServletActionContext.getRequest(); - String extension = findExtension(req.getRequestURI()); - if (extension == null) { - extension = defaultHandlerName; - } - System.out.println("extension:"+extension); - MimeTypeHandler handler = handlers.get(extension); - if (handler != null && req.getContentLength() > 0) { - invocationContext.getParameters().put("body", handler.toObject(req.getInputStream())); - } - String methodName = proxy.getMethod(); - + if (LOG.isDebugEnabled()) { LOG.debug("Executing action method = " + actionConfig.getMethodName()); } @@ -131,36 +105,37 @@ boolean methodCalled = false; Object methodResult = null; - MethodMatch methodMatch = null; + Method method = null; try { - methodMatch = findMethod(action, methodName, invocationContext.getParameters()); + method = getAction().getClass().getMethod(methodName, new Class[0]); } catch (NoSuchMethodException e) { // hmm -- OK, try doXxx instead try { String altMethodName = "do" + methodName.substring(0, 1).toUpperCase() + methodName.substring(1); - methodMatch = findMethod(action, altMethodName, invocationContext.getParameters()); + method = getAction().getClass().getMethod(altMethodName, new Class[0]); } catch (NoSuchMethodException e1) { - throw e; + // well, give the unknown handler a shot + if (unknownHandler != null) { + try { + methodResult = unknownHandler.handleUnknownActionMethod(action, methodName); + methodCalled = true; + } catch (NoSuchMethodException e2) { + // throw the original one + throw e; + } + } else { + throw e; + } } } - - if (!methodCalled) { - methodResult = callAction(action, methodMatch, invocationContext.getParameters()); - } - - if (methodResult instanceof Result) { - this.result = (Result) methodResult; - return null; - } else { - if (handler != null && methodResult != null) { - return handler.fromObject(methodResult, this); - } else { - // treat as normal result code - return (String) methodResult; - } + + if (!methodCalled) { + methodResult = method.invoke(action, new Object[0]); } + + return processResult(actionConfig, methodResult); } catch (NoSuchMethodException e) { - throw new IllegalArgumentException("The " + methodName + "() is not defined in action " + getAction().getClass() + "", e); + throw new IllegalArgumentException("The " + methodName + "() is not defined in action " + getAction().getClass() + ""); } catch (InvocationTargetException e) { // We try to return the source exception. Throwable t = e.getTargetException(); @@ -181,93 +156,41 @@ } } - protected Object callAction(Object action, MethodMatch methodMatch, Map reqParams) throws IllegalAccessException, InvocationTargetException { - Object methodResult; - Class[] argTypes = methodMatch.method.getParameterTypes(); - Object[] args = new Object[argTypes.length]; - - int x=0; - for (String name : methodMatch.params) { - args[x] = converter.convertValue(null, reqParams.get(name), argTypes[x]); - x++; - } - - methodResult = methodMatch.method.invoke(action, args); - return methodResult; - } - - protected MethodMatch findMethod(Object action, String actionName, Map<String,String> params) throws NoSuchMethodException { - - // Short cut so that only restful actions get the full method scan treatment - if (action.getClass().getAnnotation(Restful.class) == null && !(action instanceof BasicRestful)) { - return new MethodMatch(action.getClass().getMethod(actionName, new Class[0]), Collections.EMPTY_LIST); - } - Set<String> paramsInReq = params.keySet(); - - int max = -1; - Method method = null; - List<String> nameParams = null; - - for (Method m : action.getClass().getMethods()) { - String methodName = m.getName(); - if (methodName.startsWith(actionName)) { - int count = 0; - List<String> paramsInName = findParamsInName(methodName); - for (String paramName : paramsInName) { - if (paramsInReq.contains(paramName)) { - count++; - } else { - count = -1; - break; - } - } + protected String processResult(ActionConfig actionConfig, Object methodResult) throws IOException { + if (methodResult instanceof Result) { + this.explicitResult = (Result) methodResult; + return null; + } else if (methodResult != null) { + HttpServletRequest req = ServletActionContext.getRequest(); + HttpServletResponse res = ServletActionContext.getResponse(); + ContentTypeHandler handler = handlerSelector.getHandlerForRequest(req); + Object target = action; + if (target instanceof ModelDriven) { + target = ((ModelDriven)target).getModel(); + } + + if (methodResult instanceof RestInfo) { + RestInfo info = (RestInfo) methodResult; + resultCode = info.apply(req, res, target); + } else { + resultCode = (String) methodResult; + } + + String extCode = resultCode+"-"+handler.getExtension(); + if (actionConfig.getResults().get(extCode) != null) { + resultCode = extCode; + } else { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); - if (count > max) { - max = count; - method = m; - nameParams = paramsInName; + resultCode = handler.fromObject(target, resultCode, bout); + if (bout.size() > 0) { + res.setContentLength(bout.size()); + res.setContentType(handler.getContentType()); + res.getOutputStream().write(bout.toByteArray()); + res.getOutputStream().close(); } } } - - if (method == null) { - throw new NoSuchMethodException("Unable to find method for "+actionName+" with params "+params.keySet()); - } - return new MethodMatch(method, nameParams); + return resultCode; } - - protected List<String> findParamsInName(String name) { - List<String> list = new ArrayList<String>(); - int withPos = name.indexOf("With"); - if (withPos > -1) { - String[] params = name.substring(withPos+4).split("And"); - for (int x=0; x<params.length; x++) { - list.add(params[x].toLowerCase()); - } - } - return list; - } - - protected String findExtension(String url) { - int dotPos = url.lastIndexOf('.'); - int slashPos = url.lastIndexOf('/'); - if (dotPos > slashPos && dotPos > -1) { - return url.substring(dotPos+1); - } - return null; - } - - static class MethodMatch { - public Method method; - public List<String> params; - public MethodMatch(Method method, List<String> params) { - super(); - this.method = method; - this.params = params; - } - - - } - - } Modified: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionMapper.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionMapper.java?rev=586735&r1=586734&r2=586735&view=diff ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionMapper.java (original) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionMapper.java Sat Oct 20 07:48:27 2007 @@ -24,6 +24,8 @@ import com.opensymphony.xwork2.config.ConfigurationManager; import com.opensymphony.xwork2.config.entities.PackageConfig; import com.opensymphony.xwork2.inject.Inject; +import com.opensymphony.xwork2.util.logging.Logger; +import com.opensymphony.xwork2.util.logging.LoggerFactory; import javax.servlet.http.HttpServletRequest; @@ -34,8 +36,6 @@ import java.util.StringTokenizer; import java.net.URLDecoder; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.apache.struts2.RequestUtils; import org.apache.struts2.StrutsConstants; import org.apache.struts2.dispatcher.mapper.ActionMapping; @@ -80,12 +80,12 @@ * </p> * <ul> * <li><code>GET: /movie/ => method="index"</code></li> - * <li><code>GET: /movie/Thrillers => method="view", id="Thrillers"</code></li> - * <li><code>GET: /movie/Thrillers!edit => method="edit", id="Thrillers"</code></li> - * <li><code>GET: /movie/new => method="editNew"</code></li> + * <li><code>GET: /movie/Thrillers => method="show", id="Thrillers"</code></li> + * <li><code>GET: /movie/Thrillers;edit => method="input", id="Thrillers"</code></li> + * <li><code>GET: /movie/new => method="input"</code></li> * <li><code>POST: /movie/ => method="create"</code></li> * <li><code>PUT: /movie/Thrillers => method="update", id="Thrillers"</code></li> - * <li><code>DELETE: /movie/Thrillers => method="remove", id="Thrillers"</code></li> + * <li><code>DELETE: /movie/Thrillers => method="destroy", id="Thrillers"</code></li> * </ul> * <p> * To simulate the HTTP methods PUT and DELETE, since they aren't supported by HTML, @@ -102,41 +102,33 @@ */ public class RestActionMapper extends DefaultActionMapper { - protected static final Log LOG = LogFactory.getLog(RestActionMapper.class); - public static final String HTTP_METHOD_PARAM = "__http_method"; - private String idParameterName = null; - private boolean allowDynamicMethodCalls; - private List<String> extensions; + protected static final Logger LOG = LoggerFactory.getLogger(RestActionMapper.class); + public static final String HTTP_METHOD_PARAM = "_method"; + private String idParameterName = "id"; public RestActionMapper() { } - @Inject(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION) - public void setAllowDynamicMethodCalls(String allow) { - allowDynamicMethodCalls = "true".equals(allow); + public String getIdParameterName() { + return idParameterName; } - - @Inject(StrutsConstants.STRUTS_ACTION_EXTENSION) - public void setExtensions(String extensions) { - if (!"".equals(extensions)) { - this.extensions = Arrays.asList(extensions.split(",")); - } else { - this.extensions = null; - } + + @Inject(required=false,value=StrutsConstants.STRUTS_ID_PARAMETER_NAME) + public void setIdParameterName(String idParameterName) { + this.idParameterName = idParameterName; } - public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) { ActionMapping mapping = new ActionMapping(); String uri = getUri(request); - uri = dropExtension(uri); + uri = dropExtension(uri, mapping); if (uri == null) { return null; } - String fullName = parseNameAndNamespace(uri, mapping, configManager); + parseNameAndNamespace(uri, mapping, configManager); handleSpecialParameters(request, mapping); @@ -144,23 +136,27 @@ return null; } - if (allowDynamicMethodCalls) { - // handle "name!method" convention. - String name = mapping.getName(); - int exclamation = name.lastIndexOf("!"); - if (exclamation != -1) { - mapping.setName(name.substring(0, exclamation)); - mapping.setMethod(name.substring(exclamation + 1)); - } + // handle "name!method" convention. + String name = mapping.getName(); + int exclamation = name.lastIndexOf("!"); + if (exclamation != -1) { + mapping.setName(name.substring(0, exclamation)); + mapping.setMethod(name.substring(exclamation + 1)); } + String fullName = mapping.getName(); // Only try something if the action name is specified if (fullName != null && fullName.length() > 0) { int lastSlashPos = fullName.lastIndexOf('/'); + String id = null; + if (lastSlashPos > -1) { + id = fullName.substring(lastSlashPos+1); + } // If a method hasn't been explicitly named, try to guess using ReST-style patterns if (mapping.getMethod() == null) { + // Handle uris ending in '/' if (lastSlashPos == fullName.length() -1) { // Index e.g. foo/ @@ -172,75 +168,44 @@ mapping.setMethod("create"); } - } else if (lastSlashPos > -1) { - String id = fullName.substring(lastSlashPos+1); - + // Handle uris with an id at the end + } else if (id != null) { + + // Viewing the form to edit an item e.g. foo/1;edit + if (isGet(request) && id.endsWith(";edit")) { + id = id.substring(0, id.length() - ";edit".length()); + mapping.setMethod("input"); + // Viewing the form to create a new item e.g. foo/new - if (isGet(request) && "new".equals(id)) { - mapping.setMethod("editNew"); - - // Viewing an item e.g. foo/1 - } else if (isGet(request)) { - mapping.setMethod("view"); + } else if (isGet(request) && "new".equals(id)) { + mapping.setMethod("input"); // Removing an item e.g. foo/1 } else if (isDelete(request)) { - mapping.setMethod("remove"); + mapping.setMethod("destroy"); + + // Viewing an item e.g. foo/1 + } else if (isGet(request)) { + mapping.setMethod("show"); // Updating an item e.g. foo/1 } else if (isPut(request)) { mapping.setMethod("update"); } - - if (idParameterName != null) { - if (mapping.getParams() == null) { - mapping.setParams(new HashMap()); - } - mapping.getParams().put(idParameterName, id); - } - } - - if (idParameterName != null && lastSlashPos > -1) { - fullName = fullName.substring(0, lastSlashPos); } } - - // Try to determine parameters from the url before the action name - int actionSlashPos = fullName.lastIndexOf('/', lastSlashPos - 1); - if (actionSlashPos > 0 && actionSlashPos < lastSlashPos) { - String params = fullName.substring(0, actionSlashPos); - HashMap<String,String> parameters = new HashMap<String,String>(); - try { - StringTokenizer st = new StringTokenizer(params, "/"); - boolean isNameTok = true; - String paramName = null; - String paramValue; - - while (st.hasMoreTokens()) { - if (isNameTok) { - paramName = URLDecoder.decode(st.nextToken(), "UTF-8"); - isNameTok = false; - } else { - paramValue = URLDecoder.decode(st.nextToken(), "UTF-8"); - - if ((paramName != null) && (paramName.length() > 0)) { - parameters.put(paramName, paramValue); - } - - isNameTok = true; - } - } - if (parameters.size() > 0) { - if (mapping.getParams() == null) { - mapping.setParams(new HashMap()); - } - mapping.getParams().putAll(parameters); + + // cut off the id parameter, even if a method is specified + if (id != null) { + if (!"new".equals(id)) { + if (mapping.getParams() == null) { + mapping.setParams(new HashMap()); } - } catch (Exception e) { - LOG.warn(e); + mapping.getParams().put(idParameterName, new String[]{id}); } - fullName = fullName.substring(actionSlashPos+1); + fullName = fullName.substring(0, lastSlashPos); } + mapping.setName(fullName); } @@ -248,38 +213,14 @@ } /** - * Gets the uri from the request - * - * @param request - * The request - * @return The uri - */ - String getUri(HttpServletRequest request) { - // handle http dispatcher includes. - String uri = (String) request - .getAttribute("javax.servlet.include.servlet_path"); - if (uri != null) { - return uri; - } - - uri = RequestUtils.getServletPath(request); - if (uri != null && !"".equals(uri)) { - return uri; - } - - uri = request.getRequestURI(); - return uri.substring(request.getContextPath().length()); - } - - /** - * Parses the name and namespace from the uri + * Parses the name and namespace from the uri. Doesn't allow slashes in name. * * @param uri * The uri * @param mapping * The action mapping to populate */ - String parseNameAndNamespace(String uri, ActionMapping mapping, + protected void parseNameAndNamespace(String uri, ActionMapping mapping, ConfigurationManager configManager) { String namespace, name; int lastSlash = uri.lastIndexOf("/"); @@ -293,66 +234,21 @@ namespace = "/"; name = uri.substring(lastSlash + 1); } else { - // Try to find the namespace in those defined, defaulting to "" - Configuration config = configManager.getConfiguration(); - String prefix = uri.substring(0, lastSlash); - namespace = ""; - // Find the longest matching namespace, defaulting to the default - for (Iterator i = config.getPackageConfigs().values().iterator(); i - .hasNext();) { - String ns = ((PackageConfig) i.next()).getNamespace(); - if (ns != null && prefix.startsWith(ns) && (prefix.length() == ns.length() || prefix.charAt(ns.length()) == '/')) { - if (ns.length() > namespace.length()) { - namespace = ns; - } - } - } - - name = uri.substring(namespace.length() + 1); - } - String fullName = name; - - if (name != null) { - int pos = name.lastIndexOf('/'); - if (pos > -1 && pos < name.length() - 1) { - name = name.substring(pos + 1); + int secondToLastSlash = uri.lastIndexOf('/', lastSlash - 1); + if (secondToLastSlash == 0) { + namespace = "/"; + name = uri.substring(secondToLastSlash + 1); + } else if (secondToLastSlash > -1) { + namespace = uri.substring(0, secondToLastSlash); + name = uri.substring(secondToLastSlash + 1); + } else { + namespace = ""; + name = uri; } } mapping.setNamespace(namespace); mapping.setName(name); - return fullName; - } - - /** - * Drops the extension from the action name - * - * @param name - * The action name - * @return The action name without its extension - */ - String dropExtension(String name) { - if (name != null) { - int pos = name.lastIndexOf('.'); - if (pos > -1) { - return name.substring(0, name.lastIndexOf('.')); - } else { - return name; - } - - } - return null; - } - - /** - * Returns null if no extension is specified. - */ - String getDefaultExtension() { - if (extensions == null) { - return null; - } else { - return (String) extensions.get(0); - } } protected boolean isGet(HttpServletRequest request) { @@ -375,19 +271,8 @@ if ("delete".equalsIgnoreCase(request.getMethod())) { return true; } else { - return isPost(request) && "delete".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM)); + return "delete".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM)); } } - - public String getIdParameterName() { - return idParameterName; - } - - @Inject(required=false,value=StrutsConstants.STRUTS_ID_PARAMETER_NAME) - public void setIdParameterName(String idParameterName) { - this.idParameterName = idParameterName; - } - - } Modified: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionProxyFactory.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionProxyFactory.java?rev=586735&r1=586734&r2=586735&view=diff ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionProxyFactory.java (original) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionProxyFactory.java Sat Oct 20 07:48:27 2007 @@ -22,7 +22,9 @@ import java.util.Map; +import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.ActionProxy; +import com.opensymphony.xwork2.DefaultActionInvocation; import com.opensymphony.xwork2.DefaultActionProxyFactory; import com.opensymphony.xwork2.inject.Container; import com.opensymphony.xwork2.inject.Inject; @@ -34,10 +36,9 @@ public class RestActionProxyFactory extends DefaultActionProxyFactory { public ActionProxy createActionProxy(String namespace, String actionName, Map extraContext, boolean executeResult, boolean cleanupContext) throws Exception { - ActionProxy proxy = new RestActionProxy(namespace, actionName, extraContext, executeResult, cleanupContext); - container.inject(proxy); - proxy.prepare(); - return proxy; + ActionInvocation inv = new RestActionInvocation(extraContext, true); + container.inject(inv); + return createActionProxy(inv, namespace, actionName, extraContext, executeResult, cleanupContext); } } Added: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestInfo.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestInfo.java?rev=586735&view=auto ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestInfo.java (added) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestInfo.java Sat Oct 20 07:48:27 2007 @@ -0,0 +1,10 @@ +package org.apache.struts2.rest; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public interface RestInfo { + + String apply(HttpServletRequest request, + HttpServletResponse response, Object target); +} \ No newline at end of file Added: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/ContentTypeHandler.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/ContentTypeHandler.java?rev=586735&view=auto ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/ContentTypeHandler.java (added) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/ContentTypeHandler.java Sat Oct 20 07:48:27 2007 @@ -0,0 +1,37 @@ +/* + * $Id: Restful2ActionMapper.java 540819 2007-05-23 02:48:36Z mrdon $ + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.struts2.rest.handler; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import com.opensymphony.xwork2.ActionInvocation; + +public interface ContentTypeHandler { + void toObject(InputStream in, Object target); + + String fromObject(Object obj, String resultCode, OutputStream stream) throws IOException; + + String getContentType(); + + String getExtension(); +} Modified: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/HtmlHandler.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/HtmlHandler.java?rev=586735&r1=586734&r2=586735&view=diff ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/HtmlHandler.java (original) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/HtmlHandler.java Sat Oct 20 07:48:27 2007 @@ -22,20 +22,27 @@ import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.Collections; import com.opensymphony.xwork2.Action; import com.opensymphony.xwork2.ActionInvocation; -public class HtmlHandler implements MimeTypeHandler { +public class HtmlHandler implements ContentTypeHandler { - public String fromObject(Object obj, ActionInvocation inv) throws IOException { - inv.getStack().push(Collections.singletonMap("body", obj)); - return Action.SUCCESS; + public String fromObject(Object obj, String resultCode, OutputStream out) throws IOException { + return resultCode; } - public Object toObject(InputStream in) { - return null; + public void toObject(InputStream in, Object target) { + } + + public String getExtension() { + return "xhtml"; + } + + public String getContentType() { + return "application/xhtml+xml"; } } Modified: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java?rev=586735&r1=586734&r2=586735&view=diff ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java (original) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java Sat Oct 20 07:48:27 2007 @@ -32,22 +32,17 @@ import com.opensymphony.xwork2.ActionInvocation; import com.thoughtworks.xstream.XStream; -public class XStreamHandler implements MimeTypeHandler { +public class XStreamHandler implements ContentTypeHandler { - public String fromObject(Object obj, ActionInvocation inv) throws IOException { - HttpServletResponse response = ServletActionContext.getResponse(); + public String fromObject(Object obj, String resultCode, OutputStream out) throws IOException { XStream xstream = createXStream(); - ByteArrayOutputStream bout = new ByteArrayOutputStream(); - xstream.toXML(obj, bout); - response.getOutputStream().write(bout.toByteArray()); - response.setContentLength(bout.size()); - response.setContentType(getContentType()); + xstream.toXML(obj, out); return null; } - public Object toObject(InputStream in) { + public void toObject(InputStream in, Object target) { XStream xstream = createXStream(); - return xstream.fromXML(in); + xstream.fromXML(in, target); } protected XStream createXStream() { @@ -55,7 +50,10 @@ } public String getContentType() { - return "text/xml"; + return "application/xml"; } + public String getExtension() { + return "xml"; + } } Modified: struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamJsonHandler.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamJsonHandler.java?rev=586735&r1=586734&r2=586735&view=diff ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamJsonHandler.java (original) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamJsonHandler.java Sat Oct 20 07:48:27 2007 @@ -35,5 +35,7 @@ return "text/javascript"; } - + public String getExtension() { + return "json"; + } } Modified: struts/sandbox/trunk/struts2-rest-plugin/src/main/resources/struts-plugin.xml URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/main/resources/struts-plugin.xml?rev=586735&r1=586734&r2=586735&view=diff ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/main/resources/struts-plugin.xml (original) +++ struts/sandbox/trunk/struts2-rest-plugin/src/main/resources/struts-plugin.xml Sat Oct 20 07:48:27 2007 @@ -8,10 +8,14 @@ <bean type="com.opensymphony.xwork2.ActionProxyFactory" name="rest" class="org.apache.struts2.rest.RestActionProxyFactory" /> <bean type="org.apache.struts2.dispatcher.mapper.ActionMapper" name="rest" class="org.apache.struts2.rest.RestActionMapper" /> + + <bean type="com.opensymphony.xwork2.config.PackageProvider" name="rest" class="org.apache.struts2.rest.ResourceClasspathPackageProvider" /> - <bean type="org.apache.struts2.rest.handler.MimeTypeHandler" name="xml" class="org.apache.struts2.rest.handler.XStreamHandler" /> - <bean type="org.apache.struts2.rest.handler.MimeTypeHandler" name="js" class="org.apache.struts2.rest.handler.XStreamJsonHandler" /> - <bean type="org.apache.struts2.rest.handler.MimeTypeHandler" name="html" class="org.apache.struts2.rest.handler.HtmlHandler" /> + <bean class="org.apache.struts2.rest.ContentTypeHandlerSelector" /> + + <bean type="org.apache.struts2.rest.handler.ContentTypeHandler" name="xml" class="org.apache.struts2.rest.handler.XStreamHandler" /> + <bean type="org.apache.struts2.rest.handler.ContentTypeHandler" name="json" class="org.apache.struts2.rest.handler.XStreamJsonHandler" /> + <bean type="org.apache.struts2.rest.handler.ContentTypeHandler" name="html" class="org.apache.struts2.rest.handler.HtmlHandler" /> <constant name="struts.actionProxyFactory" value="rest" /> <constant name="struts.rest.defaultHandlerName" value="xml" /> @@ -21,7 +25,51 @@ <constant name="struts.configuration.classpath.defaultParentPackage" value="rest-default" /> <package name="rest-default" extends="struts-default"> - <default-interceptor-ref name="basicStack"/> + <interceptors> + <interceptor name="rest" class="org.apache.struts2.rest.ContentTypeInterceptor"/> + + <!-- A complete stack with all the common interceptors in place. + Generally, this stack should be the one you use, though it + may do more than you need. Also, the ordering can be + switched around (ex: if you wish to have your servlet-related + objects applied before prepare() is called, you'd need to move + servlet-config interceptor up. + + This stack also excludes from the normal validation and workflow + the method names input, back, and cancel. These typically are + associated with requests that should not be validated. + --> + <interceptor-stack name="restDefaultStack"> + <interceptor-ref name="exception"/> + <interceptor-ref name="alias"/> + <interceptor-ref name="servletConfig"/> + <interceptor-ref name="prepare"/> + <interceptor-ref name="i18n"/> + <interceptor-ref name="chain"/> + <interceptor-ref name="debugging"/> + <interceptor-ref name="profiling"/> + <interceptor-ref name="scopedModelDriven"/> + <interceptor-ref name="modelDriven"/> + <interceptor-ref name="fileUpload"/> + <interceptor-ref name="checkbox"/> + <interceptor-ref name="staticParams"/> + <interceptor-ref name="params"> + <param name="excludeParams">dojo\..*</param> + </interceptor-ref> + <interceptor-ref name="rest" /> + <interceptor-ref name="conversionError"/> + <interceptor-ref name="validation"> + <param name="excludeMethods">input,back,cancel,browse</param> + </interceptor-ref> + <interceptor-ref name="workflow"> + <param name="excludeMethods">input,back,cancel,browse</param> + </interceptor-ref> + </interceptor-stack> + + </interceptors> + + + <default-interceptor-ref name="restDefaultStack"/> </package> </struts> Added: struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java URL: http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java?rev=586735&view=auto ============================================================================== --- struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java (added) +++ struts/sandbox/trunk/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java Sat Oct 20 07:48:27 2007 @@ -0,0 +1,38 @@ +package org.apache.struts2.rest; + +import org.apache.struts2.dispatcher.mapper.ActionMapping; + +import junit.framework.TestCase; + +public class RestActionMapperTest extends TestCase { + + private RestActionMapper mapper; + + public void setUp() throws Exception { + mapper = new RestActionMapper(); + } + + public void testParseNameAndNamespace() { + tryUri("/foo/23", "/", "foo/23"); + tryUri("/foo/", "/", "foo/"); + tryUri("foo", "", "foo"); + tryUri("/", "/", ""); + } + + public void testParseNameAndNamespaceWithNamespaces() { + tryUri("/ns/foo/23", "/ns", "foo/23"); + tryUri("/ns/foo/", "/ns", "foo/"); + } + + public void testParseNameAndNamespaceWithEdit() { + tryUri("/ns/foo/23;edit", "/ns", "foo/23;edit"); + } + + private void tryUri(String uri, String expectedNamespace, String expectedName) { + ActionMapping mapping = new ActionMapping(); + mapper.parseNameAndNamespace(uri, mapping, null); + assertEquals(expectedName, mapping.getName()); + assertEquals(expectedNamespace, mapping.getNamespace()); + } + +}