1. /*
  2. * Copyright 2001-2004 The Apache Software Foundation
  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.apache.commons.beanutils;
  17. import java.beans.BeanInfo;
  18. import java.beans.IntrospectionException;
  19. import java.beans.Introspector;
  20. import java.beans.PropertyDescriptor;
  21. import java.lang.reflect.Constructor;
  22. import java.lang.reflect.InvocationTargetException;
  23. import java.lang.reflect.Method;
  24. import java.util.AbstractMap;
  25. import java.util.AbstractSet;
  26. import java.util.ArrayList;
  27. import java.util.Collection;
  28. import java.util.HashMap;
  29. import java.util.Iterator;
  30. import java.util.Set;
  31. import org.apache.commons.collections.list.UnmodifiableList;
  32. import org.apache.commons.collections.keyvalue.AbstractMapEntry;
  33. import org.apache.commons.collections.set.UnmodifiableSet;
  34. import org.apache.commons.collections.Transformer;
  35. /**
  36. * An implementation of Map for JavaBeans which uses introspection to
  37. * get and put properties in the bean.
  38. * <p>
  39. * If an exception occurs during attempts to get or set a property then the
  40. * property is considered non existent in the Map
  41. *
  42. * @version $Revision: 1.2.2.2 $ $Date: 2004/06/22 21:07:02 $
  43. *
  44. * @author James Strachan
  45. * @author Stephen Colebourne
  46. */
  47. public class BeanMap extends AbstractMap implements Cloneable {
  48. private transient Object bean;
  49. private transient HashMap readMethods = new HashMap();
  50. private transient HashMap writeMethods = new HashMap();
  51. private transient HashMap types = new HashMap();
  52. /**
  53. * An empty array. Used to invoke accessors via reflection.
  54. */
  55. public static final Object[] NULL_ARGUMENTS = {};
  56. /**
  57. * Maps primitive Class types to transformers. The transformer
  58. * transform strings into the appropriate primitive wrapper.
  59. */
  60. public static HashMap defaultTransformers = new HashMap();
  61. static {
  62. defaultTransformers.put(
  63. Boolean.TYPE,
  64. new Transformer() {
  65. public Object transform( Object input ) {
  66. return Boolean.valueOf( input.toString() );
  67. }
  68. }
  69. );
  70. defaultTransformers.put(
  71. Character.TYPE,
  72. new Transformer() {
  73. public Object transform( Object input ) {
  74. return new Character( input.toString().charAt( 0 ) );
  75. }
  76. }
  77. );
  78. defaultTransformers.put(
  79. Byte.TYPE,
  80. new Transformer() {
  81. public Object transform( Object input ) {
  82. return Byte.valueOf( input.toString() );
  83. }
  84. }
  85. );
  86. defaultTransformers.put(
  87. Short.TYPE,
  88. new Transformer() {
  89. public Object transform( Object input ) {
  90. return Short.valueOf( input.toString() );
  91. }
  92. }
  93. );
  94. defaultTransformers.put(
  95. Integer.TYPE,
  96. new Transformer() {
  97. public Object transform( Object input ) {
  98. return Integer.valueOf( input.toString() );
  99. }
  100. }
  101. );
  102. defaultTransformers.put(
  103. Long.TYPE,
  104. new Transformer() {
  105. public Object transform( Object input ) {
  106. return Long.valueOf( input.toString() );
  107. }
  108. }
  109. );
  110. defaultTransformers.put(
  111. Float.TYPE,
  112. new Transformer() {
  113. public Object transform( Object input ) {
  114. return Float.valueOf( input.toString() );
  115. }
  116. }
  117. );
  118. defaultTransformers.put(
  119. Double.TYPE,
  120. new Transformer() {
  121. public Object transform( Object input ) {
  122. return Double.valueOf( input.toString() );
  123. }
  124. }
  125. );
  126. }
  127. // Constructors
  128. //-------------------------------------------------------------------------
  129. /**
  130. * Constructs a new empty <code>BeanMap</code>.
  131. */
  132. public BeanMap() {
  133. }
  134. /**
  135. * Constructs a new <code>BeanMap</code> that operates on the
  136. * specified bean. If the given bean is <code>null</code>, then
  137. * this map will be empty.
  138. *
  139. * @param bean the bean for this map to operate on
  140. */
  141. public BeanMap(Object bean) {
  142. this.bean = bean;
  143. initialise();
  144. }
  145. // Map interface
  146. //-------------------------------------------------------------------------
  147. /**
  148. * Renders a string representation of this object.
  149. * @return a <code>String</code> representation of this object
  150. */
  151. public String toString() {
  152. return "BeanMap<" + String.valueOf(bean) + ">";
  153. }
  154. /**
  155. * Clone this bean map using the following process:
  156. *
  157. * <ul>
  158. * <li>If there is no underlying bean, return a cloned BeanMap without a
  159. * bean.
  160. *
  161. * <li>Since there is an underlying bean, try to instantiate a new bean of
  162. * the same type using Class.newInstance().
  163. *
  164. * <li>If the instantiation fails, throw a CloneNotSupportedException
  165. *
  166. * <li>Clone the bean map and set the newly instantiated bean as the
  167. * underlying bean for the bean map.
  168. *
  169. * <li>Copy each property that is both readable and writable from the
  170. * existing object to a cloned bean map.
  171. *
  172. * <li>If anything fails along the way, throw a
  173. * CloneNotSupportedException.
  174. *
  175. * <ul>
  176. */
  177. public Object clone() throws CloneNotSupportedException {
  178. BeanMap newMap = (BeanMap)super.clone();
  179. if(bean == null) {
  180. // no bean, just an empty bean map at the moment. return a newly
  181. // cloned and empty bean map.
  182. return newMap;
  183. }
  184. Object newBean = null;
  185. Class beanClass = null;
  186. try {
  187. beanClass = bean.getClass();
  188. newBean = beanClass.newInstance();
  189. } catch (Exception e) {
  190. // unable to instantiate
  191. throw new CloneNotSupportedException
  192. ("Unable to instantiate the underlying bean \"" +
  193. beanClass.getName() + "\": " + e);
  194. }
  195. try {
  196. newMap.setBean(newBean);
  197. } catch (Exception exception) {
  198. throw new CloneNotSupportedException
  199. ("Unable to set bean in the cloned bean map: " +
  200. exception);
  201. }
  202. try {
  203. // copy only properties that are readable and writable. If its
  204. // not readable, we can't get the value from the old map. If
  205. // its not writable, we can't write a value into the new map.
  206. Iterator readableKeys = readMethods.keySet().iterator();
  207. while(readableKeys.hasNext()) {
  208. Object key = readableKeys.next();
  209. if(getWriteMethod(key) != null) {
  210. newMap.put(key, get(key));
  211. }
  212. }
  213. } catch (Exception exception) {
  214. throw new CloneNotSupportedException
  215. ("Unable to copy bean values to cloned bean map: " +
  216. exception);
  217. }
  218. return newMap;
  219. }
  220. /**
  221. * Puts all of the writable properties from the given BeanMap into this
  222. * BeanMap. Read-only and Write-only properties will be ignored.
  223. *
  224. * @param map the BeanMap whose properties to put
  225. */
  226. public void putAllWriteable(BeanMap map) {
  227. Iterator readableKeys = map.readMethods.keySet().iterator();
  228. while (readableKeys.hasNext()) {
  229. Object key = readableKeys.next();
  230. if (getWriteMethod(key) != null) {
  231. this.put(key, map.get(key));
  232. }
  233. }
  234. }
  235. /**
  236. * This method reinitializes the bean map to have default values for the
  237. * bean's properties. This is accomplished by constructing a new instance
  238. * of the bean which the map uses as its underlying data source. This
  239. * behavior for <code>clear()</code> differs from the Map contract in that
  240. * the mappings are not actually removed from the map (the mappings for a
  241. * BeanMap are fixed).
  242. */
  243. public void clear() {
  244. if(bean == null) return;
  245. Class beanClass = null;
  246. try {
  247. beanClass = bean.getClass();
  248. bean = beanClass.newInstance();
  249. }
  250. catch (Exception e) {
  251. throw new UnsupportedOperationException( "Could not create new instance of class: " + beanClass );
  252. }
  253. }
  254. /**
  255. * Returns true if the bean defines a property with the given name.
  256. * <p>
  257. * The given name must be a <code>String</code> if not, this method
  258. * returns false. This method will also return false if the bean
  259. * does not define a property with that name.
  260. * <p>
  261. * Write-only properties will not be matched as the test operates against
  262. * property read methods.
  263. *
  264. * @param name the name of the property to check
  265. * @return false if the given name is null or is not a <code>String</code>
  266. * false if the bean does not define a property with that name; or
  267. * true if the bean does define a property with that name
  268. */
  269. public boolean containsKey(Object name) {
  270. Method method = getReadMethod(name);
  271. return method != null;
  272. }
  273. /**
  274. * Returns true if the bean defines a property whose current value is
  275. * the given object.
  276. *
  277. * @param value the value to check
  278. * @return false true if the bean has at least one property whose
  279. * current value is that object, false otherwise
  280. */
  281. public boolean containsValue(Object value) {
  282. // use default implementation
  283. return super.containsValue(value);
  284. }
  285. /**
  286. * Returns the value of the bean's property with the given name.
  287. * <p>
  288. * The given name must be a {@link String} and must not be
  289. * null; otherwise, this method returns <code>null</code>.
  290. * If the bean defines a property with the given name, the value of
  291. * that property is returned. Otherwise, <code>null</code> is
  292. * returned.
  293. * <p>
  294. * Write-only properties will not be matched as the test operates against
  295. * property read methods.
  296. *
  297. * @param name the name of the property whose value to return
  298. * @return the value of the property with that name
  299. */
  300. public Object get(Object name) {
  301. if ( bean != null ) {
  302. Method method = getReadMethod( name );
  303. if ( method != null ) {
  304. try {
  305. return method.invoke( bean, NULL_ARGUMENTS );
  306. }
  307. catch ( IllegalAccessException e ) {
  308. logWarn( e );
  309. }
  310. catch ( IllegalArgumentException e ) {
  311. logWarn( e );
  312. }
  313. catch ( InvocationTargetException e ) {
  314. logWarn( e );
  315. }
  316. catch ( NullPointerException e ) {
  317. logWarn( e );
  318. }
  319. }
  320. }
  321. return null;
  322. }
  323. /**
  324. * Sets the bean property with the given name to the given value.
  325. *
  326. * @param name the name of the property to set
  327. * @param value the value to set that property to
  328. * @return the previous value of that property
  329. * @throws IllegalArgumentException if the given name is null;
  330. * if the given name is not a {@link String}; if the bean doesn't
  331. * define a property with that name; or if the bean property with
  332. * that name is read-only
  333. */
  334. public Object put(Object name, Object value) throws IllegalArgumentException, ClassCastException {
  335. if ( bean != null ) {
  336. Object oldValue = get( name );
  337. Method method = getWriteMethod( name );
  338. if ( method == null ) {
  339. throw new IllegalArgumentException( "The bean of type: "+ bean.getClass().getName() + " has no property called: " + name );
  340. }
  341. try {
  342. Object[] arguments = createWriteMethodArguments( method, value );
  343. method.invoke( bean, arguments );
  344. Object newValue = get( name );
  345. firePropertyChange( name, oldValue, newValue );
  346. }
  347. catch ( InvocationTargetException e ) {
  348. logInfo( e );
  349. throw new IllegalArgumentException( e.getMessage() );
  350. }
  351. catch ( IllegalAccessException e ) {
  352. logInfo( e );
  353. throw new IllegalArgumentException( e.getMessage() );
  354. }
  355. return oldValue;
  356. }
  357. return null;
  358. }
  359. /**
  360. * Returns the number of properties defined by the bean.
  361. *
  362. * @return the number of properties defined by the bean
  363. */
  364. public int size() {
  365. return readMethods.size();
  366. }
  367. /**
  368. * Get the keys for this BeanMap.
  369. * <p>
  370. * Write-only properties are <b>not</b> included in the returned set of
  371. * property names, although it is possible to set their value and to get
  372. * their type.
  373. *
  374. * @return BeanMap keys. The Set returned by this method is not
  375. * modifiable.
  376. */
  377. public Set keySet() {
  378. return UnmodifiableSet.decorate(readMethods.keySet());
  379. }
  380. /**
  381. * Gets a Set of MapEntry objects that are the mappings for this BeanMap.
  382. * <p>
  383. * Each MapEntry can be set but not removed.
  384. *
  385. * @return the unmodifiable set of mappings
  386. */
  387. public Set entrySet() {
  388. return UnmodifiableSet.decorate(new AbstractSet() {
  389. public Iterator iterator() {
  390. return entryIterator();
  391. }
  392. public int size() {
  393. return BeanMap.this.readMethods.size();
  394. }
  395. });
  396. }
  397. /**
  398. * Returns the values for the BeanMap.
  399. *
  400. * @return values for the BeanMap. The returned collection is not
  401. * modifiable.
  402. */
  403. public Collection values() {
  404. ArrayList answer = new ArrayList( readMethods.size() );
  405. for ( Iterator iter = valueIterator(); iter.hasNext(); ) {
  406. answer.add( iter.next() );
  407. }
  408. return UnmodifiableList.decorate(answer);
  409. }
  410. // Helper methods
  411. //-------------------------------------------------------------------------
  412. /**
  413. * Returns the type of the property with the given name.
  414. *
  415. * @param name the name of the property
  416. * @return the type of the property, or <code>null</code> if no such
  417. * property exists
  418. */
  419. public Class getType(String name) {
  420. return (Class) types.get( name );
  421. }
  422. /**
  423. * Convenience method for getting an iterator over the keys.
  424. * <p>
  425. * Write-only properties will not be returned in the iterator.
  426. *
  427. * @return an iterator over the keys
  428. */
  429. public Iterator keyIterator() {
  430. return readMethods.keySet().iterator();
  431. }
  432. /**
  433. * Convenience method for getting an iterator over the values.
  434. *
  435. * @return an iterator over the values
  436. */
  437. public Iterator valueIterator() {
  438. final Iterator iter = keyIterator();
  439. return new Iterator() {
  440. public boolean hasNext() {
  441. return iter.hasNext();
  442. }
  443. public Object next() {
  444. Object key = iter.next();
  445. return get(key);
  446. }
  447. public void remove() {
  448. throw new UnsupportedOperationException( "remove() not supported for BeanMap" );
  449. }
  450. };
  451. }
  452. /**
  453. * Convenience method for getting an iterator over the entries.
  454. *
  455. * @return an iterator over the entries
  456. */
  457. public Iterator entryIterator() {
  458. final Iterator iter = keyIterator();
  459. return new Iterator() {
  460. public boolean hasNext() {
  461. return iter.hasNext();
  462. }
  463. public Object next() {
  464. Object key = iter.next();
  465. Object value = get(key);
  466. return new Entry( BeanMap.this, key, value );
  467. }
  468. public void remove() {
  469. throw new UnsupportedOperationException( "remove() not supported for BeanMap" );
  470. }
  471. };
  472. }
  473. // Properties
  474. //-------------------------------------------------------------------------
  475. /**
  476. * Returns the bean currently being operated on. The return value may
  477. * be null if this map is empty.
  478. *
  479. * @return the bean being operated on by this map
  480. */
  481. public Object getBean() {
  482. return bean;
  483. }
  484. /**
  485. * Sets the bean to be operated on by this map. The given value may
  486. * be null, in which case this map will be empty.
  487. *
  488. * @param newBean the new bean to operate on
  489. */
  490. public void setBean( Object newBean ) {
  491. bean = newBean;
  492. reinitialise();
  493. }
  494. /**
  495. * Returns the accessor for the property with the given name.
  496. *
  497. * @param name the name of the property
  498. * @return the accessor method for the property, or null
  499. */
  500. public Method getReadMethod(String name) {
  501. return (Method) readMethods.get(name);
  502. }
  503. /**
  504. * Returns the mutator for the property with the given name.
  505. *
  506. * @param name the name of the property
  507. * @return the mutator method for the property, or null
  508. */
  509. public Method getWriteMethod(String name) {
  510. return (Method) writeMethods.get(name);
  511. }
  512. // Implementation methods
  513. //-------------------------------------------------------------------------
  514. /**
  515. * Returns the accessor for the property with the given name.
  516. *
  517. * @param name the name of the property
  518. * @return null if the name is null; null if the name is not a
  519. * {@link String}; null if no such property exists; or the accessor
  520. * method for that property
  521. */
  522. protected Method getReadMethod( Object name ) {
  523. return (Method) readMethods.get( name );
  524. }
  525. /**
  526. * Returns the mutator for the property with the given name.
  527. *
  528. * @param name the name of the
  529. * @return null if the name is null; null if the name is not a
  530. * {@link String}; null if no such property exists; null if the
  531. * property is read-only; or the mutator method for that property
  532. */
  533. protected Method getWriteMethod( Object name ) {
  534. return (Method) writeMethods.get( name );
  535. }
  536. /**
  537. * Reinitializes this bean. Called during {@link #setBean(Object)}.
  538. * Does introspection to find properties.
  539. */
  540. protected void reinitialise() {
  541. readMethods.clear();
  542. writeMethods.clear();
  543. types.clear();
  544. initialise();
  545. }
  546. private void initialise() {
  547. if(getBean() == null) return;
  548. Class beanClass = getBean().getClass();
  549. try {
  550. //BeanInfo beanInfo = Introspector.getBeanInfo( bean, null );
  551. BeanInfo beanInfo = Introspector.getBeanInfo( beanClass );
  552. PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
  553. if ( propertyDescriptors != null ) {
  554. for ( int i = 0; i < propertyDescriptors.length; i++ ) {
  555. PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
  556. if ( propertyDescriptor != null ) {
  557. String name = propertyDescriptor.getName();
  558. Method readMethod = propertyDescriptor.getReadMethod();
  559. Method writeMethod = propertyDescriptor.getWriteMethod();
  560. Class aType = propertyDescriptor.getPropertyType();
  561. if ( readMethod != null ) {
  562. readMethods.put( name, readMethod );
  563. }
  564. if ( writeMethods != null ) {
  565. writeMethods.put( name, writeMethod );
  566. }
  567. types.put( name, aType );
  568. }
  569. }
  570. }
  571. }
  572. catch ( IntrospectionException e ) {
  573. logWarn( e );
  574. }
  575. }
  576. /**
  577. * Called during a successful {@link #put(Object,Object)} operation.
  578. * Default implementation does nothing. Override to be notified of
  579. * property changes in the bean caused by this map.
  580. *
  581. * @param key the name of the property that changed
  582. * @param oldValue the old value for that property
  583. * @param newValue the new value for that property
  584. */
  585. protected void firePropertyChange( Object key, Object oldValue, Object newValue ) {
  586. }
  587. // Implementation classes
  588. //-------------------------------------------------------------------------
  589. /**
  590. * Map entry used by {@link BeanMap}.
  591. */
  592. protected static class Entry extends AbstractMapEntry {
  593. private BeanMap owner;
  594. /**
  595. * Constructs a new <code>Entry</code>.
  596. *
  597. * @param owner the BeanMap this entry belongs to
  598. * @param key the key for this entry
  599. * @param value the value for this entry
  600. */
  601. protected Entry( BeanMap owner, Object key, Object value ) {
  602. super( key, value );
  603. this.owner = owner;
  604. }
  605. /**
  606. * Sets the value.
  607. *
  608. * @param value the new value for the entry
  609. * @return the old value for the entry
  610. */
  611. public Object setValue(Object value) {
  612. Object key = getKey();
  613. Object oldValue = owner.get( key );
  614. owner.put( key, value );
  615. Object newValue = owner.get( key );
  616. super.setValue( newValue );
  617. return oldValue;
  618. }
  619. }
  620. /**
  621. * Creates an array of parameters to pass to the given mutator method.
  622. * If the given object is not the right type to pass to the method
  623. * directly, it will be converted using {@link #convertType(Class,Object)}.
  624. *
  625. * @param method the mutator method
  626. * @param value the value to pass to the mutator method
  627. * @return an array containing one object that is either the given value
  628. * or a transformed value
  629. * @throws IllegalAccessException if {@link #convertType(Class,Object)}
  630. * raises it
  631. * @throws IllegalArgumentException if any other exception is raised
  632. * by {@link #convertType(Class,Object)}
  633. */
  634. protected Object[] createWriteMethodArguments( Method method, Object value ) throws IllegalAccessException, ClassCastException {
  635. try {
  636. if ( value != null ) {
  637. Class[] types = method.getParameterTypes();
  638. if ( types != null && types.length > 0 ) {
  639. Class paramType = types[0];
  640. if ( ! paramType.isAssignableFrom( value.getClass() ) ) {
  641. value = convertType( paramType, value );
  642. }
  643. }
  644. }
  645. Object[] answer = { value };
  646. return answer;
  647. }
  648. catch ( InvocationTargetException e ) {
  649. logInfo( e );
  650. throw new IllegalArgumentException( e.getMessage() );
  651. }
  652. catch ( InstantiationException e ) {
  653. logInfo( e );
  654. throw new IllegalArgumentException( e.getMessage() );
  655. }
  656. }
  657. /**
  658. * Converts the given value to the given type. First, reflection is
  659. * is used to find a public constructor declared by the given class
  660. * that takes one argument, which must be the precise type of the
  661. * given value. If such a constructor is found, a new object is
  662. * created by passing the given value to that constructor, and the
  663. * newly constructed object is returned.<P>
  664. *
  665. * If no such constructor exists, and the given type is a primitive
  666. * type, then the given value is converted to a string using its
  667. * {@link Object#toString() toString()} method, and that string is
  668. * parsed into the correct primitive type using, for instance,
  669. * {@link Integer#valueOf(String)} to convert the string into an
  670. * <code>int</code>.<P>
  671. *
  672. * If no special constructor exists and the given type is not a
  673. * primitive type, this method returns the original value.
  674. *
  675. * @param newType the type to convert the value to
  676. * @param value the value to convert
  677. * @return the converted value
  678. * @throws NumberFormatException if newType is a primitive type, and
  679. * the string representation of the given value cannot be converted
  680. * to that type
  681. * @throws InstantiationException if the constructor found with
  682. * reflection raises it
  683. * @throws InvocationTargetException if the constructor found with
  684. * reflection raises it
  685. * @throws IllegalAccessException never
  686. * @throws IllegalArgumentException never
  687. */
  688. protected Object convertType( Class newType, Object value )
  689. throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
  690. // try call constructor
  691. Class[] types = { value.getClass() };
  692. try {
  693. Constructor constructor = newType.getConstructor( types );
  694. Object[] arguments = { value };
  695. return constructor.newInstance( arguments );
  696. }
  697. catch ( NoSuchMethodException e ) {
  698. // try using the transformers
  699. Transformer transformer = getTypeTransformer( newType );
  700. if ( transformer != null ) {
  701. return transformer.transform( value );
  702. }
  703. return value;
  704. }
  705. }
  706. /**
  707. * Returns a transformer for the given primitive type.
  708. *
  709. * @param aType the primitive type whose transformer to return
  710. * @return a transformer that will convert strings into that type,
  711. * or null if the given type is not a primitive type
  712. */
  713. protected Transformer getTypeTransformer( Class aType ) {
  714. return (Transformer) defaultTransformers.get( aType );
  715. }
  716. /**
  717. * Logs the given exception to <code>System.out</code>. Used to display
  718. * warnings while accessing/mutating the bean.
  719. *
  720. * @param ex the exception to log
  721. */
  722. protected void logInfo(Exception ex) {
  723. // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
  724. System.out.println( "INFO: Exception: " + ex );
  725. }
  726. /**
  727. * Logs the given exception to <code>System.err</code>. Used to display
  728. * errors while accessing/mutating the bean.
  729. *
  730. * @param ex the exception to log
  731. */
  732. protected void logWarn(Exception ex) {
  733. // Deliberately do not use LOG4J or Commons Logging to avoid dependencies
  734. System.out.println( "WARN: Exception: " + ex );
  735. ex.printStackTrace();
  736. }
  737. }