package org.jivesoftware.openfire.group; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.xmpp.packet.JID; /** * This extension class provides additional methods that understand groups among * the entries in the map. * * @author Tom Evans */ public class ConcurrentGroupMap<K, V> extends ConcurrentHashMap<K, V> implements GroupAwareMap<K, V> { private static final long serialVersionUID = 5479781418678223200L; // These sets are used to optimize group operations within this map. // We only populate these sets when they are needed to dereference the // groups in the base map, but once they exist we keep them in sync // via the various put/remove operations. // NOTE: added volatile keyword for double-check idiom (lazy instantiation) private volatile transient Set<String> knownGroupNamesFromKeys; private volatile transient Set<String> knownGroupNamesFromValues; /** * Returns true if the key list contains the given JID. If the JID is not found in the * key list (exact match), search the key list for groups and look for the JID in * each of the corresponding groups (implied match). * * @param key The target, presumably a JID * @return True if the target is in the key list, or in any groups in the key list */ @Override public boolean includesKey(Object key) { boolean found = false; if (containsKey(key)) { found = true; } else if (key instanceof JID) { // look for group JIDs in the list of keys and dereference as needed JID target = (JID) key; Iterator<Group> iterator = getGroupsFromKeys().iterator(); while (!found && iterator.hasNext()) { found = iterator.next().isUser(target); } } return found; } /** * Returns true if the map has an entry value matching the given JID. If the JID is not * found in the value set (exact match), search the value set for groups and look for the * JID in each of the corresponding groups (implied match). * * @param value The target, presumably a JID * @return True if the target is in the value set, or in any groups in the value set */ @Override public boolean includesValue(Object value) { boolean found = false; if (containsValue(value)) { found = true; } else if (value instanceof JID) { // look for group JIDs in the list of values and dereference as needed JID target = (JID) value; Iterator<Group> iterator = getGroupsFromValues().iterator(); while (!found && iterator.hasNext()) { found = iterator.next().isUser(target); } } return found; } /** * Returns the groups that are implied (resolvable) from the keys in the map. * * @return A Set containing the groups among the keys */ @Override public synchronized Set<Group> getGroupsFromKeys() { Set<Group> result = new HashSet<>(); for(String groupName : getKnownGroupNamesFromKeys()) { result.add(Group.resolveFrom(groupName)); } return result; } /** * Returns the groups that are implied (resolvable) from the values in the map. * * @return A Set containing the groups among the values */ @Override public synchronized Set<Group> getGroupsFromValues() { Set<Group> result = new HashSet<>(); for(String groupName : getKnownGroupNamesFromValues()) { result.add(Group.resolveFrom(groupName)); } return result; } /** * Accessor uses the "double-check idiom" (j2se 5.0+) for proper lazy instantiation. * Additionally, nothing is cached until there is at least one group in the map's keys. * * @return the known group names among the items in the list */ private Set<String> getKnownGroupNamesFromKeys() { Set<String> result = knownGroupNamesFromKeys; if (result == null) { synchronized(this) { result = knownGroupNamesFromKeys; if (result == null) { result = new HashSet<>(); // add all the groups into the group set Iterator<K> iterator = ((ConcurrentMap) this).keySet().iterator(); // Cast prevents Java compatibility issue as described in OF-1116. Remove the cast when Java 7 support is dropped from Openfire. while (iterator.hasNext()) { K key = iterator.next(); Group group = Group.resolveFrom(key); if (group != null) { result.add(group.getName()); }; } knownGroupNamesFromKeys = result.isEmpty() ? null : result; } } } return result; } /** * Accessor uses the "double-check idiom" (j2se 5.0+) for proper lazy instantiation. * Additionally, nothing is cached until there is at least one group in the map's value set. * * @return the known group names among the items in the list */ private Set<String> getKnownGroupNamesFromValues() { Set<String> result = knownGroupNamesFromValues; if (result == null) { synchronized(this) { result = knownGroupNamesFromValues; if (result == null) { result = new HashSet<>(); // add all the groups into the group set Iterator<V> iterator = values().iterator(); while (iterator.hasNext()) { V key = iterator.next(); Group group = Group.resolveFrom(key); if (group != null) { result.add(group.getName()); }; } knownGroupNamesFromValues = result.isEmpty() ? null : result; } } } return result; } /** * This method is called from several of the mutators to keep * the group set in sync with the keys in the map. * * @param item The item to be added or removed if it is in the group set * @param keyOrValue True for keys, false for values * @param addOrRemove True to add, false to remove * @return true if the given item is a group */ private synchronized boolean syncGroups(Object item, boolean keyOrValue, boolean addOrRemove) { boolean result = false; Set<String> groupSet = (keyOrValue == KEYS) ? knownGroupNamesFromKeys : knownGroupNamesFromValues; // only sync if the group list has been instantiated if (groupSet != null) { Group group = Group.resolveFrom(item); if (group != null) { result = true; if (addOrRemove == ADD) { groupSet.add(group.getName()); } else if (addOrRemove == REMOVE) { groupSet.remove(group.getName()); if (groupSet.isEmpty()) { if (keyOrValue == KEYS) { knownGroupNamesFromKeys = null; } else { knownGroupNamesFromValues = null; } } } } } return result; } // below are overrides for the various mutators @Override public V put(K key, V value) { V priorValue = super.put(key, value); syncGroups(value, VALUES, ADD); if (priorValue == null) { syncGroups(key, KEYS, ADD); } else { syncGroups(priorValue, VALUES, REMOVE); } return priorValue; } @Override public V putIfAbsent(K key, V value) { V priorValue = super.putIfAbsent(key, value); // if the map already contains the key, there was no change if (!value.equals(priorValue)) { syncGroups(value, VALUES, ADD); if (priorValue == null) { syncGroups(key, KEYS, ADD); } else { syncGroups(priorValue, VALUES, REMOVE); } } return priorValue; } @Override public void putAll(Map<? extends K, ? extends V> m) { super.putAll(m); // drop the transient sets; will be rebuilt when/if needed clearCache(); } @Override public V remove(Object key) { V priorValue = super.remove(key); if (priorValue != null) { syncGroups(key, KEYS, REMOVE); syncGroups(priorValue, VALUES, REMOVE); } return priorValue; } @Override public boolean remove(Object key, Object value) { boolean removed = super.remove(key, value); if (removed) { syncGroups(key, KEYS, REMOVE); syncGroups(value, VALUES, REMOVE); } return removed; } @Override public boolean replace(K key, V oldValue, V newValue) { boolean replaced = super.replace(key, oldValue, newValue); if (replaced) { syncGroups(oldValue, VALUES, REMOVE); syncGroups(newValue, VALUES, ADD); } return replaced; } @Override public V replace(K key, V value) { V priorValue = super.replace(key, value); if (priorValue != null) { syncGroups(value, VALUES, ADD); syncGroups(priorValue, VALUES, REMOVE); } return priorValue; } @Override public void clear() { super.clear(); clearCache(); } /** * Certain operations imply that our locally cached group list(s) should * be dropped and recreated. For example, when an ad-hoc client command * is used to add a member to a group, the underlying group instance is * actually dropped from the global Group cache, with the effect that our * cache would be referring to an orphaned Group instance. */ private synchronized void clearCache() { knownGroupNamesFromKeys = null; knownGroupNamesFromValues = null; } private static final boolean KEYS = true; private static final boolean VALUES = false; private static final boolean ADD = true; private static final boolean REMOVE = false; }