INT-1347: add convertExceptions to request handler and error-key/code to controller

This commit is contained in:
David Syer
2010-09-01 17:23:23 +00:00
parent 79b3998572
commit 754b9a198d
11 changed files with 407 additions and 179 deletions

View File

@@ -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.
* <p/>
* 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.
* <p/>
* This endpoint will have request/reply behavior by default. That
* can be overridden by passing <code>false</code> 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 <code>false</code> 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 <code>false</code>).
* 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 <code>false</code>). 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 <code>spring.integration.http.handler.error</code>. 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<String, Object>(), "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;
}
}

View File

@@ -55,44 +55,38 @@ import org.springframework.web.servlet.DispatcherServlet;
/**
* Base class for HTTP request handling endpoints.
* <p/>
* 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)}.
* <p/>
* To customize the mapping of request headers to the MessageHeaders, provide
* a reference to a {@link HeaderMapper HeaderMapper<HttpHeaders>} 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<HttpHeaders>} implementation to the {@link #setHeaderMapper(HeaderMapper)} method.
* <p/>
* The behavior is "request/reply" by default. Pass <code>false</code>
* 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 <code>false</code> 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).
* <p/>
* 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 <code>false</code>.
* 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
* <code>false</code>.
*
* @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<HttpMethod> 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 <code>byte[].class</code>.
* 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 <code>byte[].class</code>.
*/
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<String, ?> 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 + "]");
}
}

View File

@@ -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
* <code>web.xml</code> 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 <code>web.xml</code> 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).
* <p/>
* 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}.
* <p/>
* 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).
* <p/>
* 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<MediaType> acceptTypes) throws IOException {
private void writeResponse(Object content, ServletServerHttpResponse response, List<MediaType> 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 + "]");
}
}

View File

@@ -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");
}

View File

