names = container.getInstanceNames(ContentTypeHandler.class);
+ for (String name : names) {
+ ContentTypeHandler handler = container.getInstance(ContentTypeHandler.class, name);
+
+ // Check for overriding handlers for the current extension
+ String overrideName = container.getInstance(String.class, STRUTS_REST_HANDLER_OVERRIDE_PREFIX +handler.getExtension());
+ if (overrideName != null) {
+ if (!handlers.containsKey(handler.getExtension())) {
+ handler = container.getInstance(ContentTypeHandler.class, overrideName);
+ } else {
+ // overriding handler has already been registered
+ continue;
+ }
+ }
+ this.handlers.put(handler.getExtension(), handler);
+ }
+ }
+
+ /**
+ * Gets the handler for the request by looking at the extension
+ * @param req The request
+ * @return The appropriate handler
+ */
+ public ContentTypeHandler getHandlerForRequest(HttpServletRequest req) {
+ String extension = findExtension(req.getRequestURI());
+ if (extension == null) {
+ extension = defaultExtension;
+ }
+ return handlers.get(extension);
+ }
+
+ /**
+ * Handles the result using handlers to generate content type-specific content
+ *
+ * @param actionConfig The action config for the current request
+ * @param methodResult The object returned from the action method
+ * @param target The object to return, usually the action object
+ * @return The new result code to process
+ * @throws IOException If unable to write to the response
+ */
+ public String handleResult(ActionConfig actionConfig, Object methodResult, Object target)
+ throws IOException {
+ String resultCode = null;
+ HttpServletRequest req = ServletActionContext.getRequest();
+ HttpServletResponse res = ServletActionContext.getResponse();
+ if (target instanceof ModelDriven) {
+ target = ((ModelDriven)target).getModel();
+ }
+
+ boolean statusNotOk = false;
+ if (methodResult instanceof HttpHeaders) {
+ HttpHeaders info = (HttpHeaders) methodResult;
+ resultCode = info.apply(req, res, target);
+ if (info.getStatus() != SC_OK) {
+
+ // Don't return content on a not modified
+ if (info.getStatus() == SC_NOT_MODIFIED) {
+ target = null;
+ } else {
+ statusNotOk = true;
+ }
+
+ }
+ } else {
+ resultCode = (String) methodResult;
+ }
+
+ // Don't return any content for PUT, DELETE, and POST where there are no errors
+ if (!statusNotOk && !"get".equalsIgnoreCase(req.getMethod())) {
+ target = null;
+ }
+
+ ContentTypeHandler handler = getHandlerForRequest(req);
+ if (handler != null) {
+ String extCode = resultCode+"-"+handler.getExtension();
+ if (actionConfig.getResults().get(extCode) != null) {
+ resultCode = extCode;
+ } else {
+ StringWriter writer = new StringWriter();
+ resultCode = handler.fromObject(target, resultCode, writer);
+ String text = writer.toString();
+ if (text.length() > 0) {
+ byte[] data = text.getBytes("UTF-8");
+ res.setContentLength(data.length);
+ res.setContentType(handler.getContentType());
+ res.getOutputStream().write(data);
+ res.getOutputStream().close();
+ }
+ }
+ }
+ return resultCode;
+
+ }
+
+ /**
+ * Finds the extension in the url
+ *
+ * @param url The url
+ * @return The 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;
+ }
+}
diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java
new file mode 100644
index 000000000..ecdd61204
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java
@@ -0,0 +1,65 @@
+/*
+ * $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;
+
+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;
+
+/**
+ * Uses the content handler to apply the request body to the action
+ */
+public class ContentTypeInterceptor implements Interceptor {
+
+ private static final long serialVersionUID = 1L;
+ ContentTypeHandlerManager selector;
+
+ @Inject
+ public void setContentTypeHandlerSelector(ContentTypeHandlerManager 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.getReader(), target);
+ }
+ return invocation.invoke();
+ }
+
+}
diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ControllerClasspathPackageProvider.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ControllerClasspathPackageProvider.java
new file mode 100644
index 000000000..353882f19
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/ControllerClasspathPackageProvider.java
@@ -0,0 +1,47 @@
+/*
+ * $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;
+
+import org.apache.struts2.config.ClasspathPackageProvider;
+
+import com.opensymphony.xwork2.util.ResolverUtil.ClassTest;
+
+/**
+ * Checks for actions ending in Controller indicating a Rest controller
+ */
+public class ControllerClasspathPackageProvider extends ClasspathPackageProvider {
+
+ @Override
+ protected ClassTest createActionClassTest() {
+ return new ClassTest() {
+ // Match Action implementations and classes ending with "Controller"
+ public boolean matches(Class type) {
+ return (type.getSimpleName().endsWith("Controller"));
+ }
+ };
+ }
+
+ @Override
+ protected String getClassSuffix() {
+ return "Controller";
+ }
+
+}
diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java
new file mode 100644
index 000000000..140717a71
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java
@@ -0,0 +1,156 @@
+/*
+ * $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;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import java.util.Date;
+
+/**
+ * Default implementation of rest info that uses fluent-style construction
+ */
+public class DefaultHttpHeaders implements HttpHeaders {
+ String resultCode;
+ int status = SC_OK;
+ Object etag;
+ Object locationId;
+ String location;
+ boolean disableCaching;
+ boolean noETag = false;
+ Date lastModified;
+
+ public DefaultHttpHeaders() {}
+
+ public DefaultHttpHeaders(String result) {
+ resultCode = result;
+ }
+
+ public DefaultHttpHeaders renderResult(String code) {
+ this.resultCode = code;
+ return this;
+ }
+
+ public DefaultHttpHeaders withStatus(int code) {
+ this.status = code;
+ return this;
+ }
+
+ public DefaultHttpHeaders withETag(Object etag) {
+ this.etag = etag;
+ return this;
+ }
+
+ public DefaultHttpHeaders withNoETag() {
+ this.noETag = true;
+ return this;
+ }
+
+ public DefaultHttpHeaders setLocationId(Object id) {
+ this.locationId = id;
+ return this;
+ }
+
+ public DefaultHttpHeaders setLocation(String loc) {
+ this.location = loc;
+ return this;
+ }
+
+ public DefaultHttpHeaders lastModified(Date date) {
+ this.lastModified = date;
+ return this;
+ }
+
+ public DefaultHttpHeaders disableCaching() {
+ this.disableCaching = true;
+ return this;
+ }
+
+ /* (non-Javadoc)
+ * @see org.apache.struts2.rest.HttpHeaders#apply(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object)
+ */
+ public String apply(HttpServletRequest request, HttpServletResponse response, Object target) {
+
+ if (disableCaching) {
+ response.setHeader("Cache-Control", "no-cache");
+ }
+ if (lastModified != null) {
+ response.setDateHeader("Last-Modified", lastModified.getTime());
+ }
+ if (etag == null && !noETag) {
+ etag = String.valueOf(target.hashCode());
+ }
+ if (etag != null) {
+ 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);
+ }
+
+ if (status == SC_OK && !disableCaching) {
+ boolean etagNotChanged = false;
+ boolean lastModifiedNotChanged = false;
+ String reqETag = request.getHeader("If-None-Match");
+ if (etag != null) {
+ if (etag.equals(reqETag)) {
+ etagNotChanged = true;
+ }
+ }
+
+ String reqLastModified = request.getHeader("If-Modified-Since");
+ if (lastModified != null) {
+ if (String.valueOf(lastModified.getTime()).equals(reqLastModified)) {
+ lastModifiedNotChanged = true;
+ }
+
+ }
+
+ if ((etagNotChanged && lastModifiedNotChanged) ||
+ (etagNotChanged && reqLastModified == null) ||
+ (lastModifiedNotChanged && reqETag == null)) {
+ status = SC_NOT_MODIFIED;
+ }
+ }
+
+ response.setStatus(status);
+ return resultCode;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+
+
+
+}
diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/HttpHeaders.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/HttpHeaders.java
new file mode 100644
index 000000000..7a4e0a2f8
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/HttpHeaders.java
@@ -0,0 +1,45 @@
+/*
+ * $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;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Type-safe rest-related informtion to apply to a response
+ */
+public interface HttpHeaders {
+
+ /**
+ * Applies the configured information to the response
+ * @param request The request
+ * @param response The response
+ * @param target The target object, usually the action
+ * @return The result code to process
+ */
+ String apply(HttpServletRequest request,
+ HttpServletResponse response, Object target);
+
+ /**
+ * The HTTP status code
+ */
+ int getStatus();
+}
\ No newline at end of file
diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionInvocation.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
new file mode 100644
index 000000000..557dd391f
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
@@ -0,0 +1,164 @@
+/*
+ * $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;
+
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ActionEventListener;
+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;
+import com.opensymphony.xwork2.config.ConfigurationException;
+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.logging.LoggerFactory;
+import com.opensymphony.xwork2.util.profiling.UtilTimerStack;
+import com.opensymphony.xwork2.util.logging.Logger;
+
+import org.apache.struts2.ServletActionContext;
+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;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Extends the usual {@link ActionInvocation} to add support for processing the object returned
+ * from the action execution. This allows us to support methods that return {@link HttpHeaders}
+ * as well as apply content type-specific operations to the result.
+ */
+public class RestActionInvocation extends DefaultActionInvocation {
+
+ private static final long serialVersionUID = 3485701178946428716L;
+
+ private static final Logger LOG = LoggerFactory.getLogger(RestActionInvocation.class);
+
+ private ContentTypeHandlerManager handlerSelector;
+
+ protected RestActionInvocation(Map extraContext, boolean pushAction) throws Exception {
+ super(extraContext, pushAction);
+ }
+
+ @Inject
+ public void setMimeTypeHandlerSelector(ContentTypeHandlerManager sel) {
+ this.handlerSelector = sel;
+ }
+
+ protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception {
+ String methodName = proxy.getMethod();
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Executing action method = " + actionConfig.getMethodName());
+ }
+
+ String timerKey = "invokeAction: "+proxy.getActionName();
+ try {
+ UtilTimerStack.push(timerKey);
+
+ boolean methodCalled = false;
+ Object methodResult = null;
+ Method method = null;
+ try {
+ 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);
+ method = getAction().getClass().getMethod(altMethodName, new Class[0]);
+ } catch (NoSuchMethodException e1) {
+ // 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 = 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() + "");
+ } catch (InvocationTargetException e) {
+ // We try to return the source exception.
+ Throwable t = e.getTargetException();
+
+ if (actionEventListener != null) {
+ String result = actionEventListener.handleException(t, getStack());
+ if (result != null) {
+ return result;
+ }
+ }
+ if (t instanceof Exception) {
+ throw(Exception) t;
+ } else {
+ throw e;
+ }
+ } finally {
+ UtilTimerStack.pop(timerKey);
+ }
+ }
+
+ protected String processResult(ActionConfig actionConfig, Object methodResult) throws IOException {
+ if (methodResult instanceof Result) {
+ this.explicitResult = (Result) methodResult;
+ return null;
+ } else if (methodResult != null) {
+ resultCode = handlerSelector.handleResult(actionConfig, methodResult, action);
+ }
+ return resultCode;
+ }
+
+}
diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionMapper.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionMapper.java
new file mode 100644
index 000000000..96eddd6c0
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionMapper.java
@@ -0,0 +1,329 @@
+/*
+ * $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;
+
+import com.opensymphony.xwork2.config.Configuration;
+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 org.apache.struts2.StrutsConstants;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
+import org.apache.struts2.dispatcher.mapper.DefaultActionMapper;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Iterator;
+
+/**
+ *
+ *
+ * This Restful action mapper enforces Ruby-On-Rails Rest-style mappings. If the method
+ * is not specified (via '!' or 'method:' prefix), the method is "guessed" at using
+ * ReST-style conventions that examine the URL and the HTTP method. Special care has
+ * been given to ensure this mapper works correctly with the codebehind plugin so that
+ * XML configuration is unnecessary.
+ *
+ *
+ * This mapper supports the following parameters:
+ *
+ *
+ * struts.mapper.idParameterName - If set, this value will be the name
+ * of the parameter under which the id is stored. The id will then be removed
+ * from the action name. Whether or not the method is specified, the mapper will
+ * try to truncate the identifier from the url and store it as a parameter.
+ *
+ * struts.mapper.indexMethodName - The method name to call for a GET
+ * request with no id parameter. Defaults to 'index'.
+ *
+ * struts.mapper.getMethodName - The method name to call for a GET
+ * request with an id parameter. Defaults to 'show'.
+ *
+ * struts.mapper.postMethodName - The method name to call for a POST
+ * request with no id parameter. Defaults to 'create'.
+ *
+ * struts.mapper.putMethodName - The method name to call for a PUT
+ * request with an id parameter. Defaults to 'update'.
+ *
+ * struts.mapper.deleteMethodName - The method name to call for a DELETE
+ * request with an id parameter. Defaults to 'destroy'.
+ *
+ * struts.mapper.editMethodName - The method name to call for a GET
+ * request with an id parameter and the 'edit' view specified. Defaults to 'edit'.
+ *
+ * struts.mapper.newMethodName - The method name to call for a GET
+ * request with no id parameter and the 'new' view specified. Defaults to 'editNew'.
+ *
+ *
+ *
+ * The following URL's will invoke its methods:
+ *
+ *
+ * GET: /movies => method="index"
+ * GET: /movies/Thrillers => method="show", id="Thrillers"
+ * GET: /movies/Thrillers;edit => method="edit", id="Thrillers"
+ * GET: /movies/Thrillers/edit => method="edit", id="Thrillers"
+ * GET: /movies/new => method="editNew"
+ * POST: /movies => method="create"
+ * PUT: /movies/Thrillers => method="update", id="Thrillers"
+ * DELETE: /movies/Thrillers => method="destroy", id="Thrillers"
+ *
+ *
+ * To simulate the HTTP methods PUT and DELETE, since they aren't supported by HTML,
+ * the HTTP parameter "_method" will be used.
+ *
+ *
+ */
+public class RestActionMapper extends DefaultActionMapper {
+
+ protected static final Logger LOG = LoggerFactory.getLogger(RestActionMapper.class);
+ public static final String HTTP_METHOD_PARAM = "_method";
+ private String idParameterName = "id";
+ private String indexMethodName = "index";
+ private String getMethodName = "show";
+ private String postMethodName = "create";
+ private String editMethodName = "edit";
+ private String newMethodName = "editNew";
+ private String deleteMethodName = "destroy";
+ private String putMethodName = "update";
+
+ public RestActionMapper() {
+ }
+
+ public String getIdParameterName() {
+ return idParameterName;
+ }
+
+ @Inject(required=false,value=StrutsConstants.STRUTS_ID_PARAMETER_NAME)
+ public void setIdParameterName(String idParameterName) {
+ this.idParameterName = idParameterName;
+ }
+
+ @Inject(required=false,value="struts.mapper.indexMethodName")
+ public void setIndexMethodName(String indexMethodName) {
+ this.indexMethodName = indexMethodName;
+ }
+
+ @Inject(required=false,value="struts.mapper.getMethodName")
+ public void setGetMethodName(String getMethodName) {
+ this.getMethodName = getMethodName;
+ }
+
+ @Inject(required=false,value="struts.mapper.postMethodName")
+ public void setPostMethodName(String postMethodName) {
+ this.postMethodName = postMethodName;
+ }
+
+ @Inject(required=false,value="struts.mapper.editMethodName")
+ public void setEditMethodName(String editMethodName) {
+ this.editMethodName = editMethodName;
+ }
+
+ @Inject(required=false,value="struts.mapper.newMethodName")
+ public void setNewMethodName(String newMethodName) {
+ this.newMethodName = newMethodName;
+ }
+
+ @Inject(required=false,value="struts.mapper.deleteMethodName")
+ public void setDeleteMethodName(String deleteMethodName) {
+ this.deleteMethodName = deleteMethodName;
+ }
+
+ @Inject(required=false,value="struts.mapper.putMethodName")
+ public void setPutMethodName(String putMethodName) {
+ this.putMethodName = putMethodName;
+ }
+
+ public ActionMapping getMapping(HttpServletRequest request,
+ ConfigurationManager configManager) {
+ ActionMapping mapping = new ActionMapping();
+ String uri = getUri(request);
+
+ uri = dropExtension(uri, mapping);
+ if (uri == null) {
+ return null;
+ }
+
+ parseNameAndNamespace(uri, mapping, configManager);
+
+ handleSpecialParameters(request, mapping);
+
+ if (mapping.getName() == null) {
+ return null;
+ }
+
+ // 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) {
+
+ // fun trickery to parse 'actionName/id/methodName' in the case of 'animals/dog/edit'
+ int prevSlashPos = fullName.lastIndexOf('/', lastSlashPos - 1);
+ if (prevSlashPos > -1) {
+ mapping.setMethod(fullName.substring(lastSlashPos+1));
+ fullName = fullName.substring(0, lastSlashPos);
+ lastSlashPos = prevSlashPos;
+ }
+ 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 with no id, possibly ending in '/'
+ if (lastSlashPos == -1 || lastSlashPos == fullName.length() -1) {
+
+ // Index e.g. foo
+ if (isGet(request)) {
+ mapping.setMethod(indexMethodName);
+
+ // Creating a new entry on POST e.g. foo
+ } else if (isPost(request)) {
+ mapping.setMethod(postMethodName);
+ }
+
+ // 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(editMethodName);
+
+ // Viewing the form to create a new item e.g. foo/new
+ } else if (isGet(request) && "new".equals(id)) {
+ mapping.setMethod(newMethodName);
+
+ // Removing an item e.g. foo/1
+ } else if (isDelete(request)) {
+ mapping.setMethod(deleteMethodName);
+
+ // Viewing an item e.g. foo/1
+ } else if (isGet(request)) {
+ mapping.setMethod(getMethodName);
+
+ // Updating an item e.g. foo/1
+ } else if (isPut(request)) {
+ mapping.setMethod(putMethodName);
+ }
+ }
+ }
+
+ // 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());
+ }
+ mapping.getParams().put(idParameterName, new String[]{id});
+ }
+ fullName = fullName.substring(0, lastSlashPos);
+ }
+
+ mapping.setName(fullName);
+ }
+
+ return mapping;
+ }
+
+ /**
+ * Parses the name and namespace from the uri. Uses the configured package
+ * namespaces to determine the name and id parameter, to be parsed later.
+ *
+ * @param uri
+ * The uri
+ * @param mapping
+ * The action mapping to populate
+ */
+ protected void parseNameAndNamespace(String uri, ActionMapping mapping,
+ ConfigurationManager configManager) {
+ String namespace, name;
+ int lastSlash = uri.lastIndexOf("/");
+ if (lastSlash == -1) {
+ namespace = "";
+ name = uri;
+ } else if (lastSlash == 0) {
+ // ww-1046, assume it is the root namespace, it will fallback to
+ // default
+ // namespace anyway if not found in root namespace.
+ 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);
+ }
+
+ mapping.setNamespace(namespace);
+ mapping.setName(name);
+ }
+
+ protected boolean isGet(HttpServletRequest request) {
+ return "get".equalsIgnoreCase(request.getMethod());
+ }
+
+ protected boolean isPost(HttpServletRequest request) {
+ return "post".equalsIgnoreCase(request.getMethod());
+ }
+
+ protected boolean isPut(HttpServletRequest request) {
+ if ("put".equalsIgnoreCase(request.getMethod())) {
+ return true;
+ } else {
+ return isPost(request) && "put".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
+ }
+ }
+
+ protected boolean isDelete(HttpServletRequest request) {
+ if ("delete".equalsIgnoreCase(request.getMethod())) {
+ return true;
+ } else {
+ return "delete".equalsIgnoreCase(request.getParameter(HTTP_METHOD_PARAM));
+ }
+ }
+
+}
diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionProxyFactory.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionProxyFactory.java
new file mode 100644
index 000000000..cce69f695
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestActionProxyFactory.java
@@ -0,0 +1,44 @@
+/*
+ * $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;
+
+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;
+
+
+/**
+ * Factory that creates the {@link RestActionInvocation}
+ */
+public class RestActionProxyFactory extends DefaultActionProxyFactory {
+
+ public ActionProxy createActionProxy(String namespace, String actionName, Map extraContext, boolean executeResult, boolean cleanupContext) throws Exception {
+ ActionInvocation inv = new RestActionInvocation(extraContext, true);
+ container.inject(inv);
+ return createActionProxy(inv, namespace, actionName, extraContext, executeResult, cleanupContext);
+ }
+
+}
diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestWorkflowInterceptor.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestWorkflowInterceptor.java
new file mode 100644
index 000000000..585ec7ae9
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/RestWorkflowInterceptor.java
@@ -0,0 +1,200 @@
+/*
+ * $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;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.struts2.ServletActionContext;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
+
+import com.opensymphony.xwork2.Action;
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.ValidationAware;
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+
+import static javax.servlet.http.HttpServletResponse.*;
+
+/**
+ *
+ *
+ * An interceptor that makes sure there are not validation errors before allowing the interceptor chain to continue.
+ * This interceptor does not perform any validation.
+ *
+ * Copied from the {@link DefaultWorkflowInterceptor}, this interceptor adds support for error handling of Restful
+ * operations. For example, if an validation error is discovered, a map of errors is created and processed to be
+ * returned, using the appropriate content handler for rendering the body.
+ *
+ * This interceptor does nothing if the name of the method being invoked is specified in the excludeMethods
+ * parameter. excludeMethods accepts a comma-delimited list of method names. For example, requests to
+ * foo!input.action and foo!back.action will be skipped by this interceptor if you set the
+ * excludeMethods parameter to "input, back".
+ *
+ * Note: As this method extends off MethodFilterInterceptor, it is capable of
+ * deciding if it is applicable only to selective methods in the action class. This is done by adding param tags
+ * for the interceptor element, naming either a list of excluded method names and/or a list of included method
+ * names, whereby includeMethods overrides excludedMethods. A single * sign is interpreted as wildcard matching
+ * all methods for both parameters.
+ * See {@link MethodFilterInterceptor} for more info.
+ *
+ *
+ *
+ * Interceptor parameters:
+ *
+ *
+ *
+ *
+ *
+ * - inputResultName - Default to "input". Determine the result name to be returned when
+ * an action / field error is found.
+ *
+ *
+ *
+ *
+ *
+ * Extending the interceptor:
+ *
+ *
+ *
+ *
+ *
+ * There are no known extension points for this interceptor.
+ *
+ *
+ *
+ * Example code:
+ *
+ *
+ *
+ *
+ * <action name="someAction" class="com.examples.SomeAction">
+ * <interceptor-ref name="params"/>
+ * <interceptor-ref name="validation"/>
+ * <interceptor-ref name="workflow"/>
+ * <result name="success">good_result.ftl</result>
+ * </action>
+ *
+ * <-- In this case myMethod as well as mySecondMethod of the action class
+ * will not pass through the workflow process -->
+ * <action name="someAction" class="com.examples.SomeAction">
+ * <interceptor-ref name="params"/>
+ * <interceptor-ref name="validation"/>
+ * <interceptor-ref name="workflow">
+ * <param name="excludeMethods">myMethod,mySecondMethod</param>
+ * </interceptor-ref name="workflow">
+ * <result name="success">good_result.ftl</result>
+ * </action>
+ *
+ * <-- In this case, the result named "error" will be used when
+ * an action / field error is found -->
+ * <-- The Interceptor will only be applied for myWorkflowMethod method of action
+ * classes, since this is the only included method while any others are excluded -->
+ * <action name="someAction" class="com.examples.SomeAction">
+ * <interceptor-ref name="params"/>
+ * <interceptor-ref name="validation"/>
+ * <interceptor-ref name="workflow">
+ * <param name="inputResultName">error</param>
+* <param name="excludeMethods">*</param>
+* <param name="includeMethods">myWorkflowMethod</param>
+ * </interceptor-ref>
+ * <result name="success">good_result.ftl</result>
+ * </action>
+ *
+ *
+ *
+ *
+ * @author Jason Carreira
+ * @author Rainer Hermanns
+ * @author Alexandru Popescu
+ * @author Philip Luppens
+ * @author tm_jee
+ */
+public class RestWorkflowInterceptor extends MethodFilterInterceptor {
+
+ private static final long serialVersionUID = 7563014655616490865L;
+
+ private static final Logger LOG = LoggerFactory.getLogger(RestWorkflowInterceptor.class);
+
+ private String inputResultName = Action.INPUT;
+
+ private ContentTypeHandlerManager manager;
+
+ @Inject
+ public void setContentTypeHandlerManager(ContentTypeHandlerManager mgr) {
+ this.manager = mgr;
+ }
+
+ /**
+ * Set the inputResultName (result name to be returned when
+ * a action / field error is found registered). Default to {@link Action#INPUT}
+ *
+ * @param inputResultName what result name to use when there was validation error(s).
+ */
+ public void setInputResultName(String inputResultName) {
+ this.inputResultName = inputResultName;
+ }
+
+ /**
+ * Intercept {@link ActionInvocation} and processes the errors using the {@link org.apache.struts2.rest.handler.ContentTypeHandler}
+ * appropriate for the request.
+ *
+ * @return String result name
+ */
+ protected String doIntercept(ActionInvocation invocation) throws Exception {
+ Object action = invocation.getAction();
+
+ if (action instanceof ValidationAware) {
+ ValidationAware validationAwareAction = (ValidationAware) action;
+
+ if (validationAwareAction.hasErrors()) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Errors on action "+validationAwareAction+", returning result name 'input'");
+ }
+ ActionMapping mapping = (ActionMapping) ActionContext.getContext().get(ServletActionContext.ACTION_MAPPING);
+ String method = inputResultName;
+ if ("create".equals(mapping.getMethod())) {
+ method = "editNew";
+ } else if ("update".equals(mapping.getMethod())) {
+ method = "edit";
+ }
+
+
+ HttpHeaders info = new DefaultHttpHeaders()
+ .disableCaching()
+ .renderResult(method)
+ .withStatus(SC_BAD_REQUEST);
+
+ Map errors = new HashMap();
+
+ errors.put("actionErrors", validationAwareAction.getActionErrors());
+ errors.put("fieldErrors", validationAwareAction.getFieldErrors());
+ return manager.handleResult(invocation.getProxy().getConfig(), info, errors);
+ }
+ }
+
+ return invocation.invoke();
+ }
+
+}
diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/ContentTypeHandler.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/ContentTypeHandler.java
new file mode 100644
index 000000000..32b4ec09b
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/ContentTypeHandler.java
@@ -0,0 +1,63 @@
+/*
+ * $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.*;
+
+import com.opensymphony.xwork2.ActionInvocation;
+
+/**
+ * Handles transferring content to and from objects for a specific content type
+ */
+public interface ContentTypeHandler {
+
+ /**
+ * Populates an object using data from the input stream
+ * @param in The input stream, usually the body of the request
+ * @param target The target, usually the action class
+ */
+ void toObject(Reader in, Object target) throws IOException;
+
+ /**
+ * Writes content to the stream
+ *
+ * @param obj The object to write to the stream, usually the Action class
+ * @param resultCode The original result code
+ * @param stream The output stream, usually the response
+ * @return The new result code
+ * @throws IOException If unable to write to the output stream
+ */
+ String fromObject(Object obj, String resultCode, Writer stream) throws IOException;
+
+ /**
+ * Gets the content type for this handler
+ *
+ * @return The mime type
+ */
+ String getContentType();
+
+ /**
+ * Gets the extension this handler supports
+ *
+ * @return The extension
+ */
+ String getExtension();
+}
diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/HtmlHandler.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/HtmlHandler.java
new file mode 100644
index 000000000..9041395ee
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/HtmlHandler.java
@@ -0,0 +1,47 @@
+/*
+ * $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.Reader;
+import java.io.Writer;
+
+/**
+ * Handles HTML content, usually just a simple passthrough to the framework
+ */
+public class HtmlHandler implements ContentTypeHandler {
+
+ public String fromObject(Object obj, String resultCode, Writer out) throws IOException {
+ return resultCode;
+ }
+
+ public void toObject(Reader in, Object target) {
+ }
+
+ public String getExtension() {
+ return "xhtml";
+ }
+
+ public String getContentType() {
+ return "application/xhtml+xml";
+ }
+
+}
diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/JsonLibHandler.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/JsonLibHandler.java
new file mode 100644
index 000000000..789fa4923
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/JsonLibHandler.java
@@ -0,0 +1,82 @@
+/*
+ * $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.*;
+import java.util.Collection;
+
+import net.sf.json.JSONObject;
+import net.sf.json.JsonConfig;
+import net.sf.json.JSONArray;
+
+/**
+ * Handles JSON content using json-lib
+ */
+public class JsonLibHandler implements ContentTypeHandler {
+
+ public void toObject(Reader in, Object target) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ char[] buffer = new char[1024];
+ int len = 0;
+ while ((len = in.read(buffer)) > 0) {
+ sb.append(buffer, 0, len);
+ }
+ if (target != null && sb.length() > 0 && sb.charAt(0) == '[') {
+ JSONArray jsonArray = JSONArray.fromObject(sb.toString());
+ if (target.getClass().isArray()) {
+ JSONArray.toArray(jsonArray, target, new JsonConfig());
+ } else {
+ JSONArray.toList(jsonArray, target, new JsonConfig());
+ }
+
+ } else {
+ JSONObject jsonObject = JSONObject.fromObject(sb.toString());
+ JSONObject.toBean(jsonObject, target, new JsonConfig());
+ }
+ }
+
+ public String fromObject(Object obj, String resultCode, Writer stream) throws IOException {
+ if (obj != null) {
+ if (isArray(obj)) {
+ JSONArray jsonArray = JSONArray.fromObject(obj);
+ stream.write(jsonArray.toString());
+ } else {
+ JSONObject jsonObject = JSONObject.fromObject(obj);
+ stream.write(jsonObject.toString());
+ }
+ }
+ return null;
+
+
+ }
+
+ private boolean isArray(Object obj) {
+ return obj instanceof Collection || obj.getClass().isArray();
+ }
+
+ public String getContentType() {
+ return "text/javascript";
+ }
+
+ public String getExtension() {
+ return "json";
+ }
+}
diff --git a/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java
new file mode 100644
index 000000000..29e12bd23
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java
@@ -0,0 +1,58 @@
+/*
+ * $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.Reader;
+import java.io.Writer;
+
+import com.thoughtworks.xstream.XStream;
+
+/**
+ * Handles XML content
+ */
+public class XStreamHandler implements ContentTypeHandler {
+
+ public String fromObject(Object obj, String resultCode, Writer out) throws IOException {
+ if (obj != null) {
+ XStream xstream = createXStream();
+ xstream.toXML(obj, out);
+ }
+ return null;
+ }
+
+ public void toObject(Reader in, Object target) {
+ XStream xstream = createXStream();
+ xstream.fromXML(in, target);
+ }
+
+ protected XStream createXStream() {
+ return new XStream();
+ }
+
+ public String getContentType() {
+ return "application/xml";
+ }
+
+ public String getExtension() {
+ return "xml";
+ }
+}
diff --git a/plugins/struts2-rest-plugin/src/main/resources/struts-plugin.xml b/plugins/struts2-rest-plugin/src/main/resources/struts-plugin.xml
new file mode 100644
index 000000000..fc714d5ff
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/main/resources/struts-plugin.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 303
+
+
+ 303
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AUTOMATIC
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+ dojo\..*
+
+
+
+
+ input,back,cancel,browse,index,show,edit,editNew
+
+
+ input,back,cancel,browse,index,show,edit,editNew
+
+
+
+
+
+
+
+
+
diff --git a/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java
new file mode 100644
index 000000000..480c1e574
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java
@@ -0,0 +1,128 @@
+/*
+ * $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;
+
+import com.mockobjects.dynamic.C;
+import com.mockobjects.dynamic.Mock;
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.config.entities.ActionConfig;
+import com.opensymphony.xwork2.inject.Container;
+import junit.framework.TestCase;
+import org.apache.struts2.ServletActionContext;
+import org.apache.struts2.rest.handler.ContentTypeHandler;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+public class ContentTypeHandlerManagerTest extends TestCase {
+
+ private ContentTypeHandlerManager mgr;
+ private MockHttpServletResponse mockResponse;
+ private MockHttpServletRequest mockRequest;
+
+ @Override
+ public void setUp() {
+ mgr = new ContentTypeHandlerManager();
+ mockResponse = new MockHttpServletResponse();
+ mockRequest = new MockHttpServletRequest();
+ mockRequest.setMethod("GET");
+ ActionContext.setContext(new ActionContext(new HashMap()));
+ ServletActionContext.setRequest(mockRequest);
+ ServletActionContext.setResponse(mockResponse);
+ }
+
+ @Override
+ public void tearDown() {
+ mockRequest = null;
+ mockRequest = null;
+ mgr = null;
+ }
+
+ public void testHandleResultOK() throws IOException {
+
+ String obj = "mystring";
+ ContentTypeHandler handler = new ContentTypeHandler() {
+ public void toObject(Reader in, Object target) {}
+ public String fromObject(Object obj, String resultCode, Writer stream) throws IOException {
+ stream.write(obj.toString());
+ return resultCode;
+ }
+ public String getContentType() { return "foo"; }
+ public String getExtension() { return "foo"; }
+ };
+ mgr.handlers.put("xml", handler);
+ mgr.defaultExtension = "xml";
+ mgr.handleResult(new ActionConfig(), new DefaultHttpHeaders().withStatus(SC_OK), obj);
+
+ assertEquals(obj.getBytes().length, mockResponse.getContentLength());
+ }
+
+ public void testHandleResultNotModified() throws IOException {
+
+ Mock mockHandlerXml = new Mock(ContentTypeHandler.class);
+ mockHandlerXml.matchAndReturn("getExtension", "xml");
+ mgr.handlers.put("xml", (ContentTypeHandler) mockHandlerXml.proxy());
+ mgr.handleResult(null, new DefaultHttpHeaders().withStatus(SC_NOT_MODIFIED), new Object());
+
+ assertEquals(0, mockResponse.getContentLength());
+ }
+
+ public void testHandlerOverride() {
+ Mock mockHandlerXml = new Mock(ContentTypeHandler.class);
+ mockHandlerXml.matchAndReturn("getExtension", "xml");
+ mockHandlerXml.matchAndReturn("toString", "xml");
+ Mock mockHandlerJson = new Mock(ContentTypeHandler.class);
+ mockHandlerJson.matchAndReturn("getExtension", "json");
+ mockHandlerJson.matchAndReturn("toString", "json");
+ Mock mockHandlerXmlOverride = new Mock(ContentTypeHandler.class);
+ mockHandlerXmlOverride.matchAndReturn("getExtension", "xml");
+ mockHandlerXmlOverride.matchAndReturn("toString", "xmlOverride");
+
+ Mock mockContainer = new Mock(Container.class);
+ mockContainer.matchAndReturn("getInstance", C.args(C.eq(ContentTypeHandler.class), C.eq("xmlOverride")), mockHandlerXmlOverride.proxy());
+ mockContainer.matchAndReturn("getInstance", C.args(C.eq(ContentTypeHandler.class), C.eq("xml")), mockHandlerXml.proxy());
+ mockContainer.matchAndReturn("getInstance", C.args(C.eq(ContentTypeHandler.class), C.eq("json")), mockHandlerJson.proxy());
+ mockContainer.expectAndReturn("getInstanceNames", C.args(C.eq(ContentTypeHandler.class)), new HashSet(Arrays.asList("xml", "xmlOverride", "json")));
+
+ mockContainer.matchAndReturn("getInstance", C.args(C.eq(String.class),
+ C.eq(ContentTypeHandlerManager.STRUTS_REST_HANDLER_OVERRIDE_PREFIX+"xml")), "xmlOverride");
+ mockContainer.expectAndReturn("getInstance", C.args(C.eq(String.class),
+ C.eq(ContentTypeHandlerManager.STRUTS_REST_HANDLER_OVERRIDE_PREFIX+"json")), null);
+
+ ContentTypeHandlerManager mgr = new ContentTypeHandlerManager();
+ mgr.setContainer((Container) mockContainer.proxy());
+
+ Map handlers = mgr.handlers;
+ assertNotNull(handlers);
+ assertEquals(2, handlers.size());
+ assertEquals(mockHandlerXmlOverride.proxy(), handlers.get("xml"));
+ assertEquals(mockHandlerJson.proxy(), handlers.get("json"));
+ }
+}
diff --git a/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java
new file mode 100644
index 000000000..a0aaf17ab
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java
@@ -0,0 +1,181 @@
+/*
+ * $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;
+
+import junit.framework.TestCase;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import static javax.servlet.http.HttpServletResponse.*;
+import java.util.Date;
+
+public class DefaultHttpHeadersTest extends TestCase {
+ private MockHttpServletResponse mockResponse;
+ private MockHttpServletRequest mockRequest;
+
+ @Override
+ public void setUp() {
+ mockResponse = new MockHttpServletResponse();
+ mockRequest = new MockHttpServletRequest();
+ }
+
+ @Override
+ public void tearDown() {
+ mockRequest = null;
+ mockRequest = null;
+ }
+
+ public void testApply() {
+ Date now = new Date();
+ DefaultHttpHeaders headers = new DefaultHttpHeaders()
+ .lastModified(now)
+ .withStatus(SC_OK)
+ .setLocationId("44")
+ .withETag("asdf");
+ mockRequest.setRequestURI("/foo/bar.xhtml");
+
+ headers.apply(mockRequest, mockResponse, new Object());
+
+ assertEquals(SC_OK, mockResponse.getStatus());
+ assertEquals("http://localhost:80/foo/bar/44.xhtml", mockResponse.getHeader("Location"));
+ assertEquals("asdf", mockResponse.getHeader("ETag"));
+ assertEquals(now.getTime(), mockResponse.getHeader("Last-Modified"));
+
+ }
+
+ public void testApplyNoLocationExtension() {
+ DefaultHttpHeaders headers = new DefaultHttpHeaders()
+ .setLocationId("44");
+ mockRequest.setRequestURI("/foo/bar");
+
+ headers.apply(mockRequest, mockResponse, new Object());
+ assertEquals("http://localhost:80/foo/bar/44", mockResponse.getHeader("Location"));
+
+ }
+
+ public void testApplyFullLocation() {
+ DefaultHttpHeaders headers = new DefaultHttpHeaders()
+ .setLocation("http://localhost/bar/44");
+ mockRequest.setRequestURI("/foo/bar");
+
+ headers.apply(mockRequest, mockResponse, new Object());
+ assertEquals("http://localhost/bar/44", mockResponse.getHeader("Location"));
+
+ }
+
+ public void testAutoETag() {
+ DefaultHttpHeaders headers = new DefaultHttpHeaders();
+ headers.apply(mockRequest, mockResponse, new Object() {
+ @Override
+ public int hashCode() {
+ return 123;
+ }
+ });
+
+ assertEquals("123", mockResponse.getHeader("ETag"));
+ }
+
+ public void testNoCache() {
+ DefaultHttpHeaders headers = new DefaultHttpHeaders()
+ .disableCaching();
+ headers.apply(mockRequest, mockResponse, new Object());
+
+ assertEquals("no-cache", mockResponse.getHeader("Cache-Control"));
+ }
+
+ public void testConditionalGetForJustETag() {
+ DefaultHttpHeaders headers = new DefaultHttpHeaders()
+ .withETag("asdf");
+ mockRequest.addHeader("If-None-Match", "asdf");
+ headers.apply(mockRequest, mockResponse, new Object());
+
+ assertEquals(SC_NOT_MODIFIED, mockResponse.getStatus());
+ assertEquals("asdf", mockResponse.getHeader("ETag"));
+ }
+
+ public void testConditionalGetForJustETagNotOK() {
+ DefaultHttpHeaders headers = new DefaultHttpHeaders()
+ .withETag("asdf")
+ .withStatus(SC_BAD_REQUEST);
+ mockRequest.addHeader("If-None-Match", "asdf");
+ headers.apply(mockRequest, mockResponse, new Object());
+
+ assertEquals(SC_BAD_REQUEST, mockResponse.getStatus());
+ assertEquals("asdf", mockResponse.getHeader("ETag"));
+ }
+
+ public void testConditionalGetForJustLastModified() {
+ Date now = new Date();
+ DefaultHttpHeaders headers = new DefaultHttpHeaders()
+ .lastModified(now);
+ mockRequest.addHeader("If-Modified-Since", String.valueOf(now.getTime()));
+ headers.apply(mockRequest, mockResponse, new Object());
+
+ assertEquals(SC_NOT_MODIFIED, mockResponse.getStatus());
+ }
+
+ public void testConditionalGetForJustLastModifiedDifferent() {
+ Date now = new Date();
+ DefaultHttpHeaders headers = new DefaultHttpHeaders()
+ .lastModified(now);
+ mockRequest.addHeader("If-Modified-Since", String.valueOf(new Date(2323L).getTime()));
+ headers.apply(mockRequest, mockResponse, new Object());
+
+ assertEquals(SC_OK, mockResponse.getStatus());
+ }
+
+ public void testConditionalGetForLastModifiedAndETag() {
+ Date now = new Date();
+ DefaultHttpHeaders headers = new DefaultHttpHeaders()
+ .lastModified(now)
+ .withETag("asdf");
+ mockRequest.addHeader("If-None-Match", "asdf");
+ mockRequest.addHeader("If-Modified-Since", String.valueOf(now.getTime()));
+ headers.apply(mockRequest, mockResponse, new Object());
+
+ assertEquals(SC_NOT_MODIFIED, mockResponse.getStatus());
+ }
+
+ public void testConditionalGetForLastModifiedAndETagButNoCache() {
+ Date now = new Date();
+ DefaultHttpHeaders headers = new DefaultHttpHeaders()
+ .lastModified(now)
+ .withETag("asdf")
+ .disableCaching();
+ mockRequest.addHeader("If-None-Match", "asdf");
+ mockRequest.addHeader("If-Modified-Since", String.valueOf(now.getTime()));
+ headers.apply(mockRequest, mockResponse, new Object());
+
+ assertEquals(SC_OK, mockResponse.getStatus());
+ }
+
+ public void testConditionalGetForLastModifiedAndETagWithBadETag() {
+ Date now = new Date();
+ DefaultHttpHeaders headers = new DefaultHttpHeaders()
+ .lastModified(now)
+ .withETag("fdsa");
+ mockRequest.addHeader("If-None-Match", "asdfds");
+ mockRequest.addHeader("If-Modified-Since", String.valueOf(now.getTime()));
+ headers.apply(mockRequest, mockResponse, new Object());
+
+ assertEquals(SC_OK, mockResponse.getStatus());
+ }
+}
diff --git a/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java
new file mode 100644
index 000000000..54278694f
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java
@@ -0,0 +1,161 @@
+package org.apache.struts2.rest;
+
+import com.opensymphony.xwork2.config.Configuration;
+import com.opensymphony.xwork2.config.ConfigurationManager;
+import com.opensymphony.xwork2.config.entities.PackageConfig;
+import com.opensymphony.xwork2.config.impl.DefaultConfiguration;
+import junit.framework.TestCase;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+public class RestActionMapperTest extends TestCase {
+
+ private RestActionMapper mapper;
+ private ConfigurationManager configManager;
+ private Configuration config;
+ private MockHttpServletRequest req;
+
+ protected void setUp() throws Exception {
+ super.setUp();
+ req = new MockHttpServletRequest();
+ req.setContextPath("/myapp");
+ req.setMethod("GET");
+
+ mapper = new RestActionMapper();
+
+ config = new DefaultConfiguration();
+ PackageConfig pkg = new PackageConfig("myns", "/animals", false, null);
+ PackageConfig pkg2 = new PackageConfig("my", "/my", false, null);
+ config.addPackageConfig("mvns", pkg);
+ config.addPackageConfig("my", pkg2);
+ configManager = new ConfigurationManager() {
+ public Configuration getConfiguration() {
+ return config;
+ }
+ };
+ }
+
+ public void testGetMapping() throws Exception {
+ req.setRequestURI("/myapp/animals/dog");
+ req.setServletPath("/animals/dog");
+
+ ActionMapping mapping = mapper.getMapping(req, configManager);
+
+ assertEquals("/animals", mapping.getNamespace());
+ assertEquals("dog", mapping.getName());
+ assertEquals("index", mapping.getMethod());
+ }
+
+ public void testPostMapping() throws Exception {
+ req.setRequestURI("/myapp/animals/dog");
+ req.setServletPath("/animals/dog");
+ req.setMethod("POST");
+
+ ActionMapping mapping = mapper.getMapping(req, configManager);
+
+ assertEquals("/animals", mapping.getNamespace());
+ assertEquals("dog", mapping.getName());
+ assertEquals("create", mapping.getMethod());
+ }
+
+ public void testDeleteMapping() throws Exception {
+ req.setRequestURI("/myapp/animals/dog/fido");
+ req.setServletPath("/animals/dog/fido");
+ req.setMethod("DELETE");
+
+ ActionMapping mapping = mapper.getMapping(req, configManager);
+
+ assertEquals("/animals", mapping.getNamespace());
+ assertEquals("dog", mapping.getName());
+ assertEquals("destroy", mapping.getMethod());
+ assertEquals("fido", ((String[])mapping.getParams().get("id"))[0]);
+ }
+
+ public void testPutMapping() throws Exception {
+ req.setRequestURI("/myapp/animals/dog/fido");
+ req.setServletPath("/animals/dog/fido");
+ req.setMethod("PUT");
+
+ ActionMapping mapping = mapper.getMapping(req, configManager);
+
+ assertEquals("/animals", mapping.getNamespace());
+ assertEquals("dog", mapping.getName());
+ assertEquals("update", mapping.getMethod());
+ assertEquals("fido", ((String[])mapping.getParams().get("id"))[0]);
+ }
+
+ public void testGetIdMapping() throws Exception {
+ req.setRequestURI("/myapp/animals/dog/fido");
+ req.setServletPath("/animals/dog/fido");
+ req.setMethod("GET");
+
+ ActionMapping mapping = mapper.getMapping(req, configManager);
+
+ assertEquals("/animals", mapping.getNamespace());
+ assertEquals("dog", mapping.getName());
+ assertEquals("show", mapping.getMethod());
+ assertEquals("fido", ((String[])mapping.getParams().get("id"))[0]);
+ }
+
+ public void testNewMapping() throws Exception {
+ req.setRequestURI("/myapp/animals/dog/new");
+ req.setServletPath("/animals/dog/new");
+ req.setMethod("GET");
+
+ ActionMapping mapping = mapper.getMapping(req, configManager);
+
+ assertEquals("/animals", mapping.getNamespace());
+ assertEquals("dog", mapping.getName());
+ assertEquals("editNew", mapping.getMethod());
+ }
+
+ public void testEditMapping() throws Exception {
+ req.setRequestURI("/myapp/animals/dog/fido/edit");
+ req.setServletPath("/animals/dog/fido/edit");
+ req.setMethod("GET");
+
+ ActionMapping mapping = mapper.getMapping(req, configManager);
+
+ assertEquals("/animals", mapping.getNamespace());
+ assertEquals("dog", mapping.getName());
+ assertEquals("fido", ((String[])mapping.getParams().get("id"))[0]);
+ assertEquals("edit", mapping.getMethod());
+ }
+
+ public void testEditSemicolonMapping() throws Exception {
+ req.setRequestURI("/myapp/animals/dog/fido;edit");
+ req.setServletPath("/animals/dog/fido;edit");
+ req.setMethod("GET");
+
+ ActionMapping mapping = mapper.getMapping(req, configManager);
+
+ assertEquals("/animals", mapping.getNamespace());
+ assertEquals("dog", mapping.getName());
+ assertEquals("fido", ((String[])mapping.getParams().get("id"))[0]);
+ assertEquals("edit", mapping.getMethod());
+ }
+
+ public void testParseNameAndNamespace() {
+ tryUri("/foo/23", "", "foo/23");
+ tryUri("/foo/", "", "foo/");
+ tryUri("foo", "", "foo");
+ tryUri("/", "/", "");
+ }
+
+ public void testParseNameAndNamespaceWithNamespaces() {
+ tryUri("/my/foo/23", "/my", "foo/23");
+ tryUri("/my/foo/", "/my", "foo/");
+ }
+
+ public void testParseNameAndNamespaceWithEdit() {
+ tryUri("/my/foo/23;edit", "/my", "foo/23;edit");
+ }
+
+ private void tryUri(String uri, String expectedNamespace, String expectedName) {
+ ActionMapping mapping = new ActionMapping();
+ mapper.parseNameAndNamespace(uri, mapping, configManager);
+ assertEquals(expectedName, mapping.getName());
+ assertEquals(expectedNamespace, mapping.getNamespace());
+ }
+
+}
diff --git a/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/handler/Contact.java b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/handler/Contact.java
new file mode 100644
index 000000000..701798397
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/handler/Contact.java
@@ -0,0 +1,91 @@
+/*
+ * $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.util.HashMap;
+
+public class Contact {
+ private String name;
+ private boolean important;
+ private int age;
+
+ public Contact() {}
+
+ public Contact(String name, boolean important, int age) {
+ this.name = name;
+ this.important = important;
+ this.age = age;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public boolean isImportant() {
+ return important;
+ }
+
+ public void setImportant(boolean important) {
+ this.important = important;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+
+
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Contact contact = (Contact) o;
+
+ if (age != contact.age) return false;
+ if (important != contact.important) return false;
+ if (name != null ? !name.equals(contact.name) : contact.name != null) return false;
+
+ return true;
+ }
+
+ public String toString() {
+ HashMap map = new HashMap();
+ map.put("age", age);
+ map.put("important", important);
+ map.put("name", name);
+ return map.toString();
+ }
+
+ public int hashCode() {
+ int result;
+ result = (name != null ? name.hashCode() : 0);
+ result = 31 * result + (important ? 1 : 0);
+ result = 31 * result + age;
+ return result;
+ }
+}
diff --git a/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/handler/JsonLibHandlerTest.java b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/handler/JsonLibHandlerTest.java
new file mode 100644
index 000000000..22303aaea
--- /dev/null
+++ b/plugins/struts2-rest-plugin/src/test/java/org/apache/struts2/rest/handler/JsonLibHandlerTest.java
@@ -0,0 +1,62 @@
+/*
+ * $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 junit.framework.TestCase;
+
+import java.io.StringWriter;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Arrays;
+
+public class JsonLibHandlerTest extends TestCase {
+
+ public void testFromObject() throws IOException {
+ Contact contact = new Contact("bob", true, 44);
+
+ StringWriter writer = new StringWriter();
+ JsonLibHandler handler = new JsonLibHandler();
+ handler.fromObject(contact, "success", writer);
+
+ assertEquals("{\"age\":44,\"important\":true,\"name\":\"bob\"}", writer.toString());
+ }
+
+ public void testFromObjectArray() throws IOException {
+ Contact contact = new Contact("bob", true, 44);
+
+ StringWriter writer = new StringWriter();
+ JsonLibHandler handler = new JsonLibHandler();
+ handler.fromObject(Arrays.asList(contact), "success", writer);
+
+ assertEquals("[{\"age\":44,\"important\":true,\"name\":\"bob\"}]", writer.toString());
+ }
+
+ public void testToObject() throws IOException {
+ Contact contact = new Contact("bob", true, 44);
+
+ Contact target = new Contact();
+ StringReader reader = new StringReader("{\"age\":44,\"important\":true,\"name\":\"bob\"}");
+ JsonLibHandler handler = new JsonLibHandler();
+ handler.toObject(reader, target);
+
+ assertEquals(contact, target);
+ }
+}