RESOLVED - issue BATCH-1370: Bind to non-scalar map entry values in step scope

Also fixed: no need to use custom PropertyEditor to bind to Dates from step context
This commit is contained in:
dsyer
2009-08-14 09:35:50 +00:00
parent 0bf64b4a80
commit 7c487a7abc
7 changed files with 330 additions and 104 deletions

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<beansProjectDescription>
<version>1</version>
<pluginVersion><![CDATA[2.2.4.RELEASE]]></pluginVersion>
<pluginVersion><![CDATA[2.2.6.200908051215-RELEASE]]></pluginVersion>
<configSuffixes>
<configSuffix><![CDATA[xml]]></configSuffix>
</configSuffixes>
@@ -61,6 +61,8 @@
<config>src/test/resources/org/springframework/batch/core/configuration/xml/JobRegistryJobParserTests-context.xml</config>
<config>src/test/resources/org/springframework/batch/core/launch/JobLauncherIntegrationTests-context.xml</config>
<config>src/test/resources/org/springframework/batch/core/configuration/xml/StopCustomStatusJobParserTests-context.xml</config>
<config>src/test/resources/org/springframework/batch/core/scope/util/PlaceholderTargetSourceCustomEditorTests-context.xml</config>
<config>src/test/resources/org/springframework/batch/core/scope/util/PlaceholderTargetSourceErrorTests-context.xml</config>
</configs>
<configSets>
<configSet>

View File

