INT-1394: add expression parameter source

This commit is contained in:
David Syer
2010-09-01 14:09:00 +00:00
parent c73cf27dd8
commit 93be55d84d
15 changed files with 302 additions and 164 deletions

View File

@@ -18,9 +18,12 @@ package org.springframework.integration.util;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.expression.MapAccessor;
import org.springframework.core.convert.ConversionService;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.Message;
import org.springframework.integration.MessageHandlingException;
@@ -28,16 +31,21 @@ import org.springframework.integration.context.SimpleBeanResolver;
/**
* @author Mark Fisher
* @author Dave Syer
*
* @since 2.0
*/
public abstract class AbstractExpressionEvaluator implements BeanFactoryAware {
private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
private final ExpressionParser expressionParser = new SpelExpressionParser();
private final BeanFactoryTypeConverter typeConverter = new BeanFactoryTypeConverter();
public AbstractExpressionEvaluator() {
evaluationContext.setTypeConverter(typeConverter);
evaluationContext.addPropertyAccessor(new MapAccessor());
}
/**
@@ -76,8 +84,20 @@ public abstract class AbstractExpressionEvaluator implements BeanFactoryAware {
}
}
protected <T> T evaluateExpression(Expression expression, Object message, Class<T> expectedType) {
return expression.getValue(this.evaluationContext, message, expectedType);
protected <T> T evaluateExpression(String expression, Object input) {
return evaluateExpression(expression, input, null);
}
protected <T> T evaluateExpression(String expression, Object input, Class<T> expectedType) {
return expressionParser.parseExpression(expression).getValue(this.evaluationContext, input, expectedType);
}
protected <T> T evaluateExpression(Expression expression, Object input) {
return evaluateExpression(expression, input, null);
}
protected <T> T evaluateExpression(Expression expression, Object input, Class<T> expectedType) {
return expression.getValue(this.evaluationContext, input, expectedType);
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2002-2010 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.jdbc;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.jdbc.core.namedparam.AbstractSqlParameterSource;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
/**
* A default implementation of {@link SqlParameterSourceFactory} which creates an {@link SqlParameterSource} to
* reference bean properties in its input.
*
* @author Dave Syer
* @since 2.0
*/
public class BeanPropertySqlParameterSourceFactory implements SqlParameterSourceFactory {
private Map<String, Object> staticParameters;
public BeanPropertySqlParameterSourceFactory() {
this.staticParameters = Collections.unmodifiableMap(new HashMap<String, Object>());
}
/**
* If the input is a List or a Map, the output is a map parameter source, and in that case some static parameters
* can be added (default is empty). If the input is not a List or a Map then this value is ignored.
*
* @param staticParameters the static parameters to set
*/
public void setStaticParameters(Map<String, Object> staticParameters) {
this.staticParameters = staticParameters;
}
public SqlParameterSource createParameterSource(Object input) {
SqlParameterSource toReturn = new StaticBeanPropertySqlParameterSource(input, staticParameters);
return toReturn;
}
private static class StaticBeanPropertySqlParameterSource extends AbstractSqlParameterSource implements
SqlParameterSource {
private final BeanPropertySqlParameterSource input;
private final Map<String, Object> staticParameters;
public StaticBeanPropertySqlParameterSource(Object input, Map<String, Object> staticParameters) {
this.input = new BeanPropertySqlParameterSource(input);
this.staticParameters = staticParameters;
}
public Object getValue(String paramName) throws IllegalArgumentException {
return staticParameters.containsKey(paramName) ? staticParameters.get(paramName) : input
.getValue(paramName);
}
public boolean hasValue(String paramName) {
return staticParameters.containsKey(paramName) || input.hasValue(paramName);
}
}
}

View File

@@ -1,130 +0,0 @@
/*
* Copyright 2002-2010 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.jdbc;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
/**
* A default implementation of {@link SqlParameterSourceFactory} which creates an {@link SqlParameterSource} according
* to the type of the data passed in.
*
* <ul>
* <li>
* Where the data is a List, a list of ids is generated by looking for a map entry or bean property named by default
* 'id'. The resulting {@link SqlParameterSource} is a map that contains this list under a default key of 'idList'.</li>
* <li>
* Where the data is a {@link Map}, this is wrapped in an instance of {@link MapSqlParameterSource}.</li>
* <li>
* Otherwise the result is wrapped in a {@link BeanPropertySqlParameterSource}.</li>
* </ul>
*
* @author Jonas Partner
* @author Dave Syer
* @since 2.0
*/
public class DefaultSqlParameterSourceFactory implements SqlParameterSourceFactory {
private final Log logger = LogFactory.getLog(getClass());
private Map<String, Object> staticParameters;
private String rowIdName = "id";
private String idsParamName = "idList";
public DefaultSqlParameterSourceFactory() {
this.staticParameters = Collections.unmodifiableMap(new HashMap<String, Object>());
}
/**
* Name of the id property in the input elements when the input data is List. Defaults to "id".
* If the input is not a List then this value is ignored.
* @param rowIdName the name to set
*/
public void setRowIdName(String rowIdName) {
this.rowIdName = rowIdName;
}
/**
* Name of the id list in the output parameters if the input is a List (default "idList"). If the input is not a
* List then this value is ignored.
*
* @param idsParamName the name to set
*/
public void setIdsParameterName(String idsParamName) {
this.idsParamName = idsParamName;
}
/**
* If the input is a List or a Map, the output is a map parameter source, and in that case some static parameters
* can be added (default is empty). If the input is not a List or a Map then this value is ignored.
*
* @param staticParameters the static parameters to set
*/
public void setStaticParameters(Map<String, Object> staticParameters) {
this.staticParameters = staticParameters;
}
@SuppressWarnings("unchecked")
public SqlParameterSource createParameterSource(Object input) {
SqlParameterSource toReturn;
if (input instanceof List) {
List<Object> ids = new ArrayList<Object>();
for (Object rowObj : (List) input) {
if (rowObj instanceof Map) {
ids.add(((Map) rowObj).get(this.rowIdName));
}
else {
DirectFieldAccessor accessor = new DirectFieldAccessor(rowObj);
if (accessor.isReadableProperty(this.rowIdName)) {
ids.add(accessor.getPropertyValue(this.rowIdName));
}
else {
logger.warn("No id field named '" + this.rowIdName
+ "' found for result of polled row. Update may not include all rows.");
}
}
}
MapSqlParameterSource thisParamSource = new MapSqlParameterSource();
if (this.staticParameters != null) {
thisParamSource.addValues(this.staticParameters);
}
thisParamSource.addValue(this.idsParamName, ids);
thisParamSource.getValue("idList");
toReturn = thisParamSource;
}
else if (input instanceof Map) {
MapSqlParameterSource mapParameterSource = new MapSqlParameterSource((Map) input);
mapParameterSource.addValues(this.staticParameters);
toReturn = mapParameterSource;
}
else {
BeanPropertySqlParameterSource beanParameterSource = new BeanPropertySqlParameterSource(input);
toReturn = beanParameterSource;
}
return toReturn;
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2002-2010 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.jdbc;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.expression.ExpressionException;
import org.springframework.integration.util.AbstractExpressionEvaluator;
import org.springframework.jdbc.core.namedparam.AbstractSqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
/**
* An implementation of {@link SqlParameterSourceFactory} which creates an {@link SqlParameterSource} that evaluates
* Spring EL expressions. In addition the user can supply static parameters that always take precedence.
*
* @author Dave Syer
* @since 2.0
*/
public class ExpressionEvaluatingSqlParameterSourceFactory extends AbstractExpressionEvaluator implements SqlParameterSourceFactory {
private final static Log logger = LogFactory.getLog(ExpressionEvaluatingSqlParameterSourceFactory.class);
private static final Object ERROR = new Object();
private Map<String, ?> staticParameters;
public ExpressionEvaluatingSqlParameterSourceFactory() {
this.staticParameters = Collections.unmodifiableMap(new HashMap<String, Object>());
}
/**
* If the input is a List or a Map, the output is a map parameter source, and in that case some static parameters
* can be added (default is empty). If the input is not a List or a Map then this value is ignored.
*
* @param staticParameters the static parameters to set
*/
public void setStaticParameters(Map<String, ?> staticParameters) {
this.staticParameters = staticParameters;
}
public SqlParameterSource createParameterSource(final Object input) {
SqlParameterSource toReturn = new ExpressionEvaluatingSqlParameterSource(input, staticParameters);
return toReturn;
}
private class ExpressionEvaluatingSqlParameterSource extends AbstractSqlParameterSource {
private final Object input;
private Map<String, Object> values = new ConcurrentHashMap<String, Object>();
private ExpressionEvaluatingSqlParameterSource(Object input, Map<String, ?> staticParameters) {
this.input = input;
this.values.putAll(staticParameters);
}
public Object getValue(String paramName) throws IllegalArgumentException {
if (values.containsKey(paramName)) {
return values.get(paramName);
}
String expression = paramName;
if (input instanceof Collection<?>) {
expression = "#root.!["+paramName+"]";
}
Object value = evaluateExpression(expression, input);
values.put(paramName, value);
return value;
}
public boolean hasValue(String paramName) {
try {
Object value = getValue(paramName);
if (value==ERROR) {
return false;
}
} catch (ExpressionException e) {
if (logger.isDebugEnabled()) {
logger.debug("Could not evaluate expression", e);
}
values.put(paramName, ERROR);
return false;
}
return true;
}
}
}

View File

@@ -46,7 +46,7 @@ public class JdbcMessageHandler extends AbstractMessageHandler {
private volatile String updateSql;
private volatile SqlParameterSourceFactory sqlParameterSourceFactory = new DefaultSqlParameterSourceFactory();
private volatile SqlParameterSourceFactory sqlParameterSourceFactory = new BeanPropertySqlParameterSourceFactory();
/**
* Constructor taking {@link DataSource} from which the DB Connection can be obtained and the select query to

View File

@@ -59,7 +59,7 @@ public class JdbcPollingChannelAdapter implements MessageSource<Object> {
private volatile String updateSql;
private volatile SqlParameterSourceFactory sqlParameterSourceFactory = new DefaultSqlParameterSourceFactory();
private volatile SqlParameterSourceFactory sqlParameterSourceFactory = new ExpressionEvaluatingSqlParameterSourceFactory();
private int maxRowsPerPoll = 0;

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2002-2010 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.jdbc;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
/**
* @author Dave Syer
*
*/
public class ExpressionEvaluatingSqlParameterSourceFactoryTests {
private ExpressionEvaluatingSqlParameterSourceFactory factory = new ExpressionEvaluatingSqlParameterSourceFactory();
@Test
public void testSetStaticParameters() {
factory.setStaticParameters(Collections.singletonMap("foo", "bar"));
SqlParameterSource source = factory.createParameterSource(null);
assertTrue(source.hasValue("foo"));
assertEquals("bar", source.getValue("foo"));
}
@Test
public void testMapInput() {
SqlParameterSource source = factory.createParameterSource(Collections.singletonMap("foo", "bar"));
assertTrue(source.hasValue("foo"));
assertEquals("bar", source.getValue("foo"));
}
@Test
public void testListOfMapsInput() {
@SuppressWarnings("unchecked")
SqlParameterSource source = factory.createParameterSource(Arrays.asList(Collections.singletonMap("foo", "bar"),
Collections.singletonMap("foo", "bucket")));
String expression = "foo";
assertTrue(source.hasValue(expression));
assertEquals("[bar, bucket]", source.getValue(expression).toString());
}
@Test
public void testMapInputWithExpression() {
SqlParameterSource source = factory.createParameterSource(Collections.singletonMap("foo", "bar"));
assertTrue(source.hasValue("foo.toUpperCase()"));
assertEquals("BAR", source.getValue("foo.toUpperCase()"));
}
}

View File

@@ -141,7 +141,7 @@ public class JdbcPollingChannelAdapterIntegrationTests {
JdbcPollingChannelAdapter adapter = new JdbcPollingChannelAdapter(
this.embeddedDatabase, "select * from item where status=2");
adapter
.setUpdateSql("update item set status = 10 where id in (:idList)");
.setUpdateSql("update item set status = 10 where id in (:id)");
adapter.setRowMapper(new ItemRowMapper());
this.jdbcTemplate.update("insert into item values(1,2)");

View File

@@ -71,7 +71,7 @@ public class JdbcMessageHandlerParserTests {
channel.send(message);
Map<String, Object> map = this.jdbcTemplate.queryForMap("SELECT * from FOOS");
assertEquals("Wrong id", message.getHeaders().getId().toString(), map.get("ID"));
assertEquals("Wrong name", "foo", map.get("name"));
assertEquals("Wrong name", "bar", map.get("name"));
}
@After

View File

@@ -9,12 +9,12 @@
http://www.springframework.org/schema/integration/jdbc
http://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd">
<outbound-channel-adapter query="insert into foos (id, status, name) values (:headers[$id], 0, :payload)"
<outbound-channel-adapter query="insert into foos (id, status, name) values (:headers[$id], 0, :foo)"
channel="target" data-source="dataSource" sql-parameter-source-factory="sqlParameterSourceFactory"/>
<beans:import resource="jdbcOutboundChannelAdapterCommonConfig.xml" />
<beans:bean id="sqlParameterSourceFactory" class="org.springframework.integration.jdbc.DefaultSqlParameterSourceFactory">
<beans:bean id="sqlParameterSourceFactory" class="org.springframework.integration.jdbc.BeanPropertySqlParameterSourceFactory">
<beans:property name="staticParameters">
<beans:map><beans:entry key="foo" value="bar"/></beans:map>
</beans:property>

View File

@@ -12,7 +12,7 @@
<inbound-channel-adapter channel="target" data-source="dataSource">
<query>select * from item where status=2</query>
<update>update item set status=10 where id in (:idList)</update>
<update>update item set status=10 where id in (:id)</update>
</inbound-channel-adapter>
<beans:import resource="jdbcInboundChannelAdapterCommonConfig.xml" />

View File

@@ -11,7 +11,7 @@
http://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd">
<inbound-channel-adapter query="select * from item where status=2" channel="target"
data-source="dataSource" update="update item set status=10 where id in (:idList)" />
data-source="dataSource" update="update item set status=10 where id in (:id)" />
<beans:import resource="jdbcInboundChannelAdapterCommonConfig.xml" />

View File

@@ -11,7 +11,7 @@
<inbound-channel-adapter query="select * from item where status=2"
channel="target" data-source="dataSource" max-rows-per-poll="2"
update="update item set status=10 where id in (:idList)" />
update="update item set status=10 where id in (:id)" />
<beans:import resource="jdbcInboundChannelAdapterCommonConfig.xml" />

View File

@@ -9,14 +9,17 @@
http://www.springframework.org/schema/integration/jdbc
http://www.springframework.org/schema/integration/jdbc/spring-integration-jdbc.xsd">
<inbound-channel-adapter query="select * from item where status=2" channel="target" update="update item set status=1, name=:foo where id in (:idList)"
jdbc-operations="jdbcTemplate" sql-parameter-source-factory="sqlParameterSourceFactory" />
<inbound-channel-adapter query="select * from item where status=2" channel="target"
update="update item set status=1, name=:foo where id in (:id)" jdbc-operations="jdbcTemplate"
sql-parameter-source-factory="sqlParameterSourceFactory" />
<beans:import resource="jdbcInboundChannelAdapterCommonConfig.xml" />
<beans:bean id="sqlParameterSourceFactory" class="org.springframework.integration.jdbc.DefaultSqlParameterSourceFactory">
<beans:bean id="sqlParameterSourceFactory" class="org.springframework.integration.jdbc.ExpressionEvaluatingSqlParameterSourceFactory">
<beans:property name="staticParameters">
<beans:map><beans:entry key="foo" value="bar"/></beans:map>
<beans:map>
<beans:entry key="foo" value="bar" />
</beans:map>
</beans:property>
</beans:bean>

View File

@@ -28,22 +28,23 @@
<para>The inbound adapter also requires a reference to either
<classname>JdbcTemplate</classname> instance or
<interfacename>DataSource</interfacename>. The following example defines
an inbound Channel Adapter with a <classname>DataSource</classname>
reference. <programlisting language="xml"><![CDATA[<jdbc:inbound-channel-adapter query="select * from item where status=2"
channel="target" data-source="dataSource"
update="update item set status=10 where id in (:idList)" />]]></programlisting>
<note>
The parameters in the update query are specified with a colon (:) prefix to the name of a map key. This is a standard feature of the named parameter JDBC support in Spring JDBC.
</note></para>
<interfacename>DataSource</interfacename>.</para>
<para>As well as the <code>SELECT</code> statement to generate the
messages, the adapter above also has an <code>UPDATE</code> statement that
is being used to mark the records as processed, so they don't show up in
the next poll. The update is parameterised by the list of ids from the
the next poll. The update can be parameterised by the list of ids from the
original select. This is done through a naming convention by default (a
column in the input result set called "id" is translated into a list in
the parameter map for the update called "idList"). To change the parameter
the parameter map for the update called "id"). The following example defines
an inbound Channel Adapter with an update query and a <classname>DataSource</classname>
reference. <programlisting language="xml"><![CDATA[<jdbc:inbound-channel-adapter query="select * from item where status=2"
channel="target" data-source="dataSource"
update="update item set status=10 where id in (:id)" />]]></programlisting>
<note>
The parameters in the update query are specified with a colon (:) prefix to the name of a parameter (which in this case is an expression to be applied to each of the rows in the polled result set). This is a standard feature of the named parameter JDBC support in Spring JDBC combined with a convention (projection onto the polled result list) adopted in Spring Integration. The underlying Spring JDBC features limit the available expressions (e.g. most special characters other than period are disallowed), but since the target is usually a list of or an individual object addressable by simple bean paths this isn't unduly restrictive.
</note>
To change the parameter
generation strategy you can inject a
<classname>SqlParameterSourceFactory</classname> into the adapter to
override the default behaviour (the adapter has a
@@ -92,15 +93,11 @@
example above, messages arriving on the channel "input" have a payload of
a map with key "foo", so the <code>[]</code> operator dereferences that
value from the map. The headers are also accessed as a map. <note>
The parameters in the query above are bean paths in the incoming message (they are not Spring EL expressions). This behaviour is part of the
<classname>MapSqlParameterSource</classname>
in Spring JDBC, which is the default source created by the outbound adapter. Other behaviour is possible in the adapter, and only requires the user to inject a different
<classname>SqlParameterSourceFactory</classname>
.
The parameters in the query above are bean property expressions on the incoming message (not Spring EL expressions). This behaviour is part of the
<classname>SqlParameterSource</classname> which is the default
source created by the outbound adapter. Other behaviour is possible
in the adapter, and requires the user to inject a different
<classname>SqlParameterSourceFactory</classname>.
</note></para>
<para>The outbound adapter requires a reference to either a DataSource or