GroupManager.java 26.2 KB
Newer Older
1 2 3 4 5
/**
 * $RCSfile$
 * $Revision: 3117 $
 * $Date: 2005-11-25 22:57:29 -0300 (Fri, 25 Nov 2005) $
 *
6
 * Copyright (C) 2004-2008 Jive Software. All rights reserved.
7
 *
8 9 10 11 12 13 14 15 16 17 18
 * 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.
19 20
 */

21
package org.jivesoftware.openfire.group;
22

23 24
import java.util.Collection;
import java.util.Collections;
25
import java.util.Iterator;
26 27
import java.util.Map;

28
import org.jivesoftware.openfire.XMPPServer;
29
import org.jivesoftware.openfire.clearspace.ClearspaceManager;
30 31
import org.jivesoftware.openfire.event.GroupEventDispatcher;
import org.jivesoftware.openfire.event.GroupEventListener;
32 33
import org.jivesoftware.openfire.event.UserEventDispatcher;
import org.jivesoftware.openfire.event.UserEventListener;
34
import org.jivesoftware.openfire.user.User;
35 36 37 38
import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
39 40
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
41 42
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
43 44 45 46 47 48 49 50 51 52
import org.xmpp.packet.JID;

/**
 * Manages groups.
 *
 * @see Group
 * @author Matt Tucker
 */
public class GroupManager {

53 54
	private static final Logger Log = LoggerFactory.getLogger(GroupManager.class);

55 56 57
    private static final class GroupManagerContainer {
        private static final GroupManager instance = new GroupManager();
    }
58

59 60 61
    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";
62
    private static final String PUBLIC_GROUPS = "PUBLIC_GROUPS";
63

64 65 66 67 68 69
    /**
     * Returns a singleton instance of GroupManager.
     *
     * @return a GroupManager instance.
     */
    public static GroupManager getInstance() {
70
        return GroupManagerContainer.instance;
71 72
    }

73 74
    private Cache<String, Group> groupCache;
    private Cache<String, Object> groupMetaCache;
75 76
    private GroupProvider provider;

77 78
    private GroupManager() {
        // Initialize caches.
79
        groupCache = CacheFactory.createCache("Group");
80

81 82
        // A cache for meta-data around groups: count, group names, groups associated with
        // a particular user
83
        groupMetaCache = CacheFactory.createCache("Group Metadata Cache");
84

85
        initProvider();
86 87 88

        GroupEventDispatcher.addListener(new GroupEventListener() {
            public void groupCreated(Group group, Map params) {
89 90 91 92 93 94 95 96 97

                // 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", "");
                }
                
98 99 100
                // Since the group could be created by the provider, add it possible again
                groupCache.put(group.getName(), group);

101 102 103 104 105
                // Evict only the information related to Groups.
                // Do not evict groups with 'user' as keys.
                groupMetaCache.remove(GROUP_COUNT_KEY);
                groupMetaCache.remove(GROUP_NAMES_KEY);
                groupMetaCache.remove(SHARED_GROUPS_KEY);
106

107 108
                // Evict cached information for affected users
                evictCachedUsersForGroup(group);
109 110 111

                // Evict cached paginated group names
                evictCachedPaginatedGroupNames();
112 113 114
            }

            public void groupDeleting(Group group, Map params) {
115 116 117
                // Since the group could be deleted by the provider, remove it possible again
                groupCache.remove(group.getName());
                
118 119 120 121 122
                // Evict only the information related to Groups.
                // Do not evict groups with 'user' as keys.
                groupMetaCache.remove(GROUP_COUNT_KEY);
                groupMetaCache.remove(GROUP_NAMES_KEY);
                groupMetaCache.remove(SHARED_GROUPS_KEY);
123

124 125
                // Evict cached information for affected users
                evictCachedUsersForGroup(group);
126 127 128

                // Evict cached paginated group names
                evictCachedPaginatedGroupNames();
129 130 131
            }

            public void groupModified(Group group, Map params) {
132 133
                String type = (String)params.get("type");
                // If shared group settings changed, expire the cache.
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
                if (type != null) {
                	if (type.equals("propertyModified") ||
                        type.equals("propertyDeleted") || type.equals("propertyAdded"))
	                {
                		Object key = params.get("propertyKey");
	                    if (key instanceof String && (key.equals("sharedRoster.showInRoster") || key.equals("*")))
	                    {
	                    	groupMetaCache.remove(GROUP_NAMES_KEY);
	                        groupMetaCache.remove(SHARED_GROUPS_KEY);
	                    }
	                }	
                	// clean up cache for old group name
                	if (type.equals("nameModified")) {
                		String originalName = (String) params.get("originalValue");
                		if (originalName != null) {
149
                			groupCache.remove(originalName);
150
                		}
151 152 153 154 155

                        groupMetaCache.remove(GROUP_NAMES_KEY);
                        groupMetaCache.remove(SHARED_GROUPS_KEY);
                		
                		// Evict cached information for affected users
156
                        evictCachedUsersForGroup(group);
157 158 159 160

                        // Evict cached paginated group names
                        evictCachedPaginatedGroupNames();
                        
161
                	}
162
                }
Gaston Dombiak's avatar
Gaston Dombiak committed
163 164 165
                // 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(), group);
166 167 168
            }

            public void memberAdded(Group group, Map params) {
Gaston Dombiak's avatar
Gaston Dombiak committed
169 170 171
                // 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(), group);
172 173 174 175 176 177
                
                // Remove only the collection of groups the member belongs to.
                String member = (String) params.get("member");
                if(member != null) {
	                groupMetaCache.remove(member);
                }
178 179 180
            }

            public void memberRemoved(Group group, Map params) {
Gaston Dombiak's avatar
Gaston Dombiak committed
181 182 183
                // 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(), group);
184 185 186 187 188 189
                
                // Remove only the collection of groups the member belongs to.
                String member = (String) params.get("member");
                if(member != null) {
	                groupMetaCache.remove(member);
                }
190 191 192
            }

            public void adminAdded(Group group, Map params) {
Gaston Dombiak's avatar
Gaston Dombiak committed
193 194 195
                // 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(), group);
196 197 198 199 200 201
                
                // Remove only the collection of groups the member belongs to.
                String member = (String) params.get("admin");
                if(member != null) {
	                groupMetaCache.remove(member);
                }
202 203 204
            }

            public void adminRemoved(Group group, Map params) {
Gaston Dombiak's avatar
Gaston Dombiak committed
205 206 207
                // 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(), group);
208 209 210 211 212 213
                
                // Remove only the collection of groups the member belongs to.
                String member = (String) params.get("admin");
                if(member != null) {
	                groupMetaCache.remove(member);
                }
214
            }
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229

        });

