/* * Copyright 2007-2010 Brian S O'Neill * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.cojen.util; import java.lang.ref.SoftReference; import java.lang.reflect.Method; import java.util.AbstractCollection; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; /** * Provides a simple and efficient means of reading and writing bean properties * via a map. Properties which declare throwing checked exceptions are * excluded as are properties which are read-only or write-only. * * @author Brian S O'Neill * @see BeanPropertyAccessor * @since 2.1 */ public abstract class BeanPropertyMapFactory<B> { private static final Map<Class, SoftReference<BeanPropertyMapFactory>> cFactories = new WeakIdentityMap<Class, SoftReference<BeanPropertyMapFactory>>(); /** * Returns a new or cached BeanPropertyMapFactory for the given class. */ public static <B> BeanPropertyMapFactory<B> forClass(Class<B> clazz) { synchronized (cFactories) { BeanPropertyMapFactory factory; SoftReference<BeanPropertyMapFactory> ref = cFactories.get(clazz); if (ref != null) { factory = ref.get(); if (factory != null) { return factory; } } final Map<String, BeanProperty> properties = BeanIntrospector.getAllProperties(clazz); Map<String, BeanProperty> supportedProperties = properties; // Determine which properties are to be excluded. for (Map.Entry<String, BeanProperty> entry : properties.entrySet()) { BeanProperty property = entry.getValue(); if (property.getReadMethod() == null || property.getWriteMethod() == null || BeanPropertyAccessor.throwsCheckedException(property.getReadMethod()) || BeanPropertyAccessor.throwsCheckedException(property.getWriteMethod())) { // Exclude property. if (supportedProperties == properties) { supportedProperties = new HashMap<String, BeanProperty>(properties); } supportedProperties.remove(entry.getKey()); } } if (supportedProperties.size() == 0) { factory = Empty.INSTANCE; } else { factory = new Standard<B> (BeanPropertyAccessor.forClass (clazz, BeanPropertyAccessor.PropertySet.READ_WRITE_UNCHECKED_EXCEPTIONS), supportedProperties); } cFactories.put(clazz, new SoftReference<BeanPropertyMapFactory>(factory)); return factory; } } /** * Returns a fixed-size map backed by the given bean. Map remove operations * are unsupported, as is access to excluded properties. * * @throws IllegalArgumentException if bean is null */ public static SortedMap<String, Object> asMap(Object bean) { if (bean == null) { throw new IllegalArgumentException(); } BeanPropertyMapFactory factory = forClass(bean.getClass()); return factory.createMap(bean); } /** * Returns a fixed-size map backed by the given bean. Map remove operations * are unsupported, as are put operations on non-existent properties. * * @throws IllegalArgumentException if bean is null */ public abstract SortedMap<String, Object> createMap(B bean); private static class Empty extends BeanPropertyMapFactory { static final Empty INSTANCE = new Empty(); static final SortedMap<String, Object> EMPTY_MAP = Collections.unmodifiableSortedMap(new TreeMap<String, Object>()); private Empty() { } public SortedMap<String, Object> createMap(Object bean) { if (bean == null) { throw new IllegalArgumentException(); } return EMPTY_MAP; } } private static class Standard<B> extends BeanPropertyMapFactory<B> { final BeanPropertyAccessor mAccessor; final SortedSet<String> mPropertyNames; public Standard(BeanPropertyAccessor<B> accessor, Map<String, BeanProperty> properties) { mAccessor = accessor; // Only reveal readable properties. SortedSet<String> propertyNames = new TreeSet<String>(); for (BeanProperty property : properties.values()) { if (property.getReadMethod() != null) { propertyNames.add(property.getName()); } } mPropertyNames = Collections.unmodifiableSortedSet(propertyNames); } public SortedMap<String, Object> createMap(B bean) { if (bean == null) { throw new IllegalArgumentException(); } return new BeanMap<B>(bean, mAccessor, mPropertyNames); } } private static class BeanMap<B> extends AbstractMap<String, Object> implements SortedMap<String, Object> { final B mBean; final BeanPropertyAccessor mAccessor; final SortedSet<String> mPropertyNames; BeanMap(B bean, BeanPropertyAccessor<B> accessor, SortedSet<String> propertyNames) { mBean = bean; mAccessor = accessor; mPropertyNames = propertyNames; } public Comparator<? super String> comparator() { return null; } public SortedMap<String, Object> subMap(String fromKey, String toKey) { return new SubMap<B>(mBean, mAccessor, mPropertyNames.subSet(fromKey, toKey), fromKey, toKey); } public SortedMap<String, Object> headMap(String toKey) { return new SubMap<B>(mBean, mAccessor, mPropertyNames.headSet(toKey), null, toKey); } public SortedMap<String, Object> tailMap(String fromKey) { return new SubMap<B>(mBean, mAccessor, mPropertyNames.tailSet(fromKey), fromKey, null); } public String firstKey() { return mPropertyNames.first(); } public String lastKey() { return mPropertyNames.last(); } @Override public int size() { return mPropertyNames.size(); } @Override public boolean isEmpty() { return false; } @Override public boolean containsKey(Object key) { return mAccessor.hasReadableProperty((String) key); } @Override public boolean containsValue(Object value) { return mAccessor.hasPropertyValue(mBean, value); } @Override public Object get(Object key) { return mAccessor.tryGetPropertyValue(mBean, (String) key); } @Override public Object put(String key, Object value) { Object old = mAccessor.tryGetPropertyValue(mBean, key); mAccessor.setPropertyValue(mBean, key, value); return old; } @Override public Object remove(Object key) { throw new UnsupportedOperationException(); } @Override public void clear() { throw new UnsupportedOperationException(); } @Override public Set<String> keySet() { return mPropertyNames; } @Override public Collection<Object> values() { return new AbstractCollection<Object>() { @Override public Iterator<Object> iterator() { return new Iterator<Object>() { private final Iterator<String> mPropIterator = keySet().iterator(); public boolean hasNext() { return mPropIterator.hasNext(); } public Object next() { return get(mPropIterator.next()); } public void remove() { throw new UnsupportedOperationException(); } }; } @Override public int size() { return BeanMap.this.size(); } @Override public boolean isEmpty() { return false; } @Override public boolean contains(Object v) { return containsValue(v); } @Override public boolean remove(Object e) { throw new UnsupportedOperationException(); } @Override public void clear() { throw new UnsupportedOperationException(); } }; } @Override public Set<Map.Entry<String, Object>> entrySet() { return new AbstractSet<Map.Entry<String, Object>>() { @Override public Iterator<Map.Entry<String, Object>> iterator() { return new Iterator<Map.Entry<String, Object>>() { private final Iterator<String> mPropIterator = keySet().iterator(); public boolean hasNext() { return mPropIterator.hasNext(); } public Map.Entry<String, Object> next() { final String property = mPropIterator.next(); final Object value = get(property); return new Map.Entry<String, Object>() { Object mutableValue = value; public String getKey() { return property; } public Object getValue() { return mutableValue; } public Object setValue(Object value) { Object old = BeanMap.this.put(property, value); mutableValue = value; return old; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Map.Entry) { Map.Entry other = (Map.Entry) obj; return (this.getKey() == null ? other.getKey() == null : this.getKey().equals(other.getKey())) && (this.getValue() == null ? other.getValue() == null : this.getValue().equals(other.getValue())); } return false; } @Override public int hashCode() { return (getKey() == null ? 0 : getKey().hashCode()) ^ (getValue() == null ? 0 : getValue().hashCode()); } @Override public String toString() { return property + "=" + mutableValue; } }; } public void remove() { throw new UnsupportedOperationException(); } }; } @Override public int size() { return BeanMap.this.size(); } @Override public boolean isEmpty() { return false; } @Override public boolean contains(Object e) { Map.Entry<String, Object> entry = (Map.Entry<String, Object>) e; String key = entry.getKey(); if (BeanMap.this.containsKey(key)) { Object value = BeanMap.this.get(key); return value == null ? entry.getValue() == null : value.equals(entry.getValue()); } return false; } @Override public boolean add(Map.Entry<String, Object> e) { BeanMap.this.put(e.getKey(), e.getValue()); return true; } @Override public boolean remove(Object e) { throw new UnsupportedOperationException(); } @Override public void clear() { throw new UnsupportedOperationException(); } }; } } private static class SubMap<B> extends BeanMap<B> { final String mFromKey; final String mToKey; SubMap(B bean, BeanPropertyAccessor<B> accessor, SortedSet<String> propertyNames, String fromKey, String toKey) { super(bean, accessor, propertyNames); mFromKey = fromKey; mToKey = toKey; } @Override public SortedMap<String, Object> subMap(String fromKey, String toKey) { if (mFromKey != null && mFromKey.compareTo(fromKey) > 0) { fromKey = mFromKey; } if (mToKey != null && mToKey.compareTo(toKey) < 0) { toKey = mToKey; } return new SubMap<B>(mBean, mAccessor, mPropertyNames.subSet(fromKey, toKey), fromKey, toKey); } @Override public SortedMap<String, Object> headMap(String toKey) { if (mToKey != null && mToKey.compareTo(toKey) < 0) { toKey = mToKey; } return new SubMap<B>(mBean, mAccessor, mPropertyNames.headSet(toKey), mFromKey, toKey); } @Override public SortedMap<String, Object> tailMap(String fromKey) { if (mFromKey != null && mFromKey.compareTo(fromKey) > 0) { fromKey = mFromKey; } return new SubMap<B>(mBean, mAccessor, mPropertyNames.tailSet(fromKey), fromKey, mToKey); } @Override public boolean containsKey(Object key) { if (key == null) { return false; } String strKey = (String) key; if (mFromKey != null && mFromKey.compareTo(strKey) > 0) { return false; } if (mToKey != null && mToKey.compareTo(strKey) <= 0) { return false; } return mAccessor.hasReadableProperty(strKey); } @Override public boolean containsValue(Object value) { for (String key : keySet()) { Object propValue = mAccessor.getPropertyValue(mBean, key); if (propValue == null) { if (value == null) { return true; } } else if (propValue.equals(value)) { return true; } } return false; } @Override public Object get(Object key) { if (key == null) { return null; } String strKey = (String) key; if (mFromKey != null && mFromKey.compareTo(strKey) > 0) { return null; } if (mToKey != null && mToKey.compareTo(strKey) <= 0) { return null; } return mAccessor.tryGetPropertyValue(mBean, strKey); } @Override public Object put(String key, Object value) { if (key != null) { if (mFromKey != null && mFromKey.compareTo(key) > 0) { throw rangeError(key); } if (mToKey != null && mToKey.compareTo(key) <= 0) { throw rangeError(key); } } Object old = mAccessor.tryGetPropertyValue(mBean, key); mAccessor.setPropertyValue(mBean, key, value); return old; } private IllegalArgumentException rangeError(String key) { String range; if (mFromKey == null) { range = "," + mToKey; } else if (mToKey == null) { range = mFromKey + ","; } else { range = mFromKey + "," + mToKey; } return new IllegalArgumentException ("Key out of range: key=" + key + ", range=[" + range + ')'); } } }