/*
* Copyright (c) Thomas Parker, 2010-14.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
package pcgen.cdom.facet.base;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import pcgen.base.util.WrappedMapSet;
import pcgen.cdom.enumeration.CharID;
import pcgen.cdom.facet.event.DataFacetChangeEvent;
/**
* An AbstractItemConvertingFacet is a DataFacet that converts information from
* one type to another when the source of that object should be tracked.
*
* This class is designed to assume that each original object may only be
* contained one time by the PlayerCharacter, even if received from multiple
* sources. The original object will only trigger one DATA_ADDED event (when
* added by the first source) and if removed by some sources, will only trigger
* one DATA_REMOVED event (when it is removed by the last remaining source).
* Sources do not need to be removed in the order in which they are added, and
* the first source to be added does not possess special status with respect to
* triggering a DATA_REMOVED event (it will only trigger removal if it was the
* last source when removed)
*
* The sources stored in this AbstractItemConvertingFacet are stored as a List,
* meaning the list of sources may contain the same source multiple times. If
* so, each call to remove will only remove that source one time from the list
* of sources.
*
* Note: There is no requirement that the conversion process is reversible. In
* other words, more than once source object may produce the same (or equal)
* destination objects.
*
* null is a valid source.
*
* @author Thomas Parker (thpr [at] yahoo.com)
*/
public abstract class AbstractItemConvertingFacet<S, D> extends
AbstractDataFacet<CharID, D>
{
/**
* Add the converted version of the given object with the given source to
* the list of (converted) objects stored in this
* AbstractItemConvertingFacet for the Player Character represented by the
* given CharID.
*
* @param id
* The CharID representing the Player Character for which the
* given item should be added
* @param obj
* The object for which the converted version will be added to
* the list of (converted) objects stored in this
* AbstractItemConvertingFacet for the Player Character
* represented by the given CharID
* @param source
* The source for the given object
*/
public void add(CharID id, S obj, Object source)
{
if (obj == null)
{
throw new IllegalArgumentException("Object to add may not be null");
}
Target target = getConstructingCachedSetFor(id, obj);
target.set.add(source);
if (target.dest == null)
{
target.dest = convert(obj);
fireDataFacetChangeEvent(id, target.dest,
DataFacetChangeEvent.DATA_ADDED);
}
}
/**
* Adds conversions of all of the objects in the given Collection to the
* list of (converted) objects stored in this AbstractItemConvertingFacet
* for the Player Character represented by the given CharID. All items are
* added with the given source.
*
* @param id
* The CharID representing the Player Character for which the
* given items should be added
* @param c
* The Collection of objects for which the converted versions
* will be added to the list of objects stored in this
* AbstractItemConvertingFacet for the Player Character
* represented by the given CharID
* @param source
* The source for the given objects in the collection
* @throws NullPointerException
* if the given Collection is null
*/
public void addAll(CharID id, Collection<? extends S> c, Object source)
{
for (S obj : c)
{
add(id, obj, source);
}
}
/**
* Removes the given source entry from the list of sources for conversion of
* the given object stored in this AbstractItemConvertingFacet for the
* Player Character represented by the given CharID. If the given source was
* the only source for the given object, then the converted object is
* removed from the list of objects stored in this
* AbstractItemConvertingFacet for the Player Character represented by the
* given CharID.
*
* @param id
* The CharID representing the Player Character from which the
* given item source should be removed
* @param obj
* The object for which the source should be removed from the
* converted version of that object
* @param source
* The source for the given object is to be removed from the list
* of sources for the converted version of the given object
*/
public void remove(CharID id, S obj, Object source)
{
Map<S, Target> componentMap = getCachedMap(id);
if (componentMap != null)
{
processRemoval(id, componentMap, obj, source);
}
}
/**
* Removes the given source entry from the list of sources for the converted
* version of all of the objects in the given Collection for the Player
* Character represented by the given CharID. If the given source was the
* only source for any of the (converted) objects in the collection, then
* those objects are removed from the list of objects stored in this
* AbstractItemConvertingFacet for the Player Character represented by the
* given CharID.
*
* @param id
* The CharID representing the Player Character from which the
* given items should be removed
* @param c
* The Collection of objects for which the conversions will be
* removed from the list of objects stored in this
* AbstractItemConvertingFacet for the Player Character
* represented by the given CharID
* @param source
* The source for the objects in the given Collection to be
* removed from the list of sources
* @throws NullPointerException
* if the given Collection is null
*/
public void removeAll(CharID id, Collection<S> c, Object source)
{
Map<S, Target> componentMap = getCachedMap(id);
if (componentMap != null)
{
for (S obj : c)
{
processRemoval(id, componentMap, obj, source);
}
}
}
/**
* Removes all converted objects (and all sources for those objects) from
* the list of objects stored in this AbstractItemConvertingFacet for the
* Player Character represented by the given CharID.
*
* This method is value-semantic in that ownership of the returned Map is
* transferred to the class calling this method. Since this is a remove all
* function, modification of the returned Map will not modify this
* AbstractItemConvertingFacet and modification of this
* AbstractItemConvertingFacet will not modify the returned Map.
* Modifications to the returned Map will also not modify any future or
* previous objects returned by this (or other) methods on
* AbstractItemConvertingFacet. If you wish to modify the information stored
* in this AbstractItemConvertingFacet, you must use the add*() and
* remove*() methods of AbstractItemConvertingFacet.
*
* @param id
* The CharID representing the Player Character from which all
* items should be removed
* @return A non-null Map of converted object mapped to their sources, all
* of which were removed from the list of original objects stored in
* this AbstractItemConvertingFacet for the Player Character
* represented by the given CharID
*/
public Map<S, Target> removeAll(CharID id)
{
Map<S, Target> componentMap = getCachedMap(id);
if (componentMap == null)
{
return Collections.emptyMap();
}
removeCache(id);
for (Target tgt : componentMap.values())
{
fireDataFacetChangeEvent(id, tgt.dest,
DataFacetChangeEvent.DATA_REMOVED);
}
return componentMap;
}
/**
* Returns the count of (non-equal) original objects in this
* AbstractItemConvertingFacet for the Player Character represented by the
* given CharID.
*
* Note: This does not necessarily return the count of the number of
* (non-equal) converted objects added. It may, but it will do so if and
* only if the conversion process can not produce identical conversion
* targets from two unequal sources.
*
* @param id
* The CharID representing the Player Character for which the
* count of items should be returned
* @return The count of converted objects in this
* AbstractItemConvertingFacet for the Player Character represented
* by the given CharID
*/
public int getCount(CharID id)
{
Map<S, Target> componentMap = getCachedMap(id);
if (componentMap == null)
{
return 0;
}
return componentMap.size();
}
/**
* Returns true if this AbstractItemConvertingFacet does not contain any
* items for the Player Character represented by the given CharID.
*
* @param id
* The CharId representing the PlayerCharacter to test if any
* items are contained by this AbstractsSourcedListFacet
* @return true if this AbstractItemConvertingFacet does not contain any
* items for the Player Character represented by the given CharID;
* false otherwise (if it does contain items for the Player
* Character)
*/
public boolean isEmpty(CharID id)
{
Map<S, Target> componentMap = getCachedMap(id);
return componentMap == null || componentMap.isEmpty();
}
/**
* Returns true if this AbstractItemConvertingFacet was provided with the
* given source object to be converted and stored in the list of items for
* the Player Character represented by the given CharID.
*
* @param id
* The CharID representing the Player Character used for testing
* @param obj
* The object to test if this AbstractItemConvertingFacet
* contains that original item for the Player Character
* represented by the given CharID
* @return true if this AbstractItemConvertingFacet was provided with the
* given source object to be converted and stored in the list of
* items for the Player Character represented by the given CharID;
* false otherwise
*/
public boolean contains(CharID id, S obj)
{
Map<S, Target> componentMap = getCachedMap(id);
return componentMap != null && componentMap.containsKey(obj);
}
/**
* Returns a Target storage object for this AbstractItemConvertingFacet, the
* PlayerCharacter represented by the given CharID, and the given source
* object. Will add the given object to the list of items for the
* PlayerCharacter represented by the given CharID and will return a new,
* empty Target object if no information has been set in this
* AbstractItemConvertingFacet for the given CharID and given object. Will
* not return null.
*
* Note that this method SHOULD NOT be public. The Set object is owned by
* AbstractItemConvertingFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractItemConvertingFacet.
*
* @param id
* The CharID for which the Target should be returned
* @param obj
* The object for which the Target should be returned
* @return The Target object for the given object and Player Character
* represented by the given CharID.
*/
private Target getConstructingCachedSetFor(CharID id, S obj)
{
Map<S, Target> map = getConstructingCachedMap(id);
Target target = map.get(obj);
if (target == null)
{
target = new Target();
map.put(obj, target);
}
return target;
}
/**
* Returns the type-safe Map for this AbstractItemConvertingFacet and the
* given CharID. May return null if no information has been set in this
* AbstractItemConvertingFacet for the given CharID.
*
* Note that this method SHOULD NOT be public. The Map is owned by
* AbstractItemConvertingFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractItemConvertingFacet.
*
* @param id
* The CharID for which the Set should be returned
* @return The Set for the Player Character represented by the given CharID;
* null if no information has been set in this
* AbstractItemConvertingFacet for the Player Character.
*/
protected Map<S, Target> getCachedMap(CharID id)
{
return (Map<S, Target>) getCache(id);
}
/**
* Returns a type-safe Map for this AbstractItemConvertingFacet and the
* given CharID. Will return a new, empty Map if no information has been set
* in this AbstractItemConvertingFacet for the given CharID. Will not return
* null.
*
* Note that this method SHOULD NOT be public. The Map object is owned by
* AbstractItemConvertingFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractItemConvertingFacet.
*
* @param id
* The CharID for which the Map should be returned
* @return The Map for the Player Character represented by the given CharID.
*/
private Map<S, Target> getConstructingCachedMap(CharID id)
{
Map<S, Target> componentMap = getCachedMap(id);
if (componentMap == null)
{
componentMap = getComponentMap();
setCache(id, componentMap);
}
return componentMap;
}
/**
* Returns a new (empty) Map for this AbstractItemConvertingFacet. Can be
* overridden by classes that extend AbstractItemConvertingFacet if a Map
* other than an IdentityHashMap is desired for storing the information in
* the AbstractItemConvertingFacet.
*
* Note that this method SHOULD NOT be public. The Map object is owned by
* AbstractItemConvertingFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractItemConvertingFacet.
*
* Note that this method should always be the only method used to construct
* a Map for this AbstractItemConvertingFacet. It is actually preferred to
* use getConstructingCacheMap(CharID) in order to implicitly call this
* method.
*
* @return A new (empty) Map for use in this AbstractItemConvertingFacet.
*/
protected Map<S, Target> getComponentMap()
{
return new IdentityHashMap<S, Target>();
}
/**
* Copies the contents of the AbstractItemConvertingFacet from one Player
* Character to another Player Character, based on the given CharIDs
* representing those Player Characters.
*
* This is a method in AbstractItemConvertingFacet in order to avoid
* exposing the mutable Map object to other classes. This should not be
* inlined, as the Map is internal information to
* AbstractItemConvertingFacet and should not be exposed to other classes.
*
* Note also the copy is a one-time event and no references are maintained
* between the Player Characters represented by the given CharIDs (meaning
* once this copy takes place, any change to the AbstractItemConvertingFacet
* of one Player Character will only impact the Player Character where the
* AbstractItemConvertingFacet was changed).
*
* @param source
* The CharID representing the Player Character from which the
* information should be copied
* @param destination
* The CharID representing the Player Character to which the
* information should be copied
*/
@Override
public void copyContents(CharID source, CharID destination)
{
Map<S, Target> sourceMap = getCachedMap(source);
if (sourceMap != null)
{
for (Map.Entry<S, Target> me : sourceMap.entrySet())
{
Target origTarget = me.getValue();
if (origTarget != null)
{
S obj = me.getKey();
Target target = getConstructingCachedSetFor(destination,
obj);
//This could be dangerous!
target.dest = origTarget.dest;
target.set.addAll(origTarget.set);
}
}
}
}
/**
* This method implements removal of a source for an object contained by
* this AbstractItemConvertingFacet. This implements the actual check that
* determines if the given source was the only source for the given object.
* If so, then that object is removed from the list of objects stored in
* this AbstractQualifiedListFacet for the Player Character represented by
* the given CharID and a removal event is fired.
*
* @param id
* The CharID representing the Player Character which may have
* the given item removed.
* @param componentMap
* The (private) Map for this AbstractItemConvertingFacet that
* will as least have the given source removed from the list for
* the given object.
* @param obj
* The object which may be removed if the given source is the
* only source for this object in the Player Character
* represented by the given CharID
* @param source
* The source for the given object to be removed from the list of
* sources for that object
*/
private void processRemoval(CharID id, Map<S, Target> componentMap, S obj,
Object source)
{
if (obj == null)
{
throw new IllegalArgumentException(
"Object to remove may not be null");
}
Target target = componentMap.get(obj);
if (target != null)
{
target.set.remove(source);
if (target.set.isEmpty())
{
componentMap.remove(obj);
fireDataFacetChangeEvent(id, target.dest,
DataFacetChangeEvent.DATA_REMOVED);
}
}
}
/**
* Removes all information (converted and unconverted objects) for the given
* source from this AbstractItemConvertingFacet for the PlayerCharacter
* represented by the given CharID.
*
* @param id
* The CharID representing the Player Character for which items
* from the given source will be removed
* @param source
* The source for the objects to be removed from the list of
* items stored for the Player Character identified by the given
* CharID
*/
public void removeAll(CharID id, Object source)
{
Map<S, Target> componentMap = getCachedMap(id);
if (componentMap != null)
{
for (Iterator<Target> it = componentMap.values().iterator(); it
.hasNext();)
{
Target target = it.next();
if (target != null)
{
if (target.set.remove(source) && target.set.isEmpty())
{
it.remove();
fireDataFacetChangeEvent(id, target.dest,
DataFacetChangeEvent.DATA_REMOVED);
}
}
}
}
}
/**
* Returns true if this AbstractItemConvertingFacet contains an object from
* the given source for the Player Character identified by the given CharID.
*
* @param id
* The CharID representing the Player Character which will be
* checked to see if this AbstractItemConvertingFacet contains
* any objects for that Player Character
* @param source
* The source for the objects to be checked, along with the
* Player Character identified by the given CharID, to see if
* this AbstractItemConvertingFacet contains an object from the
* given source
* @return true if this AbstractItemConvertingFacet contains an object from
* the given source for the Player Character identified by the given
* CharID; false otherwise
*/
public boolean containsFrom(CharID id, Object source)
{
Map<S, Target> componentMap = getCachedMap(id);
if (componentMap != null)
{
for (Entry<S, Target> me : componentMap.entrySet())
{
Target target = me.getValue();
if (target != null)
{
if (target.set.contains(source))
{
return true;
}
}
}
}
return false;
}
/**
* The storage class for AbstractItemConvertingFacet. Used to store both the
* converted object as well as the list of sources for the given destination
* object.
*
* @author Thomas Parker (thpr [at] yahoo.com)
*/
private class Target
{
/**
* The set of objects from which the converted object has been received
*/
public Set<Object> set =
new WrappedMapSet<>(IdentityHashMap.class);
/**
* The converted ("destination") object
*/
public D dest;
@Override
public int hashCode()
{
return dest.hashCode();
}
@Override
public boolean equals(Object o)
{
if (o == this)
{
return true;
}
if (o instanceof AbstractItemConvertingFacet.Target)
{
Target other = (Target) o;
return dest.equals(other.dest) && set.equals(other.set);
}
return false;
}
}
/**
* Converts the given object to the destination object type stored in this
* AbstractItemConvertingFacet. Must be implemented by classes that extend
* AbstractItemConvertingFacet.
*
* @param obj
* The original object stored in this AbstractItemConvertingFacet
* @return The converted object to be stored in this
* AbstractItemConvertingFacet for the given original object
*/
protected abstract D convert(S obj);
public Collection<S> getSourceObjects(CharID id)
{
Set<S> set = new WrappedMapSet<>(IdentityHashMap.class);
Map<S, Target> componentMap = getCachedMap(id);
if (componentMap != null)
{
set.addAll(componentMap.keySet());
}
return set;
}
public D getResultFor(CharID id, S obj)
{
Map<S, Target> componentMap = getCachedMap(id);
return (componentMap == null) ? null : componentMap.get(obj).dest;
}
public Collection<Object> getSourcesFor(CharID id, S obj)
{
Map<S, Target> componentMap = getCachedMap(id);
Set<Object> set = new WrappedMapSet<>(IdentityHashMap.class);
if (componentMap == null)
{
return set;
}
set.addAll(componentMap.get(obj).set);
return set;
}
}