From 3369d3f9b1752b1d7e3b9655f27f2cb924a2cc39 Mon Sep 17 00:00:00 2001 From: Wallace Wadge Date: Mon, 10 Jun 2013 11:19:08 +0200 Subject: [PATCH 1/2] INT-3054 URI Variable Mapping Improvements Permit missing URI vars in HTTP outbound gateway Do not attempt to evaluate uri variables that are not in the URL The http gateway might not know what uri variables it might obtain in the URI. This patch makes sure that only the uri variables defined in the uri are parsed (the rest are ignored) --- .../HttpRequestExecutingMessageHandler.java | 16 +++++++-- .../outbound/UriVariableExpressionTests.java | 35 ++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java index 9de441aaae..2aedef7229 100755 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java @@ -63,6 +63,7 @@ import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriTemplate; /** * A {@link MessageHandler} implementation that executes HTTP requests by delegating @@ -79,6 +80,7 @@ import org.springframework.web.util.UriComponentsBuilder; * @author Gary Russell * @author Gunnar Hillert * @author Artem Bilan + * @author Wallace Wadge * @since 2.0 */ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMessageHandler { @@ -337,11 +339,19 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe protected Object handleRequestMessage(Message requestMessage) { String uri = this.uriExpression.getValue(this.evaluationContext, requestMessage, String.class); Assert.notNull(uri, "URI Expression evaluation cannot result in null"); + List uriVariableNames = new UriTemplate(uri).getVariableNames(); + + if (logger.isWarnEnabled() && this.uriVariableExpressions.size() != uriVariableNames.size()){ + logger.warn("The number of URI variables expecting resolution in the provided uri do not match those in the provided variables"); + } try { Map uriVariables = new HashMap(); - for (Map.Entry entry : this.uriVariableExpressions.entrySet()) { - Object value = entry.getValue().getValue(this.evaluationContext, requestMessage, String.class); - uriVariables.put(entry.getKey(), value); + for (String var : uriVariableNames) { + Expression exp = this.uriVariableExpressions.get(var); + if (exp != null){ + Object value = exp.getValue(this.evaluationContext, requestMessage, String.class); + uriVariables.put(var, value); + } } HttpMethod httpMethod = this.determineHttpMethod(requestMessage); diff --git a/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/UriVariableExpressionTests.java b/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/UriVariableExpressionTests.java index b974837838..976d6695ab 100644 --- a/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/UriVariableExpressionTests.java +++ b/spring-integration-http/src/test/java/org/springframework/integration/http/outbound/UriVariableExpressionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2013 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,10 +21,13 @@ import static org.junit.Assert.assertEquals; import java.io.IOException; import java.net.URI; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; +import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; @@ -35,6 +38,7 @@ import org.springframework.integration.message.GenericMessage; /** * @author Dave Syer * @author Mark Fisher + * @author Wallace Wadge * @since 2.0 */ public class UriVariableExpressionTests { @@ -65,4 +69,33 @@ public class UriVariableExpressionTests { assertEquals("http://test/bar", uriHolder.get().toString()); } + /** Test for INT-3054: Do not break if there are extra uri variables defined in the http outbound gateway. */ + @Test + public void testFromMessageWithSuperfluousExpressionsInt3054() throws Exception { + final AtomicReference uriHolder = new AtomicReference(); + HttpRequestExecutingMessageHandler handler = new HttpRequestExecutingMessageHandler("http://test/{foo}"); + SpelExpressionParser parser = new SpelExpressionParser(); + Map multipleExpressions = new HashMap(); + multipleExpressions.put("foo", parser.parseExpression("payload")); + multipleExpressions.put("extra-to-be-ignored", parser.parseExpression("headers.extra")); + handler.setUriVariableExpressions(multipleExpressions); + handler.setRequestFactory(new SimpleClientHttpRequestFactory() { + @Override + public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { + uriHolder.set(uri); + throw new RuntimeException("intentional"); + } + }); + Message message = new GenericMessage("bar"); + Exception exception = null; + try { + handler.handleMessage(message); + } + catch (Exception e) { + exception = e; + } + assertEquals("intentional", exception.getCause().getMessage()); + assertEquals("http://test/bar", uriHolder.get().toString()); + } + } From 56812d3861d791c162d011d64d210d43856c0366 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Sat, 15 Jun 2013 14:10:12 +0300 Subject: [PATCH 2/2] INT-3054: Add ExpressionEvalMap Support Use an immutable Map wrapper to evaluate expressions only when accessed. Polishing --- .../expression/ExpressionEvalMap.java | 306 ++++++++++++++++++ .../HttpRequestExecutingMessageHandler.java | 26 +- 2 files changed, 316 insertions(+), 16 deletions(-) create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionEvalMap.java diff --git a/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionEvalMap.java b/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionEvalMap.java new file mode 100644 index 0000000000..1e726543a6 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/expression/ExpressionEvalMap.java @@ -0,0 +1,306 @@ +/* + * Copyright 2013 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.expression; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; + +/** + *

+ * An immutable {@link AbstractMap} implementation that wraps a Map + * and evaluates an {@code expression} for the provided {@code key} from the underlying + * {@code original} Map. + *

+ *

+ * Any mutating operations ({@link #put(String, Object)}, {@link #remove(Object)} etc.) + * are not allowed on instances of this class. Mutation can be performed on underlying Map + * if it supports it. + *

+ *

+ * A {@link ExpressionEvalMapBuilder} must be used to instantiate this class + * via its {@link #from(Map)} method: + * {@code + * ExpressionEvalMap evalMap = ExpressionEvalMap + * .from(expressions) + .usingCallback(new EvaluationCallback() { + Object evaluate(Expression expression) { + // return some expression evaluation + } + }) + .build(); + * } + *

+ *

+ * Thread-safety depends on the original underlying Map. + * Objects of this class are not serializable. + *

+ * + * @author Artem Bilan + * @since 3.0 + */ +public final class ExpressionEvalMap extends AbstractMap { + + public static final EvaluationCallback SIMPLE_CALLBACK = new EvaluationCallback() { + + @Override + public Object evaluate(Expression expression) { + return expression.getValue(); + } + + }; + + private final Map original; + + private final EvaluationCallback evaluationCallback; + + private ExpressionEvalMap(Map original, EvaluationCallback evaluationCallback) { + this.original = original; + this.evaluationCallback = evaluationCallback; + } + + /** + * Gets the {@code value}({@link Expression}) for the provided {@code key} + * from {@link #original} and returns the result of evaluation using {@link #evaluationCallback}. + */ + @Override + public Object get(Object key) { + Expression expression = original.get(key); + if (expression != null) { + return this.evaluationCallback.evaluate(expression); + } + return null; + } + + @Override + public Collection values() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsKey(Object key) { + return original.containsKey(key); + } + + @Override + public Set keySet() { + return original.keySet(); + } + + @Override + public boolean isEmpty() { + return original.isEmpty(); + } + + @Override + public int size() { + return original.size(); + } + + @Override + public boolean equals(Object o) { + return original.equals(o); + } + + @Override + public int hashCode() { + return original.hashCode(); + } + + @Override + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + + @Override + public Object put(String key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public Object remove(Object key) { + throw new UnsupportedOperationException(); + } + + public static ExpressionEvalMapBuilder from(Map expressions) { + return new ExpressionEvalMapBuilder(expressions); + } + + + /** + * Implementations of this interface can be provided to build 'on demand {@link #get(Object)} logic' + * for {@link ExpressionEvalMap}. + */ + public interface EvaluationCallback { + + Object evaluate(Expression expression); + + } + + + /** + * The {@link EvaluationCallback} implementation which evaluates an expression using + * the provided {@code context}, {@code root} and {@code returnType} variables. + */ + public static class ComponentsEvaluationCallback implements EvaluationCallback { + + private final EvaluationContext context; + + private final Object root; + + private final Class returnType; + + public ComponentsEvaluationCallback(EvaluationContext context, Object root, Class returnType) { + this.context = context; + this.root = root; + this.returnType = returnType; + } + + @Override + public Object evaluate(Expression expression) { + if (this.context != null) { + return expression.getValue(this.context, this.root, this.returnType); + } + return expression.getValue(this.root, this.returnType); + } + + } + + + /** + * The builder class to instantiate {@link ExpressionEvalMap}. + */ + public static final class ExpressionEvalMapBuilder { + + private final Map expressions; + + private EvaluationCallback evaluationCallback; + + private EvaluationContext context; + + private Object root; + + private Class returnType; + + private final ExpressionEvalMapComponentsBuilder evalMapComponentsBuilder = new ExpressionEvalMapComponentsBuilderImpl(); + + private final ExpressionEvalMapFinalBuilder finalBuilder = new ExpressionEvalMapFinalBuilderImpl(); + + private ExpressionEvalMapBuilder(Map expressions) { + this.expressions = expressions; + } + + public ExpressionEvalMapFinalBuilder usingCallback(EvaluationCallback callback) { + this.evaluationCallback = callback; + return finalBuilder; + } + + public ExpressionEvalMapFinalBuilder usingSimpleCallback() { + return this.usingCallback(SIMPLE_CALLBACK); + } + + public ExpressionEvalMapComponentsBuilder usingEvaluationContext(EvaluationContext context) { + this.context = context; + return this.evalMapComponentsBuilder; + } + + public ExpressionEvalMapComponentsBuilder withRoot(Object root) { + this.root = root; + return this.evalMapComponentsBuilder; + + } + + public ExpressionEvalMapComponentsBuilder withReturnType(Class returnType) { + this.returnType = returnType; + return this.evalMapComponentsBuilder; + + } + + + private class ExpressionEvalMapFinalBuilderImpl implements ExpressionEvalMapFinalBuilder { + + @Override + public ExpressionEvalMap build() { + if (evaluationCallback != null) { + return new ExpressionEvalMap(expressions, evaluationCallback); + } + return new ExpressionEvalMap(expressions, new ComponentsEvaluationCallback(context, root, returnType)); + } + + } + + + private class ExpressionEvalMapComponentsBuilderImpl extends ExpressionEvalMapFinalBuilderImpl + implements ExpressionEvalMapComponentsBuilder { + + @Override + public ExpressionEvalMapComponentsBuilder usingEvaluationContext(EvaluationContext context) { + return ExpressionEvalMapBuilder.this.usingEvaluationContext(context); + } + + @Override + public ExpressionEvalMapComponentsBuilder withRoot(Object root) { + return ExpressionEvalMapBuilder.this.withRoot(root); + } + + @Override + public ExpressionEvalMapComponentsBuilder withReturnType(Class returnType) { + return ExpressionEvalMapBuilder.this.withReturnType(returnType); + } + + } + + } + + + public interface ExpressionEvalMapFinalBuilder { + + ExpressionEvalMap build(); + + } + + + public interface ExpressionEvalMapComponentsBuilder extends ExpressionEvalMapFinalBuilder { + + ExpressionEvalMapComponentsBuilder usingEvaluationContext(EvaluationContext context); + + ExpressionEvalMapComponentsBuilder withRoot(Object root); + + ExpressionEvalMapComponentsBuilder withReturnType(Class returnType); + + } + +} diff --git a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java index 2aedef7229..d3c5e9b956 100755 --- a/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java +++ b/spring-integration-http/src/main/java/org/springframework/integration/http/outbound/HttpRequestExecutingMessageHandler.java @@ -48,6 +48,7 @@ import org.springframework.integration.Message; import org.springframework.integration.MessageHandlingException; import org.springframework.integration.MessagingException; import org.springframework.integration.core.MessageHandler; +import org.springframework.integration.expression.ExpressionEvalMap; import org.springframework.integration.expression.ExpressionUtils; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.integration.http.support.DefaultHttpHeaderMapper; @@ -63,7 +64,6 @@ import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; -import org.springframework.web.util.UriTemplate; /** * A {@link MessageHandler} implementation that executes HTTP requests by delegating @@ -339,33 +339,24 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe protected Object handleRequestMessage(Message requestMessage) { String uri = this.uriExpression.getValue(this.evaluationContext, requestMessage, String.class); Assert.notNull(uri, "URI Expression evaluation cannot result in null"); - List uriVariableNames = new UriTemplate(uri).getVariableNames(); - - if (logger.isWarnEnabled() && this.uriVariableExpressions.size() != uriVariableNames.size()){ - logger.warn("The number of URI variables expecting resolution in the provided uri do not match those in the provided variables"); - } try { - Map uriVariables = new HashMap(); - for (String var : uriVariableNames) { - Expression exp = this.uriVariableExpressions.get(var); - if (exp != null){ - Object value = exp.getValue(this.evaluationContext, requestMessage, String.class); - uriVariables.put(var, value); - } - } - HttpMethod httpMethod = this.determineHttpMethod(requestMessage); if (!this.shouldIncludeRequestBody(httpMethod) && this.extractPayloadExplicitlySet){ if (logger.isWarnEnabled()){ logger.warn("The 'extractPayload' attribute has no relevance for the current request since the HTTP Method is '" + - httpMethod + "', and no request body will be sent for that method."); + httpMethod + "', and no request body will be sent for that method."); } } Class expectedResponseType = this.determineExpectedResponseType(requestMessage); HttpEntity httpRequest = this.generateHttpRequest(requestMessage, httpMethod); + Map uriVariables = ExpressionEvalMap + .from(this.uriVariableExpressions) + .usingEvaluationContext(this.evaluationContext) + .withRoot(requestMessage) + .build(); UriComponents uriComponents = UriComponentsBuilder.fromUriString(uri).buildAndExpand(uriVariables); URI realUri = this.encodeUri ? uriComponents.toUri() : new URI(uriComponents.toUriString()); ResponseEntity httpResponse = this.restTemplate.exchange(realUri, httpMethod, httpRequest, expectedResponseType); @@ -564,6 +555,7 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe return HttpMethod.valueOf(strHttpMethod); } + private Class determineExpectedResponseType(Message requestMessage) throws Exception{ Class expectedResponseType = null; String expectedResponseTypeName = null; @@ -574,5 +566,7 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe expectedResponseType = ClassUtils.forName(expectedResponseTypeName, ClassUtils.getDefaultClassLoader()); } return expectedResponseType; + } + }