EMMA Coverage Report (generated Thu Jan 24 13:37:04 CST 2013)
[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%  (435/494)85%  (108.6/128)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class BeanWrapperFieldSetMapper$DistanceHolder100% (1/1)100% (3/3)81%  (64/79)75%  (17.9/24)
equals (Object): boolean 100% (1/1)70%  (31/44)60%  (9/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%  (371/415)87%  (90.7/104)
getBean (): Object 100% (1/1)52%  (13/25)33%  (3/9)
getPropertyValue (Object, String): Object 100% (1/1)77%  (23/30)61%  (6.7/11)
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        public void setBeanFactory(BeanFactory beanFactory) {
113                this.beanFactory = beanFactory;
114        }
115 
116        /**
117         * The maximum difference that can be tolerated in spelling between input
118         * key names and bean property names. Defaults to 5, but could be set lower
119         * if the field names match the bean names.
120         * 
121         * @param distanceLimit the distance limit to set
122         */
123        public void setDistanceLimit(int distanceLimit) {
124                this.distanceLimit = distanceLimit;
125        }
126 
127        /**
128         * The bean name (id) for an object that can be populated from the field set
129         * that will be passed into {@link #mapFieldSet(FieldSet)}. Typically a
130         * prototype scoped bean so that a new instance is returned for each field
131         * set mapped.
132         * 
133         * Either this property or the type property must be specified, but not
134         * both.
135         * 
136         * @param name the name of a prototype bean in the enclosing BeanFactory
137         */
138        public void setPrototypeBeanName(String name) {
139                this.name = name;
140        }
141 
142        /**
143         * Public setter for the type of bean to create instead of using a prototype
144         * bean. An object of this type will be created from its default constructor
145         * for every call to {@link #mapFieldSet(FieldSet)}.<br/>
146         * 
147         * Either this property or the prototype bean name must be specified, but
148         * not both.
149         * 
150         * @param type the type to set
151         */
152        public void setTargetType(Class<? extends T> type) {
153                this.type = type;
154        }
155 
156        /**
157         * Check that precisely one of type or prototype bean name is specified.
158         * 
159         * @throws IllegalStateException if neither is set or both properties are
160         * set.
161         * 
162         * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
163         */
164        public void afterPropertiesSet() throws Exception {
165                Assert.state(name != null || type != null, "Either name or type must be provided.");
166                Assert.state(name == null || type == null, "Both name and type cannot be specified together.");
167        }
168 
169        /**
170         * Map the {@link FieldSet} to an object retrieved from the enclosing Spring
171         * context, or to a new instance of the required type if no prototype is
172         * available.
173         * @throws BindException if there is a type conversion or other error (if
174         * the {@link DataBinder} from {@link #createBinder(Object)} has errors
175         * after binding).
176         * 
177         * @throws NotWritablePropertyException if the {@link FieldSet} contains a
178         * field that cannot be mapped to a bean property.
179         * @see org.springframework.batch.item.file.mapping.FieldSetMapper#mapFieldSet(FieldSet)
180         */
181        public T mapFieldSet(FieldSet fs) throws BindException {
182                T copy = getBean();
183                DataBinder binder = createBinder(copy);
184                binder.bind(new MutablePropertyValues(getBeanProperties(copy, fs.getProperties())));
185                if (binder.getBindingResult().hasErrors()) {
186                        throw new BindException(binder.getBindingResult());
187                }
188                return copy;
189        }
190 
191        /**
192         * Create a binder for the target object. The binder will then be used to
193         * bind the properties form a field set into the target object. This
194         * implementation creates a new {@link DataBinder} and calls out to
195         * {@link #initBinder(DataBinder)} and
196         * {@link #registerCustomEditors(PropertyEditorRegistry)}.
197         * 
198         * @param target
199         * @return a {@link DataBinder} that can be used to bind properties to the
200         * target.
201         */
202        protected DataBinder createBinder(Object target) {
203                DataBinder binder = new DataBinder(target);
204                binder.setIgnoreUnknownFields(!this.strict);
205                initBinder(binder);
206                registerCustomEditors(binder);
207                return binder;
208        }
209 
210        /**
211         * Initialize a new binder instance. This hook allows customization of
212         * binder settings such as the {@link DataBinder#initDirectFieldAccess()
213         * direct field access}. Called by {@link #createBinder(Object)}.
214         * <p>
215         * Note that registration of custom property editors can be done in
216         * {@link #registerCustomEditors(PropertyEditorRegistry)}.
217         * </p>
218         * @param binder new binder instance
219         * @see #createBinder(Object)
220         */
221        protected void initBinder(DataBinder binder) {
222        }
223 
224        @SuppressWarnings("unchecked")
225        private T getBean() {
226                if (name != null) {
227                        return (T) beanFactory.getBean(name);
228                }
229                try {
230                        return type.newInstance();
231                }
232                catch (InstantiationException e) {
233                        ReflectionUtils.handleReflectionException(e);
234                }
235                catch (IllegalAccessException e) {
236                        ReflectionUtils.handleReflectionException(e);
237                }
238                // should not happen
239                throw new IllegalStateException("Internal error: could not create bean instance for mapping.");
240        }
241 
242        /**
243         * @param bean
244         * @param properties
245         * @return
246         */
247        @SuppressWarnings({ "unchecked", "rawtypes" })
248        private Properties getBeanProperties(Object bean, Properties properties) {
249 
250                Class<?> cls = bean.getClass();
251 
252                // Map from field names to property names
253                DistanceHolder distanceKey = new DistanceHolder(cls, distanceLimit);
254                if (!propertiesMatched.containsKey(distanceKey)) {
255                        propertiesMatched.putIfAbsent(distanceKey, new ConcurrentHashMap<String, String>());
256                }
257                Map<String, String> matches = new HashMap<String, String>(propertiesMatched.get(distanceKey));
258 
259                Set<String> keys = new HashSet(properties.keySet());
260                for (String key : keys) {
261 
262                        if (matches.containsKey(key)) {
263                                switchPropertyNames(properties, key, matches.get(key));
264                                continue;
265                        }
266 
267                        String name = findPropertyName(bean, key);
268 
269                        if (name != null) {
270                                if (matches.containsValue(name)) {
271                                        throw new NotWritablePropertyException(
272                                                        cls,
273                                                        name,
274                                                        "Duplicate match with distance <= "
275                                                                        + distanceLimit
276                                                                        + " found for this property in input keys: "
277                                                                        + keys
278                                                                        + ". (Consider reducing the distance limit or changing the input key names to get a closer match.)");
279                                }
280                                matches.put(key, name);
281                                switchPropertyNames(properties, key, name);
282                        }
283                }
284 
285                propertiesMatched.replace(distanceKey, new ConcurrentHashMap<String, String>(matches));
286                return properties;
287        }
288 
289        private String findPropertyName(Object bean, String key) {
290 
291                if (bean == null) {
292                        return null;
293                }
294 
295                Class<?> cls = bean.getClass();
296 
297                int index = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(key);
298                String prefix;
299                String suffix;
300 
301                // If the property name is nested recurse down through the properties
302                // looking for a match.
303                if (index > 0) {
304                        prefix = key.substring(0, index);
305                        suffix = key.substring(index + 1, key.length());
306                        String nestedName = findPropertyName(bean, prefix);
307                        if (nestedName == null) {
308                                return null;
309                        }
310 
311                        Object nestedValue = getPropertyValue(bean, nestedName);
312                        String nestedPropertyName = findPropertyName(nestedValue, suffix);
313                        return nestedPropertyName == null ? null : nestedName + "." + nestedPropertyName;
314                }
315 
316                String name = null;
317                int distance = 0;
318                index = key.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);
319 
320                if (index > 0) {
321                        prefix = key.substring(0, index);
322                        suffix = key.substring(index);
323                }
324                else {
325                        prefix = key;
326                        suffix = "";
327                }
328 
329                while (name == null && distance <= distanceLimit) {
330                        String[] candidates = PropertyMatches.forProperty(prefix, cls, distance).getPossibleMatches();
331                        // If we find precisely one match, then use that one...
332                        if (candidates.length == 1) {
333                                String candidate = candidates[0];
334                                if (candidate.equals(prefix)) { // if it's the same don't
335                                        // replace it...
336                                        name = key;
337                                }
338                                else {
339                                        name = candidate + suffix;
340                                }
341                        }
342                        distance++;
343                }
344                return name;
345        }
346 
347        private Object getPropertyValue(Object bean, String nestedName) {
348                BeanWrapperImpl wrapper = new BeanWrapperImpl(bean);
349                Object nestedValue = wrapper.getPropertyValue(nestedName);
350                if (nestedValue == null) {
351                        try {
352                                nestedValue = wrapper.getPropertyType(nestedName).newInstance();
353                                wrapper.setPropertyValue(nestedName, nestedValue);
354                        }
355                        catch (InstantiationException e) {
356                                ReflectionUtils.handleReflectionException(e);
357                        }
358                        catch (IllegalAccessException e) {
359                                ReflectionUtils.handleReflectionException(e);
360                        }
361                }
362                return nestedValue;
363        }
364 
365        private void switchPropertyNames(Properties properties, String oldName, String newName) {
366                String value = properties.getProperty(oldName);
367                properties.remove(oldName);
368                properties.setProperty(newName, value);
369        }
370 
371        /**
372         * Public setter for the 'strict' property. If true, then
373         * {@link #mapFieldSet(FieldSet)} will fail of the FieldSet contains fields
374         * that cannot be mapped to the bean.
375         * 
376         * @param strict
377         */
378        public void setStrict(boolean strict) {
379                this.strict = strict;
380        }
381 
382        private static class DistanceHolder {
383                private final Class<?> cls;
384 
385                private final int distance;
386 
387                public DistanceHolder(Class<?> cls, int distance) {
388                        this.cls = cls;
389                        this.distance = distance;
390 
391                }
392 
393                @Override
394                public int hashCode() {
395                        final int prime = 31;
396                        int result = 1;
397                        result = prime * result + ((cls == null) ? 0 : cls.hashCode());
398                        result = prime * result + distance;
399                        return result;
400                }
401 
402                @Override
403                public boolean equals(Object obj) {
404                        if (this == obj)
405                                return true;
406                        if (obj == null)
407                                return false;
408                        if (getClass() != obj.getClass())
409                                return false;
410                        DistanceHolder other = (DistanceHolder) obj;
411                        if (cls == null) {
412                                if (other.cls != null)
413                                        return false;
414                        }
415                        else if (!cls.equals(other.cls))
416                                return false;
417                        if (distance != other.distance)
418                                return false;
419                        return true;
420                }
421        }
422 
423}

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