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 9de441aaae..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; @@ -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 { @@ -338,24 +340,23 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe String uri = this.uriExpression.getValue(this.evaluationContext, requestMessage, String.class); Assert.notNull(uri, "URI Expression evaluation cannot result in null"); 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); - } - 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); @@ -554,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; @@ -564,5 +566,7 @@ public class HttpRequestExecutingMessageHandler extends AbstractReplyProducingMe expectedResponseType = ClassUtils.forName(expectedResponseTypeName, ClassUtils.getDefaultClassLoader()); } return expectedResponseType; + } + } 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()); + } + }