@@ -39,14 +39,14 @@ import org.springframework.util.StringValueResolver;
/**
* A {@link TargetSource} that lazily initializes its target, replacing bean
* definition properties dynamically if they are marked as placeholders. String
* values with embedded <code>#{key}</code> patterns will be replaced with the
* values with embedded <code>%{key}</code> patterns will be replaced with the
* corresponding value from the injected context (which must also be a String).
* This includes dynamically locating a bean reference (e.g.
* <code>ref="#{foo}"</code>), and partial replacement of patterns (e.g.
* <code>value="#{foo}-bar-#{spam}"</code>). These replacements work for context
* <code>ref="%{foo}"</code>), and partial replacement of patterns (e.g.
* <code>value="%{foo}-bar-%{spam}"</code>). These replacements work for context
* values that are primitive (String, Long, Integer). You can also replace
* non-primitive values directly by making the whole bean property value into a
* placeholder (e.g. <code>value="#{foo}"</code> where <code>foo</code> is a
* placeholder (e.g. <code>value="%{foo}"</code> where <code>foo</code> is a
* property in the context).
*
* @author Dave Syer
@@ -116,7 +116,7 @@ public class PlaceholderTargetSource extends SimpleBeanTargetSource implements I
String key = (String) value;
if (key.startsWith(PLACEHOLDER_PREFIX) && key.endsWith(PLACEHOLDER_SUFFIX)) {
key = extractKey(key);
result = convertFromContext(key, requiredType, typeConverter);
result = convertFromContext(key, requiredType);
if (result == null) {
Object property = getPropertyFromContext(key);
// Give the normal type converter a chance by
@@ -126,11 +126,15 @@ public class PlaceholderTargetSource extends SimpleBeanTargetSource implements I
if (property != null) {
value = property;
}
logger.debug(String.format("Bound %%{%s} to String value [%s]", key, result));
}
else {
throw new IllegalStateException("Cannot bind to placeholder: " + key);
}
}
else {
logger.debug(String.format("Bound %%{%s} to [%s]", key, result));
}
}
}
else if (requiredType.isAssignableFrom(value.getClass())) {
@@ -165,30 +169,7 @@ public class PlaceholderTargetSource extends SimpleBeanTargetSource implements I
.getMergedBeanDefinition(targetBeanName));
logger.debug("Rehydrating scoped target: [" + targetBeanName + "]");
BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(new StringValueResolver() {
public String resolveStringValue(String strVal) {
if (!strVal.contains(PLACEHOLDER_PREFIX)) {
return strVal;
}
return replacePlaceholders(strVal, contextTypeConverter);
}
}) {
protected Object resolveValue(Object value) {
if (value instanceof TypedStringValue) {
TypedStringValue typedStringValue = (TypedStringValue) value;
String stringValue = typedStringValue.getValue();
if (stringValue != null) {
String visitedString = resolveStringValue(stringValue);
value = new TypedStringValue(visitedString);
}
}
else {
value = super.resolveValue(value);
}
return value;
}
};
BeanDefinitionVisitor visitor = new PlaceholderBeanDefinitionVisitor(contextTypeConverter);
beanFactory.registerBeanDefinition(beanName, beanDefinition);
// Make the replacements before the target is hydrated
@@ -266,10 +247,9 @@ public class PlaceholderTargetSource extends SimpleBeanTargetSource implements I
/**
* @param value
* @param requiredType
* @param typeConverter
* @return
*/
private Object convertFromContext(String key, Class<?> requiredType, TypeConverter typeConverter) {
private Object convertFromContext(String key, Class<?> requiredType) {
Object result = null;
Object property = getPropertyFromContext(key);
if (property == null || requiredType.isAssignableFrom(property.getClass())) {
@@ -287,57 +267,141 @@ public class PlaceholderTargetSource extends SimpleBeanTargetSource implements I
}
private String extractKey(String value) {
if (value.startsWith(PLACEHOLDER_PREFIX)) {
value = value.substring(PLACEHOLDER_PREFIX.length());
value = value.substring(0, value.length() - PLACEHOLDER_SUFFIX.length());
}
return value;
return value.substring(value.indexOf(PLACEHOLDER_PREFIX) + PLACEHOLDER_PREFIX.length(), value
.indexOf(PLACEHOLDER_SUFFIX));
}
/**
* @param typeConverter
* @param strVal
* @return
* Determine whether the input is a whole key in the form
* <code>%{...}</code>, i.e. starting with the correct prefix, ending with
* the correct suffix and containing only one of each.
*
* @param value a String with placeholder patterns
* @return true if the value is a key
*/
private String replacePlaceholders(String value, TypeConverter typeConverter) {
private boolean isKey(String value) {
return value.indexOf(PLACEHOLDER_PREFIX) == value.lastIndexOf(PLACEHOLDER_PREFIX)
&& value.startsWith(PLACEHOLDER_PREFIX) && value.endsWith(PLACEHOLDER_SUFFIX);
}
StringBuilder result = new StringBuilder(value);
int first = result.indexOf(PLACEHOLDER_PREFIX);
int next = result.indexOf(PLACEHOLDER_SUFFIX, first + 1);
while (first >= 0) {
Assert.state(next > 0, String.format("Placeholder key incorrectly specified: use %skey%s (in %s)",
PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, value));
String key = result.substring(first + PLACEHOLDER_PREFIX.length(), next);
boolean replaced = replaceIfTypeMatches(result, first, next, key, String.class, typeConverter);
replaced |= replaceIfTypeMatches(result, first, next, key, Long.class, typeConverter);
replaced |= replaceIfTypeMatches(result, first, next, key, Integer.class, typeConverter);
replaced |= replaceIfTypeMatches(result, first, next, key, Date.class, typeConverter);
if (!replaced) {
logger.debug("Cannot bind to placeholder: " + key);
}
first = result.indexOf(PLACEHOLDER_PREFIX, first + 1);
next = result.indexOf(PLACEHOLDER_SUFFIX, first + 1);
/**
* A {@link BeanDefinitionVisitor} that will replace embedded placeholders
* with values from the provided context.
*
* @author Dave Syer
*
*/
private final class PlaceholderBeanDefinitionVisitor extends BeanDefinitionVisitor {
public PlaceholderBeanDefinitionVisitor(final TypeConverter typeConverter) {
super(new PlaceholderStringValueResolver(typeConverter));
}
logger.debug(String.format("Replaced [%s] with [%s]", value, result));
return result.toString();
protected Object resolveValue(Object value) {
if (value instanceof TypedStringValue) {
TypedStringValue typedStringValue = (TypedStringValue) value;
String stringValue = typedStringValue.getValue();
if (stringValue != null) {
// If the value is a whole key, try to simply replace it from context.
if (isKey(stringValue)) {
Object result = getPropertyFromContext(extractKey(stringValue));
if (result != null) {
value = result;
}
}
else {
// Otherwise it might contain embedded keys so we try to replace those
String visitedString = resolveStringValue(stringValue);
typedStringValue.setValue(visitedString);
}
}
}
else {
value = super.resolveValue(value);
}
return value;
}
}
private boolean replaceIfTypeMatches(StringBuilder result, int first, int next, String key, Class<?> requiredType,
TypeConverter typeConverter) {
Object property = convertFromContext(key, requiredType, typeConverter);
if (property != null) {
result.replace(first, next + 1, (String) typeConverter.convertIfNecessary(property, String.class));
return true;
private final class PlaceholderStringValueResolver implements StringValueResolver {
private final TypeConverter typeConverter;
private PlaceholderStringValueResolver(TypeConverter typeConverter) {
this.typeConverter = typeConverter;
}
return false;
public String resolveStringValue(String strVal) {
if (!strVal.contains(PLACEHOLDER_PREFIX)) {
return strVal;
}
return replacePlaceholders(strVal, typeConverter);
}
/**
* Convenience method to replace all the placeholders in the input.
*
* @param typeConverter a {@link TypeConverter} that can be used to
* convert placeholder keys to context values
* @param value the value to replace placeholders in
* @return the input with placeholders replaced
*/
private String replacePlaceholders(String value, TypeConverter typeConverter) {
StringBuilder result = new StringBuilder(value);
int first = result.indexOf(PLACEHOLDER_PREFIX);
int next = result.indexOf(PLACEHOLDER_SUFFIX, first + 1);
while (first >= 0) {
Assert.state(next > 0, String.format("Placeholder key incorrectly specified: use %skey%s (in %s)",
PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, value));
String key = result.substring(first + PLACEHOLDER_PREFIX.length(), next);
boolean replaced = replaceIfTypeMatches(result, first, next, key, String.class, typeConverter);
replaced |= replaceIfTypeMatches(result, first, next, key, Long.class, typeConverter);
replaced |= replaceIfTypeMatches(result, first, next, key, Integer.class, typeConverter);
replaced |= replaceIfTypeMatches(result, first, next, key, Date.class, typeConverter);
if (!replaced) {
if (!value.startsWith(PLACEHOLDER_PREFIX) || !value.endsWith(PLACEHOLDER_SUFFIX)) {
throw new IllegalStateException(String.format("Cannot bind to partial key %%{%s} in %s", key,
value));
}
logger.debug(String.format("Deferring binding of placeholder: %%{%s}", key));
}
else {
logger.debug(String.format("Bound %%{%s} to obtain [%s]", key, result));
}
first = result.indexOf(PLACEHOLDER_PREFIX, first + 1);
next = result.indexOf(PLACEHOLDER_SUFFIX, first + 1);
}
return result.toString();
}
private boolean replaceIfTypeMatches(StringBuilder result, int first, int next, String key,
Class<?> requiredType, TypeConverter typeConverter) {
Object property = convertFromContext(key, requiredType);
if (property != null) {
result.replace(first, next + 1, (String) typeConverter.convertIfNecessary(property, String.class));
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,61 @@
package org.springframework.batch.core.scope.util;
import static org.junit.Assert.assertEquals;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class PlaceholderTargetSourceCustomEditorTests extends ContextFactorySupport {
@Autowired
@Qualifier("withEmbeddedDate")
private PlaceholderTargetSource withEmbeddedDate;
private Date date = new Date(1L);
public Object getContext() {
return this;
}
public Date getDate() {
return date;
}
@Test
public void testGetEmbeddedDate() {
Node target = (Node) withEmbeddedDate.getTarget();
String date = new SimpleDateFormat("yyyy/MM/dd").format(new Date(1L));
assertEquals("bar-"+date, target.getName());
}
public static interface Node {
String getName();
}
public static class Foo implements Node {
private String name;
public Foo() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@@ -10,6 +10,7 @@ import java.util.Map;
import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.ByteArrayResource;
@@ -74,8 +75,15 @@ public class PlaceholderTargetSourceErrorTests extends ContextFactorySupport {
@Test
public void testPartialReplaceMissingProperty() throws Exception {
Node target = (Node) createValue("name", "%{garbage}-bar").getTarget();
assertEquals("%{garbage}-bar", target.getName());
try {
Node target = (Node) createValue("name", "%{garbage}-bar").getTarget();
assertEquals("%{garbage}-bar", target.getName());
fail("Expected IllegalStateException");
}
catch (IllegalStateException e) {
String message = e.getMessage();
assertTrue("Wrong message: " + message, message.toLowerCase().contains("cannot bind"));
}
}
@Test
@@ -91,7 +99,7 @@ public class PlaceholderTargetSourceErrorTests extends ContextFactorySupport {
assertEquals("bar", target.getName());
fail("Expected IllegalStateException");
}
catch (Exception e) {
catch (BeanCreationException e) {
String message = e.getMessage();
assertTrue("Wrong message: " + message, message.toLowerCase().contains("cannot bind"));
}
@@ -99,8 +107,8 @@ public class PlaceholderTargetSourceErrorTests extends ContextFactorySupport {
@Test
public void testPartialReplaceIntegerToString() throws Exception {
Node target = (Node) createValue("name", "foo-%{integer}").getTarget();
assertEquals("foo-4321", target.getName());
Node target = (Node) createValue("name", "foo-%{integer}").getTarget();
assertEquals("foo-4321", target.getName());
}
@Test

View File

@@ -3,7 +3,7 @@ package org.springframework.batch.core.scope.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@@ -40,6 +40,14 @@ public class PlaceholderTargetSourceTests extends ContextFactorySupport {
@Qualifier("withList")
private PlaceholderTargetSource withList;
@Autowired
@Qualifier("withLiteralList")
private PlaceholderTargetSource withLiteralList;
@Autowired
@Qualifier("withMap")
private PlaceholderTargetSource withMap;
@Autowired
@Qualifier("withMultiple")
private PlaceholderTargetSource withMultiple;
@@ -84,6 +92,10 @@ public class PlaceholderTargetSourceTests extends ContextFactorySupport {
return map;
}
public List<String> getList() {
return Arrays.asList("bar", "spam");
}
public Node getParent() {
return new Foo("spam");
}
@@ -165,6 +177,20 @@ public class PlaceholderTargetSourceTests extends ContextFactorySupport {
assertEquals("[bar, foo-4321, bar-4321]", target.getList().toString());
}
@Test
public void testGetLiteralList() {
Node target = (Node) withLiteralList.getTarget();
assertEquals(2, target.getList().size());
assertEquals("[bar, spam]", target.getList().toString());
}
@Test
public void testGetMap() {
Node target = (Node) withMap.getTarget();
assertEquals(3, target.getMap().size());
assertEquals("{foo=bar, bar=foo-4321, spam=[bar, spam]}", target.getMap().toString());
}
@Test
public void testGetMultiple() {
Node target = (Node) withMultiple.getTarget();
@@ -180,16 +206,14 @@ public class PlaceholderTargetSourceTests extends ContextFactorySupport {
@Test
public void testGetEmbeddedDate() {
Node target = (Node) withEmbeddedDate.getTarget();
String date = new SimpleDateFormat("yyyy/MM/dd").format(new Date(1L));
String date = new Date(1L).toString();
assertEquals("bar-"+date, target.getName());
}
@Test
public void testGetDate() {
Node target = (Node) withDate.getTarget();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
String date = sdf.format(new Date(1L));
assertEquals(date, sdf.format(target.getDate()));
assertEquals(new Date(1L), target.getDate());
}
public static interface Node {
@@ -200,6 +224,8 @@ public class PlaceholderTargetSourceTests extends ContextFactorySupport {
Node getParent();
List<String> getList();
Map<String, Object> getMap();
}
public static class Foo implements Node {
@@ -212,6 +238,8 @@ public class PlaceholderTargetSourceTests extends ContextFactorySupport {
private List<String> list;
private Map<String, Object> map;
public Foo() {
}
@@ -251,6 +279,14 @@ public class PlaceholderTargetSourceTests extends ContextFactorySupport {
return list;
}
public Map<String, Object> getMap() {
return map;
}
public void setMap(Map<String, Object> map) {
this.map = map;
}
}
}

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<bean id="context"
class="org.springframework.batch.core.scope.util.PlaceholderTargetSourceCustomEditorTests" />
<bean id="withEmbeddedDate"
class="org.springframework.batch.core.scope.util.PlaceholderTargetSource">
<property name="contextFactory" ref="context" />
<property name="targetBeanName" value="withEmbeddedDateTarget" />
</bean>
<bean id="withEmbeddedDateTarget"
class="org.springframework.batch.core.scope.util.PlaceholderTargetSourceCustomEditorTests$Foo"
lazy-init="true">
<property name="name" value="bar-%{date}" />
</bean>
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<bean
class="org.springframework.batch.support.DefaultPropertyEditorRegistrar">
<property name="customEditors">
<map>
<entry key="java.util.Date">
<bean
class="org.springframework.beans.propertyeditors.CustomDateEditor">
<constructor-arg>
<bean class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy/MM/dd" />
</bean>
</constructor-arg>
<constructor-arg value="false" />
</bean>
</entry>
</map>
</property>
</bean>
</property>
</bean>
</beans>

View File

@@ -58,6 +58,18 @@
<property name="targetBeanName" value="withListTarget" />
</bean>
<bean id="withLiteralList"
class="org.springframework.batch.core.scope.util.PlaceholderTargetSource">
<property name="contextFactory" ref="context" />
<property name="targetBeanName" value="withLiteralListTarget" />
</bean>
<bean id="withMap"
class="org.springframework.batch.core.scope.util.PlaceholderTargetSource">
<property name="contextFactory" ref="context" />
<property name="targetBeanName" value="withMapTarget" />
</bean>
<bean id="withMultiple"
class="org.springframework.batch.core.scope.util.PlaceholderTargetSource">
<property name="contextFactory" ref="context" />
@@ -135,6 +147,24 @@
</property>
</bean>
<bean id="withLiteralListTarget"
class="org.springframework.batch.core.scope.util.PlaceholderTargetSourceTests$Foo"
lazy-init="true">
<property name="list" value="%{list}"/>
</bean>
<bean id="withMapTarget"
class="org.springframework.batch.core.scope.util.PlaceholderTargetSourceTests$Foo"
lazy-init="true">
<property name="map">
<map value-type="java.lang.Object">
<entry key="foo" value="%{foo}"/>
<entry key="bar" value="foo-%{integer}"/>
<entry key="spam" value="%{list}"/>
</map>
</property>
</bean>
<bean id="withMultipleTarget"
class="org.springframework.batch.core.scope.util.PlaceholderTargetSourceTests$Foo"
lazy-init="true">
@@ -159,27 +189,4 @@
<property name="date" value="%{date}" />
</bean>
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<bean
class="org.springframework.batch.support.DefaultPropertyEditorRegistrar">
<property name="customEditors">
<map>
<entry key="java.util.Date">
<bean
class="org.springframework.beans.propertyeditors.CustomDateEditor">
<constructor-arg>
<bean class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy/MM/dd" />
</bean>
</constructor-arg>
<constructor-arg value="false" />
</bean>
</entry>
</map>
</property>
</bean>
</property>
</bean>
</beans>