Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id D0F7D200D5E for ; Sat, 23 Dec 2017 17:51:02 +0100 (CET) Received: by cust-asf.ponee.io (Postfix) id CF125160C2F; Sat, 23 Dec 2017 16:51:02 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id 9EA16160C1C for ; Sat, 23 Dec 2017 17:51:00 +0100 (CET) Received: (qmail 59282 invoked by uid 500); 23 Dec 2017 16:50:59 -0000 Mailing-List: contact commits-help@juneau.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@juneau.apache.org Delivered-To: mailing list commits@juneau.apache.org Received: (qmail 59273 invoked by uid 99); 23 Dec 2017 16:50:59 -0000 Received: from pnap-us-west-generic-nat.apache.org (HELO spamd1-us-west.apache.org) (209.188.14.142) by apache.org (qpsmtpd/0.29) with ESMTP; Sat, 23 Dec 2017 16:50:59 +0000 Received: from localhost (localhost [127.0.0.1]) by spamd1-us-west.apache.org (ASF Mail Server at spamd1-us-west.apache.org) with ESMTP id 4410BC0D74 for ; Sat, 23 Dec 2017 16:50:59 +0000 (UTC) X-Virus-Scanned: Debian amavisd-new at spamd1-us-west.apache.org X-Spam-Flag: NO X-Spam-Score: -4.231 X-Spam-Level: X-Spam-Status: No, score=-4.231 tagged_above=-999 required=6.31 tests=[KAM_ASCII_DIVIDERS=0.8, RCVD_IN_DNSWL_HI=-5, RCVD_IN_MSPIKE_H3=-0.01, RCVD_IN_MSPIKE_WL=-0.01, SPF_PASS=-0.001, T_RP_MATCHES_RCVD=-0.01] autolearn=disabled Received: from mx1-lw-eu.apache.org ([10.40.0.8]) by localhost (spamd1-us-west.apache.org [10.40.0.7]) (amavisd-new, port 10024) with ESMTP id q5IHUkYSkdY7 for ; Sat, 23 Dec 2017 16:50:53 +0000 (UTC) Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx1-lw-eu.apache.org (ASF Mail Server at mx1-lw-eu.apache.org) with SMTP id 477055F3FF for ; Sat, 23 Dec 2017 16:50:47 +0000 (UTC) Received: (qmail 59030 invoked by uid 99); 23 Dec 2017 16:50:46 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Sat, 23 Dec 2017 16:50:46 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 320F1DFC32; Sat, 23 Dec 2017 16:50:43 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: jamesbognar@apache.org To: commits@juneau.incubator.apache.org Date: Sat, 23 Dec 2017 16:50:55 -0000 Message-Id: In-Reply-To: <77ee031a87da47bca49cf99a70ac04bd@git.apache.org> References: <77ee031a87da47bca49cf99a70ac04bd@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [14/21] juneau git commit: JUNEAU-76 Improvements to Context/Builder APIs archived-at: Sat, 23 Dec 2017 16:51:03 -0000 http://git-wip-us.apache.org/repos/asf/juneau/blob/227719b2/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStore.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStore.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStore.java deleted file mode 100644 index c8d4325..0000000 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStore.java +++ /dev/null @@ -1,1485 +0,0 @@ -// *************************************************************************************************************************** -// * 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<>(); - - // 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<>(); - - 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 volatile static BeanSession beanSession; - - // Used to keep properties in alphabetical order regardless of whether - // they're not strings. - static final 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 = getBeanSession().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.) - */ - static final 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. - */ - final 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; - - 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. - */ - 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. - */ - 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") - 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. - */ - 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(); - } - } - - 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(); - } - } - - 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(); - } - } - - 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(); - } - } - - 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(); - } - } - - 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 { - final String name, type; - final Object value; - - static final 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 final class SimpleProperty extends Property { - - SimpleProperty(String name, Object value) { - super(name, "SIMPLE", value); - } - } - - @SuppressWarnings({"unchecked"}) - private static final class SetProperty extends Property { - private final Set value; - - 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 final class ListProperty extends Property { - private final LinkedList value; - - 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 final 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. - */ - static Object unswap(Object o) { - if (o instanceof Class) - return ((Class)o).getName(); - if (o instanceof Number || o instanceof Boolean) - return o.toString(); - return o; - } - - 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). - */ - 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" }) - 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(); - } - } -} http://git-wip-us.apache.org/repos/asf/juneau/blob/227719b2/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStore2.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStore2.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStore2.java new file mode 100644 index 0000000..87e9d89 --- /dev/null +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStore2.java @@ -0,0 +1,748 @@ +// *************************************************************************************************************************** +// * 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.PropertyType.*; + +import java.lang.reflect.*; +import java.util.*; + +import org.apache.juneau.PropertyStoreBuilder.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.json.*; +import org.apache.juneau.serializer.*; + + +/** + * Represents an immutable collection of properties. + * + *

