INT-2975: Add ValueExpression for Typed SpEL

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

INT-2975: Add `type` attr. for enricher property

INT-2975: Check for `type`, polishing, docs

Enricher: deprecate property expression type attr
This commit is contained in:
Artem Bilan
2014-04-13 16:35:43 +03:00
committed by Gary Russell
parent de123c1ee4
commit 466af8a2f5
6 changed files with 290 additions and 29 deletions

View File

@@ -21,10 +21,12 @@ import java.util.List;
import org.w3c.dom.Element;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.ExpressionFactoryBean;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.integration.transformer.ContentEnricher;
import org.springframework.integration.transformer.support.ExpressionEvaluatingHeaderValueMessageProcessor;
import org.springframework.util.CollectionUtils;
@@ -55,9 +57,48 @@ public class EnricherParser extends AbstractConsumerEndpointParser {
ManagedMap<String, Object> expressions = new ManagedMap<String, Object>();
for (Element subElement : subElements) {
String name = subElement.getAttribute("name");
BeanDefinition beanDefinition = IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("value",
"expression", parserContext, subElement, true);
expressions.put(name, beanDefinition);
String value = subElement.getAttribute("value");
String type = subElement.getAttribute("type");
String expression = subElement.getAttribute("expression");
boolean hasAttributeValue = StringUtils.hasText(value);
boolean hasAttributeExpression = StringUtils.hasText(expression);
if (hasAttributeValue && hasAttributeExpression){
parserContext.getReaderContext().error("Only one of 'value' or 'expression' is allowed", element);
}
if (!hasAttributeValue && !hasAttributeExpression){
parserContext.getReaderContext().error("One of 'value' or 'expression' is required", element);
}
BeanDefinition expressionDef;
if (hasAttributeValue) {
BeanDefinitionBuilder expressionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ValueExpression.class);
if (StringUtils.hasText(type)) {
expressionBuilder.addConstructorArgValue(new TypedStringValue(value, type));
}
else {
expressionBuilder.addConstructorArgValue(value);
}
expressionDef = expressionBuilder.getBeanDefinition();
}
else {
if (StringUtils.hasText(type)) {
parserContext.getReaderContext().error("The 'type' attribute for '<property>' of '<enricher>' " +
"is not allowed with an 'expression' attribute.", element);
}
expressionDef = BeanDefinitionBuilder
.genericBeanDefinition(ExpressionFactoryBean.class)
.addConstructorArgValue(expression)
.getBeanDefinition();
}
expressions.put(name, expressionDef);
}
builder.addPropertyValue("propertyExpressions", expressions);
}
@@ -67,10 +108,18 @@ public class EnricherParser extends AbstractConsumerEndpointParser {
ManagedMap<String, Object> expressions = new ManagedMap<String, Object>();
for (Element subElement : subElements) {
String name = subElement.getAttribute("name");
BeanDefinition expressionDefinition = IntegrationNamespaceUtils.createExpressionDefinitionFromValueOrExpression("value",
"expression", parserContext, subElement, true);
BeanDefinitionBuilder valueProcessorBuilder = BeanDefinitionBuilder.genericBeanDefinition(ExpressionEvaluatingHeaderValueMessageProcessor.class);
valueProcessorBuilder.addConstructorArgValue(expressionDefinition)
BeanDefinition expressionDefinition = IntegrationNamespaceUtils
.createExpressionDefinitionFromValueOrExpression("value", "expression", parserContext,
subElement, true);
if (StringUtils.hasText(subElement.getAttribute("expression"))
&& StringUtils.hasText(subElement.getAttribute("type"))) {
parserContext.getReaderContext()
.warning("The use of a 'type' attribute is deprecated since 4.0 "
+ "when using 'expression'", element);
}
BeanDefinitionBuilder valueProcessorBuilder = BeanDefinitionBuilder
.genericBeanDefinition(ExpressionEvaluatingHeaderValueMessageProcessor.class)
.addConstructorArgValue(expressionDefinition)
.addConstructorArgValue(subElement.getAttribute("type"));
IntegrationNamespaceUtils.setValueIfAttributeDefined(valueProcessorBuilder, subElement, "overwrite");
expressions.put(name, valueProcessorBuilder.getBeanDefinition());

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2014 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 org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.TypedValue;
import org.springframework.util.Assert;
/**
* A very simple hardcoded implementation of the {@link Expression} interface that represents an
* immutable value. It is used as value holder in the context of expression evaluation.
*
* @param <V> - The expected value type.
*
* @author Artem Bilan
* @since 4.0
*/
public class ValueExpression<V> implements Expression {
/** Fixed value of this expression */
private final V value;
private final Class<V> aClass;
private final TypedValue typedResultValue;
private final TypeDescriptor typeDescriptor;
@SuppressWarnings("unchecked")
public ValueExpression(V value) {
Assert.notNull(value);
this.value = value;
this.aClass = (Class<V>) this.value.getClass();
this.typedResultValue = new TypedValue(this.value);
this.typeDescriptor = this.typedResultValue.getTypeDescriptor();
}
@Override
public V getValue() throws EvaluationException {
return this.value;
}
@Override
public V getValue(Object rootObject) throws EvaluationException {
return this.value;
}
@Override
public V getValue(EvaluationContext context) throws EvaluationException {
return this.value;
}
@Override
public V getValue(EvaluationContext context, Object rootObject) throws EvaluationException {
return this.value;
}
@Override
public <T> T getValue(Object rootObject, Class<T> desiredResultType) throws EvaluationException {
return getValue(desiredResultType);
}
@Override
public <T> T getValue(Class<T> desiredResultType) throws EvaluationException {
return org.springframework.expression.common.ExpressionUtils
.convertTypedValue(null, this.typedResultValue, desiredResultType);
}
@Override
public <T> T getValue(EvaluationContext context, Object rootObject, Class<T> desiredResultType) throws EvaluationException {
return getValue(context, desiredResultType);
}
@Override
public <T> T getValue(EvaluationContext context, Class<T> desiredResultType) throws EvaluationException {
return org.springframework.expression.common.ExpressionUtils
.convertTypedValue(context, this.typedResultValue, desiredResultType);
}
@Override
public Class<V> getValueType() throws EvaluationException {
return this.aClass;
}
@Override
public Class<V> getValueType(Object rootObject) throws EvaluationException {
return this.aClass;
}
@Override
public Class<V> getValueType(EvaluationContext context) throws EvaluationException {
return this.aClass;
}
@Override
public Class<V> getValueType(EvaluationContext context, Object rootObject) throws EvaluationException {
return this.aClass;
}
@Override
public TypeDescriptor getValueTypeDescriptor() throws EvaluationException {
return this.typeDescriptor;
}
@Override
public TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException {
return this.typeDescriptor;
}
@Override
public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException {
return this.typeDescriptor;
}
@Override
public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) throws EvaluationException {
return this.typeDescriptor;
}
@Override
public boolean isWritable(EvaluationContext context) throws EvaluationException {
return false;
}
@Override
public boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException {
return false;
}
@Override
public boolean isWritable(Object rootObject) throws EvaluationException {
return false;
}
@Override
public void setValue(EvaluationContext context, Object value) throws EvaluationException {
setValue(context, null, value);
}
@Override
public void setValue(Object rootObject, Object value) throws EvaluationException {
setValue(null, rootObject, value);
}
@Override
public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException {
throw new EvaluationException(this.value.toString(), "Cannot call setValue() on a ValueExpression");
}
@Override
public String getExpressionString() {
return this.value.toString();
}
}

View File

@@ -1325,13 +1325,6 @@
<xsd:union memberTypes="xsd:boolean xsd:string" />
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="type" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:java.lang.Class">
The fully qualified class name of the header value's expected type.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
@@ -1486,6 +1479,14 @@
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="type" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:java.lang.Class"><![CDATA[
The fully qualified class name of the header value's expected type.
Allowed only in case of 'value' attribute.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:element name="delayer">

View File

@@ -2,10 +2,13 @@
<beans:beans xmlns="http://www.springframework.org/schema/integration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration
http://www.springframework.org/schema/integration/spring-integration.xsd">
http://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd">
<channel id="input"/>
@@ -27,7 +30,8 @@
order="99" should-clone-payload="true" output-channel="output">
<property name="name" expression="payload.sourceName"/>
<property name="age" value="42"/>
<property name="gender" expression="@testBean"/>
<property name="gender" value="#{testBean}"/>
<property name="married" value="1" type="java.lang.Boolean"/>
<header name="foo" value="bar"/>
<header name="testBean" expression="@testBean"/>
@@ -43,8 +47,6 @@
<header name="foo" expression="new java.util.Date()" type="int"/>
</enricher>
<beans:bean id="testBean" class="java.lang.String">
<beans:constructor-arg value="male"/>
</beans:bean>
<util:constant id="testBean" static-field="org.springframework.integration.config.xml.EnricherParserTests$Gender.MALE"/>
</beans:beans>

View File

@@ -91,10 +91,14 @@ public class EnricherParserTests {
assertEquals("42", e.getValue().getExpressionString());
}
else if ("gender".equals(e.getKey().getExpressionString())) {
assertEquals("@testBean", e.getValue().getExpressionString());
assertEquals(Gender.MALE.name(), e.getValue().getExpressionString());
}
else if ("married".equals(e.getKey().getExpressionString())) {
assertEquals(Boolean.TRUE.toString(), e.getValue().getExpressionString());
}
else {
throw new IllegalStateException("expected 'name', 'age', and 'gender' only, not: " + e.getKey().getExpressionString());
throw new IllegalStateException("expected 'name', 'age', 'gender' and married only, not: "
+ e.getKey().getExpressionString());
}
}
@@ -127,12 +131,15 @@ public class EnricherParserTests {
@Test
public void integrationTest() {
SubscribableChannel requests = context.getBean("requests", SubscribableChannel.class);
class Foo extends AbstractReplyProducingMessageHandler {
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
return new Source("foo");
}
};
}
Foo foo = new Foo();
foo.setOutputChannel(context.getBean("replies", MessageChannel.class));
requests.subscribe(foo);
@@ -146,13 +153,14 @@ public class EnricherParserTests {
Target enriched = (Target) reply.getPayload();
assertEquals("foo", enriched.getName());
assertEquals(42, enriched.getAge());
assertEquals("male", enriched.getGender());
assertEquals(Gender.MALE, enriched.getGender());
assertTrue(enriched.isMarried());
assertNotSame(original, enriched);
assertEquals(1, adviceCalled);
MessageHeaders headers = reply.getHeaders();
assertEquals("bar", headers.get("foo"));
assertEquals("male", headers.get("testBean"));
assertEquals(Gender.MALE, headers.get("testBean"));
assertEquals("foo", headers.get("sourceName"));
assertEquals("test", headers.get("notOverwrite"));
}
@@ -191,7 +199,9 @@ public class EnricherParserTests {
private volatile int age;
private volatile String gender;
private volatile Gender gender;
private volatile boolean married;
public String getName() {
return name;
@@ -209,23 +219,37 @@ public class EnricherParserTests {
this.age = age;
}
public String getGender() {
public Gender getGender() {
return gender;
}
public void setGender(String gender) {
public void setGender(Gender gender) {
this.gender = gender;
}
public boolean isMarried() {
return married;
}
public void setMarried(boolean married) {
this.married = married;
}
@Override
public Object clone() {
Target copy = new Target();
copy.setName(this.name);
copy.setAge(this.age);
copy.setGender(this.gender);
copy.setMarried(this.married);
return copy;
}
}
public static enum Gender {
MALE, FEMALE
}
public static class FooAdvice extends AbstractRequestHandlerAdvice {
@Override

View File

@@ -274,8 +274,8 @@
should-clone-payload="false"> ]]><co id="payload-enricher09-co" linkends="payload-enricher09" /><![CDATA[
<int:poller></int:poller> ]]><co id="payload-enricher10-co" linkends="payload-enricher10" /><![CDATA[
<int:property name="" expression=""/> ]]><co id="payload-enricher11-co" linkends="payload-enricher11" /><![CDATA[
<int:property name="" value=""/>
<int:header name="" expression=""/> ]]><co id="payload-enricher12-co" linkends="payload-enricher12" /><![CDATA[
<int:property name="" value="23" type="java.lang.Integer"/>
<int:header name="" expression=""/> ]]><co id="payload-enricher12-co" linkends="payload-enricher12" /><![CDATA[
<int:header name="" value="" overwrite="" type=""/>
</int:enricher>]]></programlisting>
@@ -408,6 +408,19 @@
application context (using the '@&lt;beanName&gt;.&lt;beanProperty&gt;'
SpEL syntax).
</para>
<para>
Starting with <emphasis>4.0</emphasis>, when specifying
a <code>value</code> attribute, you can also specify an optional
<code>type</code> attribute. When the destination is a
typed setter method, the framework will coerce the value
appropriately (as long as a <classname>PropertyEditor</classname>)
exists to handle the conversion. If however, the target
payload is a <interfacename>Map</interfacename> the
entry will be populated with the value without conversion. The
<code>type</code> attribute allows you to, say, convert
a String containing a number to an <classname>Integer</classname>
value in the target payload.
</para>
</callout>
<callout arearefs="payload-enricher12-co" id="payload-enricher12">
<para>