@@ -1,14 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.springframework.org/schema/integration/http"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:tool="http://www.springframework.org/schema/tool"
targetNamespace="http://www.springframework.org/schema/integration/http"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:schema xmlns="http://www.springframework.org/schema/integration/http" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:tool="http://www.springframework.org/schema/tool"
targetNamespace="http://www.springframework.org/schema/integration/http" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="http://www.springframework.org/schema/beans"/>
<xsd:import namespace="http://www.springframework.org/schema/tool"/>
<xsd:import namespace="http://www.springframework.org/schema/beans" />
<xsd:import namespace="http://www.springframework.org/schema/tool" />
<xsd:annotation>
<xsd:documentation><![CDATA[
@@ -20,16 +17,16 @@
<xsd:complexType>
<xsd:annotation>
<xsd:documentation>
Defines an inbound HTTP-based Channel Adapter.
Defines an inbound HTTP-based Channel Adapter.
</xsd:documentation>
</xsd:annotation>
<xsd:attribute name="id" type="xsd:ID"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="id" type="xsd:ID" />
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="channel" type="xsd:string" use="required">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.core.MessageChannel"/>
<tool:expected-type type="org.springframework.integration.core.MessageChannel" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
@@ -41,8 +38,8 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="send-timeout" type="xsd:string"/>
<xsd:attribute name="supported-methods" type="xsd:string"/>
<xsd:attribute name="send-timeout" type="xsd:string" />
<xsd:attribute name="supported-methods" type="xsd:string" />
<xsd:attribute name="view-name" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
@@ -50,6 +47,28 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="errors-key" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
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).
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="error-code" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
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.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="message-converters" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
@@ -61,7 +80,7 @@
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.mapping.HeaderMapper"/>
<tool:expected-type type="org.springframework.integration.mapping.HeaderMapper" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
@@ -73,14 +92,14 @@
<xsd:complexType>
<xsd:annotation>
<xsd:documentation>
Defines an inbound HTTP-based Messaging Gateway.
Defines an inbound HTTP-based Messaging Gateway.
</xsd:documentation>
</xsd:annotation>
<xsd:complexContent>
<xsd:extension base="gatewayType">
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="extract-reply-payload" type="xsd:string" default="true"/>
<xsd:attribute name="supported-methods" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="extract-reply-payload" type="xsd:string" default="true" />
<xsd:attribute name="supported-methods" type="xsd:string" />
<xsd:attribute name="view-name" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
@@ -88,6 +107,35 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="errors-key" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
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).
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="error-code" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
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.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="convert-exceptions" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
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.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="request-payload-type" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
@@ -106,13 +154,13 @@
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.mapping.HeaderMapper"/>
<tool:expected-type type="org.springframework.integration.mapping.HeaderMapper" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="reply-key" type="xsd:string"/>
<xsd:attribute name="reply-timeout" type="xsd:string"/>
<xsd:attribute name="reply-key" type="xsd:string" />
<xsd:attribute name="reply-timeout" type="xsd:string" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
@@ -122,13 +170,13 @@
<xsd:complexType>
<xsd:annotation>
<xsd:documentation>
Defines an outbound HTTP-based Channel Adapter.
Defines an outbound HTTP-based Channel Adapter.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="uri-variable" type="uriVariableType" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="uri-variable" type="uriVariableType" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="id" type="xsd:ID"/>
<xsd:attribute name="id" type="xsd:ID" />
<xsd:attribute name="url" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
@@ -140,7 +188,7 @@
<xsd:attribute name="http-method">
<xsd:annotation>
<xsd:documentation>
The HTTP method to use when executing requests with this adapter.
The HTTP method to use when executing requests with this adapter.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
@@ -151,21 +199,21 @@
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.core.MessageChannel"/>
<tool:expected-type type="org.springframework.integration.core.MessageChannel" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="charset" type="xsd:string"/>
<xsd:attribute name="extract-payload" type="xsd:string"/>
<xsd:attribute name="charset" type="xsd:string" />
<xsd:attribute name="extract-payload" type="xsd:string" />
<xsd:attribute name="expected-response-type" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
The expected type to which the response body should be converted.
The expected type to which the response body should be converted.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="direct">
<tool:expected-type type="java.lang.Class"/>
<tool:expected-type type="java.lang.Class" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
@@ -173,8 +221,8 @@
<xsd:attribute name="message-converters" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
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.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
@@ -182,7 +230,7 @@
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.mapping.HeaderMapper"/>
<tool:expected-type type="org.springframework.integration.mapping.HeaderMapper" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
@@ -199,7 +247,7 @@
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.http.client.ClientHttpRequestFactory"/>
<tool:expected-type type="org.springframework.http.client.ClientHttpRequestFactory" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
@@ -214,7 +262,7 @@
<xsd:attribute name="auto-startup" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Specifies whether this adapter should start automatically.
Specifies whether this adapter should start automatically.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
@@ -225,25 +273,25 @@
<xsd:complexType>
<xsd:annotation>
<xsd:documentation>
Defines an outbound HTTP-based Messaging Gateway.
Defines an outbound HTTP-based Messaging Gateway.
</xsd:documentation>
</xsd:annotation>
<xsd:complexContent>
<xsd:extension base="gatewayType">
<xsd:sequence>
<xsd:element name="uri-variable" type="uriVariableType" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="uri-variable" type="uriVariableType" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="url" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation>
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.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="http-method">
<xsd:annotation>
<xsd:documentation>
The HTTP method to use when executing requests with this adapter.
The HTTP method to use when executing requests with this adapter.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
@@ -253,8 +301,8 @@
<xsd:attribute name="message-converters" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
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.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
@@ -262,7 +310,7 @@
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.mapping.HeaderMapper"/>
<tool:expected-type type="org.springframework.integration.mapping.HeaderMapper" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
@@ -283,25 +331,25 @@
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="extract-request-payload" type="xsd:string"/>
<xsd:attribute name="extract-request-payload" type="xsd:string" />
<xsd:attribute name="expected-response-type" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
The expected type to which the response body should be converted.
The expected type to which the response body should be converted.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="direct">
<tool:expected-type type="java.lang.Class"/>
<tool:expected-type type="java.lang.Class" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="charset" type="xsd:string"/>
<xsd:attribute name="charset" type="xsd:string" />
<xsd:attribute name="request-factory" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.http.client.ClientHttpRequestFactory"/>
<tool:expected-type type="org.springframework.http.client.ClientHttpRequestFactory" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
@@ -313,7 +361,7 @@
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="auto-startup" type="xsd:string"/>
<xsd:attribute name="auto-startup" type="xsd:string" />
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
@@ -359,15 +407,15 @@
<xsd:complexType name="gatewayType">
<xsd:annotation>
<xsd:documentation>
Defines common configuration for gateway adapters.
Defines common configuration for gateway adapters.
</xsd:documentation>
</xsd:annotation>
<xsd:attribute name="id" type="xsd:ID"/>
<xsd:attribute name="id" type="xsd:ID" />
<xsd:attribute name="request-channel" type="xsd:string" use="required">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.core.MessageChannel"/>
<tool:expected-type type="org.springframework.integration.core.MessageChannel" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
@@ -376,12 +424,12 @@
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.core.MessageChannel"/>
<tool:expected-type type="org.springframework.integration.core.MessageChannel" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="request-timeout" type="xsd:string"/>
<xsd:attribute name="request-timeout" type="xsd:string" />
</xsd:complexType>
</xsd:schema>

View File

@@ -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"));
}
}

View File

@@ -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.<HttpMessageConverter<?>>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<Exception> {
public DumbHttpMessageConverter() {
setSupportedMediaTypes(Arrays.asList(MediaType.ALL));
}
@Override
protected Exception readInternal(Class<? extends Exception> 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();
}
}
}

View File

@@ -20,4 +20,6 @@
<inbound-channel-adapter id="putOrDeleteAdapter" channel="requests" supported-methods="PUT, delete"/>
<inbound-channel-adapter id="inboundController" channel="requests" view-name="foo" error-code="oops"/>
</beans:beans>

View File

@@ -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 {

View File

@@ -17,6 +17,8 @@
<si:queue/>
</si:channel>
<inbound-gateway id="inboundGateway" request-channel="requests" reply-channel="responses"/>
<inbound-gateway id="inboundGateway" request-channel="requests" reply-channel="responses" convert-exceptions="true"/>
<inbound-gateway id="inboundController" request-channel="requests" reply-channel="responses" view-name="foo" error-code="oops"/>
</beans:beans>

View File

@@ -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);
}
}