All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.juneau.PropertyStore Maven / Gradle / Ivy

There is a newer version: 9.0.1
Show newest version
// ***************************************************************************************************************************
// * 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.juneau;

import static org.apache.juneau.BeanContext.*;
import static org.apache.juneau.internal.ClassUtils.*;
import static org.apache.juneau.internal.StringUtils.*;

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

import org.apache.juneau.internal.*;
import org.apache.juneau.json.*;
import org.apache.juneau.parser.*;

/**
 * A store for instantiating {@link Context} objects.
 *
 * 

* The hierarchy of these objects are... *

    *
  • * {@link PropertyStore} - A thread-safe, modifiable context property store. *
    Used to create {@link Context} objects. *
  • * {@link Context} - A reusable, cacheable, thread-safe, read-only context with configuration properties copied * from the store. *
    Often used to create {@link Session} objects. *
  • * {@link Session} - A one-time-use non-thread-safe object. *
    Used by serializers and parsers to retrieve context properties and to be used as scratchpads. *
* *
PropertyStore objects
* * Property stores can be thought of as consisting of the following: *
    *
  • A Map<String,Object> of context properties. *
  • A Map<Class,Context> of context instances. *
* *

* Property stores are used to create and cache {@link Context} objects using the {@link #getContext(Class)} method. * *

* As a general rule, {@link PropertyStore} objects are 'slow'. *
Setting and retrieving properties on a store can involve relatively slow data conversion and synchronization. *
However, the {@link #getContext(Class)} method is fast, and will return cached context objects if the context * properties have not changed. * *

* Property stores can be used to store context properties for a variety of contexts. *
For example, a single store can store context properties for the JSON serializer, XML serializer, HTML serializer * etc... and can thus be used to retrieve context objects for those serializers. * *

Context properties
* * Context properties are 'settings' for serializers and parsers. *
For example, the {@link BeanContext#BEAN_sortProperties} context property defines whether bean properties should be * serialized in alphabetical order. * *

* Each {@link Context} object should contain the context properties that apply to it as static fields * (e.g {@link BeanContext#BEAN_sortProperties}). * *

* Context properties can be of the following types: *

    *
  • * SIMPLE - A simple property. *
    Examples include: booleans, integers, Strings, Classes, etc... *
    An example of this would be the {@link BeanContext#BEAN_sortProperties} property. *
    It's name is simply "BeanContext.sortProperties". *
  • * SET - A sorted set of objects. *
    These are denoted by appending ".set" to the property name. *
    Objects can be of any type, even complex types. *
    Sorted sets use tree sets to maintain the value in alphabetical order. *
    For example, the {@link BeanContext#BEAN_notBeanClasses} property is used to store classes that should not be * treated like beans. *
    It's name is "BeanContext.notBeanClasses.set". *
  • * LIST - A list of unique objects. *
    These are denoted by appending ".list" to the property name. *
    Objects can be of any type, even complex types. *
    Use lists if the ordering of the values in the set is important (similar to how the order of entries in a * classpath is important). *
    For example, the {@link BeanContext#BEAN_beanFilters} property is used to store bean filters. *
    It's name is "BeanContext.transforms.list". *
  • * MAP - A sorted map of key-value pairs. *
    These are denoted by appending ".map" to the property name. *
    Keys can be any type directly convertible to and from Strings. * Values can be of any type, even complex types. *
    For example, the {@link BeanContext#BEAN_implClasses} property is used to specify the names of implementation * classes for interfaces. *
    It's name is "BeanContext.implClasses.map". *
* *

* All context properties are set using the {@link #setProperty(String, Object)} method. * *

* Default values for context properties can be specified globally as system properties. *
Example: System.setProperty(BEAN_sortProperties, true); * *

* SET and LIST properties can be added to using the {@link #addToProperty(String, Object)} method and removed from * using the {@link #removeFromProperty(String, Object)} method. * *

* SET and LIST properties can also be added to and removed from by appending ".add" or ".remove" to * the property name and using the {@link #setProperty(String, Object)} method. * *

* The following shows the two different ways to append to a set or list property: *

* PropertyStore ps = new PropertyStore().setProperty("BeanContext.notBeanClasses.set", * Collections.emptySet()); * * // Append to set property using addTo(). * ps.addToProperty("BeanContext.notBeanClasses.set", MyNotBeanClass.class); * * // Append to set property using set(). * ps.setProperty("BeanContext.notBeanClasses.set.add", MyNotBeanClass.class); *

* *

* SET and LIST properties can also be set and manipulated using JSON strings. *

* PropertyStore ps = PropertyStore.create(); * * // Set SET value using JSON array. * ps.setProperty("BeanContext.notBeanClasses.set", "['com.my.MyNotBeanClass1']"); * * // Add to SET using simple string. * ps.addToProperty("BeanContext.notBeanClasses.set", "com.my.MyNotBeanClass2"); * * // Add an array of values as a JSON array.. * ps.addToProperty("BeanContext.notBeanClasses.set", "['com.my.MyNotBeanClass3']"); * * // Remove an array of values as a JSON array.. * ps.removeFromProperty("BeanContext.notBeanClasses.set", "['com.my.MyNotBeanClass3']"); *

* *

* MAP properties can be added to using the {@link #putToProperty(String, Object, Object)} and * {@link #putToProperty(String, Object)} methods. *
MAP property entries can be removed by setting the value to null * (e.g. putToProperty("BEAN_implClasses", MyNotBeanClass.class, null);. *
MAP properties can also be added to by appending ".put" to the property name and using the * {@link #setProperty(String, Object)} method. * *

* The following shows the two different ways to append to a set property: *

* PropertyStore ps = PropertyStore.create().setProperty("BeanContext.implClasses.map", * Collections.emptyMap()); * * // Append to map property using putTo(). * ps.putToProperty("BeanContext.implClasses.map", MyInterface.class, MyInterfaceImpl.class); * * // Append to map property using set(). * Map m = new AMap().append(MyInterface.class,MyInterfaceImpl.class); * ps.setProperty("BeanContext.implClasses.map.put", m); *

* *

* MAP properties can also be set and manipulated using JSON strings. *

* PropertyStore ps = PropertyStore.create(); * * // Set MAP value using JSON object. * ps.setProperty("BeanContext.implClasses.map", * "{'com.my.MyInterface1':'com.my.MyInterfaceImpl1'}"); * * // Add to MAP using JSON object. * ps.putToProperty("BeanContext.implClasses.map", * "{'com.my.MyInterface2':'com.my.MyInterfaceImpl2'}"); * * // Remove from MAP using JSON object. * ps.putToProperty("BeanContext.implClasses.map", "{'com.my.MyInterface2':null}"); *

* *

* Context properties are retrieved from this store using the following 3 methods: *

    *
  • * {@link #getProperty(String, Class, Object)} - Retrieve a SIMPLE or SET property converted to the specified * class type. *
  • * {@link #getMap(String, Class, Class, Map)} - Retrieve a MAP property with keys/values converted to the * specified class types. *
  • * {@link #getPropertyMap(String)} - Retrieve a map of all context properties with the specified prefix * (e.g. "BeanContext" for {@link BeanContext} properties). *
* *

* As a general rule, only {@link Context} objects will use these read methods. * *

Context objects
* * A Context object can be thought of as unmodifiable snapshot of a store. *
They should be 'fast' by avoiding synchronization by using final fields whenever possible. *
However, they MUST be thread safe. * *

* Context objects are created using the {@link #getContext(Class)} method. *
As long as the properties on a store have not been modified, the store will return a cached copy of a context. *

* PropertyStore ps = PropertyStore.create(); * * // Get BeanContext with default store settings. * BeanContext bc = ps.getContext(BeanContext.class); * * // Get another one. This will be the same one. * BeanContext bc2 = ps.getContext(BeanContext.class); * assertTrue(bc1 == bc2); * * // Set a property. * ps.setProperty(BEAN_sortProperties, true); * * // Get another one. This will be different! * bc2 = f.getContext(BeanContext.class); * assertFalse(bc1 == bc2); *

* *
Session objects
* * Session objects are created through {@link Context} objects, typically through a createContext() method. *
Unlike context objects, they are NOT reusable and NOT thread safe. *
They are meant to be used one time and then thrown away. *
They should NEVER need to use synchronization. * *

* Session objects are also often used as scratchpads for information such as keeping track of call stack information * to detect recursive loops when serializing beans. */ public final class PropertyStore { // All configuration properties in this object. // Keys are property prefixes (e.g. 'BeanContext'). // Values are maps containing properties for that specific prefix. private Map properties = new ConcurrentSkipListMap(); // Context cache. // This gets cleared every time any properties change on this object. private final Map,Context> contexts = new ConcurrentHashMap,Context>(); // Global Context cache. // Property stores that are the 'same' will use the same maps from this cache. // 'same' means the context properties are all the same when converted to strings. private static final ConcurrentHashMap,Context>> globalContextCache = new ConcurrentHashMap,Context>>(); private ReadWriteLock lock = new ReentrantReadWriteLock(); private Lock rl = lock.readLock(), wl = lock.writeLock(); // Classloader used to instantiate Class instances. ClassLoader classLoader = ClassLoader.getSystemClassLoader(); // Parser to use to convert JSON strings to POJOs ReaderParser defaultParser; // Bean session for converting strings to POJOs. private static BeanSession beanSession; // Used to keep properties in alphabetical order regardless of whether // they're not strings. private static Comparator PROPERTY_COMPARATOR = new Comparator() { @Override public int compare(Object o1, Object o2) { return unswap(o1).toString().compareTo(unswap(o2).toString()); } }; /** * Create a new property store with default settings. * * @return A new property store with default settings. */ public static PropertyStore create() { PropertyStore f = new PropertyStore(); BeanContext.loadDefaults(f); return f; } PropertyStore() {} /** * Copy constructor. * * @param copyFrom The store to copy properties from. */ private PropertyStore(PropertyStore copyFrom) { copyFrom(copyFrom); } /** * Copies the properties from the specified store into this store. * *

* Properties of type set/list/collection will be appended to the existing properties if they already exist. * * @param ps The store to copy from. * @return This object (for method chaining). */ public PropertyStore copyFrom(PropertyStore ps) { if (ps != null) { // TODO - Needs more testing. for (Map.Entry e : ps.properties.entrySet()) this.properties.put(e.getKey(), new PropertyMap(this.properties.get(e.getKey()), e.getValue())); this.classLoader = ps.classLoader; this.defaultParser = ps.defaultParser; } return this; } /** * Creates a new modifiable copy of this property store. * * @return A new modifiable copy of this property store. */ public PropertyStore copy() { return new PropertyStore(this); } /** * Sets a configuration property value on this object. * *

* A typical usage is to set or overwrite configuration values like so... *

* PropertyStore ps = PropertyStore.create(); * ps.setProperty(BEAN_sortProperties, true); *

* *

* The possible class types of the value depend on the property type: *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Property typeExampleAllowed value type
Set SIMPLE"Foo.x"Any object type.
Set SET/LIST"Foo.x.set"Any collection or array of any objects, or a String containing a JSON array.
Add/Remove SET/LIST"Foo.x.set.add"If a collection, adds or removes the entries in the collection. Otherwise, adds/removes a single * entry.
Set MAP"Foo.x.map"A map, or a String containing a JSON object. Entries overwrite existing map.
Put MAP"Foo.x.map.put"A map, or a String containing a JSON object. Entries are added to existing map.
* * @param name * The configuration property name. *
If name ends with .add, then the specified value is added to the existing property value as an entry * in a SET or LIST property. *
If name ends with .put, then the specified value is added to the existing property value as a * key/value pair in a MAP property. *
If name ends with .remove, then the specified value is removed from the existing property property * value in a SET or LIST property. * @param value * The new value. * If null, the property value is deleted. * In general, the value type can be anything. * @return This object (for method chaining). */ public PropertyStore setProperty(String name, Object value) { String prefix = prefix(name); if (name.endsWith(".add")) return addToProperty(name.substring(0, name.lastIndexOf('.')), value); if (name.endsWith(".put")) return putToProperty(name.substring(0, name.lastIndexOf('.')), value); if (name.endsWith(".remove")) return removeFromProperty(name.substring(0, name.lastIndexOf('.')), value); wl.lock(); try { contexts.clear(); if (! properties.containsKey(prefix)) properties.put(prefix, new PropertyMap(prefix)); properties.get(prefix).set(name, value); } finally { wl.unlock(); } return this; } /** * Synonym for {@link #setProperty(String, Object)}. * * @param name * The configuration property name. *
If name ends with .add, then the specified value is added to the existing property value as an entry * in a SET or LIST property. *
If name ends with .put, then the specified value is added to the existing property value as a * key/value pair in a MAP property. *
If name ends with .remove, then the specified value is removed from the existing property property * value in a SET or LIST property. * @param value * The new value. * If null, the property value is deleted. * In general, the value type can be anything. * @return This object (for method chaining). */ public PropertyStore append(String name, Object value) { return setProperty(name, value); } /** * Convenience method for setting multiple properties in one call. * *

* This appends to any previous configuration properties set on this store. * * @param newProperties The new properties to set. * @return This object (for method chaining). */ public PropertyStore setProperties(Map newProperties) { if (newProperties == null || newProperties.isEmpty()) return this; wl.lock(); try { contexts.clear(); for (Map.Entry e : newProperties.entrySet()) { String name = e.getKey().toString(); Object value = e.getValue(); String prefix = prefix(name); if (name.endsWith(".add")) addToProperty(name.substring(0, name.lastIndexOf('.')), value); else if (name.endsWith(".remove")) removeFromProperty(name.substring(0, name.lastIndexOf('.')), value); else { if (! properties.containsKey(prefix)) properties.put(prefix, new PropertyMap(prefix)); properties.get(prefix).set(name, value); } } } finally { wl.unlock(); } return this; } /** * Adds several properties to this store. * * @param properties The properties to add to this store. * @return This object (for method chaining). */ public PropertyStore addProperties(Map properties) { if (properties != null) for (Map.Entry e : properties.entrySet()) setProperty(e.getKey(), e.getValue()); return this; } /** * Adds a value to a SET property. * * @param name The property name. * @param value The new value to add to the SET property. * @return This object (for method chaining). * @throws ConfigException If property is not a SET property. */ public PropertyStore addToProperty(String name, Object value) { String prefix = prefix(name); wl.lock(); try { contexts.clear(); if (! properties.containsKey(prefix)) properties.put(prefix, new PropertyMap(prefix)); properties.get(prefix).addTo(name, value); } finally { wl.unlock(); } return this; } /** * Adds or overwrites a value to a MAP property. * * @param name The property name. * @param key The property value map key. * @param value The property value map value. * @return This object (for method chaining). * @throws ConfigException If property is not a MAP property. */ public PropertyStore putToProperty(String name, Object key, Object value) { String prefix = prefix(name); wl.lock(); try { contexts.clear(); if (! properties.containsKey(prefix)) properties.put(prefix, new PropertyMap(prefix)); properties.get(prefix).putTo(name, key, value); } finally { wl.unlock(); } return this; } /** * Adds or overwrites a value to a MAP property. * * @param name The property value. * @param value The property value map value. * @return This object (for method chaining). * @throws ConfigException If property is not a MAP property. */ public PropertyStore putToProperty(String name, Object value) { String prefix = prefix(name); wl.lock(); try { contexts.clear(); if (! properties.containsKey(prefix)) properties.put(prefix, new PropertyMap(prefix)); properties.get(prefix).putTo(name, value); } finally { wl.unlock(); } return this; } /** * Removes a value from a SET property. * * @param name The property name. * @param value The property value in the SET property. * @return This object (for method chaining). * @throws ConfigException If property is not a SET property. */ public PropertyStore removeFromProperty(String name, Object value) { String prefix = prefix(name); wl.lock(); try { contexts.clear(); if (properties.containsKey(prefix)) properties.get(prefix).removeFrom(name, value); } finally { wl.unlock(); } return this; } /** * Returns an instance of the specified context initialized with the properties in this store. * *

* Multiple calls to this method for the same store class will return the same cached value as long as the * properties on this store are not touched. * *

* As soon as any properties are modified on this store, all cached entries are discarded and recreated as needed. * * @param c The context class to instantiate. * @return The context instance. */ @SuppressWarnings("unchecked") public T getContext(Class c) { rl.lock(); try { try { if (! contexts.containsKey(c)) { // Try to get it from the global cache. Integer key = hashCode(); if (! globalContextCache.containsKey(key)) globalContextCache.putIfAbsent(key, new ConcurrentHashMap,Context>()); ConcurrentHashMap, Context> cacheForThisConfig = globalContextCache.get(key); if (! cacheForThisConfig.containsKey(c)) cacheForThisConfig.putIfAbsent(c, newInstance(c, c, this)); contexts.put(c, cacheForThisConfig.get(c)); } return (T)contexts.get(c); } catch (Exception e) { throw new ConfigException("Could not instantiate context class ''{0}''", className(c)).initCause(e); } } finally { rl.unlock(); } } /** * Returns the configuration properties with the specified prefix. * *

* For example, if prefix is "BeanContext", then retrieves all configuration properties that are * prefixed with "BeanContext.". * * @param prefix The prefix of properties to retrieve. * @return The configuration properties with the specified prefix, never null. */ public PropertyMap getPropertyMap(String prefix) { rl.lock(); try { PropertyMap m = properties.get(prefix); return m == null ? new PropertyMap(prefix) : m; } finally { rl.unlock(); } } /** * Specifies the classloader to use when resolving classes from strings. * *

* Can be used for resolving class names when the classes being created are in a different classloader from the * Juneau code. * *

* If null, the system classloader will be used to resolve classes. * * @param classLoader The new classloader. * @return This object (for method chaining). */ public PropertyStore setClassLoader(ClassLoader classLoader) { this.classLoader = (classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader); return this; } /** * Specifies the parser to use to convert Strings to POJOs. * *

* If null, {@link JsonParser#DEFAULT} will be used. * * @param defaultParser The new defaultParser. * @return This object (for method chaining). */ public PropertyStore setDefaultParser(ReaderParser defaultParser) { this.defaultParser = defaultParser == null ? JsonParser.DEFAULT : defaultParser; return this; } /** * Returns a property value converted to the specified type. * * @param name The full name of the property (e.g. "BeanContext.sortProperties") * @param type The class type to convert the property value to. * @param def The default value if the property is not set. * @return The property value. * @throws ConfigException If property has a value that cannot be converted to a boolean. */ public T getProperty(String name, Class type, T def) { rl.lock(); try { PropertyMap pm = getPropertyMap(prefix(name)); if (pm != null) return pm.get(name, type, def); String s = System.getProperty(name); if ((! isEmpty(s)) && isBeanSessionAvailable()) return getBeanSession().convertToType(s, type); return def; } finally { rl.unlock(); } } /** * Returns a property value either cast to the specified type, or a new instance of the specified type. * *

* It's assumed that the current property value is either an instance of that type, or a Class that's * a subclass of the type to be instantiated. * * @param name The full name of the property (e.g. "BeanContext.sortProperties") * @param type The class type to convert the property value to. * @param def The type to instantiate if the property is not set. * @param args The arguments to pass to the default type constructor. * @return The property either cast to the specified type, or instantiated from a Class object. * @throws ConfigException If property has a value that cannot be converted to a boolean. */ @SuppressWarnings("unchecked") public T getTypedProperty(String name, Class type, Class def, Object...args) { rl.lock(); try { Object o = null; PropertyMap pm = getPropertyMap(prefix(name)); if (pm != null) o = pm.get(name, type, null); if (o == null && def != null) o = ClassUtils.newInstance(type, def, args); if (o == null) return null; if (ClassUtils.isParentClass(type, o.getClass())) return (T)o; throw new FormattedRuntimeException( "Invalid object of type {0} found in call to PropertyStore.getTypeProperty({1},{2},{3},...)", o.getClass(), name, type, def); } finally { rl.unlock(); } } /** * Returns a property value converted to a {@link LinkedHashMap} with the specified key and value types. * * @param name The full name of the property (e.g. "BeanContext.sortProperties") * @param keyType The class type of the keys in the map. * @param valType The class type of the values in the map. * @param def The default value if the property is not set. * @return The property value. * @throws ConfigException If property has a value that cannot be converted to a boolean. */ public Map getMap(String name, Class keyType, Class valType, Map def) { rl.lock(); try { PropertyMap pm = getPropertyMap(prefix(name)); if (pm != null) return pm.getMap(name, keyType, valType, def); return def; } finally { rl.unlock(); } } //------------------------------------------------------------------------------------- // Convenience methods. //------------------------------------------------------------------------------------- /** * Shortcut for calling getContext(BeanContext.class);. * * @return The bean context instance. */ public BeanContext getBeanContext() { return getContext(BeanContext.class); } /** * Shortcut for calling setProperty(BEAN_notBeanClasses, classes). * * @param classes The new setting value for the bean context. * @return This object (for method chaining). * @see PropertyStore#setProperty(String, Object) * @see BeanContext#BEAN_notBeanClasses */ public PropertyStore setNotBeanClasses(Class...classes) { setProperty(BEAN_notBeanClasses, classes); return this; } /** * Shortcut for calling addToProperty(BEAN_notBeanClasses, classes). * * @see PropertyStore#addToProperty(String,Object) * @param classes The new setting value for the bean context. * @return This object (for method chaining). * @see PropertyStore#addToProperty(String, Object) * @see BeanContext#BEAN_notBeanClasses */ public PropertyStore addNotBeanClasses(Class...classes) { addToProperty(BEAN_notBeanClasses, classes); return this; } /** * Shortcut for calling setProperty(BEAN_beanFilters, classes). * * @param classes The new setting value for the bean context. * @return This object (for method chaining). * @see PropertyStore#setProperty(String, Object) * @see BeanContext#BEAN_beanFilters */ public PropertyStore setBeanFilters(Class...classes) { setProperty(BEAN_beanFilters, classes); return this; } /** * Shortcut for calling addToProperty(BEAN_beanFilters, classes). * * @param classes The new setting value for the bean context. * @return This object (for method chaining). * @see PropertyStore#addToProperty(String, Object) * @see BeanContext#BEAN_beanFilters */ public PropertyStore addBeanFilters(Class...classes) { addToProperty(BEAN_beanFilters, classes); return this; } /** * Shortcut for calling setProperty(BEAN_pojoSwaps, classes). * * @param classes The new setting value for the bean context. * @return This object (for method chaining). * @see PropertyStore#setProperty(String, Object) * @see BeanContext#BEAN_pojoSwaps */ public PropertyStore setPojoSwaps(Class...classes) { setProperty(BEAN_pojoSwaps, classes); return this; } /** * Shortcut for calling addToProperty(BEAN_pojoSwaps, classes). * * @param classes The new setting value for the bean context. * @return This object (for method chaining). * @see PropertyStore#addToProperty(String, Object) * @see BeanContext#BEAN_pojoSwaps */ public PropertyStore addPojoSwaps(Class...classes) { addToProperty(BEAN_pojoSwaps, classes); return this; } /** * Shortcut for calling setProperty(BEAN_beanDictionary, classes). * * @param classes The new setting value for the bean context. * @return This object (for method chaining). * @see PropertyStore#setProperty(String, Object) * @see BeanContext#BEAN_beanDictionary */ public PropertyStore setBeanDictionary(Class...classes) { addToProperty(BEAN_beanDictionary, classes); return this; } /** * Shortcut for calling addToProperty(BEAN_beanDictionary, classes). * * @param classes The new setting value for the bean context. * @return This object (for method chaining). * @see PropertyStore#addToProperty(String, Object) * @see BeanContext#BEAN_beanDictionary */ public PropertyStore addToBeanDictionary(Class...classes) { addToProperty(BEAN_beanDictionary, classes); return this; } /** * Shortcut for calling putTo(BEAN_implCLasses, interfaceClass, implClass). * * @param interfaceClass The interface class. * @param implClass The implementation class. * @param The class type of the interface. * @return This object (for method chaining). * @see PropertyStore#putToProperty(String, Object, Object) * @see BeanContext#BEAN_implClasses */ public PropertyStore addImplClass(Class interfaceClass, Class implClass) { putToProperty(BEAN_implClasses, interfaceClass, implClass); return this; } //------------------------------------------------------------------------------------- // Object methods. //------------------------------------------------------------------------------------- @Override /* Object */ public int hashCode() { HashCode c = new HashCode(); for (PropertyMap m : properties.values()) c.add(m); return c.get(); } //-------------------------------------------------------------------------------- // Utility classes and methods. //-------------------------------------------------------------------------------- /** * Hashcode generator that treats strings and primitive values the same. * (e.g. 123 and "123" result in the same hashcode.) */ private static class NormalizingHashCode extends HashCode { @Override /* HashCode */ protected Object unswap(Object o) { return PropertyStore.unswap(o); } } /** * Contains all the properties for a particular property prefix (e.g. 'BeanContext') * *

* Instances of this map are immutable from outside this class. * *

* The {@link PropertyMap#hashCode()} and {@link PropertyMap#equals(Object)} methods can be used to compare with * other property maps. */ public class PropertyMap { private final Map map = new ConcurrentSkipListMap(); private volatile int hashCode = 0; private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock rl = lock.readLock(), wl = lock.writeLock(); private final String prefix; private PropertyMap(String prefix) { this.prefix = prefix; prefix = prefix + '.'; Properties p = System.getProperties(); for (Map.Entry e : p.entrySet()) if (e.getKey().toString().startsWith(prefix)) set(e.getKey().toString(), e.getValue()); } /** * Copy constructor. */ private PropertyMap(PropertyMap orig, PropertyMap apply) { this.prefix = apply.prefix; if (orig != null) for (Map.Entry e : orig.map.entrySet()) this.map.put(e.getKey(), Property.create(e.getValue().name, e.getValue().value())); for (Map.Entry e : apply.map.entrySet()) { Property prev = this.map.get(e.getKey()); if (prev == null || "SIMPLE".equals(prev.type)) this.map.put(e.getKey(), Property.create(e.getValue().name, e.getValue().value())); else { if ("SET".equals(prev.type) || "LIST".equals(prev.type)) prev.add(e.getValue().value()); else if ("MAP".equals(prev.type)) prev.put(e.getValue().value()); } } } /** * Returns the specified property as the specified class type. * * @param name The property name. * @param type The type of object to convert the value to. * @param def The default value if the specified property is not set. * @return The property value. */ public T get(String name, Class type, T def) { rl.lock(); try { Property p = map.get(name); if (p == null || type == null) return def; try { if (! isBeanSessionAvailable()) return def; return getBeanSession().convertToType(p.value, type); } catch (InvalidDataConversionException e) { throw new ConfigException("Could not retrieve property store property ''{0}''. {1}", p.name, e.getMessage()); } } finally { rl.unlock(); } } /** * Returns the specified property as a map with the specified key and value types. * *

* The map returned is an instance of {@link LinkedHashMap}. * * @param name The property name. * @param keyType The class type of the keys of the map. * @param valueType The class type of the values of the map. * @param def The default value if the specified property is not set. * @return The property value. */ @SuppressWarnings("unchecked") public Map getMap(String name, Class keyType, Class valueType, Map def) { rl.lock(); try { Property p = map.get(name); if (p == null || keyType == null || valueType == null) return def; try { if (isBeanSessionAvailable()) { BeanSession session = getBeanSession(); return (Map)session.convertToType(p.value, session.getClassMeta(LinkedHashMap.class, keyType, valueType)); } return def; } catch (InvalidDataConversionException e) { throw new ConfigException("Could not retrieve property store property ''{0}''. {1}", p.name, e.getMessage()); } } finally { rl.unlock(); } } /** * Convenience method for returning all values in this property map as a simple map. * *

* Primarily useful for debugging. * * @return A new {@link LinkedHashMap} with all values in this property map. */ public Map asMap() { rl.lock(); try { Map m = new LinkedHashMap(); for (Property p : map.values()) m.put(p.name, p.value); return m; } finally { rl.unlock(); } } private void set(String name, Object value) { wl.lock(); hashCode = 0; try { if (value == null) map.remove(name); else map.put(name, Property.create(name, value)); } finally { wl.unlock(); } } private void addTo(String name, Object value) { wl.lock(); hashCode = 0; try { if (! map.containsKey(name)) map.put(name, Property.create(name, Collections.emptyList())); map.get(name).add(value); } finally { wl.unlock(); } } private void putTo(String name, Object key, Object value) { wl.lock(); hashCode = 0; try { if (! map.containsKey(name)) map.put(name, Property.create(name, Collections.emptyMap())); map.get(name).put(key, value); } finally { wl.unlock(); } } private void putTo(String name, Object value) { wl.lock(); hashCode = 0; try { if (! map.containsKey(name)) map.put(name, Property.create(name, Collections.emptyMap())); map.get(name).put(value); } finally { wl.unlock(); } } private void removeFrom(String name, Object value) { wl.lock(); hashCode = 0; try { if (map.containsKey(name)) map.get(name).remove(value); } finally { wl.unlock(); } } @Override public int hashCode() { rl.lock(); try { if (hashCode == 0) { HashCode c = new HashCode().add(prefix); for (Property p : map.values()) c.add(p); this.hashCode = c.get(); } return hashCode; } finally { rl.unlock(); } } @Override public boolean equals(Object o) { rl.lock(); try { if (o instanceof PropertyMap) { PropertyMap m = (PropertyMap)o; if (m.hashCode() != hashCode()) return false; return this.map.equals(m.map); } return false; } finally { rl.unlock(); } } @Override public String toString() { return "PropertyMap(id="+System.identityHashCode(this)+")"; } } private abstract static class Property { private final String name, type; private final Object value; private static Property create(String name, Object value) { if (name.endsWith(".set")) return new SetProperty(name, value); else if (name.endsWith(".list")) return new ListProperty(name, value); else if (name.endsWith(".map")) return new MapProperty(name, value); return new SimpleProperty(name, value); } Property(String name, String type, Object value) { this.name = name; this.type = type; this.value = value; } void add(Object val) { throw new ConfigException("Cannot add value {0} ({1}) to property ''{2}'' ({3}).", JsonSerializer.DEFAULT_LAX.toString(val), getReadableClassNameForObject(val), name, type); } void remove(Object val) { throw new ConfigException("Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", JsonSerializer.DEFAULT_LAX.toString(val), getReadableClassNameForObject(val), name, type); } void put(Object val) { throw new ConfigException("Cannot put value {0} ({1}) to property ''{2}'' ({3}).", JsonSerializer.DEFAULT_LAX.toString(val), getReadableClassNameForObject(val), name, type); } void put(Object key, Object val) { throw new ConfigException("Cannot put value {0}({1})->{2}({3}) to property ''{4}'' ({5}).", JsonSerializer.DEFAULT_LAX.toString(key), getReadableClassNameForObject(key), JsonSerializer.DEFAULT_LAX.toString(val), getReadableClassNameForObject(val), name, type); } protected Object value() { return value; } @Override /* Object */ public int hashCode() { HashCode c = new NormalizingHashCode().add(name); if (value instanceof Map) { for (Map.Entry e : ((Map)value).entrySet()) c.add(e.getKey()).add(e.getValue()); } else if (value instanceof Collection) { for (Object o : (Collection)value) c.add(o); } else { c.add(value); } return c.get(); } @Override public String toString() { return "Property(name="+name+",type="+type+")"; } } private static class SimpleProperty extends Property { SimpleProperty(String name, Object value) { super(name, "SIMPLE", value); } } @SuppressWarnings({"unchecked"}) private static class SetProperty extends Property { private final Set value; private SetProperty(String name, Object value) { super(name, "SET", new ConcurrentSkipListSet(PROPERTY_COMPARATOR)); this.value = (Set)value(); add(value); } @Override void add(Object val) { if (val.getClass().isArray()) for (int i = 0; i < Array.getLength(val); i++) add(Array.get(val, i)); else if (val instanceof Collection) for (Object o : (Collection)val) add(o); else { if (val instanceof String) { String s = val.toString(); if (s.startsWith("[") && s.endsWith("]")) { try { add(new ObjectList(s)); return; } catch (Exception e) {} } } for (Object o : value) if (same(val, o)) return; value.add(val); } } @Override void remove(Object val) { if (val.getClass().isArray()) for (int i = 0; i < Array.getLength(val); i++) remove(Array.get(val, i)); else if (val instanceof Collection) for (Object o : (Collection)val) remove(o); else { if (val instanceof String) { String s = val.toString(); if (s.startsWith("[") && s.endsWith("]")) { try { remove(new ObjectList(s)); return; } catch (Exception e) {} } } for (Iterator i = value.iterator(); i.hasNext();) if (same(i.next(), val)) i.remove(); } } } @SuppressWarnings({"unchecked"}) private static class ListProperty extends Property { private final LinkedList value; private ListProperty(String name, Object value) { super(name, "LIST", new LinkedList()); this.value = (LinkedList)value(); add(value); } @Override void add(Object val) { if (val.getClass().isArray()) { for (int i = Array.getLength(val) - 1; i >= 0; i--) add(Array.get(val, i)); } else if (val instanceof List) { List l = (List)val; for (ListIterator i = l.listIterator(l.size()); i.hasPrevious();) add(i.previous()); } else if (val instanceof Collection) { List l = new ArrayList((Collection)val); for (ListIterator i = l.listIterator(l.size()); i.hasPrevious();) add(i.previous()); } else { String s = val.toString(); if (s.startsWith("[") && s.endsWith("]")) { try { add(new ObjectList(s)); return; } catch (Exception e) {} } for (Iterator i = value.iterator(); i.hasNext(); ) if (same(val, i.next())) i.remove(); value.addFirst(val); } } @Override void remove(Object val) { if (val.getClass().isArray()) for (int i = 0; i < Array.getLength(val); i++) remove(Array.get(val, i)); else if (val instanceof Collection) for (Object o : (Collection)val) remove(o); else { String s = val.toString(); if (s.startsWith("[") && s.endsWith("]")) { try { remove(new ObjectList(s)); return; } catch (Exception e) {} } for (Iterator i = value.iterator(); i.hasNext();) if (same(i.next(), val)) i.remove(); } } } @SuppressWarnings({"unchecked","rawtypes"}) private static class MapProperty extends Property { final Map value; MapProperty(String name, Object value) { // ConcurrentSkipListMap doesn't support Map.Entry.remove(), so use TreeMap instead. super(name, "MAP", Collections.synchronizedMap(new TreeMap(PROPERTY_COMPARATOR))); this.value = (Map)value(); put(value); } @Override void put(Object val) { try { if (isBeanSessionAvailable() && ! (val instanceof Map)) val = getBeanSession().convertToType(val, Map.class); if (val instanceof Map) { Map m = (Map)val; for (Map.Entry e : (Set)m.entrySet()) put(e.getKey(), e.getValue()); return; } } catch (Exception e) {} super.put(val); } @Override void put(Object key, Object val) { // ConcurrentSkipListMap doesn't support Map.Entry.remove(). for (Map.Entry e : value.entrySet()) { if (same(e.getKey(), key)) { e.setValue(val); return; } } value.put(key, val); } } /** * Converts an object to a normalized form for comparison purposes. * * @param o The object to normalize. * @return The normalized object. */ private static final Object unswap(Object o) { if (o instanceof Class) return ((Class)o).getName(); if (o instanceof Number || o instanceof Boolean) return o.toString(); return o; } private static BeanSession getBeanSession() { if (beanSession == null && BeanContext.DEFAULT != null) beanSession = BeanContext.DEFAULT.createSession(); return beanSession; } /** * Returns true if a bean session is available. * *

* Note that a bean session will not be available when constructing the BeanContext.DEFAULT context. * (it's a chicken-and-egg thing). */ private static boolean isBeanSessionAvailable() { return getBeanSession() != null; } /* * Compares two objects for "string"-equality. * Basically mean both objects are equal if they're the same when converted to strings. */ @SuppressWarnings({ "rawtypes", "unchecked" }) private static boolean same(Object o1, Object o2) { if (o1 == o2) return true; if (o1 instanceof Map) { if (o2 instanceof Map) { Map m1 = (Map)o1, m2 = (Map)o2; if (m1.size() == m2.size()) { Set s1 = m1.entrySet(), s2 = m2.entrySet(); for (Iterator i1 = s1.iterator(), i2 = s2.iterator(); i1.hasNext();) { Map.Entry e1 = i1.next(), e2 = i2.next(); if (! same(e1.getKey(), e2.getKey())) return false; if (! same(e1.getValue(), e2.getValue())) return false; } return true; } } return false; } else if (o1 instanceof Collection) { if (o2 instanceof Collection) { Collection c1 = (Collection)o1, c2 = (Collection)o2; if (c1.size() == c2.size()) { for (Iterator i1 = c1.iterator(), i2 = c2.iterator(); i1.hasNext();) { if (! same(i1.next(), i2.next())) return false; } return true; } } return false; } else { return unswap(o1).equals(unswap(o2)); } } private static String prefix(String name) { if (name == null) throw new ConfigException("Invalid property name specified: 'null'"); if (name.indexOf('.') == -1) return ""; return name.substring(0, name.indexOf('.')); } private static String className(Object o) { if (o == null) return null; if (o instanceof Class) return getReadableClassName((Class)o); return getReadableClassName(o.getClass()); } @Override /* Object */ public String toString() { rl.lock(); try { ObjectMap m = new ObjectMap(); m.put("id", System.identityHashCode(this)); m.put("hashCode", hashCode()); m.put("properties.id", System.identityHashCode(properties)); m.put("contexts.id", System.identityHashCode(contexts)); m.put("properties", properties); m.put("contexts", contexts); return m.toString(); } finally { rl.unlock(); } } }