diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapper.java index 6a845778e..4c3504bdd 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapper.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapper.java @@ -94,9 +94,9 @@ public class BeanWrapperFieldSetMapper extends DefaultPropertyEditorRegistrar private BeanFactory beanFactory; - private static Map, Map> propertiesMatched = new HashMap, Map>(); + private static Map> propertiesMatched = new HashMap>(); - private static int distanceLimit = 5; + private int distanceLimit = 5; private boolean strict = true; @@ -111,6 +111,17 @@ public class BeanWrapperFieldSetMapper 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 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 matches = propertiesMatched.get(cls); + DistanceHolder distanceKey = new DistanceHolder(cls, distanceLimit); + Map matches = propertiesMatched.get(distanceKey); if (matches == null) { matches = new HashMap(); - propertiesMatched.put(cls, matches); + propertiesMatched.put(distanceKey, matches); } Set keys = new HashSet(properties.keySet()); @@ -254,6 +266,16 @@ public class BeanWrapperFieldSetMapper 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 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; + } + } + + } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/PropertyMatches.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/PropertyMatches.java index 40e484373..6004b136a 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/PropertyMatches.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/PropertyMatches.java @@ -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()]; } - } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapperFuzzyMatchingTest.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapperFuzzyMatchingTest.java new file mode 100644 index 000000000..4dbf70c76 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/BeanWrapperFieldSetMapperFuzzyMatchingTest.java @@ -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 mapper = new BeanWrapperFieldSetMapper(); + 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 mapper = new BeanWrapperFieldSetMapper(); + 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 mapper = new BeanWrapperFieldSetMapper(); + 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; + } + + } + +}