/* * Copyright (c) Thomas Parker, 2009. * * 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.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.ListSet; import pcgen.cdom.enumeration.CharID; import pcgen.cdom.facet.event.DataFacetChangeEvent; /** * An AbstractSingleSourceListFacet is a DataFacet that contains information * about Objects that are contained in a PlayerCharacter when a PlayerCharacter * may have more than one of that type of Object (e.g. Language, PCTemplate) and * the source of that object should be tracked. * * Using this class, an object may have only one source. If the object is * re-added with a second source, this will not trigger a DATA_ADDED event, but * the object considered the source of the item in the list of this * AbstractSingleSourceListFacet will be updated to the source provided in the * call to add. * * null is NOT a valid source. * * @author Thomas Parker (thpr [at] yahoo.com) */ public abstract class AbstractSingleSourceListFacet<T, ST> extends AbstractDataFacet<CharID, T> { /** * Add the given object with the given source to the list of objects stored * in this AbstractSingleSourceListFacet 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 * AbstractSingleSourceListFacet for the Player Character * represented by the given CharID * @param source * The source for the given object */ public void add(CharID id, T obj, ST source) { if (obj == null) { throw new IllegalArgumentException("Object to add may not be null"); } if (source == null) { throw new IllegalArgumentException("Source may not be null"); } Map<T, ST> map = getConstructingCachedMap(id); Object oldsource = map.get(obj); boolean fireNew = (oldsource == null); map.put(obj, source); if (fireNew) { fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_ADDED); } } /** * Adds all of the objects with the given source in the given Collection to * the list of objects stored in this AbstractSingleSourceListFacet for the * Player Character represented by the given CharID * * @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 AbstractSingleSourceListFacet for the Player * Character represented by the given CharID * @param source * The source for the given object * @throws NullPointerException * if the given Collection is null */ public void addAll(CharID id, Collection<? extends T> c, ST source) { for (T obj : c) { add(id, obj, source); } } /** * Removes the given source entry for the given object stored in this * AbstractSingleSourceListFacet for the Player Character represented by the * given CharID. * * If the given source is the source of the object recognized by this * AbstractSingleSourceListFacet, then the object is removed from the list * of objects stored in this AbstractSingleSourceListFacet for the Player * Character represented by the given CharID. * * If the given source is not the source recognized by this * AbstractSingleSourceListFacet, then this call to remove is ignored. * * @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, ST source) { Map<T, ST> 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 is the source (recognized by this * AbstractSingleSourceListFacet) of an object in the given collection, then * that object is removed from the list of objects stored in this * AbstractSingleSourceListFacet for the Player Character represented by the * given CharID. * * If the given source is not the source recognized by this * AbstractSingleSourceListFacet of an object in the given collection, then * no change is made for that object to the list of objects stored in this * AbstractSingleSourceListFacet 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 to be removed from the list of * objects stored in this AbstractSingleSourceListFacet 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, ST source) { Map<T, ST> 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 AbstractSingleSourceListFacet 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 * AbstractSingleSourceListFacet and modification of this * AbstractSingleSourceListFacet 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 * AbstractSingleSourceListFacet. If you wish to modify the information * stored in this AbstractSingleSourceListFacet, you must use the add*() and * remove*() methods of AbstractSingleSourceListFacet. * * @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 AbstractSingleSourceListFacet for the Player Character * represented by the given CharID */ public Map<T, ST> removeAll(CharID id) { Map<T, ST> 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 * AbstractSingleSourceListFacet for the Player Character represented by the * given CharID. This method returns an empty set if no objects are in this * AbstractSingleSourceListFacet 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 AbstractSingleSourceListFacet and * modification of this AbstractSingleSourceListFacet 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 * AbstractSingleSourceListFacet. If you wish to modify the information * stored in this AbstractSingleSourceListFacet, you must use the add*() and * remove*() methods of AbstractSingleSourceListFacet. * * * @param id * The CharID representing the Player Character for which the * items in this AbstractSingleSourceListFacet should be * returned. * @return A non-null Set of objects in this AbstractSingleSourceListFacet * for the Player Character represented by the given CharID */ public Set<T> getSet(CharID id) { Map<T, ST> componentMap = getCachedMap(id); if (componentMap == null) { return Collections.emptySet(); } return Collections.unmodifiableSet(new ListSet<>(componentMap.keySet())); } /** * Returns the count of items in this AbstractSingleSourceListFacet 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 AbstractSingleSourceListFacet for the * Player Character represented by the given CharID */ public int getCount(CharID id) { Map<T, ST> componentMap = getCachedMap(id); if (componentMap == null) { return 0; } return componentMap.size(); } /** * Returns true if this AbstractSingleSourceListFacet 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 AbstractSingleSourceListFacet * @return true if this AbstractSingleSourceListFacet 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, ST> componentMap = getCachedMap(id); return componentMap == null || componentMap.isEmpty(); } /** * Returns true if this AbstractSingleSourceListFacet 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 AbstractSingleSourceListFacet * contains that item for the Player Character represented by the * given CharID * @return true if this AbstractSingleSourceListFacet contains the given * value for the Player Character represented by the given CharID; * false otherwise */ public boolean contains(CharID id, T obj) { Map<T, ST> componentMap = getCachedMap(id); return componentMap != null && componentMap.containsKey(obj); } /** * Returns the type-safe Map for this AbstractSingleSourceListFacet and the * given CharID. May return null if no information has been set in this * AbstractSingleSourceListFacet for the given CharID. * * Note that this method SHOULD NOT be public. The Map is owned by * AbstractSingleSourceListFacet, and since it can be modified, a reference * to that object should not be exposed to any object other than * AbstractSingleSourceListFacet. * * @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 * AbstractSingleSourceListFacet for the Player Character. */ protected Map<T, ST> getCachedMap(CharID id) { return (Map<T, ST>) getCache(id); } /** * Returns a type-safe Map for this AbstractSingleSourceListFacet and the * given CharID. Will return a new, empty Map if no information has been set * in this AbstractSingleSourceListFacet for the given CharID. Will not * return null. * * Note that this method SHOULD NOT be public. The Map object is owned by * AbstractSingleSourceListFacet, and since it can be modified, a reference * to that object should not be exposed to any object other than * AbstractSingleSourceListFacet. * * @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, ST> getConstructingCachedMap(CharID id) { Map<T, ST> componentMap = getCachedMap(id); if (componentMap == null) { componentMap = getComponentMap(); setCache(id, componentMap); } return componentMap; } /** * Returns a new (empty) Map for this AbstractSingleSourceListFacet. Can be * overridden by classes that extend AbstractSingleSourceListFacet if a Map * other than an IdentityHashMap is desired for storing the information in * the AbstractSingleSourceListFacet. * * Note that this method SHOULD NOT be public. The Map object is owned by * AbstractSingleSourceListFacet, and since it can be modified, a reference * to that object should not be exposed to any object other than * AbstractSingleSourceListFacet. * * Note that this method should always be the only method used to construct * a Map for this AbstractSingleSourceListFacet. It is actually preferred to * use getConstructingCacheMap(CharID) in order to implicitly call this * method. * * @return A new (empty) Map for use in this AbstractSingleSourceListFacet. */ protected Map<T, ST> getComponentMap() { return new IdentityHashMap<>(); } /** * Copies the contents of the AbstractSingleSourceListFacet from one Player * Character to another Player Character, based on the given CharIDs * representing those Player Characters. * * This is a method in AbstractSingleSourceListFacet in order to avoid * exposing the mutable Map object to other classes. This should not be * inlined, as the Map is internal information to * AbstractSingleSourceListFacet 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 * AbstractSingleSourceListFacet of one Player Character will only impact * the Player Character where the AbstractSingleSourceListFacet 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, ST> sourceMap = getCachedMap(source); if (sourceMap != null) { getConstructingCachedMap(destination).putAll(sourceMap); } } /** * This method implements removal of a source for an object contained by * this AbstractSingleSourceListFacet. This implements the actual check that * determines if the given source was the 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 AbstractSingleSourceListFacet that * will be removed if the given source is the recognized source * for the given object in this AbstractSingleSourceListFacet. * @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, ST> componentMap, T obj, ST source) { /* * TODO obj Null? * * Behavior should be consistent with AbstractlistFacet & others on * remove */ Object oldSource = componentMap.get(obj); if (oldSource != null) { if (oldSource.equals(source)) { componentMap.remove(obj); fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_REMOVED); } } } /** * Removes all information for the given source from this * AbstractSingleSourceListFacet 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, ST source) { Map<T, ST> 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, ST>> it = componentMap.entrySet() .iterator(); it.hasNext();) { Entry<T, ST> me = it.next(); Object currentsource = me.getValue(); if (currentsource.equals(source)) { 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 * AbstractSingleSourceListFacet 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 AbstractSingleSourceListFacet 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 AbstractSingleSourceListFacet and * modification of this AbstractSingleSourceListFacet 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 * AbstractSingleSourceListFacet. If you wish to modify the information * stored in this AbstractSingleSourceListFacet, you must use the add*() and * remove*() methods of AbstractSingleSourceListFacet. * * @param id * The CharID representing the Player Character for which the * items in this AbstractSingleSourceListFacet should be * returned. * @param owner * The source object for which a copy of the List of objects in * this AbstractSingleSourceListFacet should be returned. * @return A non-null Set of objects in this AbstractSingleSourceListFacet * for the Player Character represented by the given CharID */ public List<? extends T> getSet(CharID id, ST owner) { List<T> list = new ArrayList<>(); Map<T, ST> componentMap = getCachedMap(id); if (componentMap != null) { for (Entry<T, ST> me : componentMap.entrySet()) { Object source = me.getValue(); if (source.equals(owner)) { list.add(me.getKey()); } } } return Collections.unmodifiableList(list); } /** * Gets the source for the Player Character (identified by the given CharID) * and the given object. * * @param id * The CharID identifying the Player Character for which the * association get is being performed. * @param obj * The object for which the source is to be returned. * @return The source of the given object for the Player Character * identified by the given CharID */ public ST getSource(CharID id, T obj) { Map<T, ST> map = getCachedMap(id); if (map == null) { return null; } return map.get(obj); } /** * Removes (unconditionally, regardless of the source) the given object * stored in this AbstractSingleSourceListFacet for the Player Character * represented by the given CharID. * * @param id * The CharID representing the Player Character from which the * object should be removed * @param obj * The object which should be removed */ public void remove(CharID id, T obj) { Map<T, ST> map = getCachedMap(id); if (map != null) { if (map.remove(obj) != null) { fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_REMOVED); } } } }