EMMA Coverage Report (generated Thu May 22 12:08:10 CDT 2014)
[all classes][org.springframework.batch.item.file.mapping]

COVERAGE SUMMARY FOR SOURCE FILE [BeanWrapperFieldSetMapper.java]

nameclass, %method, %block, %line, %
BeanWrapperFieldSetMapper.java100% (2/2)100% (18/18)88%  (436/497)84%  (108.6/129)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class BeanWrapperFieldSetMapper$DistanceHolder100% (1/1)100% (3/3)78%  (62/79)70%  (16.9/24)
equals (Object): boolean 100% (1/1)66%  (29/44)53%  (8/15)
hashCode (): int 100% (1/1)92%  (24/26)98%  (4.9/5)
BeanWrapperFieldSetMapper$DistanceHolder (Class, int): void 100% (1/1)100% (9/9)100% (4/4)
     
class BeanWrapperFieldSetMapper100% (1/1)100% (15/15)89%  (374/418)87%  (91.7/105)
getBean (): Object 100% (1/1)52%  (13/25)33%  (3/9)
getPropertyValue (Object, String): Object 100% (1/1)79%  (26/33)64%  (7.7/12)
getBeanProperties (Object, Properties): Properties 100% (1/1)82%  (94/115)95%  (18/19)
findPropertyName (Object, String): String 100% (1/1)97%  (119/123)94%  (29/31)
BeanWrapperFieldSetMapper (): void 100% (1/1)100% (14/14)100% (5/5)
afterPropertiesSet (): void 100% (1/1)100% (23/23)100% (3/3)
createBinder (Object): DataBinder 100% (1/1)100% (21/21)100% (5/5)
initBinder (DataBinder): void 100% (1/1)100% (1/1)100% (1/1)
mapFieldSet (FieldSet): Object 100% (1/1)100% (29/29)100% (6/6)
setBeanFactory (BeanFactory): void 100% (1/1)100% (4/4)100% (2/2)
setDistanceLimit (int): void 100% (1/1)100% (4/4)100% (2/2)
setPrototypeBeanName (String): void 100% (1/1)100% (4/4)100% (2/2)
setStrict (boolean): void 100% (1/1)100% (4/4)100% (2/2)
setTargetType (Class): void 100% (1/1)100% (4/4)100% (2/2)
switchPropertyNames (Properties, String, String): void 100% (1/1)100% (14/14)100% (4/4)

