INT-2448 Add view-expression to Http Inbound

https://jira.springsource.org/browse/INT-2488

Previously the view name could be specified; with this
change, either the view name or a view-epression can be
provided, with the reply message being the root object
of the evaluation context.

INT-2448 Polishing

Remove deprecation from view-name.

Reuse evaluation context.

INT-2448 Polishing

PR Comments
This commit is contained in:
Gary Russell
2012-05-22 15:01:09 -04:00
committed by Oleg Zhurakousky
parent 72f5a26682
commit 733e0ead45
10 changed files with 301 additions and 100 deletions

View File

@@ -27,9 +27,12 @@ import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.config.ExpressionFactoryBean;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.http.inbound.HttpRequestHandlingController;
import org.springframework.integration.http.inbound.HttpRequestHandlingMessagingGateway;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
@@ -39,8 +42,8 @@ import org.w3c.dom.Element;
* Parser for the 'inbound-channel-adapter' and 'inbound-gateway' elements
* of the 'http' namespace. The constructor's boolean value specifies whether
* a reply is to be expected. This value should be 'false' for the
* 'inbound-channel-adapter' and 'true' for the 'inbound-gateway'.
*
* 'inbound-channel-adapter' and 'true' for the 'inbound-gateway'.
*
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Gary Russell
@@ -57,9 +60,9 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
@Override
protected String getBeanClassName(Element element) {
return element.hasAttribute("view-name")
? "org.springframework.integration.http.inbound.HttpRequestHandlingController"
: "org.springframework.integration.http.inbound.HttpRequestHandlingMessagingGateway";
return element.hasAttribute("view-name") || element.hasAttribute("view-expression")
? HttpRequestHandlingController.class.getName()
: HttpRequestHandlingMessagingGateway.class.getName();
}
@Override
@@ -96,18 +99,18 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
}
builder.addPropertyReference("requestChannel", inputChannelRef);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-channel");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "path");
String payloadExpression = element.getAttribute("payload-expression");
if (StringUtils.hasText(payloadExpression)){
if (StringUtils.hasText(payloadExpression)) {
RootBeanDefinition expressionDef = new RootBeanDefinition(ExpressionFactoryBean.class);
expressionDef.getConstructorArgumentValues().addGenericArgumentValue(payloadExpression);
builder.addPropertyValue("payloadExpression", expressionDef);
}
List<Element> headerElements = DomUtils.getChildElementsByTagName(element, "header");
if (!CollectionUtils.isEmpty(headerElements)) {
ManagedMap<String, Object> headerElementsMap = new ManagedMap<String, Object>();
for (Element headerElement : headerElements) {
@@ -117,11 +120,11 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
RootBeanDefinition expressionDef = new RootBeanDefinition(ExpressionFactoryBean.class);
expressionDef.getConstructorArgumentValues().addGenericArgumentValue(expression);
headerElementsMap.put(name, expressionDef);
}
}
}
builder.addPropertyValue("headerExpressions", headerElementsMap);
}
if (this.expectReply) {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-timeout");
@@ -136,18 +139,34 @@ 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");
String viewName = element.getAttribute("view-name");
String viewExpression = element.getAttribute("view-expression");
boolean hasViewName = StringUtils.hasText(viewName);
boolean hasViewExpression = StringUtils.hasText(viewExpression);
if (hasViewName ? hasViewExpression : false) {
parserContext.getReaderContext().error("Only one of 'view' or 'view-expression' is allowed", element);
}
if (hasViewName) {
RootBeanDefinition expressionDef = new RootBeanDefinition(LiteralExpression.class);
expressionDef.getConstructorArgumentValues().addGenericArgumentValue(viewName);
builder.addPropertyValue("viewExpression", expressionDef);
}
else if (hasViewExpression) {
RootBeanDefinition expressionDef = new RootBeanDefinition(ExpressionFactoryBean.class);
expressionDef.getConstructorArgumentValues().addGenericArgumentValue(viewExpression);
builder.addPropertyValue("viewExpression", expressionDef);
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "errors-key");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "error-code");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "message-converters");
//IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "header-mapper");
String headerMapper = element.getAttribute("header-mapper");
String mappedRequestHeaders = element.getAttribute("mapped-request-headers");
String mappedResponseHeaders = element.getAttribute("mapped-response-headers");
if (StringUtils.hasText(headerMapper)) {
if (StringUtils.hasText(mappedRequestHeaders) || StringUtils.hasText(mappedResponseHeaders)) {
parserContext.getReaderContext().error("Neither 'mappped-request-headers' or 'mapped-response-headers' " +
@@ -159,7 +178,7 @@ public class HttpInboundEndpointParser extends AbstractSingleBeanDefinitionParse
BeanDefinitionBuilder headerMapperBuilder = BeanDefinitionBuilder.genericBeanDefinition(
"org.springframework.integration.http.support.DefaultHttpHeaderMapper");
headerMapperBuilder.setFactoryMethod("inboundMapper");
IntegrationNamespaceUtils.setValueIfAttributeDefined(headerMapperBuilder, element, "mapped-request-headers", "inboundHeaderNames");
IntegrationNamespaceUtils.setValueIfAttributeDefined(headerMapperBuilder, element, "mapped-response-headers", "outboundHeaderNames");
builder.addPropertyValue("headerMapper", headerMapperBuilder.getBeanDefinition());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-2012 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.
@@ -24,9 +24,16 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.MessageSource;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.Message;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.MapBindingResult;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.Controller;
/**
@@ -40,8 +47,9 @@ import org.springframework.web.servlet.mvc.Controller;
* 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
* @author Gary Russell
* @since 2.0
*/
public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSupport implements Controller {
@@ -52,7 +60,9 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu
private static final String DEFAULT_ERRORS_KEY = "errors";
private volatile String viewName;
private volatile Expression viewExpression;
private volatile StandardEvaluationContext evaluationContext;
private volatile String replyKey = DEFAULT_REPLY_KEY;
@@ -72,7 +82,8 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu
* Specify the view name.
*/
public void setViewName(String viewName) {
this.viewName = viewName;
Assert.isTrue(StringUtils.hasText(viewName), "View name must contain text");
this.viewExpression = new LiteralExpression(viewName);
}
/**
@@ -98,13 +109,27 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu
* 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;
}
/**
* Specifies a SpEL expression to evaluate in order to generate the view name.
* The EvaluationContext will be populated with the reply message as the root object,
*/
public void setViewExpression(Expression viewExpression) {
this.viewExpression = viewExpression;
}
@Override
protected void onInit() throws Exception {
super.onInit();
this.evaluationContext = this.createEvaluationContext();
}
/**
* 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.
@@ -112,14 +137,30 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu
public final ModelAndView handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
throws Exception {
ModelAndView modelAndView = new ModelAndView();
if (this.viewName != null) {
modelAndView.setViewName(this.viewName);
}
try {
Object reply = super.doHandleRequest(servletRequest, servletResponse);
if (reply != null) {
Message<?> replyMessage = super.doHandleRequest(servletRequest, servletResponse);
if (replyMessage != null) {
Object reply = setupResponseAndConvertReply(servletResponse, replyMessage);
modelAndView.addObject(this.replyKey, reply);
}
if (this.viewExpression != null) {
Object view;
if (replyMessage != null) {
view = this.viewExpression.getValue(this.evaluationContext, replyMessage);
}
else {
view = this.viewExpression.getValue(this.evaluationContext);
}
if (view instanceof View) {
modelAndView.setView((View) view);
}
else if (view instanceof String) {
modelAndView.setViewName((String) view);
}
else {
throw new IllegalStateException("view expression must resolve to a View or String");
}
}
}
catch (Exception e) {
MapBindingResult errors = new MapBindingResult(new HashMap<String, Object>(), "dummy");

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 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.
@@ -91,13 +91,14 @@ import org.springframework.web.util.UrlPathHelper;
* 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
* @author Oleg Zhurakousky
* @author Gary Russell
* @since 2.0
*/
abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewaySupport {
private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder",
HttpRequestHandlingEndpointSupport.class.getClassLoader());
@@ -105,7 +106,7 @@ abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewaySuppor
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());
@@ -128,7 +129,7 @@ abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewaySuppor
private volatile boolean extractReplyPayload = true;
private volatile MultipartResolver multipartResolver;
private volatile Expression payloadExpression;
private volatile Map<String, Expression> headerExpressions;
@@ -154,10 +155,10 @@ abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewaySuppor
}
if (romePresent) {
this.messageConverters.add(new AtomFeedHttpMessageConverter());
this.messageConverters.add(new RssChannelHttpMessageConverter());
this.messageConverters.add(new RssChannelHttpMessageConverter());
}
}
/**
* @return whether to expect reply
*/
@@ -167,12 +168,12 @@ abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewaySuppor
/**
* Set the path template for which this endpoint expects requests.
* May include path variable {keys} to match against.
* May include path variable {keys} to match against.
*/
public void setPath(String path) {
this.path = path;
}
String getPath() {
return path;
}
@@ -300,9 +301,10 @@ abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewaySuppor
/**
* 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.
* @return a the response Message
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
protected final Object doHandleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException {
protected final Message<?> doHandleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException {
try {
ServletServerHttpRequest request = this.prepareRequest(servletRequest);
if (!this.supportedMethods.contains(request.getMethod())) {
@@ -351,13 +353,13 @@ abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewaySuppor
if (payload == null) {
if (requestBody != null) {
payload = requestBody;
payload = requestBody;
}
else {
payload = requestParams;
}
}
MessageBuilder<?> messageBuilder = null;
if (payload instanceof Message<?>){
@@ -366,28 +368,16 @@ abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewaySuppor
else {
messageBuilder = MessageBuilder.withPayload(payload).copyHeaders(headers);
}
Message<?> message = messageBuilder
.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;
Message<?> reply = null;
if (this.expectReply) {
reply = this.sendAndReceiveMessage(message);
if (reply != null) {
ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse);
this.headerMapper.fromHeaders(((Message<?>) reply).getHeaders(), response.getHeaders());
HttpStatus httpStatus = this.resolveHttpStatusFromHeaders(((Message<?>) reply).getHeaders());
if (httpStatus != null) {
response.setStatusCode(httpStatus);
}
response.close();
if (this.extractReplyPayload) {
reply = ((Message<?>) reply).getPayload();
}
}
}
else {
this.send(message);
@@ -399,6 +389,29 @@ abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewaySuppor
}
}
/**
* Converts the reply message to the appropriate HTTP reply object and
* sets up the servlet response.
* @param servletResponse The servlet response.
* @param replyMessage The reply message.
* @return The message payload (if {@link #extractReplyPayload}) otherwise the
* message.
*/
protected final Object setupResponseAndConvertReply(HttpServletResponse servletResponse, Message<?> replyMessage) {
ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse);
this.headerMapper.fromHeaders(replyMessage.getHeaders(), response.getHeaders());
HttpStatus httpStatus = this.resolveHttpStatusFromHeaders(((Message<?>) replyMessage).getHeaders());
if (httpStatus != null) {
response.setStatusCode(httpStatus);
}
response.close();
Object reply = replyMessage;
if (this.extractReplyPayload) {
reply = replyMessage.getPayload();
}
return reply;
}
/**
* 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,
@@ -485,8 +498,8 @@ abstract class HttpRequestHandlingEndpointSupport extends MessagingGatewaySuppor
}
return httpStatus;
}
private StandardEvaluationContext createEvaluationContext(){
protected StandardEvaluationContext createEvaluationContext(){
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
evaluationContext.addPropertyAccessor(new MapAccessor());
BeanFactory beanFactory = this.getBeanFactory();

View File

@@ -29,6 +29,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.integration.Message;
import org.springframework.integration.MessagingException;
import org.springframework.integration.http.converter.MultipartAwareFormHttpMessageConverter;
import org.springframework.util.CollectionUtils;
@@ -53,7 +54,7 @@ import org.springframework.web.HttpRequestHandler;
* <p/>
* 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
* @author Oleg Zhurakousky
* @since 2.0
@@ -76,7 +77,7 @@ public class HttpRequestHandlingMessagingGateway extends HttpRequestHandlingEndp
* 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) {
@@ -91,8 +92,12 @@ public class HttpRequestHandlingMessagingGateway extends HttpRequestHandlingEndp
public final void handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
throws ServletException, IOException {
Object responseContent = null;
Message<?> responseMessage;
try {
responseContent = super.doHandleRequest(servletRequest, servletResponse);
responseMessage = super.doHandleRequest(servletRequest, servletResponse);
if (responseMessage != null) {
responseContent = setupResponseAndConvertReply(servletResponse, responseMessage);
}
}
catch (Exception e) {
responseContent = handleExceptionInternal(e);

View File

@@ -191,6 +191,15 @@ The String "HTTP_REQUEST_HEADERS" will match against any of the standard HTTP Re
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="view-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
SpEL expression that resolves to a view to be resolved when rendering a response.
The expression can resolve to a view name or View object.
The root object of the evaluation context is the reply message.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="errors-key" type="xsd:string">
<xsd:annotation>
<xsd:documentation>