+ * The general idea behind a property store is to serve as a reusable configuration of an artifact (e.g. a Serializer) + * such that the artifact can be cached and reused if the property stores are 'equal'. + * + *

+ * For example, two serializers of the same type created with the same configuration will always end up being + * the same serializer: + * + *

+ * WriterSerializer s1 = JsonSerializer.create().pojoSwaps(MySwap.class).simple().build(); + * WriterSerializer s2 = JsonSerializer.create().simple().pojoSwaps(MySwap.class).build(); + * assert(s1 == s2); + *

+ * + *

+ * This has the effect of significantly improving performance, especially if you're creating many Serializers and + * Parsers. + * + *

+ * Property names must have the following format... + *

+ * "{group}.{name}.{type}" + *

+ * ...where the parts consist of the following... + *
    + *
  • "{group}" - The group name of the property (e.g. "JsonSerializer"). + *
    It's always going to be the simple class name of the class it's associated with. + *
  • "{name}" - The property name (e.g. "useWhitespace"). + *
  • "{type}" - The property data type. + *
    A 1 or 2 character string that identifies the data type of the property. + *
    Valid values are: + *
      + *
    • "s": String + *
    • "b": Boolean + *
    • "i": Integer + *
    • "c": Class + *
    • "o": Object + *
    • "ss": TreeSet<String> + *
    • "si": TreeSet<Integer> + *
    • "sc": TreeSet<Class> + *
    • "ls": ArrayList<String> + *
    • "li": ArrayList<Integer> + *
    • "lc": ArrayList<Class> + *
    • "lo": ArrayList<Object> + *
    • "ms": TreeMap<String,String> + *
    • "mi": TreeMap<String,Integer> + *
    • "mc": TreeMap<String,Class> + *
    • "mo": TreeMap<String,Object> + *
    + *
+ * + *

+ * For example, the {@link JsonSerializer} extends from {@link Serializer} and {@link BeanContext}, and therefore + * is configured by properties defined in the "JsonSerializer", "Serializer", and "BeanContext" + * groups. Since serializers and bean contexts can be relatively expensive to instantiate, we can cache and + * reuse JsonSerializer instances if the property store used to instantiate them have equivalent + * "JsonSerializer", "Serializer", and "BeanContext" groups. + * + *

+ * Set and list properties have the additional convenience 'command' names for adding and removing entries: + *

+ * "{group}.{name}.{type}/add" // Add a value to the set/list. + * "{group}.{name}.{type}/remove" // Remove a value from the set/list. + *

+ * + *

+ * Set properties are typically used in cases where the order of the value is not important. + * A TreeSet is used so that the order in which you add elements does not affect the resulting order + * of the property. + * + *

+ * List properties are typically used in cases where the order in which entries are added is important. + * Adding to a list property will cause the new entries to be added to the BEGINNING of the list. + * This ensures that the resulting order of the list is in most-to-least importance. + * For example, multiple calls to pojoSwaps() causes new entries to be added to the beginning of the list + * so that previous values can be 'overridden': + *

+ * // Swap order: [MySwap2.class, MySwap1.class] + * JsonSerializer.create().pojoSwaps(MySwap1.class).pojoSwaps(MySwap2.class).build(); + *

+ *

+ * Note that the order is different when passing multiple values into the pojoSwaps() method, in which + * case the order should be first-match-wins: + *

+ * // Swap order: [MySwap1.class, MySwap2.class] + * JsonSerializer.create().pojoSwaps(MySwap1.class,MySwap2.class).build(); + *

+ * + *

+ * Map properties have the additional convenience property name for adding and removing map entries: + *

+ * "{group}.{name}.{type}/add.{key}" // Add a map entry (or delete if the value is null). + *

+ * + *

