RESOLVED - BATCH-1643: add configurable distance limit to beanwrapper field set mapper
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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()];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user