http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/ContextFactory.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/ContextFactory.java b/juneau-core/src/main/java/org/apache/juneau/ContextFactory.java new file mode 100644 index 0000000..bc957ca --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/ContextFactory.java @@ -0,0 +1,1301 @@ +/*************************************************************************************************************************** + * 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 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 factory for instantiating {@link Context} objects. + * <p> + * The hierarchy of these objects are... + * <ul class='spaced-list'> + * <li>{@link ContextFactory} - A thread-safe, modifiable context property store.<br> + * Used to create {@link Context} objects. + * <li>{@link Context} - A reusable, cachable, thread-safe, read-only context with configuration properties copied from the factory.<br> + * Often used to create {@link Session} objects. + * <li>{@link Session} - A one-time-use non-thread-safe object.<br> + * Used by serializers and parsers to retrieve context properties and to be used as scratchpads. + * </ul> + * + * + * <h6 class='topic'>ContextFactory objects</h6> + * <p> + * Context factories can be thought of as consisting of the following: + * <ul class='spaced-list'> + * <li>A <code>Map<String,Object></code> of context properties. + * <li>A <code>Map<Class,Context></code> of context instances. + * </ul> + * <p> + * Context factories are used to create and cache {@link Context} objects using the {@link #getContext(Class)} method. + * <p> + * As a general rule, {@link ContextFactory} objects are 'slow'.<br> + * Setting and retrieving properties on a factory can involve relatively slow data conversion and synchronization.<br> + * However, the {@link #getContext(Class)} method is fast, and will return cached context objects if the context properties have not changed. + * <p> + * Context factories can be used to store context properties for a variety of contexts.<br> + * For example, a single factory 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.<br> + * <p> + * Other notes: + * <ul class='spaced-list'> + * <li>Context factories can be locked using the {@link #lock()} method.<br> + * This prevents the context properties from being further modified. + * <li>Context factories can be cloned using the {@link #clone} method.<br> + * This will return a new unlocked factory with the same context properties. + * </ul> + * + * <h6 class='topic'>Context properties</h6> + * <p> + * Context properties are 'settings' for serializers and parsers.<br> + * For example, the {@link BeanContext#BEAN_sortProperties} context property defines whether + * bean properties should be serialized in alphabetical order. + * <p> + * Each {@link Context} object should contain the context properties that apply to it as static + * fields (e.g {@link BeanContext#BEAN_sortProperties}). + * <p> + * Context properties can be of the following types: + * <ul class='spaced-list'> + * <li><l>SIMPLE</l> - A simple property.<br> + * Examples include: booleans, integers, Strings, Classes, etc...<br> + * <br> + * An example of this would be the {@link BeanContext#BEAN_sortProperties} property.<br> + * It's name is simply <js>"BeanContext.sortProperties"</js>. + * + * <li><l>SET</l> - A sorted set of objects.<br> + * These are denoted by appending <js>".set"</js> to the property name.<br> + * Objects can be of any type, even complex types.<br> + * Sorted sets use tree sets to maintain the value in alphabetical order.<br> + * <br> + * For example, the {@link BeanContext#BEAN_notBeanClasses} property is used to store classes that should not be treated like beans.<br> + * It's name is <js>"BeanContext.notBeanClasses.set"</js>. + * + * <li><l>LIST</l> - A list of unique objects.<br> + * These are denoted by appending <js>".list"</js> to the property name.<br> + * Objects can be of any type, even complex types.<br> + * 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).<br> + * <br> + * For example, the {@link BeanContext#BEAN_transforms} property is used to store transform classes.<br> + * It's name is <js>"BeanContext.transforms.list"</js>. + * + * <li><l>MAP</l> - A sorted map of key-value pairs.<br> + * These are denoted by appending <js>".map"</js> to the property name.<br> + * Keys can be any type directly convertable to and from Strings. + * Values can be of any type, even complex types.<br> + * <br> + * For example, the {@link BeanContext#BEAN_implClasses} property is used to specify the names of implementation classes for interfaces.<br> + * It's name is <js>"BeanContext.implClasses.map"</js>.<br> + * </ul> + * <p> + * All context properties are set using the {@link #setProperty(String, Object)} method. + * <p> + * Default values for context properties can be specified globally as system properties.<br> + * Example: <code>System.<jsm>setProperty</jsm>(<jsf>BEAN_sortProperties</jsf>, <jk>true</jk>);</code> + * <p> + * 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. + * <p> + * SET and LIST properties can also be added to and removed from by appending <js>".add"</js> or <js>".remove"</js> to the property name and using the {@link #setProperty(String, Object)} method. + * <p> + * The following shows the two different ways to append to a set or list property: + * <p class='bcode'> + * Config config = <jk>new</jk> Config().set(<js>"BeanContext.notBeanClasses.set"</js>, Collections.<jsm>emptySet</jsm>()); + * + * <jc>// Append to set property using addTo().</jc> + * config.addTo(<js>"BeanContext.notBeanClasses.set"</js>, MyNotBeanClass.<jk>class</jk>); + * + * <jc>// Append to set property using set().</jc> + * config.set(<js>"BeanContext.notBeanClasses.set.add"</js>, MyNotBeanClass.<jk>class</jk>); + * </p> + * <p> + * Lists are appended to the beginning of the set so that behavior can be overridden.<br> + * <p> + * For sample, the following code shows the order in which POJO transforms are applied.<br> + * In this case, we want F3 and F4 to appear at the beginning of the set so that they + * take precedence over F1 and F2.... + * <p class='bcode'> + * <jc>// Result will be F3,F4,F1,F2</jc> + * config.addTo(<js>"BeanContext.transforms.list"</js>, Arrays.<jsm>asList</jsm>(F1.<jk>class</jk>, F2.<jk>class</jk>)); + * config.addTo(<js>"BeanContext.transforms.list"</js>, Arrays.<jsm>asList</jsm>(F3.<jk>class</jk>,F4.<jk>class</jk>)); + * </p> + * <p> + * SET and LIST properties can also be set and manipulated using JSON strings. + * <p class='bcode'> + * ContextFactory f = ContextFactory.<jsm>create</jsm>(); + * + * <jc>// Set SET value using JSON array. + * f.set(<js>"BeanContext.notBeanClasses.set"</js>, <js>"['com.my.MyNotBeanClass1']"</js>); + * + * <jc>// Add to SET using simple string. + * f.addTo(<js>"BeanContext.notBeanClasses.set"</js>, <js>"com.my.MyNotBeanClass2"</js>); + * + * <jc>// Add an array of values as a JSON array.. + * f.addTo(<js>"BeanContext.notBeanClasses.set"</js>, <js>"['com.my.MyNotBeanClass3']"</js>); + * + * <jc>// Remove an array of values as a JSON array.. + * f.removeFrom(<js>"BeanContext.notBeanClasses.set"</js>, <js>"['com.my.MyNotBeanClass3']"</js>); + * </p> + * <p> + * MAP properties can be added to using the {@link #putToProperty(String, Object, Object)} and {@link #putToProperty(String, Object)} methods.<br> + * MAP property entries can be removed by setting the value to <jk>null</jk> (e.g. <code>config.putTo(<js>"BEAN_implClasses"</js>, MyNotBeanClass.<jk>class</jk>, <jk>null</jk>);</code>.<br> + * MAP properties can also be added to by appending <js>".put"</js> to the property name and using the {@link #setProperty(String, Object)} method.<br> + * <p> + * The following shows the two different ways to append to a set property: + * <p class='bcode'> + * ContextFactory f = ContextFactory.<jsm>create</jsm>().set(<js>"BeanContext.implClasses.map"</js>, Collections.<jsm>emptyMap</jsm>()); + * + * <jc>// Append to map property using putTo().</jc> + * f.putTo(<js>"BeanContext.implClasses.map"</js>, MyInterface.<jk>class</jk>, MyInterfaceImpl.<jk>class</jk>); + * + * <jc>// Append to map property using set().</jc> + * Map m = <jk>new</jk> HashMap(){{put(MyInterface.<jk>class</jk>,MyInterfaceImpl.<jk>class</jk>)}}; + * f.set(<js>"BeanContext.implClasses.map.put"</js>, m); + * </p> + * <p> + * MAP properties can also be set and manipulated using JSON strings. + * <p class='bcode'> + * ContextFactory f = ContextFactory.<jsm>create</jsm>(); + * + * <jc>// Set MAP value using JSON object.</jc> + * f.set(<js>"BeanContext.implClasses.map"</js>, <js>"{'com.my.MyInterface1':'com.my.MyInterfaceImpl1'}"</js>); + * + * <jc>// Add to MAP using JSON object.</jc> + * f.putTo(<js>"BeanContext.implClasses.map"</js>, <js>"{'com.my.MyInterface2':'com.my.MyInterfaceImpl2'}"</js>); + * + * <jc>// Remove from MAP using JSON object.</jc> + * f.putTo(<js>"BeanContext.implClasses.map"</js>, <js>"{'com.my.MyInterface2':null}"</js>); + * </p> + * <p> + * Context properties are retrieved from this factory using the following 3 methods: + * <ul class='spaced-list'> + * <li>{@link #getProperty(String, Class, Object)} - Retrieve a SIMPLE or SET property converted to the specified class type. + * <li>{@link #getMap(String, Class, Class, Map)} - Retrieve a MAP property with keys/values converted to the specified class types. + * <li>{@link #getPropertyMap(String)} - Retrieve a map of all context properties with the specified prefix (e.g. <js>"BeanContext"</js> for {@link BeanContext} properties). + * </ul> + * <p> + * As a general rule, only {@link Context} objects will use these read methods. + * + * + * <h6 class='topic'>Context objects</h6> + * <p> + * A Context object can be thought of as unmodifiable snapshot of a factory.<br> + * They should be 'fast' by avoiding synchronization by using final fields whenever possible.<br> + * However, they MUST be thread safe. + * <p> + * Context objects are created using the {@link #getContext(Class)} method.<br> + * As long as the properties on a factory have not been modified, the factory will return a cached copy + * of a context. + * <p class='bcode'> + * ContextFactory f = ContextFactory.<jsm>create</jsm>(); + * + * <jc>// Get BeanContext with default factory settings.</jc> + * BeanContext bc = f.getContext(BeanContext.<jk>class</jk>); + * + * <jc>// Get another one. This will be the same one.</jc> + * BeanContext bc2 = f.getContext(BeanContext.<jk>class</jk>); + * <jsm>assertTrue</jsm>(bc1 == bc2); + * + * <jc>// Set a property.</jc> + * f.set(<jsf>BEAN_sortProperties</jsf>, <jk>true</jk>); + * + * <jc>// Get another one. This will be different!</jc> + * bc2 = f.getContext(BeanContext.<jk>class</jk>); + * <jsm>assertFalse</jsm>(bc1 == bc2); + * </p> + * + * + * <h6 class='topic'>Session objects</h6> + * <p> + * Session objects are created through {@link Context} objects, typically through a <code>createContext()</code> method.<br> + * Unlike context objects, they are NOT reusable and NOT thread safe.<br> + * They are meant to be used one time and then thrown away.<br> + * They should NEVER need to use synchronization. + * <p> + * 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. + * + * + * @author James Bognar ([email protected]) + */ +public final class ContextFactory extends Lockable { + + // All configuration properties in this object. + // Keys are property prefixes (e.g. 'BeanContext'). + // Values are maps containing properties for that specific prefix. + private Map<String,PropertyMap> properties = new ConcurrentSkipListMap<String,PropertyMap>(); + + // Context cache. + // This gets cleared every time any properties change on this object. + private final Map<Class<? extends Context>,Context> contexts = new ConcurrentHashMap<Class<? extends Context>,Context>(); + + // Global Context cache. + // Context factories 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<Integer, ConcurrentHashMap<Class<? extends Context>,Context>> globalContextCache = new ConcurrentHashMap<Integer, ConcurrentHashMap<Class<? extends Context>,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; + + // Used to keep properties in alphabetical order regardless of whether + // they're not strings. + private static Comparator<Object> PROPERTY_COMPARATOR = new Comparator<Object>() { + @Override + public int compare(Object o1, Object o2) { + return normalize(o1).toString().compareTo(normalize(o2).toString()); + } + }; + + /** + * Create a new context factory with default settings. + * + * @return A new context factory with default settings. + */ + public static ContextFactory create() { + ContextFactory f = new ContextFactory(); + BeanContext.loadDefaults(f); + return f; + } + + /** + * Create a new context factory with settings copied from the specified factory. + * + * @param copyFrom The existing factory to copy properties from. + * @return A new context factory with default settings. + */ + public static ContextFactory create(ContextFactory copyFrom) { + return new ContextFactory().copyFrom(copyFrom); + } + + + ContextFactory() {} + + /** + * Copy constructor. + * + * @param copyFrom The factory to copy properties from. + */ + public ContextFactory(ContextFactory copyFrom) { + copyFrom(copyFrom); + } + + /** + * Copies the properties from the specified factory into this factory. + * + * @param cf The factory to copy from. + * @return This object (for method chaining). + */ + public ContextFactory copyFrom(ContextFactory cf) { + for (Map.Entry<String,PropertyMap> e : cf.properties.entrySet()) + this.properties.put(e.getKey(), new PropertyMap(e.getValue())); + this.classLoader = cf.classLoader; + this.defaultParser = cf.defaultParser; + return this; + } + + /** + * Sets a configuration property value on this object. + * <p> + * A typical usage is to set or overwrite configuration values like so... + * <p class='bcode'> + * ContextFactory g = ContextFactory.<jsm>create</jsm>(); + * f.setProperty(<jsf>BEAN_sortProperties</jsf>, <jk>true</jk>); + * </p> + * <p> + * The possible class types of the value depend on the property type: + * <p> + * <table class='styled'> + * <tr> + * <th>Property type</th> + * <th>Example</th> + * <th>Allowed value type</th> + * </tr> + * <tr> + * <td>Set <l>SIMPLE</l></td> + * <td><js>"Foo.x"</js></td> + * <td>Any object type.</td> + * </tr> + * <tr> + * <td>Set <l>SET/LIST</l></td> + * <td><js>"Foo.x.set"</js></td> + * <td>Any collection or array of any objects, or a String containing a JSON array.</td> + * </tr> + * <tr> + * <td>Add/Remove <l>SET/LIST</l></td> + * <td><js>"Foo.x.set.add"</js></td> + * <td>If a collection, adds or removes the entries in the collection. Otherwise, adds/removes a single entry.</td> + * </tr> + * <tr> + * <td>Set <l>MAP</l></td> + * <td><js>"Foo.x.map"</js></td> + * <td>A map, or a String containing a JSON object. Entries overwrite existing map.</td> + * </tr> + * <tr> + * <td>Put <l>MAP</l></td> + * <td><js>"Foo.x.map.put"</js></td> + * <td>A map, or a String containing a JSON object. Entries are added to existing map.</td> + * </tr> + * </table> + * + * @param name The configuration property name.<br> + * If name ends with <l>.add</l>, then the specified value is added to the + * existing property value as an entry in a SET or LIST property.<br> + * If name ends with <l>.put</l>, then the specified value is added to the + * existing property value as a key/value pair in a MAP property.<br> + * If name ends with <l>.remove</l>, then the specified value is removed from the + * existing property property value in a SET or LIST property.<br> + * + * @param value The new value. + * If <jk>null</jk>, the property value is deleted.<br> + * In general, the value type can be anything.<br> + * + * @return This object (for method chaining). + */ + public ContextFactory 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 { + checkLock(); + contexts.clear(); + if (! properties.containsKey(prefix)) + properties.put(prefix, new PropertyMap(prefix)); + properties.get(prefix).set(name, value); + } finally { + wl.unlock(); + } + return this; + } + + /** + * Convenience method for setting multiple properties in one call. + * <p> + * This appends to any previous configuration properties set on this config. + * + * @param newProperties The new properties to set. + * @return This object (for method chaining). + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public ContextFactory setProperties(Map newProperties) { + wl.lock(); + try { + checkLock(); + contexts.clear(); + for (Map.Entry e : (Set<Map.Entry>)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 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 ContextFactory addToProperty(String name, Object value) { + String prefix = prefix(name); + wl.lock(); + try { + checkLock(); + 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 ContextFactory putToProperty(String name, Object key, Object value) { + String prefix = prefix(name); + wl.lock(); + try { + checkLock(); + 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 ContextFactory putToProperty(String name, Object value) { + String prefix = prefix(name); + wl.lock(); + try { + checkLock(); + 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 ContextFactory removeFromProperty(String name, Object value) { + String prefix = prefix(name); + wl.lock(); + try { + checkLock(); + 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 config. + * <p> + * Multiple calls to this method for the same config class will return the same + * cached value as long as the config properties on this config are not touched. + * <p> + * As soon as any properties are modified on this config, all cached entries + * are discarded and recreated as needed. + * + * @param c The context class to instantiate. + * @return The context instance. + */ + @SuppressWarnings("unchecked") + public <T extends Context> T getContext(Class<T> 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<Class<? extends Context>,Context>()); + ConcurrentHashMap<Class<? extends Context>, Context> cacheForThisConfig = globalContextCache.get(key); + + if (! cacheForThisConfig.containsKey(c)) + cacheForThisConfig.putIfAbsent(c, c.getConstructor(ContextFactory.class).newInstance(this)); + + contexts.put(c, cacheForThisConfig.get(c)); + } + return (T)contexts.get(c); + } catch (Exception e) { + throw new ConfigException("Could not instantiate config class ''{0}''", className(c)).initCause(e); + } + } finally { + rl.unlock(); + } + } + + /** + * Returns the configuration properties with the specified prefix. + * <p> + * For example, if <l>prefix</l> is <js>"BeanContext"</js>, then retrieves + * all configuration properties that are prefixed with <js>"BeanContext."</js>. + * + * @param prefix The prefix of properties to retrieve. + * @return The configuration properties with the specified prefix, never <jk>null</jk>. + */ + 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. + * <p> + * Can be used for resolving class names when the classes being created are in a different + * classloader from the Juneau code. + * <p> + * If <jk>null</jk>, the system classloader will be used to resolve classes. + * + * @param classLoader The new classloader. + * @throws LockedException If {@link #lock()} was called on this object. + * @return This object (for method chaining). + */ + public ContextFactory setClassLoader(ClassLoader classLoader) { + checkLock(); + this.classLoader = (classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader); + return this; + } + + /** + * Specifies the parser to use to convert Strings to POJOs. + * <p> + * If <jk>null</jk>, {@link JsonParser#DEFAULT} will be used. + * + * @param defaultParser The new defaultParser. + * @throws LockedException If {@link #lock()} was called on this object. + * @return This object (for method chaining). + */ + public ContextFactory setDefaultParser(ReaderParser defaultParser) { + checkLock(); + 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. <js>"BeanContext.sortProperties"</js>) + * @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> T getProperty(String name, Class<T> 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 (! StringUtils.isEmpty(s)) + return BeanContext.DEFAULT.convertToType(s, type); + return 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. <js>"BeanContext.sortProperties"</js>) + * @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 <K,V> Map<K,V> getMap(String name, Class<K> keyType, Class<V> valType, Map<K,V> 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 <code>getContext(BeanContext.<jk>class</jk>);</code>. + * + * @return The bean context instance. + */ + public BeanContext getBeanContext() { + return getContext(BeanContext.class); + } + + /** + * Shortcut for calling <code>addTo(<jsf>BEAN_notBeanClasses</jsf>, <jf>classes</jf>)</code>. + * + * @see ContextFactory#addToProperty(String,Object) + * @param classes The new setting value for the bean context. + * @throws LockedException If {@link ContextFactory#lock()} was called on this class or the bean context. + * @return This object (for method chaining). + * @see ContextFactory#addToProperty(String, Object) + * @see BeanContext#BEAN_notBeanClasses + */ + public ContextFactory addNotBeanClasses(Class<?>...classes) throws LockedException { + checkLock(); + addToProperty(BEAN_notBeanClasses, classes); + return this; + } + + /** + * Shortcut for calling <code>addTo(<jsf>BEAN_transforms</jsf>, <jf>classes</jf>)</code>. + * + * @param classes The new setting value for the bean context. + * @throws LockedException If {@link ContextFactory#lock()} was called on this class or the bean context. + * @return This object (for method chaining). + * @see ContextFactory#addToProperty(String, Object) + * @see BeanContext#BEAN_transforms + */ + public ContextFactory addTransforms(Class<?>...classes) throws LockedException { + checkLock(); + addToProperty(BEAN_transforms, classes); + return this; + } + + /** + * Shortcut for calling <code>putTo(<jsf>BEAN_implCLasses</jsf>, <jf>interfaceClass</jf>, <jf>implClass</jf>)</code>. + * + * @param interfaceClass The interface class. + * @param implClass The implementation class. + * @throws LockedException If {@link ContextFactory#lock()} was called on this class or the bean context. + * @param <T> The class type of the interface. + * @return This object (for method chaining). + * @see ContextFactory#putToProperty(String, Object, Object) + * @see BeanContext#BEAN_implClasses + */ + public <T> ContextFactory addImplClass(Class<T> interfaceClass, Class<? extends T> implClass) throws LockedException { + checkLock(); + 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. <code>123</code> and <js>"123"</js> result in the same hashcode.) + */ + protected static class NormalizingHashCode extends HashCode { + @Override /* HashCode */ + protected Object normalize(Object o) { + return ContextFactory.normalize(o); + } + } + + /** + * Contains all the properties for a particular property prefix (e.g. <js>'BeanContext'</js>) + * <p> + * Instances of this map are immutable from outside this class. + * <p> + * The {@link PropertyMap#hashCode()} and {@link PropertyMap#equals(Object)} methods + * can be used to compare with other property maps. + * + * @author James Bognar ([email protected]) + */ + @SuppressWarnings("hiding") + public class PropertyMap { + + private final Map<String,Property> map = new ConcurrentSkipListMap<String,Property>(); + 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<Object,Object> e : p.entrySet()) + if (e.getKey().toString().startsWith(prefix)) + set(e.getKey().toString(), e.getValue()); + } + + /** + * Copy constructor. + */ + private PropertyMap(PropertyMap orig) { + this.prefix = orig.prefix; + for (Map.Entry<String,Property> e : orig.map.entrySet()) + this.map.put(e.getKey(), Property.create(e.getValue().name, 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> T get(String name, Class<T> type, T def) { + rl.lock(); + try { + Property p = map.get(name); + if (p == null || type == null) + return def; + try { + if (BeanContext.DEFAULT == null) + return def; + return BeanContext.DEFAULT.convertToType(p.value, type); + } catch (InvalidDataConversionException e) { + throw new ConfigException("Could not retrieve config property ''{0}''. {1}", p.name, e.getMessage()); + } + } finally { + rl.unlock(); + } + } + + /** + * Returns the specified property as a map with the specified key and value types. + * <p> + * 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 <K,V> Map<K,V> getMap(String name, Class<K> keyType, Class<V> valueType, Map<K,V> def) { + rl.lock(); + try { + Property p = map.get(name); + if (p == null || keyType == null || valueType == null) + return def; + try { + BeanContext bc = BeanContext.DEFAULT; + if (bc != null) + return (Map<K,V>)bc.convertToType(p.value, bc.getMapClassMeta(LinkedHashMap.class, keyType, valueType)); + return def; + } catch (InvalidDataConversionException e) { + throw new ConfigException("Could not retrieve config property ''{0}''. {1}", p.name, e.getMessage()); + } + } finally { + rl.unlock(); + } + } + + /** + * Convenience method for returning all values in this property map as a simple map. + * <p> + * Primarily useful for debugging. + * + * @return A new {@link LinkedHashMap} with all values in this property map. + */ + public Map<String,Object> asMap() { + rl.lock(); + try { + Map<String,Object> m = new LinkedHashMap<String,Object>(); + 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), ClassUtils.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), ClassUtils.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), ClassUtils.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), ClassUtils.getReadableClassNameForObject(key), JsonSerializer.DEFAULT_LAX.toString(val), ClassUtils.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<Object> value; + + private SetProperty(String name, Object value) { + super(name, "SET", new ConcurrentSkipListSet<Object>(PROPERTY_COMPARATOR)); + this.value = (Set<Object>)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<Object>)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<Object>)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<Object> i = value.iterator(); i.hasNext();) + if (same(i.next(), val)) + i.remove(); + } + } + } + + @SuppressWarnings({"unchecked"}) + private static class ListProperty extends Property { + private final LinkedList<Object> value; + + private ListProperty(String name, Object value) { + super(name, "LIST", new LinkedList<Object>()); + this.value = (LinkedList<Object>)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<Object> l = (List<Object>)val; + for (ListIterator<Object> i = l.listIterator(l.size()); i.hasPrevious();) + add(i.previous()); + } else if (val instanceof Collection) { + List<Object> l = new ArrayList<Object>((Collection<Object>)val); + for (ListIterator<Object> 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<Object> 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<Object>)val) + remove(o); + else { + String s = val.toString(); + if (s.startsWith("[") && s.endsWith("]")) { + try { + remove(new ObjectList(s)); + return; + } catch (Exception e) {} + } + for (Iterator<Object> i = value.iterator(); i.hasNext();) + if (same(i.next(), val)) + i.remove(); + } + } + } + + @SuppressWarnings({"unchecked","rawtypes"}) + private static class MapProperty extends Property { + final Map<Object,Object> value; + + MapProperty(String name, Object value) { + // ConcurrentSkipListMap doesn't support Map.Entry.remove(), so use TreeMap instead. + super(name, "MAP", Collections.synchronizedMap(new TreeMap<Object,Object>(PROPERTY_COMPARATOR))); + this.value = (Map<Object,Object>)value(); + put(value); + } + + @Override + void put(Object val) { + try { + if (BeanContext.DEFAULT != null && ! (val instanceof Map)) + val = BeanContext.DEFAULT.convertToType(val, Map.class); + if (val instanceof Map) { + Map m = (Map)val; + for (Map.Entry e : (Set<Map.Entry>)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<Object,Object> 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 normalize(Object o) { + if (o instanceof Class) + return ((Class<?>)o).getName(); + if (o instanceof Number || o instanceof Boolean) + return o.toString(); + return o; + } + + /* + * 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<Map.Entry> s1 = m1.entrySet(), s2 = m2.entrySet(); + for (Iterator<Map.Entry> 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 normalize(o1).equals(normalize(o2)); + } + } + + private 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 String className(Object o) { + if (o == null) + return null; + if (o instanceof Class) + return ClassUtils.getReadableClassName((Class<?>)o); + return ClassUtils.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(); + } + } + + /** + * Creates an unlocked clone of this object. + * + * @throws CloneNotSupportedException If class cannot be cloned. + */ + @Override /* Object */ + public ContextFactory clone() throws CloneNotSupportedException { + rl.lock(); + try { + return new ContextFactory(this); + } finally { + rl.unlock(); + } + } +}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/CoreApi.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/CoreApi.java b/juneau-core/src/main/java/org/apache/juneau/CoreApi.java new file mode 100644 index 0000000..34e6429 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/CoreApi.java @@ -0,0 +1,213 @@ +/*************************************************************************************************************************** + * 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; + +/** + * Common super class for all core-API serializers, parsers, and serializer/parser groups. + * + * <h6 class='topic'>Description</h6> + * <p> + * Maintains an inner {@link ContextFactory} instance that can be used by serializer and parser subclasses + * to work with beans in a consistent way. + * <p> + * Provides several duplicate convenience methods from the {@link ContextFactory} class to set properties on that class from this class. + * <p> + * Also implements the {@link Lockable} interface to allow for easy locking and cloning. + * + * @author James Bognar ([email protected]) + */ +public abstract class CoreApi extends Lockable { + + private ContextFactory contextFactory = ContextFactory.create(); + private BeanContext beanContext; + + /** + * Returns the {@link ContextFactory} object associated with this class. + * <p> + * The context factory stores all configuration properties for this class. + * Adding/modifying properties on this factory will alter the behavior of this object. + * <p> + * Calling the {@link ContextFactory#lock()} method on the returned object will prevent + * any further modifications to the configuration for this object + * ANY ANY OTHERS THAT SHARE THE SAME FACTORY!. + * Note that calling the {@link #lock()} method on this class will only + * lock the configuration for this particular instance of the class. + * + * @return The context factory associated with this object. + */ + public ContextFactory getContextFactory() { + return contextFactory; + } + + /** + * Returns the bean context to use for this class. + * + * @return The bean context object. + */ + public BeanContext getBeanContext() { + if (beanContext == null) + return contextFactory.getContext(BeanContext.class); + return beanContext; + } + + /** + * Creates a {@link Context} class instance of the specified type. + * + * @param contextClass The class instance to create. + * @return A context class instance of the specified type. + */ + protected final <T extends Context> T getContext(Class<T> contextClass) { + return contextFactory.getContext(contextClass); + } + + /** + * Shortcut for calling <code>getContextFactory().setProperty(<jf>property</jf>, <jf>value</jf>);</code>. + * + * @param property The property name. + * @param value The property value. + * @return This class (for method chaining). + * @throws LockedException If {@link #lock()} has been called on this object or {@link ContextFactory} object. + * @see ContextFactory#setProperty(String, Object) + */ + public CoreApi setProperty(String property, Object value) throws LockedException { + checkLock(); + contextFactory.setProperty(property, value); + return this; + } + + /** + * Shortcut for calling <code>getContextFactory().setProperties(<jf>properties</jf>);</code>. + * + * @param properties The properties to set on this class. + * @return This class (for method chaining). + * @throws LockedException If {@link #lock()} has been called on this object. + * @see ContextFactory#setProperties(java.util.Map) + */ + public CoreApi setProperties(ObjectMap properties) throws LockedException { + checkLock(); + contextFactory.setProperties(properties); + return this; + } + + /** + * Shortcut for calling <code>getContextFactory().addNotBeanClasses(<jf>classes</jf>)</code>. + * + * @see ContextFactory#addToProperty(String,Object) + * @param classes The new setting value for the bean context. + * @throws LockedException If {@link ContextFactory#lock()} was called on this class or the bean context. + * @return This object (for method chaining). + * @see ContextFactory#addToProperty(String, Object) + * @see BeanContext#BEAN_notBeanClasses + */ + public CoreApi addNotBeanClasses(Class<?>...classes) throws LockedException { + checkLock(); + contextFactory.addNotBeanClasses(classes); + return this; + } + + /** + * Shortcut for calling <code>getContextFactory().addTransforms(<jf>classes</jf>)</code>. + * + * @param classes The new setting value for the bean context. + * @throws LockedException If {@link ContextFactory#lock()} was called on this class or the bean context. + * @return This object (for method chaining). + * @see ContextFactory#addToProperty(String, Object) + * @see BeanContext#BEAN_transforms + */ + public CoreApi addTransforms(Class<?>...classes) throws LockedException { + checkLock(); + contextFactory.addTransforms(classes); + return this; + } + + /** + * Shortcut for calling <code>getContextFactory().addImplClass(<jf>interfaceClass</jf>, <jf>implClass</jf>)</code>. + * + * @param interfaceClass The interface class. + * @param implClass The implementation class. + * @throws LockedException If {@link ContextFactory#lock()} was called on this class or the bean context. + * @param <T> The class type of the interface. + * @return This object (for method chaining). + * @see ContextFactory#putToProperty(String, Object, Object) + * @see BeanContext#BEAN_implClasses + */ + public <T> CoreApi addImplClass(Class<T> interfaceClass, Class<? extends T> implClass) throws LockedException { + checkLock(); + contextFactory.addImplClass(interfaceClass, implClass); + return this; + } + + /** + * Shortcut for calling <code>getContextFactory().setClassLoader(<jf>classLoader</jf>)</code>. + * + * @param classLoader The new classloader. + * @throws LockedException If {@link ContextFactory#lock()} was called on this class or the bean context. + * @return This object (for method chaining). + * @see ContextFactory#setClassLoader(ClassLoader) + */ + public CoreApi setClassLoader(ClassLoader classLoader) throws LockedException { + checkLock(); + contextFactory.setClassLoader(classLoader); + return this; + } + + /** + * Shortcut for calling {@link BeanContext#object()}. + * + * @return The reusable {@link ClassMeta} for representing the {@link Object} class. + */ + public ClassMeta<Object> object() { + return getBeanContext().object(); + } + + /** + * Shortcut for calling {@link BeanContext#string()}. + * + * @return The reusable {@link ClassMeta} for representing the {@link String} class. + */ + public ClassMeta<String> string() { + return getBeanContext().string(); + } + + + //-------------------------------------------------------------------------------- + // Overridden methods + //-------------------------------------------------------------------------------- + + @Override /* Lockable */ + public void checkLock() { + super.checkLock(); + beanContext = null; + } + + @Override /* Lockable */ + public CoreApi lock() { + try { + super.lock(); + contextFactory = contextFactory.clone(); + contextFactory.lock(); + beanContext = contextFactory.getContext(BeanContext.class); + return this; + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } + + @Override /* Lockable */ + public CoreApi clone() throws CloneNotSupportedException { + CoreApi c = (CoreApi)super.clone(); + c.contextFactory = ContextFactory.create(contextFactory); + c.beanContext = null; + return c; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/Delegate.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/Delegate.java b/juneau-core/src/main/java/org/apache/juneau/Delegate.java new file mode 100644 index 0000000..dff2d66 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/Delegate.java @@ -0,0 +1,33 @@ +/*************************************************************************************************************************** + * 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; + +/** + * An object that represents another object, often wrapping that object. + * <p> + * <b>*** Internal Interface - Not intended for external use ***</b> + * <p> + * For example, {@link BeanMap} is a map representation of a bean. + * + * @author James Bognar ([email protected]) + * @param <T> The represented class type. + */ +public interface Delegate<T> { + + /** + * The {@link ClassMeta} of the class of the represented object. + * + * @return The class type of the represented object. + */ + public ClassMeta<T> getClassMeta(); +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/FormattedException.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/FormattedException.java b/juneau-core/src/main/java/org/apache/juneau/FormattedException.java new file mode 100644 index 0000000..add0a79 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/FormattedException.java @@ -0,0 +1,57 @@ +/*************************************************************************************************************************** + * 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 java.text.*; + +/** + * Subclass of non-runtime exceptions that take in a message and zero or more arguments. + * <p> + * + * @author James Bognar ([email protected]) + */ +public class FormattedException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructor. + * + * @param message The {@link MessageFormat}-style message. + * @param args The arguments in the message. + */ + public FormattedException(String message, Object...args) { + super(args.length == 0 ? message : MessageFormat.format(message, args)); + } + + /** + * Constructor. + * + * @param causedBy The cause of this exception. + * @param message The {@link MessageFormat}-style message. + * @param args The arguments in the message. + */ + public FormattedException(Throwable causedBy, String message, Object...args) { + this(message, args); + initCause(causedBy); + } + + /** + * Constructor. + * + * @param causedBy The cause of this exception. + */ + public FormattedException(Throwable causedBy) { + this(causedBy, causedBy.getLocalizedMessage()); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/FormattedRuntimeException.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/FormattedRuntimeException.java b/juneau-core/src/main/java/org/apache/juneau/FormattedRuntimeException.java new file mode 100644 index 0000000..f010d9f --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/FormattedRuntimeException.java @@ -0,0 +1,47 @@ +/*************************************************************************************************************************** + * 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 java.text.*; + +/** + * Subclass of runtime exceptions that take in a message and zero or more arguments. + * + * @author James Bognar ([email protected]) + */ +public class FormattedRuntimeException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Constructor. + * + * @param message The {@link MessageFormat}-style message. + * @param args The arguments in the message. + */ + public FormattedRuntimeException(String message, Object...args) { + super(args.length == 0 ? message : MessageFormat.format(message, args)); + } + + /** + * Constructor. + * + * @param causedBy The cause of this exception. + * @param message The {@link MessageFormat}-style message. + * @param args The arguments in the message. + */ + public FormattedRuntimeException(Throwable causedBy, String message, Object...args) { + this(message, args); + initCause(causedBy); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/InvalidDataConversionException.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/InvalidDataConversionException.java b/juneau-core/src/main/java/org/apache/juneau/InvalidDataConversionException.java new file mode 100644 index 0000000..21d0e57 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/InvalidDataConversionException.java @@ -0,0 +1,54 @@ +/*************************************************************************************************************************** + * 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 java.text.*; + +import org.apache.juneau.internal.*; +import org.apache.juneau.json.*; + +/** + * General invalid conversion exception. + * <p> + * Exception that gets thrown if you try to perform an invalid conversion, such as when calling {@code ObjectMap.getInt(...)} on a non-numeric <code>String</code>. + * + * @author James Bognar ([email protected]) + */ +public final class InvalidDataConversionException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * @param toType Attempting to convert to this class type. + * @param cause The cause. + * @param value The value being converted. + */ + public InvalidDataConversionException(Object value, Class<?> toType, Exception cause) { + super(MessageFormat.format("Invalid data conversion from type ''{0}'' to type ''{1}''. Value={2}.", ClassUtils.getReadableClassNameForObject(value), ClassUtils.getReadableClassName(toType), getValue(value)), cause); + } + + /** + * @param toType Attempting to convert to this class type. + * @param cause The cause. + * @param value The value being converted. + */ + public InvalidDataConversionException(Object value, ClassMeta<?> toType, Exception cause) { + super(MessageFormat.format("Invalid data conversion from type ''{0}'' to type ''{1}''. Value={2}.", ClassUtils.getReadableClassNameForObject(value), toType.toString(), getValue(value)), cause); + } + + private static String getValue(Object o) { + if (o instanceof Class) + return "'" + ClassUtils.getReadableClassName((Class<?>)o) + "'"; + return JsonSerializer.DEFAULT_LAX == null ? "'" + o.toString() + "'" : JsonSerializer.DEFAULT_LAX.toString(o); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/Lockable.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/Lockable.java b/juneau-core/src/main/java/org/apache/juneau/Lockable.java new file mode 100644 index 0000000..4d3d470 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/Lockable.java @@ -0,0 +1,88 @@ +/*************************************************************************************************************************** + * 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; + +/** + * Superclass of all classes that have a locked state. + * <p> + * Used to mark bean contexts, serializers, and parsers as read-only so that + * settings can no longer be modified. + * <p> + * Also keeps track of when the object has been cloned and allows for lazy cloning through + * the {@link #onUnclone()} method. The idea behind this is that certain expensive fields don't + * need to be cloned unless the object is actually being modified. + * <p> + * Calling {@link #lock()} on the object causes it to be put into a read-only state. + * Once called, subsequent calls to {@link #checkLock()} will cause {@link LockedException LockedExceptions} + * to be thrown. + * <p> + * As a rule, cloned objects are unlocked by default. + * + * @author James Bognar ([email protected]) + */ +public abstract class Lockable implements Cloneable { + + private boolean isLocked = false; + private boolean isCloned = false; + + /** + * Locks this object so that settings on it cannot be modified. + * + * @return This object (for method chaining). + */ + public Lockable lock() { + isLocked = true; + return this; + } + + /** + * @return <code><jk>true</jk></code> if this object has been locked. + */ + public boolean isLocked() { + return isLocked; + } + + /** + * Causes a {@link LockedException} to be thrown if this object has been locked. + * <p> + * Also calls {@link #onUnclone()} if this is the first time this method has been called since cloning. + * + * @throws LockedException If {@link #lock()} has been called on this object. + */ + public void checkLock() throws LockedException { + if (isLocked) + throw new LockedException(); + if (isCloned) + onUnclone(); + isCloned = false; + } + + /** + * Subclass can override this method to handle lazy-cloning on the first time {@link #checkLock()} is called after + * the object has been cloned. + */ + public void onUnclone() {} + + /** + * Creates an unlocked clone of this object. + * + * @throws CloneNotSupportedException If class cannot be cloned. + */ + @Override /* Object */ + public Lockable clone() throws CloneNotSupportedException { + Lockable c = (Lockable)super.clone(); + c.isLocked = false; + c.isCloned = true; + return c; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/LockedException.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/LockedException.java b/juneau-core/src/main/java/org/apache/juneau/LockedException.java new file mode 100644 index 0000000..4d841f5 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/LockedException.java @@ -0,0 +1,32 @@ +/*************************************************************************************************************************** + * 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; + +/** + * Exception that gets thrown when trying to modify settings on a locked {@link Lockable} object. + * <p> + * A locked exception indicates a programming error. + * Certain objects that are meant for reuse, such as serializers and parsers, provide + * the ability to lock the current settings so that they cannot be later changed. + * This exception indicates that a setting change was attempted on a previously locked object. + * + * @author James Bognar ([email protected]) + */ +public final class LockedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + LockedException() { + super("Object is locked and object settings cannot be modified."); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/MediaRange.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/MediaRange.java b/juneau-core/src/main/java/org/apache/juneau/MediaRange.java new file mode 100644 index 0000000..788243d --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/MediaRange.java @@ -0,0 +1,316 @@ +/*************************************************************************************************************************** + * 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 java.util.*; +import java.util.Map.*; + +/** + * Describes a single type used in content negotiation between an HTTP client and server, as described in + * Section 14.1 and 14.7 of RFC2616 (the HTTP/1.1 specification). + */ +public final class MediaRange implements Comparable<MediaRange> { + + private final String type; // The media type (e.g. "text" for Accept, "utf-8" for Accept-Charset) + private final String subType; // The media sub-type (e.g. "json" for Accept, not used for Accept-Charset) + private final Float qValue; + private final Map<String,Set<String>> parameters, extensions; + + /** + * Returns the media type enclosed by this media range. + * <p> + * Examples: + * <ul> + * <li><js>"text/html"</js> + * <li><js>"text/*"</js> + * <li><js>"*\/*"</js> + * </ul> + * + * @return The media type of this media range, lowercased, never <jk>null</jk>. + */ + public String getMediaType() { + return type + "/" + subType; + } + + /** + * Return just the type portion of this media range. + * + * @return The type portion of this media range. + */ + public String getType() { + return type; + } + + /** + * Returns the <js>'q'</js> (quality) value for this type, as described in Section 3.9 of RFC2616. + * <p> + * The quality value is a float between <code>0.0</code> (unacceptable) and <code>1.0</code> (most acceptable). + * <p> + * If 'q' value doesn't make sense for the context (e.g. this range was extracted from a <js>"content-*"</js> header, as opposed to <js>"accept-*"</js> + * header, its value will always be <js>"1"</js>. + * + * @return The 'q' value for this type, never <jk>null</jk>. + */ + public Float getQValue() { + return qValue; + } + + /** + * Returns the optional set of parameters associated to the type as returned by {@link #getMediaType()}. + * <p> + * The parameters are those values as described in standardized MIME syntax. + * An example of such a parameter in string form might be <js>"level=1"</js>. + * <p> + * Values are lowercase and never <jk>null</jk>. + * + * @return The optional list of parameters, never <jk>null</jk>. + */ + public Map<String,Set<String>> getParameters() { + return parameters; + } + + /** + * Returns the optional set of custom extensions defined for this type. + * <p> + * Values are lowercase and never <jk>null</jk>. + * + * @return The optional list of extensions, never <jk>null</jk>. + */ + public Map<String,Set<String>> getExtensions() { + return extensions; + } + + /** + * Provides a string representation of this media range, suitable for use as an <code>Accept</code> header value. + * <p> + * The literal text generated will be all lowercase. + * + * @return A media range suitable for use as an Accept header value, never <code>null</code>. + */ + @Override /* Object */ + public String toString() { + StringBuffer sb = new StringBuffer().append(type).append('/').append(subType); + + if (! parameters.isEmpty()) + for (Entry<String,Set<String>> e : parameters.entrySet()) { + String k = e.getKey(); + for (String v : e.getValue()) + sb.append(';').append(k).append('=').append(v); + } + + // '1' is equivalent to specifying no qValue. If there's no extensions, then we won't include a qValue. + if (qValue.floatValue() == 1.0) { + if (! extensions.isEmpty()) { + sb.append(";q=").append(qValue); + for (Entry<String,Set<String>> e : extensions.entrySet()) { + String k = e.getKey(); + for (String v : e.getValue()) + sb.append(';').append(k).append('=').append(v); + } + } + } else { + sb.append(";q=").append(qValue); + for (Entry<String,Set<String>> e : extensions.entrySet()) { + String k = e.getKey(); + for (String v : e.getValue()) + sb.append(';').append(k).append('=').append(v); + } + } + return sb.toString(); + } + + /** + * Returns <jk>true</jk> if the specified object is also a <code>MediaType</code>, and has the same qValue, type, parameters, and extensions. + * + * @return <jk>true</jk> if object is equivalent. + */ + @Override /* Object */ + public boolean equals(Object o) { + + if (o == null || !(o instanceof MediaRange)) + return false; + + if (this == o) + return true; + + MediaRange o2 = (MediaRange) o; + return qValue.equals(o2.qValue) + && type.equals(o2.type) + && subType.equals(o2.subType) + && parameters.equals(o2.parameters) + && extensions.equals(o2.extensions); + } + + /** + * Returns a hash based on this instance's <code>media-type</code>. + * + * @return A hash based on this instance's <code>media-type</code>. + */ + @Override /* Object */ + public int hashCode() { + return type.hashCode() + subType.hashCode(); + } + + /** + * Creates a <code>MediaRange</code> object with the referenced values. + * + * @param type The MIME type of this media range (e.g. <js>"application"</js> in <js>"application/json"</js>) + * @param subType The MIME subtype of this media range (e.g. <js>"json"</js> in <js>"application/json"</js>). + * @param parameters The optional parameters for this range. + * @param qValue The quality value of this range. Must be between <code>0</code> and <code>1.0</code>. + * @param extensions The optional extensions to this quality value. + */ + private MediaRange(String type, String subType, Map<String,Set<String>> parameters, Float qValue, Map<String,Set<String>> extensions) { + this.type = type; + this.subType = subType; + this.parameters = (parameters == null ? new TreeMap<String,Set<String>>() : parameters); + this.extensions = (extensions == null ? new TreeMap<String,Set<String>>() : extensions); + this.qValue = qValue; + } + + /** + * Parses an <code>Accept</code> header value into an array of media ranges. + * <p> + * The returned media ranges are sorted such that the most acceptable media is available at ordinal position <js>'0'</js>, and the least acceptable at position n-1. + * <p> + * The syntax expected to be found in the referenced <code>value</code> complies with the syntax described in RFC2616, Section 14.1, as described below: + * <p class='bcode'> + * Accept = "Accept" ":" + * #( media-range [ accept-params ] ) + * + * media-range = ( "*\/*" + * | ( type "/" "*" ) + * | ( type "/" subtype ) + * ) *( ";" parameter ) + * accept-params = ";" "q" "=" qvalue *( accept-extension ) + * accept-extension = ";" token [ "=" ( token | quoted-string ) ] + * </p> + * This method can also be used on other headers such as <code>Accept-Charset</code> and <code>Accept-Encoding</code>... + * <p class='bcode'> + * Accept-Charset = "Accept-Charset" ":" + * 1#( ( charset | "*" )[ ";" "q" "=" qvalue ] ) + * </p> + * + * @param value The value to parse. If <jk>null</jk> or empty, returns a single <code>MediaRange</code> is returned that represents all types. + * @return The media ranges described by the string. + * The ranges are sorted such that the most acceptable media is available at ordinal position <js>'0'</js>, and the least acceptable at position n-1. + */ + public static MediaRange[] parse(String value) { + + Set<MediaRange> ranges = new TreeSet<MediaRange>(); + + if (value == null || value.length() == 0) + return new MediaRange[]{new MediaRange("*", "*", null, 1f, null)}; + + value = value.toLowerCase(Locale.ENGLISH); + + for (String r : value.trim().split("\\s*,\\s*")) { + r = r.trim(); + + if (r.isEmpty()) + continue; + + String[] tokens = r.split("\\s*;\\s*"); + + tokens[0] = tokens[0].replace(' ', '+'); + + // There is at least a type. + String[] t = tokens[0].split("/"); + String type = t[0], subType = (t.length == 1 ? "*" : t[1]); + + // Only the type of the range is specified + if (tokens.length == 1) { + ranges.add(new MediaRange(type, subType, null, 1f, null)); + continue; + } + + Float qValue = 1f; + Map<String,Set<String>> params = new TreeMap<String,Set<String>>(); + Map<String,Set<String>> exts = new TreeMap<String,Set<String>>(); + + boolean isInExtensions = false; + for (int i = 1; i < tokens.length; i++) { + String[] parm = tokens[i].split("\\s*=\\s*"); + if (parm.length == 2) { + String k = parm[0], v = parm[1]; + if (isInExtensions) { + if (! exts.containsKey(parm[0])) + exts.put(parm[0], new TreeSet<String>()); + exts.get(parm[0]).add(parm[1]); + } else if (k.equals("q")) { + qValue = new Float(v); + isInExtensions = true; + } else /*(! isInExtensions)*/ { + if (! params.containsKey(parm[0])) + params.put(parm[0], new TreeSet<String>()); + params.get(parm[0]).add(parm[1]); + } + } + } + + ranges.add(new MediaRange(type, subType, params, qValue, exts)); + } + + return ranges.toArray(new MediaRange[ranges.size()]); + } + + /** + * Compares two MediaRanges for equality. + * <p> + * The values are first compared according to <code>qValue</code> values. + * Should those values be equal, the <code>type</code> is then lexicographically compared (case-insensitive) in ascending order, + * with the <js>"*"</js> type demoted last in that order. + * <code>MediaRanges</code> with the same type but different sub-types are compared - a more specific subtype is + * promoted over the 'wildcard' subtype. + * <code>MediaRanges</code> with the same types but with extensions are promoted over those same types with no extensions. + * + * @param o The range to compare to. Never <jk>null</jk>. + */ + @Override /* Comparable */ + public int compareTo(MediaRange o) { + + // Compare q-values. + int qCompare = Float.compare(o.qValue, qValue); + if (qCompare != 0) + return qCompare; + + // Compare media-types. + // Note that '*' comes alphabetically before letters, so just do a reverse-alphabetical comparison. + int i = o.type.compareTo(type); + if (i == 0) + i = o.subType.compareTo(subType); + return i; + } + + /** + * Returns <jk>true</jk> if the specified <code>MediaRange</code> matches this range. + * <p> + * This implies the types and subtypes are the same as or encompasses the other (e.g. <js>'application/xml'</js> and <js>'application/*'</js>). + * + * @param o The other media rage. + * @return <jk>true</jk> if the media ranges are the same or one encompasses the other. + */ + public boolean matches(MediaRange o) { + if (this == o) + return true; + + if (qValue == 0 || o.qValue == 0) + return false; + + if (type.equals(o.type) || (type.equals("*")) || (o.type.equals("*"))) + if (subType.equals(o.subType) || subType.equals("*") || o.subType.equals("*")) + return true; + + return false; + } +}