+ * Property stores are entirely immutable which means their hashcode is calculated exactly once, meaning they + * are particularly good for use as hashmap keys. + */ +@SuppressWarnings("unchecked") +public final class PropertyStore2 { + + /** + * A default empty property store. + */ + public static PropertyStore2 DEFAULT = PropertyStore2.create().build(); + + final SortedMap groups; + private final int hashCode; + + // Created by PropertyStoreBuilder.build() + PropertyStore2(Map propertyMaps) { + TreeMap m = new TreeMap<>(); + for (Map.Entry p : propertyMaps.entrySet()) + m.put(p.getKey(), p.getValue().build()); + this.groups = Collections.unmodifiableSortedMap(m); + this.hashCode = groups.hashCode(); + } + + /** + * Creates a new empty builder for a property store. + * + * @return A new empty builder for a property store. + */ + public static PropertyStoreBuilder create() { + return new PropertyStoreBuilder(); + } + + /** + * Creates a new property store builder initialized with the values in this property store. + * + * @return A new property store builder. + */ + public PropertyStoreBuilder builder() { + return new PropertyStoreBuilder(this); + } + + private Property findProperty(String key) { + String g = group(key); + String k = key.substring(g.length()+1); + PropertyGroup pm = groups.get(g); + + if (pm != null) { + Property p = pm.get(k); + if (p != null) + return p; + } + + String s = System.getProperty(key); + if (s != null) + return PropertyStoreBuilder.MutableProperty.create(k, s).build(); + + return null; + } + + /** + * Returns the raw property value with the specified name. + * + * @param key The property name. + * @return The property value, or null if it doesn't exist. + */ + public Object getProperty(String key) { + Property p = findProperty(key); + return p == null ? null : p.value; + } + + /** + * Returns the property value with the specified name. + * + * @param key The property name. + * @param c The class to cast or convert the value to. + * @param def The default value. + * @return The property value, or the default value if it doesn't exist. + */ + public T getProperty(String key, Class c, T def) { + Property p = findProperty(key); + return p == null ? def : p.as(c); + } + + /** + * Returns the class property with the specified name. + * + * @param key The property name. + * @param type The class type of the property. + * @param def The default value. + * @return The property value, or the default value if it doesn't exist. + */ + public Class getClassProperty(String key, Class type, Class def) { + Property p = findProperty(key); + return p == null ? def : (Class)p.as(Class.class); + } + + /** + * Returns the array property value with the specified name. + * + * @param key The property name. + * @param eType The class type of the elements in the property. + * @return The property value, or an empty array if it doesn't exist. + */ + public T[] getArrayProperty(String key, Class eType) { + Property p = findProperty(key); + return (T[]) (p == null ? Array.newInstance(eType, 0) : p.asArray(eType)); + } + + /** + * Returns the array property value with the specified name. + * + * @param key The property name. + * @param eType The class type of the elements in the property. + * @param def The default value. + * @return The property value, or an empty array if it doesn't exist. + */ + public T[] getArrayProperty(String key, Class eType, T[] def) { + Property p = findProperty(key); + return p == null ? def : p.asArray(eType); + } + + /** + * Returns the class array property with the specified name. + * + * @param key The property name. + * @return The property value, or an empty array if it doesn't exist. + */ + public Class[] getClassArrayProperty(String key) { + Property p = findProperty(key); + return p == null ? new Class[0] : p.as(Class[].class); + } + + /** + * Returns the class array property with the specified name. + * + * @param key The property name. + * @param def The default value. + * @return The property value, or an empty array if it doesn't exist. + */ + public Class[] getClassArrayProperty(String key, Class[] def) { + Property p = findProperty(key); + return p == null ? def : p.as(Class[].class); + } + + /** + * Returns the class array property with the specified name. + * + * @param key The property name. + * @param eType The class type of the elements in the property. + * @return The property value, or an empty array if it doesn't exist. + */ + public Class[] getClassArrayProperty(String key, Class eType) { + Property p = findProperty(key); + return p == null ? new Class[0] : p.as(Class[].class); + } + + /** + * Returns the set property with the specified name. + * + * @param key The property name. + * @param eType The class type of the elements in the property. + * @return The property value as an unmodifiable LinkedHashSet, or an empty set if it doesn't exist. + */ + public Set getSetProperty(String key, Class eType) { + Property p = findProperty(key); + return p == null ? Collections.EMPTY_SET : p.asSet(eType); + } + + /** + * Returns the class set property with the specified name. + * + * @param key The property name. + * @return The property value as an unmodifiable LinkedHashSet, or an empty set if it doesn't exist. + */ + public Set> getClassSetProperty(String key) { + Property p = findProperty(key); + return p == null ? Collections.EMPTY_SET : p.asSet(Class.class); + } + + /** + * Returns the class set property with the specified name. + * + * @param key The property name. + * @param eType The class type of the elements in the property. + * @return The property value as an unmodifiable LinkedHashSet, or an empty set if it doesn't exist. + */ + public Set> getClassSetProperty(String key, Class eType) { + Property p = findProperty(key); + return p == null ? Collections.EMPTY_SET : p.asSet(Class.class); + } + + /** + * Returns the list property with the specified name. + * + * @param key The property name. + * @param eType The class type of the elements in the property. + * @return The property value as an unmodifiable ArrayList, or an empty list if it doesn't exist. + */ + public List getListProperty(String key, Class eType) { + Property p = findProperty(key); + return p == null ? Collections.EMPTY_LIST : p.asList(eType); + } + + /** + * Returns the class list property with the specified name. + * + * @param key The property name. + * @return The property value as an unmodifiable ArrayList, or an empty list if it doesn't exist. + */ + public List> getClassListProperty(String key) { + Property p = findProperty(key); + return p == null ? Collections.EMPTY_LIST : p.asList(Class.class); + } + + /** + * Returns the class list property with the specified name. + * + * @param key The property name. + * @param eType The class type of the elements in the property. + * @return The property value as an unmodifiable ArrayList, or an empty list if it doesn't exist. + */ + public List> getClassListProperty(String key, Class eType) { + Property p = findProperty(key); + return p == null ? Collections.EMPTY_LIST : p.asList(Class.class); + } + + /** + * Returns the map property with the specified name. + * + * @param key The property name. + * @param eType The class type of the elements in the property. + * @return The property value as an unmodifiable LinkedHashMap, or an empty map if it doesn't exist. + */ + public Map getMapProperty(String key, Class eType) { + Property p = findProperty(key); + return p == null ? Collections.EMPTY_MAP : p.asMap(eType); + } + + /** + * Returns the class map property with the specified name. + * + * @param key The property name. + * @return The property value as an unmodifiable LinkedHashMap, or an empty map if it doesn't exist. + */ + public Map> getClassMapProperty(String key) { + Property p = findProperty(key); + return p == null ? Collections.EMPTY_MAP : p.asMap(Class.class); + } + + /** + * Returns the class map property with the specified name. + * + * @param key The property name. + * @param eType The class type of the elements in the property. + * @return The property value as an unmodifiable LinkedHashMap, or an empty map if it doesn't exist. + */ + public Map> getClassMapProperty(String key, Class eType) { + Property p = findProperty(key); + return p == null ? Collections.EMPTY_MAP : p.asMap(Class.class); + } + + /** + * Returns an instance of the specified class or string property. + * + * @param key The property name. + * @param type The class type of the property. + * @param def + * The default value if the property doesn't exist. + *
Can either be an instance of T, or a Class<? extends T>. + * @return A new property instance. + */ + public T getInstanceProperty(String key, Class type, Object def) { + Property p = findProperty(key); + if (p != null) + return p.asInstance(type); + if (def == null) + return null; + if (def instanceof Class) { + T t = ClassUtils.newInstance(type, def); + if (t != null) + return t; + } + if (type.isInstance(def)) + return (T)def; + throw new ConfigException("Could not instantiate property ''{0}'' as type ''{1}'' with default value ''{2}''", key, type, def); + } + + /** + * Returns the specified property as an array of instantiated objects. + * + * @param key The property name. + * @param type The class type of the property. + * @param def The default object to return if the property doesn't exist. + * @return A new property instance. + */ + public T[] getInstanceArrayProperty(String key, Class type, T[] def) { + Property p = findProperty(key); + return p == null ? def : p.asInstanceArray(type); + } + + /** + * Returns the keys found in the specified property group. + * + *

+ * The keys are NOT prefixed with group names. + * + * @param group The group name. + * @return The set of property keys, or an empty set if the group was not found. + */ + public Set getPropertyKeys(String group) { + if (group == null) + return Collections.EMPTY_SET; + PropertyGroup g = groups.get(group); + return g == null ? Collections.EMPTY_SET : g.keySet(); + } + + @Override /* Object */ + public int hashCode() { + return hashCode; + } + + /** + * Returns a hashcode of this property store using only the specified group names. + * + * @param groups The names of the property groups to use in the calculation. + * @return The hashcode. + */ + public Integer hashCode(String...groups) { + HashCode c = new HashCode(); + for (String p : groups) + if (p != null) + c.add(p).add(this.groups.get(p)); + return c.get(); + } + + @Override /* Object */ + public boolean equals(Object o) { + if (this == o) + return true; + if (o instanceof PropertyStore2) + return (this.groups.equals(((PropertyStore2)o).groups)); + return false; + } + + /** + * Compares two property stores, but only based on the specified group names. + * + * @param ps The property store to compare to. + * @param groups The groups to compare. + * @return true if the two property stores are equal in the specified groups. + */ + public boolean equals(PropertyStore2 ps, String...groups) { + if (this == ps) + return true; + for (String p : groups) { + if (p != null) { + PropertyGroup pg1 = this.groups.get(p), pg2 = ps.groups.get(p); + if (pg1 == null && pg2 == null) + continue; + if (pg1 == null || pg2 == null) + return false; + if (! pg1.equals(pg2)) + return false; + } + } + return true; + } + + /** + * Used for debugging. + * + *

+ * Allows property stores to be serialized to easy-to-read JSON objects. + * + * @param beanSession The bean session. + * @return The property groups. + */ + public Map swap(BeanSession beanSession) { + return groups; + } + + //------------------------------------------------------------------------------------------------------------------- + // PropertyGroup + //------------------------------------------------------------------------------------------------------------------- + + static class PropertyGroup { + final SortedMap properties; + private final int hashCode; + + PropertyGroup(Map properties) { + TreeMap m = new TreeMap<>(); + for (Map.Entry p : properties.entrySet()) + m.put(p.getKey(), p.getValue().build()); + this.properties = Collections.unmodifiableSortedMap(m); + this.hashCode = this.properties.hashCode(); + } + + PropertyGroupBuilder builder() { + return new PropertyGroupBuilder(properties); + } + + Property get(String key) { + return properties.get(key); + } + + @Override /* Object */ + public int hashCode() { + return hashCode; + } + + @Override /* Object */ + public boolean equals(Object o) { + if (o instanceof PropertyGroup) + return properties.equals(((PropertyGroup)o).properties); + return false; + } + + Set keySet() { + return properties.keySet(); + } + + public Map swap(BeanSession beanSession) { + return properties; + } + } + + //------------------------------------------------------------------------------------------------------------------- + // Property + //------------------------------------------------------------------------------------------------------------------- + + static class Property { + private final String name; + final Object value; + private final int hashCode; + private final PropertyType type; + + Property(String name, Object value, PropertyType type) { + this.name = name; + this.value = value; + this.type = type; + this.hashCode = value.hashCode(); + } + + MutableProperty mutable() { + switch(type) { + case STRING: + case BOOLEAN: + case INTEGER: + case CLASS: + case OBJECT: return new MutableSimpleProperty(name, type, value); + case SET_STRING: + case SET_INTEGER: + case SET_CLASS: return new MutableSetProperty(name, type, value); + case LIST_STRING: + case LIST_INTEGER: + case LIST_CLASS: + case LIST_OBJECT: return new MutableListProperty(name, type, value); + case MAP_STRING: + case MAP_INTEGER: + case MAP_CLASS: + case MAP_OBJECT: return new MutableMapProperty(name, type, value); + } + throw new ConfigException("Invalid type specified: ''{0}''", type); + } + + public T as(Class c) { + Class c2 = ClassUtils.getPrimitiveWrapper(c); + if (c2 != null) + c = (Class)c2; + if (c.isInstance(value)) + return (T)value; + if (c.isArray() && value instanceof Collection) + return (T)asArray(c.getComponentType()); + if (type == STRING) { + T t = ClassUtils.fromString(c, value.toString()); + if (t != null) + return t; + } + throw new ConfigException("Invalid property conversion ''{0}'' to ''{1}'' on property ''{2}''", type, c, name); + } + + public T[] asArray(Class eType) { + if (value instanceof Collection) { + Collection l = (Collection)value; + Object t = Array.newInstance(eType, l.size()); + int i = 0; + for (Object o : l) { + Object o2 = null; + if (eType.isInstance(o)) + o2 = o; + else if (type == SET_STRING || type == LIST_STRING) { + o2 = ClassUtils.fromString(eType, o.toString()); + if (o2 == null) + throw new ConfigException("Invalid property conversion ''{0}'' to ''{1}[]'' on property ''{2}''", type, eType, name); + } else { + throw new ConfigException("Invalid property conversion ''{0}'' to ''{1}[]'' on property ''{2}''", type, eType, name); + } + Array.set(t, i++, o2); + } + return (T[])t; + } + throw new ConfigException("Invalid property conversion ''{0}'' to ''{1}[]'' on property ''{2}''", type, eType, name); + } + + public Set asSet(Class eType) { + if (type == SET_STRING && eType == String.class || type == SET_INTEGER && eType == Integer.class || type == SET_CLASS && eType == Class.class) { + return (Set)value; + } else if (type == SET_STRING) { + Set s = new LinkedHashSet<>(); + for (Object o : (Set)value) { + T t = ClassUtils.fromString(eType, o.toString()); + if (t == null) + throw new ConfigException("Invalid property conversion ''{0}'' to ''Set<{1}>'' on property ''{2}''", type, eType, name); + s.add(t); + } + return Collections.unmodifiableSet(s); + } else { + throw new ConfigException("Invalid property conversion ''{0}'' to ''Set<{1}>'' on property ''{2}''", type, eType, name); + } + } + + public List asList(Class eType) { + if (type == LIST_STRING && eType == String.class || type == LIST_INTEGER && eType == Integer.class || type == LIST_CLASS && eType == Class.class || type == LIST_OBJECT) { + return (List)value; + } else if (type == PropertyType.LIST_STRING) { + List l = new ArrayList<>(); + for (Object o : (List)value) { + T t = ClassUtils.fromString(eType, o.toString()); + if (t == null) + throw new ConfigException("Invalid property conversion ''{0}'' to ''List<{1}>'' on property ''{2}''", type, eType, name); + l.add(t); + } + return Collections.unmodifiableList(l); + } else { + throw new ConfigException("Invalid property conversion ''{0}'' to ''List<{1}>'' on property ''{2}''", type, eType, name); + } + } + + public Map asMap(Class eType) { + if (type == MAP_STRING && eType == String.class || type == MAP_INTEGER && eType == Integer.class || type == MAP_CLASS && eType == Class.class || type == MAP_OBJECT) { + return (Map)value; + } else if (type == MAP_STRING) { + Map m = new LinkedHashMap<>(); + for (Map.Entry e : ((Map)value).entrySet()) { + T t = ClassUtils.fromString(eType, e.getValue()); + if (t == null) + throw new ConfigException("Invalid property conversion ''{0}'' to ''Map'' on property ''{2}''", type, eType, name); + m.put(e.getKey(), t); + } + return Collections.unmodifiableMap(m); + } else { + throw new ConfigException("Invalid property conversion ''{0}'' to ''Map'' on property ''{2}''", type, eType, name); + } + } + + public T asInstance(Class iType) { + T t = null; + if (type == STRING) + t = ClassUtils.fromString(iType, value.toString()); + else if (type == OBJECT && iType.isInstance(value)) + t = (T)value; + else if (type == CLASS) + t = ClassUtils.newInstance(iType, value); + if (t != null) + return t; + throw new ConfigException("Invalid property instantiation ''{0}'' to ''{1}'' on property ''{2}''", type, iType, name); + } + + public T[] asInstanceArray(Class eType) { + if (value instanceof Collection) { + Collection l = (Collection)value; + Object t = Array.newInstance(eType, l.size()); + int i = 0; + for (Object o : l) { + Object o2 = null; + if (eType.isInstance(o)) + o2 = o; + else if (type == SET_STRING || type == LIST_STRING) + o2 = ClassUtils.fromString(eType, o.toString()); + else if (type == SET_CLASS || type == LIST_CLASS) + o2 = ClassUtils.newInstance(eType, o); + if (o2 == null) + throw new ConfigException("Invalid property conversion ''{0}'' to ''{1}[]'' on property ''{2}''", type, eType, name); + Array.set(t, i++, o2); + } + return (T[])t; + } + throw new ConfigException("Invalid property conversion ''{0}'' to ''{1}[]'' on property ''{2}''", type, eType, name); + } + + @Override /* Object */ + public int hashCode() { + return hashCode; + } + + @Override /* Object */ + public boolean equals(Object o) { + if (o instanceof Property) + return ((Property)o).value.equals(value); + return false; + } + + public Object swap(BeanSession beanSession) { + return value; + } + } + + //------------------------------------------------------------------------------------------------------------------- + // Utility methods + //------------------------------------------------------------------------------------------------------------------- + + private static String group(String key) { + if (key == null || key.indexOf('.') == -1 || key.charAt(key.length()-1) == '.') + throw new ConfigException("Invalid property name specified: ''{0}''", key); + String g = key.substring(0, key.indexOf('.')); + if (g.isEmpty()) + throw new ConfigException("Invalid property name specified: ''{0}''", key); + return g; + } + + @Override /* Object */ + public String toString() { + return JsonSerializer.DEFAULT_LAX.toString(this); + } +}