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]); + } }