/*
* 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.LinkedHashSet;
import pcgen.cdom.base.PCGenIdentifier;
import pcgen.cdom.facet.event.DataFacetChangeEvent;
/**
* A AbstractListFacet is a DataFacet that contains information about Objects
* that are contained in a Player Character when a Player Character may have
* more than one of that type of Object (e.g. Language, PCTemplate). This is not
* used for Objects where the Player Character only possesses one of that type
* of object (e.g. Race, Deity)
*
* This class is also used when the source of the Objects in a Player Character
* do not need to be tracked. If the source needs to be tracked, then
* AbstractSourcedListFacet should be used.
*
* null is not a valid object to be stored.
*
* @param <IDT>
* The Type of identifier used in this AbstractListFacet
* @param <T>
* The Type of object stored in this AbstractListFacet
*/
public abstract class AbstractListFacet<IDT extends PCGenIdentifier, T> extends
AbstractDataFacet<IDT, T>
{
/**
* Add the given object to the list of objects stored in this
* AbstractListFacet for the Player Character represented by the given
* PCGenIdentifier
*
* @param id
* The PCGenIdentifier 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
* AbstractListFacet for the Player Character represented by the
* given PCGenIdentifier
* @return true if the object was added; false otherwise
*/
public boolean add(IDT id, T obj)
{
if (obj == null)
{
throw new IllegalArgumentException("Object to add may not be null");
}
if (getConstructingCachedSet(id).add(obj))
{
fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_ADDED);
return true;
}
else
{
return false;
}
}
/**
* Adds all of the objects in the given Collection to the list of objects
* stored in this AbstractListFacet for the Player Character represented by
* the given PCGenIdentifier
*
* @param id
* The PCGenIdentifier 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 AbstractListFacet for the Player Character
* represented by the given PCGenIdentifier
* @throws NullPointerException
* if the given Collection is null
*/
public void addAll(IDT id, Collection<T> c)
{
if (c.isEmpty())
{
return;
}
Collection<T> set = getConstructingCachedSet(id);
for (T obj : c)
{
if (obj == null)
{
throw new IllegalArgumentException(
"Object to add may not be null");
}
if (set.add(obj))
{
fireDataFacetChangeEvent(id, obj,
DataFacetChangeEvent.DATA_ADDED);
}
}
}
/**
* Removes the given object from the list of objects stored in this
* AbstractListFacet for the Player Character represented by the given
* PCGenIdentifier
*
* @param id
* The PCGenIdentifier representing the Player Character from
* which the given item should be removed
* @param obj
* The object to be removed from the list of objects stored in
* this AbstractListFacet for the Player Character represented by
* the given PCGenIdentifier
* @return true if an element was removed as a result of this call; false
* otherwise
*/
public boolean remove(IDT id, T obj)
{
if (obj == null)
{
throw new IllegalArgumentException("Object to add may not be null");
}
Collection<T> componentSet = getCachedSet(id);
if (componentSet != null)
{
if (componentSet.remove(obj))
{
fireDataFacetChangeEvent(id, obj,
DataFacetChangeEvent.DATA_REMOVED);
if (componentSet.isEmpty())
{
removeCache(id);
}
return true;
}
}
return false;
}
/**
* Removes all of the objects in the given Collection from the list of
* objects stored in this AbstractListFacet for the Player Character
* represented by the given PCGenIdentifier
*
* @param id
* The PCGenIdentifier 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 AbstractListFacet for the Player
* Character represented by the given PCGenIdentifier
* @throws NullPointerException
* if the given Collection is null
*/
public void removeAll(IDT id, Collection<T> c)
{
Collection<T> componentSet = getCachedSet(id);
if (componentSet != null)
{
for (T obj : c)
{
if (obj == null)
{
throw new IllegalArgumentException(
"Object to add may not be null");
}
if (componentSet.remove(obj))
{
fireDataFacetChangeEvent(id, obj,
DataFacetChangeEvent.DATA_REMOVED);
}
}
}
}
/**
* Removes all objects from the list of objects stored in this
* AbstractListFacet for the Player Character represented by the given
* PCGenIdentifier
*
* This method is value-semantic in that ownership of the returned
* Collection is transferred to the class calling this method. Since this is
* a remove all function, modification of the returned Collection will not
* modify this AbstractListFacet and modification of this AbstractListFacet
* 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 AbstractListFacet. If you wish to modify
* the information stored in this AbstractListFacet, you must use the add*()
* and remove*() methods of AbstractListFacet.
*
* @param id
* The PCGenIdentifier 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 AbstractListFacet for the Player Character represented by
* the given PCGenIdentifier
*/
public Collection<T> removeAll(IDT id)
{
Collection<T> componentSet = (Collection<T>) removeCache(id);
if (componentSet == null)
{
return Collections.emptySet();
}
for (T obj : componentSet)
{
fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_REMOVED);
}
return componentSet;
}
/**
* Returns a non-null copy of the Set of objects in this AbstractListFacet
* for the Player Character represented by the given PCGenIdentifier. This
* method returns an empty Set if no objects are in this AbstractListFacet
* for the Player Character identified by the given PCGenIdentifier.
*
* 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 AbstractListFacet and modification of
* this AbstractListFacet 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 AbstractListFacet. If you wish to
* modify the information stored in this AbstractListFacet, you must use the
* add*() and remove*() methods of AbstractListFacet.
*
* @param id
* The PCGenIdentifier representing the Player Character for
* which a copy of the items in this AbstractListFacet should be
* returned.
* @return A non-null Collection of objects in this AbstractListFacet for
* the Player Character represented by the given PCGenIdentifier
*/
public Collection<T> getSet(IDT id)
{
Collection<T> componentSet = getCachedSet(id);
if (componentSet == null)
{
return Collections.emptySet();
}
return Collections.unmodifiableList(new ArrayList<>(componentSet));
}
/**
* Returns the count of items in this AbstractListFacet for the Player
* Character represented by the given PCGenIdentifier
*
* @param id
* The PCGenIdentifier representing the Player Character for
* which the count of items should be returned
* @return The count of items in this AbstractListFacet for the Player
* Character represented by the given PCGenIdentifier
*/
public int getCount(IDT id)
{
Collection<T> componentSet = getCachedSet(id);
if (componentSet == null)
{
return 0;
}
return componentSet.size();
}
/**
* Returns true if this AbstractListFacet does not contain any items for the
* Player Character represented by the given PCGenIdentifier
*
* @param id
* The PCGenIdentifier representing the PlayerCharacter to test
* if any items are contained by this AbstractListFacet
* @return true if this AbstractListFacet does not contain any items for the
* Player Character represented by the given PCGenIdentifier; false
* otherwise (if it does contain items for the Player Character)
*/
public boolean isEmpty(IDT id)
{
Collection<T> componentSet = getCachedSet(id);
return componentSet == null || componentSet.isEmpty();
}
/**
* Returns true if this AbstractListFacet contains the given value in the
* list of items for the Player Character represented by the given
* PCGenIdentifier.
*
* @param id
* The PCGenIdentifier representing the Player Character used for
* testing
* @param obj
* The object to test if this AbstractListFacet contains that
* item for the Player Character represented by the given
* PCGenIdentifier
* @return true if this AbstractListFacet contains the given value for the
* Player Character represented by the given PCGenIdentifier; false
* otherwise
*/
public boolean contains(IDT id, T obj)
{
/*
* TODO obj == null? - log an error?
*/
Collection<T> componentSet = getCachedSet(id);
return componentSet != null && componentSet.contains(obj);
}
/**
* Returns the type-safe Set for this AbstractListFacet and the given
* PCGenIdentifier. May return null if no information has been set in this
* AbstractListFacet for the given PCGenIdentifier.
*
* Note that this method SHOULD NOT be public. The Set is owned by
* AbstractListFacet, and since it can be modified, a reference to that
* object should not be exposed to any object other than AbstractListFacet.
*
* @param id
* The PCGenIdentifier for which the Set should be returned
* @return The Set for the Player Character represented by the given
* PCGenIdentifier; null if no information has been set in this
* AbstractListFacet for the Player Character.
*/
protected Collection<T> getCachedSet(IDT id)
{
return (Collection<T>) getCache(id);
}
/**
* Returns a type-safe Set for this AbstractListFacet and the given
* PCGenIdentifier. Will return a new, empty Set if no information has been
* set in this AbstractListFacet for the given PCGenIdentifier. Will not
* return null.
*
* Note that this method SHOULD NOT be public. The Set object is owned by
* AbstractListFacet, and since it can be modified, a reference to that
* object should not be exposed to any object other than AbstractListFacet.
*
* @param id
* The PCGenIdentifier for which the Set should be returned
* @return The Set for the Player Character represented by the given
* PCGenIdentifier.
*/
private Collection<T> getConstructingCachedSet(IDT id)
{
Collection<T> componentSet = getCachedSet(id);
if (componentSet == null)
{
componentSet = getComponentSet();
setCache(id, componentSet);
}
return componentSet;
}
/**
* Returns a new (empty) Collection for this AbstractListFacet. Can be
* overridden by classes that extend AbstractListFacet if a Collection other
* than an IdentityHashSet is desired for storing the information in the
* AbstractListFacet.
*
* Note that this method SHOULD NOT be public. The Collection object is
* owned by AbstractListFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractListFacet.
*
* Note that this method should always be the only method used to construct
* a Collection for this AbstractListFacet. It is actually preferred to use
* getConstructingCacheSet(PCGenIdentifier) in order to implicitly call this
* method.
*
* @return A new (empty) Collection for use in this AbstractListFacet.
*/
protected Collection<T> getComponentSet()
{
return new LinkedHashSet<>();
}
/**
* Copies the contents of the AbstractListFacet from one Player Character to
* another Player Character, based on the given PCGenIdentifiers
* representing those Player Characters.
*
* This is a method in AbstractListFacet in order to avoid exposing the
* mutable Collection object to other classes. This should not be inlined,
* as the Collection is internal information to AbstractListFacet 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 PCGenIdentifiers
* (meaning once this copy takes place, any change to the AbstractListFacet
* of one Player Character will only impact the Player Character where the
* AbstractListFacet was changed).
*
* @param source
* The PCGenIdentifier representing the Player Character from
* which the information should be copied
* @param copy
* The PCGenIdentifier representing the Player Character to which
* the information should be copied
*/
@Override
public void copyContents(IDT source, IDT copy)
{
Collection<T> componentSet = getCachedSet(source);
if (componentSet != null)
{
getConstructingCachedSet(copy).addAll(
getCopyForNewOwner(componentSet));
}
}
/**
* Create a new copy of this list facet's data. This defaults to a return of
* the same set, but subclasses may need to do deep cloning of their objects
* as part of the copy. Note: The returned collection is directly saved,
* only its contents.
*
* @param componentSet
* The collection of data held by the facet for a character.
* @return The data ready to be saved to a new collection for a new
* character.
*/
protected Collection<T> getCopyForNewOwner(Collection<T> componentSet)
{
return componentSet;
}
/**
* Replaces an item in this AbstractListFacet with another object.
*
* NOTE: Use of this method is HIGHLY DISCOURAGED. Please consider another
* way of achieving the same results as this method. In other words, this
* method was required in order to maintain compatibility with the code in
* PCGen that tends to copy & clone things, but in the future, we are
* attempting to move away from that structure, so use of this method (which
* implies order dependency) is discouraged.
*
* This method is equivalent of a replaceAll in a String. In other words, if
* the underlying Collection stored in this AbstractListFacet is a List (not
* the Set used by default), then this method will replace ALL instances of
* an old object in the List, not just the first instance.
*
* @param id
* The PCGenIdentifier representing the Player Character from
* which data will be replaced
* @param old
* The old object to be replaced in the Collection for the Player
* Character represented by the given PCGenIdentifier
* @param replacement
* The replacement object to replace the given source object in
* the Collection for the Player Character represented by the
* given PCGenIdentifier
* @return true if the given old object was found in the Collection for the
* Player Character represented by the given PCGenIdentifier (and
* thus true if a replacement was successfully made); false
* otherwise
*/
public boolean replace(IDT id, T old, T replacement)
{
Collection<T> componentSet = getCachedSet(id);
if (componentSet == null || !componentSet.contains(old))
{
return false;
}
Collection<T> replaceSet = getComponentSet();
for (T obj : componentSet)
{
if (obj == old)
{
replaceSet.add(replacement);
}
else
{
replaceSet.add(obj);
}
}
setCache(id, componentSet);
fireDataFacetChangeEvent(id, old, DataFacetChangeEvent.DATA_REMOVED);
fireDataFacetChangeEvent(id, replacement,
DataFacetChangeEvent.DATA_ADDED);
return true;
}
/**
* This method will add the given added object within the underlying
* Collection of this AbstractListFacet directly after the given trigger
* object.
*
* If the underlying Collection for this AbstractListFacet is not an ordered
* Collection (e.g. HashSet), then this method is a MUCH slower way of
* calling add(PCGenIdentifier, T).
*
* NOTE: Use of this method is HIGHLY DISCOURAGED. Please consider another
* way of achieving the same results as this method. In other words, this
* method was required in order to maintain compatibility with the code in
* PCGen that tends to copy & clone things, but in the future, we are
* attempting to move away from that structure, so use of this method (which
* implies order dependency) is discouraged.
*
* @param id
* The PCGenIdentifier representing the Player Character from
* which data will be replaced
* @param trigger
* The trigger object to be used as the identifier to indicate
* which object precedes the location where the given added
* object is to be placed in the Collection for the Player
* Character represented by the given PCGenIdentifier
* @param added
* The object to be added to the Collection for the Player
* Character represented by the given PCGenIdentifier
*/
public void addAfter(IDT id, T trigger, T added)
{
Collection<T> componentSet = getCachedSet(id);
if (componentSet != null && componentSet.contains(trigger))
{
Collection<T> replaceSet = getComponentSet();
for (T obj : componentSet)
{
replaceSet.add(obj);
if (obj == trigger)
{
replaceSet.add(added);
}
}
setCache(id, componentSet);
fireDataFacetChangeEvent(id, added, DataFacetChangeEvent.DATA_ADDED);
}
}
}