Java tutorial
/* * Copyright (C) 2004-2008 Jive Software. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.openfire.group; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.StringTokenizer; import org.apache.commons.lang3.StringUtils; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.event.GroupEventDispatcher; import org.jivesoftware.openfire.event.GroupEventListener; import org.jivesoftware.openfire.event.UserEventDispatcher; import org.jivesoftware.openfire.event.UserEventListener; import org.jivesoftware.openfire.user.User; import org.jivesoftware.util.CacheableOptional; import org.jivesoftware.util.SystemProperty; import org.jivesoftware.util.cache.Cache; import org.jivesoftware.util.cache.CacheFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.packet.JID; /** * Manages groups. * * @see Group * @author Matt Tucker */ public class GroupManager { public static final SystemProperty<Class> GROUP_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("provider.group.className").setBaseClass(GroupProvider.class) .setDefaultValue(DefaultGroupProvider.class).addListener(GroupManager::initProvider).setDynamic(true) .build(); private static final Logger Log = LoggerFactory.getLogger(GroupManager.class); private static final class GroupManagerContainer { private static final GroupManager instance = new GroupManager(); } private static final String MUTEX_SUFFIX_GROUP = " grp"; private static final String MUTEX_SUFFIX_USER = " grpu"; private static final String MUTEX_SUFFIX_KEY = " grpk"; private static final String GROUP_COUNT_KEY = "GROUP_COUNT"; private static final String SHARED_GROUPS_KEY = "SHARED_GROUPS"; private static final String GROUP_NAMES_KEY = "GROUP_NAMES"; private static final String PUBLIC_GROUPS = "PUBLIC_GROUPS"; private static final String USER_SHARED_GROUPS_KEY = "USER_SHARED_GROUPS"; private static final String USER_GROUPS_KEY = "USER_GROUPS"; /** * Returns a singleton instance of GroupManager. * * @return a GroupManager instance. */ public static GroupManager getInstance() { return GroupManagerContainer.instance; } private Cache<String, CacheableOptional<Group>> groupCache; private Cache<String, Serializable> groupMetaCache; private static GroupProvider provider; private GroupManager() { // Initialize caches. groupCache = CacheFactory.createCache("Group"); // A cache for meta-data around groups: count, group names, groups associated with // a particular user groupMetaCache = CacheFactory.createCache("Group Metadata Cache"); initProvider(GROUP_PROVIDER.getValue()); GroupEventDispatcher.addListener(new GroupEventListener() { @Override public void groupCreated(Group group, Map params) { // Adds default properties if they don't exists, since the creator of // the group could set them. if (group.getProperties().get("sharedRoster.showInRoster") == null) { group.getProperties().put("sharedRoster.showInRoster", "nobody"); group.getProperties().put("sharedRoster.displayName", ""); group.getProperties().put("sharedRoster.groupList", ""); } // Since the group could be created by the provider, add it possible again groupCache.put(group.getName(), CacheableOptional.of(group)); // Evict only the information related to Groups. // Do not evict groups with 'user' as keys. clearGroupCountCache(); clearGroupNameCache(); clearSharedGroupCache(); // Evict cached information for affected users evictCachedUsersForGroup(group); // Evict cached paginated group names evictCachedPaginatedGroupNames(); } @Override public void groupDeleting(Group group, Map params) { // Since the group could be deleted by the provider, remove it possible again groupCache.put(group.getName(), CacheableOptional.of(null)); // Evict only the information related to Groups. // Do not evict groups with 'user' as keys. clearGroupCountCache(); clearGroupNameCache(); clearSharedGroupCache(); // Evict cached information for affected users evictCachedUsersForGroup(group); // Evict cached paginated group names evictCachedPaginatedGroupNames(); } @Override public void groupModified(Group group, Map params) { String type = (String) params.get("type"); // If shared group settings changed, expire the cache. if (type != null) { if (type.equals("propertyModified") || type.equals("propertyDeleted") || type.equals("propertyAdded")) { Object key = params.get("propertyKey"); if ("sharedRoster.showInRoster".equals(key) || "*".equals(key)) { clearGroupNameCache(); clearSharedGroupCache(); String originalValue = (String) params.get("originalValue"); String newValue = group.getProperties().get("sharedRoster.showInRoster"); // 'showInRoster' has changed if (!StringUtils.equals(originalValue, newValue)) { if ("everybody".equals(originalValue) || "everybody".equals(newValue)) { evictCachedUserSharedGroups(); } } } else if ("sharedRoster.groupList".equals(key)) { String originalValue = (String) params.get("originalValue"); String newValue = group.getProperties().get("sharedRoster.groupList"); // 'groupList' has changed if (!StringUtils.equals(originalValue, newValue)) { evictCachedUsersForGroup(group, originalValue); } } } // clean up cache for old group name if (type.equals("nameModified")) { String originalName = (String) params.get("originalValue"); if (originalName != null) { groupCache.remove(originalName); } clearGroupNameCache(); clearSharedGroupCache(); // Evict cached information for affected users evictCachedUsersForGroup(group); // Evict cached paginated group names evictCachedPaginatedGroupNames(); } } // Set object again in cache. This is done so that other cluster nodes // get refreshed with latest version of the object groupCache.put(group.getName(), CacheableOptional.of(group)); } @Override public void memberAdded(Group group, Map params) { // Set object again in cache. This is done so that other cluster nodes // get refreshed with latest version of the object groupCache.put(group.getName(), CacheableOptional.of(group)); // Remove only the collection of groups the member belongs to. String member = (String) params.get("member"); evictCachedUserForGroup(member); } @Override public void memberRemoved(Group group, Map params) { // Set object again in cache. This is done so that other cluster nodes // get refreshed with latest version of the object groupCache.put(group.getName(), CacheableOptional.of(group)); // Remove only the collection of groups the member belongs to. String member = (String) params.get("member"); evictCachedUserForGroup(member); } @Override public void adminAdded(Group group, Map params) { // Set object again in cache. This is done so that other cluster nodes // get refreshed with latest version of the object groupCache.put(group.getName(), CacheableOptional.of(group)); // Remove only the collection of groups the member belongs to. String member = (String) params.get("admin"); evictCachedUserForGroup(member); } @Override public void adminRemoved(Group group, Map params) { // Set object again in cache. This is done so that other cluster nodes // get refreshed with latest version of the object groupCache.put(group.getName(), CacheableOptional.of(group)); // Remove only the collection of groups the member belongs to. String member = (String) params.get("admin"); evictCachedUserForGroup(member); } }); UserEventDispatcher.addListener(new UserEventListener() { @Override public void userCreated(User user, Map<String, Object> params) { // ignore } @Override public void userDeleting(User user, Map<String, Object> params) { deleteUser(user); } @Override public void userModified(User user, Map<String, Object> params) { // ignore } }); } private static void initProvider(final Class clazz) { if (provider == null || !clazz.equals(provider.getClass())) { try { provider = (GroupProvider) clazz.newInstance(); } catch (Exception e) { Log.error("Error loading group provider: " + clazz.getName(), e); provider = new DefaultGroupProvider(); } } } /** * Factory method for creating a new Group. A unique name is the only required field. * * @param name the new and unique name for the group. * @return a new Group. * @throws GroupAlreadyExistsException if the group name already exists in the system. */ public Group createGroup(String name) throws GroupAlreadyExistsException { synchronized ((name + MUTEX_SUFFIX_GROUP).intern()) { Group newGroup; try { getGroup(name); // The group already exists since now exception, so: throw new GroupAlreadyExistsException(); } catch (GroupNotFoundException unfe) { // The group doesn't already exist so we can create a new group newGroup = provider.createGroup(name); // Update caches. clearGroupNameCache(); clearGroupCountCache(); groupCache.put(name, CacheableOptional.of(newGroup)); // Fire event. GroupEventDispatcher.dispatchEvent(newGroup, GroupEventDispatcher.EventType.group_created, Collections.emptyMap()); } return newGroup; } } /** * Returns the corresponding group if the given JID represents a group. * * @param jid The JID for the group to retrieve * @return The group corresponding to the JID, or null if the JID does not represent a group * @throws GroupNotFoundException if the JID represents a group that does not exist */ public Group getGroup(JID jid) throws GroupNotFoundException { JID groupJID = GroupJID.fromJID(jid); return (groupJID instanceof GroupJID) ? getGroup(((GroupJID) groupJID).getGroupName()) : null; } /** * Returns a Group by name. * * @param name The name of the group to retrieve * @return The group corresponding to that name * @throws GroupNotFoundException if the group does not exist. */ public Group getGroup(String name) throws GroupNotFoundException { return getGroup(name, false); } /** * Returns a Group by name. * * @param name The name of the group to retrieve * @param forceLookup Invalidate the group cache for this group * @return The group corresponding to that name * @throws GroupNotFoundException if the group does not exist. */ public Group getGroup(String name, boolean forceLookup) throws GroupNotFoundException { CacheableOptional<Group> coGroup = null; if (forceLookup) { groupCache.remove(name); } else { coGroup = groupCache.get(name); } if (coGroup == null) { synchronized ((name + MUTEX_SUFFIX_GROUP).intern()) { coGroup = groupCache.get(name); if (coGroup == null || coGroup.isAbsent()) { if (groupCache.containsKey(name) && !forceLookup) { throw new GroupNotFoundException("Group with name " + name + " not found (cached)."); } try { final Group group = provider.getGroup(name); coGroup = CacheableOptional.of(group); groupCache.put(name, coGroup); } catch (GroupNotFoundException e) { groupCache.put(name, CacheableOptional.of(null)); throw e; } } } } return coGroup.get(); } /** * Deletes a group from the system. * * @param group the group to delete. */ public void deleteGroup(Group group) { // Fire event. GroupEventDispatcher.dispatchEvent(group, GroupEventDispatcher.EventType.group_deleting, Collections.emptyMap()); // Delete the group. provider.deleteGroup(group.getName()); // Add a no-hit to the cache. groupCache.put(group.getName(), CacheableOptional.of(null)); clearGroupNameCache(); clearGroupCountCache(); } /** * Deletes a user from all the groups where he/she belongs. The most probable cause * for this request is that the user has been deleted from the system. * * @param user the deleted user from the system. */ public void deleteUser(User user) { JID userJID = XMPPServer.getInstance().createJID(user.getUsername(), null); for (Group group : getGroups(userJID)) { if (group.getAdmins().contains(userJID)) { if (group.getAdmins().remove(userJID)) { // Remove the group from cache. groupCache.remove(group.getName()); } } else { if (group.getMembers().remove(userJID)) { // Remove the group from cache. groupCache.remove(group.getName()); } } } evictCachedUserForGroup(userJID.toBareJID()); } /** * Returns the total number of groups in the system. * * @return the total number of groups. */ public int getGroupCount() { Integer count = getGroupCountFromCache(); if (count == null) { synchronized (GROUP_COUNT_KEY) { count = getGroupCountFromCache(); if (count == null) { count = provider.getGroupCount(); saveGroupCountInCache(count); } } } return count; } /** * Returns an unmodifiable Collection of all groups in the system. * * NOTE: Iterating through the resulting collection has the effect of loading * every group into memory. This may be an issue for large deployments. You * may call the size() method on the resulting collection to determine the best * approach to take before iterating over (and thus instantiating) the groups. * * @return an unmodifiable Collection of all groups. */ public Collection<Group> getGroups() { HashSet<String> groupNames = getGroupNamesFromCache(); if (groupNames == null) { synchronized (GROUP_NAMES_KEY) { groupNames = getGroupNamesFromCache(); if (groupNames == null) { groupNames = new HashSet<>(provider.getGroupNames()); saveGroupNamesInCache(groupNames); } } } return new GroupCollection(groupNames); } /** * Returns an unmodifiable Collection of all shared groups in the system. * * NOTE: Iterating through the resulting collection has the effect of loading all * shared groups into memory. This may be an issue for large deployments. You * may call the size() method on the resulting collection to determine the best * approach to take before iterating over (and thus instantiating) the groups. * * @return an unmodifiable Collection of all shared groups. */ public Collection<Group> getSharedGroups() { HashSet<String> groupNames = getSharedGroupsFromCache(); if (groupNames == null) { synchronized (SHARED_GROUPS_KEY) { groupNames = getSharedGroupsFromCache(); if (groupNames == null) { groupNames = new HashSet<>(provider.getSharedGroupNames()); saveSharedGroupsInCache(groupNames); } } } return new GroupCollection(groupNames); } /** * Returns an unmodifiable Collection of all shared groups in the system for a given userName. * * @param userName the user to check * @return an unmodifiable Collection of all shared groups for the given userName. */ public Collection<Group> getSharedGroups(String userName) { HashSet<String> groupNames = getSharedGroupsForUserFromCache(userName); if (groupNames == null) { synchronized ((userName + MUTEX_SUFFIX_USER).intern()) { groupNames = getSharedGroupsForUserFromCache(userName); if (groupNames == null) { // assume this is a local user groupNames = new HashSet<>(provider.getSharedGroupNames( new JID(userName, XMPPServer.getInstance().getServerInfo().getXMPPDomain(), null))); saveSharedGroupsForUserInCache(userName, groupNames); } } } return new GroupCollection(groupNames); } /** * Returns an unmodifiable Collection of all shared groups in the system for a given userName. * * @param groupToCheck The group to check * @return an unmodifiable Collection of all shared groups for the given userName. */ public Collection<Group> getVisibleGroups(Group groupToCheck) { // Get all the public shared groups. HashSet<String> groupNames = getPublicGroupsFromCache(); if (groupNames == null) { synchronized (PUBLIC_GROUPS) { groupNames = getPublicGroupsFromCache(); if (groupNames == null) { groupNames = new HashSet<>(provider.getPublicSharedGroupNames()); savePublicGroupsInCache(groupNames); } } } // Now get all visible groups to the given group. groupNames.addAll(provider.getVisibleGroupNames(groupToCheck.getName())); return new GroupCollection(groupNames); } /** * Returns an unmodifiable Collection of all public shared groups in the system. * * @return an unmodifiable Collection of all shared groups. */ public Collection<Group> getPublicSharedGroups() { HashSet<String> groupNames = getPublicGroupsFromCache(); if (groupNames == null) { synchronized (PUBLIC_GROUPS) { groupNames = getPublicGroupsFromCache(); if (groupNames == null) { groupNames = new HashSet<>(provider.getPublicSharedGroupNames()); savePublicGroupsInCache(groupNames); } } } return new GroupCollection(groupNames); } /** * Returns an unmodifiable Collection of all groups in the system that * match given propValue for the specified propName. * * @param propName the property name to search for * @param propValue the property value to search for * @return an unmodifiable Collection of all shared groups. */ public Collection<Group> search(String propName, String propValue) { Collection<String> groupsWithProps = provider.search(propName, propValue); return new GroupCollection(groupsWithProps); } /** * Returns all groups given a start index and desired number of results. This is * useful to support pagination in a GUI where you may only want to display a certain * number of results per page. It is possible that the number of results returned will * be less than that specified by numResults if numResults is greater than the number * of records left in the system to display. * * @param startIndex start index in results. * @param numResults number of results to return. * @return an Iterator for all groups in the specified range. */ public Collection<Group> getGroups(int startIndex, int numResults) { HashSet<String> groupNames = getPagedGroupNamesFromCache(startIndex, numResults); if (groupNames == null) { synchronized ((getPagedGroupNameKey(startIndex, numResults) + MUTEX_SUFFIX_KEY).intern()) { groupNames = getPagedGroupNamesFromCache(startIndex, numResults); if (groupNames == null) { groupNames = new HashSet<>(provider.getGroupNames(startIndex, numResults)); savePagedGroupNamesFromCache(groupNames, startIndex, numResults); } } } return new GroupCollection(groupNames); } /** * Returns an iterator for all groups that the User is a member of. * * @param user the user. * @return all groups the user belongs to. */ public Collection<Group> getGroups(User user) { return getGroups(XMPPServer.getInstance().createJID(user.getUsername(), null, true)); } /** * Returns an iterator for all groups that the entity with the specified JID is a member of. * * @param user the JID of the entity to get a list of groups for. * @return all groups that an entity belongs to. */ public Collection<Group> getGroups(JID user) { HashSet<String> groupNames = getUserGroupsFromCache(user); if (groupNames == null) { synchronized ((user.getNode() + MUTEX_SUFFIX_USER).intern()) { groupNames = getUserGroupsFromCache(user); if (groupNames == null) { groupNames = new HashSet<>(provider.getGroupNames(user)); saveUserGroupsInCache(user, groupNames); } } } return new GroupCollection(groupNames); } /** * Returns true if groups are read-only. * * @return true if groups are read-only. */ public boolean isReadOnly() { return provider.isReadOnly(); } /** * Returns true if searching for groups is supported. * * @return true if searching for groups are supported. */ public boolean isSearchSupported() { return provider.isSearchSupported(); } /** * Returns the groups that match the search. The search is over group names and * implicitly uses wildcard matching (although the exact search semantics are left * up to each provider implementation). For example, a search for "HR" should match * the groups "HR", "HR Department", and "The HR People".<p> * * Before searching or showing a search UI, use the {@link #isSearchSupported} method * to ensure that searching is supported. * * @param query the search string for group names. * @return all groups that match the search. */ public Collection<Group> search(String query) { Collection<String> groupNames = provider.search(query); return new GroupCollection(groupNames); } /** * Returns the groups that match the search given a start index and desired number * of results. The search is over group names and implicitly uses wildcard matching * (although the exact search semantics are left up to each provider implementation). * For example, a search for "HR" should match the groups "HR", "HR Department", and * "The HR People".<p> * * Before searching or showing a search UI, use the {@link #isSearchSupported} method * to ensure that searching is supported. * * @param query the search string for group names. * @param startIndex the start index to retrieve the group list from * @param numResults the maximum number of results to return * @return all groups that match the search. */ public Collection<Group> search(String query, int startIndex, int numResults) { Collection<String> groupNames = provider.search(query, startIndex, numResults); return new GroupCollection(groupNames); } /** * Returns the configured group provider. Note that this method has special access * privileges since only a few certain classes need to access the provider directly. * * @return the group provider. */ public GroupProvider getProvider() { return provider; } private void evictCachedUserForGroup(String userJid) { if (userJid != null) { JID user = new JID(userJid); // remove cache for getGroups synchronized (USER_GROUPS_KEY) { clearUserGroupsCache(user); } // remove cache for getSharedGroups if (XMPPServer.getInstance().isLocal(user)) { synchronized (USER_SHARED_GROUPS_KEY) { clearSharedGroupsForUserCache(user.getNode()); } } } } private void evictCachedUsersForGroup(Group group) { evictCachedUsersForGroup(group, null); } private void evictCachedUsersForGroup(Group group, String oldGroupList) { // Evict cached information for affected users for (JID user : group.getAdmins()) { evictCachedUserForGroup(user.toBareJID()); } for (JID user : group.getMembers()) { evictCachedUserForGroup(user.toBareJID()); } final String showInRoster = group.getProperties().get("sharedRoster.showInRoster"); if (showInRoster != null) { switch (showInRoster.toLowerCase()) { case "everybody": evictCachedUserSharedGroups(); break; case "onlygroup": String groupList = group.getProperties().get("sharedRoster.groupList"); if (groupList != null && oldGroupList != null) { groupList = groupList + "," + oldGroupList; } else if (groupList == null) { groupList = oldGroupList; } if (groupList != null) { HashSet<String> spefgroups = new HashSet<>(); final StringTokenizer tokenizer = new StringTokenizer(groupList, ",\t\n\r\f"); while (tokenizer.hasMoreTokens()) { spefgroups.add(tokenizer.nextToken().trim()); } for (String spefgroup : spefgroups) { try { final Group nested = getGroup(spefgroup); evictCachedUsersForGroup(nested); } catch (StackOverflowError e) { Log.warn("Cyclic sharing groups found. Please remove the cycle of groups '{}' and '{}'", group.getName(), spefgroup); } catch (GroupNotFoundException e) { Log.debug( "While evicting cached users for group '{}', an unrecognized spefgroup was found: '{}'", group.getName(), spefgroup, e); } } } break; } } } private void evictCachedPaginatedGroupNames() { groupMetaCache.keySet().stream().filter(key -> key.startsWith(GROUP_NAMES_KEY)) .forEach(key -> groupMetaCache.remove(key)); } private void evictCachedUserSharedGroups() { synchronized (USER_SHARED_GROUPS_KEY) { groupMetaCache.keySet().stream() .filter(key -> key.startsWith(USER_SHARED_GROUPS_KEY) || key.startsWith(GROUP_NAMES_KEY)) .forEach(key -> groupMetaCache.remove(key)); } } /* For reasons currently unclear, this class stores a number of different objects in the groupMetaCache. To better encapsulate this, all access to the groupMetaCache is via these methods */ @SuppressWarnings("unchecked") private HashSet<String> getGroupNamesFromCache() { return (HashSet<String>) groupMetaCache.get(GROUP_NAMES_KEY); } private void clearGroupNameCache() { groupMetaCache.remove(GROUP_NAMES_KEY); } private void saveGroupNamesInCache(final HashSet<String> groupNames) { groupMetaCache.put(GROUP_NAMES_KEY, groupNames); } private String getPagedGroupNameKey(final int startIndex, final int numResults) { return GROUP_NAMES_KEY + startIndex + "," + numResults; } @SuppressWarnings("unchecked") private HashSet<String> getPagedGroupNamesFromCache(final int startIndex, final int numResults) { return (HashSet<String>) groupMetaCache.get(getPagedGroupNameKey(startIndex, numResults)); } private void savePagedGroupNamesFromCache(final HashSet<String> groupNames, final int startIndex, final int numResults) { groupMetaCache.put(getPagedGroupNameKey(startIndex, numResults), groupNames); } private Integer getGroupCountFromCache() { return (Integer) groupMetaCache.get(GROUP_COUNT_KEY); } private void saveGroupCountInCache(final int count) { groupMetaCache.put(GROUP_COUNT_KEY, count); } private void clearGroupCountCache() { groupMetaCache.remove(GROUP_COUNT_KEY); } @SuppressWarnings("unchecked") private HashSet<String> getSharedGroupsFromCache() { return (HashSet<String>) groupMetaCache.get(SHARED_GROUPS_KEY); } private void clearSharedGroupCache() { groupMetaCache.remove(SHARED_GROUPS_KEY); } private void saveSharedGroupsInCache(final HashSet<String> groupNames) { groupMetaCache.put(SHARED_GROUPS_KEY, groupNames); } private String getSharedGroupsForUserKey(final String userName) { return USER_SHARED_GROUPS_KEY + userName; } @SuppressWarnings("unchecked") private HashSet<String> getSharedGroupsForUserFromCache(final String userName) { return (HashSet<String>) groupMetaCache.get(getSharedGroupsForUserKey(userName)); } private void clearSharedGroupsForUserCache(final String userName) { groupMetaCache.remove(getSharedGroupsForUserKey(userName)); } private void saveSharedGroupsForUserInCache(final String userName, final HashSet<String> groupNames) { groupMetaCache.put(getSharedGroupsForUserKey(userName), groupNames); } @SuppressWarnings("unchecked") private HashSet<String> getPublicGroupsFromCache() { return (HashSet<String>) groupMetaCache.get(PUBLIC_GROUPS); } private void clearPublicGroupsCache() { groupMetaCache.remove(PUBLIC_GROUPS); } private void savePublicGroupsInCache(final HashSet<String> groupNames) { groupMetaCache.put(PUBLIC_GROUPS, groupNames); } @SuppressWarnings("unchecked") private HashSet<String> getUserGroupsFromCache(final JID user) { return (HashSet<String>) groupMetaCache.get(getUserGroupsKey(user)); } private void clearUserGroupsCache(final JID user) { groupMetaCache.remove(getUserGroupsKey(user)); } private void saveUserGroupsInCache(final JID user, final HashSet<String> groupNames) { groupMetaCache.put(getUserGroupsKey(user), groupNames); } private String getUserGroupsKey(final JID user) { return USER_GROUPS_KEY + user.toBareJID(); } }