
org.flexdock.util.ClassMapping Maven / Gradle / Ivy
/*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.flexdock.util;
import java.util.WeakHashMap;
/**
* This class manages associations between classes and object instances. It
* allows for mappings between a class and its subclasses and another associated
* class, or an associated instance of a class.
*
* This class is useful for "handler" type logic in which a handler class must
* be mapped to the classes it is designed to handle. Consider the class
* hierarchy of {@code Foo}, {@code Bar}, and {@code Baz}, where {@code Bar}
* extends {@code Foo} and {@code Baz} extends {@code Bar}.
*
*
* Foo.class
* |-Bar.class
* |-Baz.class
*
*
* Each of these classes is ultimately a type of {@code Foo}. Some operation is
* performed on instances of {@code Foo} and a set of handler classes are used
* to handle different types of {@code Foo}. Adding a mapping between
* {@code Foo.class} and {@code Handler1.class} will create an association
* between {@code Foo} and all strict, non-specific subclasses of
* {@code Foo} and {@code Handler1.class}.
*
* This means that given any instance of {@code Foo}, calling
* {@code getClassMapping(Object obj)} will return {@code Handler1.class} as the
* class responsible for handling the {@code Foo} instance. This includes
* {@code Bar} and {@code Baz}. All types of {@code Foo} now have a implicit
* association with {@code Handler1.class}
*
* However, if this method is subsequently called with arguments of
* {@code Baz.class} and {@code Handler2.class}, then a specific
* subclass mapping has been introduced for {@code Baz}. Associations apply to
* the given class and non-specific subclasses. Thus, the
* {@code Handler1.class} association remains for {@code Foo} and {@code Bar},
* but no longer for {@code Baz}. Calling {@code getClassMapping(Object obj)}
* with an instance of {@code Baz} will now return {@code Handler2.class}.
*
*
* Foo.class ---------------> (maps to Handler1.class)
* |-Bar.class -----------> (maps to Handler1.class)
* |-Baz.class -------> (maps to Handler2.class)
*
*
* Polymorphic identity within the class association uses strict
* subclasses. This means that the {@code Handler1.class} mapping for
* {@code Foo}, {@code Bar}, and all non-specific subclasses will hold true.
* However, if {@code Foo} happens to implement the interface {@code Qwerty},
* the class mapping relationship will not hold true for all implementations of
* {@code Qwerty}. Only subclasses of {@code Foo}.
*
*
* Foo.class (implements Qwerty) ----------------> (maps to Handler1.class)
* |-Bar.class (implements Qwerty) ------------> (maps to Handler1.class)
* |-Baz.class (implements Qwerty) --------> (maps to Handler2.class)
* Asdf.class (implements Qwerty) ---------------> (maps to nothing)
*
*
* @author Christopher Butler
*/
public class ClassMapping {
private WeakHashMap classes;
private WeakHashMap instances;
private Class defaultClass;
private Object defaultInstance;
/**
* Creates a new {@code ClassMapping} instance with the specified default
* values. All calls to {@code getClassMapping(Class key)} for this
* {@code ClassMapping} in which a specific mapping cannot be found will
* return the specified {@code defaultClass}. All calls to
* {@code getClassInstance(Class key)} in which a specific mapping cannot be
* found will return the specified {@code defaultInstance}.
*
* @param defaultClass
* the default class used by this {@code ClassMapping}
* @param defaultInstance
* the default object instance used by this {@code ClassMapping}
*/
public ClassMapping(Class defaultClass, Object defaultInstance) {
this.defaultClass = defaultClass;
this.defaultInstance = defaultInstance;
classes = new WeakHashMap(4);
instances = new WeakHashMap(4);
}
/**
* Adds a mapping between the {@code Class} type of the specified
* {@code Object} and the specified {@code value}. This method calls
* {@code getClass()} on the specified {@code Object} and dispatches to
* {@code addClassMapping(Class key, Class value)}. If either {@code obj}
* or {@code value} are {@code null}, then this method returns with no
* action taken. The {@code value} class may later be retrieved by calling
* {@code getClassMapping(Class key)} using the specified {@code key} class ({@code obj.getClass()})
* or any subclass thereof for which a specific class mapping does not
* already exist.
*
* @param obj
* the {@code Object} whose {@code Class} will be mapped to the
* specified {@code value}.
* @param value
* the {@code Class} to be associated with the specified key
* @see #addClassMapping(Object, Class)
* @see #getClassMapping(Object)
* @see #getClassMapping(Class)
* @see #removeClassMapping(Object)
* @see #removeClassMapping(Class)
*/
public void addClassMapping(Object obj, Class value) {
Class key = obj == null ? null : obj.getClass();
addClassMapping(key, value);
}
/**
* Adds a mapping between the key {@code Class} and the specified
* {@code value}. If either {@code key} or {@code value} are {@code null},
* then this method returns with no action taken. This method creates an
* association between the specified {@code key} {@code Class} and all
* strict, non-specific subclasses and the specified {@code value}
* {@code Class}. The {@code value} class may later be retrieved by calling
* getClassMapping(Class key) using the specified {@code key} class or any
* subclass thereof for which a specific class mapping does not already
* exist.
*
* @param key
* the {@code Class} to be mapped to the specified {@code value}.
* @param value
* the {@code Class} to be associated with the specified key
* @see #addClassMapping(Class, Class, Object)
* @see #getClassMapping(Class)
* @see #removeClassMapping(Class)
*/
public void addClassMapping(Class key, Class value) {
addClassMapping(key, value, null);
}
/**
* Adds a mapping between the key {@code Class} and both the specified
* {@code value} and specified object instance.. If either {@code key} or
* {@code value} are {@code null}, then this method returns with no action
* taken. This method creates an association between the specified
* {@code key} {@code Class} and all strict, non-specific subclasses and the
* specified {@code value} {@code Class}. The {@code value} class may later
* be retrieved by calling {@code getClassMapping(Class key)} using the
* specified {@code key} class or any subclass thereof for which a specific
* class mapping does not already exist.
*
* This method also creates an optional mapping between the {@code key} and
* a particular object instance, defined by the {@code instance} parameter.
* If {@code instance} is non-{@code null}, then a mapping is defined
* between {@code key} and all strict, non-specific subclasses and the
* object instance itself. The {@code instance} object may later be
* retrieved by calling {@code getClassInstance(Class key)} using the
* specified {@code key} class or any subclass thereof for which a specific
* instance mapping does not already exist. If {@code instance} is
* {@code null}, then no instance mapping is created.
*
* @param key
* the {@code Class} to be mapped to the specified {@code value}.
* @param value
* the {@code Class} to be associated with the specified key
* @param instance
* the object instance to be associated with the specified key
* @see #getClassMapping(Class)
* @see #getClassInstance(Class)
* @see #removeClassMapping(Class)
*/
public void addClassMapping(Class key, Class value, Object instance) {
if (key == null || value == null) {
return;
}
synchronized (classes) {
classes.put(key, value);
}
if (instance != null) {
synchronized (instances) {
instances.put(key, instance);
}
}
}
/**
* Removes any existing class mappings for the {@code Class} type of the
* specified {@code Object}. This method calls {@code getClass()} on the
* specified {@code Object} and dispatches to
* {@code removeClassMapping(Class key)}. If {@code obj} is {@code null},
* then this method returns {@code null}.
*
* Removing the mapping for the specified {@code Class} will also remove it
* for all non-specific subclasses. This means that subclasses of the
* specified {@code Class} will require specific mappings if the it is
* desired for the existing mapping behavior for these classes to remain the
* same.
*
* If any instance mappings exist for the specified {@code Class}, they are
* also removed. This means non-specific subclass instance mappings will
* also be removed.
*
* @param obj
* the {@code Object} whose {@code Class} will be removed from
* the internal mapping
* @return the {@code Class} whose mapping has been removed
* @see #removeClassMapping(Class)
* @see #addClassMapping(Object, Class)
* @see #getClassMapping(Object)
* @see #getClassInstance(Class)
*/
public Class removeClassMapping(Object obj) {
Class key = obj == null ? null : obj.getClass();
return removeClassMapping(key);
}
/**
* Removes any existing class mappings for the {@code Class} type of the
* specified {@code Object}. This method calls {@code getClass()} on the
* specified {@code Object} and dispatches to
* {@code removeClassMapping(Class key)}. If {@code obj} is {@code null},
* then this method returns {@code null}.
*
* Removing the mapping for the specified {@code Class} will also remove it
* for all non-specific subclasses. This means that subclasses of the
* specified {@code Class} will require specific mappings if the it is
* desired for the existing mapping behavior for these classes to remain the
* same.
*
* If any instance mappings exist for the specified {@code Class}, they are
* also removed. This means non-specific subclass instance mappings will
* also be removed.
*
* @param key
* the {@code Class} whose internal mapping will be removed
* @return the {@code Class} whose mapping has been removed
* @see #addClassMapping(Class, Class)
* @see #addClassMapping(Class, Class, Object)
* @see #getClassMapping(Object)
* @see #getClassInstance(Class)
*/
public Class removeClassMapping(Class key) {
if (key == null) {
return null;
}
Class c = null;
synchronized (classes) {
c = (Class) classes.remove(key);
}
synchronized (instances) {
instances.remove(key);
}
return c;
}
/**
* Returns the {@code Class} associated with the {@code Class} of the
* specified {@code Object}. If {@code obj} is {@code null}, this method
* will return the value retrieved from {@code getDefaultMapping()}.
* Otherwise, this method calls {@code obj.getClass()} and dispatches to
* {@code getClassMapping(Class key)}.
*
* If no mapping has been defined for the specified {@code Class}, then
* it's superclass is checked, and then that classes' superclass, and so on
* until {@code java.lang.Object} is reached. If a mapping is found anywhere
* within the superclass hierarchy, then the mapped {@code Class} is
* returned. Otherwise, the value returned by {@code getDefaultMapping()} is
* returned.
*
* @param obj
* the {@code Object} whose {@code Class's} internal mapping will
* be returned
* @return the {@code Class} that is mapped internally to the specified key
* {@code Class}
* @see #getDefaultMapping()
* @see #addClassMapping(Object, Class)
* @see #removeClassMapping(Object)
*/
public Class getClassMapping(Object obj) {
Class key = obj == null ? null : obj.getClass();
return getClassMapping(key);
}
/**
* Returns the {@code Class} associated with the specified {@code Class}.
* If {@code key} is {@code null}, this method will return the value
* retrieved from {@code getDefaultMapping()}. If no mapping has been
* defined for the specified {@code Class}, then it's superclass is
* checked, and then that classes' superclass, and so on until
* {@code java.lang.Object} is reached. If a mapping is found anywhere
* within the superclass hierarchy, then the mapped {@code Class} is
* returned. Otherwise, the value returned by {@code getDefaultMapping()} is
* returned.
*
* @param key
* the {@code Class} whose internal mapping will be returned
* @return the {@code Class} that is mapped internally to the specified
* {@code key}
* @see #getDefaultMapping()
* @see #addClassMapping(Class, Class)
* @see #removeClassMapping(Class)
*/
public Class getClassMapping(Class key) {
if (key == null) {
return defaultClass;
}
Class value = null;
synchronized (classes) {
for (Class c = key; c != null && value == null; c = c
.getSuperclass()) {
value = (Class) classes.get(c);
}
}
return value == null ? defaultClass : value;
}
/**
* Returns the {@code Object} instance associated with the specified
* {@code Class}. If {@code key} is {@code null}, this method will return
* the value retrieved from {@code getDefaultInstance()}. If no mapping has
* been defined for the specified {@code Class}, then it's superclass is
* checked, and then that classes' superclass, and so on until
* {@code java.lang.Object} is reached. If an instance mapping is found
* anywhere within the superclass hierarchy, then the mapped {@code Object}
* is returned. Otherwise, the value returned by
* {@code getDefaultInstance()} is returned.
*
* @param key
* the {@code Class} whose internal mapping will be returned
* @return the {@code Object} instance that is mapped internally to the
* specified {@code key}
* @see #getDefaultInstance()
* @see #addClassMapping(Class, Class, Object)
* @see #removeClassMapping(Class)
*/
public Object getClassInstance(Class key) {
if (key == null) {
return defaultInstance;
}
Object value = null;
synchronized (instances) {
for (Class c = key; c != null && value == null; c = c
.getSuperclass()) {
value = instances.get(c);
}
}
return value == null ? defaultInstance : value;
}
/**
* Returns the default {@code Class} used for situations in which there is
* no internal class mapping. This property is read-only and is initialized
* within the {@code ClassMapping} constructor.
*
* @return the default {@code Class} used for situations in which there is
* no internal class mapping.
* @see #ClassMapping(Class, Object)
*/
public Class getDefaultMapping() {
return defaultClass;
}
/**
* Returns the default {@code Object} used for situations in which there is
* no internal instance mapping. This property is read-only and is
* initialized within the {@code ClassMapping} constructor.
*
* @return the default {@code Object} used for situations in which there is
* no internal instance mapping.
* @see #ClassMapping(Class, Object)
*/
public Object getDefaultInstance() {
return defaultInstance;
}
}