From 754b9a198dcd3b9a94219aed6529be2aa6351ec4 Mon Sep 17 00:00:00 2001 From: David Syer Date: Wed, 1 Sep 2010 17:23:23 +0000 Subject: [PATCH] INT-1347: add convertExceptions to request handler and error-key/code to controller --- .../http/HttpRequestHandlingController.java | 87 +++++++--- .../HttpRequestHandlingEndpointSupport.java | 137 ++++++++------- .../HttpRequestHandlingMessagingGateway.java | 84 +++++++--- .../config/HttpInboundEndpointParser.java | 3 + .../config/spring-integration-http-2.0.xsd | 158 ++++++++++++------ .../HttpRequestHandlingControllerTests.java | 28 +++- ...pRequestHandlingMessagingGatewayTests.java | 58 ++++++- ...boundChannelAdapterParserTests-context.xml | 2 + .../HttpInboundChannelAdapterParserTests.java | 11 ++ .../HttpInboundGatewayParserTests-context.xml | 4 +- .../config/HttpInboundGatewayParserTests.java | 14 ++ 11 files changed, 407 insertions(+), 179 deletions(-) diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/HttpRequestHandlingController.java b/spring-integration-http/src/main/java/org/springframework/integration/http/HttpRequestHandlingController.java index 9f27dbe4f0..4dafe5a78c 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/HttpRequestHandlingController.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/HttpRequestHandlingController.java @@ -16,39 +16,49 @@ package org.springframework.integration.http; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.context.MessageSource; +import org.springframework.validation.Errors; +import org.springframework.validation.MapBindingResult; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.Controller; /** - * Inbound HTTP endpoint that implements Spring's {@link Controller} - * interface to be used with a DispatcherServlet front controller. + * Inbound HTTP endpoint that implements Spring's {@link Controller} interface to be used with a DispatcherServlet front + * controller. *

- * The {@link #setViewName(String) viewName} will be passed into the - * ModelAndView return value. + * The {@link #setViewName(String) viewName} will be passed into the ModelAndView return value. *

- * This endpoint will have request/reply behavior by default. That - * can be overridden by passing false to the constructor. - * In the request/reply case, the model map will be passed to the view, and it - * will contain either the reply Message or payload depending on the value of - * {@link #extractReplyPayload} (true by default, meaning just the payload). - * The corresponding key in the map is determined by the {@link #replyKey} - * property (with a default of "reply"). + * This endpoint will have request/reply behavior by default. That can be overridden by passing false to + * the constructor. In the request/reply case, the model map will be passed to the view, and it will contain either the + * reply Message or payload depending on the value of {@link #extractReplyPayload} (true by default, meaning just the + * payload). The corresponding key in the map is determined by the {@link #replyKey} property (with a default of + * "reply"). * * @author Mark Fisher * @since 2.0 */ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSupport implements Controller { + private static final String DEFAULT_ERROR_CODE = "spring.integration.http.handler.error"; + private static final String DEFAULT_REPLY_KEY = "reply"; + private static final String DEFAULT_ERRORS_KEY = "errors"; private volatile String viewName; private volatile String replyKey = DEFAULT_REPLY_KEY; + private volatile String errorsKey = DEFAULT_ERRORS_KEY; + + private volatile String errorCode = DEFAULT_ERROR_CODE; public HttpRequestHandlingController() { this(true); @@ -58,7 +68,6 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu super(expectReply); } - /** * Specify the view name. */ @@ -67,30 +76,60 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu } /** - * Specify the key to be used when adding the reply Message or payload - * to the model map (will be payload only unless the value of - * {@link HttpRequestHandlingController#setExtractReplyPayload(boolean)} is false). - * The default key is "reply". + * Specify the key to be used when adding the reply Message or payload to the model map (will be payload only unless + * the value of {@link HttpRequestHandlingController#setExtractReplyPayload(boolean)} is false). The + * default key is "reply". */ public void setReplyKey(String replyKey) { this.replyKey = (replyKey != null) ? replyKey : DEFAULT_REPLY_KEY; } /** - * Handles the HTTP request by generating a Message and sending it to the request channel. - * If this gateway's 'expectReply' property is true, it will also generate a response from - * the reply Message once received. + * The key used to expose {@link Errors} in the model, in the case that message handling fails. Defaults to + * "errors". + * @param errorsKey the key value to set */ - public final ModelAndView handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws Exception { + public void setErrorsKey(String errorsKey) { + this.errorsKey = errorsKey; + } + + /** + * The error code to use to signal an error in the message handling. In the case of an error this code will be + * provided in an object error to be optionally translated in the standard MVC way using a {@link MessageSource}. + * The default value is spring.integration.http.handler.error. Three arguments are provided: the + * exception, its message and its stack trace as a String. + * + * @param errorCode the error code to set + */ + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + /** + * Handles the HTTP request by generating a Message and sending it to the request channel. If this gateway's + * 'expectReply' property is true, it will also generate a response from the reply Message once received. + */ + public final ModelAndView handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse) + throws Exception { ModelAndView modelAndView = new ModelAndView(); if (this.viewName != null) { modelAndView.setViewName(this.viewName); } - Object reply = super.doHandleRequest(servletRequest, servletResponse); - if (reply != null) { - modelAndView.addObject(this.replyKey, reply); + try { + Object reply = super.doHandleRequest(servletRequest, servletResponse); + if (reply != null) { + modelAndView.addObject(this.replyKey, reply); + } + } + catch (Exception e) { + MapBindingResult errors = new MapBindingResult(new HashMap(), "dummy"); + PrintWriter stackTrace = new PrintWriter(new StringWriter()); + e.printStackTrace(stackTrace); + errors.reject(errorCode, new Object[] { e, e.getMessage(), stackTrace.toString() }, + "A Spring Integration handler raised an exception while handling an HTTP request. The exception is of type " + + e.getClass() + " and it has a message: (" + e.getMessage() + ")"); + modelAndView.addObject(errorsKey, errors); } return modelAndView; } - } diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/HttpRequestHandlingEndpointSupport.java b/spring-integration-http/src/main/java/org/springframework/integration/http/HttpRequestHandlingEndpointSupport.java index 2857dbddec..e18b765bdf 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/HttpRequestHandlingEndpointSupport.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/HttpRequestHandlingEndpointSupport.java @@ -55,44 +55,38 @@ import org.springframework.web.servlet.DispatcherServlet; /** * Base class for HTTP request handling endpoints. *

- * By default GET and POST requests are accepted, but the 'supportedMethods' - * property may be set to include others or limit the options (e.g. POST only). - * A GET request will generate a payload containing its 'parameterMap' while - * a POST request will be converted to a Message payload according to - * the registered {@link HttpMessageConverter}s. Several are registered - * by default, but the list can be explicitly set via {@link #setMessageConverters(List)}. + * By default GET and POST requests are accepted, but the 'supportedMethods' property may be set to include others or + * limit the options (e.g. POST only). A GET request will generate a payload containing its 'parameterMap' while a POST + * request will be converted to a Message payload according to the registered {@link HttpMessageConverter}s. Several are + * registered by default, but the list can be explicitly set via {@link #setMessageConverters(List)}. *

- * To customize the mapping of request headers to the MessageHeaders, provide - * a reference to a {@link HeaderMapper HeaderMapper} implementation - * to the {@link #setHeaderMapper(HeaderMapper)} method. + * To customize the mapping of request headers to the MessageHeaders, provide a reference to a {@link HeaderMapper + * HeaderMapper} implementation to the {@link #setHeaderMapper(HeaderMapper)} method. *

- * The behavior is "request/reply" by default. Pass false - * to the constructor to force send-only as opposed to sendAndReceive. - * Send-only means that as soon as the Message is created and passed to the - * {@link #setRequestChannel(org.springframework.integration.core.MessageChannel) request channel}, - * a response will be generated. Subclasses determine how that response is - * generated (e.g. simple status response or rendering a View). + * The behavior is "request/reply" by default. Pass false to the constructor to force send-only as opposed + * to sendAndReceive. Send-only means that as soon as the Message is created and passed to the + * {@link #setRequestChannel(org.springframework.integration.MessageChannel) request channel}, a response will be + * generated. Subclasses determine how that response is generated (e.g. simple status response or rendering a View). *

- * In a request-reply scenario, the reply Message's payload will be - * extracted prior to generating a response by default. To have the entire - * serialized Message available for the response, switch the - * {@link #extractReplyPayload} value to false. + * In a request-reply scenario, the reply Message's payload will be extracted prior to generating a response by default. + * To have the entire serialized Message available for the response, switch the {@link #extractReplyPayload} value to + * false. * * @author Mark Fisher * @since 2.0 */ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGateway { - private static final boolean jaxb2Present = - ClassUtils.isPresent("javax.xml.bind.Binder", HttpRequestHandlingEndpointSupport.class.getClassLoader()); + private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", + HttpRequestHandlingEndpointSupport.class.getClassLoader()); - private static final boolean jacksonPresent = - ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", HttpRequestHandlingEndpointSupport.class.getClassLoader()) && - ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", HttpRequestHandlingEndpointSupport.class.getClassLoader()); - - private static boolean romePresent = - ClassUtils.isPresent("com.sun.syndication.feed.WireFeed", HttpRequestHandlingEndpointSupport.class.getClassLoader()); + private static final boolean jacksonPresent = ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", + HttpRequestHandlingEndpointSupport.class.getClassLoader()) + && ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", HttpRequestHandlingEndpointSupport.class + .getClassLoader()); + private static boolean romePresent = ClassUtils.isPresent("com.sun.syndication.feed.WireFeed", + HttpRequestHandlingEndpointSupport.class.getClassLoader()); private volatile List supportedMethods = Arrays.asList(HttpMethod.GET, HttpMethod.POST); @@ -108,7 +102,6 @@ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGatew private volatile MultipartResolver multipartResolver; - public HttpRequestHandlingEndpointSupport() { this(true); } @@ -130,11 +123,17 @@ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGatew } if (romePresent) { // TODO add deps for: - //this.messageConverters.add(new AtomFeedHttpMessageConverter()); - //this.messageConverters.add(new RssChannelHttpMessageConverter()); + // this.messageConverters.add(new AtomFeedHttpMessageConverter()); + // this.messageConverters.add(new RssChannelHttpMessageConverter()); } } - + + /** + * @return whether to expect reply + */ + protected boolean isExpectReply() { + return expectReply; + } /** * Set the message body converters to use. These converters are used to convert from and to HTTP requests and @@ -158,8 +157,7 @@ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGatew } /** - * Specify the supported request method names for this gateway. - * By default, only GET and POST are supported. + * Specify the supported request method names for this gateway. By default, only GET and POST are supported. */ public void setSupportedMethodNames(String... supportedMethods) { Assert.notEmpty(supportedMethods, "at least one supported method is required"); @@ -171,8 +169,7 @@ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGatew } /** - * Specify the supported request methods for this gateway. - * By default, only GET and POST are supported. + * Specify the supported request methods for this gateway. By default, only GET and POST are supported. */ public void setSupportedMethods(HttpMethod... supportedMethods) { Assert.notEmpty(supportedMethods, "at least one supported method is required"); @@ -180,29 +177,26 @@ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGatew } /** - * Specify the type of payload to be generated when the inbound HTTP request content - * is read by the {@link HttpMessageConverter}s. By default this value is null which - * means at runtime any "text" Content-Type will result in String while all others - * default to byte[].class. + * Specify the type of payload to be generated when the inbound HTTP request content is read by the + * {@link HttpMessageConverter}s. By default this value is null which means at runtime any "text" Content-Type will + * result in String while all others default to byte[].class. */ public void setRequestPayloadType(Class requestPayloadType) { this.requestPayloadType = requestPayloadType; } /** - * Specify whether only the reply Message's payload should be passed - * in the response. If this is set to 'false', the entire Message will - * be used to generate the response. The default is 'true'. + * Specify whether only the reply Message's payload should be passed in the response. If this is set to 'false', the + * entire Message will be used to generate the response. The default is 'true'. */ public void setExtractReplyPayload(boolean extractReplyPayload) { - this.extractReplyPayload = extractReplyPayload; + this.extractReplyPayload = extractReplyPayload; } /** - * Specify the {@link MultipartResolver} to use when checking requests. - * If no resolver is provided, the "multipartResolver" bean in the context - * will be used as a fallback. If that is not available either, this endpoint - * will not support multipart requests. + * Specify the {@link MultipartResolver} to use when checking requests. If no resolver is provided, the + * "multipartResolver" bean in the context will be used as a fallback. If that is not available either, this + * endpoint will not support multipart requests. */ public void setMultipartResolver(MultipartResolver multipartResolver) { this.multipartResolver = multipartResolver; @@ -210,12 +204,12 @@ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGatew @Override public String getComponentType() { - return (this.expectReply) ? "http:inbound-gateway" : "http:inbound-channel-adapter"; + return (this.expectReply) ? "http:inbound-gateway" : "http:inbound-channel-adapter"; } /** - * Locates the {@link MultipartResolver} bean based on the default name defined by - * the {@link DispatcherServlet#MULTIPART_RESOLVER_BEAN_NAME} constant if available. + * Locates the {@link MultipartResolver} bean based on the default name defined by the + * {@link DispatcherServlet#MULTIPART_RESOLVER_BEAN_NAME} constant if available. */ @Override protected void onInit() throws Exception { @@ -223,8 +217,8 @@ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGatew BeanFactory beanFactory = this.getBeanFactory(); if (this.multipartResolver == null && beanFactory != null) { try { - MultipartResolver multipartResolver = - this.getBeanFactory().getBean(DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class); + MultipartResolver multipartResolver = this.getBeanFactory().getBean( + DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class); if (logger.isDebugEnabled()) { logger.debug("Using MultipartResolver [" + multipartResolver + "]"); } @@ -232,19 +226,20 @@ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGatew } catch (NoSuchBeanDefinitionException e) { if (logger.isDebugEnabled()) { - logger.debug("Unable to locate MultipartResolver with name '" + DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME + - "': no multipart request handling will be supported."); + logger.debug("Unable to locate MultipartResolver with name '" + + DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME + + "': no multipart request handling will be supported."); } } } } /** - * Handles the HTTP request by generating a Message and sending it to the request channel. - * If this gateway's 'expectReply' property is true, it will also generate a response from - * the reply Message once received. + * Handles the HTTP request by generating a Message and sending it to the request channel. If this gateway's + * 'expectReply' property is true, it will also generate a response from the reply Message once received. */ - protected final Object doHandleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException { + protected final Object doHandleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse) + throws IOException { try { ServletServerHttpRequest request = this.prepareRequest(servletRequest); if (!this.supportedMethods.contains(request.getMethod())) { @@ -259,12 +254,12 @@ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGatew payload = this.convertParameterMap(servletRequest.getParameterMap()); } Map headers = this.headerMapper.toHeaders(request.getHeaders()); - Message message = MessageBuilder.withPayload(payload) - .copyHeaders(headers) - .setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_URL, request.getURI().toString()) - .setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD, request.getMethod().toString()) - .setHeader(org.springframework.integration.http.HttpHeaders.USER_PRINCIPAL, servletRequest.getUserPrincipal()) - .build(); + Message message = MessageBuilder.withPayload(payload).copyHeaders(headers).setHeader( + org.springframework.integration.http.HttpHeaders.REQUEST_URL, request.getURI().toString()) + .setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD, + request.getMethod().toString()).setHeader( + org.springframework.integration.http.HttpHeaders.USER_PRINCIPAL, + servletRequest.getUserPrincipal()).build(); Object reply = null; if (this.expectReply) { reply = this.sendAndReceiveMessage(message); @@ -287,9 +282,9 @@ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGatew } /** - * Prepares an instance of {@link ServletServerHttpRequest} from the raw {@link HttpServletRequest}. - * Also converts the request into a multipart request to make multiparts available if necessary. - * If no multipart resolver is set, simply returns the existing request. + * Prepares an instance of {@link ServletServerHttpRequest} from the raw {@link HttpServletRequest}. Also converts + * the request into a multipart request to make multiparts available if necessary. If no multipart resolver is set, + * simply returns the existing request. * @param request current HTTP request * @return the processed request (multipart wrapper if necessary) * @see MultipartResolver#resolveMultipart @@ -305,8 +300,7 @@ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGatew } /** - * Checks if the request has a readable body (not a GET, HEAD, or OPTIONS request) - * and a Content-Type header. + * Checks if the request has a readable body (not a GET, HEAD, or OPTIONS request) and a Content-Type header. */ private boolean isReadable(ServletServerHttpRequest request) { HttpMethod method = request.getMethod(); @@ -354,8 +348,9 @@ abstract class HttpRequestHandlingEndpointSupport extends AbstractMessagingGatew return converter.read((Class) expectedType, request); } } - throw new MessagingException("Could not convert request: no suitable HttpMessageConverter found for expected type [" + - expectedType.getName() + "] and content type [" + contentType + "]"); + throw new MessagingException( + "Could not convert request: no suitable HttpMessageConverter found for expected type [" + + expectedType.getName() + "] and content type [" + contentType + "]"); } } diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/HttpRequestHandlingMessagingGateway.java b/spring-integration-http/src/main/java/org/springframework/integration/http/HttpRequestHandlingMessagingGateway.java index 8dd94f67ee..efcdbc2f57 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/HttpRequestHandlingMessagingGateway.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/HttpRequestHandlingMessagingGateway.java @@ -32,31 +32,31 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.HttpRequestHandler; /** - * Inbound Messaging Gateway that handles HTTP Requests. May be configured as a bean in the - * Application Context and delegated to from a simple HttpRequestHandlerServlet in - * web.xml where the servlet and bean both have the same name. If the - * {@link #expectReply} property is set to true, a response can generated from a - * reply Message. Otherwise, the gateway will play the role of a unidirectional - * Channel Adapter with a simple status-based response (e.g. 200 OK). + * Inbound Messaging Gateway that handles HTTP Requests. May be configured as a bean in the Application Context and + * delegated to from a simple HttpRequestHandlerServlet in web.xml where the servlet and bean both have the + * same name. If the {@link #expectReply} property is set to true, a response can generated from a reply Message. + * Otherwise, the gateway will play the role of a unidirectional Channel Adapter with a simple status-based response + * (e.g. 200 OK). *

