001/* 002 * Copyright 2006-2007 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.batch.item.file.mapping; 018 019import java.beans.PropertyEditor; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.Map; 023import java.util.Properties; 024import java.util.Set; 025import java.util.concurrent.ConcurrentHashMap; 026import java.util.concurrent.ConcurrentMap; 027 028import org.springframework.batch.item.file.transform.FieldSet; 029import org.springframework.batch.support.DefaultPropertyEditorRegistrar; 030import org.springframework.beans.BeanWrapperImpl; 031import org.springframework.beans.MutablePropertyValues; 032import org.springframework.beans.NotWritablePropertyException; 033import org.springframework.beans.PropertyAccessor; 034import org.springframework.beans.PropertyAccessorUtils; 035import org.springframework.beans.PropertyEditorRegistry; 036import org.springframework.beans.factory.BeanFactory; 037import org.springframework.beans.factory.BeanFactoryAware; 038import org.springframework.beans.factory.InitializingBean; 039import org.springframework.beans.factory.config.CustomEditorConfigurer; 040import org.springframework.core.convert.ConversionService; 041import org.springframework.util.Assert; 042import org.springframework.util.ReflectionUtils; 043import org.springframework.validation.BindException; 044import org.springframework.validation.DataBinder; 045 046/** 047 * {@link FieldSetMapper} implementation based on bean property paths. The 048 * {@link FieldSet} to be mapped should have field name meta data corresponding 049 * to bean property paths in an instance of the desired type. The instance is 050 * created and initialized either by referring to a prototype object by bean 051 * name in the enclosing BeanFactory, or by providing a class to instantiate 052 * reflectively.<br> 053 * <br> 054 * 055 * Nested property paths, including indexed properties in maps and collections, 056 * can be referenced by the {@link FieldSet} names. They will be converted to 057 * nested bean properties inside the prototype. The {@link FieldSet} and the 058 * prototype are thus tightly coupled by the fields that are available and those 059 * that can be initialized. If some of the nested properties are optional (e.g. 060 * collection members) they need to be removed by a post processor.<br> 061 * <br> 062 * 063 * To customize the way that {@link FieldSet} values are converted to the 064 * desired type for injecting into the prototype there are several choices. You 065 * can inject {@link PropertyEditor} instances directly through the 066 * {@link #setCustomEditors(Map) customEditors} property, or you can override 067 * the {@link #createBinder(Object)} and {@link #initBinder(DataBinder)} 068 * methods, or you can provide a custom {@link FieldSet} implementation. 069 * You can also use a {@link ConversionService} to convert to the desired type 070 * through the {@link #setConversionService(ConversionService) conversionService} 071 * property. 072 * <br> 073 * <br> 074 * 075 * Property name matching is "fuzzy" in the sense that it tolerates close 076 * matches, as long as the match is unique. For instance: 077 * 078 * <ul> 079 * <li>Quantity = quantity (field names can be capitalised)</li> 080 * <li>ISIN = isin (acronyms can be lower case bean property names, as per Java 081 * Beans recommendations)</li> 082 * <li>DuckPate = duckPate (capitalisation including camel casing)</li> 083 * <li>ITEM_ID = itemId (capitalisation and replacing word boundary with 084 * underscore)</li> 085 * <li>ORDER.CUSTOMER_ID = order.customerId (nested paths are recursively 086 * checked)</li> 087 * </ul> 088 * 089 * The algorithm used to match a property name is to start with an exact match 090 * and then search successively through more distant matches until precisely one 091 * match is found. If more than one match is found there will be an error. 092 * 093 * @author Dave Syer 094 * 095 */ 096public class BeanWrapperFieldSetMapper<T> extends DefaultPropertyEditorRegistrar implements FieldSetMapper<T>, 097 BeanFactoryAware, InitializingBean { 098 099 private String name; 100 101 private Class<? extends T> type; 102 103 private BeanFactory beanFactory; 104 105 private ConcurrentMap<DistanceHolder, ConcurrentMap<String, String>> propertiesMatched = new ConcurrentHashMap<DistanceHolder, ConcurrentMap<String, String>>(); 106 107 private int distanceLimit = 5; 108 109 private boolean strict = true; 110 111 private ConversionService conversionService; 112 113 private boolean isCustomEditorsSet; 114 115 /* 116 * (non-Javadoc) 117 * 118 * @see 119 * org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org 120 * .springframework.beans.factory.BeanFactory) 121 */ 122 @Override 123 public void setBeanFactory(BeanFactory beanFactory) { 124 this.beanFactory = beanFactory; 125 } 126 127 /** 128 * The maximum difference that can be tolerated in spelling between input 129 * key names and bean property names. Defaults to 5, but could be set lower 130 * if the field names match the bean names. 131 * 132 * @param distanceLimit the distance limit to set 133 */ 134 public void setDistanceLimit(int distanceLimit) { 135 this.distanceLimit = distanceLimit; 136 } 137 138 /** 139 * The bean name (id) for an object that can be populated from the field set 140 * that will be passed into {@link #mapFieldSet(FieldSet)}. Typically a 141 * prototype scoped bean so that a new instance is returned for each field 142 * set mapped. 143 * 144 * Either this property or the type property must be specified, but not 145 * both. 146 * 147 * @param name the name of a prototype bean in the enclosing BeanFactory 148 */ 149 public void setPrototypeBeanName(String name) { 150 this.name = name; 151 } 152 153 /** 154 * Public setter for the type of bean to create instead of using a prototype 155 * bean. An object of this type will be created from its default constructor 156 * for every call to {@link #mapFieldSet(FieldSet)}.<br> 157 * 158 * Either this property or the prototype bean name must be specified, but 159 * not both. 160 * 161 * @param type the type to set 162 */ 163 public void setTargetType(Class<? extends T> type) { 164 this.type = type; 165 } 166 167 /** 168 * Check that precisely one of type or prototype bean name is specified. 169 * 170 * @throws IllegalStateException if neither is set or both properties are 171 * set. 172 * 173 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 174 */ 175 @Override 176 public void afterPropertiesSet() throws Exception { 177 Assert.state(name != null || type != null, "Either name or type must be provided."); 178 Assert.state(name == null || type == null, "Both name and type cannot be specified together."); 179 Assert.state(!this.isCustomEditorsSet || this.conversionService == null, "Both customEditor and conversionService cannot be specified together."); 180 } 181 182 /** 183 * Map the {@link FieldSet} to an object retrieved from the enclosing Spring 184 * context, or to a new instance of the required type if no prototype is 185 * available. 186 * @throws BindException if there is a type conversion or other error (if 187 * the {@link DataBinder} from {@link #createBinder(Object)} has errors 188 * after binding). 189 * 190 * @throws NotWritablePropertyException if the {@link FieldSet} contains a 191 * field that cannot be mapped to a bean property. 192 * @see org.springframework.batch.item.file.mapping.FieldSetMapper#mapFieldSet(FieldSet) 193 */ 194 @Override 195 public T mapFieldSet(FieldSet fs) throws BindException { 196 T copy = getBean(); 197 DataBinder binder = createBinder(copy); 198 binder.bind(new MutablePropertyValues(getBeanProperties(copy, fs.getProperties()))); 199 if (binder.getBindingResult().hasErrors()) { 200 throw new BindException(binder.getBindingResult()); 201 } 202 return copy; 203 } 204 205 /** 206 * Create a binder for the target object. The binder will then be used to 207 * bind the properties form a field set into the target object. This 208 * implementation creates a new {@link DataBinder} and calls out to 209 * {@link #initBinder(DataBinder)} and 210 * {@link #registerCustomEditors(PropertyEditorRegistry)}. 211 * 212 * @param target Object to bind to 213 * @return a {@link DataBinder} that can be used to bind properties to the 214 * target. 215 */ 216 protected DataBinder createBinder(Object target) { 217 DataBinder binder = new DataBinder(target); 218 binder.setIgnoreUnknownFields(!this.strict); 219 initBinder(binder); 220 registerCustomEditors(binder); 221 if(this.conversionService != null) { 222 binder.setConversionService(this.conversionService); 223 } 224 return binder; 225 } 226 227 /** 228 * Initialize a new binder instance. This hook allows customization of 229 * binder settings such as the {@link DataBinder#initDirectFieldAccess() 230 * direct field access}. Called by {@link #createBinder(Object)}. 231 * <p> 232 * Note that registration of custom property editors can be done in 233 * {@link #registerCustomEditors(PropertyEditorRegistry)}. 234 * </p> 235 * @param binder new binder instance 236 * @see #createBinder(Object) 237 */ 238 protected void initBinder(DataBinder binder) { 239 } 240 241 @SuppressWarnings("unchecked") 242 private T getBean() { 243 if (name != null) { 244 return (T) beanFactory.getBean(name); 245 } 246 try { 247 return type.newInstance(); 248 } 249 catch (InstantiationException | IllegalAccessException e) { 250 ReflectionUtils.handleReflectionException(e); 251 } 252 // should not happen 253 throw new IllegalStateException("Internal error: could not create bean instance for mapping."); 254 } 255 256 /** 257 * @param bean Object to get properties for 258 * @param properties Properties to retrieve 259 */ 260 private Properties getBeanProperties(Object bean, Properties properties) { 261 262 Class<?> cls = bean.getClass(); 263 264 // Map from field names to property names 265 DistanceHolder distanceKey = new DistanceHolder(cls, distanceLimit); 266 if (!propertiesMatched.containsKey(distanceKey)) { 267 propertiesMatched.putIfAbsent(distanceKey, new ConcurrentHashMap<>()); 268 } 269 Map<String, String> matches = new HashMap<>(propertiesMatched.get(distanceKey)); 270 271 @SuppressWarnings({ "unchecked", "rawtypes" }) 272 Set<String> keys = new HashSet(properties.keySet()); 273 for (String key : keys) { 274 275 if (matches.containsKey(key)) { 276 switchPropertyNames(properties, key, matches.get(key)); 277 continue; 278 } 279 280 String name = findPropertyName(bean, key); 281 282 if (name != null) { 283 if (matches.containsValue(name)) { 284 throw new NotWritablePropertyException( 285 cls, 286 name, 287 "Duplicate match with distance <= " 288 + distanceLimit 289 + " found for this property in input keys: " 290 + keys 291 + ". (Consider reducing the distance limit or changing the input key names to get a closer match.)"); 292 } 293 matches.put(key, name); 294 switchPropertyNames(properties, key, name); 295 } 296 } 297 298 propertiesMatched.replace(distanceKey, new ConcurrentHashMap<>(matches)); 299 return properties; 300 } 301 302 private String findPropertyName(Object bean, String key) { 303 304 if (bean == null) { 305 return null; 306 } 307 308 Class<?> cls = bean.getClass(); 309 310 int index = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(key); 311 String prefix; 312 String suffix; 313 314 // If the property name is nested recurse down through the properties 315 // looking for a match. 316 if (index > 0) { 317 prefix = key.substring(0, index); 318 suffix = key.substring(index + 1, key.length()); 319 String nestedName = findPropertyName(bean, prefix); 320 if (nestedName == null) { 321 return null; 322 } 323 324 Object nestedValue = getPropertyValue(bean, nestedName); 325 String nestedPropertyName = findPropertyName(nestedValue, suffix); 326 return nestedPropertyName == null ? null : nestedName + "." + nestedPropertyName; 327 } 328 329 String name = null; 330 int distance = 0; 331 index = key.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR); 332 333 if (index > 0) { 334 prefix = key.substring(0, index); 335 suffix = key.substring(index); 336 } 337 else { 338 prefix = key; 339 suffix = ""; 340 } 341 342 while (name == null && distance <= distanceLimit) { 343 String[] candidates = PropertyMatches.forProperty(prefix, cls, distance).getPossibleMatches(); 344 // If we find precisely one match, then use that one... 345 if (candidates.length == 1) { 346 String candidate = candidates[0]; 347 if (candidate.equals(prefix)) { // if it's the same don't 348 // replace it... 349 name = key; 350 } 351 else { 352 name = candidate + suffix; 353 } 354 } 355 distance++; 356 } 357 return name; 358 } 359 360 private Object getPropertyValue(Object bean, String nestedName) { 361 BeanWrapperImpl wrapper = new BeanWrapperImpl(bean); 362 wrapper.setAutoGrowNestedPaths(true); 363 364 Object nestedValue = wrapper.getPropertyValue(nestedName); 365 if (nestedValue == null) { 366 try { 367 nestedValue = wrapper.getPropertyType(nestedName).newInstance(); 368 wrapper.setPropertyValue(nestedName, nestedValue); 369 } 370 catch (InstantiationException | IllegalAccessException e) { 371 ReflectionUtils.handleReflectionException(e); 372 } 373 } 374 return nestedValue; 375 } 376 377 private void switchPropertyNames(Properties properties, String oldName, String newName) { 378 String value = properties.getProperty(oldName); 379 properties.remove(oldName); 380 properties.setProperty(newName, value); 381 } 382 383 /** 384 * Public setter for the 'strict' property. If true, then 385 * {@link #mapFieldSet(FieldSet)} will fail of the FieldSet contains fields 386 * that cannot be mapped to the bean. 387 * 388 * @param strict indicator 389 */ 390 public void setStrict(boolean strict) { 391 this.strict = strict; 392 } 393 394 395 /** 396 * Public setter for the 'conversionService' property. 397 * {@link #createBinder(Object)} will use it if not null. 398 * 399 * @param conversionService {@link ConversionService} to be used for type conversions 400 */ 401 public void setConversionService(ConversionService conversionService) { 402 this.conversionService = conversionService; 403 } 404 405 /** 406 * Specify the {@link PropertyEditor custom editors} to register. 407 * 408 * 409 * @param customEditors a map of Class to PropertyEditor (or class name to 410 * PropertyEditor). 411 * @see CustomEditorConfigurer#setCustomEditors(Map) 412 */ 413 @Override 414 public void setCustomEditors(Map<? extends Object, ? extends PropertyEditor> customEditors) { 415 this.isCustomEditorsSet = true; 416 super.setCustomEditors(customEditors); 417 } 418 419 private static class DistanceHolder { 420 private final Class<?> cls; 421 422 private final int distance; 423 424 public DistanceHolder(Class<?> cls, int distance) { 425 this.cls = cls; 426 this.distance = distance; 427 428 } 429 430 @Override 431 public int hashCode() { 432 final int prime = 31; 433 int result = 1; 434 result = prime * result + ((cls == null) ? 0 : cls.hashCode()); 435 result = prime * result + distance; 436 return result; 437 } 438 439 @Override 440 public boolean equals(Object obj) { 441 if (this == obj) 442 return true; 443 if (obj == null) 444 return false; 445 if (getClass() != obj.getClass()) 446 return false; 447 DistanceHolder other = (DistanceHolder) obj; 448 if (cls == null) { 449 if (other.cls != null) 450 return false; 451 } 452 else if (!cls.equals(other.cls)) 453 return false; 454 if (distance != other.distance) 455 return false; 456 return true; 457 } 458 } 459 460}