/*
* Copyright (c) Thomas Parker, 2013.
*
* 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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import pcgen.base.util.WrappedMapSet;
import pcgen.cdom.base.QualifiedActor;
import pcgen.cdom.base.QualifyingObject;
import pcgen.cdom.enumeration.CharID;
import pcgen.cdom.facet.FacetLibrary;
import pcgen.cdom.facet.PrerequisiteFacet;
import pcgen.cdom.facet.event.DataFacetChangeEvent;
/**
* An AbstractQualifiedListFacet is a DataFacet that contains information about
* QualifyingObjects that are contained in a PlayerCharacter when a
* PlayerCharacter may have more than one of that type of QualifyingObject (e.g.
* Language, PCTemplate), the source of that object should be tracked, and the
* PlayerCharacter can qualify for the object (they have prerequisites)
*
* This class is designed to assume that each QualifyingObject may only be
* contained one time by the PlayerCharacter, even if received from multiple
* sources. The QualifyingObject 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 AbstractQualifiedListFacet 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.
*
* In general, QualifyingObjects that are stored in an
* AbstractQualifiedListFacet are those where the Prerequisites are those that
* are considered requirements. This means that as the Player Character changes,
* the state of the Prerequisite can change and alter whether the underlying
* object is granted to the Player Character. For PCGen 5.16, this will mean
* things like the Prerequisite on the end of an ABILITY token (which are
* continuously evaluated) not the PRExxx: tokens that appear directly on the
* line of an Ability in the Ability LST file (those are evaluated only once,
* when the Ability is first added to the Player Character)
*
* null is a valid source but a valid item to be added to the list of objects
* stored by AbstractQualifiedListFacet.
*
* @author Thomas Parker (thpr [at] yahoo.com)
*/
public abstract class AbstractQualifiedListFacet<T extends QualifyingObject>
extends AbstractDataFacet<CharID, T>
{
private PrerequisiteFacet prereqFacet = FacetLibrary
.getFacet(PrerequisiteFacet.class);
/**
* Add the given object with the given source to the list of objects stored
* in this AbstractQualifiedListFacet 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 to be added to the list of objects stored in this
* AbstractQualifiedListFacet for the Player Character
* represented by the given CharID
* @param source
* The source for the given object
*/
public void add(CharID id, T obj, Object source)
{
if (obj == null)
{
throw new IllegalArgumentException("Object to add may not be null");
}
Map<T, Set<Object>> map = getConstructingCachedMap(id);
Set<Object> set = map.get(obj);
boolean fireNew = (set == null);
if (fireNew)
{
set = new WrappedMapSet<>(IdentityHashMap.class);
map.put(obj, set);
}
set.add(source);
if (fireNew)
{
fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_ADDED);
}
}
/**
* Adds all of the objects in the given Collection to the list of objects
* stored in this AbstractQualifiedListFacet for the Player Character
* represented by the given CharID. All objects are added as if granted 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 to be added to the list of objects
* stored in this AbstractQualifiedListFacet for the Player
* Character represented by the given CharID
* @param source
* The source for the objects in the given Collection
* @throws NullPointerException
* if the given Collection is null
*/
public void addAll(CharID id, Collection<? extends T> c, Object source)
{
for (T obj : c)
{
add(id, obj, source);
}
}
/**
* Removes the given source entry from the list of sources for the given
* object stored in this AbstractQualifiedListFacet for the Player Character
* represented by the given CharID. If the given source was the only source
* for the given object, then the 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 from which the
* given item source should be removed
* @param obj
* The object for which the source should be removed
* @param source
* The source for the given object to be removed from the list of
* sources.
*/
public void remove(CharID id, T obj, Object source)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap != null)
{
processRemoval(id, componentMap, obj, source);
}
}
/**
* Removes the given source entry from the list of sources for 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
* objects in the collection, then those objects are 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 from which the
* given items should be removed
* @param c
* The Collection of objects to be removed from the list of
* objects stored in this AbstractQualifiedListFacet 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<T> c, Object source)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap != null)
{
for (T obj : c)
{
processRemoval(id, componentMap, obj, source);
}
}
}
/**
* Removes all objects (and all sources for those objects) from the list of
* objects stored in this AbstractQualifiedListFacet 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
* AbstractQualifiedListFacet and modification of this
* AbstractQualifiedListFacet 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
* AbstractQualifiedListFacet. If you wish to modify the information stored
* in this AbstractQualifiedListFacet, you must use the add*() and remove*()
* methods of AbstractQualifiedListFacet.
*
* @param id
* The CharID representing the Player Character from which all
* items should be removed
* @return A non-null Set of objects removed from the list of objects stored
* in this AbstractQualifiedListFacet for the Player Character
* represented by the given CharID
*/
public Map<T, Set<Object>> removeAll(CharID id)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap == null)
{
return Collections.emptyMap();
}
removeCache(id);
for (T obj : componentMap.keySet())
{
fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_REMOVED);
}
return componentMap;
}
/**
* Returns a non-null copy of the Set of objects in this
* AbstractQualifiedListFacet for the Player Character represented by the
* given CharID. This method returns an empty set if no objects are in this
* AbstractQualifiedListFacet for the Player Character identified by the
* given CharID.
*
* This method is value-semantic in that ownership of the returned List is
* transferred to the class calling this method. Modification of the
* returned List will not modify this AbstractQualifiedListFacet and
* modification of this AbstractQualifiedListFacet will not modify the
* returned List. Modifications to the returned List will also not modify
* any future or previous objects returned by this (or other) methods on
* AbstractQualifiedListFacet. If you wish to modify the information stored
* in this AbstractQualifiedListFacet, you must use the add*() and remove*()
* methods of AbstractQualifiedListFacet.
*
* @param id
* The CharID representing the Player Character for which the
* items in this AbstractQualifiedListFacet should be returned.
* @return A non-null Set of objects in this AbstractQualifiedListFacet for
* the Player Character represented by the given CharID
*/
public Set<T> getSet(CharID id)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap == null)
{
return Collections.emptySet();
}
return Collections.unmodifiableSet(componentMap.keySet());
}
/**
* Returns the count of items in this AbstractQualifiedListFacet for the
* Player Character represented by the given CharID
*
* @param id
* The CharID representing the Player Character for which the
* count of items should be returned
* @return The count of items in this AbstractQualifiedListFacet for the
* Player Character represented by the given CharID
*/
public int getCount(CharID id)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap == null)
{
return 0;
}
return componentMap.size();
}
/**
* Returns true if this AbstractQualifiedListFacet 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 AbstractQualifiedListFacet 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<T, Set<Object>> componentMap = getCachedMap(id);
return componentMap == null || componentMap.isEmpty();
}
/**
* Returns true if this AbstractQualifiedListFacet contains the given value
* 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 AbstractQualifiedListFacet contains
* that item for the Player Character represented by the given
* CharID
* @return true if this AbstractQualifiedListFacet contains the given value
* for the Player Character represented by the given CharID; false
* otherwise
*/
public boolean contains(CharID id, T obj)
{
/*
* TODO obj == null? - log an error?
*
* This should share behavior with AbstractListFacet
*/
Map<T, Set<Object>> componentMap = getCachedMap(id);
return componentMap != null && componentMap.containsKey(obj);
}
/**
* Returns a Set of sources for this AbstractQualifiedListFacet, the
* PlayerCharacter represented by the given CharID, and the given 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 Set if no
* information has been set in this AbstractQualifiedListFacet 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
* AbstractQualifiedListFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractQualifiedListFacet.
*
* @param id
* The CharID for which the Set should be returned
* @param obj
* The object for which the Set of sources should be returned
* @return The Set of sources for the given object and Player Character
* represented by the given CharID.
*/
private Set<Object> getConstructingCachedSetFor(CharID id, T obj)
{
Map<T, Set<Object>> map = getConstructingCachedMap(id);
Set<Object> set = map.get(obj);
if (set == null)
{
set = new WrappedMapSet<>(IdentityHashMap.class);
map.put(obj, set);
}
return set;
}
/**
* Returns the type-safe Map for this AbstractQualifiedListFacet and the
* given CharID. May return null if no information has been set in this
* AbstractQualifiedListFacet for the given CharID.
*
* Note that this method SHOULD NOT be public. The Map is owned by
* AbstractQualifiedListFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractQualifiedListFacet.
*
* @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
* AbstractQualifiedListFacet for the Player Character.
*/
private Map<T, Set<Object>> getCachedMap(CharID id)
{
return (Map<T, Set<Object>>) getCache(id);
}
/**
* Returns a type-safe Map for this AbstractQualifiedListFacet and the given
* CharID. Will return a new, empty Map if no information has been set in
* this AbstractQualifiedListFacet for the given CharID. Will not return
* null.
*
* Note that this method SHOULD NOT be public. The Map object is owned by
* AbstractQualifiedListFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractQualifiedListFacet.
*
* @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<T, Set<Object>> getConstructingCachedMap(CharID id)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap == null)
{
componentMap = getComponentMap();
setCache(id, componentMap);
}
return componentMap;
}
/**
* Returns a new (empty) Map for this AbstractQualifiedListFacet. Can be
* overridden by classes that extend AbstractQualifiedListFacet if a Map
* other than an IdentityHashMap is desired for storing the information in
* the AbstractQualifiedListFacet.
*
* Note that this method SHOULD NOT be public. The Map object is owned by
* AbstractQualifiedListFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractQualifiedListFacet.
*
* Note that this method should always be the only method used to construct
* a Map for this AbstractQualifiedListFacet. It is actually preferred to
* use getConstructingCacheMap(CharID) in order to implicitly call this
* method.
*
* @return A new (empty) Map for use in this AbstractQualifiedListFacet.
*/
protected Map<T, Set<Object>> getComponentMap()
{
return new IdentityHashMap<>();
}
/**
* Copies the contents of the AbstractQualifiedListFacet from one Player
* Character to another Player Character, based on the given CharIDs
* representing those Player Characters.
*
* This is a method in AbstractQualifiedListFacet in order to avoid exposing
* the mutable Map object to other classes. This should not be inlined, as
* the Map is internal information to AbstractQualifiedListFacet 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 AbstractQualifiedListFacet
* of one Player Character will only impact the Player Character where the
* AbstractQualifiedListFacet 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<T, Set<Object>> sourceMap = getCachedMap(source);
if (sourceMap != null)
{
for (Map.Entry<T, Set<Object>> me : sourceMap.entrySet())
{
T obj = me.getKey();
Set<Object> sourceSet = me.getValue();
Set<Object> targetSet = getConstructingCachedSetFor(
destination, obj);
targetSet.addAll(sourceSet);
}
}
}
/**
* This method implements removal of a source for an object contained by
* this AbstractQualifiedListFacet. 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.
*
* @param id
* The CharID representing the Player Character which may have
* the given item removed.
* @param componentMap
* The (private) Map for this AbstractQualifiedListFacet 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<T, Set<Object>> componentMap,
T obj, Object source)
{
if (obj == null)
{
throw new IllegalArgumentException("Object to remove may not be null");
}
Set<Object> set = componentMap.get(obj);
if (set != null)
{
set.remove(source);
if (set.isEmpty())
{
componentMap.remove(obj);
fireDataFacetChangeEvent(id, obj,
DataFacetChangeEvent.DATA_REMOVED);
}
}
}
/**
* Removes all information for the given source from this
* AbstractQualifiedListFacet 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<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap != null)
{
/*
* This list exists primarily to eliminate the possibility of a
* concurrent modification exception on a recursive remove
*/
List<T> removedKeys = new ArrayList<>();
for (Iterator<Map.Entry<T, Set<Object>>> it =
componentMap.entrySet().iterator(); it.hasNext();)
{
Entry<T, Set<Object>> me = it.next();
Set<Object> set = me.getValue();
if (set.remove(source) && set.isEmpty())
{
T obj = me.getKey();
it.remove();
removedKeys.add(obj);
}
}
if (componentMap.isEmpty())
{
removeCache(id);
}
for (T obj : removedKeys)
{
fireDataFacetChangeEvent(id, obj,
DataFacetChangeEvent.DATA_REMOVED);
}
}
}
/**
* Returns a non-null copy of the Set of objects in this
* AbstractQualifiedListFacet for the Player Character represented by the
* given CharID and the given source. This method returns an empty set if no
* objects are in this AbstractQualifiedListFacet for the Player Character
* identified by the given CharID and source.
*
* This method is value-semantic in that ownership of the returned List is
* transferred to the class calling this method. Modification of the
* returned List will not modify this AbstractQualifiedListFacet and
* modification of this AbstractQualifiedListFacet will not modify the
* returned List. Modifications to the returned List will also not modify
* any future or previous objects returned by this (or other) methods on
* AbstractQualifiedListFacet. If you wish to modify the information stored
* in this AbstractQualifiedListFacet, you must use the add*() and remove*()
* methods of AbstractQualifiedListFacet.
*
* @param id
* The CharID representing the Player Character for which the
* items in this AbstractQualifiedListFacet should be returned.
* @param owner
* The source object for which a copy of the List of objects in
* this AbstractQualifiedListFacet should be returned.
* @return A non-null Set of objects in this AbstractQualifiedListFacet for
* the Player Character represented by the given CharID
*/
public List<? extends T> getSet(CharID id, Object owner)
{
List<T> list = new ArrayList<>();
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap != null)
{
for (Entry<T, Set<Object>> me : componentMap.entrySet())
{
Set<Object> set = me.getValue();
if (set.contains(owner))
{
list.add(me.getKey());
}
}
}
return list;
}
/**
* Returns a non-null copy of the Set of objects the character qualifies for
* in this AbstractQualifiedListFacet for the Player Character represented
* by the given CharID. This method returns an empty set if the Player
* Character identified by the given CharID qualifies for none of the
* objects in this AbstractQualifiedListFacet.
*
* This method is value-semantic in that ownership of the returned
* Collection is transferred to the class calling this method. Modification
* of the returned Collection will not modify this
* AbstractQualifiedListFacet and modification of this
* AbstractQualifiedListFacet will not modify the returned Collection.
* Modifications to the returned Collection will also not modify any future
* or previous objects returned by this (or other) methods on
* AbstractQualifiedListFacet. If you wish to modify the information stored
* in this AbstractQualifiedListFacet, you must use the add*() and remove*()
* methods of AbstractQualifiedListFacet.
*
* @param id
* The CharID representing the Player Character for which the
* items in this AbstractQualifiedListFacet should be returned.
* @return A non-null Set of objects the Player Character represented by the
* given CharID qualifies for in this AbstractQualifiedListFacet
*/
public Collection<T> getQualifiedSet(CharID id)
{
Set<T> set = new HashSet<>();
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap != null)
{
for (Map.Entry<T, Set<Object>> me : componentMap.entrySet())
{
T obj = me.getKey();
Set<Object> sources = me.getValue();
for (Object source : sources)
{
if (prereqFacet.qualifies(id, obj, source))
{
set.add(obj);
break;
}
}
}
}
return set;
}
/**
* Returns a non-null copy of the Set of objects the character qualifies for
* in this AbstractQualifiedListFacet for the Player Character represented
* by the given CharID and the given source. This method returns an empty
* set if the Player Character identified by the given CharID qualifies for
* none of the objects in this AbstractQualifiedListFacet granted by the
* given source.
*
* This method is value-semantic in that ownership of the returned List is
* transferred to the class calling this method. Modification of the
* returned List will not modify this AbstractQualifiedListFacet and
* modification of this AbstractQualifiedListFacet will not modify the
* returned List. Modifications to the returned List will also not modify
* any future or previous objects returned by this (or other) methods on
* AbstractQualifiedListFacet. If you wish to modify the information stored
* in this AbstractQualifiedListFacet, you must use the add*() and remove*()
* methods of AbstractQualifiedListFacet.
*
* Generally, use of this method is discouraged in general operational
* aspects. However, it is recognized that certain output tokens can list
* certain items by source, and thus this method is required, and it is
* unreasonable to expect complete elimination of this method or entirely
* prohibit future use of this method.
*
* @param id
* The CharID representing the Player Character for which the
* items in this AbstractQualifiedListFacet should be returned.
* @param source
* The source object for which a copy of the List of objects the
* Player Character qualifies for should be returned.
* @return A non-null Set of objects the Player Character represented by the
* given CharID qualifies for in this AbstractQualifiedListFacet
*/
public Collection<T> getQualifiedSet(CharID id, Object source)
{
Set<T> set = new WrappedMapSet<>(IdentityHashMap.class);
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap != null)
{
for (Map.Entry<T, Set<Object>> me : componentMap.entrySet())
{
T obj = me.getKey();
Set<Object> sources = me.getValue();
if (sources.contains(source))
{
if (prereqFacet.qualifies(id, obj, source))
{
set.add(obj);
}
}
}
}
return set;
}
/**
* Acts on the Set of objects the character qualifies for in this
* AbstractQualifiedListFacet for the Player Character represented by the
* given CharID. The results of each action as provided by the given
* QualifiedActor are returned in a non-null List.
*
* This method returns an empty List if the Player Character identified by
* the given CharID qualifies for none of the objects in this
* AbstractQualifiedListFacet.
*
* This method is value-semantic in that ownership of the returned List is
* transferred to the class calling this method. Modification of the
* returned List will not modify this AbstractQualifiedListFacet and
* modification of this AbstractQualifiedListFacet will not modify the
* returned List. Modifications to the returned List will also not modify
* any future or previous objects returned by this (or other) methods on
* AbstractQualifiedListFacet. If you wish to modify the information stored
* in this AbstractQualifiedListFacet, you must use the add*() and remove*()
* methods of AbstractQualifiedListFacet.
*
* Note: If a particular item has been granted by more than one source, then
* the QualifiedActor will only be called for the first source that
* (successfully grants) the underlying object.
*
* @param id
* The CharID representing the Player Character for which the
* items in this AbstractQualifiedListFacet should be returned.
* @param qa
* The QualifiedActor which will act on each of the items in this
* AbstractQualifiedListFacet for which the Player Character
* qualifies.
* @return A non-null List of objects created by the QualifiedActor from
* each of the objects in this AbstractQualifiedListFacet for which
* the Player Character qualifies.
*/
public <R> List<R> actOnQualifiedSet(CharID id, QualifiedActor<T, R> qa)
{
List<R> list = new ArrayList<>();
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap != null)
{
for (Map.Entry<T, Set<Object>> me : componentMap.entrySet())
{
T obj = me.getKey();
Set<Object> sources = me.getValue();
for (Object source : sources)
{
if (prereqFacet.qualifies(id, obj, source))
{
list.add(qa.act(obj, source));
}
}
}
}
return list;
}
public Collection<Object> getSources(CharID id, T obj)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap != null)
{
Set<Object> sources = componentMap.get(obj);
if (sources != null)
{
return Collections.unmodifiableSet(sources);
}
}
return Collections.emptySet();
}
}