package ilarkesto.base; import ilarkesto.core.logging.Log; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * The BeanMap is a full Map implementation where a java object (bean) acts as the data storage. A call to the * get(key)-method invokes the getter in the bean and the set(key, value)-method invokes the setter. */ public class BeanMap<T> extends AbstractMap<String, Object> implements Cloneable { private transient T bean; private transient HashMap<String, Method> readMethods = new HashMap<String, Method>(); private transient HashMap<String, Method> writeMethods = new HashMap<String, Method>(); private transient HashMap<String, Class> types = new HashMap<String, Class>(); /** * An empty array. Used to invoke accessors via reflection. */ public static final Object[] NULL_ARGUMENTS = {}; /** * Maps primitive Class types to transformers. The transformer transform strings into the appropriate * primitive wrapper. */ public static HashMap defaultTransformers = new HashMap(); static { defaultTransformers.put(Boolean.TYPE, new Transformer() { public Object transform(Object input) { return Boolean.valueOf(input.toString()); } }); defaultTransformers.put(Character.TYPE, new Transformer() { public Object transform(Object input) { return new Character(input.toString().charAt(0)); } }); defaultTransformers.put(Byte.TYPE, new Transformer() { public Object transform(Object input) { return Byte.valueOf(input.toString()); } }); defaultTransformers.put(Short.TYPE, new Transformer() { public Object transform(Object input) { return Short.valueOf(input.toString()); } }); defaultTransformers.put(Integer.TYPE, new Transformer() { public Object transform(Object input) { return Integer.valueOf(input.toString()); } }); defaultTransformers.put(Long.TYPE, new Transformer() { public Object transform(Object input) { return Long.valueOf(input.toString()); } }); defaultTransformers.put(Float.TYPE, new Transformer() { public Object transform(Object input) { return Float.valueOf(input.toString()); } }); defaultTransformers.put(Double.TYPE, new Transformer() { public Object transform(Object input) { return Double.valueOf(input.toString()); } }); } // Constructors // ------------------------------------------------------------------------- /** * Constructs a new empty <code>BeanMap</code>. */ public BeanMap() {} /** * Constructs a new <code>BeanMap</code> that operates on the specified bean. If the given bean is * <code>null</code>, then this map will be empty. * * @param bean the bean for this map to operate on */ public BeanMap(T bean) { this.bean = bean; initialise(); } // Map interface // ------------------------------------------------------------------------- @Override public String toString() { return "BeanMap<" + String.valueOf(bean) + ">"; } /** * Clone this bean map using the following process: * <ul> * <li>If there is no underlying bean, return a cloned BeanMap without a bean. * <li>Since there is an underlying bean, try to instantiate a new bean of the same type using * Class.newInstance(). * <li>If the instantiation fails, throw a CloneNotSupportedException * <li>Clone the bean map and set the newly instantiated bean as the underlying bean for the bean map. * <li>Copy each property that is both readable and writable from the existing object to a cloned bean * map. * <li>If anything fails along the way, throw a CloneNotSupportedException. * <ul> */ @Override public Object clone() throws CloneNotSupportedException { BeanMap newMap = (BeanMap) super.clone(); if (bean == null) { // no bean, just an empty bean map at the moment. return a newly // cloned and empty bean map. return newMap; } Object newBean = null; Class beanClass = null; try { beanClass = bean.getClass(); newBean = beanClass.newInstance(); } catch (Exception e) { // unable to instantiate throw new CloneNotSupportedException("Unable to instantiate the underlying bean \"" + beanClass.getName() + "\": " + e); } try { newMap.setBean(newBean); } catch (Exception exception) { throw new CloneNotSupportedException("Unable to set bean in the cloned bean map: " + exception); } try { // copy only properties that are readable and writable. If its // not readable, we can't get the value from the old map. If // its not writable, we can't write a value into the new map. Iterator<String> readableKeys = readMethods.keySet().iterator(); while (readableKeys.hasNext()) { String key = readableKeys.next(); if (getWriteMethod(key) != null) { newMap.put(key, get(key)); } } } catch (Exception exception) { throw new CloneNotSupportedException("Unable to copy bean values to cloned bean map: " + exception); } return newMap; } /** * Puts all of the writable properties from the given BeanMap into this BeanMap. Read-only and Write-only * properties will be ignored. * * @param map the BeanMap whose properties to put */ public void putAllWriteable(BeanMap map) { Iterator<String> readableKeys = map.readMethods.keySet().iterator(); while (readableKeys.hasNext()) { String key = readableKeys.next(); if (getWriteMethod(key) != null) { this.put(key, map.get(key)); } } } /** * Returns true if the bean defines a property with the given name. * <p> * The given name must be a <code>String</code>; if not, this method returns false. This method will also * return false if the bean does not define a property with that name. * <p> * Write-only properties will not be matched as the test operates against property read methods. * * @param name the name of the property to check * @return false if the given name is null or is not a <code>String</code>; false if the bean does not * define a property with that name; or true if the bean does define a property with that name */ @Override public boolean containsKey(Object name) { Method method = getReadMethod(name); return method != null; } /** * Returns true if the bean defines a property whose current value is the given object. * * @param value the value to check * @return false true if the bean has at least one property whose current value is that object, false * otherwise */ @Override public boolean containsValue(Object value) { // use default implementation return super.containsValue(value); } /** * Returns the value of the bean's property with the given name. * <p> * The given name must be a {@link String} and must not be null; otherwise, this method returns * <code>null</code>. If the bean defines a property with the given name, the value of that property is * returned. Otherwise, <code>null</code> is returned. * <p> * Write-only properties will not be matched as the test operates against property read methods. * * @param name the name of the property whose value to return * @return the value of the property with that name */ @Override public Object get(Object name) { if (bean != null) { Method method = getReadMethod(name); if (method != null) { try { return method.invoke(bean, NULL_ARGUMENTS); } catch (Exception e) { throw new RuntimeException("Invoking " + bean.getClass().getSimpleName() + "." + method.getName() + "() failed", e); } } } return null; } /** * Sets the bean property with the given name to the given value. * * @param name the name of the property to set * @param value the value to set that property to * @return the previous value of that property * @throws IllegalArgumentException if the given name is null; if the given name is not a {@link String}; * if the bean doesn't define a property with that name; or if the bean property with that * name is read-only */ @Override public Object put(String name, Object value) throws IllegalArgumentException, ClassCastException { Log.DEBUG("------------- setting property ", name, "->", Utl.toStringWithType(value)); if (bean != null) { Object oldValue = get(name); Reflect.setProperty(bean, name, value); return oldValue; } return null; } /** * Returns the number of properties defined by the bean. * * @return the number of properties defined by the bean */ @Override public int size() { return readMethods.size(); } /** * Get the keys for this BeanMap. * <p> * Write-only properties are <b>not</b> included in the returned set of property names, although it is * possible to set their value and to get their type. * * @return BeanMap keys. The Set returned by this method is not modifiable. */ @Override public Set<String> keySet() { return readMethods.keySet(); } /** * Gets a Set of MapEntry objects that are the mappings for this BeanMap. * <p> * Each MapEntry can be set but not removed. * * @return the unmodifiable set of mappings */ @Override public Set entrySet() { return new AbstractSet() { @Override public Iterator iterator() { return entryIterator(); } @Override public int size() { return BeanMap.this.readMethods.size(); } }; } /** * Returns the values for the BeanMap. * * @return values for the BeanMap. The returned collection is not modifiable. */ @Override public Collection values() { ArrayList answer = new ArrayList(readMethods.size()); for (Iterator iter = valueIterator(); iter.hasNext();) { answer.add(iter.next()); } return answer; } // Helper methods // ------------------------------------------------------------------------- /** * Returns the type of the property with the given name. * * @param name the name of the property * @return the type of the property, or <code>null</code> if no such property exists */ public Class getType(String name) { return types.get(name); } /** * Convenience method for getting an iterator over the keys. * <p> * Write-only properties will not be returned in the iterator. * * @return an iterator over the keys */ public Iterator keyIterator() { return readMethods.keySet().iterator(); } /** * Convenience method for getting an iterator over the values. * * @return an iterator over the values */ public Iterator valueIterator() { final Iterator iter = keyIterator(); return new Iterator() { public boolean hasNext() { return iter.hasNext(); } public Object next() { Object key = iter.next(); return get(key); } public void remove() { throw new UnsupportedOperationException("remove() not supported for BeanMap"); } }; } /** * Convenience method for getting an iterator over the entries. * * @return an iterator over the entries */ public Iterator entryIterator() { final Iterator iter = keyIterator(); return new Iterator() { public boolean hasNext() { return iter.hasNext(); } public Object next() { Object key = iter.next(); Object value = get(key); return new MyMapEntry(BeanMap.this, key, value); } public void remove() { throw new UnsupportedOperationException("remove() not supported for BeanMap"); } }; } // Properties // ------------------------------------------------------------------------- /** * Returns the bean currently being operated on. The return value may be null if this map is empty. * * @return the bean being operated on by this map */ public T getBean() { return bean; } /** * Sets the bean to be operated on by this map. The given value may be null, in which case this map will * be empty. * * @param newBean the new bean to operate on */ public void setBean(T newBean) { bean = newBean; reinitialise(); } /** * Returns the accessor for the property with the given name. * * @param name the name of the property * @return the accessor method for the property, or null */ public Method getReadMethod(String name) { return readMethods.get(name); } /** * Returns the mutator for the property with the given name. * * @param name the name of the property * @return the mutator method for the property, or null */ public Method getWriteMethod(String name) { return writeMethods.get(name); } // Implementation methods // ------------------------------------------------------------------------- /** * Returns the accessor for the property with the given name. * * @param name the name of the property * @return null if the name is null; null if the name is not a {@link String}; null if no such property * exists; or the accessor method for that property */ protected Method getReadMethod(Object name) { return readMethods.get(name); } /** * Returns the mutator for the property with the given name. * * @param name the name of the * @return null if the name is null; null if the name is not a {@link String}; null if no such property * exists; null if the property is read-only; or the mutator method for that property */ protected Method getWriteMethod(Object name) { return writeMethods.get(name); } /** * Reinitializes this bean. Called during {@link #setBean(Object)}. Does introspection to find properties. */ protected void reinitialise() { readMethods.clear(); writeMethods.clear(); types.clear(); initialise(); } private void initialise() { if (getBean() == null) return; Class beanClass = getBean().getClass(); try { // BeanInfo beanInfo = Introspector.getBeanInfo( bean, null ); BeanInfo beanInfo = Introspector.getBeanInfo(beanClass); PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); if (propertyDescriptors != null) { for (int i = 0; i < propertyDescriptors.length; i++) { PropertyDescriptor propertyDescriptor = propertyDescriptors[i]; if (propertyDescriptor != null) { String name = propertyDescriptor.getName(); Method readMethod = propertyDescriptor.getReadMethod(); Method writeMethod = propertyDescriptor.getWriteMethod(); Class aType = propertyDescriptor.getPropertyType(); if (readMethod != null) { readMethods.put(name, readMethod); } if (writeMethods != null) { writeMethods.put(name, writeMethod); } types.put(name, aType); } } } } catch (IntrospectionException e) { throw new RuntimeException(e); } } protected void firePropertyChange(Object key, Object oldValue, Object newValue) {} protected class MyMapEntry extends AbstractMapEntry { private BeanMap owner; protected MyMapEntry(BeanMap owner, Object key, Object value) { super(key, value); this.owner = owner; } @Override public Object setValue(Object value) { String key = (String) getKey(); Object oldValue = owner.get(key); owner.put(key, value); Object newValue = owner.get(key); super.setValue(newValue); return oldValue; } } protected Object[] createWriteMethodArguments(Method method, Object value) throws IllegalAccessException, ClassCastException { try { if (value != null) { Class[] types = method.getParameterTypes(); if (types != null && types.length > 0) { Class paramType = types[0]; if (!paramType.isAssignableFrom(value.getClass())) { value = convertType(paramType, value); } } } Object[] answer = { value }; return answer; } catch (InvocationTargetException e) { throw new IllegalArgumentException(e); } catch (InstantiationException e) { throw new IllegalArgumentException(e); } } protected Object convertType(Class newType, Object value) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { // try call constructor Class[] types = { value.getClass() }; try { Constructor constructor = newType.getConstructor(types); Object[] arguments = { value }; return constructor.newInstance(arguments); } catch (NoSuchMethodException e) { // try using the transformers Transformer transformer = getTypeTransformer(newType); if (transformer != null) { return transformer.transform(value); } return value; } } protected Transformer getTypeTransformer(Class aType) { return (Transformer) defaultTransformers.get(aType); } private static interface Transformer { public Object transform(Object input); } private abstract class AbstractMapEntry extends AbstractKeyValue implements Map.Entry { protected AbstractMapEntry(Object key, Object value) { super(key, value); } public Object setValue(Object value) { Object answer = this.value; this.value = value; return answer; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof Map.Entry == false) { return false; } Map.Entry other = (Map.Entry) obj; return (getKey() == null ? other.getKey() == null : getKey().equals(other.getKey())) && (getValue() == null ? other.getValue() == null : getValue().equals(other.getValue())); } @Override public int hashCode() { return (getKey() == null ? 0 : getKey().hashCode()) ^ (getValue() == null ? 0 : getValue().hashCode()); } } public abstract class AbstractKeyValue { protected Object key; protected Object value; protected AbstractKeyValue(Object key, Object value) { super(); this.key = key; this.value = value; } public Object getKey() { return key; } public Object getValue() { return value; } @Override public String toString() { return new StringBuffer().append(getKey()).append('=').append(getValue()).toString(); } } }