From 0b8fcdf609b1da4b92387ffc9258870b3182b9ae Mon Sep 17 00:00:00 2001 From: "Donald J. Brown" Date: Sat, 12 Jul 2008 15:55:58 +0000 Subject: [PATCH] Adding ability to customize validation failure status code, refactoring content type handler manager into interface and impl for easier testing WW-2358 git-svn-id: https://svn.apache.org/repos/asf/struts/struts2/trunk@676195 13f79535-47bb-0310-9956-ffa450edef68 --- .../rest/ContentTypeHandlerManager.java | 155 +------------- .../DefaultContentTypeHandlerManager.java | 196 ++++++++++++++++++ .../struts2/rest/RestWorkflowInterceptor.java | 9 +- .../rest/src/main/resources/struts-plugin.xml | 2 +- .../rest/ContentTypeHandlerManagerTest.java | 6 +- .../rest/RestWorkflowInterceptorTest.java | 65 ++++++ 6 files changed, 284 insertions(+), 149 deletions(-) create mode 100644 plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java create mode 100644 plugins/rest/src/test/java/org/apache/struts2/rest/RestWorkflowInterceptorTest.java diff --git a/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java b/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java index 53920cf66..5efab766d 100644 --- a/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java +++ b/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeHandlerManager.java @@ -21,177 +21,44 @@ package org.apache.struts2.rest; -import com.opensymphony.xwork2.ModelDriven; -import com.opensymphony.xwork2.config.entities.ActionConfig; -import com.opensymphony.xwork2.inject.Container; -import com.opensymphony.xwork2.inject.Inject; -import org.apache.struts2.ServletActionContext; import org.apache.struts2.rest.handler.ContentTypeHandler; 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 com.opensymphony.xwork2.config.entities.ActionConfig; + import java.io.IOException; -import java.io.StringWriter; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; /** - * Manages {@link ContentTypeHandler} instances and uses them to - * process results + * Manages content type handlers */ -public class ContentTypeHandlerManager { +public interface ContentTypeHandlerManager { + String STRUTS_REST_HANDLER_OVERRIDE_PREFIX = "struts.rest.handlerOverride."; - /** ContentTypeHandlers keyed by the extension */ - Map handlersByExtension = new HashMap(); - /** ContentTypeHandlers keyed by the content-type */ - Map handlersByContentType = new HashMap(); - - String defaultExtension; - public static final String STRUTS_REST_HANDLER_OVERRIDE_PREFIX = "struts.rest.handlerOverride."; - - @Inject("struts.rest.defaultExtension") - public void setDefaultExtension(String name) { - this.defaultExtension = name; - } - - @Inject - public void setContainer(Container container) { - Set names = container.getInstanceNames(ContentTypeHandler.class); - for (String name : names) { - ContentTypeHandler handler = container.getInstance(ContentTypeHandler.class, name); - - if (handler.getExtension() != null) { - // 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 (!handlersByExtension.containsKey(handler.getExtension())) { - handler = container.getInstance(ContentTypeHandler.class, overrideName); - } else { - // overriding handler has already been registered - continue; - } - } - this.handlersByExtension.put(handler.getExtension(), handler); - } - - if (handler.getContentType() != null) { - this.handlersByContentType.put(handler.getContentType(), handler); - } - } - } - /** * Gets the handler for the request by looking at the request content type and extension * @param req The request * @return The appropriate handler */ - public ContentTypeHandler getHandlerForRequest(HttpServletRequest req) { - ContentTypeHandler handler = null; - String contentType = req.getContentType(); - if (contentType != null) { - handler = handlersByContentType.get(contentType); - } - if (handler == null) { - String extension = findExtension(req.getRequestURI()); - if (extension == null) { - extension = defaultExtension; - } - handler = handlersByExtension.get(extension); - } - return handler; - } + ContentTypeHandler getHandlerForRequest(HttpServletRequest req); /** * Gets the handler for the response by looking at the extension of the request * @param req The request * @return The appropriate handler */ - public ContentTypeHandler getHandlerForResponse(HttpServletRequest req, HttpServletResponse res) { - String extension = findExtension(req.getRequestURI()); - if (extension == null) { - extension = defaultExtension; - } - return handlersByExtension.get(extension); - } + ContentTypeHandler getHandlerForResponse(HttpServletRequest req, HttpServletResponse res); /** * 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 = getHandlerForResponse(req, res); - 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; - } + String handleResult(ActionConfig actionConfig, Object methodResult, Object target) + throws IOException; } diff --git a/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java b/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java new file mode 100644 index 000000000..11b751162 --- /dev/null +++ b/plugins/rest/src/main/java/org/apache/struts2/rest/DefaultContentTypeHandlerManager.java @@ -0,0 +1,196 @@ +/* + * $Id$ + * + * 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.ModelDriven; +import com.opensymphony.xwork2.config.entities.ActionConfig; +import com.opensymphony.xwork2.inject.Container; +import com.opensymphony.xwork2.inject.Inject; +import org.apache.struts2.ServletActionContext; +import org.apache.struts2.rest.handler.ContentTypeHandler; + +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.io.IOException; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Manages {@link ContentTypeHandler} instances and uses them to + * process results + */ +public class DefaultContentTypeHandlerManager implements ContentTypeHandlerManager { + + /** ContentTypeHandlers keyed by the extension */ + Map handlersByExtension = new HashMap(); + /** ContentTypeHandlers keyed by the content-type */ + Map handlersByContentType = new HashMap(); + + String defaultExtension; + + @Inject("struts.rest.defaultExtension") + public void setDefaultExtension(String name) { + this.defaultExtension = name; + } + + @Inject + public void setContainer(Container container) { + Set names = container.getInstanceNames(ContentTypeHandler.class); + for (String name : names) { + ContentTypeHandler handler = container.getInstance(ContentTypeHandler.class, name); + + if (handler.getExtension() != null) { + // 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 (!handlersByExtension.containsKey(handler.getExtension())) { + handler = container.getInstance(ContentTypeHandler.class, overrideName); + } else { + // overriding handler has already been registered + continue; + } + } + this.handlersByExtension.put(handler.getExtension(), handler); + } + + if (handler.getContentType() != null) { + this.handlersByContentType.put(handler.getContentType(), handler); + } + } + } + + /** + * Gets the handler for the request by looking at the request content type and extension + * @param req The request + * @return The appropriate handler + */ + public ContentTypeHandler getHandlerForRequest(HttpServletRequest req) { + ContentTypeHandler handler = null; + String contentType = req.getContentType(); + if (contentType != null) { + handler = handlersByContentType.get(contentType); + } + if (handler == null) { + String extension = findExtension(req.getRequestURI()); + if (extension == null) { + extension = defaultExtension; + } + handler = handlersByExtension.get(extension); + } + return handler; + } + + /** + * Gets the handler for the response by looking at the extension of the request + * @param req The request + * @return The appropriate handler + */ + public ContentTypeHandler getHandlerForResponse(HttpServletRequest req, HttpServletResponse res) { + String extension = findExtension(req.getRequestURI()); + if (extension == null) { + extension = defaultExtension; + } + return handlersByExtension.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 = getHandlerForResponse(req, res); + 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/rest/src/main/java/org/apache/struts2/rest/RestWorkflowInterceptor.java b/plugins/rest/src/main/java/org/apache/struts2/rest/RestWorkflowInterceptor.java index 6da928186..72193ae94 100644 --- a/plugins/rest/src/main/java/org/apache/struts2/rest/RestWorkflowInterceptor.java +++ b/plugins/rest/src/main/java/org/apache/struts2/rest/RestWorkflowInterceptor.java @@ -145,6 +145,8 @@ public class RestWorkflowInterceptor extends MethodFilterInterceptor { private String newMethodName = "editNew"; private String putMethodName = "update"; + private int validationFailureStatusCode = SC_BAD_REQUEST; + @Inject(required=false,value="struts.mapper.postMethodName") public void setPostMethodName(String postMethodName) { this.postMethodName = postMethodName; @@ -165,6 +167,11 @@ public class RestWorkflowInterceptor extends MethodFilterInterceptor { this.putMethodName = putMethodName; } + @Inject(required=false,value="struts.rest.validationFailureStatusCode") + public void setValidationFailureStatusCode(String code) { + this.validationFailureStatusCode = Integer.parseInt(code); + } + @Inject public void setContentTypeHandlerManager(ContentTypeHandlerManager mgr) { this.manager = mgr; @@ -208,7 +215,7 @@ public class RestWorkflowInterceptor extends MethodFilterInterceptor { HttpHeaders info = new DefaultHttpHeaders() .disableCaching() .renderResult(method) - .withStatus(SC_BAD_REQUEST); + .withStatus(validationFailureStatusCode); Map errors = new HashMap(); diff --git a/plugins/rest/src/main/resources/struts-plugin.xml b/plugins/rest/src/main/resources/struts-plugin.xml index d43b8bcca..c3b7bfb4d 100644 --- a/plugins/rest/src/main/resources/struts-plugin.xml +++ b/plugins/rest/src/main/resources/struts-plugin.xml @@ -30,7 +30,7 @@ - + diff --git a/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java b/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java index 5f84273f5..cb8943bdb 100644 --- a/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java +++ b/plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeHandlerManagerTest.java @@ -45,13 +45,13 @@ import java.util.Map; public class ContentTypeHandlerManagerTest extends TestCase { - private ContentTypeHandlerManager mgr; + private DefaultContentTypeHandlerManager mgr; private MockHttpServletResponse mockResponse; private MockHttpServletRequest mockRequest; @Override public void setUp() { - mgr = new ContentTypeHandlerManager(); + mgr = new DefaultContentTypeHandlerManager(); mockResponse = new MockHttpServletResponse(); mockRequest = new MockHttpServletRequest(); mockRequest.setMethod("GET"); @@ -121,7 +121,7 @@ public class ContentTypeHandlerManagerTest extends TestCase { mockContainer.expectAndReturn("getInstance", C.args(C.eq(String.class), C.eq(ContentTypeHandlerManager.STRUTS_REST_HANDLER_OVERRIDE_PREFIX+"json")), null); - ContentTypeHandlerManager mgr = new ContentTypeHandlerManager(); + DefaultContentTypeHandlerManager mgr = new DefaultContentTypeHandlerManager(); mgr.setContainer((Container) mockContainer.proxy()); Map handlers = mgr.handlersByExtension; diff --git a/plugins/rest/src/test/java/org/apache/struts2/rest/RestWorkflowInterceptorTest.java b/plugins/rest/src/test/java/org/apache/struts2/rest/RestWorkflowInterceptorTest.java new file mode 100644 index 000000000..4d04fe083 --- /dev/null +++ b/plugins/rest/src/test/java/org/apache/struts2/rest/RestWorkflowInterceptorTest.java @@ -0,0 +1,65 @@ +/* + * $Id: RestWorkflowInterceptor.java 666756 2008-06-11 18:11:00Z hermanns $ + * + * 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.AnyConstraintMatcher; +import com.mockobjects.dynamic.Mock; +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.ActionProxy; +import com.opensymphony.xwork2.ActionSupport; +import junit.framework.TestCase; +import org.apache.struts2.ServletActionContext; +import org.apache.struts2.dispatcher.mapper.ActionMapping; + +import java.util.HashMap; + +public class RestWorkflowInterceptorTest extends TestCase { + + public void testCustomValidationFailureStatusCode() throws Exception { + RestWorkflowInterceptor wf = new RestWorkflowInterceptor(); + + ActionSupport action = new ActionSupport(); + action.addActionError("some error"); + + wf.setValidationFailureStatusCode("666"); + Mock mockActionInvocation = new Mock(ActionInvocation.class); + Mock mockActionProxy = new Mock(ActionProxy.class); + mockActionProxy.expectAndReturn("getConfig", null); + mockActionInvocation.expectAndReturn("getProxy", mockActionProxy.proxy()); + mockActionInvocation.expectAndReturn("getAction", action); + Mock mockContentTypeHandlerManager = new Mock(ContentTypeHandlerManager.class); + mockContentTypeHandlerManager.expectAndReturn("handleResult", new AnyConstraintMatcher() { + public boolean matches(Object[] args) { + DefaultHttpHeaders headers = (DefaultHttpHeaders) args[1]; + return 666 == headers.status; + } + }, null); + wf.setContentTypeHandlerManager((ContentTypeHandlerManager) mockContentTypeHandlerManager.proxy()); + + ActionContext.setContext(new ActionContext(new HashMap() {{ + put(ServletActionContext.ACTION_MAPPING, new ActionMapping()); + }})); + wf.doIntercept((ActionInvocation) mockActionInvocation.proxy()); + mockContentTypeHandlerManager.verify(); + mockActionInvocation.verify(); + } +}