blob: 3f6af5b138ebbd327d2a1d71e0408ff796549304 [file] [log] [blame]
/*
* Copyright 1999-2004 The Apache Software Foundation
*
* 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.apache.directory.server.protocol.shared.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 org.apache.directory.server.protocol.shared.chain.Context;
/**
* <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</p>. 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>
*
* @author Craig R. McClanahan
* @version $Revision$ $Date$
*/
public class ContextBase extends HashMap implements Context
{
// ------------------------------------------------------------ Constructors
private static final long serialVersionUID = 5536081240350960868L;
/**
* 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 map)
{
super( map );
initialize();
putAll( map );
}
// ------------------------------------------------------ Instance Variables
/**
* <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 Map descriptors = null;
/**
* <p>The same <code>PropertyDescriptor</code>s as an array.</p>
*/
private 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 Object singleton;
static
{
singleton = new Object()
{
public boolean equals( Object object )
{
return ( false );
}
};
}
/**
* <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>
*/
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();
}
}
}
}
/**
* <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>
*
* @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 );
}
/**
* <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>
*/
public Set 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
*
* @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 ) );
}
/**
* <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>
*/
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>
*/
public Set 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
*
* @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 ) );
}
/**
* <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 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() )
{
Map.Entry pair = ( Map.Entry ) pairs.next();
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
*
* @exception UnsupportedOperationException if the specified
* <code>key</code> 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 ) );
}
/**
* <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>
*/
public Collection values()
{
return ( new ValuesImpl() );
}
// --------------------------------------------------------- Private Methods
/**
* <p>Eliminate the specified property descriptor from the list of
* property descriptors in <code>pd</code>.</p>
*
* @param name Name of the property to eliminate
*
* @exception IllegalArgumentException if the specified property name
* is not present
*/
private void eliminate( String name )
{
int j = -1;
for ( int i = 0; i < pd.length; i++ )
{
if ( name.equals( pd[i].getName() ) )
{
j = i;
break;
}
}
if ( j < 0 )
{
throw new IllegalArgumentException( "Property '" + name + "' is not present" );
}
PropertyDescriptor[] results = new PropertyDescriptor[pd.length - 1];
System.arraycopy( pd, 0, results, 0, j );
System.arraycopy( pd, j + 1, results, j, pd.length - ( j + 1 ) );
pd = results;
}
/**
* <p>Return an <code>Iterator</code> over the set of <code>Map.Entry</code>
* objects representing our key-value pairs.</p>
*/
private Iterator 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 entry( Object key )
{
if ( containsKey( key ) )
{
return ( new MapEntryImpl( key, get( key ) ) );
}
else
{
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
}
eliminate( "class" ); // Because of "getClass()"
eliminate( "empty" ); // Because of "isEmpty()"
// Initialize the underlying Map contents
if ( pd.length > 0 )
{
descriptors = new HashMap();
for ( int i = 0; i < pd.length; i++ )
{
descriptors.put( pd[i].getName(), pd[i] );
super.put( pd[i].getName(), 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 entry )
{
Map.Entry 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 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
{
public void clear()
{
ContextBase.this.clear();
}
public boolean contains( Object obj )
{
if ( !( obj instanceof Map.Entry ) )
{
return ( false );
}
Map.Entry entry = ( Map.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( ( Map.Entry ) obj ) );
}
else
{
return ( false );
}
}
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
{
Map.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 );
}
}
/**
* <p>Private implementation of <code>Map.Entry</code> for each item in
* <code>EntrySetImpl</code>.</p>
*/
private class MapEntryImpl implements Map.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 );
}
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 );
}
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 );
}
}
/**
* <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
{
public void clear()
{
ContextBase.this.clear();
}
public boolean contains( Object obj )
{
if ( !( obj instanceof Map.Entry ) )
{
return ( false );
}
Map.Entry entry = ( Map.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( ( Map.Entry ) obj ) );
}
else
{
return ( false );
}
}
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
{
Map.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 );
}
}
}