EMMA Coverage Report (generated Fri Jan 30 13:20:29 EST 2009)
[all classes][org.springframework.batch.item.file.mapping]

COVERAGE SUMMARY FOR SOURCE FILE [BeanWrapperFieldSetMapper.java]

nameclass, %method, %block, %line, %
BeanWrapperFieldSetMapper.java100% (1/1)100% (13/13)85%  (298/350)84%  (75/89)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class BeanWrapperFieldSetMapper100% (1/1)100% (13/13)85%  (298/350)84%  (75/89)
mapLine (FieldSet): Object 100% (1/1)38%  (23/61)42%  (5/12)
getBean (): Object 100% (1/1)52%  (13/25)33%  (3/9)
findPropertyName (Object, String): String 100% (1/1)98%  (118/120)97%  (28/29)
<static initializer> 100% (1/1)100% (7/7)100% (2/2)
BeanWrapperFieldSetMapper (): void 100% (1/1)100% (3/3)100% (1/1)
afterPropertiesSet (): void 100% (1/1)100% (23/23)100% (3/3)
createBinder (Object): DataBinder 100% (1/1)100% (16/16)100% (5/5)
getBeanProperties (Object, Properties): Properties 100% (1/1)100% (68/68)100% (17/17)
initBinder (DataBinder): void 100% (1/1)100% (1/1)100% (1/1)
setBeanFactory (BeanFactory): void 100% (1/1)100% (4/4)100% (2/2)
setPrototypeBeanName (String): 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.util.ArrayList;
20import java.util.HashMap;
21import java.util.HashSet;
22import java.util.Iterator;
23import java.util.List;
24import java.util.Map;
25import java.util.Properties;
26import java.util.Set;
27 
28import org.springframework.batch.support.DefaultPropertyEditorRegistrar;
29import org.springframework.beans.BeanWrapperImpl;
30import org.springframework.beans.MutablePropertyValues;
31import org.springframework.beans.NotWritablePropertyException;
32import org.springframework.beans.PropertyAccessor;
33import org.springframework.beans.PropertyAccessorUtils;
34import org.springframework.beans.PropertyEditorRegistry;
35import org.springframework.beans.factory.BeanFactory;
36import org.springframework.beans.factory.BeanFactoryAware;
37import org.springframework.beans.factory.InitializingBean;
38import org.springframework.util.Assert;
39import org.springframework.util.ReflectionUtils;
40import org.springframework.validation.DataBinder;
41import org.springframework.validation.ObjectError;
42 
43/**
44 * {@link FieldSetMapper} implementation based on bean property paths. The
45 * {@link DefaultFieldSet} to be mapped should have field name meta data corresponding
46 * to bean property paths in a prototype instance of the desired type. The
47 * prototype instance is initialized either by referring to to object by bean
48 * name in the enclosing BeanFactory, or by providing a class to instantiate
49 * reflectively.<br/>
50 * 
51 * Nested property paths, including indexed properties in maps and collections,
52 * can be referenced by the {@link DefaultFieldSet} names. They will be converted to
53 * nested bean properties inside the prototype. The {@link DefaultFieldSet} and the
54 * prototype are thus tightly coupled by the fields that are available and those
55 * that can be initialized. If some of the nested properties are optional (e.g.
56 * collection members) they need to be removed by a post processor.<br/>
57 * 
58 * Property name matching is "fuzzy" in the sense that it tolerates close
59 * matches, as long as the match is unique. For instance:
60 * 
61 * <ul>
62 * <li>Quantity = quantity (field names can be capitalised)</li>
63 * <li>ISIN = isin (acronyms can be lower case bean property names, as per Java
64 * Beans recommendations)</li>
65 * <li>DuckPate = duckPate (capitalisation including camel casing)</li>
66 * <li>ITEM_ID = itemId (capitalisation and replacing word boundary with
67 * underscore)</li>
68 * <li>ORDER.CUSTOMER_ID = order.customerId (nested paths are recursively
69 * checked)</li>
70 * </ul>
71 * 
72 * The algorithm used to match a property name is to start with an exact match
73 * and then search successively through more distant matches until precisely one
74 * match is found. If more than one match is found there will be an error.
75 * 
76 * @author Dave Syer
77 * 
78 */
79public class BeanWrapperFieldSetMapper extends DefaultPropertyEditorRegistrar implements FieldSetMapper, BeanFactoryAware, InitializingBean {
80 
81        private String name;
82 
83        private Class type;
84 
85        private BeanFactory beanFactory;
86 
87        private static Map propertiesMatched = new HashMap();
88 
89        private static int distanceLimit = 5;
90 
91        /*
92         * (non-Javadoc)
93         * 
94         * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
95         */
96        public void setBeanFactory(BeanFactory beanFactory) {
97                this.beanFactory = beanFactory;
98        }
99 
100        /**
101         * The bean name (id) for an object that can be populated from the field set
102         * that will be passed into {@link #mapLine(FieldSet)}. Typically a
103         * prototype scoped bean so that a new instance is returned for each field
104         * set mapped.
105         * 
106         * Either this property or the type property must be specified, but not
107         * both.
108         * 
109         * @param name the name of a prototype bean in the enclosing BeanFactory
110         */
111        public void setPrototypeBeanName(String name) {
112                this.name = name;
113        }
114 
115        /**
116         * Public setter for the type of bean to create instead of using a prototype
117         * bean. An object of this type will be created from its default constructor
118         * for every call to {@link #mapLine(FieldSet)}.<br/>
119         * 
120         * Either this property or the prototype bean name must be specified, but
121         * not both.
122         * 
123         * @param type the type to set
124         */
125        public void setTargetType(Class type) {
126                this.type = type;
127        }
128 
129        /**
130         * Check that precisely one of type or prototype bean name is specified.
131         * 
132         * @throws IllegalStateException if neither is set or both properties are
133         * set.
134         * 
135         * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
136         */
137        public void afterPropertiesSet() throws Exception {
138                Assert.state(name != null || type != null, "Either name or type must be provided.");
139                Assert.state(name == null || type == null, "Both name and type cannot be specified together.");
140        }
141 
142        /**
143         * Map the {@link DefaultFieldSet} to an object retrieved from the enclosing Spring
144         * context, or to a new instance of the required type if no prototype is
145         * available.
146         * 
147         * @throws NotWritablePropertyException if the {@link DefaultFieldSet} contains a
148         * field that cannot be mapped to a bean property.
149         * @throws BindingException if there is a type conversion or other error (if
150         * the {@link DataBinder} from {@link #createBinder(Object)} has errors
151         * after binding).
152         * 
153         * @see org.springframework.batch.item.file.mapping.FieldSetMapper#mapLine(org.springframework.batch.item.file.mapping.FieldSet)
154         */
155        public Object mapLine(FieldSet fs) {
156                Object copy = getBean();
157                DataBinder binder = createBinder(copy);
158                binder.bind(new MutablePropertyValues(getBeanProperties(copy, fs.getProperties())));
159                if (binder.getBindingResult().hasErrors()) {
160                        List errors = binder.getBindingResult().getAllErrors();
161                        List messages = new ArrayList(errors.size());
162                        for (Iterator iterator = errors.iterator(); iterator.hasNext();) {
163                                ObjectError error = (ObjectError) iterator.next();
164                                messages.add(error.getDefaultMessage());
165                        }
166                        throw new BindingException("" + messages);
167                }
168                return copy;
169        }
170 
171        /**
172         * Create a binder for the target object. The binder will then be used to
173         * bind the properties form a field set into the target object. This
174         * implementation creates a new {@link DataBinder} and calls out to
175         * {@link #initBinder(DataBinder)} and
176         * {@link #registerCustomEditors(PropertyEditorRegistry)}.
177         * 
178         * @param target
179         * @return a {@link DataBinder} that can be used to bind properties to the
180         * target.
181         */
182        protected DataBinder createBinder(Object target) {
183                DataBinder binder = new DataBinder(target);
184                binder.setIgnoreUnknownFields(false);
185                initBinder(binder);
186                registerCustomEditors(binder);
187                return binder;
188        }
189 
190        /**
191         * Initialize a new binder instance. This hook allows customization of
192         * binder settings such as the
193         * {@link DataBinder#initDirectFieldAccess() direct field access}. Called
194         * by {@link #createBinder(Object)}.
195         * <p>
196         * Note that registration of custom property editors should be done in
197         * {@link #registerCustomEditors(PropertyEditorRegistry)}, not here! This
198         * method will only be called when a <b>new</b> data binder is created.
199         * @param binder new binder instance
200         * @see #createBinder(Object)
201         */
202        protected void initBinder(DataBinder binder) {
203        }
204 
205        private Object getBean() {
206                if (name != null) {
207                        return beanFactory.getBean(name);
208                }
209                try {
210                        return type.newInstance();
211                }
212                catch (InstantiationException e) {
213                        ReflectionUtils.handleReflectionException(e);
214                }
215                catch (IllegalAccessException e) {
216                        ReflectionUtils.handleReflectionException(e);
217                }
218                // should not happen
219                throw new IllegalStateException("Internal error: could not create bean instance for mapping.");
220        }
221 
222        /**
223         * @param bean
224         * @param properties
225         * @return
226         */
227        private Properties getBeanProperties(Object bean, Properties properties) {
228 
229                Class cls = bean.getClass();
230 
231                // Map from field names to property names
232                Map matches = (Map) propertiesMatched.get(cls);
233                if (matches == null) {
234                        matches = new HashMap();
235                        propertiesMatched.put(cls, matches);
236                }
237 
238                Set keys = new HashSet(properties.keySet());
239                for (Iterator iter = keys.iterator(); iter.hasNext();) {
240                        String key = (String) iter.next();
241 
242                        if (matches.containsKey(key)) {
243                                switchPropertyNames(properties, key, (String) matches.get(key));
244                                continue;
245                        }
246 
247                        String name = findPropertyName(bean, key);
248 
249                        if (name != null) {
250                                matches.put(key, name);
251                                switchPropertyNames(properties, key, name);
252                        }
253                }
254 
255                return properties;
256        }
257 
258        private String findPropertyName(Object bean, String key) {
259 
260                Class cls = bean.getClass();
261 
262                int index = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(key);
263                String prefix;
264                String suffix;
265 
266                // If the property name is nested recurse down through the properties
267                // looking for a match.
268                if (index > 0) {
269                        prefix = key.substring(0, index);
270                        suffix = key.substring(index + 1, key.length());
271                        String nestedName = findPropertyName(bean, prefix);
272                        if (nestedName == null) {
273                                return null;
274                        }
275 
276                        Object nestedValue = new BeanWrapperImpl(bean).getPropertyValue(nestedName);
277                        String nestedPropertyName = findPropertyName(nestedValue, suffix);
278                        return nestedPropertyName == null ? null : nestedName + "." + nestedPropertyName;
279                }
280 
281                String name = null;
282                int distance = 0;
283                index = key.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);
284 
285                if (index > 0) {
286                        prefix = key.substring(0, index);
287                        suffix = key.substring(index);
288                }
289                else {
290                        prefix = key;
291                        suffix = "";
292                }
293 
294                while (name == null && distance <= distanceLimit) {
295                        String[] candidates = PropertyMatches.forProperty(prefix, cls, distance).getPossibleMatches();
296                        // If we find precisely one match, then use that one...
297                        if (candidates.length == 1) {
298                                String candidate = candidates[0];
299                                if (candidate.equals(prefix)) { // if it's the same don't
300                                        // replace it...
301                                        name = key;
302                                }
303                                else {
304                                        name = candidate + suffix;
305                                }
306                        }
307                        distance++;
308                }
309                return name;
310        }
311 
312        private void switchPropertyNames(Properties properties, String oldName, String newName) {
313                String value = properties.getProperty(oldName);
314                properties.remove(oldName);
315                properties.setProperty(newName, value);
316        }
317 
318}

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