1   package com.atlassian.user.impl.cache;
2   
3   import com.atlassian.cache.Cache;
4   import com.atlassian.cache.CacheFactory;
5   import com.atlassian.user.Entity;
6   import com.atlassian.user.EntityException;
7   import com.atlassian.user.User;
8   import com.atlassian.user.UserManager;
9   import com.atlassian.user.impl.DefaultUser;
10  import com.atlassian.user.repository.RepositoryIdentifier;
11  import com.atlassian.user.search.page.Pager;
12  import com.atlassian.user.security.password.Credential;
13  import org.apache.log4j.Logger;
14  
15  public class CachingUserManager implements UserManager
16  {
17      private static final Logger log = (Logger) Logger.getLogger(CachingUserManager.class);
18  
19      private final UserManager underlyingUserManager;
20      private final CacheFactory cacheFactory;
21  
22      private String userCacheName = null;
23      private String userROCacheName = null;
24      private String repositoryCacheName = null;
25  
26      private static final String CACHE_SUFFIX_USERS = "users";
27      private static final String CACHE_SUFFIX_USERS_RO = "users_ro";
28      private static final String CACHE_SUFFIX_REPOSITORIES = "repository";
29  
30      protected static User NULL_USER = new DefaultUser()
31      {
32          public String toString()
33          {
34              return "NULL USER";
35          }
36      };
37  
38      public CachingUserManager(UserManager underlyingUserManager, CacheFactory cacheFactory)
39      {
40          this.underlyingUserManager = underlyingUserManager;
41          this.cacheFactory = cacheFactory;
42      }
43  
44      public Pager<User> getUsers() throws EntityException
45      {
46          return underlyingUserManager.getUsers();
47      }
48  
49      public Pager<String> getUserNames() throws EntityException
50      {
51          return underlyingUserManager.getUserNames();
52      }
53  
54      /**
55       * Caches users retrieved.<br>
56       * Will also cache the fact that a user could not be found (so that we don't incur the expense of another search when have previously determined that a user doesn't exist)
57       * Hence this method will also cache null results.
58       *
59       * @return - an {@link com.atlassian.user.User} if one could be found, otherwise null.
60       * @throws com.atlassian.user.EntityException
61       *          - representing the exception which prohibited looking for or
62       *          retrieving the user.
63       */
64      public User getUser(String username) throws EntityException
65      {
66          User cachedUser = (User) getUserCache().get(username);
67          if (cachedUser != null)
68          {
69              return NULL_USER.equals(cachedUser) ? null : cachedUser;
70          }
71          else
72          {
73              User user = underlyingUserManager.getUser(username);
74              cacheUser(username, user);
75              return user;
76          }
77      }
78  
79      private void cacheUser(String username, User user)
80      {
81          getUserCache().put(username, user == null ? NULL_USER : user);
82      }
83  
84      private void cacheRepository(String username, RepositoryIdentifier repository)
85      {
86          getRepositoryCache().put(username, repository);
87      }
88  
89      private void cacheUserROFlag(User user, boolean ro)
90      {
91          getUserROFlagCache().put(user.getName(), ro);
92      }
93  
94      private Cache getUserCache()
95      {
96          synchronized(this)
97          {
98              if (userCacheName == null)
99                  userCacheName = getCacheKey(CACHE_SUFFIX_USERS);
100         }
101         return cacheFactory.getCache(userCacheName);
102     }
103 
104     private Cache getUserROFlagCache()
105     {
106         synchronized(this)
107         {
108             if (userROCacheName == null)
109                 userROCacheName = getCacheKey(CACHE_SUFFIX_USERS_RO);
110         }
111         return cacheFactory.getCache(userROCacheName);
112     }
113 
114     private Cache getRepositoryCache()
115     {
116         synchronized(this)
117         {
118             if (repositoryCacheName == null)
119                 repositoryCacheName = getCacheKey(CACHE_SUFFIX_REPOSITORIES);
120         }
121         return cacheFactory.getCache(repositoryCacheName);
122     }
123 
124     public User createUser(String username) throws EntityException
125     {
126         User user = underlyingUserManager.createUser(username);
127 
128         if (user != null)
129             cacheUser(user.getName(), user);
130 
131         return user;
132     }
133 
134     public User createUser(User userTemplate, Credential credential) throws EntityException
135     {
136         User user = underlyingUserManager.createUser(userTemplate, credential);
137 
138         if (user != null)
139             cacheUser(user.getName(), user);
140 
141         return user;
142     }
143 
144     /**
145      * Encrypts the plain password, sets it on the user, and saves the user.
146      */
147     public void alterPassword(User user, String plainTextPass) throws EntityException
148     {
149         underlyingUserManager.alterPassword(user, plainTextPass);
150         if (user != null)
151             cacheUser(user.getName(), underlyingUserManager.getUser(user.getName()));
152     }
153 
154     public void saveUser(User user) throws EntityException
155     {
156         underlyingUserManager.saveUser(user);
157 
158         if (user != null)
159             cacheUser(user.getName(), user);
160     }
161 
162     /**
163      * Removes the specified group, if it is present.
164      */
165     public void removeUser(User user) throws EntityException
166     {
167         if (log.isDebugEnabled())
168             log.debug("removing user: " + user.getName());
169         underlyingUserManager.removeUser(user);
170         if (log.isDebugEnabled())
171             log.debug("user " + user.getName() + " removed from underlying user manager " + underlyingUserManager.getIdentifier().getName());
172 
173         try
174         {
175             if (log.isDebugEnabled())
176                 log.debug("removing user from cache: " + user.getName());
177             removeUserFromCache(user);
178             if (log.isDebugEnabled())
179             {
180                 log.debug("removed user from cache: " + user.getName());
181                 // Lets be paranoid
182                 // TODO delete it
183                 if (getUserCache().get(user.getName()) != null)
184                 {
185                     log.error("WTF???");    
186                 }
187             }
188         }
189         catch (Exception e)
190         {
191             throw new EntityException("User removed in underlying repository but could not remove from cache");
192         }
193     }
194 
195     private void removeUserFromCache(User user)
196     {
197         if (user != null)
198             getUserCache().remove(user.getName());
199     }
200 
201     /**
202      * @return true indicates that information on the user object cannot be altered in the storage system
203      *         (see {@link com.atlassian.user.repository.RepositoryIdentifier}),
204      *         false indicates that the storage system will save changes or that this {@link com.atlassian.user.UserManager} does not
205      *         know about the {@link com.atlassian.user.User}.
206      *
207      */
208     public boolean isReadOnly(User user) throws EntityException
209     {
210         Boolean cachedROFlag = (Boolean) getUserROFlagCache().get(user.getName());
211 
212         if (cachedROFlag == null)
213         {
214             boolean ro = underlyingUserManager.isReadOnly(user);
215             cacheUserROFlag(user, ro);
216             return ro;
217         }
218         else
219         {
220             return cachedROFlag;
221         }
222     }
223 
224     public RepositoryIdentifier getIdentifier()
225     {
226         return underlyingUserManager.getIdentifier();
227     }
228 
229     /**
230      * @return the {@link com.atlassian.user.repository.RepositoryIdentifier} in which the entity is stored, otherwise null.
231      */
232     public RepositoryIdentifier getRepository(Entity entity) throws EntityException
233     {
234         RepositoryIdentifier cachedRepository = (RepositoryIdentifier) getRepositoryCache().get(entity.getName());
235         if (cachedRepository != null)
236             return cachedRepository;
237 
238         RepositoryIdentifier repository = underlyingUserManager.getRepository(entity);
239         cacheRepository(entity.getName(), repository);
240         return repository;
241     }
242 
243     /**
244      * Used to detemine whether an entity can be created (eg, can call {@link com.atlassian.user.UserManager#createUser(String)} or
245      * {@link com.atlassian.user.GroupManager#createGroup(String)}
246      *
247      * @return true to indicate that {@link com.atlassian.user.Entity} objects can be created by this manager, or false to indicate
248      *         not.
249      */
250     public boolean isCreative()
251     {
252         return underlyingUserManager.isCreative();
253     }
254 
255     /**
256      * Generates a unique cache key. This cache key should not be shared by other instances of CachingUserManager that delegate to different underlying user managers.
257      * @return cache key
258      */
259     private String getCacheKey(String cacheName)
260     {
261         String className = underlyingUserManager.getClass().getName();
262         String repositoryKey = underlyingUserManager.getIdentifier().getKey(); // use the repository key to compose the cache key, so we can have a cache per (repository + userManager) combination.
263         return className + "." + repositoryKey + "." + cacheName;
264     }
265 
266 }