| 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 | } |