View Javadoc

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.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   * {@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   */
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 	 * (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 }