RESOLVED - BATCH-1643: add configurable distance limit to beanwrapper field set mapper

This commit is contained in:
dsyer
2010-10-30 17:57:25 +00:00
parent a9cbc02e0b
commit f1667b7d8f
3 changed files with 173 additions and 7 deletions

View File

@@ -94,9 +94,9 @@ public class BeanWrapperFieldSetMapper<T> extends DefaultPropertyEditorRegistrar
private BeanFactory beanFactory;
private static Map<Class<?>, Map<String, String>> propertiesMatched = new HashMap<Class<?>, Map<String, String>>();
private static Map<DistanceHolder, Map<String, String>> propertiesMatched = new HashMap<DistanceHolder, Map<String, String>>();
private static int distanceLimit = 5;
private int distanceLimit = 5;
private boolean strict = true;
@@ -111,6 +111,17 @@ public class BeanWrapperFieldSetMapper<T> extends DefaultPropertyEditorRegistrar
this.beanFactory = beanFactory;
}
/**
* The maximum difference that can be tolerated in spelling between input
* key names and bean property names. Defaults to 5, but could be set lower
* if the field names match the bean names.
*
* @param distanceLimit the distance limit to set
*/
public void setDistanceLimit(int distanceLimit) {
this.distanceLimit = distanceLimit;
}
/**
* The bean name (id) for an object that can be populated from the field set
* that will be passed into {@link #mapFieldSet(FieldSet)}. Typically a
@@ -231,16 +242,17 @@ public class BeanWrapperFieldSetMapper<T> extends DefaultPropertyEditorRegistrar
* @param properties
* @return
*/
@SuppressWarnings("unchecked")
@SuppressWarnings({ "unchecked", "rawtypes" })
private Properties getBeanProperties(Object bean, Properties properties) {
Class<?> cls = bean.getClass();
// Map from field names to property names
Map<String, String> matches = propertiesMatched.get(cls);
DistanceHolder distanceKey = new DistanceHolder(cls, distanceLimit);
Map<String, String> matches = propertiesMatched.get(distanceKey);
if (matches == null) {
matches = new HashMap<String, String>();
propertiesMatched.put(cls, matches);
propertiesMatched.put(distanceKey, matches);
}
Set<String> keys = new HashSet(properties.keySet());
@@ -254,6 +266,16 @@ public class BeanWrapperFieldSetMapper<T> extends DefaultPropertyEditorRegistrar
String name = findPropertyName(bean, key);
if (name != null) {
if (matches.containsValue(name)) {
throw new NotWritablePropertyException(
cls,
name,
"Duplicate match with distance <= "
+ distanceLimit
+ " found for this property in input keys: "
+ keys
+ ". (Consider reducing the distance limit or changing the input key names to get a closer match.)");
}
matches.put(key, name);
switchPropertyNames(properties, key, name);
}
@@ -354,4 +376,46 @@ public class BeanWrapperFieldSetMapper<T> extends DefaultPropertyEditorRegistrar
public void setStrict(boolean strict) {
this.strict = strict;
}
private static class DistanceHolder {
private final Class<?> cls;
private final int distance;
public DistanceHolder(Class<?> cls, int distance) {
this.cls = cls;
this.distance = distance;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((cls == null) ? 0 : cls.hashCode());
result = prime * result + distance;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
DistanceHolder other = (DistanceHolder) obj;
if (cls == null) {
if (other.cls != null)
return false;
}
else if (!cls.equals(other.cls))
return false;
if (distance != other.distance)
return false;
return true;
}
}
}

View File

@@ -138,7 +138,8 @@ final class PropertyMatches {
for (int i = 0; i < propertyDescriptors.length; i++) {
if (propertyDescriptors[i].getWriteMethod() != null) {
String possibleAlternative = propertyDescriptors[i].getName();
if (calculateStringDistance(this.propertyName, possibleAlternative) <= maxDistance) {
int distance = calculateStringDistance(this.propertyName, possibleAlternative);
if (distance <= maxDistance) {
candidates.add(possibleAlternative);
}
}
@@ -187,5 +188,4 @@ final class PropertyMatches {
return d[s1.length()][s2.length()];
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2006-2007 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.batch.item.file.mapping;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.beans.NotWritablePropertyException;
import org.springframework.validation.BindException;
public class BeanWrapperFieldSetMapperFuzzyMatchingTest {
@Test(expected = NotWritablePropertyException.class)
public void testFuzzyMatchingWithKeyCandidateCollision() throws BindException {
BeanWrapperFieldSetMapper<GreenBean> mapper = new BeanWrapperFieldSetMapper<GreenBean>();
mapper.setStrict(true);
mapper.setTargetType(GreenBean.class);
DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
String[] names = { "brown", "green", "great", "groin", "braun" };
lineTokenizer.setNames(names);
GreenBean bean = mapper.mapFieldSet(lineTokenizer.tokenize("brown,green,great,groin,braun"));
Assert.assertEquals("green", bean.getGreen());
}
@Test
public void testFuzzyMatchingWithLowerLimit() throws BindException {
BeanWrapperFieldSetMapper<GreenBean> mapper = new BeanWrapperFieldSetMapper<GreenBean>();
mapper.setDistanceLimit(0);
mapper.setStrict(false);
mapper.setTargetType(GreenBean.class);
DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
String[] names = { "brown", "green", "great", "groin", "braun" };
lineTokenizer.setNames(names);
GreenBean bean = mapper.mapFieldSet(lineTokenizer.tokenize("brown,green,great,groin,braun"));
Assert.assertEquals("green", bean.getGreen());
}
@Test
public void testFuzzyMatchingWithPropertyCollision() throws BindException {
BeanWrapperFieldSetMapper<BlueBean> mapper = new BeanWrapperFieldSetMapper<BlueBean>();
mapper.setStrict(true);
mapper.setTargetType(BlueBean.class);
DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
String[] names = { "blue" };
lineTokenizer.setNames(names);
BlueBean bean = mapper.mapFieldSet(lineTokenizer.tokenize("blue"));
// An exact match always wins...
Assert.assertEquals("blue", bean.getBlue());
Assert.assertEquals(null, bean.getBleu());
}
public static class GreenBean {
private String green;
public String getGreen() {
return green;
}
public void setGreen(String green) {
this.green = green;
}
}
public static class BlueBean {
private String blue;
private String bleu;
public String getBleu() {
return bleu;
}
public void setBleu(String bleu) {
this.bleu = bleu;
}
public String getBlue() {
return blue;
}
public void setBlue(String blue) {
this.blue = blue;
}
}
}