1. /*
  2. * Copyright 2002-2004 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. package org.springframework.beans;
  17. import java.beans.PropertyChangeEvent;
  18. import java.beans.PropertyDescriptor;
  19. import java.beans.PropertyEditor;
  20. import java.beans.PropertyEditorManager;
  21. import java.io.File;
  22. import java.io.InputStream;
  23. import java.lang.reflect.Array;
  24. import java.lang.reflect.InvocationTargetException;
  25. import java.lang.reflect.Method;
  26. import java.math.BigDecimal;
  27. import java.math.BigInteger;
  28. import java.net.URL;
  29. import java.util.ArrayList;
  30. import java.util.HashMap;
  31. import java.util.Iterator;
  32. import java.util.List;
  33. import java.util.Locale;
  34. import java.util.Map;
  35. import java.util.Properties;
  36. import java.util.Set;
  37. import org.apache.commons.logging.Log;
  38. import org.apache.commons.logging.LogFactory;
  39. import org.springframework.beans.propertyeditors.ByteArrayPropertyEditor;
  40. import org.springframework.beans.propertyeditors.ClassEditor;
  41. import org.springframework.beans.propertyeditors.CustomBooleanEditor;
  42. import org.springframework.beans.propertyeditors.CustomNumberEditor;
  43. import org.springframework.beans.propertyeditors.FileEditor;
  44. import org.springframework.beans.propertyeditors.InputStreamEditor;
  45. import org.springframework.beans.propertyeditors.LocaleEditor;
  46. import org.springframework.beans.propertyeditors.PropertiesEditor;
  47. import org.springframework.beans.propertyeditors.StringArrayPropertyEditor;
  48. import org.springframework.beans.propertyeditors.URLEditor;
  49. import org.springframework.util.StringUtils;
  50. /**
  51. * Default implementation of the BeanWrapper interface that should be sufficient
  52. * for all normal uses. Caches introspection results for efficiency.
  53. *
  54. * <p>Note: This class never tries to load a class by name, as this can pose
  55. * class loading problems in J2EE applications with multiple deployment modules.
  56. * The caller is responsible for loading a target class.
  57. *
  58. * <p>Note: Auto-registers all default property editors (not the custom ones)
  59. * in the org.springframework.beans.propertyeditors package.
  60. * Applications can either use a standard PropertyEditorManager to register a
  61. * custom editor before using a BeanWrapperImpl instance, or call the instance's
  62. * registerCustomEditor method to register an editor for the particular instance.
  63. *
  64. * <p>BeanWrapperImpl will convert List and array values to the corresponding
  65. * target arrays, if necessary. Custom property editors that deal with Lists or
  66. * arrays can be written against a comma delimited String as String arrays are
  67. * converted in such a format if the array itself is not assignable.
  68. *
  69. * @author Rod Johnson
  70. * @author Juergen Hoeller
  71. * @author Jean-Pierre Pawlak
  72. * @since 15 April 2001
  73. * @version $Id: BeanWrapperImpl.java,v 1.41 2004/06/02 00:47:17 jhoeller Exp $
  74. * @see #registerCustomEditor
  75. * @see java.beans.PropertyEditorManager
  76. * @see org.springframework.beans.propertyeditors.ClassEditor
  77. * @see org.springframework.beans.propertyeditors.FileEditor
  78. * @see org.springframework.beans.propertyeditors.LocaleEditor
  79. * @see org.springframework.beans.propertyeditors.PropertiesEditor
  80. * @see org.springframework.beans.propertyeditors.StringArrayPropertyEditor
  81. * @see org.springframework.beans.propertyeditors.URLEditor
  82. */
  83. public class BeanWrapperImpl implements BeanWrapper {
  84. /** We'll create a lot of these objects, so we don't want a new logger every time */
  85. private static final Log logger = LogFactory.getLog(BeanWrapperImpl.class);
  86. //---------------------------------------------------------------------
  87. // Instance data
  88. //---------------------------------------------------------------------
  89. /** The wrapped object */
  90. private Object object;
  91. /** The nested path of the object */
  92. private String nestedPath = "";
  93. /** Registry for default PropertyEditors */
  94. private final Map defaultEditors;
  95. /** Map with custom PropertyEditor instances */
  96. private Map customEditors;
  97. /**
  98. * Cached introspections results for this object, to prevent encountering the cost
  99. * of JavaBeans introspection every time.
  100. */
  101. private CachedIntrospectionResults cachedIntrospectionResults;
  102. /* Map with cached nested BeanWrappers */
  103. private Map nestedBeanWrappers;
  104. //---------------------------------------------------------------------
  105. // Constructors
  106. //---------------------------------------------------------------------
  107. /**
  108. * Create new empty BeanWrapperImpl. Wrapped instance needs to be set afterwards.
  109. * @see #setWrappedInstance
  110. */
  111. public BeanWrapperImpl() {
  112. // Register default editors in this class, for restricted environments.
  113. // We're not using the JRE's PropertyEditorManager to avoid potential
  114. // SecurityExceptions when running in a SecurityManager.
  115. this.defaultEditors = new HashMap(16);
  116. // Simple editors, without parameterization capabilities.
  117. this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor());
  118. this.defaultEditors.put(Class.class, new ClassEditor());
  119. this.defaultEditors.put(File.class, new FileEditor());
  120. this.defaultEditors.put(InputStream.class, new InputStreamEditor());
  121. this.defaultEditors.put(Locale.class, new LocaleEditor());
  122. this.defaultEditors.put(Properties.class, new PropertiesEditor());
  123. this.defaultEditors.put(String[].class, new StringArrayPropertyEditor());
  124. this.defaultEditors.put(URL.class, new URLEditor());
  125. // Default instances of parameterizable editors.
  126. // Can be overridden by registering custom instances of those as custom editors.
  127. this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(false));
  128. this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, false));
  129. this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, false));
  130. this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, false));
  131. this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, false));
  132. this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, false));
  133. this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, false));
  134. this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, false));
  135. }
  136. /**
  137. * Create new BeanWrapperImpl for the given object.
  138. * @param object object wrapped by this BeanWrapper
  139. */
  140. public BeanWrapperImpl(Object object) {
  141. this();
  142. setWrappedInstance(object);
  143. }
  144. /**
  145. * Create new BeanWrapperImpl, wrapping a new instance of the specified class.
  146. * @param clazz class to instantiate and wrap
  147. */
  148. public BeanWrapperImpl(Class clazz) {
  149. this();
  150. setWrappedInstance(BeanUtils.instantiateClass(clazz));
  151. }
  152. /**
  153. * Create new BeanWrapperImpl for the given object,
  154. * registering a nested path that the object is in.
  155. * @param object object wrapped by this BeanWrapper.
  156. * @param nestedPath the nested path of the object
  157. */
  158. public BeanWrapperImpl(Object object, String nestedPath) {
  159. this();
  160. setWrappedInstance(object, nestedPath);
  161. }
  162. /**
  163. * Create new BeanWrapperImpl for the given object,
  164. * registering a nested path that the object is in.
  165. * @param object object wrapped by this BeanWrapper.
  166. * @param nestedPath the nested path of the object
  167. * @param superBw the containing BeanWrapper (must not be null)
  168. */
  169. private BeanWrapperImpl(Object object, String nestedPath, BeanWrapperImpl superBw) {
  170. this.defaultEditors = superBw.defaultEditors;
  171. setWrappedInstance(object, nestedPath);
  172. }
  173. //---------------------------------------------------------------------
  174. // Implementation of BeanWrapper
  175. //---------------------------------------------------------------------
  176. /**
  177. * Switch the target object, replacing the cached introspection results only
  178. * if the class of the new object is different to that of the replaced object.
  179. * @param object new target
  180. */
  181. public void setWrappedInstance(Object object) {
  182. setWrappedInstance(object, "");
  183. }
  184. /**
  185. * Switch the target object, replacing the cached introspection results only
  186. * if the class of the new object is different to that of the replaced object.
  187. * @param object new target
  188. * @param nestedPath the nested path of the object
  189. */
  190. public void setWrappedInstance(Object object, String nestedPath) {
  191. if (object == null) {
  192. throw new IllegalArgumentException("Cannot set BeanWrapperImpl target to a null object");
  193. }
  194. this.object = object;
  195. this.nestedPath = nestedPath;
  196. this.nestedBeanWrappers = null;
  197. if (this.cachedIntrospectionResults == null ||
  198. !this.cachedIntrospectionResults.getBeanClass().equals(object.getClass())) {
  199. this.cachedIntrospectionResults = CachedIntrospectionResults.forClass(object.getClass());
  200. }
  201. }
  202. public Object getWrappedInstance() {
  203. return this.object;
  204. }
  205. public Class getWrappedClass() {
  206. return this.object.getClass();
  207. }
  208. public void registerCustomEditor(Class requiredType, PropertyEditor propertyEditor) {
  209. registerCustomEditor(requiredType, null, propertyEditor);
  210. }
  211. public void registerCustomEditor(Class requiredType, String propertyPath, PropertyEditor propertyEditor) {
  212. if (propertyPath != null) {
  213. List bws = getBeanWrappersForPropertyPath(propertyPath);
  214. for (Iterator it = bws.iterator(); it.hasNext();) {
  215. BeanWrapperImpl bw = (BeanWrapperImpl) it.next();
  216. bw.doRegisterCustomEditor(requiredType, getFinalPath(bw, propertyPath), propertyEditor);
  217. }
  218. }
  219. else {
  220. doRegisterCustomEditor(requiredType, propertyPath, propertyEditor);
  221. }
  222. }
  223. private void doRegisterCustomEditor(Class requiredType, String propertyName, PropertyEditor propertyEditor) {
  224. if (this.customEditors == null) {
  225. this.customEditors = new HashMap();
  226. }
  227. if (propertyName != null) {
  228. this.customEditors.put(propertyName, propertyEditor);
  229. }
  230. else {
  231. if (requiredType == null) {
  232. throw new IllegalArgumentException("No propertyName and no requiredType specified");
  233. }
  234. this.customEditors.put(requiredType, propertyEditor);
  235. }
  236. }
  237. public PropertyEditor findCustomEditor(Class requiredType, String propertyPath) {
  238. if (propertyPath != null) {
  239. BeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyPath);
  240. return nestedBw.doFindCustomEditor(requiredType, getFinalPath(nestedBw, propertyPath));
  241. }
  242. else {
  243. return doFindCustomEditor(requiredType, propertyPath);
  244. }
  245. }
  246. private PropertyEditor doFindCustomEditor(Class requiredType, String propertyName) {
  247. if (this.customEditors == null) {
  248. return null;
  249. }
  250. if (propertyName != null) {
  251. // check property-specific editor first
  252. try {
  253. PropertyEditor editor = (PropertyEditor) this.customEditors.get(propertyName);
  254. if (editor == null) {
  255. int keyIndex = propertyName.indexOf('[');
  256. if (keyIndex != -1) {
  257. editor = (PropertyEditor) this.customEditors.get(propertyName.substring(0, keyIndex));
  258. }
  259. }
  260. if (editor != null) {
  261. return editor;
  262. }
  263. else {
  264. if (requiredType == null) {
  265. // try property type
  266. requiredType = getPropertyDescriptor(propertyName).getPropertyType();
  267. }
  268. }
  269. }
  270. catch (InvalidPropertyException ex) {
  271. // probably an indexed or mapped property
  272. // we need to retrieve the value to determine the type
  273. Object value = getPropertyValue(propertyName);
  274. if (value == null) {
  275. return null;
  276. }
  277. requiredType = value.getClass();
  278. }
  279. }
  280. // no property-specific editor -> check type-specific editor
  281. return (PropertyEditor) this.customEditors.get(requiredType);
  282. }
  283. /**
  284. * Get the last component of the path. Also works if not nested.
  285. * @param bw BeanWrapper to work on
  286. * @param nestedPath property path we know is nested
  287. * @return last component of the path (the property on the target bean)
  288. */
  289. private String getFinalPath(BeanWrapper bw, String nestedPath) {
  290. if (bw == this) {
  291. return nestedPath;
  292. }
  293. return nestedPath.substring(nestedPath.lastIndexOf(NESTED_PROPERTY_SEPARATOR) + 1);
  294. }
  295. /**
  296. * Recursively navigate to return a BeanWrapper for the nested property path.
  297. * @param propertyPath property property path, which may be nested
  298. * @return a BeanWrapper for the target bean
  299. */
  300. private BeanWrapperImpl getBeanWrapperForPropertyPath(String propertyPath) throws BeansException {
  301. int pos = propertyPath.indexOf(NESTED_PROPERTY_SEPARATOR);
  302. // handle nested properties recursively
  303. if (pos > -1) {
  304. String nestedProperty = propertyPath.substring(0, pos);
  305. String nestedPath = propertyPath.substring(pos + 1);
  306. BeanWrapperImpl nestedBw = getNestedBeanWrapper(nestedProperty);
  307. return nestedBw.getBeanWrapperForPropertyPath(nestedPath);
  308. }
  309. else {
  310. return this;
  311. }
  312. }
  313. /**
  314. * Recursively navigate to return a BeanWrapper for the nested property path.
  315. * In case of an indexed or mapped property, all BeanWrappers that apply will
  316. * be returned.
  317. * @param propertyPath property property path, which may be nested
  318. * @return a BeanWrapper for the target bean
  319. */
  320. private List getBeanWrappersForPropertyPath(String propertyPath) throws BeansException {
  321. List beanWrappers = new ArrayList();
  322. int pos = propertyPath.indexOf(NESTED_PROPERTY_SEPARATOR);
  323. // handle nested properties recursively
  324. if (pos > -1) {
  325. String nestedProperty = propertyPath.substring(0, pos);
  326. String nestedPath = propertyPath.substring(pos + 1);
  327. if (nestedProperty.indexOf('[') == -1) {
  328. Class propertyType = getPropertyDescriptor(nestedProperty).getPropertyType();
  329. if (propertyType.isArray()) {
  330. Object[] array = (Object[]) getPropertyValue(nestedProperty);
  331. for (int i = 0; i < array.length; i++) {
  332. beanWrappers.addAll(
  333. getBeanWrappersForNestedProperty(nestedProperty + PROPERTY_KEY_PREFIX + i + PROPERTY_KEY_SUFFIX,
  334. nestedPath));
  335. }
  336. return beanWrappers;
  337. }
  338. else if (List.class.isAssignableFrom(propertyType)) {
  339. List list = (List) getPropertyValue(nestedProperty);
  340. for (int i = 0; i < list.size(); i++) {
  341. beanWrappers.addAll(
  342. getBeanWrappersForNestedProperty(nestedProperty + PROPERTY_KEY_PREFIX + i + PROPERTY_KEY_SUFFIX,
  343. nestedPath));
  344. }
  345. return beanWrappers;
  346. }
  347. else if (Map.class.isAssignableFrom(propertyType)) {
  348. Map map = (Map) getPropertyValue(nestedProperty);
  349. for (Iterator it = map.keySet().iterator(); it.hasNext();) {
  350. beanWrappers.addAll(
  351. getBeanWrappersForNestedProperty(nestedProperty + PROPERTY_KEY_PREFIX + it.next() + PROPERTY_KEY_SUFFIX,
  352. nestedPath));
  353. }
  354. return beanWrappers;
  355. }
  356. }
  357. beanWrappers.addAll(getBeanWrappersForNestedProperty(nestedProperty, nestedPath));
  358. return beanWrappers;
  359. }
  360. else {
  361. beanWrappers.add(this);
  362. return beanWrappers;
  363. }
  364. }
  365. private List getBeanWrappersForNestedProperty(String nestedProperty, String nestedPath) throws BeansException {
  366. BeanWrapperImpl nestedBw = getNestedBeanWrapper(nestedProperty);
  367. return nestedBw.getBeanWrappersForPropertyPath(nestedPath);
  368. }
  369. /**
  370. * Retrieve a BeanWrapper for the given nested property.
  371. * Create a new one if not found in the cache.
  372. * <p>Note: Caching nested BeanWrappers is necessary now,
  373. * to keep registered custom editors for nested properties.
  374. * @param nestedProperty property to create the BeanWrapper for
  375. * @return the BeanWrapper instance, either cached or newly created
  376. */
  377. private BeanWrapperImpl getNestedBeanWrapper(String nestedProperty) throws BeansException {
  378. if (this.nestedBeanWrappers == null) {
  379. this.nestedBeanWrappers = new HashMap();
  380. }
  381. // get value of bean property
  382. String[] tokens = getPropertyNameTokens(nestedProperty);
  383. Object propertyValue = getPropertyValue(tokens[0], tokens[1], tokens[2]);
  384. String canonicalName = tokens[0];
  385. if (propertyValue == null) {
  386. throw new NullValueInNestedPathException(getWrappedClass(), this.nestedPath + canonicalName);
  387. }
  388. // lookup cached sub-BeanWrapper, create new one if not found
  389. BeanWrapperImpl nestedBw = (BeanWrapperImpl) this.nestedBeanWrappers.get(canonicalName);
  390. if (nestedBw == null) {
  391. if (logger.isDebugEnabled()) {
  392. logger.debug("Creating new nested BeanWrapper for property '" + canonicalName + "'");
  393. }
  394. nestedBw = new BeanWrapperImpl(propertyValue, this.nestedPath + canonicalName + NESTED_PROPERTY_SEPARATOR,
  395. this);
  396. // inherit all type-specific PropertyEditors
  397. if (this.customEditors != null) {
  398. for (Iterator it = this.customEditors.keySet().iterator(); it.hasNext();) {
  399. Object key = it.next();
  400. if (key instanceof Class) {
  401. Class requiredType = (Class) key;
  402. PropertyEditor propertyEditor = (PropertyEditor) this.customEditors.get(key);
  403. nestedBw.registerCustomEditor(requiredType, propertyEditor);
  404. }
  405. }
  406. }
  407. this.nestedBeanWrappers.put(canonicalName, nestedBw);
  408. }
  409. else {
  410. if (logger.isDebugEnabled()) {
  411. logger.debug("Using cached nested BeanWrapper for property '" + canonicalName + "'");
  412. }
  413. }
  414. return nestedBw;
  415. }
  416. private String[] getPropertyNameTokens(String propertyName) {
  417. String actualName = propertyName;
  418. String key = null;
  419. int keyStart = propertyName.indexOf('[');
  420. if (keyStart != -1 && propertyName.endsWith("]")) {
  421. actualName = propertyName.substring(0, keyStart);
  422. key = propertyName.substring(keyStart + 1, propertyName.length() - 1);
  423. if (key.startsWith("'") && key.endsWith("'")) {
  424. key = key.substring(1, key.length() - 1);
  425. }
  426. else if (key.startsWith("\"") && key.endsWith("\"")) {
  427. key = key.substring(1, key.length() - 1);
  428. }
  429. }
  430. String canonicalName = actualName;
  431. if (key != null) {
  432. canonicalName += PROPERTY_KEY_PREFIX + key + PROPERTY_KEY_SUFFIX;
  433. }
  434. return new String[] {canonicalName, actualName, key};
  435. }
  436. public Object getPropertyValue(String propertyName) throws BeansException {
  437. BeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName);
  438. String[] tokens = getPropertyNameTokens(getFinalPath(nestedBw, propertyName));
  439. return nestedBw.getPropertyValue(tokens[0], tokens[1], tokens[2]);
  440. }
  441. private Object getPropertyValue(String propertyName, String actualName, String key) throws BeansException {
  442. PropertyDescriptor pd = getPropertyDescriptorInternal(actualName);
  443. if (pd == null || pd.getReadMethod() == null) {
  444. throw new NotReadablePropertyException(getWrappedClass(), this.nestedPath + propertyName);
  445. }
  446. if (logger.isDebugEnabled())
  447. logger.debug("About to invoke read method [" + pd.getReadMethod() +
  448. "] on object of class [" + this.object.getClass().getName() + "]");
  449. try {
  450. Object value = pd.getReadMethod().invoke(this.object, null);
  451. if (key != null) {
  452. if (value == null) {
  453. throw new NullValueInNestedPathException(getWrappedClass(), this.nestedPath + propertyName,
  454. "Cannot access indexed value of property referenced in indexed " +
  455. "property path '" + propertyName + "': returned null");
  456. }
  457. else if (value.getClass().isArray()) {
  458. return Array.get(value, Integer.parseInt(key));
  459. }
  460. else if (value instanceof List) {
  461. List list = (List) value;
  462. return list.get(Integer.parseInt(key));
  463. }
  464. else if (value instanceof Set) {
  465. // apply index to Iterator in case of a Set
  466. Set set = (Set) value;
  467. int index = Integer.parseInt(key);
  468. Iterator it = set.iterator();
  469. for (int i = 0; it.hasNext(); i++) {
  470. Object elem = it.next();
  471. if (i == index) {
  472. return elem;
  473. }
  474. }
  475. throw new InvalidPropertyException(getWrappedClass(), this.nestedPath + propertyName,
  476. "Cannot get element with index " + index + " from Set of size " +
  477. set.size() + ", accessed using property path '" + propertyName + "'");
  478. }
  479. else if (value instanceof Map) {
  480. Map map = (Map) value;
  481. return map.get(key);
  482. }
  483. else {
  484. throw new InvalidPropertyException(getWrappedClass(), this.nestedPath + propertyName,
  485. "Property referenced in indexed property path '" + propertyName +
  486. "' is neither an array nor a List nor a Map; returned value was [" +
  487. value + "]");
  488. }
  489. }
  490. else {
  491. return value;
  492. }
  493. }
  494. catch (InvocationTargetException ex) {
  495. throw new InvalidPropertyException(getWrappedClass(), this.nestedPath + propertyName,
  496. "Getter for property '" + actualName + "' threw exception", ex);
  497. }
  498. catch (IllegalAccessException ex) {
  499. throw new InvalidPropertyException(getWrappedClass(), this.nestedPath + propertyName,
  500. "Illegal attempt to get property '" + actualName + "' threw exception", ex);
  501. }
  502. catch (IndexOutOfBoundsException ex) {
  503. throw new InvalidPropertyException(getWrappedClass(), this.nestedPath + propertyName,
  504. "Index of out of bounds in property path '" + propertyName + "'", ex);
  505. }
  506. catch (NumberFormatException ex) {
  507. throw new InvalidPropertyException(getWrappedClass(), this.nestedPath + propertyName,
  508. "Invalid index in property path '" + propertyName + "'", ex);
  509. }
  510. }
  511. public void setPropertyValue(String propertyName, Object value) throws BeansException {
  512. BeanWrapperImpl nestedBw = null;
  513. try {
  514. nestedBw = getBeanWrapperForPropertyPath(propertyName);
  515. }
  516. catch (NotReadablePropertyException ex) {
  517. throw new NotWritablePropertyException(getWrappedClass(), this.nestedPath + propertyName,
  518. "Nested property in path '" + propertyName + "' does not exist",
  519. ex);
  520. }
  521. String[] tokens = getPropertyNameTokens(getFinalPath(nestedBw, propertyName));
  522. nestedBw.setPropertyValue(tokens[0], tokens[1], tokens[2], value);
  523. }
  524. private void setPropertyValue(String propertyName, String actualName, String key, Object value)
  525. throws BeansException {
  526. if (key != null) {
  527. Object propValue = null;
  528. try {
  529. propValue = getPropertyValue(actualName);
  530. }
  531. catch (NotReadablePropertyException ex) {
  532. throw new NotWritablePropertyException(getWrappedClass(), this.nestedPath + propertyName,
  533. "Cannot access indexed value in property referenced " +
  534. "in indexed property path '" + propertyName + "'",
  535. ex);
  536. }
  537. if (propValue == null) {
  538. throw new NullValueInNestedPathException(getWrappedClass(), this.nestedPath + propertyName,
  539. "Cannot access indexed value in property referenced " +
  540. "in indexed property path '" + propertyName + "': returned null");
  541. }
  542. else if (propValue.getClass().isArray()) {
  543. Object newValue = doTypeConversionIfNecessary(propertyName, propertyName, null, value,
  544. propValue.getClass().getComponentType());
  545. Array.set(propValue, Integer.parseInt(key), newValue);
  546. }
  547. else if (propValue instanceof List) {
  548. Object newValue = doTypeConversionIfNecessary(propertyName, propertyName, null, value, null);
  549. List list = (List) propValue;
  550. int index = Integer.parseInt(key);
  551. if (index < list.size()) {
  552. list.set(index, newValue);
  553. }
  554. else if (index >= list.size()) {
  555. for (int i = list.size(); i < index; i++) {
  556. try {
  557. list.add(null);
  558. }
  559. catch (NullPointerException ex) {
  560. throw new InvalidPropertyException(getWrappedClass(), this.nestedPath + propertyName,
  561. "Cannot set element with index " + index + " in List of size " +
  562. list.size() + ", accessed using property path '" + propertyName +
  563. "': List does not support filling up gaps with null elements");
  564. }
  565. }
  566. list.add(newValue);
  567. }
  568. }
  569. else if (propValue instanceof Map) {
  570. Object newValue = doTypeConversionIfNecessary(propertyName, propertyName, null, value, null);
  571. Map map = (Map) propValue;
  572. map.put(key, newValue);
  573. }
  574. else {
  575. throw new InvalidPropertyException(getWrappedClass(), this.nestedPath + propertyName,
  576. "Property referenced in indexed property path '" + propertyName +
  577. "' is neither an array nor a List nor a Map; returned value was [" +
  578. value + "]");
  579. }
  580. }
  581. else {
  582. if (!isWritableProperty(propertyName)) {
  583. throw new NotWritablePropertyException(getWrappedClass(), this.nestedPath + propertyName);
  584. }
  585. PropertyDescriptor pd = getPropertyDescriptor(propertyName);
  586. Method writeMethod = pd.getWriteMethod();
  587. Object newValue = null;
  588. try {
  589. // old value may still be null
  590. newValue = doTypeConversionIfNecessary(propertyName, propertyName, null, value, pd.getPropertyType());
  591. if (pd.getPropertyType().isPrimitive() &&
  592. (newValue == null || "".equals(newValue))) {
  593. throw new IllegalArgumentException("Invalid value [" + value + "] for property '" +
  594. pd.getName() + "' of primitive type [" + pd.getPropertyType() + "]");
  595. }
  596. if (logger.isDebugEnabled()) {
  597. logger.debug("About to invoke write method [" + writeMethod +
  598. "] on object of class [" + object.getClass().getName() + "]");
  599. }
  600. writeMethod.invoke(this.object, new Object[] { newValue });
  601. if (logger.isDebugEnabled()) {
  602. String msg = "Invoked write method [" + writeMethod + "] with value ";
  603. // only cause toString invocation of new value in case of simple property
  604. if (newValue == null || BeanUtils.isSimpleProperty(pd.getPropertyType())) {
  605. logger.debug(msg + PROPERTY_KEY_PREFIX + newValue + PROPERTY_KEY_SUFFIX);
  606. }
  607. else {
  608. logger.debug(msg + "of type [" + pd.getPropertyType().getName() + "]");
  609. }
  610. }
  611. }
  612. catch (InvocationTargetException ex) {
  613. PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent(this.object, this.nestedPath + propertyName,
  614. null, newValue);
  615. if (ex.getTargetException() instanceof ClassCastException) {
  616. throw new TypeMismatchException(propertyChangeEvent, pd.getPropertyType(), ex.getTargetException());
  617. }
  618. else {
  619. throw new MethodInvocationException(propertyChangeEvent, ex.getTargetException());
  620. }
  621. }
  622. catch (IllegalArgumentException ex) {
  623. PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent(this.object, this.nestedPath + propertyName,
  624. null, newValue);
  625. throw new TypeMismatchException(propertyChangeEvent, pd.getPropertyType(), ex);
  626. }
  627. catch (IllegalAccessException ex) {
  628. PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent(this.object, this.nestedPath + propertyName,
  629. null, newValue);
  630. throw new MethodInvocationException(propertyChangeEvent, ex);
  631. }
  632. }
  633. }
  634. public void setPropertyValue(PropertyValue pv) throws BeansException {
  635. setPropertyValue(pv.getName(), pv.getValue());
  636. }
  637. /**
  638. * Bulk update from a Map.
  639. * Bulk updates from PropertyValues are more powerful: this method is
  640. * provided for convenience.
  641. * @param map map containing properties to set, as name-value pairs.
  642. * The map may include nested properties.
  643. * @throws BeansException if there's a fatal, low-level exception
  644. */
  645. public void setPropertyValues(Map map) throws BeansException {
  646. setPropertyValues(new MutablePropertyValues(map));
  647. }
  648. public void setPropertyValues(PropertyValues pvs) throws BeansException {
  649. setPropertyValues(pvs, false);
  650. }
  651. public void setPropertyValues(PropertyValues propertyValues, boolean ignoreUnknown) throws BeansException {
  652. List propertyAccessExceptions = new ArrayList();
  653. PropertyValue[] pvs = propertyValues.getPropertyValues();
  654. for (int i = 0; i < pvs.length; i++) {
  655. try {
  656. // This method may throw any BeansException, which won't be caught
  657. // here, if there is a critical failure such as no matching field.
  658. // We can attempt to deal only with less serious exceptions.
  659. setPropertyValue(pvs[i]);
  660. }
  661. catch (NotWritablePropertyException ex) {
  662. if (!ignoreUnknown) {
  663. throw ex;
  664. }
  665. // otherwise, just ignore it and continue...
  666. }
  667. catch (TypeMismatchException ex) {
  668. propertyAccessExceptions.add(ex);
  669. }
  670. catch (MethodInvocationException ex) {
  671. propertyAccessExceptions.add(ex);
  672. }
  673. }
  674. // If we encountered individual exceptions, throw the composite exception.
  675. if (!propertyAccessExceptions.isEmpty()) {
  676. Object[] paeArray = propertyAccessExceptions.toArray(new PropertyAccessException[propertyAccessExceptions.size()]);
  677. throw new PropertyAccessExceptionsException(this, (PropertyAccessException[]) paeArray);
  678. }
  679. }
  680. private PropertyChangeEvent createPropertyChangeEvent(String propertyName, Object oldValue, Object newValue)
  681. throws BeansException {
  682. return new PropertyChangeEvent((this.object != null ? this.object : "constructor"),
  683. (propertyName != null ? this.nestedPath + propertyName : null),
  684. oldValue, newValue);
  685. }
  686. /**
  687. * Convert the value to the required type (if necessary from a String).
  688. * Conversions from String to any type use the setAsText() method of
  689. * the PropertyEditor class. Note that a PropertyEditor must be registered
  690. * for this class for this to work. This is a standard Java Beans API.
  691. * A number of property editors are automatically registered by this class.
  692. * @param newValue proposed change value.
  693. * @param requiredType type we must convert to
  694. * @throws BeansException if there is an internal error
  695. * @return new value, possibly the result of type convertion
  696. */
  697. public Object doTypeConversionIfNecessary(Object newValue, Class requiredType) throws BeansException {
  698. return doTypeConversionIfNecessary(null, null, null, newValue, requiredType);
  699. }
  700. /**
  701. * Convert the value to the required type (if necessary from a String),
  702. * for the specified property.
  703. * @param propertyName name of the property
  704. * @param oldValue previous value, if available (may be null)
  705. * @param newValue proposed change value.
  706. * @param requiredType type we must convert to
  707. * @throws BeansException if there is an internal error
  708. * @return new value, possibly the result of type convertion
  709. */
  710. protected Object doTypeConversionIfNecessary(String propertyName, String fullPropertyName,
  711. Object oldValue, Object newValue,
  712. Class requiredType) throws BeansException {
  713. if (newValue != null) {
  714. if (requiredType != null && requiredType.isArray()) {
  715. // convert individual elements to array elements
  716. Class componentType = requiredType.getComponentType();
  717. if (newValue instanceof List) {
  718. List list = (List) newValue;
  719. Object result = Array.newInstance(componentType, list.size());
  720. for (int i = 0; i < list.size(); i++) {
  721. Object value = doTypeConversionIfNecessary(propertyName,
  722. propertyName + PROPERTY_KEY_PREFIX + i + PROPERTY_KEY_SUFFIX,
  723. null, list.get(i), componentType);
  724. Array.set(result, i, value);
  725. }
  726. return result;
  727. }
  728. else if (newValue instanceof Object[]) {
  729. Object[] array = (Object[]) newValue;
  730. Object result = Array.newInstance(componentType, array.length);
  731. for (int i = 0; i < array.length; i++) {
  732. Object value = doTypeConversionIfNecessary(propertyName,
  733. propertyName + PROPERTY_KEY_PREFIX + i + PROPERTY_KEY_SUFFIX,
  734. null, array[i], componentType);
  735. Array.set(result, i, value);
  736. }
  737. return result;
  738. }
  739. }
  740. // custom editor for this type?
  741. PropertyEditor pe = findCustomEditor(requiredType, fullPropertyName);
  742. // value not of required type?
  743. if (pe != null || (requiredType != null && !requiredType.isAssignableFrom(newValue.getClass()))) {
  744. if (pe == null && requiredType != null) {
  745. // no custom editor -> check BeanWrapperImpl's default editors
  746. pe = (PropertyEditor) this.defaultEditors.get(requiredType);
  747. if (pe == null) {
  748. // no BeanWrapper default editor -> check standard JavaBean editors
  749. pe = PropertyEditorManager.findEditor(requiredType);
  750. }
  751. }
  752. if (newValue instanceof String[]) {
  753. if (logger.isDebugEnabled()) {
  754. logger.debug("Converting String array to comma-delimited String [" + newValue + "]");
  755. }
  756. newValue = StringUtils.arrayToCommaDelimitedString((String[]) newValue);
  757. }
  758. if (newValue instanceof String) {
  759. if (pe != null) {
  760. // use PropertyEditor's setAsText in case of a String value
  761. if (logger.isDebugEnabled()) {
  762. logger.debug("Converting String to [" + requiredType + "] using property editor [" + pe + "]");
  763. }
  764. try {
  765. pe.setAsText((String) newValue);
  766. newValue = pe.getValue();
  767. }
  768. catch (IllegalArgumentException ex) {
  769. throw new TypeMismatchException(createPropertyChangeEvent(fullPropertyName, oldValue, newValue),
  770. requiredType, ex);
  771. }
  772. }
  773. else {
  774. throw new TypeMismatchException(createPropertyChangeEvent(fullPropertyName, oldValue, newValue),
  775. requiredType);
  776. }
  777. }
  778. else if (pe != null) {
  779. // Not a String -> use PropertyEditor's setValue.
  780. // With standard PropertyEditors, this will return the very same object;
  781. // we just want to allow special PropertyEditors to override setValue
  782. // for type conversion from non-String values to the required type.
  783. try {
  784. pe.setValue(newValue);
  785. newValue = pe.getValue();
  786. }
  787. catch (IllegalArgumentException ex) {
  788. throw new TypeMismatchException(createPropertyChangeEvent(fullPropertyName, oldValue, newValue),
  789. requiredType, ex);
  790. }
  791. }
  792. }
  793. if (requiredType != null && requiredType.isArray() && !newValue.getClass().isArray()) {
  794. Class componentType = requiredType.getComponentType();
  795. Object result = Array.newInstance(componentType, 1) ;
  796. Object val = doTypeConversionIfNecessary(propertyName, propertyName + "[0]",
  797. null, newValue, componentType);
  798. Array.set(result, 0, val) ;
  799. return result;
  800. }
  801. }
  802. return newValue;
  803. }
  804. public PropertyDescriptor[] getPropertyDescriptors() {
  805. return this.cachedIntrospectionResults.getBeanInfo().getPropertyDescriptors();
  806. }
  807. public PropertyDescriptor getPropertyDescriptor(String propertyName) throws BeansException {
  808. if (propertyName == null) {
  809. throw new IllegalArgumentException("Can't find property descriptor for null property");
  810. }
  811. PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
  812. if (pd != null) {
  813. return pd;
  814. }
  815. else {
  816. throw new InvalidPropertyException(getWrappedClass(), this.nestedPath + propertyName,
  817. "No property '" + propertyName + "' found");
  818. }
  819. }
  820. /**
  821. * Internal version of getPropertyDescriptor:
  822. * Returns null if not found rather than throwing an exception.
  823. */
  824. protected PropertyDescriptor getPropertyDescriptorInternal(String propertyName) throws BeansException {
  825. BeanWrapperImpl nestedBw = getBeanWrapperForPropertyPath(propertyName);
  826. return nestedBw.cachedIntrospectionResults.getPropertyDescriptor(getFinalPath(nestedBw, propertyName));
  827. }
  828. public Class getPropertyType(String propertyName) throws BeansException {
  829. Class type = null;
  830. try {
  831. type = getPropertyDescriptor(propertyName).getPropertyType();
  832. }
  833. catch (InvalidPropertyException ex) {
  834. // probably an indexed or mapped element
  835. Object value = getPropertyValue(propertyName);
  836. if (value != null) {
  837. type = value.getClass();
  838. }
  839. }
  840. return type;
  841. }
  842. public boolean isReadableProperty(String propertyName) {
  843. // This is a programming error, although asking for a property
  844. // that doesn't exist is not.
  845. if (propertyName == null) {
  846. throw new IllegalArgumentException("Can't find readability status for null property");
  847. }
  848. try {
  849. PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
  850. return (pd != null && pd.getReadMethod() != null);
  851. }
  852. catch (InvalidPropertyException ex) {
  853. // doesn't exist, so can't be readable
  854. return false;
  855. }
  856. }
  857. public boolean isWritableProperty(String propertyName) {
  858. // This is a programming error, although asking for a property
  859. // that doesn't exist is not.
  860. if (propertyName == null) {
  861. throw new IllegalArgumentException("Can't find writability status for null property");
  862. }
  863. try {
  864. PropertyDescriptor pd = getPropertyDescriptorInternal(propertyName);
  865. return (pd != null && pd.getWriteMethod() != null);
  866. }
  867. catch (InvalidPropertyException ex) {
  868. // doesn't exist, so can't be writable
  869. return false;
  870. }
  871. }
  872. //---------------------------------------------------------------------
  873. // Diagnostics
  874. //---------------------------------------------------------------------
  875. /**
  876. * This method is expensive! Only call for diagnostics and debugging reasons,
  877. * not in production.
  878. * @return a string describing the state of this object
  879. */
  880. public String toString() {
  881. StringBuffer sb = new StringBuffer();
  882. try {
  883. sb.append("BeanWrapperImpl:"
  884. + " wrapping class [" + getWrappedInstance().getClass().getName() + "]; ");
  885. PropertyDescriptor pds[] = getPropertyDescriptors();
  886. if (pds != null) {
  887. for (int i = 0; i < pds.length; i++) {
  888. Object val = getPropertyValue(pds[i].getName());
  889. String valStr = (val != null) ? val.toString() : "null";
  890. sb.append(pds[i].getName() + "={" + valStr + "}");
  891. }
  892. }
  893. }
  894. catch (Exception ex) {
  895. sb.append("exception encountered: " + ex);
  896. }
  897. return sb.toString();
  898. }
  899. }