        UserEventDispatcher.addListener(new UserEventListener() {
            public void userCreated(User user, Map<String, Object> params) {
                // ignore
            }

            public void userDeleting(User user, Map<String, Object> params) {
                deleteUser(user);
            }

            public void userModified(User user, Map<String, Object> params) {
                // ignore
            }
230
        });
231

232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
        // Detect when a new auth provider class is set
        PropertyEventListener propListener = new PropertyEventListener() {
            public void propertySet(String property, Map params) {
                if ("provider.group.className".equals(property)) {
                    initProvider();
                }
            }

            public void propertyDeleted(String property, Map params) {
                //Ignore
            }

            public void xmlPropertySet(String property, Map params) {
                //Ignore
            }

            public void xmlPropertyDeleted(String property, Map params) {
                //Ignore
            }
        };
        PropertyEventDispatcher.addListener(propListener);
253 254
    }

255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
    private void initProvider() {
        // Convert XML based provider setup to Database based
        JiveGlobals.migrateProperty("provider.group.className");

        // Load a group provider.
        String className = JiveGlobals.getProperty("provider.group.className",
                "org.jivesoftware.openfire.group.DefaultGroupProvider");
        try {
            Class c = ClassUtils.forName(className);
            provider = (GroupProvider) c.newInstance();
        }
        catch (Exception e) {
            Log.error("Error loading group provider: " + className, e);
            provider = new DefaultGroupProvider();
        }
    }

272 273 274 275 276 277 278 279 280
    /**
     * 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.intern()) {
281
            Group newGroup;
282 283 284 285 286 287 288 289
            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);
290
                // Update caches.
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
                groupCache.put(name, newGroup);

                // Fire event.
                GroupEventDispatcher.dispatchEvent(newGroup,
                        GroupEventDispatcher.EventType.group_created, Collections.emptyMap());
            }
            return newGroup;
        }
    }

    /**
     * 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 {
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
        return getGroup(name, false);
    }

    /**
     * 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, boolean forceLookup) throws GroupNotFoundException {
        Group group = null;
        if (!forceLookup) {
            group = groupCache.get(name);
        }
324 325 326
        // If ID wan't found in cache, load it up and put it there.
        if (group == null) {
            synchronized (name.intern()) {
327
                group = groupCache.get(name);
328
                // If group wan't found in cache, load it up and put it there.
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
                if (group == null) {
                    group = provider.getGroup(name);
                    groupCache.put(name, group);
                }
            }
        }
        return group;
    }

    /**
     * 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());

351
        // Expire cache.
352 353 354 355 356 357 358 359 360
        groupCache.remove(group.getName());
    }

    /**
     * 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.
     */