- * The default supported request methods are GET and POST, but the list of values can - * be configured with the {@link #supportedMethods} property. The payload generated from - * a GET request (or HEAD or OPTIONS if supported) will be a {@link MultiValueMap} - * containing the parameter values. For a request containing a body (e.g. a POST), - * the type of the payload is determined by the {@link #setRequestPayloadType(Class) request payload type}. + * The default supported request methods are GET and POST, but the list of values can be configured with the + * {@link #supportedMethods} property. The payload generated from a GET request (or HEAD or OPTIONS if supported) will + * be a {@link MultiValueMap} containing the parameter values. For a request containing a body (e.g. a POST), the type + * of the payload is determined by the {@link #setRequestPayloadType(Class) request payload type}. *

- * If the HTTP request is a multipart and a "multipartResolver" bean has been defined - * in the context, then it will be converted by the {@link MultipartAwareFormHttpMessageConverter} - * as long as the default message converters have not been overwritten (although - * providing a customized instance of the Multipart-aware converter is also an option). + * If the HTTP request is a multipart and a "multipartResolver" bean has been defined in the context, then it will be + * converted by the {@link MultipartAwareFormHttpMessageConverter} as long as the default message converters have not + * been overwritten (although providing a customized instance of the Multipart-aware converter is also an option). *

- * By default a number of {@link HttpMessageConverter}s are already configured. The list - * can be overridden by calling the {@link #setMessageConverters(List)} method. + * By default a number of {@link HttpMessageConverter}s are already configured. The list can be overridden by calling + * the {@link #setMessageConverters(List)} method. * * @author Mark Fisher * @since 2.0 */ -public class HttpRequestHandlingMessagingGateway extends HttpRequestHandlingEndpointSupport implements HttpRequestHandler { +public class HttpRequestHandlingMessagingGateway extends HttpRequestHandlingEndpointSupport implements + HttpRequestHandler { + + private volatile boolean convertExceptions; public HttpRequestHandlingMessagingGateway() { this(true); @@ -66,14 +66,31 @@ public class HttpRequestHandlingMessagingGateway extends HttpRequestHandlingEndp super(expectReply); } + /** + * Flag to determine if conversion and writing out of message handling exceptions should be attempted (default + * false, in which case they will simply be re-thrown). If the flag is true and no message converter can convert the + * exception a new exception will be thrown. + * + * @param convertExceptions the flag to set + */ + public void setConvertExceptions(boolean convertExceptions) { + this.convertExceptions = convertExceptions; + } /** - * Handles the HTTP request by generating a Message and sending it to the request channel. - * If this gateway's 'expectReply' property is true, it will also generate a response from - * the reply Message once received. That response will be written by the {@link HttpMessageConverter}s. + * Handles the HTTP request by generating a Message and sending it to the request channel. If this gateway's + * 'expectReply' property is true, it will also generate a response from the reply Message once received. That + * response will be written by the {@link HttpMessageConverter}s. */ - public final void handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws ServletException, IOException { - Object responseContent = super.doHandleRequest(servletRequest, servletResponse); + public final void handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse) + throws ServletException, IOException { + Object responseContent = null; + try { + responseContent = super.doHandleRequest(servletRequest, servletResponse); + } + catch (Exception e) { + responseContent = handleExceptionInternal(e); + } if (responseContent != null) { ServletServerHttpRequest request = new ServletServerHttpRequest(servletRequest); ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse); @@ -81,8 +98,23 @@ public class HttpRequestHandlingMessagingGateway extends HttpRequestHandlingEndp } } + private Object handleExceptionInternal(Exception e) throws IOException { + if (convertExceptions && isExpectReply()) { + return e; + } + else { + if (e instanceof IOException) { + throw (IOException) e; + } + else { + throw (RuntimeException) e; + } + } + } + @SuppressWarnings("unchecked") - private void writeResponse(Object content, ServletServerHttpResponse response, List acceptTypes) throws IOException { + private void writeResponse(Object content, ServletServerHttpResponse response, List acceptTypes) + throws IOException { for (HttpMessageConverter converter : this.getMessageConverters()) { for (MediaType acceptType : acceptTypes) { if (converter.canWrite(content.getClass(), acceptType)) { @@ -91,8 +123,8 @@ public class HttpRequestHandlingMessagingGateway extends HttpRequestHandlingEndp } } } - throw new MessagingException("Could not convert reply: no suitable HttpMessageConverter found for type [" + - content.getClass().getName() + "] and accept types [" + acceptTypes + "]"); + throw new MessagingException("Could not convert reply: no suitable HttpMessageConverter found for type [" + + content.getClass().getName() + "] and accept types [" + acceptTypes + "]"); } } diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpInboundEndpointParser.java b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpInboundEndpointParser.java index 51a30cdb32..954ba30691 100644 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpInboundEndpointParser.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/config/HttpInboundEndpointParser.java @@ -80,6 +80,7 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-timeout"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "extract-reply-payload"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-key"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "convert-exceptions"); } else { IntegrationNamespaceUtils.setValueIfAttributeDefined( @@ -88,6 +89,8 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "supported-methods", "supportedMethodNames"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-payload-type"); IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "view-name"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "errors-key"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "error-code"); IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "message-converters"); IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "header-mapper"); } diff --git a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-2.0.xsd b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-2.0.xsd index 6e4f9cac29..201555339b 100644 --- a/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-2.0.xsd +++ b/spring-integration-http/src/main/resources/org/springframework/integration/http/config/spring-integration-http-2.0.xsd @@ -1,14 +1,11 @@ - + - - + + - Defines an inbound HTTP-based Channel Adapter. + Defines an inbound HTTP-based Channel Adapter. - - + + - + @@ -41,8 +38,8 @@ - - + + @@ -50,6 +47,28 @@ + + + + In the case that a view-name is specified this attribute can be used to + override the default key of the Errors (if the request cannot be handled). + Defaults to "errors" (similar to normal MVC + usage). + + + + + + + In the case that a view-name is specified this attribute can be used to + override the default error code under which the handling exception is exposed. + Defaults to + "spring.integration.http.handler.error" and is supplied with 3 + parameters: the exception itself, its message and + its stack trace as a String. + + + @@ -61,7 +80,7 @@ - + @@ -73,14 +92,14 @@ - Defines an inbound HTTP-based Messaging Gateway. + Defines an inbound HTTP-based Messaging Gateway. - - - + + + @@ -88,6 +107,35 @@ + + + + In the case that a view-name is specified this attribute can be used to + override the default key of the Errors (if the request cannot be handled). + Defaults to "errors" (similar to normal MVC usage). + + + + + + + In the case that a view-name is specified this attribute can be used to + override the default error code under which the handling exception is exposed. + Defaults to "spring.integration.http.handler.error" and is supplied with 3 + parameters: the exception itself, its message and its stack trace as a String. + + + + + + + In the case that a view-name is not specified this attribute can be used to + override the default behaviour when there is a message handling exception (which + is to rethrow). If this flag is true then the normal conversion process will be + applied to the exception and written out to the response body. + + + @@ -106,13 +154,13 @@ - + - - + + @@ -122,13 +170,13 @@ - Defines an outbound HTTP-based Channel Adapter. + Defines an outbound HTTP-based Channel Adapter. - + - + @@ -140,7 +188,7 @@ - The HTTP method to use when executing requests with this adapter. + The HTTP method to use when executing requests with this adapter. @@ -151,21 +199,21 @@ - + - - + + - The expected type to which the response body should be converted. + The expected type to which the response body should be converted. - + @@ -173,8 +221,8 @@ - Provide a reference to a list of HttpMessageConverter instances. If specified, these converters will replace - all of the default converters that would normally be present on the underlying RestTemplate. + Provide a reference to a list of HttpMessageConverter instances. If specified, these converters will replace + all of the default converters that would normally be present on the underlying RestTemplate. @@ -182,7 +230,7 @@ - + @@ -199,7 +247,7 @@ - + @@ -214,7 +262,7 @@ - Specifies whether this adapter should start automatically. + Specifies whether this adapter should start automatically. @@ -225,25 +273,25 @@ - Defines an outbound HTTP-based Messaging Gateway. + Defines an outbound HTTP-based Messaging Gateway. - + - URL to be used as a fallback for any request Message does not contain the request URL Message header. + URL to be used as a fallback for any request Message does not contain the request URL Message header. - The HTTP method to use when executing requests with this adapter. + The HTTP method to use when executing requests with this adapter. @@ -253,8 +301,8 @@ - Provide a reference to a list of HttpMessageConverter instances. If specified, these converters will replace - all of the default converters that would normally be present on the underlying RestTemplate. + Provide a reference to a list of HttpMessageConverter instances. If specified, these converters will replace + all of the default converters that would normally be present on the underlying RestTemplate. @@ -262,7 +310,7 @@ - + @@ -283,25 +331,25 @@ ]]> - + - The expected type to which the response body should be converted. + The expected type to which the response body should be converted. - + - + - + @@ -313,7 +361,7 @@ ]]> - + @@ -359,15 +407,15 @@ - Defines common configuration for gateway adapters. + Defines common configuration for gateway adapters. - + - + @@ -376,12 +424,12 @@ - + - + \ No newline at end of file diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/HttpRequestHandlingControllerTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/HttpRequestHandlingControllerTests.java index 4f80db507d..ddd27e0ec0 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/HttpRequestHandlingControllerTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/HttpRequestHandlingControllerTests.java @@ -22,12 +22,13 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import org.junit.Test; - import org.springframework.integration.Message; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.validation.Errors; +import org.springframework.validation.ObjectError; import org.springframework.web.servlet.ModelAndView; /** @@ -136,4 +137,29 @@ public class HttpRequestHandlingControllerTests { assertEquals("ABC", ((Message) reply).getPayload()); } + @Test + public void testSendWithError() throws Exception { + QueueChannel requestChannel = new QueueChannel() { + @Override + protected boolean doSend(Message message, long timeout) { + throw new RuntimeException("Planned"); + } + }; + HttpRequestHandlingController controller = new HttpRequestHandlingController(false); + controller.setRequestChannel(requestChannel); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setContent("hello".getBytes()); + request.setContentType("text/plain"); + MockHttpServletResponse response = new MockHttpServletResponse(); + ModelAndView modelAndView = controller.handleRequest(request, response); + assertEquals(1, modelAndView.getModel().size()); + Errors errors = (Errors) modelAndView.getModel().get("errors"); + assertEquals(1, errors.getErrorCount()); + ObjectError error = errors.getAllErrors().get(0); + assertEquals(3, error.getArguments().length); + assertTrue("Wrong message: "+error, ((String)error.getArguments()[1]).startsWith("failed to send Message")); + } + + } diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/HttpRequestHandlingMessagingGatewayTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/HttpRequestHandlingMessagingGatewayTests.java index 0ffcf6cfe2..660f79df27 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/HttpRequestHandlingMessagingGatewayTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/HttpRequestHandlingMessagingGatewayTests.java @@ -19,8 +19,18 @@ package org.springframework.integration.http; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import org.junit.Test; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Arrays; +import org.junit.Test; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.integration.Message; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.channel.QueueChannel; @@ -92,4 +102,50 @@ public class HttpRequestHandlingMessagingGatewayTests { assertEquals("HELLO", response.getContentAsString()); } + @Test + public void testExceptionConversion() throws Exception { + QueueChannel requestChannel = new QueueChannel() { + @Override + protected boolean doSend(Message message, long timeout) { + throw new RuntimeException("Planned"); + } + }; + HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true); + gateway.setRequestChannel(requestChannel); + gateway.setConvertExceptions(true); + gateway.setMessageConverters(Arrays.>asList(new DumbHttpMessageConverter())); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Accept", "application/x-java-serialized-object"); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + gateway.handleRequest(request, response); + String content = response.getContentAsString(); + assertEquals("Planned", content); + } + + private static class DumbHttpMessageConverter extends AbstractHttpMessageConverter { + + public DumbHttpMessageConverter() { + setSupportedMediaTypes(Arrays.asList(MediaType.ALL)); + } + + @Override + protected Exception readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, + HttpMessageNotReadableException { + return null; + } + + @Override + protected boolean supports(Class clazz) { + return true; + } + + @Override + protected void writeInternal(Exception t, HttpOutputMessage outputMessage) throws IOException, + HttpMessageNotWritableException { + new PrintWriter(outputMessage.getBody()).append(t.getCause().getMessage()).flush(); + } + + } + } diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests-context.xml b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests-context.xml index f07aaa81aa..3fb2abab9e 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests-context.xml +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests-context.xml @@ -20,4 +20,6 @@ + + diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests.java index 79eb81e8a3..a53a318d58 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundChannelAdapterParserTests.java @@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.integration.Message; import org.springframework.integration.core.PollableChannel; +import org.springframework.integration.http.HttpRequestHandlingController; import org.springframework.integration.http.HttpRequestHandlingMessagingGateway; import org.springframework.integration.http.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -62,6 +63,9 @@ public class HttpInboundChannelAdapterParserTests { @Autowired private HttpRequestHandlingMessagingGateway putOrDeleteAdapter; + @Autowired + private HttpRequestHandlingController inboundController; + @Test @SuppressWarnings("unchecked") @@ -137,6 +141,13 @@ public class HttpInboundChannelAdapterParserTests { assertTrue(supportedMethods.contains(HttpMethod.DELETE)); } + @Test + public void testController() throws Exception { + DirectFieldAccessor accessor = new DirectFieldAccessor(inboundController); + String errorCode = (String) accessor.getPropertyValue("errorCode"); + assertEquals("oops", errorCode); + } + @SuppressWarnings("serial") private static class TestObject implements Serializable { diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundGatewayParserTests-context.xml b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundGatewayParserTests-context.xml index f98dc5e77d..6303b7a348 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundGatewayParserTests-context.xml +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundGatewayParserTests-context.xml @@ -17,6 +17,8 @@ - + + + diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundGatewayParserTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundGatewayParserTests.java index ead8ea842c..2381618c0b 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundGatewayParserTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/config/HttpInboundGatewayParserTests.java @@ -18,6 +18,7 @@ package org.springframework.integration.http.config; import static org.hamcrest.CoreMatchers.any; import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.springframework.integration.test.util.TestUtils.getPropertyValue; @@ -28,10 +29,12 @@ import javax.servlet.http.HttpServletResponse; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.Message; import org.springframework.integration.core.PollableChannel; import org.springframework.integration.core.SubscribableChannel; +import org.springframework.integration.http.HttpRequestHandlingController; import org.springframework.integration.http.HttpRequestHandlingMessagingGateway; import org.springframework.integration.http.MockHttpServletRequest; import org.springframework.integration.http.MockHttpServletResponse; @@ -49,6 +52,9 @@ public class HttpInboundGatewayParserTests { @Autowired private HttpRequestHandlingMessagingGateway gateway; + @Autowired + private HttpRequestHandlingController inboundController; + @Autowired private SubscribableChannel requests; @@ -60,6 +66,7 @@ public class HttpInboundGatewayParserTests { public void checkConfig() { assertNotNull(gateway); assertThat((Boolean) getPropertyValue(gateway, "expectReply"), is(true)); + assertThat((Boolean) getPropertyValue(gateway, "convertExceptions"), is(true)); assertThat((PollableChannel) getPropertyValue(gateway, "replyChannel"), is(responses)); } @@ -76,4 +83,11 @@ public class HttpInboundGatewayParserTests { assertThat(response.getContentType(), is("application/x-java-serialized-object")); } + @Test + public void testController() throws Exception { + DirectFieldAccessor accessor = new DirectFieldAccessor(inboundController); + String errorCode = (String) accessor.getPropertyValue("errorCode"); + assertEquals("oops", errorCode); + } + }