Factored out RequestMapper strategy (INT-609).

This commit is contained in:
Mark Fisher
2009-03-15 19:28:13 +00:00
parent 4e62f25364
commit b0f8dedf12
8 changed files with 415 additions and 151 deletions

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.http;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.integration.core.Message;
import org.springframework.integration.message.MessageBuilder;
import org.springframework.util.Assert;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.servlet.handler.DispatcherServletWebRequest;
/**
* RequestMapper implementation that binds the request parameter map to a
* target instance. The target instance may be a non-singleton bean as
* specified by the {@link #setTargetBeanName(String) 'targetBeanName'}
* property. Otherwise, this transformer's target type must provide a
* default no-arg constructor.
*
* @author Mark Fisher
* @since 1.0.2
*/
public class DataBindingRequestMapper implements RequestMapper, BeanFactoryAware, InitializingBean {
private final Class<?> targetType;
private volatile String targetBeanName;
private volatile WebBindingInitializer webBindingInitializer;
private volatile BeanFactory beanFactory;
private volatile boolean validated;
public DataBindingRequestMapper(Class<?> targetType) {
Assert.notNull(targetType, "targetType must not be null");
this.targetType = targetType;
}
/**
* Specify the name of a bean definition to use when creating the target
* instance. The bean must <em>not</em> be a singleton, and it must be
* compatible with the {@link #targetType}.
* <p>If no 'targetBeanName' value is provided, the target type must
* provide a default, no-arg constructor.
*/
public void setTargetBeanName(String targetBeanName) {
this.targetBeanName = targetBeanName;
}
/**
* Specify an optional {@link WebBindingInitializer} to be invoked prior
* to the request binding process.
*/
public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) {
this.webBindingInitializer = webBindingInitializer;
}
/**
* Provides the {@link BeanFactory} necessary to look up a
* {@link #setTargetBeanName(String) 'targetBeanName'} if specified.
* This method is typically invoked automatically by the container.
*/
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
public final void afterPropertiesSet() {
this.validateTargetBeanIfNecessary();
}
private void validateTargetBeanIfNecessary() {
if (this.targetBeanName != null && !this.validated) {
Assert.notNull(this.beanFactory, "beanFactory is required for binding to a bean");
if (this.beanFactory.isSingleton(this.targetBeanName)) {
throw new IllegalArgumentException("binding target bean must not be a singleton");
}
this.validated = true;
}
}
@SuppressWarnings("unchecked")
public Message<?> mapRequest(HttpServletRequest request) throws Exception {
ServletRequestDataBinder binder = new ServletRequestDataBinder(getTarget());
this.initBinder(binder, request);
binder.bind(request);
// this will immediately throw any bind Exceptions
Map map = binder.close();
Object payload = map.get(ServletRequestDataBinder.DEFAULT_OBJECT_NAME);
return MessageBuilder.withPayload(payload).build();
}
private void initBinder(ServletRequestDataBinder binder, HttpServletRequest request) {
if (this.webBindingInitializer != null) {
this.webBindingInitializer.initBinder(binder, new DispatcherServletWebRequest(request));
}
}
private Object getTarget() throws InstantiationException, IllegalAccessException {
if (this.targetBeanName != null) {
this.validateTargetBeanIfNecessary();
return this.beanFactory.getBean(this.targetBeanName, this.targetType);
}
return this.targetType.newInstance();
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.http;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.integration.core.Message;
import org.springframework.integration.message.MessageBuilder;
/**
* Default implementation of {@link RequestMapper} for inbound HttpServletRequests.
* The request will be mapped according to the following rules:
* <ul>
* <li>For a GET request, the parameter Map will be copied as the payload.
* The map's keys will be Strings, and the values will be String arrays
* as described for {@link ServletRequest#getParameterMap()}.</li>
* <li>For other request types, the request body will be used as the payload
* and the type will depend on the Content-Type header value. If it
* begins with "text", a String will be created. If the Content-Type
* is "application/x-java-serialized-object", the request body will be
* expected to contain a Serializable Object, and that will be used as
* the message payload. Otherwise, the payload will be a byte array.
* The parameter Map values will then be added as Message headers.</li>
* </ul>
* In both cases, the original request headers will be passed in the
* MessageHeaders. Likewise, the following headers will be added:
* <ul>
* <li>{@link HttpHeaders#REQUEST_URL}</li>
* <li>{@link HttpHeaders#REQUEST_METHOD}</li>
* <li>{@link HttpHeaders#USER_PRINCIPAL} (if available)</li>
* </ul>
*
* @author Mark Fisher
* @since 1.0.2
*/
public class DefaultRequestMapper implements RequestMapper {
private Log logger = LogFactory.getLog(getClass());
public Message<?> mapRequest(HttpServletRequest request) throws ServletException, IOException, ResponseStatusCodeException {
Message<?> message = null;
String contentType = request.getContentType();
if (request.getMethod().equals("GET")) {
message = this.mapGetRequest(request);
}
else {
Object payload = null;
if (contentType != null && contentType.startsWith("text")) {
if (logger.isDebugEnabled()) {
logger.debug("received " + request.getMethod()
+ " request, creating payload with text content");
}
StringBuilder sb = new StringBuilder();
BufferedReader reader = request.getReader();
String line = reader.readLine();
while (line != null) {
sb.append(line);
line = reader.readLine();
}
payload = sb.toString();
}
else if (contentType != null && contentType.equals("application/x-java-serialized-object")) {
try {
payload = new ObjectInputStream(request.getInputStream()).readObject();
}
catch (ClassNotFoundException e) {
throw new ServletException("failed to deserialize Object in request", e);
}
}
else {
InputStream stream = request.getInputStream();
int length = request.getContentLength();
if (length == -1) {
throw new ResponseStatusCodeException(HttpServletResponse.SC_LENGTH_REQUIRED);
}
if (logger.isDebugEnabled()) {
logger.debug("received " + request.getMethod() + " request, "
+ "creating byte array payload with content lenth: " + length);
}
byte[] bytes = new byte[length];
stream.read(bytes, 0, length);
payload = bytes;
}
MessageBuilder<?> builder = MessageBuilder.withPayload(payload);
this.populateHeaders(request, builder, true);
message = builder.build();
}
return message;
}
@SuppressWarnings("unchecked")
private Message<?> mapGetRequest(HttpServletRequest request) {
if (logger.isDebugEnabled()) {
logger.debug("received GET request, using parameter map as payload");
}
Map<String, String[]> parameterMap = new HashMap<String, String[]>(request.getParameterMap());
MessageBuilder<?> builder = MessageBuilder.withPayload(Collections.unmodifiableMap(parameterMap));
this.populateHeaders(request, builder, false);
return builder.build();
}
@SuppressWarnings("unchecked")
private void populateHeaders(HttpServletRequest request, MessageBuilder<?> builder, boolean includeParameters) {
Enumeration<?> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String headerName = (String) headerNames.nextElement();
Enumeration<?> headerEnum = request.getHeaders(headerName);
if (headerEnum != null) {
List<Object> headers = new ArrayList<Object>();
while (headerEnum.hasMoreElements()) {
headers.add(headerEnum.nextElement());
}
if (headers.size() == 1) {
builder.setHeader(headerName, headers.get(0));
}
else if (headers.size() > 1) {
builder.setHeader(headerName, headers);
}
}
}
}
if (includeParameters) {
builder.copyHeaders(request.getParameterMap());
}
builder.setHeader(HttpHeaders.REQUEST_URL, request.getRequestURL().toString());
builder.setHeader(HttpHeaders.REQUEST_METHOD, request.getMethod());
builder.setHeader(HttpHeaders.USER_PRINCIPAL, request.getUserPrincipal());
}
}

