/*
* Copyright 2001-2013 Geert Bevin <gbevin[remove] at uwyn dot com>
* Licensed under the Apache License, Version 2.0 (the "License")
*/
package com.uwyn.rife.ioc;
import com.uwyn.rife.ioc.exceptions.IncompatiblePropertyValueTypeException;
import com.uwyn.rife.ioc.exceptions.PropertyValueException;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.*;
/**
* This class allows the creation of a hierarchical tree of named {@link
* PropertyValue} instances.
* <p>When a property is looked up in a child
* <code>HierarchicalProperties</code> instance, the lookup will be propagated
* to its parent when it couldn't be found in the child. A single hierarchical
* line is thus considered to be one collection that groups all involved
* <code>HierarchicalProperties</code> instances. Retrieving the names and the
* size will recursively take all the properties of the parents into account
* and return the consolidated result. To offer these features, intelligent
* caching has been implemented to ensure optimal performance.
*
* @author Geert Bevin (gbevin[remove] at uwyn dot com)
* @since 1.1
*/
public class HierarchicalProperties
{
private Map<String, PropertyValue> properties;
private HierarchicalProperties parent;
private Set<HierarchicalProperties> children;
private Set<String> cachedNames;
private Set<String> cachedInjectableNames;
public HierarchicalProperties()
{
}
private HierarchicalProperties(HierarchicalProperties shadow)
{
properties = shadow.properties;
}
/**
* Creates a copy of this <code>HierarchicalProperties</code> hierarchy
* until a certain instance is reached.
* <p/>
* Each copied instance will shared the datastructure in which the
* properties are stored with the original. Creating a shadow is for
* changing the hierarchical structure but maintaining a centralized
* management of the properties.
*
* @param limit the <code>HierarchicalProperties</code> instance that will
* not be part of the shadow copy and interrupt the copying process; or
* <code>null</code> if the entire hierachy should be copied.
* @return the shadow copy of this <code>HierarchicalProperties</code>
* hierarchy
* hierarchy
* @since 1.1
*/
public HierarchicalProperties createShadow(HierarchicalProperties limit)
{
HierarchicalProperties result = new HierarchicalProperties(this);
HierarchicalProperties original = this;
HierarchicalProperties shadow = result;
while (original.getParent() != null &&
original.getParent() != limit)
{
shadow.setParent(new HierarchicalProperties(original.getParent()));
original = original.getParent();
shadow = shadow.getParent();
}
return result;
}
/**
* Retrieves the first parent of this <code>HierarchicalProperties</code>
* hierarchy.
*
* @return the root of this <code>HierarchicalProperties</code>
* hierarchy
* @since 1.1
*/
public HierarchicalProperties getRoot()
{
HierarchicalProperties root = this;
while (root.getParent() != null)
{
root = root.getParent();
}
return root;
}
/**
* Retrieves the <code>Map</code> with only the properties that are
* locally present in this <code>HierarchicalProperties</code> instance.
*
* @return the local <code>Map</code> of this
* <code>HierarchicalProperties</code> instance
* @since 1.1
*/
public Map<String, PropertyValue> getLocalMap()
{
if (null == properties)
{
return Collections.EMPTY_MAP;
}
return properties;
}
/**
* Sets the parent of this <code>HierarchicalProperties</code> instance.
*
* @param parent the parent of this instance; or <code>null</code> if this
* instance should be isolated
* @return this <code>HierarchicalProperties</code> instance
* @see #getParent
* @since 1.1
*/
public HierarchicalProperties parent(HierarchicalProperties parent)
{
setParent(parent);
return this;
}
/**
* Retrieves the parent of this <code>HierarchicalProperties</code>
* instance.
*
* @return the parent of this <code>HierarchicalProperties</code>
* instance; or
* <p><code>null</code> if this instance is isolated
* @see #parent
* @since 1.1
*/
public HierarchicalProperties getParent()
{
return parent;
}
/**
* Sets the parent of this <code>HierarchicalProperties</code> instance.
*
* @param parent the parent of this instance; or <code>null</code> if this
* instance should be isolated
* @see #getParent
* @since 1.1
*/
public void setParent(HierarchicalProperties parent)
{
clearCaches();
if (this.parent != null)
{
this.parent.removeChild(this);
}
this.parent = parent;
if (this.parent != null)
{
this.parent.addChild(this);
}
}
/**
* Associates the specified value with the specified name in this
* <code>HierarchicalProperties</code> instance. If it previously
* contained a mapping for this name, the old value is replaced by the
* specified value.
*
* @param name the name that will be associated with the property
* @param value the property value that will be associated with the
* specified name
* @return this <code>HierarchicalProperties</code> instance
* @see #put(String, Object)
* @see #putAll
* @since 1.1
*/
public HierarchicalProperties put(String name, PropertyValue value)
{
clearCaches();
if (null == properties)
{
properties = new LinkedHashMap<>();
}
properties.put(name, value);
return this;
}
/**
* Associates the specified fixed object value with the specified name
* in this <code>HierarchicalProperties</code> instance. If it previously
* contained a mapping for this name, the old value is replaced by the
* specified value.
*
* @param name the name that will be associated with the property
* @param value the property value that will be associated with the
* specified name, note that this method will create a {@link PropertyValueObject}
* instance that will contain the value in a fixed manner
* @return this <code>HierarchicalProperties</code> instance
* @see #put(String, PropertyValue)
* @see #putAll
* @since 1.6
*/
public HierarchicalProperties put(String name, Object value)
{
put(name, new PropertyValueObject(value));
return this;
}
/**
* Removes the mapping for this name from this
* <code>HierarchicalProperties</code> instance, if it is present.
*
* @param name the name that will be removed
* @return the previously associated value; or
* <p><code>null</code> if the name wasn't found in this
* <code>HierarchicalProperties</code> instance
* @since 1.1
*/
public PropertyValue remove(String name)
{
if (null == properties)
{
return null;
}
clearCaches();
return properties.remove(name);
}
/**
* Copies all of the named properties from the specified
* <code>HierarchicalProperties</code> instance to this
* <code>HierarchicalProperties</code> instance. The effect of this call
* is equivalent to that of calling {@link #put} on this
* <code>HierarchicalProperties</code> once for each mapping from the
* specified <code>HierarchicalProperties</code> instance.
*
* @param source the properties that will be stored in this
* <code>HierarchicalProperties</code> instance
* @return this <code>HierarchicalProperties</code> instance
* @see #put
* @since 1.1
*/
public HierarchicalProperties putAll(HierarchicalProperties source)
{
clearCaches();
if (source.properties != null)
{
if (null == properties)
{
properties = new LinkedHashMap<>();
}
properties.putAll(source.properties);
}
return this;
}
/**
* Copies all of the named properties from the specified
* <code>HierarchicalProperties</code> instance to this
* <code>HierarchicalProperties</code> instance, without replacing existing
* properties. The effect of this call
* is equivalent to that of calling {@link #put} on this
* <code>HierarchicalProperties</code> once for each mapping from the
* specified <code>HierarchicalProperties</code> instance that doesn't
* have a key in this instance yet.
*
* @param source the properties that will be stored in this
* <code>HierarchicalProperties</code> instance
* @return this <code>HierarchicalProperties</code> instance
* @see #put
* @since 1.6.2
*/
public HierarchicalProperties putAllWithoutReplacing(HierarchicalProperties source)
{
clearCaches();
if (source.properties != null)
{
if (null == properties)
{
properties = new LinkedHashMap<>();
}
for (Map.Entry<String, PropertyValue> entry : source.properties.entrySet())
{
if (!properties.containsKey(entry.getKey()))
{
properties.put(entry.getKey(), entry.getValue());
}
}
}
return this;
}
/**
* Copies all of the entries for a <code>Map</code> instance to this
* <code>HierarchicalProperties</code> instance.
*
* @param source the map entries that will be stored in this
* <code>HierarchicalProperties</code> instance
* @return this <code>HierarchicalProperties</code> instance
* @since 1.5
*/
public HierarchicalProperties putAll(Map source)
{
if (null == source)
{
return this;
}
clearCaches();
if (null == properties)
{
properties = new LinkedHashMap<>();
}
for (Map.Entry entry : (Set<Map.Entry>)source.entrySet())
{
properties.put(String.valueOf(entry.getKey()), new PropertyValueObject(entry.getValue()));
}
return this;
}
/**
* Checks the <code>HierarchicalProperties</code> hierarchy for the
* presence of the specified name.
*
* @param name the name whose presence will be checked
* @return <code>true</code> if the name was found; or
* <p><code>false</code> otherwise
* @see #get
* @since 1.1
*/
public boolean contains(String name)
{
HierarchicalProperties current = this;
Map<String, PropertyValue> properties;
while (true)
{
properties = current.properties;
if (properties != null)
{
if (properties.containsKey(name))
{
return true;
}
}
if (null == current.parent)
{
break;
}
current = current.parent;
}
return false;
}
/**
* Retrieves the <code>PropertyValue</code> for a specific name from the
* <code>HierarchicalProperties</code> hierarchy.
*
* @param name the name whose associated value will be returned
* @return the associated <code>PropertyValue</code>; or
* <p><code>null</code> if the name could not be found
* @see #contains
* @since 1.1
*/
public PropertyValue get(String name)
{
HierarchicalProperties current = this;
PropertyValue result;
Map<String, PropertyValue> properties;
while (true)
{
properties = current.properties;
if (properties != null)
{
result = properties.get(name);
if (result != null)
{
return result;
}
}
if (null == current.parent)
{
break;
}
current = current.parent;
}
return null;
}
/**
* Retrieves the value of <code>PropertyValue</code> for a specific name from
* the <code>HierarchicalProperties</code> hierarchy.
*
* @param name the name whose associated value will be returned
* @return the associated <code>PropertyValue</code>; or
* <p><code>null</code> if the name could not be found
* @throws PropertyValueException when an error occurred while retrieving the
* property value
* @see #get
* @see #getValue(String, Object)
* @since 1.1
*/
public Object getValue(String name)
throws PropertyValueException
{
return getValue(name, null);
}
/**
* Retrieves the value of <code>PropertyValue</code> for a specific name from
* the <code>HierarchicalProperties</code> hierarchy. If the property couldn't
* be found or if the value was <code>null</code>, the default value will be
* returned.
*
* @param name the name whose associated value will be returned
* @param defaultValue the value that should be used as a fallback
* @return the associated <code>PropertyValue</code>; or
* <p>the <code>defaultValue</code> if the property couldn't be found or if
* the value was <code>null</code>
* @throws PropertyValueException when an error occurred while retrieving the
* property value
* @see #get
* @see #getValue(String)
* @since 1.1
*/
public Object getValue(String name, Object defaultValue)
throws PropertyValueException
{
Object result = null;
PropertyValue property = get(name);
if (property != null)
{
result = property.getValue();
}
if (null == result)
{
return defaultValue;
}
return result;
}
/**
* Retrieves the string value of <code>PropertyValue</code> for a specific name from
* the <code>HierarchicalProperties</code> hierarchy.
*
* @param name the name whose associated value will be returned
* @return the string value of the retrieved <code>PropertyValue</code>; or
* <p><code>null</code> if the name could not be found
* @throws PropertyValueException when an error occurred while retrieving the
* property value
* @see #get
* @see #getValueString(String, String)
* @see #getValueTyped
* @since 1.1
*/
public String getValueString(String name)
throws PropertyValueException
{
return getValueString(name, null);
}
/**
* Retrieves the string value of <code>PropertyValue</code> for a specific name from
* the <code>HierarchicalProperties</code> hierarchy. If the property couldn't
* be found, if the value was <code>null</code> or if the value was empty, the
* default value will be returned.
*
* @param name the name whose associated value will be returned
* @param defaultValue the value that should be used as a fallback
* @return the string value of the retrieved <code>PropertyValue</code>; or
* <p>the <code>defaultValue</code> if the property couldn't be found or if
* the value was <code>null</code> or an empty string
* @throws PropertyValueException when an error occurred while retrieving the
* property value
* @see #get
* @see #getValueString(String)
* @see #getValueTyped
* @since 1.1
*/
public String getValueString(String name, String defaultValue)
throws PropertyValueException
{
String result = null;
PropertyValue property = get(name);
if (property != null)
{
result = property.getValueString();
}
if (null == result ||
0 == result.length())
{
return defaultValue;
}
return result;
}
/**
* Retrieves the typed value of <code>PropertyValue</code> for a specific name from
* the <code>HierarchicalProperties</code> hierarchy.
* <p/>
* Note that no conversion will occurr, the value is simple verified to be
* assignable to the requested type and then casted to it.
*
* @param name the name whose associated value will be returned
* @param type the class that the value has to be retrieved as
* @return the associated <code>PropertyValue</code> as an instance of the
* provided type; or
* <p><code>null</code> if the name could not be found
* @throws IncompatiblePropertyValueTypeException
* when the type of the property
* value wasn't compatible with the requested type
* @throws PropertyValueException when an error occurred while retrieving the
* property value
* @see #get
* @see #getValueString
* @see #getValueTyped(String, Class)
* @since 1.6
*/
public <T> T getValueTyped(String name, Class<T> type)
throws PropertyValueException
{
return getValueTyped(name, type, null);
}
/**
* Retrieves the typed value of <code>PropertyValue</code> for a specific name from
* the <code>HierarchicalProperties</code> hierarchy.
* <p/>
* Note that no conversion will occurr, the value is simple verified to be
* assignable to the requested type and then casted to it.
*
* @param name the name whose associated value will be returned
* @param type the class that the value has to be retrieved as
* @param defaultValue the value that should be used as a fallback
* @return the associated <code>PropertyValue</code> as an instance of the
* provided type; or
* <p>the <code>defaultValue</code> if the property couldn't be found or if
* the value was <code>null</code>
* @throws IncompatiblePropertyValueTypeException
* when the type of the property
* value wasn't compatible with the requested type
* @throws PropertyValueException when an error occurred while retrieving the
* property value
* @see #get
* @see #getValueString
* @see #getValueTyped(String, Class)
* @since 1.6
*/
public <T> T getValueTyped(String name, Class<T> type, T defaultValue)
throws PropertyValueException
{
if (null == name ||
null == type ||
0 == name.length())
{
return defaultValue;
}
Object result = null;
PropertyValue property = get(name);
if (property != null)
{
result = property.getValue();
}
if (null == result)
{
return defaultValue;
}
if (!type.isAssignableFrom(result.getClass()))
{
throw new IncompatiblePropertyValueTypeException(name, type, result.getClass(), null);
}
return (T)result;
}
/**
* Retrieves the number of unique names in the
* <code>HierarchicalProperties</code> hierarchy.
*
* @return the amount of unique names
* @since 1.1
*/
public int size()
{
return getNames().size();
}
/**
* Retrieves a <code>Set</code> with the unique names that are present in
* the <code>HierarchicalProperties</code> hierarchy.
*
* @return a collection with the unique names
* @see #getInjectableNames
* @since 1.1
*/
public Collection<String> getNames()
{
if (cachedNames != null)
{
return cachedNames;
}
HierarchicalProperties current = this;
Set<String> names = new LinkedHashSet<>();
Map<String, PropertyValue> properties;
while (true)
{
properties = current.properties;
if (properties != null)
{
names.addAll(properties.keySet());
}
if (null == current.parent)
{
break;
}
current = current.parent;
}
cachedNames = names;
return names;
}
/**
* Retrieves a <code>Set</code> with the unique names that are present in
* the <code>HierarchicalProperties</code> hierarchy and that conform to
* the <a
* href="http://java.sun.com/docs/books/jls/third_edition/html/lexical.html#3.8">Java
* rules for valid identifiers</a>. The names in this set are thus usable
* for injection through bean setters.
*
* @return a <code>Set</code> with the unique injectable names
* @see #getNames
* @since 1.1
*/
public Collection<String> getInjectableNames()
{
if (cachedInjectableNames != null)
{
return cachedInjectableNames;
}
Set<String> injectable_names = new LinkedHashSet<>();
Collection<String> names = getNames();
for (String name : names)
{
boolean injectable = true;
CharacterIterator it = new StringCharacterIterator(name);
for (char c = it.first(); c != CharacterIterator.DONE; c = it.next())
{
if (!Character.isJavaIdentifierPart(c))
{
injectable = false;
break;
}
}
if (injectable)
{
injectable_names.add(name);
}
}
cachedInjectableNames = injectable_names;
return injectable_names;
}
private void clearCaches()
{
cachedNames = null;
cachedInjectableNames = null;
if (null == children)
{
return;
}
for (HierarchicalProperties child : children)
{
child.clearCaches();
}
}
private void addChild(HierarchicalProperties child)
{
if (null == children)
{
children = new LinkedHashSet<>();
}
children.add(child);
}
private void removeChild(HierarchicalProperties child)
{
if (null == children)
{
return;
}
children.remove(child);
}
}