Author: lukaszlenart
Date: Sat Oct 23 20:19:47 2010
New Revision: 1026675
URL: http://svn.apache.org/viewvc?rev=1026675&view=rev
Log:
Solved WW-3494 - many improvments to REST plugin
Added:
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java
Modified:
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/HttpHeaders.java
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionSupport.java
struts/struts2/trunk/plugins/rest/src/main/resources/struts-plugin.xml
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java
Modified:
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java
URL:
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
---
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java
(original)
+++
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java
Sat Oct 23 20:19:47 2010
@@ -61,4 +61,12 @@ public interface ContentTypeHandlerManag
*/
String handleResult(ActionConfig actionConfig, Object methodResult, Object
target)
throws IOException;
+
+ /**
+ * Finds the extension in the url
+ *
+ * @param url The url
+ * @return The extension
+ */
+ String findExtension(String url);
}
Modified:
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java
URL:
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
---
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java
(original)
+++
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java
Sat Oct 23 20:19:47 2010
@@ -102,9 +102,6 @@ public class DefaultContentTypeHandlerMa
if (handler == null) {
String extension = findExtension(req.getRequestURI());
- if (extension == null) {
- extension = defaultExtension;
- }
handler = handlersByExtension.get(extension);
}
return handler;
@@ -117,9 +114,6 @@ public class DefaultContentTypeHandlerMa
*/
public ContentTypeHandler getHandlerForResponse(HttpServletRequest req,
HttpServletResponse res) {
String extension = findExtension(req.getRequestURI());
- if (extension == null) {
- extension = defaultExtension;
- }
return handlersByExtension.get(extension);
}
@@ -137,33 +131,7 @@ public class DefaultContentTypeHandlerMa
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 = getHandlerForResponse(req, res);
if (handler != null) {
String extCode = resultCode+"-"+handler.getExtension();
@@ -192,12 +160,12 @@ public class DefaultContentTypeHandlerMa
* @param url The url
* @return The extension
*/
- protected String findExtension(String url) {
+ public String findExtension(String url) {
int dotPos = url.lastIndexOf('.');
int slashPos = url.lastIndexOf('/');
if (dotPos > slashPos && dotPos > -1) {
return url.substring(dotPos+1);
}
- return null;
+ return defaultExtension;
}
}
Modified:
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java
URL:
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
---
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java
(original)
+++
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultHttpHeaders.java
Sat Oct 23 20:19:47 2010
@@ -152,6 +152,14 @@ public class DefaultHttpHeaders implemen
return status;
}
+ public void setStatus(int s) {
+ status = s;
+ }
+
+ public String getResultCode() {
+ return resultCode;
+ }
+
Modified:
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/HttpHeaders.java
URL:
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/HttpHeaders.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
---
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/HttpHeaders.java
(original)
+++
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/HttpHeaders.java
Sat Oct 23 20:19:47 2010
@@ -43,4 +43,15 @@ public interface HttpHeaders {
* The HTTP status code
*/
int getStatus();
+
+ /**
+ * The HTTP status code
+ */
+ void setStatus(int status);
+
+ /**
+ * The result code to process
+ */
+ String getResultCode();
+
}
\ No newline at end of file
Modified:
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
URL:
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
---
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
(original)
+++
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
Sat Oct 23 20:19:47 2010
@@ -24,17 +24,30 @@ package org.apache.struts2.rest;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.DefaultActionInvocation;
import com.opensymphony.xwork2.Result;
-import com.opensymphony.xwork2.UnknownHandlerManager;
+import com.opensymphony.xwork2.Action;
+import com.opensymphony.xwork2.ModelDriven;
+import com.opensymphony.xwork2.ValidationAware;
+import com.opensymphony.xwork2.config.ConfigurationException;
+import com.opensymphony.xwork2.config.entities.ResultConfig;
import com.opensymphony.xwork2.config.entities.ActionConfig;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;
import com.opensymphony.xwork2.util.profiling.UtilTimerStack;
-import java.io.IOException;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.struts2.ServletActionContext;
+import org.apache.struts2.dispatcher.HttpHeaderResult;
+import org.apache.struts2.rest.handler.ContentTypeHandler;
+import org.apache.struts2.rest.handler.HtmlHandler;
/**
@@ -49,11 +62,28 @@ public class RestActionInvocation extend
private static final Logger LOG =
LoggerFactory.getLogger(RestActionInvocation.class);
private ContentTypeHandlerManager handlerSelector;
+ private boolean logger;
+ private String defaultErrorResultName;
+ protected HttpHeaders httpHeaders;
+ protected Object target;
+ protected boolean isFirstInterceptor = true;
+ protected boolean hasErrors;
+
protected RestActionInvocation(Map extraContext, boolean pushAction) {
super(extraContext, pushAction);
}
+ @Inject("struts.rest.logger")
+ public void setLogger(String value) {
+ logger = new Boolean(value);
+ }
+
+ @Inject("struts.rest.defaultErrorResultName")
+ public void setDefaultErrorResultName(String value) {
+ defaultErrorResultName = value;
+ }
+
@Inject
public void setMimeTypeHandlerSelector(ContentTypeHandlerManager sel) {
this.handlerSelector = sel;
@@ -100,7 +130,7 @@ public class RestActionInvocation extend
methodResult = method.invoke(action, new Object[0]);
}
- return processResult(actionConfig, methodResult);
+ return saveResult(actionConfig, methodResult);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("The " + methodName + "() is
not defined in action " + getAction().getClass() + "");
} catch (InvocationTargetException e) {
@@ -123,16 +153,301 @@ public class RestActionInvocation extend
}
}
- protected String processResult(ActionConfig actionConfig, Object
methodResult) throws IOException {
- if (methodResult instanceof Result) {
- this.explicitResult = (Result) methodResult;
+ /**
+ * Save the result to be used later.
+ * @param actionConfig
+ * @param methodResult the result of the action.
+ * @return the result code to process.
+ *
+ * @throws ConfigurationException If it is an incorrect result.
+ */
+ protected String saveResult(ActionConfig actionConfig, Object
methodResult) {
+ if (methodResult instanceof Result) {
+ explicitResult = (Result) methodResult;
// Wire the result automatically
container.inject(explicitResult);
- return null;
+ } else if (methodResult instanceof HttpHeaders) {
+ httpHeaders = (HttpHeaders) methodResult;
+ resultCode = httpHeaders.getResultCode();
+ } else if (methodResult instanceof String) {
+ resultCode = (String) methodResult;
} else if (methodResult != null) {
- resultCode = handlerSelector.handleResult(actionConfig,
methodResult, action);
+ throw new ConfigurationException("The result type " +
methodResult.getClass()
+ + " is not allowed. Use the type String,
HttpHeaders or Result.");
}
return resultCode;
}
+ @Override
+ public String invoke() throws Exception {
+ long startTime = 0;
+
+ boolean executeResult = false;
+ if (isFirstInterceptor) {
+ startTime = System.currentTimeMillis();
+ executeResult = true;
+ isFirstInterceptor = false;
+ }
+
+ // Normal invoke without execute the result
+ proxy.setExecuteResult(false);
+ resultCode = super.invoke();
+
+ // Execute the result when the last interceptor has finished
+ if (executeResult) {
+ long middleTime = System.currentTimeMillis();
+
+ try {
+ processResult();
+
+ } catch (ConfigurationException e) {
+ throw e;
+
+ } catch (Exception e) {
+
+ // Error proccesing the result
+ LOG.error("Exception processing the result.", e);
+
+ if (!ServletActionContext.getResponse().isCommitted()) {
+ ServletActionContext.getResponse()
+
.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ stack.set("exception", e);
+ result = null;
+ resultCode = null;
+ processResult();
+ }
+ }
+
+ // Log execution + result time
+ logger(startTime, middleTime);
+ }
+
+ return resultCode;
+ }
+
+ protected void processResult() throws Exception {
+ String timerKey = "processResult: " + getResultCode();
+ try {
+ UtilTimerStack.push(timerKey);
+
+ HttpServletRequest request = ServletActionContext.getRequest();
+ HttpServletResponse response = ServletActionContext.getResponse();
+
+ // Select the target
+ selectTarget();
+
+ // Get the httpHeaders
+ if (httpHeaders == null) {
+ httpHeaders = new DefaultHttpHeaders(resultCode);
+ }
+
+ // Apply headers
+ if (!hasErrors) {
+ httpHeaders.apply(request, response, target);
+ } else {
+ disableCatching(response);
+ }
+
+ // Don't return content on a not modified
+ if (httpHeaders.getStatus() != HttpServletResponse.SC_NOT_MODIFIED
) {
+ executeResult();
+ } else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Result not processed because the status code
is not modified.");
+ }
+ }
+
+ } finally {
+ UtilTimerStack.pop(timerKey);
+ }
+ }
+
+ /**
+ * Execute the current result. If it is an error and no result is selected
load
+ * the default error result (default-error).
+ */
+ private void executeResult() throws Exception {
+
+ // Get handler by representation
+ ContentTypeHandler handler = handlerSelector.getHandlerForResponse(
+ ServletActionContext.getRequest(),
ServletActionContext.getResponse());
+
+ // get the result
+ this.result = createResult();
+
+ if (this.result instanceof HttpHeaderResult) {
+
+ // execute the result to apply headers and status in
every representations
+ this.result.execute(this);
+ updateStatusFromResult();
+ }
+
+ if (handler != null && !(handler instanceof HtmlHandler)) {
+
+ // Specific representation (json, xml...)
+ resultCode = handlerSelector.handleResult(
+ this.getProxy().getConfig(),
httpHeaders, target);
+
+ } else {
+
+ // Normal struts execution (html o other struts result)
+ findResult();
+ if (result != null) {
+ this.result.execute(this);
+
+ } else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("No result returned for action " +
getAction().getClass().getName()
+ + " at " +
proxy.getConfig().getLocation());
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the status code from HttpHeaderResult
+ * and it is saved in the HttpHeaders object.
+ * @throws Exception
+ */
+ private void updateStatusFromResult() {
+
+ if (this.result instanceof HttpHeaderResult) {
+ try {
+ Field field =
result.getClass().getDeclaredField("status");
+ if (field != null) {
+ field.setAccessible(true);
+ int status = (Integer)field.get(result);
+ if (status != -1) {
+ this.httpHeaders.setStatus(status);
+ }
+ }
+ } catch (Exception e) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(e.getMessage(), e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Find the most appropriate result:
+ * - Find by result code.
+ * - If it is an error, find the default error result.
+ *
+ * @throws ConfigurationException If not result can be found
+ */
+ private void findResult() throws Exception {
+
+ boolean isHttpHeaderResult = false;
+ if (result != null && this.result instanceof HttpHeaderResult) {
+ result = null;
+ isHttpHeaderResult = true;
+ }
+
+ if (result == null && resultCode != null &&
!Action.NONE.equals(resultCode)
+ && unknownHandlerManager.hasUnknownHandlers()) {
+
+ // Find result by resultCode
+ this.result = unknownHandlerManager.handleUnknownResult(
+ invocationContext, proxy.getActionName(),
proxy.getConfig(), resultCode);
+ }
+
+ if (this.result == null && this.hasErrors &&
defaultErrorResultName != null) {
+
+ // Get default error result
+ ResultConfig resultConfig = this.proxy.getConfig().getResults()
+ .get(defaultErrorResultName);
+ if (resultConfig != null) {
+ this.result = objectFactory.buildResult(resultConfig,
+ invocationContext.getContextMap());
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Found default error result.");
+ }
+ }
+ }
+
+ if (result == null && resultCode != null &&
+ !Action.NONE.equals(resultCode) && !isHttpHeaderResult)
{
+ throw new ConfigurationException("No result defined for action "
+ + getAction().getClass().getName()
+ + " and result " + getResultCode(), proxy.getConfig());
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ protected void selectTarget() {
+
+ // Select target (content to return)
+ Throwable e = (Throwable)stack.findValue("exception");
+ if (e != null) {
+
+ // Exception
+ target = e;
+ hasErrors = true;
+
+ } else if (action instanceof ValidationAware &&
((ValidationAware)action).hasErrors()) {
+
+ // Error messages
+ ValidationAware validationAwareAction =
((ValidationAware)action);
+
+ Map errors = new HashMap();
+ if (validationAwareAction.getActionErrors().size() > 0) {
+ errors.put("actionErrors",
validationAwareAction.getActionErrors());
+ }
+ if (validationAwareAction.getFieldErrors().size() > 0) {
+ errors.put("fieldErrors",
validationAwareAction.getFieldErrors());
+ }
+ target = errors;
+ hasErrors = true;
+
+ } else if (action instanceof ModelDriven) {
+
+ // Model
+ target = ((ModelDriven)action).getModel();
+
+ } else {
+ target = action;
+ }
+
+ // don't return any content for PUT, DELETE, and POST where there are
no errors
+ if (!hasErrors &&
!"get".equalsIgnoreCase(ServletActionContext.getRequest().getMethod())) {
+ target = null;
+ }
+ }
+
+ private void disableCatching(HttpServletResponse response) {
+ // No cache
+ response.setHeader("Cache-Control", "no-cache");
+ response.setDateHeader("Last-Modified", 0);
+ response.setHeader("ETag", "-1");
+ }
+
+ private void logger(long startTime, long middleTime) {
+ if (logger && LOG.isInfoEnabled()) {
+ long endTime = System.currentTimeMillis();
+ long executionTime = middleTime - startTime;
+ long processResult = endTime - middleTime;
+ long total = endTime - startTime;
+
+ String message = "Executed action [/";
+ String namespace = getProxy().getNamespace();
+ if ((namespace != null) && (namespace.trim().length() >
1)) {
+ message += namespace + "/";
+ }
+ message += getProxy().getActionName() + "!" +
getProxy().getMethod();
+ String extension = handlerSelector.findExtension(
+
ServletActionContext.getRequest().getRequestURI());
+ if (extension != null) {
+ message += "!" + extension;
+ }
+ if (httpHeaders != null) {
+ message += "!" + httpHeaders.getStatus();
+ }
+ message += "] took " + total + " ms (execution: " +
executionTime
+ + " ms, result: " + processResult + " ms)";
+
+ LOG.info(message);
+ }
+ }
+
}
Modified:
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java
URL:
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
---
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java
(original)
+++
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionMapper.java
Sat Oct 23 20:19:47 2010
@@ -106,6 +106,9 @@ public class RestActionMapper extends De
private String newMethodName = "editNew";
private String deleteMethodName = "destroy";
private String putMethodName = "update";
+ private String optionsMethodName = "options";
+ private String postContinueMethodName = "createContinue";
+ private String putContinueMethodName = "updateContinue";
public RestActionMapper() {
}
@@ -154,6 +157,21 @@ public class RestActionMapper extends De
this.putMethodName = putMethodName;
}
+ @Inject(required=false,value="struts.mapper.optionsMethodName")
+ public void setOptionsMethodName(String optionsMethodName) {
+ this.optionsMethodName = optionsMethodName;
+ }
+
+ @Inject(required=false,value="struts.mapper.postContinueMethodName")
+ public void setPostContinueMethodName(String postContinueMethodName) {
+ this.postContinueMethodName = postContinueMethodName;
+ }
+
+ @Inject(required=false,value="struts.mapper.putContinueMethodName")
+ public void setPutContinueMethodName(String putContinueMethodName) {
+ this.putContinueMethodName = putContinueMethodName;
+ }
+
public ActionMapping getMapping(HttpServletRequest request,
ConfigurationManager configManager) {
ActionMapping mapping = new ActionMapping();
@@ -209,8 +227,11 @@ public class RestActionMapper extends De
// 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) {
+ if (isOptions(request)) {
+ mapping.setMethod(optionsMethodName);
+
+ // Handle uris with no id, possibly ending in '/'
+ } else if (lastSlashPos == -1 || lastSlashPos ==
fullName.length() -1) {
// Index e.g. foo
if (isGet(request)) {
@@ -218,7 +239,11 @@ public class RestActionMapper extends De
// Creating a new entry on POST e.g. foo
} else if (isPost(request)) {
- mapping.setMethod(postMethodName);
+ if (isExpectContinue(request)) {
+ mapping.setMethod(postContinueMethodName);
+ } else {
+ mapping.setMethod(postMethodName);
+ }
}
// Handle uris with an id at the end
@@ -243,7 +268,11 @@ public class RestActionMapper extends De
// Updating an item e.g. foo/1
} else if (isPut(request)) {
- mapping.setMethod(putMethodName);
+ if (isExpectContinue(request)) {
+ mapping.setMethod(putContinueMethodName);
+ } else {
+ mapping.setMethod(putMethodName);
+ }
}
}
}
@@ -334,4 +363,13 @@ public class RestActionMapper extends De
}
}
+ protected boolean isOptions(HttpServletRequest request) {
+ return "options".equalsIgnoreCase(request.getMethod());
+ }
+
+ protected boolean isExpectContinue(HttpServletRequest request) {
+ String expect = request.getHeader("Expect");
+ return (expect != null &&
expect.toLowerCase().contains("100-continue"));
+ }
+
}
Modified:
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionSupport.java
URL:
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionSupport.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
---
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionSupport.java
(original)
+++
struts/struts2/trunk/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionSupport.java
Sat Oct 23 20:19:47 2010
@@ -21,6 +21,13 @@
package org.apache.struts2.rest;
+import java.lang.reflect.Method;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.struts2.ServletActionContext;
+
import com.opensymphony.xwork2.ActionSupport;
/**
@@ -29,7 +36,83 @@ import com.opensymphony.xwork2.ActionSup
*/
public class RestActionSupport extends ActionSupport {
- public String index() throws Exception {
+ private static final long serialVersionUID = -889518620073576882L;
+
+ private static final String DELETE = "DELETE";
+ private static final String PUT = "PUT";
+ private static final String POST = "POST";
+ private static final String GET = "GET";
+ private static final String OPTIONS = "OPTIONS";
+ private static final String DIVIDER = ", ";
+
+ /**
+ * Default execution.
+ * @return object because it can return string, result or httpHeader.
+ * @throws Exception
+ */
+ public Object index() throws Exception {
return execute();
}
+
+ /**
+ * Inspect the implemented methods to know the allowed http methods.
+ *
+ * @return Include the header "Allow" with the allowed http methods.
+ */
+ public HttpHeaders options() {
+
+ String methods = OPTIONS;
+
+ Method[] meths = this.getClass().getDeclaredMethods();
+ for (Method m : meths) {
+ String methodName = m.getName();
+ if (!methods.contains(GET) &&
+ (methodName.equals("index")
+ || methodName.equals("show")
+ || methodName.equals("edit")
+ || methodName.equals("editNew"))) {
+ methods += DIVIDER + GET;
+ } else if (methodName.equals("create")) {
+ methods += DIVIDER + POST;
+ } else if (methodName.equals("update")) {
+ methods += DIVIDER + PUT;
+ }else if (methodName.equals("destroy")) {
+ methods += DIVIDER + DELETE;
+ }
+ }
+
+ HttpServletRequest request = ServletActionContext.getRequest();
+ HttpServletResponse response = ServletActionContext.getResponse();
+ response.addHeader("Allow", methods);
+
+ DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
+ httpHeaders.apply(request, response, this);
+ httpHeaders.disableCaching().withStatus(HttpServletResponse.SC_OK);
+
+ return httpHeaders;
+ }
+
+ /**
+ * By default, return continue.
+ * Is possible override the method to return expectation failed.
+ *
+ * @return continue
+ */
+ public HttpHeaders createContinue() {
+ return new DefaultHttpHeaders()
+ .disableCaching()
+ .withStatus(HttpServletResponse.SC_CONTINUE);
+ }
+
+ /**
+ * By default, return continue.
+ * Is possible override the method to return expectation failed.
+ *
+ * @return continue
+ */
+ public HttpHeaders updateContinue() {
+ return new DefaultHttpHeaders()
+ .disableCaching()
+ .withStatus(HttpServletResponse.SC_CONTINUE);
+ }
}
Modified: struts/struts2/trunk/plugins/rest/src/main/resources/struts-plugin.xml
URL:
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/main/resources/struts-plugin.xml?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
--- struts/struts2/trunk/plugins/rest/src/main/resources/struts-plugin.xml
(original)
+++ struts/struts2/trunk/plugins/rest/src/main/resources/struts-plugin.xml Sat
Oct 23 20:19:47 2010
@@ -40,6 +40,8 @@
<constant name="struts.actionProxyFactory" value="rest" />
<constant name="struts.rest.defaultExtension" value="xhtml" />
+ <constant name="struts.rest.logger" value="true" />
+ <constant name="struts.rest.defaultErrorResultName" value="default-error"
/>
<constant name="struts.mapper.class" value="rest" />
<constant name="struts.mapper.idParameterName" value="id" />
<constant name="struts.action.extension" value="xhtml,,xml,json" />
Modified:
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java
URL:
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
---
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java
(original)
+++
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/DefaultHttpHeadersTest.java
Sat Oct 23 20:19:47 2010
@@ -180,4 +180,20 @@ public class DefaultHttpHeadersTest exte
assertEquals(SC_OK, mockResponse.getStatus());
}
+
+ public void testApplyOptions() {
+
+ String methods = "OPTIONS, GET, POST, PUT";
+ String allow = "Allow";
+
+ mockResponse.addHeader(allow, methods);
+
+ DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
+ httpHeaders.apply(mockRequest, mockResponse, this);
+ httpHeaders.disableCaching().withStatus(SC_OK);
+
+ assertEquals(methods, mockResponse.getHeader(allow));
+ assertEquals(SC_OK, mockResponse.getStatus());
+
+ }
}
Added:
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java
URL:
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java?rev=1026675&view=auto
==============================================================================
---
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java
(added)
+++
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java
Sat Oct 23 20:19:47 2010
@@ -0,0 +1,270 @@
+package org.apache.struts2.rest;
+
+import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import junit.framework.TestCase;
+
+import org.apache.struts2.ServletActionContext;
+import org.apache.struts2.dispatcher.HttpHeaderResult;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.DefaultUnknownHandlerManager;
+import com.opensymphony.xwork2.ModelDriven;
+import com.opensymphony.xwork2.ObjectFactory;
+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.mock.MockActionProxy;
+import com.opensymphony.xwork2.mock.MockInterceptor;
+import com.opensymphony.xwork2.util.XWorkTestCaseHelper;
+
+public class RestActionInvocationTest extends TestCase {
+
+ RestActionInvocation restActionInvocation;
+ MockHttpServletRequest request;
+ MockHttpServletResponse response;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ restActionInvocation = new RestActionInvocationTester();
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ ServletActionContext.setRequest(request);
+ ServletActionContext.setResponse(response);
+
+ }
+
+ /**
+ * Test the correct action results: null, String, HttpHeaders, Result
+ * @throws Exception
+ */
+ public void testSaveResult() throws Exception {
+
+ Object methodResult = "index";
+ ActionConfig actionConfig =
restActionInvocation.getProxy().getConfig();
+ assertEquals("index",
restActionInvocation.saveResult(actionConfig, methodResult));
+
+ setUp();
+ methodResult = new DefaultHttpHeaders("show");
+ assertEquals("show", restActionInvocation.saveResult(actionConfig,
methodResult));
+ assertEquals(methodResult, restActionInvocation.httpHeaders);
+
+ setUp();
+ methodResult = new HttpHeaderResult(HttpServletResponse.SC_ACCEPTED);
+ assertEquals(null, restActionInvocation.saveResult(actionConfig,
methodResult));
+ assertEquals(methodResult, restActionInvocation.createResult());
+
+ setUp();
+ try {
+ methodResult = new Object();
+ restActionInvocation.saveResult(actionConfig, methodResult);
+
+ // ko
+ assertFalse(true);
+
+ } catch (ConfigurationException c) {
+ // ok, object not allowed
+ }
+ }
+
+ /**
+ * Test the target selection: exception, error messages, model and null
+ * @throws Exception
+ */
+ public void testSelectTarget() throws Exception {
+
+ // Exception
+ Exception e = new Exception();
+ restActionInvocation.getStack().set("exception", e);
+ restActionInvocation.selectTarget();
+ assertEquals(e, restActionInvocation.target);
+
+ // Error messages
+ setUp();
+ String actionMessage = "Error!";
+ RestActionSupport action =
(RestActionSupport)restActionInvocation.getAction();
+ action.addActionError(actionMessage);
+ Map errors = new HashMap();
+ List<String> list = new ArrayList<String>();
+ list.add(actionMessage);
+ errors.put("actionErrors", list);
+ restActionInvocation.selectTarget();
+ assertEquals(errors, restActionInvocation.target);
+
+ // Model with get and no content in post, put, delete
+ setUp();
+ RestAction restAction =
(RestAction)restActionInvocation.getAction();
+ List<String> model = new ArrayList<String>();
+ model.add("Item");
+ restAction.model = model;
+ request.setMethod("GET");
+ restActionInvocation.selectTarget();
+ assertEquals(model, restActionInvocation.target);
+ request.setMethod("POST");
+ restActionInvocation.selectTarget();
+ assertEquals(null, restActionInvocation.target);
+ request.setMethod("PUT");
+ restActionInvocation.selectTarget();
+ assertEquals(null, restActionInvocation.target);
+ request.setMethod("DELETE");
+ restActionInvocation.selectTarget();
+ assertEquals(null, restActionInvocation.target);
+
+ }
+
+ /**
+ * Test the not modified status code.
+ * @throws Exception
+ */
+ public void testResultNotModified() throws Exception {
+
+ request.addHeader("If-None-Match", "123");
+ request.setMethod("GET");
+
+ RestAction restAction =
(RestAction)restActionInvocation.getAction();
+ List<String> model = new ArrayList<String>() {
+ @Override
+ public int hashCode() {
+ return 123;
+ }
+ };
+ model.add("Item");
+ restAction.model = model;
+
+ restActionInvocation.processResult();
+ assertEquals(SC_NOT_MODIFIED, response.getStatus());
+
+ }
+
+ /**
+ * Test the default error result.
+ * @throws Exception
+ */
+ public void testDefaultErrorResult() throws Exception {
+
+ // Exception
+ Exception e = new Exception();
+ restActionInvocation.getStack().set("exception", e);
+ request.setMethod("GET");
+
+ RestAction restAction =
(RestAction)restActionInvocation.getAction();
+ List<String> model = new ArrayList<String>();
+ model.add("Item");
+ restAction.model = model;
+
+ restActionInvocation.setDefaultErrorResultName("default-error");
+ ResultConfig resultConfig = new
ResultConfig.Builder("default-error",
+ "org.apache.struts2.dispatcher.HttpHeaderResult")
+ .addParam("status", "123").build();
+ ActionConfig actionConfig = new
ActionConfig.Builder("org.apache.rest",
+ "RestAction", "org.apache.rest.RestAction")
+ .addResultConfig(resultConfig)
+ .build();
+
((MockActionProxy)restActionInvocation.getProxy()).setConfig(actionConfig);
+
+ restActionInvocation.processResult();
+ assertEquals(123, response.getStatus());
+
+ }
+
+ public void testNoResult() throws Exception {
+
+ RestAction restAction =
(RestAction)restActionInvocation.getAction();
+ List<String> model = new ArrayList<String>();
+ model.add("Item");
+ restAction.model = model;
+ request.setMethod("GET");
+ restActionInvocation.setResultCode("index");
+
+ try {
+ restActionInvocation.processResult();
+
+ // ko
+ assertFalse(true);
+
+ } catch (ConfigurationException c) {
+ // ok, no result
+ }
+
+ }
+
+ /**
+ * Test the global execution
+ * @throws Exception
+ */
+ public void testInvoke() throws Exception {
+
+ // Default index method return 'success'
+
((MockActionProxy)restActionInvocation.getProxy()).setMethod("index");
+
+ // Define result 'success'
+ ResultConfig resultConfig = new ResultConfig.Builder("success",
+ "org.apache.struts2.dispatcher.HttpHeaderResult")
+ .addParam("status", "123").build();
+ ActionConfig actionConfig = new
ActionConfig.Builder("org.apache.rest",
+ "RestAction", "org.apache.rest.RestAction")
+ .addResultConfig(resultConfig)
+ .build();
+
((MockActionProxy)restActionInvocation.getProxy()).setConfig(actionConfig);
+
+ request.setMethod("GET");
+
+ restActionInvocation.invoke();
+
+ assertEquals(123, response.getStatus());
+ }
+
+
+ class RestActionInvocationTester extends RestActionInvocation {
+ RestActionInvocationTester() {
+ super(new HashMap<String,String>(), true);
+ List<InterceptorMapping> interceptorMappings = new
ArrayList<InterceptorMapping>();
+ MockInterceptor mockInterceptor = new MockInterceptor();
+ mockInterceptor.setFoo("interceptor");
+ mockInterceptor.setExpectedFoo("interceptor");
+ interceptorMappings.add(new InterceptorMapping("interceptor",
mockInterceptor));
+ interceptors = interceptorMappings.iterator();
+ MockActionProxy actionProxy = new MockActionProxy();
+ ActionConfig actionConfig = new
ActionConfig.Builder("org.apache.rest",
+ "RestAction",
"org.apache.rest.RestAction").build();
+ actionProxy.setConfig(actionConfig);
+ proxy = actionProxy;
+ action = new RestAction();
+ setMimeTypeHandlerSelector(new DefaultContentTypeHandlerManager());
+ unknownHandlerManager = new DefaultUnknownHandlerManager();
+ try {
+ XWorkTestCaseHelper.setUp();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ invocationContext = ActionContext.getContext();
+ container = ActionContext.getContext().getContainer();
+ stack = ActionContext.getContext().getValueStack();
+ objectFactory =
container.getInstance(ObjectFactory.class);
+
+ }
+
+ }
+
+ class RestAction extends RestActionSupport implements
ModelDriven<List<String>> {
+
+ List<String> model;
+
+ public List<String> getModel() {
+ return model;
+ }
+
+ }
+}
Modified:
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java
URL:
http://svn.apache.org/viewvc/struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java?rev=1026675&r1=1026674&r2=1026675&view=diff
==============================================================================
---
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java
(original)
+++
struts/struts2/trunk/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionMapperTest.java
Sat Oct 23 20:19:47 2010
@@ -191,5 +191,44 @@ public class RestActionMapperTest extend
assertEquals(expectedName, mapping.getName());
assertEquals(expectedNamespace, mapping.getNamespace());
}
+
+ public void testOptionsMapping() throws Exception {
+ req.setRequestURI("/myapp/animals/dog");
+ req.setServletPath("/animals/dog");
+ req.setMethod("OPTIONS");
+
+ ActionMapping mapping = mapper.getMapping(req, configManager);
+
+ assertEquals("/animals", mapping.getNamespace());
+ assertEquals("dog", mapping.getName());
+ assertEquals("options", mapping.getMethod());
+ }
+
+ public void testPostContinueMapping() throws Exception {
+ req.setRequestURI("/myapp/animals/dog");
+ req.setServletPath("/animals/dog");
+ req.setMethod("POST");
+ req.addHeader("Expect", "100-continue");
+
+ ActionMapping mapping = mapper.getMapping(req, configManager);
+
+ assertEquals("/animals", mapping.getNamespace());
+ assertEquals("dog", mapping.getName());
+ assertEquals("createContinue", mapping.getMethod());
+ }
+
+ public void testPutContinueMapping() throws Exception {
+ req.setRequestURI("/myapp/animals/dog/fido");
+ req.setServletPath("/animals/dog/fido");
+ req.setMethod("PUT");
+ req.addHeader("Expect", "100-continue");
+
+ ActionMapping mapping = mapper.getMapping(req, configManager);
+
+ assertEquals("/animals", mapping.getNamespace());
+ assertEquals("dog", mapping.getName());
+ assertEquals("updateContinue", mapping.getMethod());
+ assertEquals("fido", ((String[])mapping.getParams().get("id"))[0]);
+ }
}