1/*
2 * Copyright 2006-2007 the original author or authors.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 
17package org.springframework.batch.item.file.mapping;
18 
19import java.beans.PropertyEditor;
20import java.util.HashMap;
21import java.util.HashSet;
22import java.util.Map;
23import java.util.Properties;
24import java.util.Set;
25import java.util.concurrent.ConcurrentHashMap;
26import java.util.concurrent.ConcurrentMap;
27 
28import org.springframework.batch.item.file.transform.FieldSet;
29import org.springframework.batch.support.DefaultPropertyEditorRegistrar;
30import org.springframework.beans.BeanWrapperImpl;
31import org.springframework.beans.MutablePropertyValues;
32import org.springframework.beans.NotWritablePropertyException;
33import org.springframework.beans.PropertyAccessor;
34import org.springframework.beans.PropertyAccessorUtils;
35import org.springframework.beans.PropertyEditorRegistry;
36import org.springframework.beans.factory.BeanFactory;
37import org.springframework.beans.factory.BeanFactoryAware;
38import org.springframework.beans.factory.InitializingBean;
39import org.springframework.util.Assert;
40import org.springframework.util.ReflectionUtils;
41import org.springframework.validation.BindException;
42import org.springframework.validation.DataBinder;
43 
44/**
45 * {@link FieldSetMapper} implementation based on bean property paths. The
46 * {@link FieldSet} to be mapped should have field name meta data corresponding
47 * to bean property paths in an instance of the desired type. The instance is
48 * created and initialized either by referring to to a prototype object by bean
49 * name in the enclosing BeanFactory, or by providing a class to instantiate
50 * reflectively.<br/>
51 * <br/>
52 * 
53 * Nested property paths, including indexed properties in maps and collections,
54 * can be referenced by the {@link FieldSet} names. They will be converted to
55 * nested bean properties inside the prototype. The {@link FieldSet} and the
56 * prototype are thus tightly coupled by the fields that are available and those
57 * that can be initialized. If some of the nested properties are optional (e.g.
58 * collection members) they need to be removed by a post processor.<br/>
59 * <br/>
60 * 
61 * To customize the way that {@link FieldSet} values are converted to the
62 * desired type for injecting into the prototype there are several choices. You
63 * can inject {@link PropertyEditor} instances directly through the
64 * {@link #setCustomEditors(Map) customEditors} property, or you can override
65 * the {@link #createBinder(Object)} and {@link #initBinder(DataBinder)}
66 * methods, or you can provide a custom {@link FieldSet} implementation.<br/>
67 * <br/>
68 * 
69 * Property name matching is "fuzzy" in the sense that it tolerates close
70 * matches, as long as the match is unique. For instance:
71 * 
72 * <ul>
73 * <li>Quantity = quantity (field names can be capitalised)</li>
74 * <li>ISIN = isin (acronyms can be lower case bean property names, as per Java
75 * Beans recommendations)</li>
76 * <li>DuckPate = duckPate (capitalisation including camel casing)</li>
77 * <li>ITEM_ID = itemId (capitalisation and replacing word boundary with
78 * underscore)</li>
79 * <li>ORDER.CUSTOMER_ID = order.customerId (nested paths are recursively
80 * checked)</li>
81 * </ul>
82 * 
83 * The algorithm used to match a property name is to start with an exact match
84 * and then search successively through more distant matches until precisely one
85 * match is found. If more than one match is found there will be an error.
86 * 
87 * @author Dave Syer
88 * 
89 */
90public class BeanWrapperFieldSetMapper<T> extends DefaultPropertyEditorRegistrar implements FieldSetMapper<T>,
91                BeanFactoryAware, InitializingBean {
92 
93        private String name;
94 
95        private Class<? extends T> type;
96 
97        private BeanFactory beanFactory;
98 
99        private ConcurrentMap<DistanceHolder, ConcurrentMap<String, String>> propertiesMatched = new ConcurrentHashMap<DistanceHolder, ConcurrentMap<String, String>>();
100 
101        private int distanceLimit = 5;
102 
103        private boolean strict = true;
104 
105        /*
106         * (non-Javadoc)
107         * 
108         * @see
109         * org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org
110         * .springframework.beans.factory.BeanFactory)
111         */
112    @Override
113        public void setBeanFactory(BeanFactory beanFactory) {
114                this.beanFactory = beanFactory;
115        }
116 
117        /**
118         * The maximum difference that can be tolerated in spelling between input
119         * key names and bean property names. Defaults to 5, but could be set lower
120         * if the field names match the bean names.
121         * 
122         * @param distanceLimit the distance limit to set
123         */
124        public void setDistanceLimit(int distanceLimit) {
125                this.distanceLimit = distanceLimit;
126        }
127 
128        /**
129         * The bean name (id) for an object that can be populated from the field set
130         * that will be passed into {@link #mapFieldSet(FieldSet)}. Typically a
131         * prototype scoped bean so that a new instance is returned for each field
132         * set mapped.
133         * 
134         * Either this property or the type property must be specified, but not
135         * both.
136         * 
137         * @param name the name of a prototype bean in the enclosing BeanFactory
138         */
139        public void setPrototypeBeanName(String name) {
140                this.name = name;
141        }
142 
143        /**
144         * Public setter for the type of bean to create instead of using a prototype
145         * bean. An object of this type will be created from its default constructor
146         * for every call to {@link #mapFieldSet(FieldSet)}.<br/>
147         * 
148         * Either this property or the prototype bean name must be specified, but
149         * not both.
150         * 
151         * @param type the type to set
152         */
153        public void setTargetType(Class<? extends T> type) {
154                this.type = type;
155        }
156 
157        /**
158         * Check that precisely one of type or prototype bean name is specified.
159         * 
160         * @throws IllegalStateException if neither is set or both properties are
161         * set.
162         * 
163         * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
164         */
165    @Override
166        public void afterPropertiesSet() throws Exception {
167                Assert.state(name != null || type != null, "Either name or type must be provided.");
168                Assert.state(name == null || type == null, "Both name and type cannot be specified together.");
169        }
170 
171        /**
172         * Map the {@link FieldSet} to an object retrieved from the enclosing Spring
173         * context, or to a new instance of the required type if no prototype is
174         * available.
175         * @throws BindException if there is a type conversion or other error (if
176         * the {@link DataBinder} from {@link #createBinder(Object)} has errors
177         * after binding).
178         * 
179         * @throws NotWritablePropertyException if the {@link FieldSet} contains a
180         * field that cannot be mapped to a bean property.
181         * @see org.springframework.batch.item.file.mapping.FieldSetMapper#mapFieldSet(FieldSet)
182         */
183    @Override
184        public T mapFieldSet(FieldSet fs) throws BindException {
185                T copy = getBean();
186                DataBinder binder = createBinder(copy);
187                binder.bind(new MutablePropertyValues(getBeanProperties(copy, fs.getProperties())));
188                if (binder.getBindingResult().hasErrors()) {
189                        throw new BindException(binder.getBindingResult());
190                }
191                return copy;
192        }
193 
194        /**
195         * Create a binder for the target object. The binder will then be used to
196         * bind the properties form a field set into the target object. This
197         * implementation creates a new {@link DataBinder} and calls out to
198         * {@link #initBinder(DataBinder)} and
199         * {@link #registerCustomEditors(PropertyEditorRegistry)}.
200         * 
201         * @param target
202         * @return a {@link DataBinder} that can be used to bind properties to the
203         * target.
204         */
205        protected DataBinder createBinder(Object target) {
206                DataBinder binder = new DataBinder(target);
207                binder.setIgnoreUnknownFields(!this.strict);
208                initBinder(binder);
209                registerCustomEditors(binder);
210                return binder;
211        }
212 
213        /**
214         * Initialize a new binder instance. This hook allows customization of
215         * binder settings such as the {@link DataBinder#initDirectFieldAccess()
216         * direct field access}. Called by {@link #createBinder(Object)}.
217         * <p>
218         * Note that registration of custom property editors can be done in
219         * {@link #registerCustomEditors(PropertyEditorRegistry)}.
220         * </p>
221         * @param binder new binder instance
222         * @see #createBinder(Object)
223         */
224        protected void initBinder(DataBinder binder) {
225        }
226 
227        @SuppressWarnings("unchecked")
228        private T getBean() {
229                if (name != null) {
230                        return (T) beanFactory.getBean(name);
231                }
232                try {
233                        return type.newInstance();
234                }
235                catch (InstantiationException e) {
236                        ReflectionUtils.handleReflectionException(e);
237                }
238                catch (IllegalAccessException e) {
239                        ReflectionUtils.handleReflectionException(e);
240                }
241                // should not happen
242                throw new IllegalStateException("Internal error: could not create bean instance for mapping.");
243        }
244 
245        /**
246         * @param bean
247         * @param properties
248         * @return
249         */
250        @SuppressWarnings({ "unchecked", "rawtypes" })
251        private Properties getBeanProperties(Object bean, Properties properties) {
252 
253                Class<?> cls = bean.getClass();
254 
255                // Map from field names to property names
256                DistanceHolder distanceKey = new DistanceHolder(cls, distanceLimit);
257                if (!propertiesMatched.containsKey(distanceKey)) {
258                        propertiesMatched.putIfAbsent(distanceKey, new ConcurrentHashMap<String, String>());
259                }
260                Map<String, String> matches = new HashMap<String, String>(propertiesMatched.get(distanceKey));
261 
262                Set<String> keys = new HashSet(properties.keySet());
263                for (String key : keys) {
264 
265                        if (matches.containsKey(key)) {
266                                switchPropertyNames(properties, key, matches.get(key));
267                                continue;
268                        }
269 
270                        String name = findPropertyName(bean, key);
271 
272                        if (name != null) {
273                                if (matches.containsValue(name)) {
274                                        throw new NotWritablePropertyException(
275                                                        cls,
276                                                        name,
277                                                        "Duplicate match with distance <= "
278                                                                        + distanceLimit
279                                                                        + " found for this property in input keys: "
280                                                                        + keys
281                                                                        + ". (Consider reducing the distance limit or changing the input key names to get a closer match.)");
282                                }
283                                matches.put(key, name);
284                                switchPropertyNames(properties, key, name);
285                        }
286                }
287 
288                propertiesMatched.replace(distanceKey, new ConcurrentHashMap<String, String>(matches));
289                return properties;
290        }
291 
292        private String findPropertyName(Object bean, String key) {
293 
294                if (bean == null) {
295                        return null;
296                }
297 
298                Class<?> cls = bean.getClass();
299 
300                int index = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(key);
301                String prefix;
302                String suffix;
303 
304                // If the property name is nested recurse down through the properties
305                // looking for a match.
306                if (index > 0) {
307                        prefix = key.substring(0, index);
308                        suffix = key.substring(index + 1, key.length());
309                        String nestedName = findPropertyName(bean, prefix);
310                        if (nestedName == null) {
311                                return null;
312                        }
313 
314                        Object nestedValue = getPropertyValue(bean, nestedName);
315                        String nestedPropertyName = findPropertyName(nestedValue, suffix);
316                        return nestedPropertyName == null ? null : nestedName + "." + nestedPropertyName;
317                }
318 
319                String name = null;
320                int distance = 0;
321                index = key.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);
322 
323                if (index > 0) {
324                        prefix = key.substring(0, index);
325                        suffix = key.substring(index);
326                }
327                else {
328                        prefix = key;
329                        suffix = "";
330                }
331 
332                while (name == null && distance <= distanceLimit) {
333                        String[] candidates = PropertyMatches.forProperty(prefix, cls, distance).getPossibleMatches();
334                        // If we find precisely one match, then use that one...
335                        if (candidates.length == 1) {
336                                String candidate = candidates[0];
337                                if (candidate.equals(prefix)) { // if it's the same don't
338                                        // replace it...
339                                        name = key;
340                                }
341                                else {
342                                        name = candidate + suffix;
343                                }
344                        }
345                        distance++;
346                }
347                return name;
348        }
349 
350        private Object getPropertyValue(Object bean, String nestedName) {
351                BeanWrapperImpl wrapper = new BeanWrapperImpl(bean);
352                wrapper.setAutoGrowNestedPaths(true);
353 
354                Object nestedValue = wrapper.getPropertyValue(nestedName);
355                if (nestedValue == null) {
356                        try {
357                                nestedValue = wrapper.getPropertyType(nestedName).newInstance();
358                                wrapper.setPropertyValue(nestedName, nestedValue);
359                        }
360                        catch (InstantiationException e) {
361                                ReflectionUtils.handleReflectionException(e);
362                        }
363                        catch (IllegalAccessException e) {
364                                ReflectionUtils.handleReflectionException(e);
365                        }
366                }
367                return nestedValue;
368        }
369 
370        private void switchPropertyNames(Properties properties, String oldName, String newName) {
371                String value = properties.getProperty(oldName);
372                properties.remove(oldName);
373                properties.setProperty(newName, value);
374        }
375 
376        /**
377         * Public setter for the 'strict' property. If true, then
378         * {@link #mapFieldSet(FieldSet)} will fail of the FieldSet contains fields
379         * that cannot be mapped to the bean.
380         * 
381         * @param strict
382         */
383        public void setStrict(boolean strict) {
384                this.strict = strict;
385        }
386 
387        private static class DistanceHolder {
388                private final Class<?> cls;
389 
390                private final int distance;
391 
392                public DistanceHolder(Class<?> cls, int distance) {
393                        this.cls = cls;
394                        this.distance = distance;
395 
396                }
397 
398                @Override
399                public int hashCode() {
400                        final int prime = 31;
401                        int result = 1;
402                        result = prime * result + ((cls == null) ? 0 : cls.hashCode());
403                        result = prime * result + distance;
404                        return result;
405                }
406 
407                @Override
408                public boolean equals(Object obj) {
409                        if (this == obj)
410                                return true;
411                        if (obj == null)
412                                return false;
413                        if (getClass() != obj.getClass())
414                                return false;
415                        DistanceHolder other = (DistanceHolder) obj;
416                        if (cls == null) {
417                                if (other.cls != null)
418                                        return false;
419                        }
420                        else if (!cls.equals(other.cls))
421                                return false;
422                        if (distance != other.distance)
423                                return false;
424                        return true;
425                }
426        }
427 
428}

[all classes][org.springframework.batch.item.file.mapping]
EMMA 2.0.5312 (C) Vladimir Roubtsov