INT-4479: RequestEntity as root for status code

JIRA: https://jira.spring.io/browse/INT-4479

For better user experience expose a `RequestEntity<?>` as a root object
for evaluation context for `statusCodeExpression` execution
This commit is contained in:
Artem Bilan
2018-10-04 16:26:01 -04:00
committed by Gary Russell
parent a05004ae38
commit 262e624ef4
10 changed files with 130 additions and 76 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2018 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.
@@ -27,6 +27,7 @@ import org.springframework.expression.Expression;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.integration.dsl.ComponentsRegistration;
import org.springframework.integration.dsl.MessagingGatewaySpec;
@@ -277,7 +278,7 @@ public abstract class HttpInboundEndpointSupportSpec<S extends HttpInboundEndpoi
* @return the current Spec.
* @see HttpRequestHandlingEndpointSupport#setStatusCodeExpression(Expression)
*/
public S statusCodeFunction(Function<Void, ?> statusCodeFunction) {
public S statusCodeFunction(Function<RequestEntity<?>, ?> statusCodeFunction) {
return statusCodeExpression(new FunctionExpression<>(statusCodeFunction));
}

View File

@@ -25,6 +25,7 @@ import org.springframework.core.ResolvableType;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
@@ -276,8 +277,8 @@ public class BaseHttpInboundEndpoint extends MessagingGatewaySupport implements
}
}
protected HttpStatus evaluateHttpStatus() {
Object value = this.statusCodeExpression.getValue(this.evaluationContext);
protected HttpStatus evaluateHttpStatus(HttpEntity<?> httpEntity) {
Object value = this.statusCodeExpression.getValue(this.evaluationContext, httpEntity);
return buildHttpStatus(value);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2018 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.
@@ -27,6 +27,8 @@ 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.http.RequestEntity;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
@@ -52,15 +54,25 @@ import org.springframework.web.servlet.mvc.Controller;
* @author Mark Fisher
* @author Gary Russell
* @author Artem Bilan
*
* @since 2.0
*/
public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSupport implements Controller {
private static final String DEFAULT_ERROR_CODE = "spring.integration.http.handler.error";
/**
* The View model key for the error code.
*/
public static final String DEFAULT_ERROR_CODE = "spring.integration.http.handler.error";
private static final String DEFAULT_REPLY_KEY = "reply";
/**
* The View model key for reply.
*/
public static final String DEFAULT_REPLY_KEY = "reply";
private static final String DEFAULT_ERRORS_KEY = "errors";
/**
* The View model key for errors.
*/
public static final String DEFAULT_ERRORS_KEY = "errors";
private volatile Expression viewExpression;
@@ -82,7 +94,6 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu
/**
* Specify the view name.
*
* @param viewName The view name.
*/
public void setViewName(String viewName) {
@@ -94,7 +105,6 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu
* Specify the key to be used when adding the reply Message or payload to the core map (will be payload only unless
* the value of {@link HttpRequestHandlingController#setExtractReplyPayload(boolean)} is <code>false</code>). The
* default key is "reply".
*
* @param replyKey The reply key.
*/
public void setReplyKey(String replyKey) {
@@ -104,7 +114,6 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu
/**
* The key used to expose {@link Errors} in the core, in the case that message handling fails. Defaults to
* "errors".
*
* @param errorsKey The key value to set.
*/
public void setErrorsKey(String errorsKey) {
@@ -116,7 +125,6 @@ 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) {
@@ -126,7 +134,6 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu
/**
* 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,
*
* @param viewExpression The view expression.
*/
public void setViewExpression(Expression viewExpression) {
@@ -144,11 +151,14 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu
* 'expectReply' property is true, it will also generate a response from the reply Message once received.
*/
@Override
public final ModelAndView handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
throws Exception {
public final ModelAndView handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
ModelAndView modelAndView = new ModelAndView();
try {
Message<?> replyMessage = super.doHandleRequest(servletRequest, servletResponse);
ServletServerHttpRequest request = prepareRequest(servletRequest);
RequestEntity<Object> httpEntity = prepareRequestEntity(request);
Message<?> replyMessage = doHandleRequest(servletRequest, httpEntity, servletResponse);
ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse);
if (replyMessage != null) {
Object reply = setupResponseAndConvertReply(response, replyMessage);
@@ -156,7 +166,7 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu
modelAndView.addObject(this.replyKey, reply);
}
else {
setStatusCodeIfNeeded(response);
setStatusCodeIfNeeded(response, httpEntity);
}
if (this.viewExpression != null) {
@@ -183,8 +193,9 @@ public class HttpRequestHandlingController extends HttpRequestHandlingEndpointSu
PrintWriter stackTrace = new PrintWriter(new StringWriter());
e.printStackTrace(stackTrace);
errors.reject(this.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() + ")");
"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(this.errorsKey, errors);
}
return modelAndView;

View File

@@ -36,6 +36,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
@@ -245,33 +246,26 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
* 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.
* @param servletRequest The servlet request.
* @param httpEntity the request entity to use.
* @param servletResponse The servlet response.
* @return The response Message.
* @throws IOException Any IOException.
*/
protected final Message<?> doHandleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
throws IOException {
protected final Message<?> doHandleRequest(HttpServletRequest servletRequest, RequestEntity<?> httpEntity,
HttpServletResponse servletResponse) {
if (isRunning()) {
return actualDoHandleRequest(servletRequest, servletResponse);
return actualDoHandleRequest(servletRequest, httpEntity, servletResponse);
}
else {
return createServiceUnavailableResponse();
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private Message<?> actualDoHandleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
throws IOException {
@SuppressWarnings("unchecked")
private Message<?> actualDoHandleRequest(HttpServletRequest servletRequest, RequestEntity<?> httpEntity,
HttpServletResponse servletResponse) {
this.activeCount.incrementAndGet();
try {
ServletServerHttpRequest request = this.prepareRequest(servletRequest);
Object requestBody = null;
if (isReadable(request)) {
requestBody = extractRequestBody(request);
}
HttpEntity httpEntity = new HttpEntity(requestBody, request.getHeaders());
StandardEvaluationContext evaluationContext = this.createEvaluationContext();
evaluationContext.setRootObject(httpEntity);
@@ -284,7 +278,7 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
Cookie[] requestCookies = servletRequest.getCookies();
if (!ObjectUtils.isEmpty(requestCookies)) {
Map<String, Cookie> cookies = new HashMap<String, Cookie>(requestCookies.length);
Map<String, Cookie> cookies = new HashMap<>(requestCookies.length);
for (Cookie requestCookie : requestCookies) {
cookies.put(requestCookie.getName(), requestCookie);
}
@@ -312,7 +306,7 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
evaluationContext.setVariable("matrixVariables", matrixVariables);
}
Map<String, Object> headers = getHeaderMapper().toHeaders(request.getHeaders());
Map<String, Object> headers = getHeaderMapper().toHeaders(httpEntity.getHeaders());
Object payload = null;
if (getPayloadExpression() != null) {
// create payload based on SpEL
@@ -330,8 +324,8 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
}
if (payload == null) {
if (requestBody != null) {
payload = requestBody;
if (httpEntity.getBody() != null) {
payload = httpEntity.getBody();
}
else {
payload = requestParams;
@@ -350,9 +344,9 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
Message<?> message = messageBuilder
.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_URL,
request.getURI().toString())
httpEntity.getUrl().toString())
.setHeader(org.springframework.integration.http.HttpHeaders.REQUEST_METHOD,
request.getMethod().toString())
httpEntity.getMethod().toString())
.setHeader(org.springframework.integration.http.HttpHeaders.USER_PRINCIPAL,
servletRequest.getUserPrincipal())
.build();
@@ -366,7 +360,7 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
if (getStatusCodeExpression() != null) {
reply = getMessageBuilderFactory().withPayload(e.getMessage())
.setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE,
evaluateHttpStatus())
evaluateHttpStatus(httpEntity))
.build();
}
else {
@@ -419,9 +413,9 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
}
protected void setStatusCodeIfNeeded(ServerHttpResponse response) {
protected void setStatusCodeIfNeeded(ServerHttpResponse response, HttpEntity<?> httpEntity) {
if (getStatusCodeExpression() != null) {
HttpStatus httpStatus = evaluateHttpStatus();
HttpStatus httpStatus = evaluateHttpStatus(httpEntity);
if (httpStatus != null) {
response.setStatusCode(httpStatus);
}
@@ -437,7 +431,7 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
* @return the processed request (multipart wrapper if necessary)
* @see MultipartResolver#resolveMultipart
*/
private ServletServerHttpRequest prepareRequest(HttpServletRequest servletRequest) {
protected ServletServerHttpRequest prepareRequest(HttpServletRequest servletRequest) {
if (servletRequest instanceof MultipartHttpServletRequest) {
return new MultipartHttpInputMessage((MultipartHttpServletRequest) servletRequest);
}
@@ -472,8 +466,17 @@ public abstract class HttpRequestHandlingEndpointSupport extends BaseHttpInbound
return convertedMap;
}
protected RequestEntity<Object> prepareRequestEntity(ServletServerHttpRequest request) throws IOException {
Object requestBody = null;
if (isReadable(request)) {
requestBody = extractRequestBody(request);
}
return new RequestEntity<>(requestBody, request.getHeaders(), request.getMethod(), request.getURI());
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Object extractRequestBody(ServletServerHttpRequest request) throws IOException {
protected Object extractRequestBody(ServletServerHttpRequest request) throws IOException {
MediaType contentType = request.getHeaders().getContentType();
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2018 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.
@@ -20,13 +20,13 @@ import java.io.IOException;
import java.util.Collections;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
@@ -60,6 +60,7 @@ import org.springframework.web.HttpRequestHandler;
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Artem Bilan
*
* @since 2.0
*/
public class HttpRequestHandlingMessagingGateway extends HttpRequestHandlingEndpointSupport
@@ -81,7 +82,6 @@ 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) {
@@ -94,15 +94,18 @@ public class HttpRequestHandlingMessagingGateway extends HttpRequestHandlingEndp
* response will be written by the {@link HttpMessageConverter}s.
*/
public final void handleRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
throws ServletException, IOException {
throws IOException {
Object responseContent = null;
Message<?> responseMessage;
final ServletServerHttpRequest request = new ServletServerHttpRequest(servletRequest);
final ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse);
ServletServerHttpRequest request = new ServletServerHttpRequest(servletRequest);
ServletServerHttpResponse response = new ServletServerHttpResponse(servletResponse);
RequestEntity<Object> httpEntity = prepareRequestEntity(request);
try {
responseMessage = super.doHandleRequest(servletRequest, servletResponse);
responseMessage = doHandleRequest(servletRequest, httpEntity, servletResponse);
if (responseMessage != null) {
responseContent = setupResponseAndConvertReply(response, responseMessage);
}
@@ -140,7 +143,7 @@ public class HttpRequestHandlingMessagingGateway extends HttpRequestHandlingEndp
}
}
else {
setStatusCodeIfNeeded(response);
setStatusCodeIfNeeded(response, httpEntity);
}
}
@@ -164,10 +167,11 @@ public class HttpRequestHandlingMessagingGateway extends HttpRequestHandlingEndp
@SuppressWarnings({ "unchecked", "rawtypes" })
private void writeResponse(Object content, ServletServerHttpResponse response, List<MediaType> acceptTypes)
throws IOException {
if (CollectionUtils.isEmpty(acceptTypes)) {
acceptTypes = Collections.singletonList(MediaType.ALL);
}
for (HttpMessageConverter converter : this.getMessageConverters()) {
for (HttpMessageConverter converter : getMessageConverters()) {
for (MediaType acceptType : acceptTypes) {
if (converter.canWrite(content.getClass(), acceptType)) {
converter.write(content, acceptType, response);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2018 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.
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import java.io.IOException;
import java.util.Map;
import org.junit.Test;
@@ -28,6 +29,8 @@ import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.http.RequestEntity;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.http.AbstractHttpInboundTests;
import org.springframework.messaging.Message;
@@ -47,11 +50,11 @@ import org.springframework.web.servlet.HandlerMapping;
*/
public class HttpRequestHandlingMessagingGatewayWithPathMappingTests extends AbstractHttpInboundTests {
private static ExpressionParser PARSER = new SpelExpressionParser();
private static final ExpressionParser PARSER = new SpelExpressionParser();
@Test
public void withoutExpression() throws Exception {
public void withoutExpression() throws IOException {
DirectChannel echoChannel = new DirectChannel();
echoChannel.subscribe(message -> {
MessageChannel replyChannel = (MessageChannel) message.getHeaders().getReplyChannel();
@@ -62,7 +65,8 @@ public class HttpRequestHandlingMessagingGatewayWithPathMappingTests extends Abs
request.setContentType("text/plain");
request.setParameter("foo", "bar");
request.setContent("hello".getBytes());
String body = "hello";
request.setContent(body.getBytes());
request.setRequestURI("/fname/bill/lname/clinton");
HttpRequestHandlingMessagingGateway gateway = new HttpRequestHandlingMessagingGateway(true);
@@ -78,7 +82,9 @@ public class HttpRequestHandlingMessagingGatewayWithPathMappingTests extends Abs
MockHttpServletResponse response = new MockHttpServletResponse();
Object result = gateway.doHandleRequest(request, response);
RequestEntity<Object> httpEntity = prepareRequestEntity(body, new ServletServerHttpRequest(request));
Object result = gateway.doHandleRequest(request, httpEntity, response);
assertThat(result, instanceOf(Message.class));
assertEquals("hello", ((Message<?>) result).getPayload());
@@ -97,7 +103,8 @@ public class HttpRequestHandlingMessagingGatewayWithPathMappingTests extends Abs
request.setMethod("POST");
request.setContentType("text/plain");
request.setParameter("foo", "bar");
request.setContent("hello".getBytes());
String body = "hello";
request.setContent(body.getBytes());
String requestURI = "/fname/bill/lname/clinton";
@@ -120,7 +127,9 @@ public class HttpRequestHandlingMessagingGatewayWithPathMappingTests extends Abs
gateway.afterPropertiesSet();
gateway.start();
Object result = gateway.doHandleRequest(request, response);
RequestEntity<Object> httpEntity = prepareRequestEntity(body, new ServletServerHttpRequest(request));
Object result = gateway.doHandleRequest(request, httpEntity, response);
assertThat(result, instanceOf(Message.class));
assertEquals("bill", ((Message<?>) result).getPayload());
}
@@ -140,7 +149,8 @@ public class HttpRequestHandlingMessagingGatewayWithPathMappingTests extends Abs
request.setMethod("POST");
request.setContentType("text/plain");
request.setParameter("foo", "bar");
request.setContent("hello".getBytes());
String body = "hello";
request.setContent(body.getBytes());
String requestURI = "/fname/bill/lname/clinton";
@@ -163,9 +173,15 @@ public class HttpRequestHandlingMessagingGatewayWithPathMappingTests extends Abs
gateway.afterPropertiesSet();
gateway.start();
Object result = gateway.doHandleRequest(request, response);
RequestEntity<Object> httpEntity = prepareRequestEntity(body, new ServletServerHttpRequest(request));
Object result = gateway.doHandleRequest(request, httpEntity, response);
assertThat(result, instanceOf(Message.class));
assertEquals("bill", ((Map<String, Object>) ((Message<?>) result).getPayload()).get("f"));
}
private static RequestEntity<Object> prepareRequestEntity(Object body, ServletServerHttpRequest request) throws IOException {
return new RequestEntity<>(body, request.getHeaders(), request.getMethod(), request.getURI());
}
}