View Javadoc

1   package com.atlassian.user.impl.cache;
2   
3   import com.atlassian.cache.CacheFactory;
4   import com.atlassian.user.Entity;
5   import com.atlassian.user.EntityException;
6   import com.atlassian.user.Group;
7   import com.atlassian.user.GroupManager;
8   import com.atlassian.user.User;
9   import com.atlassian.user.repository.RepositoryIdentifier;
10  import com.atlassian.user.search.page.DefaultPager;
11  import com.atlassian.user.search.page.Pager;
12  import com.atlassian.user.search.page.PagerUtils;
13  import org.apache.log4j.Category;
14  
15  import java.util.LinkedList;
16  import java.util.List;
17  
18  /**
19   * Uses a generic caching strategy to provide caching for any implementation of
20   * {@link com.atlassian.user.GroupManager}.
21   * <p/>
22   * Typically used by putting the 'caching="true"' attribute on the repository in
23   * the XML configuration. The default repository loader will then wrap the
24   * GroupManager implementation in an instance of this class.
25   * <p/>
26   * To minimise memory use, except for the direct name->object caches, the
27   * underlying caches do not store the actual group or user objects. Instead,
28   * they store just the user or group name and methods in this class retrieve the
29   * objects as required. The name->object caches will ensure that is relatively
30   * efficient.
31   */
32  public class CachingGroupManager implements GroupManager
33  {
34      private static final Category log = Category.getInstance(CachingGroupManager.class);
35  
36      protected final GroupManager underlyingGroupManager;
37      protected final CacheFactory cacheFactory;
38  
39      protected GroupCache groupCache = null;
40      protected MembershipCache membershipCache = null;
41      protected GroupsForUserCache groupsForUserCache = null;
42      protected EntityRepositoryCache entityRepositoryCache = null;
43  
44      public CachingGroupManager(GroupManager underlyingGroupManager, CacheFactory cacheFactory)
45      {
46          this.underlyingGroupManager = underlyingGroupManager;
47          this.cacheFactory = cacheFactory;
48          initialiseCaches();
49      }
50  
51      /**
52       * Caches the list of groups retrieved for a particular user. This incurs a performance hit
53       * the first time a user's groups are retrieved, because we iterate over the entire pager
54       * and cache all groups to which the user belongs.
55       *
56       * The cached stores only group names to reduce memory usage. Since this method must return
57       * {@link com.atlassian.user.Group} objects in the Pager, they are retrieved using {@link #getGroup(String)}. This
58       * should be relatively fast for subsequent lookups because this method also uses a cache.
59       *
60       * @param user the user whose groups will be retrieved. Must not be null.
61       * @return a Pager containing all the groups in the underlying group manager which have this user
62       * as a member. Each item in the list is an instance of the {@link com.atlassian.user.Group} type used by the underlying manager.
63       * Returns an empty pager if no such groups exist.
64       * @throws com.atlassian.user.EntityException if there is an error retrieving the groups.
65       */
66      public Pager<Group> getGroups(User user) throws EntityException
67      {
68          if (user == null)
69              throw new IllegalArgumentException("User cannot be null.");
70          if (log.isInfoEnabled())
71              log.info("Retrieving groups for user [" + user.getName() + "]");
72  
73          List<String> groupNames = groupsForUserCache.get(user);
74  
75          if (groupNames != null)
76          {
77              if (log.isDebugEnabled())
78                  log.debug("Cache hit. Returning pager with " + groupNames.size() + " items.");
79  
80              List<Group> groups = new LinkedList<Group>();
81  
82              for (Object groupName1 : groupNames)
83              {
84                  groups.add(getGroup((String) groupName1));
85              }
86              return new DefaultPager<Group>(groups);
87          }
88  
89          if (log.isDebugEnabled())
90              log.debug("Cache miss. Retrieving groups from underlying group manager.");
91  
92          List<Group> groups = new LinkedList<Group>();
93          groupNames = new LinkedList<String>();
94  
95          for (Group group : underlyingGroupManager.getGroups(user))
96          {
97              groups.add(group);
98              groupCache.put(group.getName(), group);
99              groupNames.add(group.getName());
100         }
101 
102         if (log.isDebugEnabled())
103             log.debug("Retrieved " + groupNames.size() + " groups for user [" + user + "], putting in cache.");
104 
105         groupsForUserCache.put(user, groupNames);
106 
107         return new DefaultPager<Group>(groups);
108     }
109 
110     public List<Group> getWritableGroups()
111     {
112         return underlyingGroupManager.getWritableGroups();
113     }
114 
115     public Group getGroup(String groupName) throws EntityException
116     {
117         Group cachedGroup = groupCache.get(groupName);
118         if (cachedGroup != null)
119         {
120             return GroupCache.NULL_GROUP.equals(cachedGroup) ? null : cachedGroup;
121         }
122         else
123         {
124             Group group = underlyingGroupManager.getGroup(groupName);
125             groupCache.put(groupName, group);
126             return group;
127         }
128     }
129 
130     public Group createGroup(String groupName) throws EntityException
131     {
132         Group createdGroup = underlyingGroupManager.createGroup(groupName);
133 
134         if (createdGroup != null)
135             groupCache.put(createdGroup.getName(), createdGroup);
136 
137         return createdGroup;
138     }
139 
140     public void removeGroup(Group group) throws EntityException
141     {
142         List<String> memberNames = PagerUtils.toList(getMemberNames(group));
143         underlyingGroupManager.removeGroup(group);
144         groupCache.remove(group.getName());
145         groupsForUserCache.remove(memberNames);
146         membershipCache.remove(memberNames, group);
147         entityRepositoryCache.remove(group);
148     }
149 
150     public void addMembership(Group group, User user) throws EntityException
151     {
152         underlyingGroupManager.addMembership(group, user);
153         membershipCache.put(user, group, true);
154         groupsForUserCache.remove(user);
155     }
156 
157     public boolean hasMembership(Group group, User user) throws EntityException
158     {
159         if(group==null)
160             return false;
161         
162         Boolean membershipCheckElement = membershipCache.get(user, group);
163 
164         if (membershipCheckElement != null)
165         {
166             return membershipCheckElement;
167         }
168         else
169         {
170             boolean isMember = underlyingGroupManager.hasMembership(group, user);
171             membershipCache.put(user, group, isMember);
172             return isMember;
173         }
174     }
175 
176     public void removeMembership(Group group, User user) throws EntityException
177     {
178         underlyingGroupManager.removeMembership(group, user);
179         membershipCache.remove(user, group);
180         groupsForUserCache.remove(user);
181     }
182 
183     public RepositoryIdentifier getRepository(Entity entity) throws EntityException
184     {
185         RepositoryIdentifier cachedRepository = entityRepositoryCache.get(entity);
186         if (cachedRepository != null)
187             return cachedRepository;
188 
189         RepositoryIdentifier repository = underlyingGroupManager.getRepository(entity);
190         entityRepositoryCache.put(entity, repository);
191         return repository;
192     }
193 
194     public Pager<Group> getGroups() throws EntityException
195     {
196         return underlyingGroupManager.getGroups();
197     }
198 
199     public Pager<String> getMemberNames(Group group) throws EntityException
200     {
201         return underlyingGroupManager.getMemberNames(group);
202     }
203 
204     public Pager<String> getLocalMemberNames(Group group) throws EntityException
205     {
206         return underlyingGroupManager.getLocalMemberNames(group);
207     }
208 
209     public Pager<String> getExternalMemberNames(Group group) throws EntityException
210     {
211         return underlyingGroupManager.getExternalMemberNames(group);
212     }
213 
214     public boolean supportsExternalMembership() throws EntityException
215     {
216         return underlyingGroupManager.supportsExternalMembership();
217     }
218 
219     public boolean isReadOnly(Group group) throws EntityException
220     {
221         return underlyingGroupManager.isReadOnly(group);
222     }
223 
224     public RepositoryIdentifier getIdentifier()
225     {
226         return underlyingGroupManager.getIdentifier();
227     }
228 
229     public boolean isCreative()
230     {
231         return underlyingGroupManager.isCreative();
232     }
233 
234     /**
235      * To be called once the underlyingGroupManager and cacheManager have been set up by a constructor.
236      */
237     private void initialiseCaches()
238     {
239         entityRepositoryCache = new EntityRepositoryCache(cacheFactory, getCacheKey("repositories"));
240         groupCache = new GroupCache(cacheFactory, getCacheKey("groups"));
241         membershipCache = new MembershipCache(cacheFactory, getCacheKey("groups_hasMembership"));
242         groupsForUserCache = new GroupsForUserCache(cacheFactory, getCacheKey("groups_getGroupsForUser"));
243     }
244 
245     /**
246      * Generates a unique cache key. This cache key should not be shared by other instances of CachingGroupManager that delegate to different underlying group managers.
247      */
248     private String getCacheKey(String cacheName)
249     {
250         String className = underlyingGroupManager.getClass().getName();
251         String repositoryKey = underlyingGroupManager.getIdentifier().getKey(); // use the repository key to compose the cache key, so we can have a cache per (repository + groupManager) combination.
252         return className + "." + repositoryKey + "." + cacheName;
253     }
254 }