
org.apache.commons.chain.impl.ContextBase Maven / Gradle / Ivy
/*
* 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.chain.impl;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
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;
import java.io.Serializable;
import org.apache.commons.chain.Context;
/**
* Convenience base class for {@link Context} implementations.
*
* In addition to the minimal functionality required by the {@link Context}
* interface, this class implements the recommended support for
* Attribute-Property Transparency. This is implemented by
* analyzing the available JavaBeans properties of this class (or its
* subclass), exposes them as key-value pairs in the Map
,
* with the key being the name of the property itself.
*
* IMPLEMENTATION NOTE - Because empty
is a
* read-only property defined by the Map
interface, it may not
* be utilized as an attribute key or property name.
*
* @author Craig R. McClanahan
* @version $Revision: 499247 $ $Date: 2007-01-24 04:09:44 +0000 (Wed, 24 Jan 2007) $
*/
public class ContextBase extends HashMap implements Context {
// ------------------------------------------------------------ Constructors
/**
* Default, no argument constructor.
*/
public ContextBase() {
super();
initialize();
}
/**
* Initialize the contents of this {@link Context} by copying the
* values from the specified Map
. Any keys in map
* that correspond to local properties will cause the setter method for
* that property to be called.
*
* @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 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.
/**
* The PropertyDescriptor
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.
*/
private transient Map descriptors = null;
/**
* The same PropertyDescriptor
s as an array.
*/
private transient PropertyDescriptor[] pd = null;
/**
* Distinguished singleton value that is stored in the map for each
* key that is actually a property. This value is used to ensure that
* equals()
comparisons will always fail.
*/
private static Object singleton;
static {
singleton = new Serializable() {
public boolean equals(Object object) {
return (false);
}
};
}
/**
* Zero-length array of parameter values for calling property getters.
*
*/
private static Object[] zeroParams = new Object[0];
// ------------------------------------------------------------- Map Methods
/**
* Override the default Map
behavior to clear all keys and
* values except those corresponding to JavaBeans properties.
*/
public void clear() {
if (descriptors == null) {
super.clear();
} else {
Iterator keys = keySet().iterator();
while (keys.hasNext()) {
Object key = keys.next();
if (!descriptors.containsKey(key)) {
keys.remove();
}
}
}
}
/**
* Override the default Map
behavior to return
* true
if the specified value is present in either the
* underlying Map
or one of the local property values.
*
* @param value the value look for in the context.
* @return true
if found in this context otherwise
* false
.
* @exception IllegalArgumentException if a property getter
* throws an exception
*/
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 (int i = 0; i < pd.length; i++) {
if (pd[i].getReadMethod() != null) {
Object prop = readProperty(pd[i]);
if (value == null) {
if (prop == null) {
return (true);
}
} else if (value.equals(prop)) {
return (true);
}
}
}
return (false);
}
/**
* Override the default Map
behavior to return a
* Set
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
* UnsupportedOperationException
.
*
* @return Set of entries in the Context.
*/
public Set entrySet() {
return (new EntrySetImpl());
}
/**
* Override the default Map
behavior to return the value
* of a local property if the specified key matches a local property name.
*
*
* IMPLEMENTATION NOTE - If the specified
* key
identifies a write-only property, null
* will arbitrarily be returned, in order to avoid difficulties implementing
* the contracts of the Map
interface.
*
* @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.
*/
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 =
(PropertyDescriptor) 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));
}
/**
* Override the default Map
behavior to return
* true
if the underlying Map
only contains
* key-value pairs for local properties (if any).
*
* @return true
if this Context is empty, otherwise
* false
.
*/
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());
}
/**
* Override the default Map
behavior to return a
* Set
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
* UnsupportedOperationException
.
*
* @return The set of keys for objects in this Context.
*/
public Set keySet() {
return (super.keySet());
}
/**
* Override the default Map
behavior to set the value
* of a local property if the specified key matches a local property name.
*
*
* @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 wrting this local property value
* @exception UnsupportedOperationException if this local property does not
* have both a read method and a write method
*/
public Object put(Object key, Object value) {
// 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 =
(PropertyDescriptor) 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));
}
/**
* Override the default Map
behavior to call the
* put()
method individually for each key-value pair
* in the specified Map
.
*
* @param map Map
containing key-value pairs to store
* (or replace)
*
* @exception IllegalArgumentException if an exception is thrown
* reading or wrting a local property value
* @exception UnsupportedOperationException if a local property does not
* have both a read method and a write method
*/
public void putAll(Map map) {
Iterator pairs = map.entrySet().iterator();
while (pairs.hasNext()) {
Entry pair = (Entry) pairs.next();
put(pair.getKey(), pair.getValue());
}
}
/**
* Override the default Map
behavior to throw
* UnsupportedOperationException
on any attempt to
* remove a key that is the name of a local property.
*
* @param key Key to be removed
* @return The value removed from the Context.
*
* @exception UnsupportedOperationException if the specified
* key
matches the name of a local property
*/
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 =
(PropertyDescriptor) 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));
}
/**
* Override the default Map
behavior to return a
* Collection
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
* UnsupportedOperationException
.
*
* @return The collection of values in this Context.
*/
public Collection values() {
return (new ValuesImpl());
}
// --------------------------------------------------------- Private Methods
/**
* Return an Iterator
over the set of Map.Entry
* objects representing our key-value pairs.
*/
private Iterator entriesIterator() {
return (new EntrySetIterator());
}
/**
* Return a Map.Entry
for the specified key value, if it
* is present; otherwise, return null
.
*
* @param key Attribute key or property name
*/
private Entry entry(Object key) {
if (containsKey(key)) {
return (new MapEntryImpl(key, get(key)));
} else {
return (null);
}
}
/**
* Customize the contents of our underlying Map
so that
* it contains keys corresponding to all of the JavaBeans properties of
* the {@link Context} implementation class.
*
*
* @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 (int i = 0; i < pd.length; i++) {
String name = pd[i].getName();
// Add descriptor (ignoring getClass() and isEmpty())
if (!("class".equals(name) || "empty".equals(name))) {
if (descriptors == null) {
descriptors = new HashMap((pd.length - 2));
}
descriptors.put(name, pd[i]);
super.put(name, singleton);
}
}
}
/**
* Get and return the value for the specified property.
*
* @param descriptor PropertyDescriptor
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());
}
}
/**
* Remove the specified key-value pair, if it exists, and return
* true
. If this pair does not exist, return
* false
.
*
* @param entry Key-value pair to be removed
*
* @exception UnsupportedOperationException if the specified key
* identifies a property instead of an attribute
*/
private boolean remove(Entry entry) {
Entry actual = entry(entry.getKey());
if (actual == null) {
return (false);
} else if (!entry.equals(actual)) {
return (false);
} else {
remove(entry.getKey());
return (true);
}
}
/**
* Return an Iterator
over the set of values in this
* Map
.
*/
private Iterator valuesIterator() {
return (new ValuesIterator());
}
/**
* Set the value for the specified property.
*
* @param descriptor PropertyDescriptor
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
/**
* Private implementation of Set
that implements the
* semantics required for the value returned by entrySet()
.
*/
private class EntrySetImpl extends AbstractSet {
public void clear() {
ContextBase.this.clear();
}
public boolean contains(Object obj) {
if (!(obj instanceof Map.Entry)) {
return (false);
}
Entry entry = (Entry) obj;
Entry actual = ContextBase.this.entry(entry.getKey());
if (actual != null) {
return (actual.equals(entry));
} else {
return (false);
}
}
public boolean isEmpty() {
return (ContextBase.this.isEmpty());
}
public Iterator iterator() {
return (ContextBase.this.entriesIterator());
}
public boolean remove(Object obj) {
if (obj instanceof Map.Entry) {
return (ContextBase.this.remove((Entry) obj));
} else {
return (false);
}
}
public int size() {
return (ContextBase.this.size());
}
}
/**
* Private implementation of Iterator
for the
* Set
returned by entrySet()
.
*/
private class EntrySetIterator implements Iterator {
private Entry entry = null;
private Iterator keys = ContextBase.this.keySet().iterator();
public boolean hasNext() {
return (keys.hasNext());
}
public Object next() {
entry = ContextBase.this.entry(keys.next());
return (entry);
}
public void remove() {
ContextBase.this.remove(entry);
}
}
/**
* Private implementation of Map.Entry
for each item in
* EntrySetImpl
.
*/
private class MapEntryImpl implements Entry {
MapEntryImpl(Object key, Object value) {
this.key = key;
this.value = value;
}
private Object key;
private Object value;
public boolean equals(Object obj) {
if (obj == null) {
return (false);
} else if (!(obj instanceof Map.Entry)) {
return (false);
}
Entry entry = (Entry) obj;
if (key == null) {
return (entry.getKey() == null);
}
if (key.equals(entry.getKey())) {
if (value == null) {
return (entry.getValue() == null);
} else {
return (value.equals(entry.getValue()));
}
} else {
return (false);
}
}
public Object getKey() {
return (this.key);
}
public Object getValue() {
return (this.value);
}
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);
}
public String toString() {
return getKey() + "=" + getValue();
}
}
/**
* Private implementation of Collection
that implements the
* semantics required for the value returned by values()
.
*/
private class ValuesImpl extends AbstractCollection {
public void clear() {
ContextBase.this.clear();
}
public boolean contains(Object obj) {
if (!(obj instanceof Map.Entry)) {
return (false);
}
Entry entry = (Entry) obj;
return (ContextBase.this.containsValue(entry.getValue()));
}
public boolean isEmpty() {
return (ContextBase.this.isEmpty());
}
public Iterator iterator() {
return (ContextBase.this.valuesIterator());
}
public boolean remove(Object obj) {
if (obj instanceof Map.Entry) {
return (ContextBase.this.remove((Entry) obj));
} else {
return (false);
}
}
public int size() {
return (ContextBase.this.size());
}
}
/**
* Private implementation of Iterator
for the
* Collection
returned by values()
.
*/
private class ValuesIterator implements Iterator {
private Entry entry = null;
private Iterator 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);
}
}
}