/*
* 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.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import pcgen.base.util.GenericMapToList;
import pcgen.base.util.MapToList;
import pcgen.base.util.WrappedMapSet;
import pcgen.cdom.base.PCGenIdentifier;
import pcgen.cdom.facet.event.ScopeFacetChangeEvent;
import pcgen.cdom.facet.event.ScopeFacetChangeListener;
public class AbstractScopeFacet<IDT extends PCGenIdentifier, S, T> extends
AbstractStorageFacet<IDT>
{
private Map<S, Map<T, Set<Object>>> getConstructingInfo(IDT id)
{
Map<S, Map<T, Set<Object>>> map = getInfo(id);
if (map == null)
{
map = new IdentityHashMap<>();
setCache(id, map);
}
return map;
}
private Map<S, Map<T, Set<Object>>> getInfo(IDT id)
{
return (Map<S, Map<T, Set<Object>>>) getCache(id);
}
public void add(IDT id, S scope, T obj, Object source)
{
if (scope == null)
{
throw new IllegalArgumentException("Scope cannot be null");
}
if (obj == null)
{
throw new IllegalArgumentException("Object cannot be null");
}
Map<S, Map<T, Set<Object>>> map = getConstructingInfo(id);
Map<T, Set<Object>> scopeMap = map.get(scope);
if (scopeMap == null)
{
scopeMap = new IdentityHashMap<>();
map.put(scope, scopeMap);
}
Set<Object> sources = scopeMap.get(obj);
boolean isNew = (sources == null);
if (isNew)
{
sources = new WrappedMapSet<>(IdentityHashMap.class);
scopeMap.put(obj, sources);
}
sources.add(source);
if (isNew)
{
fireScopeFacetChangeEvent(id, scope, obj,
ScopeFacetChangeEvent.DATA_ADDED);
}
}
public void addAll(IDT id, S scope, Collection<T> coll, Object source)
{
if (scope == null)
{
throw new IllegalArgumentException("Scope cannot be null");
}
if (coll == null)
{
throw new IllegalArgumentException("Collection cannot be null");
}
Map<S, Map<T, Set<Object>>> map = getConstructingInfo(id);
Map<T, Set<Object>> scopeMap = map.get(scope);
if (scopeMap == null)
{
scopeMap = new IdentityHashMap<>();
map.put(scope, scopeMap);
}
for (T obj : coll)
{
Set<Object> sources = scopeMap.get(obj);
boolean isNew = (sources == null);
if (isNew)
{
sources = new WrappedMapSet<>(IdentityHashMap.class);
scopeMap.put(obj, sources);
}
sources.add(source);
if (isNew)
{
fireScopeFacetChangeEvent(id, scope, obj,
ScopeFacetChangeEvent.DATA_ADDED);
}
}
}
public void remove(IDT id, S scope, T obj, Object source)
{
if (scope == null)
{
throw new IllegalArgumentException("Scope cannot be null");
}
if (obj == null)
{
throw new IllegalArgumentException("Object cannot be null");
}
Map<S, Map<T, Set<Object>>> map = getInfo(id);
if (map == null)
{
return;
}
Map<T, Set<Object>> scopeMap = map.get(scope);
if (scopeMap == null)
{
return;
}
Set<Object> sources = scopeMap.get(obj);
if (sources == null)
{
return;
}
if (sources.remove(source) && sources.isEmpty())
{
fireScopeFacetChangeEvent(id, scope, obj,
ScopeFacetChangeEvent.DATA_REMOVED);
scopeMap.remove(obj);
}
if (scopeMap.isEmpty())
{
map.remove(scope);
}
if (map.isEmpty())
{
removeCache(id);
}
}
public Collection<T> getSet(IDT id, S scope)
{
Map<S, Map<T, Set<Object>>> map = getInfo(id);
if (map == null)
{
return Collections.emptyList();
}
Map<T, Set<Object>> scopeMap = map.get(scope);
if (scopeMap == null)
{
return Collections.emptyList();
}
return new ArrayList<>(scopeMap.keySet());
}
public Collection<S> getScopes(IDT id)
{
Map<S, Map<T, Set<Object>>> map = getInfo(id);
if (map == null)
{
return Collections.emptyList();
}
return new ArrayList<>(map.keySet());
}
public boolean contains(IDT id, S scope, T obj)
{
Map<S, Map<T, Set<Object>>> map = getInfo(id);
if (map == null)
{
return false;
}
Map<T, Set<Object>> scopeMap = map.get(scope);
return (scopeMap != null) && scopeMap.containsKey(obj);
}
public void removeAllFromSource(IDT id, Object source)
{
Map<S, Map<T, Set<Object>>> map = getInfo(id);
/*
* This list exists primarily to eliminate the possibility of a
* concurrent modification exception on a recursive remove
*/
MapToList<S, T> removed = GenericMapToList.getMapToList(IdentityHashMap.class);
if (map != null)
{
for (Iterator<Map.Entry<S, Map<T, Set<Object>>>> it =
map.entrySet().iterator(); it.hasNext();)
{
Entry<S, Map<T, Set<Object>>> entry = it.next();
S scope = entry.getKey();
Map<T, Set<Object>> scopeMap = entry.getValue();
for (Iterator<Map.Entry<T, Set<Object>>> lmit =
scopeMap.entrySet().iterator(); lmit.hasNext();)
{
Entry<T, Set<Object>> lme = lmit.next();
Set<Object> sources = lme.getValue();
if (sources.remove(source) && sources.isEmpty())
{
T obj = lme.getKey();
lmit.remove();
removed.addToListFor(scope, obj);
}
}
if (scopeMap.isEmpty())
{
it.remove();
}
}
if (map.isEmpty())
{
removeCache(id);
}
for (S scope : removed.getKeySet())
{
for (T obj : removed.getListFor(scope))
{
fireScopeFacetChangeEvent(id, scope, obj,
ScopeFacetChangeEvent.DATA_REMOVED);
}
}
}
}
/**
* Copies the contents of the AbstractScopeFacet from one resource to
* another resource, based on the given IDTs representing those resources.
*
* This is a method in AbstractScopeFacet in order to avoid exposing the
* mutable Map object to other classes. This should not be inlined, as the
* Map is internal information to AbstractScopeFacet 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 IDTs (meaning once this
* copy takes place, any change to the AbstractScopeFacet of one resource
* will only impact the resource where the AbstractScopeFacet was changed).
*
* @param source
* The IDT representing the resource from which the information
* should be copied
* @param copy
* The IDT representing the resource to which the information
* should be copied
*/
@Override
public void copyContents(IDT source, IDT copy)
{
Map<S, Map<T, Set<Object>>> map = getInfo(source);
if (map != null)
{
for (Entry<S, Map<T, Set<Object>>> lme : map.entrySet())
{
S scope = lme.getKey();
for (Entry<T, Set<Object>> ome : lme.getValue().entrySet())
{
T sp = ome.getKey();
for (Object spsource : ome.getValue())
{
add(copy, scope, sp, spsource);
}
}
}
}
}
private final Map<Integer, ScopeFacetChangeListener<? super IDT, ? super S, ? super T>[]> listeners =
new TreeMap<>();
/**
* Adds a new ScopeFacetChangeListener to receive ScopeFacetChangeEvents
* (EdgeChangeEvent and NodeChangeEvent) from this AbstractScopeFacet. The
* given ScopeFacetChangeListener is added at the default priority (zero).
*
* Note that the ScopeFacetChangeListeners are a list, meaning a given
* ScopeFacetChangeListener can be added more than once at a given priority,
* and if that occurs, it must be removed an equivalent number of times in
* order to no longer receive events from this AbstractScopeFacet.
*
* @param listener
* The ScopeFacetChangeListener to receive ScopeFacetChangeEvents
* from this AbstractScopeFacet
*/
public void addScopeFacetChangeListener(
ScopeFacetChangeListener<? super IDT, ? super S, ? super T> listener)
{
addScopeFacetChangeListener(0, listener);
}
/**
* Adds a new ScopeFacetChangeListener to receive ScopeFacetChangeEvents
* (EdgeChangeEvent and NodeChangeEvent) from this AbstractScopeFacet.
*
* The ScopeFacetChangeListener is added at the given priority.
*
* Note that the ScopeFacetChangeListeners are a list, meaning a given
* ScopeFacetChangeListener can be added more than once at a given priority,
* and if that occurs, it must be removed an equivalent number of times in
* order to no longer receive events from this AbstractScopeFacet.
*
* @param listener
* The ScopeFacetChangeListener to receive ScopeFacetChangeEvents
* from this AbstractScopeFacet
*/
public void addScopeFacetChangeListener(int priority,
ScopeFacetChangeListener<? super IDT, ? super S, ? super T> listener)
{
ScopeFacetChangeListener<? super IDT, ? super S, ? super T>[] dfcl =
listeners.get(priority);
int newSize = (dfcl == null) ? 1 : (dfcl.length + 1);
ScopeFacetChangeListener<? super IDT, ? super S, ? super T>[] newArray =
new ScopeFacetChangeListener[newSize];
if (dfcl != null)
{
System.arraycopy(dfcl, 0, newArray, 1, dfcl.length);
}
newArray[0] = listener;
listeners.put(priority, newArray);
}
/**
* Removes a ScopeFacetChangeListener so that it will no longer receive
* ScopeFacetChangeEvents from this AbstractScopeFacet. This will remove the
* data facet change listener from the default priority (zero).
*
* Note that if the given ScopeFacetChangeListener has been registered under
* a different priority, it will still receive events at that priority
* level.
*
* @param listener
* The ScopeFacetChangeListener to be removed
*/
public void removeScopeFacetChangeListener(
ScopeFacetChangeListener<? super IDT, ? super S, ? super T> listener)
{
removeScopeFacetChangeListener(0, listener);
}
/**
* Removes a ScopeFacetChangeListener so that it will no longer receive
* ScopeFacetChangeEvents from the source DataFacet. This will remove the
* data facet change listener from the given priority.
*
* Note that if the given ScopeFacetChangeListener has been registered under
* a different priority, it will still receive events at that priority
* level.
*
* @param listener
* The ScopeFacetChangeListener to be removed
*/
public void removeScopeFacetChangeListener(int priority,
ScopeFacetChangeListener<? super IDT, ? super S, ? super T> listener)
{
ScopeFacetChangeListener<? super IDT, ? super S, ? super T>[] dfcl =
listeners.get(priority);
if (dfcl == null)
{
// No worries
return;
}
int foundLoc = -1;
int newSize = dfcl.length - 1;
for (int i = newSize; i >= 0; i--)
{
if (dfcl[i] == listener)
{
foundLoc = i;
break;
}
}
if (foundLoc != -1)
{
if (dfcl.length == 1)
{
listeners.remove(priority);
}
else
{
ScopeFacetChangeListener<? super IDT, ? super S, ? super T>[] newArray =
new ScopeFacetChangeListener[newSize];
if (foundLoc != 0)
{
System.arraycopy(dfcl, 0, newArray, 0, foundLoc);
}
if (foundLoc != newSize)
{
System.arraycopy(dfcl, foundLoc + 1, newArray, foundLoc,
newSize - foundLoc);
}
listeners.put(priority, newArray);
}
}
}
/**
* Sends a NodeChangeEvent to the ScopeFacetChangeListeners that are
* receiving ScopeFacetChangeEvents from this AbstractScopeFacet.
*
* @param id
* The PCGenIdentifier identifying the resource to which the
* NodeChangeEvent relates
* @param scope
* The Scope through which this facet's contents are viewed
* @param node
* The Node that has been added to or removed from this
* AbstractScopeFacet for the given PCGenIdentifier
* @param type
* An identifier indicating whether the given CDOMObject was
* added to or removed from this AbstractScopeFacet
*/
@SuppressWarnings("rawtypes")
protected void fireScopeFacetChangeEvent(IDT id, S scope, T node, int type)
{
for (ScopeFacetChangeListener<? super IDT, ? super S, ? super T>[] dfclArray : listeners
.values())
{
/*
* This list is decremented from the end of the list to the
* beginning in order to maintain consistent operation with how Java
* AWT and Swing listeners are notified of Events. This is obviously
* subordinate to the priority (loop above).
*/
ScopeFacetChangeEvent<IDT, S, T> ccEvent = null;
for (int i = dfclArray.length - 1; i >= 0; i--)
{
// Lazily create event
if (ccEvent == null)
{
ccEvent =
new ScopeFacetChangeEvent<>(id, scope,
node, this, type);
}
ScopeFacetChangeListener dfcl = dfclArray[i];
switch (ccEvent.getEventType())
{
case ScopeFacetChangeEvent.DATA_ADDED:
dfcl.dataAdded(ccEvent);
break;
case ScopeFacetChangeEvent.DATA_REMOVED:
dfcl.dataRemoved(ccEvent);
break;
default:
break;
}
}
}
}
}