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 | |
17 | package org.springframework.batch.item.file.mapping; |
18 | |
19 | import java.util.ArrayList; |
20 | import java.util.HashMap; |
21 | import java.util.HashSet; |
22 | import java.util.Iterator; |
23 | import java.util.List; |
24 | import java.util.Map; |
25 | import java.util.Properties; |
26 | import java.util.Set; |
27 | |
28 | import org.springframework.batch.support.DefaultPropertyEditorRegistrar; |
29 | import org.springframework.beans.BeanWrapperImpl; |
30 | import org.springframework.beans.MutablePropertyValues; |
31 | import org.springframework.beans.NotWritablePropertyException; |
32 | import org.springframework.beans.PropertyAccessor; |
33 | import org.springframework.beans.PropertyAccessorUtils; |
34 | import org.springframework.beans.PropertyEditorRegistry; |
35 | import org.springframework.beans.factory.BeanFactory; |
36 | import org.springframework.beans.factory.BeanFactoryAware; |
37 | import org.springframework.beans.factory.InitializingBean; |
38 | import org.springframework.util.Assert; |
39 | import org.springframework.util.ReflectionUtils; |
40 | import org.springframework.validation.DataBinder; |
41 | import 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 | */ |
79 | public 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 | } |