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