361
    public void deleteUser(User user) {
362 363 364
        JID userJID = XMPPServer.getInstance().createJID(user.getUsername(), null);
        for (Group group : getGroups(userJID)) {
            if (group.getAdmins().contains(userJID)) {
365 366 367 368
                if (group.getAdmins().remove(userJID)) {
                    // Remove the group from cache.
                    groupCache.remove(group.getName());
                }
369 370
            }
            else {
371 372 373 374
                if (group.getMembers().remove(userJID)) {
                    // Remove the group from cache.
                    groupCache.remove(group.getName());
                }
375 376 377 378 379 380 381 382 383 384
            }
        }
    }

    /**
     * Returns the total number of groups in the system.
     *
     * @return the total number of groups.
     */
    public int getGroupCount() {
385
            Integer count = (Integer)groupMetaCache.get(GROUP_COUNT_KEY);
386 387 388 389 390 391 392 393 394 395
        if (count == null) {
            synchronized(GROUP_COUNT_KEY.intern()) {
                count = (Integer)groupMetaCache.get(GROUP_COUNT_KEY);
                if (count == null) {
                    count = provider.getGroupCount();
                    groupMetaCache.put(GROUP_COUNT_KEY, count);
                }
            }
        }
        return count;
396 397 398 399
    }

    /**
     * Returns an unmodifiable Collection of all groups in the system.
400 401 402 403 404
     * 
     * 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.
405 406 407 408
     *
     * @return an unmodifiable Collection of all groups.
     */
    public Collection<Group> getGroups() {
409 410 411 412 413 414 415 416 417 418
        Collection<String> groupNames = (Collection<String>)groupMetaCache.get(GROUP_NAMES_KEY);
        if (groupNames == null) {
            synchronized(GROUP_NAMES_KEY.intern()) {
                groupNames = (Collection<String>)groupMetaCache.get(GROUP_NAMES_KEY);
                if (groupNames == null) {
                    groupNames = provider.getGroupNames();
                    groupMetaCache.put(GROUP_NAMES_KEY, groupNames);
                }
            }
        }
419
        return new GroupCollection(groupNames);
420 421
    }