View File

@@ -16,28 +16,21 @@
package org.springframework.integration.http;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.integration.core.Message;
import org.springframework.integration.gateway.SimpleMessagingGateway;
import org.springframework.integration.message.MessageBuilder;
import org.springframework.integration.message.MessageTimeoutException;
import org.springframework.util.Assert;
import org.springframework.web.HttpRequestHandler;
@@ -49,29 +42,11 @@ import org.springframework.web.servlet.View;
* 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).
* By default the request will be converted to a Message payload according to
* the following rules:
* <ul>
* <li>For a GET request, the parameter Map will be used as the payload.
* The map's keys will be Strings, and the values will be String arrays
* as described for {@link ServletRequest#getParameterMap()}</li>.
* <li>For other request types, the request body will be used as the payload
* and the type will depend on the Content-Type header value. If it
* begins with "text", a String will be created. Otherwise, the payload
* will be a byte array. The parameter Map values are then added as
* Message headers.</li>
* </ul>
* In both cases, when extracting a request payload, the original request
* headers will be passed in the MessageHeaders. Likewise, the following
* headers will be added:
* <ul>
* <li>{@link HttpHeaders#REQUEST_URL}</li>
* <li>{@link HttpHeaders#REQUEST_METHOD}</li>
* <li>{@link HttpHeaders#USER_PRINCIPAL} (if available)</li>
* </ul>
* To have the full request object passed in the Message payload instead,
* set the {@link #extractRequestPayload} value to <code>false</code>.
* This can be useful if you intend to use a MessageTransformer downstream
* to convert the request in some custom way.
* the rules of the {@link DefaultRequestMapper}.
* <p/>
* To customize the mapping of the request to the Message payload, provide
* a reference to a {@link RequestMapper} implementation to the
* {@link #setRequestMapper(RequestMapper)} method.
* <p/>
* The value for {@link #expectReply} is <code>false</code> by default.
* This means that as soon as the Message is created and passed to the
@@ -112,7 +87,7 @@ public class HttpInboundEndpoint extends SimpleMessagingGateway implements HttpR
private volatile boolean expectReply;
private volatile boolean extractRequestPayload = true;
private volatile RequestMapper requestMapper = new DefaultRequestMapper();
private volatile boolean extractReplyPayload = true;
@@ -145,16 +120,13 @@ public class HttpInboundEndpoint extends SimpleMessagingGateway implements HttpR
}
/**
* Specify whether the inbound request's content should be passed as
* the payload of the Message. If this is set to 'false', the entire
* request will be sent as the payload. Otherwise, for a GET request
* the parameter map will be the payload. For other supported request
* methods, the body will be extracted, and the type of the payload
* depends on the Content-Type of the request.
* The default value is 'true'.
* Specify a {@link RequestMapper} implementation to map from the
* inbound HTTP request to a Message. The default implementation
* is {@link DefaultRequestMapper}.
*/
public void setExtractRequestPayload(boolean extractRequestPayload) {
this.extractRequestPayload = extractRequestPayload;
public void setRequestMapper(RequestMapper requestMapper) {
Assert.notNull(requestMapper, "requestMapper must not be null");
this.requestMapper = requestMapper;
}
/**
@@ -206,111 +178,22 @@ public class HttpInboundEndpoint extends SimpleMessagingGateway implements HttpR
return;
}
try {
Message<?> requestMessage = this.createRequestMessage(request);
Message<?> requestMessage = this.requestMapper.mapRequest(request);
Object reply = this.handleRequestMessage(requestMessage);
this.generateResponse(requestMessage, reply, request, response);
}
catch (RequiredContentLengthUnavailableException e) {
response.setStatus(HttpServletResponse.SC_LENGTH_REQUIRED);
catch (ResponseStatusCodeException e) {
response.setStatus(e.getStatusCode());
}
}
/**
* Create a request Message for the provided HTTP request.
* @see #setExtractRequestPayload(boolean)
*/
private Message<?> createRequestMessage(HttpServletRequest httpRequest) throws ServletException, IOException {
if (this.extractRequestPayload) {
return this.createMessageFromHttpRequestContent(httpRequest);
catch (ServletException e) {
throw e;
}
else {
return MessageBuilder.withPayload(httpRequest).build();
catch (IOException e) {
throw e;
}
}
private Message<?> createMessageFromHttpRequestContent(HttpServletRequest request) throws ServletException, IOException {
Message<?> message = null;
String contentType = request.getContentType();
if (request.getMethod().equals("GET")) {
if (logger.isDebugEnabled()) {
logger.debug("received GET request, using parameter map as payload");
}
MessageBuilder<?> builder = MessageBuilder.withPayload(request.getParameterMap());
this.populateHeaders(request, builder, false);
message = builder.build();
catch (Exception e) {
throw new ServletException(e);
}
else {
Object payload = null;
if (contentType != null && contentType.startsWith("text")) {
if (logger.isDebugEnabled()) {
logger.debug("received " + request.getMethod()
+ " request, creating payload with text content");
}
StringBuilder sb = new StringBuilder();
BufferedReader reader = request.getReader();
String line = reader.readLine();
while (line != null) {
sb.append(line);
line = reader.readLine();
}
payload = sb.toString();
}
else if (contentType != null && contentType.equals("application/x-java-serialized-object")) {
try {
payload = new ObjectInputStream(request.getInputStream()).readObject();
}
catch (ClassNotFoundException e) {
throw new ServletException("failed to deserialize Object in request", e);
}
}
else {
InputStream stream = request.getInputStream();
int length = request.getContentLength();
if (length == -1) {
throw new RequiredContentLengthUnavailableException();
}
if (logger.isDebugEnabled()) {
logger.debug("received " + request.getMethod() + " request, "
+ "creating byte array payload with content lenth: " + length);
}
byte[] bytes = new byte[length];
stream.read(bytes, 0, length);
payload = bytes;
}
MessageBuilder<?> builder = MessageBuilder.withPayload(payload);
this.populateHeaders(request, builder, true);
message = builder.build();
}
return message;
}
@SuppressWarnings("unchecked")
private void populateHeaders(HttpServletRequest request, MessageBuilder<?> builder, boolean includeParameters) {
Enumeration<?> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String headerName = (String) headerNames.nextElement();
Enumeration<?> headerEnum = request.getHeaders(headerName);
if (headerEnum != null) {
List<Object> headers = new ArrayList<Object>();
while (headerEnum.hasMoreElements()) {
headers.add(headerEnum.nextElement());
}
if (headers.size() == 1) {
builder.setHeader(headerName, headers.get(0));
}
else if (headers.size() > 1) {
builder.setHeader(headerName, headers);
}
}
}
}
if (includeParameters) {
builder.copyHeaders(request.getParameterMap());
}
builder.setHeader(HttpHeaders.REQUEST_URL, request.getRequestURL().toString());
builder.setHeader(HttpHeaders.REQUEST_METHOD, request.getMethod());
builder.setHeader(HttpHeaders.USER_PRINCIPAL, request.getUserPrincipal());
}
private Object handleRequestMessage(Message<?> requestMessage) {
@@ -379,9 +262,4 @@ public class HttpInboundEndpoint extends SimpleMessagingGateway implements HttpR
}
}
@SuppressWarnings("serial")
private static class RequiredContentLengthUnavailableException extends RuntimeException {
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.http;
import javax.servlet.http.HttpServletRequest;
import org.springframework.integration.core.Message;
/**
* Strategy interface for mapping from an inbound {@link HttpServletRequest}
* to a Message.
*
* @author Mark Fisher
*/
public interface RequestMapper {
Message<?> mapRequest(HttpServletRequest request) throws Exception;
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.http;
/**
* Exception that provides a response status code. This can be used by
* {@link RequestMapper} implementations to indicate an error.
*
* @author Mark Fisher
*/
@SuppressWarnings("serial")
public class ResponseStatusCodeException extends Exception {
private final int statusCode;
public ResponseStatusCodeException(int statusCode) {
this.statusCode = statusCode;
}
public int getStatusCode() {
return this.statusCode;
}
}

View File

@@ -76,19 +76,17 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-timeout");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-timeout");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "extract-request-payload");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "extract-reply-payload");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-key");
}
else {
IntegrationNamespaceUtils.setValueIfAttributeDefined(
builder, element, "send-timeout", "requestTimeout");
IntegrationNamespaceUtils.setValueIfAttributeDefined(
builder, element, "extract-payload", "extractRequestPayload");
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "supported-methods");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "view");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-key");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "request-mapper");
}
private String getInputChannelAttributeName() {

View File

@@ -36,7 +36,6 @@
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="extract-payload" type="xsd:string" default="true"/>
<xsd:attribute name="send-timeout" type="xsd:string"/>
<xsd:attribute name="supported-methods" type="xsd:string"/>
<xsd:attribute name="view" type="xsd:string">
@@ -48,6 +47,15 @@
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="request-mapper" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.http.RequestMapper"/>
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="request-key" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
@@ -61,7 +69,6 @@
</xsd:annotation>
<xsd:complexContent>
<xsd:extension base="gatewayType">
<xsd:attribute name="extract-request-payload" type="xsd:string" default="true"/>
<xsd:attribute name="extract-reply-payload" type="xsd:string" default="true"/>
<xsd:attribute name="supported-methods" type="xsd:string"/>
<xsd:attribute name="view" type="xsd:string">
@@ -73,6 +80,15 @@
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="request-mapper" type="xsd:string">
<xsd:annotation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.integration.http.RequestMapper"/>
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="request-key" type="xsd:string"/>
<xsd:attribute name="reply-key" type="xsd:string"/>
</xsd:extension>

View File

@@ -40,6 +40,7 @@ import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.easymock.IAnswer;
@@ -50,6 +51,7 @@ import org.junit.Test;
import org.springframework.integration.core.Message;
import org.springframework.integration.core.MessageChannel;
import org.springframework.integration.core.MessageHeaders;
import org.springframework.integration.message.StringMessage;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.servlet.View;
@@ -153,14 +155,18 @@ public class HttpInboundEndpointTests {
}
@Test
public void handleRequest_withExtractRequestPayloadIsFalse_requestObjectIsInPayload()
public void handleRequest_withCustomRequestMapper_requestObjectIsInPayload()
throws ServletException, IOException {
endpoint.setExtractRequestPayload(false);
endpoint.setRequestMapper(new RequestMapper() {
public Message<?> mapRequest(HttpServletRequest request) throws Exception {
return new StringMessage(request.getRequestURI());
}
});
expect(requestChannel.send(isA(Message.class))).andAnswer(
new IAnswer<Boolean>() {
@SuppressWarnings("unchecked")
public Boolean answer() throws Throwable {
assertThat(((Message) getCurrentArguments()[0]).getPayload(), is((Object) request));
assertThat(((Message) getCurrentArguments()[0]).getPayload(), is((Object) "/anyurl"));
return true;
}
});