blob: 4888ba8797b76297e1369d414490ac93aecc620b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.commons.chain2.impl;
import org.apache.commons.chain2.Context;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* <p>Convenience base class for {@link Context} implementations.</p>
*
* <p>In addition to the minimal functionality required by the {@link Context}
* interface, this class implements the recommended support for
* <em>Attribute-Property Transparency</em>. This is implemented by
* analyzing the available JavaBeans properties of this class (or its
* subclass), exposes them as key-value pairs in the <code>Map</code>,
* with the key being the name of the property itself.</p>
*
* <p><strong>IMPLEMENTATION NOTE</strong> - Because <code>empty</code> is a
* read-only property defined by the <code>Map</code> interface, it may not
* be utilized as an attribute key or property name.</p>
*
* @version $Id$
*/
public class ContextBase extends ContextMap<String, Object> {
// ------------------------------------------------------------ Constructors
/**
*
*/
private static final long serialVersionUID = 20120724L;
/**
* Default, no argument constructor.
*/
public ContextBase() {
super();
initialize();
}
/**
* <p>Initialize the contents of this {@link Context} by copying the
* values from the specified <code>Map</code>. Any keys in <code>map</code>
* that correspond to local properties will cause the setter method for
* that property to be called.</p>
*
* @param map Map whose key-value pairs are added
*
* @exception IllegalArgumentException if an exception is thrown
* writing a local property value
* @exception UnsupportedOperationException if a local property does not
* have a write method.
*/
public ContextBase(Map<? extends String, ? extends Object> map) {
super(map);
initialize();
putAll(map);
}
// ------------------------------------------------------ Instance Variables
// NOTE - PropertyDescriptor instances are not Serializable, so the
// following variables must be declared as transient. When a ContextBase
// instance is deserialized, the no-arguments constructor is called,
// and the initialize() method called there will repoopulate them.
// Therefore, no special restoration activity is required.
/**
* <p>The <code>PropertyDescriptor</code>s for all JavaBeans properties
* of this {@link Context} implementation class, keyed by property name.
* This collection is allocated only if there are any JavaBeans
* properties.</p>
*/
private transient Map<String, PropertyDescriptor> descriptors = null;
/**
* <p>The same <code>PropertyDescriptor</code>s as an array.</p>
*/
private transient PropertyDescriptor[] pd = null;
/**
* <p>Distinguished singleton value that is stored in the map for each
* key that is actually a property. This value is used to ensure that
* <code>equals()</code> comparisons will always fail.</p>
*/
private static final Object singleton;
static {
singleton = new Serializable() {
private static final long serialVersionUID = 20120724L;
@Override
public boolean equals(Object object) {
return false;
}
@Override
public int hashCode() {
return super.hashCode();
}
};
}
/**
* <p>Zero-length array of parameter values for calling property getters.
* </p>
*/
private static Object[] zeroParams = new Object[0];
// ------------------------------------------------------------- Map Methods
/**
* <p>Override the default <code>Map</code> behavior to clear all keys and
* values except those corresponding to JavaBeans properties.</p>
*/
@Override
public void clear() {
if (descriptors == null) {
super.clear();
} else {
Iterator<String> keys = keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
if (!descriptors.containsKey(key)) {
keys.remove();
}
}
}
}
/**
* <p>Override the default <code>Map</code> behavior to return
* <code>true</code> if the specified value is present in either the
* underlying <code>Map</code> or one of the local property values.</p>
*
* @param value the value look for in the context.
* @return <code>true</code> if found in this context otherwise
* <code>false</code>.
* @exception IllegalArgumentException if a property getter
* throws an exception
*/
@Override
public boolean containsValue(Object value) {
// Case 1 -- no local properties
if (descriptors == null) {
return super.containsValue(value);
// Case 2 -- value found in the underlying Map
} else if (super.containsValue(value)) {
return true;
}
// Case 3 -- check the values of our readable properties
for (PropertyDescriptor aPd : pd) {
if (aPd.getReadMethod() != null) {
Object prop = readProperty(aPd);
if (value == null) {
if (prop == null) {
return true;
}
} else if (value.equals(prop)) {
return true;
}
}
}
return false;
}
/**
* <p>Override the default <code>Map</code> behavior to return a
* <code>Set</code> that meets the specified default behavior except
* for attempts to remove the key for a property of the {@link Context}
* implementation class, which will throw
* <code>UnsupportedOperationException</code>.</p>
*
* @return Set of entries in the Context.
*/
@Override
public Set<Entry<String, Object>> entrySet() {
return new EntrySetImpl();
}
/**
* <p>Override the default <code>Map</code> behavior to return the value
* of a local property if the specified key matches a local property name.
* </p>
*
* <p><strong>IMPLEMENTATION NOTE</strong> - If the specified
* <code>key</code> identifies a write-only property, <code>null</code>
* will arbitrarily be returned, in order to avoid difficulties implementing
* the contracts of the <code>Map</code> interface.</p>
*
* @param key Key of the value to be returned
* @return The value for the specified key.
*
* @exception IllegalArgumentException if an exception is thrown
* reading this local property value
* @exception UnsupportedOperationException if this local property does not
* have a read method.
*/
@Override
public Object get(Object key) {
// Case 1 -- no local properties
if (descriptors == null) {
return super.get(key);
}
// Case 2 -- this is a local property
if (key != null) {
PropertyDescriptor descriptor = descriptors.get(key);
if (descriptor != null) {
if (descriptor.getReadMethod() != null) {
return readProperty(descriptor);
} else {
return null;
}
}
}
// Case 3 -- retrieve value from our underlying Map
return super.get(key);
}
/**
* <p>Override the default <code>Map</code> behavior to return
* <code>true</code> if the underlying <code>Map</code> only contains
* key-value pairs for local properties (if any).</p>
*
* @return <code>true</code> if this Context is empty, otherwise
* <code>false</code>.
*/
@Override
public boolean isEmpty() {
// Case 1 -- no local properties
if (descriptors == null) {
return super.isEmpty();
}
// Case 2 -- compare key count to property count
return super.size() <= descriptors.size();
}
/**
* <p>Override the default <code>Map</code> behavior to return a
* <code>Set</code> that meets the specified default behavior except
* for attempts to remove the key for a property of the {@link Context}
* implementation class, which will throw
* <code>UnsupportedOperationException</code>.</p>
*
* @return The set of keys for objects in this Context.
*/
@Override
public Set<String> keySet() {
return super.keySet();
}
/**
* <p>Override the default <code>Map</code> behavior to set the value
* of a local property if the specified key matches a local property name.
* </p>
*
* @param key Key of the value to be stored or replaced
* @param value New value to be stored
* @return The value added to the Context.
*
* @exception IllegalArgumentException if an exception is thrown
* reading or writing this local property value
* @exception UnsupportedOperationException if this local property does not
* have both a read method and a write method
*/
@Override
public Object put(String key, Object value) {
/*
* ConcurrentHashMap doesn't accept null values, see
* http://download.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/ConcurrentHashMap.html#put(String, Object)
*/
if (value == null) {
if (containsKey(key)) {
remove(key);
}
return null;
}
// Case 1 -- no local properties
if (descriptors == null) {
return super.put(key, value);
}
// Case 2 -- this is a local property
if (key != null) {
PropertyDescriptor descriptor = descriptors.get(key);
if (descriptor != null) {
Object previous = null;
if (descriptor.getReadMethod() != null) {
previous = readProperty(descriptor);
}
writeProperty(descriptor, value);
return previous;
}
}
// Case 3 -- store or replace value in our underlying map
return super.put(key, value);
}
/**
* <p>Override the default <code>Map</code> behavior to call the
* <code>put()</code> method individually for each key-value pair
* in the specified <code>Map</code>.</p>
*
* @param map <code>Map</code> containing key-value pairs to store
* (or replace)
*
* @exception IllegalArgumentException if an exception is thrown
* reading or writing a local property value
* @exception UnsupportedOperationException if a local property does not
* have both a read method and a write method
*/
@Override
public void putAll(Map<? extends String, ? extends Object> map) {
for (Entry<? extends String, ? extends Object> pair : map.entrySet()) {
put(pair.getKey(), pair.getValue());
}
}
/**
* <p>Override the default <code>Map</code> behavior to throw
* <code>UnsupportedOperationException</code> on any attempt to
* remove a key that is the name of a local property.</p>
*
* @param key Key to be removed
* @return The value removed from the Context.
*
* @exception UnsupportedOperationException if the specified
* <code>key</code> matches the name of a local property
*/
@Override
public Object remove(Object key) {
// Case 1 -- no local properties
if (descriptors == null) {
return super.remove(key);
}
// Case 2 -- this is a local property
if (key != null) {
PropertyDescriptor descriptor = descriptors.get(key);
if (descriptor != null) {
throw new UnsupportedOperationException
("Local property '" + key + "' cannot be removed");
}
}
// Case 3 -- remove from underlying Map
return super.remove(key);
}
/**
* <p>Override the default <code>Map</code> behavior to return a
* <code>Collection</code> that meets the specified default behavior except
* for attempts to remove the key for a property of the {@link Context}
* implementation class, which will throw
* <code>UnsupportedOperationException</code>.</p>
*
* @return The collection of values in this Context.
*/
@Override
public Collection<Object> values() {
return new ValuesImpl();
}
// --------------------------------------------------------- Private Methods
/**
* <p>Return an <code>Iterator</code> over the set of <code>Map.Entry</code>
* objects representing our key-value pairs.</p>
*/
private Iterator<Entry<String, Object>> entriesIterator() {
return new EntrySetIterator();
}
/**
* <p>Return a <code>Map.Entry</code> for the specified key value, if it
* is present; otherwise, return <code>null</code>.</p>
*
* @param key Attribute key or property name
*/
private Map.Entry<String, Object> entry(String key) {
if (containsKey(key)) {
return new MapEntryImpl(key, get(key));
}
return null;
}
/**
* <p>Customize the contents of our underlying <code>Map</code> so that
* it contains keys corresponding to all of the JavaBeans properties of
* the {@link Context} implementation class.</p>
*
*
* @exception IllegalArgumentException if an exception is thrown
* writing this local property value
* @exception UnsupportedOperationException if this local property does not
* have a write method.
*/
private void initialize() {
// Retrieve the set of property descriptors for this Context class
try {
pd = Introspector.getBeanInfo
(getClass()).getPropertyDescriptors();
} catch (IntrospectionException e) {
pd = new PropertyDescriptor[0]; // Should never happen
}
// Initialize the underlying Map contents
for (PropertyDescriptor propertyDescriptor : pd) {
String name = propertyDescriptor.getName();
// Add descriptor (ignoring getClass() and isEmpty())
if (!("class".equals(name) || "empty".equals(name))) {
if (descriptors == null) {
descriptors = new HashMap<String, PropertyDescriptor>(pd.length - 2);
}
descriptors.put(name, propertyDescriptor);
super.put(name, singleton);
}
}
}
/**
* <p>Get and return the value for the specified property.</p>
*
* @param descriptor <code>PropertyDescriptor</code> for the
* specified property
*
* @exception IllegalArgumentException if an exception is thrown
* reading this local property value
* @exception UnsupportedOperationException if this local property does not
* have a read method.
*/
private Object readProperty(PropertyDescriptor descriptor) {
try {
Method method = descriptor.getReadMethod();
if (method == null) {
throw new UnsupportedOperationException
("Property '" + descriptor.getName()
+ "' is not readable");
}
return method.invoke(this, zeroParams);
} catch (Exception e) {
throw new UnsupportedOperationException
("Exception reading property '" + descriptor.getName()
+ "': " + e.getMessage());
}
}
/**
* <p>Remove the specified key-value pair, if it exists, and return
* <code>true</code>. If this pair does not exist, return
* <code>false</code>.</p>
*
* @param entry Key-value pair to be removed
*
* @exception UnsupportedOperationException if the specified key
* identifies a property instead of an attribute
*/
private boolean remove(Map.Entry<String, Object> entry) {
Map.Entry<String, Object> actual = entry(entry.getKey());
if (actual == null) {
return false;
} else if (!entry.equals(actual)) {
return false;
} else {
remove(entry.getKey());
return true;
}
}
/**
* <p>Return an <code>Iterator</code> over the set of values in this
* <code>Map</code>.</p>
*/
private Iterator<Object> valuesIterator() {
return new ValuesIterator();
}
/**
* <p>Set the value for the specified property.</p>
*
* @param descriptor <code>PropertyDescriptor</code> for the
* specified property
* @param value The new value for this property (must be of the
* correct type)
*
* @exception IllegalArgumentException if an exception is thrown
* writing this local property value
* @exception UnsupportedOperationException if this local property does not
* have a write method.
*/
private void writeProperty(PropertyDescriptor descriptor, Object value) {
try {
Method method = descriptor.getWriteMethod();
if (method == null) {
throw new UnsupportedOperationException
("Property '" + descriptor.getName()
+ "' is not writeable");
}
method.invoke(this, new Object[] {value});
} catch (Exception e) {
throw new UnsupportedOperationException
("Exception writing property '" + descriptor.getName()
+ "': " + e.getMessage());
}
}
// --------------------------------------------------------- Private Classes
/**
* <p>Private implementation of <code>Set</code> that implements the
* semantics required for the value returned by <code>entrySet()</code>.</p>
*/
private class EntrySetImpl extends AbstractSet<Entry<String, Object>> {
@Override
public void clear() {
ContextBase.this.clear();
}
@Override
public boolean contains(Object obj) {
if (!(obj instanceof Map.Entry)) {
return false;
}
/* The contains method is expecting the search type to be of the
* same type stored. This contract is enforced as a precondition.
* So we can safely suppress type safety warnings below. */
@SuppressWarnings("unchecked")
Map.Entry<String, Object> entry = (Map.Entry<String, Object>) obj;
Entry<String, Object> actual = ContextBase.this.entry(entry.getKey());
if (actual != null) {
return actual.equals(entry);
}
return false;
}
@Override
public boolean isEmpty() {
return ContextBase.this.isEmpty();
}
@Override
public Iterator<Entry<String, Object>> iterator() {
return ContextBase.this.entriesIterator();
}
@Override
public boolean remove(Object obj) {
if (obj instanceof Map.Entry) {
/* The remove method is expecting an input of the the same
* type as the entry set. This precondition is checked above,
* so we can safely suppress the unchecked warnings. */
@SuppressWarnings("unchecked")
Map.Entry<String, Object> entry = (Map.Entry<String, Object>) obj;
return ContextBase.this.remove(entry);
}
return false;
}
@Override
public int size() {
return ContextBase.this.size();
}
}
/**
* <p>Private implementation of <code>Iterator</code> for the
* <code>Set</code> returned by <code>entrySet()</code>.</p>
*/
private class EntrySetIterator implements Iterator<Entry<String, Object>> {
private Map.Entry<String, Object> entry = null;
private Iterator<String> keys = ContextBase.this.keySet().iterator();
public boolean hasNext() {
return keys.hasNext();
}
public Entry<String, Object> next() {
entry = ContextBase.this.entry(keys.next());
return entry;
}
public void remove() {
ContextBase.this.remove(entry);
}
}
/**
* <p>Private implementation of <code>Map.Entry</code> for each item in
* <code>EntrySetImpl</code>.</p>
*/
private class MapEntryImpl implements Map.Entry<String, Object> {
MapEntryImpl(String key, Object value) {
this.key = key;
this.value = value;
}
private String key;
private Object value;
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
} else if (!(obj instanceof Map.Entry)) {
return false;
}
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) obj;
if (key == null) {
return entry.getKey() == null;
}
if (key.equals(entry.getKey())) {
if (value == null) {
return entry.getValue() == null;
}
return value.equals(entry.getValue());
}
return false;
}
public String getKey() {
return this.key;
}
public Object getValue() {
return this.value;
}
@Override
public int hashCode() {
return (key == null ? 0 : key.hashCode())
^ (value == null ? 0 : value.hashCode());
}
public Object setValue(Object value) {
Object previous = this.value;
ContextBase.this.put(this.key, value);
this.value = value;
return previous;
}
@Override
public String toString() {
return getKey() + "=" + getValue();
}
}
/**
* <p>Private implementation of <code>Collection</code> that implements the
* semantics required for the value returned by <code>values()</code>.</p>
*/
private class ValuesImpl extends AbstractCollection<Object> {
@Override
public void clear() {
ContextBase.this.clear();
}
@Override
public boolean contains(Object obj) {
if (!(obj instanceof Map.Entry)) {
return false;
}
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) obj;
return ContextBase.this.containsValue(entry.getValue());
}
@Override
public boolean isEmpty() {
return ContextBase.this.isEmpty();
}
@Override
public Iterator<Object> iterator() {
return ContextBase.this.valuesIterator();
}
@Override
public boolean remove(Object obj) {
if (obj instanceof Map.Entry) {
/* We are expecting the passed entry to be of a type
* Entry<String, Object>. This is checked in the precondition
* above, so we can safely suppress unchecked warnings. */
@SuppressWarnings("unchecked")
Map.Entry<String, Object> entry = (Map.Entry<String, Object>) obj;
return ContextBase.this.remove(entry);
}
return false;
}
@Override
public int size() {
return ContextBase.this.size();
}
}
/**
* <p>Private implementation of <code>Iterator</code> for the
* <code>Collection</code> returned by <code>values()</code>.</p>
*/
private class ValuesIterator implements Iterator<Object> {
private Map.Entry<String, Object> entry = null;
private Iterator<String> keys = ContextBase.this.keySet().iterator();
public boolean hasNext() {
return keys.hasNext();
}
public Object next() {
entry = ContextBase.this.entry(keys.next());
return entry.getValue();
}
public void remove() {
ContextBase.this.remove(entry);
}
}
}