422 423
    /**
     * Returns an unmodifiable Collection of all shared groups in the system.
424 425 426 427 428
     * 
     * 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.
429 430 431 432
     *
     * @return an unmodifiable Collection of all shared groups.
     */
    public Collection<Group> getSharedGroups() {
433 434 435 436 437
        Collection<String> groupNames = (Collection<String>)groupMetaCache.get(SHARED_GROUPS_KEY);
        if (groupNames == null) {
            synchronized(SHARED_GROUPS_KEY.intern()) {
                groupNames = (Collection<String>)groupMetaCache.get(SHARED_GROUPS_KEY);
                if (groupNames == null) {
438
                    groupNames = provider.getSharedGroupNames();
439 440 441 442
                    groupMetaCache.put(SHARED_GROUPS_KEY, groupNames);
                }
            }
        }
443
        return new GroupCollection(groupNames);
444
    }
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517
    
    /**
     * Returns an unmodifiable Collection of all shared groups in the system for a given userName.
     *
     * @return an unmodifiable Collection of all shared groups for the given userName.
     */
    public Collection<Group> getSharedGroups(String userName) {
        Collection<String> groupNames = (Collection<String>)groupMetaCache.get(userName);
        if (groupNames == null) {
            synchronized(userName.intern()) {
                groupNames = (Collection<String>)groupMetaCache.get(userName);
                if (groupNames == null) {
                	// assume this is a local user
                    groupNames = provider.getSharedGroupNames(new JID(userName, 
                    		XMPPServer.getInstance().getServerInfo().getXMPPDomain(), null));
                    groupMetaCache.put(userName, groupNames);
                }
            }
        }
        return new GroupCollection(groupNames);
    }
    
    /**
     * Returns an unmodifiable Collection of all shared groups in the system for a given userName.
     *
     * @return an unmodifiable Collection of all shared groups for the given userName.
     */
    public Collection<Group> getVisibleGroups(Group groupToCheck) {
    	// Get all the public shared groups.
    	Collection<String> groupNames = (Collection<String>)groupMetaCache.get(PUBLIC_GROUPS);
        if (groupNames == null) {
            synchronized(PUBLIC_GROUPS.intern()) {
                groupNames = (Collection<String>)groupMetaCache.get(PUBLIC_GROUPS);
                if (groupNames == null) {
                    groupNames = provider.getPublicSharedGroupNames();
                    groupMetaCache.put(PUBLIC_GROUPS, 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() {
        Collection<String> groupNames = (Collection<String>)groupMetaCache.get(PUBLIC_GROUPS);
        if (groupNames == null) {
            synchronized(PUBLIC_GROUPS.intern()) {
                groupNames = (Collection<String>)groupMetaCache.get(PUBLIC_GROUPS);
                if (groupNames == null) {
                    groupNames = provider.getPublicSharedGroupNames();
                    groupMetaCache.put(PUBLIC_GROUPS, groupNames);
                }
            }
        }
        return new GroupCollection(groupNames);
    }
    
    /**
     * Returns an unmodifiable Collection of all groups in the system that
     * match given propValue for the specified propName.
     *
     * @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);
    }
518

519
    /**
520 521 522 523 524
     * 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.
525 526 527 528 529 530
     *
     * @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) {
531 532 533 534 535 536 537 538 539 540 541 542
        String key = GROUP_NAMES_KEY + startIndex + "," + numResults;

        Collection<String> groupNames = (Collection<String>)groupMetaCache.get(key);
        if (groupNames == null) {
            synchronized(key.intern()) {
                groupNames = (Collection<String>)groupMetaCache.get(key);
                if (groupNames == null) {
                    groupNames = provider.getGroupNames(startIndex, numResults);
                    groupMetaCache.put(key, groupNames);
                }
            }
        }
543
        return new GroupCollection(groupNames);
544 545
    }

546 547 548 549 550 551 552
    /**
     * 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) {
553
        return getGroups(XMPPServer.getInstance().createJID(user.getUsername(), null, true));
554 555
    }

556 557 558 559 560 561 562
    /**
     * 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) {
563 564 565 566 567 568 569 570 571 572 573 574
        String key = user.toBareJID();

        Collection<String> groupNames = (Collection<String>)groupMetaCache.get(key);
        if (groupNames == null) {
            synchronized(key.intern()) {
                groupNames = (Collection<String>)groupMetaCache.get(key);
                if (groupNames == null) {
                    groupNames = provider.getGroupNames(user);
                    groupMetaCache.put(key, groupNames);
                }
            }
        }
575
        return new GroupCollection(groupNames);
576 577
    }

578 579 580 581 582 583 584 585 586
    /**
     * Returns true if groups are read-only.
     *
     * @return true if groups are read-only.
     */
    public boolean isReadOnly() {
        return provider.isReadOnly();
    }

587 588 589 590 591 592 593 594 595 596
    /**
     * Returns true if properties of groups are read only.
     * They are read only if Clearspace is the group provider.
     *
     * @return true if properties of groups are read only.
     */
    public boolean isPropertyReadOnly() {
        return ClearspaceManager.isEnabled();
    }
    
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
    /**
     * 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) {
619 620
        Collection<String> groupNames = provider.search(query);
        return new GroupCollection(groupNames);
621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636
    }

    /**
     * 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.
     * @return all groups that match the search.
     */
    public Collection<Group> search(String query, int startIndex, int numResults) {
637 638
        Collection<String> groupNames = provider.search(query, startIndex, numResults);
        return new GroupCollection(groupNames);
639 640
    }

641 642 643 644 645 646
    /**
     * 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.
     */
647
    public GroupProvider getProvider() {
648 649
        return provider;
    }
650 651 652 653 654 655 656 657 658 659
    
    private void evictCachedUsersForGroup(Group group) {
        // Evict cached information for affected users
        for (JID user : group.getAdmins()) {
        	groupMetaCache.remove(user.getNode());
        }
        for (JID user : group.getMembers()) {
        	groupMetaCache.remove(user.getNode());
        }
    }
660 661 662 663 664 665 666 667 668

    private void evictCachedPaginatedGroupNames() {
        for(Map.Entry<String, Object> entry : groupMetaCache.entrySet())
        {
            if (entry.getKey().startsWith(GROUP_NAMES_KEY)) {
                groupMetaCache.remove(entry.getKey());
            }
        }
    }
669
}