1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.springframework.batch.item.file.mapping;
18
19 import java.beans.PropertyEditor;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.Map;
23 import java.util.Properties;
24 import java.util.Set;
25 import java.util.concurrent.ConcurrentHashMap;
26 import java.util.concurrent.ConcurrentMap;
27
28 import org.springframework.batch.item.file.transform.FieldSet;
29 import org.springframework.batch.support.DefaultPropertyEditorRegistrar;
30 import org.springframework.beans.BeanWrapperImpl;
31 import org.springframework.beans.MutablePropertyValues;
32 import org.springframework.beans.NotWritablePropertyException;
33 import org.springframework.beans.PropertyAccessor;
34 import org.springframework.beans.PropertyAccessorUtils;
35 import org.springframework.beans.PropertyEditorRegistry;
36 import org.springframework.beans.factory.BeanFactory;
37 import org.springframework.beans.factory.BeanFactoryAware;
38 import org.springframework.beans.factory.InitializingBean;
39 import org.springframework.util.Assert;
40 import org.springframework.util.ReflectionUtils;
41 import org.springframework.validation.BindException;
42 import org.springframework.validation.DataBinder;
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90 public 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
107
108
109
110
111
112 @Override
113 public void setBeanFactory(BeanFactory beanFactory) {
114 this.beanFactory = beanFactory;
115 }
116
117
118
119
120
121
122
123
124 public void setDistanceLimit(int distanceLimit) {
125 this.distanceLimit = distanceLimit;
126 }
127
128
129
130
131
132
133
134
135
136
137
138
139 public void setPrototypeBeanName(String name) {
140 this.name = name;
141 }
142
143
144
145
146
147
148
149
150
151
152
153 public void setTargetType(Class<? extends T> type) {
154 this.type = type;
155 }
156
157
158
159
160
161
162
163
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
173
174
175
176
177
178
179
180
181
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
196
197
198
199
200
201
202
203
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
215
216
217
218
219
220
221
222
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
242 throw new IllegalStateException("Internal error: could not create bean instance for mapping.");
243 }
244
245
246
247
248
249
250 @SuppressWarnings({ "unchecked", "rawtypes" })
251 private Properties getBeanProperties(Object bean, Properties properties) {
252
253 Class<?> cls = bean.getClass();
254
255
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
305
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
335 if (candidates.length == 1) {
336 String candidate = candidates[0];
337 if (candidate.equals(prefix)) {
338
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
378
379
380
381
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 }