/*
* Copyright (c) Thomas Parker, 2009-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.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.base.util.WrappedMapSet;
import pcgen.cdom.base.CDOMObject;
import pcgen.cdom.base.PCGenIdentifier;
import pcgen.cdom.facet.event.DataFacetChangeEvent;
/**
* An AbstractSourcedListFacet is a DataFacet that contains information about
* Objects that are contained in a resource when a resource may have more than
* one of that type of Object (e.g. Language, PCTemplate) and the source of that
* object should be tracked.
*
* This class is designed to assume that each Object may only be contained one
* time by the resource, even if received from multiple sources. The 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 AbstractSourcedListFacet 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.
*
* null is a valid source.
*
* @param <IDT>
* The Type of identifier used in this AbstractSourcedListFacet
* @param <T>
* The Type of object stored in this AbstractSourcedListFacet
*/
public abstract class AbstractSourcedListFacet<IDT extends PCGenIdentifier, T>
extends AbstractDataFacet<IDT, T>
{
/**
* Add the given object with the given source to the list of objects stored
* in this AbstractSourcedListFacet for the resource represented by the
* given PCGenIdentifier
*
* @param id
* The PCGenIdentifier representing the resource for which the
* given item should be added
* @param obj
* The object to be added to the list of objects stored in this
* AbstractSourcedListFacet for the resource represented by the
* given PCGenIdentifier
* @param source
* The source for the given object
*/
public void add(IDT 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 with the given source in the given Collection to
* the list of objects stored in this AbstractSourcedListFacet for the
* resource represented by the given PCGenIdentifier
*
* @param id
* The PCGenIdentifier representing the resource 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 AbstractSourcedListFacet for the resource
* represented by the given PCGenIdentifier
* @param source
* The source for the given object
* @throws NullPointerException
* if the given Collection is null
*/
public void addAll(IDT 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 AbstractSourcedListFacet for the resource
* represented by the given PCGenIdentifier. 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 AbstractSourcedListFacet for the resource
* represented by the given PCGenIdentifier.
*
* @param id
* The PCGenIdentifier representing the resource 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 boolean remove(IDT id, T obj, Object source)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
return (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 resource represented by the given
* PCGenIdentifier. 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 AbstractSourcedListFacet for the resource
* represented by the given PCGenIdentifier.
*
* @param id
* The PCGenIdentifier representing the resource 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 AbstractSourcedListFacet for the
* resource represented by the given PCGenIdentifier
* @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(IDT 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 AbstractSourcedListFacet for the resource
* represented by the given PCGenIdentifier
*
* 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
* AbstractSourcedListFacet and modification of this
* AbstractSourcedListFacet 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 AbstractSourcedListFacet. If you
* wish to modify the information stored in this AbstractSourcedListFacet,
* you must use the add*() and remove*() methods of
* AbstractSourcedListFacet.
*
* @param id
* The PCGenIdentifier representing the resource from which all
* items should be removed
* @return A non-null Set of objects removed from the list of objects stored
* in this AbstractSourcedListFacet for the resource represented by
* the given PCGenIdentifier
*/
public Map<T, Set<Object>> removeAll(IDT 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
* AbstractSourcedListFacet for the resource represented by the given
* PCGenIdentifier. This method returns an empty set if no objects are in
* this AbstractSourcedListFacet for the resource identified by the given
* PCGenIdentifier.
*
* This method is value-semantic in that ownership of the returned Set is
* transferred to the class calling this method. Modification of the
* returned Set will not modify this AbstractSourcedListFacet and
* modification of this AbstractSourcedListFacet will not modify the
* returned Set. Modifications to the returned Set will also not modify any
* future or previous objects returned by this (or other) methods on
* AbstractSourcedListFacet. If you wish to modify the information stored in
* this AbstractSourcedListFacet, you must use the add*() and remove*()
* methods of AbstractSourcedListFacet.
*
* @param id
* The PCGenIdentifier representing the resource for which the
* items in this AbstractSourcedListFacet should be returned.
* @return A non-null copy of the Set of objects in this
* AbstractSourcedListFacet for the resource represented by the
* given PCGenIdentifier
*/
public Set<T> getSet(IDT id)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap == null)
{
return Collections.emptySet();
}
return Collections
.unmodifiableSet(new ListSet<>(componentMap.keySet()));
}
/**
* Returns the count of items in this AbstractSourcedListFacet for the
* resource represented by the given PCGenIdentifier
*
* @param id
* The PCGenIdentifier representing the resource for which the
* count of items should be returned
* @return The count of items in this AbstractSourcedListFacet for the
* resource represented by the given PCGenIdentifier
*/
public int getCount(IDT id)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap == null)
{
return 0;
}
return componentMap.size();
}
/**
* Returns true if this AbstractSourcedListFacet does not contain any items
* for the resource represented by the given PCGenIdentifier
*
* @param id
* The PCGenIdentifier representing the resource to test if any
* items are contained by this AbstractsSourcedListFacet
* @return true if this AbstractSourcedListFacet does not contain any items
* for the resource represented by the given PCGenIdentifier; false
* otherwise (if it does contain items for the resource)
*/
public boolean isEmpty(IDT id)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
return componentMap == null || componentMap.isEmpty();
}
/**
* Returns true if this AbstractSourcedListFacet contains the given value in
* the list of items for the resource represented by the given
* PCGenIdentifier.
*
* @param id
* The PCGenIdentifier representing the resource used for testing
* @param obj
* The object to test if this AbstractSourcedListFacet contains
* that item for the resource represented by the given
* PCGenIdentifier
* @return true if this AbstractSourcedListFacet contains the given value
* for the resource represented by the given PCGenIdentifier; false
* otherwise
*/
public boolean contains(IDT 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 AbstractSourcedListFacet, the resource
* represented by the given PCGenIdentifier, and the given object. Will add
* the given object to the list of items for the resource represented by the
* given PCGenIdentifier and will return a new, empty Set if no information
* has been set in this AbstractSourcedListFacet for the given
* PCGenIdentifier and given object. Will not return null.
*
* Note that this method SHOULD NOT be public. The Set object is owned by
* AbstractSourcedListFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractSourcedListFacet.
*
* @param id
* The PCGenIdentifier 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 resource represented
* by the given PCGenIdentifier.
*/
private Set<Object> getConstructingCachedSetFor(IDT 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 AbstractSourcedListFacet and the given
* PCGenIdentifier. May return null if no information has been set in this
* AbstractSourcedListFacet for the given PCGenIdentifier.
*
* Note that this method SHOULD NOT be public. The Map is owned by
* AbstractSourcedListFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractSourcedListFacet.
*
* @param id
* The PCGenIdentifier for which the Set should be returned
* @return The Map for the resource represented by the given
* PCGenIdentifier; null if no information has been set in this
* AbstractSourcedListFacet for the resource.
*/
protected Map<T, Set<Object>> getCachedMap(IDT id)
{
return (Map<T, Set<Object>>) getCache(id);
}
/**
* Returns the type-safe Map for this AbstractSourcedListFacet and the given
* PCGenIdentifier. Will return a new, empty Map if no information has been
* set in this AbstractSourcedListFacet for the given PCGenIdentifier. Will
* not return null.
*
* Note that this method SHOULD NOT be public. The Map object is owned by
* AbstractSourcedListFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractSourcedListFacet.
*
* @param id
* The PCGenIdentifier for which the Map should be returned
* @return The Map for the resource represented by the given
* PCGenIdentifier.
*/
private Map<T, Set<Object>> getConstructingCachedMap(IDT 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 AbstractSourcedListFacet. Can be
* overridden by classes that extend AbstractSourcedListFacet if a Map other
* than an IdentityHashMap is desired for storing the information in the
* AbstractSourcedListFacet.
*
* Note that this method SHOULD NOT be public. The Map object is owned by
* AbstractSourcedListFacet, and since it can be modified, a reference to
* that object should not be exposed to any object other than
* AbstractSourcedListFacet.
*
* Note that this method should always be the only method used to construct
* a Map for this AbstractSourcedListFacet. It is actually preferred to use
* getConstructingCacheMap(PCGenIdentifier) in order to implicitly call this
* method.
*
* @return A new (empty) Map for use in this AbstractSourcedListFacet.
*/
protected Map<T, Set<Object>> getComponentMap()
{
return new IdentityHashMap<>();
}
/**
* Copies the contents of the AbstractSourcedListFacet from one resource to
* another resource, based on the given PCGenIdentifiers representing those
* resources.
*
* This is a method in AbstractSourcedListFacet in order to avoid exposing
* the mutable Map object to other classes. This should not be inlined, as
* the Map is internal information to AbstractSourcedListFacet and should
* not be exposed to other classes.
*
* Note also the copy is a one-time event and no references are maintained
* between the resources represented by the given PCGenIdentifiers (meaning
* once this copy takes place, any change to the AbstractSourcedListFacet of
* one resource will only impact the resource where the
* AbstractSourcedListFacet was changed).
*
* @param source
* The PCGenIdentifier representing the resource from which the
* information should be copied
* @param destination
* The PCGenIdentifier representing the resource to which the
* information should be copied
*/
@Override
public void copyContents(IDT source, IDT 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 AbstractSourcedListFacet. 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 resource represented by the given
* PCGenIdentifier.
*
* @param id
* The PCGenIdentifier representing the resource which may have
* the given item removed.
* @param componentMap
* The (private) Map for this AbstractSourcedListFacet 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 resource represented by the
* given PCGenIdentifier
* @param source
* The source for the given object to be removed from the list of
* sources for that object
*/
private boolean processRemoval(IDT 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)
{
return false;
}
boolean returnVal = set.remove(source);
if (set.isEmpty())
{
componentMap.remove(obj);
fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_REMOVED);
if (componentMap.isEmpty())
{
removeCache(id);
}
}
return returnVal;
}
/**
* Removes all information for the given source from this
* AbstractSourcedListFacet for the resource represented by the given
* PCGenIdentifier.
*
* @param id
* The PCGenIdentifier representing the resource 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 resource identified by the given
* PCGenIdentifier
*/
public void removeAll(IDT 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
* AbstractSourcedListFacet for the resource represented by the given
* PCGenIdentifier and the given source. This method returns an empty set if
* no objects are in this AbstractSourcedListFacet for the resource
* identified by the given PCGenIdentifier 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 AbstractSourcedListFacet and
* modification of this AbstractSourcedListFacet 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
* AbstractSourcedListFacet. If you wish to modify the information stored in
* this AbstractSourcedListFacet, you must use the add*() and remove*()
* methods of AbstractSourcedListFacet.
*
* @param id
* The PCGenIdentifier representing the resource for which the
* items in this AbstractSourcedListFacet should be returned.
* @param owner
* The source object for which a copy of the List of objects in
* this AbstractSourcedListFacet should be returned.
* @return A non-null Set of objects in this AbstractSourcedListFacet for
* the resource represented by the given PCGenIdentifier
*/
public List<? extends T> getSet(IDT 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 Collections.unmodifiableList(list);
}
/**
* Returns true if this AbstractSourcedListFacet contains any item from the
* given source in the list of items for the resource represented by the
* given PCGenIdentifier.
*
* @param id
* The PCGenIdentifier representing the resource used for testing
* @param owner
* The source object for which must have granted an object in
* this AbstractSourcedListFacet for the resource identified by
* the given PCGenIdentifier
* @return true if this AbstractSourcedListFacet contains any item from the
* given source for the resource represented by the given
* PCGenIdentifier; false otherwise
*/
public boolean containsFrom(IDT id, Object owner)
{
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))
{
return true;
}
}
}
return false;
}
/**
* Returns the count of items granted by the given source in this
* AbstractSourcedListFacet for the resource represented by the given
* PCGenIdentifier.
*
* @param id
* The PCGenIdentifier representing the resource for which the
* count of items should be returned
* @param owner
* The source object used to determine the count of objects in
* this AbstractSourcedListFacet for the resource identified by
* the given PCGenIdentifier
* @return The count of items granted by the given source in this
* AbstractSourcedListFacet for the resource represented by the
* given PCGenIdentifier
*/
public int getCountFrom(IDT id, CDOMObject owner)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
int count = 0;
if (componentMap != null)
{
for (Entry<T, Set<Object>> me : componentMap.entrySet())
{
Set<Object> set = me.getValue();
if (set.contains(owner))
{
count++;
}
}
}
return count;
}
/**
* Returns true if this AbstractSourcedListFacet contains the given value
* (granted by the given source) in the list of items for the resource
* represented by the given PCGenIdentifier.
*
* @param id
* The PCGenIdentifier representing the resource used for testing
* @param owner
* The source object for which must have granted the object being
* tested to see if it is contained by this
* AbstractSourcedListFacet for the resource identified by the
* given PCGenIdentifier
* @param obj
* The object to test if this AbstractSourcedListFacet contains
* that item for the resource represented by the given
* PCGenIdentifier
* @return true if this AbstractSourcedListFacet contains the given value
* (granted by the given source) for the resource represented by the
* given PCGenIdentifier; false otherwise
*/
public boolean containsFrom(IDT id, T obj, CDOMObject owner)
{
Map<T, Set<Object>> componentMap = getCachedMap(id);
if (componentMap != null)
{
Set<Object> sources = componentMap.get(obj);
return sources != null && sources.contains(owner);
}
return false;
}
public Collection<Object> getSources(IDT 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();
}
}