/* * 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.WrappedMapSet; import pcgen.cdom.enumeration.CharID; import pcgen.cdom.facet.event.SubScopeFacetChangeEvent; import pcgen.cdom.facet.event.SubScopeFacetChangeListener; public class AbstractSubScopeFacet<S1, S2, T> extends AbstractStorageFacet<CharID> { private Map<S1, Map<S2, Map<T, Set<Object>>>> getConstructingInfo(CharID id) { Map<S1, Map<S2, Map<T, Set<Object>>>> map = getInfo(id); if (map == null) { map = new IdentityHashMap<>(); setCache(id, map); } return map; } private Map<S1, Map<S2, Map<T, Set<Object>>>> getInfo(CharID id) { return (Map<S1, Map<S2, Map<T, Set<Object>>>>) getCache(id); } public void add(CharID id, S1 scope1, S2 scope2, T obj, Object source) { if (scope1 == null) { throw new IllegalArgumentException("Scope 1 cannot be null"); } if (scope2 == null) { throw new IllegalArgumentException("Scope 2 cannot be null"); } if(obj == null) { throw new IllegalArgumentException("Object cannot be null"); } Map<S1, Map<S2, Map<T, Set<Object>>>> map = getConstructingInfo(id); Map<S2, Map<T, Set<Object>>> scope1Map = map.get(scope1); if (scope1Map == null) { scope1Map = new IdentityHashMap<>(); map.put(scope1, scope1Map); } Map<T, Set<Object>> scope2Map = scope1Map.get(scope2); if (scope2Map == null) { scope2Map = new IdentityHashMap<>(); scope1Map.put(scope2, scope2Map); } Set<Object> sources = scope2Map.get(obj); boolean isNew = (sources == null); if (isNew) { sources = new WrappedMapSet<>(IdentityHashMap.class); scope2Map.put(obj, sources); } sources.add(source); if (isNew) { fireSubScopeFacetChangeEvent(id, scope1, scope2, obj, SubScopeFacetChangeEvent.DATA_ADDED); } } public void remove(CharID id, S1 scope1, S2 scope2, T obj, Object source) { if (scope1 == null) { throw new IllegalArgumentException("Scope 1 cannot be null"); } if (scope2 == null) { throw new IllegalArgumentException("Scope 2 cannot be null"); } if(obj == null) { throw new IllegalArgumentException("Object cannot be null"); } Map<S1, Map<S2, Map<T, Set<Object>>>> map = getInfo(id); if (map == null) { return; } Map<S2, Map<T, Set<Object>>> scope1Map = map.get(scope1); if (scope1Map == null) { return; } Map<T, Set<Object>> scope2Map = scope1Map.get(scope2); if (scope2Map == null) { return; } Set<Object> sources = scope2Map.get(obj); if (sources == null) { return; } if (sources.remove(source) && sources.isEmpty()) { fireSubScopeFacetChangeEvent(id, scope1, scope2, obj, SubScopeFacetChangeEvent.DATA_REMOVED); scope2Map.remove(obj); } if (scope2Map.isEmpty()) { scope1Map.remove(scope2); } if (scope1Map.isEmpty()) { map.remove(scope1); } if (map.isEmpty()) { removeCache(id); } } public Collection<T> getSet(CharID id, S1 scope1, S2 scope2) { if (scope1 == null) { throw new IllegalArgumentException("Scope 1 cannot be null"); } if (scope2 == null) { throw new IllegalArgumentException("Scope 2 cannot be null"); } Map<S1, Map<S2, Map<T, Set<Object>>>> map = getInfo(id); if (map == null) { return Collections.emptyList(); } Map<S2, Map<T, Set<Object>>> scope1Map = map.get(scope1); if (scope1Map == null) { return Collections.emptyList(); } Map<T, Set<Object>> scope2Map = scope1Map.get(scope2); if (scope2Map == null) { return Collections.emptyList(); } return new ArrayList<>(scope2Map.keySet()); } public int getSize(CharID id, S1 scope1, S2 scope2) { if (scope1 == null) { throw new IllegalArgumentException("Scope 1 cannot be null"); } if (scope2 == null) { throw new IllegalArgumentException("Scope 2 cannot be null"); } Map<S1, Map<S2, Map<T, Set<Object>>>> map = getInfo(id); if (map == null) { return 0; } Map<S2, Map<T, Set<Object>>> scope1Map = map.get(scope1); if (scope1Map == null) { return 0; } Map<T, Set<Object>> scope2Map = scope1Map.get(scope2); if (scope2Map == null) { return 0; } return scope2Map.size(); } public boolean contains(CharID id, S1 scope1, S2 scope2, T obj) { if (scope1 == null) { throw new IllegalArgumentException("Scope 1 cannot be null"); } if (scope2 == null) { throw new IllegalArgumentException("Scope 2 cannot be null"); } Map<S1, Map<S2, Map<T, Set<Object>>>> map = getInfo(id); if (map == null) { return false; } Map<S2, Map<T, Set<Object>>> scope1Map = map.get(scope1); if (scope1Map == null) { return false; } Map<T, Set<Object>> scope2Map = scope1Map.get(scope2); return (scope2Map != null) && scope2Map.containsKey(obj); } public Collection<S1> getScopes1(CharID id) { Map<S1, Map<S2, Map<T, Set<Object>>>> map = getInfo(id); if (map == null) { return Collections.emptyList(); } return new ArrayList<>(map.keySet()); } public Collection<S2> getScopes2(CharID id, S1 scope1) { Map<S1, Map<S2, Map<T, Set<Object>>>> map = getInfo(id); if (map == null) { return Collections.emptyList(); } Map<S2, Map<T, Set<Object>>> submap = map.get(scope1); if (submap == null) { return Collections.emptyList(); } return new ArrayList<>(submap.keySet()); } public void removeAllFromSource(CharID id, Object source) { Map<S1, Map<S2, Map<T, Set<Object>>>> map = getInfo(id); if (map != null) { for (Iterator<Entry<S1, Map<S2, Map<T, Set<Object>>>>> s1it = map.entrySet().iterator(); s1it.hasNext();) { Entry<S1, Map<S2, Map<T, Set<Object>>>> s1entry = s1it.next(); S1 scope1 = s1entry.getKey(); Map<S2, Map<T, Set<Object>>> scope1Map = s1entry.getValue(); for (Iterator<Entry<S2, Map<T, Set<Object>>>> s2it = scope1Map.entrySet().iterator(); s2it.hasNext();) { Entry<S2, Map<T, Set<Object>>> s2entry = s2it.next(); S2 scope2 = s2entry.getKey(); Map<T, Set<Object>> scope2Map = s2entry.getValue(); for (Iterator<Map.Entry<T, Set<Object>>> lmit = scope2Map.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(); fireSubScopeFacetChangeEvent(id, scope1, scope2, obj, SubScopeFacetChangeEvent.DATA_REMOVED); } } if (scope2Map.isEmpty()) { s2it.remove(); } } if (scope1Map.isEmpty()) { s1it.remove(); } } if (map.isEmpty()) { removeCache(id); } } } /** * Copies the contents of the AbstractScopeFacet from one Player Character * to another Player Character, based on the given CharIDs representing * those Player Characters. * * 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 Player Characters represented by the given CharIDs (meaning * once this copy takes place, any change to the AbstractScopeFacet of one * Player Character will only impact the Player Character where the * AbstractScopeFacet was changed). * * @param source * The CharID representing the Player Character from which the * information should be copied * @param copy * The CharID representing the Player Character to which the * information should be copied */ @Override public void copyContents(CharID source, CharID copy) { Map<S1, Map<S2, Map<T, Set<Object>>>> map = getInfo(source); if (map != null) { for (Entry<S1, Map<S2, Map<T, Set<Object>>>> l1me : map.entrySet()) { S1 scope1 = l1me.getKey(); for (Entry<S2, Map<T, Set<Object>>> l2me : l1me.getValue() .entrySet()) { S2 scope2 = l2me.getKey(); for (Entry<T, Set<Object>> ome : l2me.getValue().entrySet()) { T sp = ome.getKey(); for (Object spsource : ome.getValue()) { add(copy, scope1, scope2, sp, spsource); } } } } } } private final Map<Integer, SubScopeFacetChangeListener<? super S1, ? super S2, ? super T>[]> listeners = new TreeMap<>(); /** * Adds a new ScopeFacetChangeListener to receive TwoScopeFacetChangeEvents * (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 * TwoScopeFacetChangeEvents from this AbstractScopeFacet */ public void addSubScopeFacetChangeListener( SubScopeFacetChangeListener<? super S1, ? super S2, ? super T> listener) { addSubScopeFacetChangeListener(0, listener); } /** * Adds a new ScopeFacetChangeListener to receive TwoScopeFacetChangeEvents * (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 * TwoScopeFacetChangeEvents from this AbstractScopeFacet */ public void addSubScopeFacetChangeListener(int priority, SubScopeFacetChangeListener<? super S1, ? super S2, ? super T> listener) { SubScopeFacetChangeListener<? super S1, ? super S2, ? super T>[] dfcl = listeners.get(priority); int newSize = (dfcl == null) ? 1 : (dfcl.length + 1); SubScopeFacetChangeListener<? super S1, ? super S2, ? super T>[] newArray = new SubScopeFacetChangeListener[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 * TwoScopeFacetChangeEvents 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 removeSubScopeFacetChangeListener( SubScopeFacetChangeListener<? super S1, ? super S2, ? super T> listener) { removeSubScopeFacetChangeListener(0, listener); } /** * Removes a ScopeFacetChangeListener so that it will no longer receive * TwoScopeFacetChangeEvents 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 removeSubScopeFacetChangeListener(int priority, SubScopeFacetChangeListener<? super S1, ? super S2, ? super T> listener) { SubScopeFacetChangeListener<? super S1, ? super S2, ? 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 { SubScopeFacetChangeListener<? super S1, ? super S2, ? super T>[] newArray = new SubScopeFacetChangeListener[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 TwoScopeFacetChangeEvents from this AbstractScopeFacet. * * @param id * The CharID identifying the Player Character to which the * NodeChangeEvent relates. * @param scope1 * A Scope through which this facet's contents are viewed. * @param scope2 * Another Scope passed on to the listener. * @param node * The Node that has been added to or removed from this * AbstractScopeFacet for the given CharID. * @param type * An identifier indicating whether the given CDOMObject was * added to or removed from this AbstractScopeFacet. */ @SuppressWarnings("rawtypes") protected void fireSubScopeFacetChangeEvent(CharID id, S1 scope1, S2 scope2, T node, int type) { for (SubScopeFacetChangeListener<? super S1, ? super S2, ? 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). */ SubScopeFacetChangeEvent<S1, S2, T> ccEvent = null; for (int i = dfclArray.length - 1; i >= 0; i--) { // Lazily create event if (ccEvent == null) { ccEvent = new SubScopeFacetChangeEvent<>(id, scope1, scope2, node, this, type); } SubScopeFacetChangeListener dfcl = dfclArray[i]; switch (ccEvent.getEventType()) { case SubScopeFacetChangeEvent.DATA_ADDED: dfcl.dataAdded(ccEvent); break; case SubScopeFacetChangeEvent.DATA_REMOVED: dfcl.dataRemoved(ccEvent); break; default: break; } } } } public boolean containsFor(CharID id, S1 scope1) { Map<S1, Map<S2, Map<T, Set<Object>>>> map = getInfo(id); return (map != null) && map.containsKey(scope1); } }