001 /*
002 * Copyright 2007-2009 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.rice.kew.useroptions;
017
018 import java.lang.ref.SoftReference;
019 import java.util.Collection;
020 import java.util.List;
021 import java.util.Map;
022
023 import org.kuali.rice.ksb.api.KsbApiServiceLocator;
024
025 /**
026 * This class decorates a UserOptionsService and provides distributed caching for findByOptionId calls, and
027 * intelligently clears cache entries on update/delete type calls.
028 *
029 * @author Kuali Rice Team (rice.collab@kuali.org)
030 *
031 */
032 public class UserOptionsServiceCache implements UserOptionsService {
033
034 private UserOptionsService inner;
035
036 private final ClusterSafeMethodCache cache = new ClusterSafeMethodCache(UserOptionsService.class.getSimpleName());
037
038 /**
039 * clears any entry in the cache for this option id
040 * @see org.kuali.rice.kew.useroptions.UserOptionsService#deleteUserOptions(org.kuali.rice.kew.useroptions.UserOptions)
041 */
042 public void deleteUserOptions(UserOptions userOptions) {
043 if (userOptions != null && userOptions.getWorkflowId() != null) {
044 cache.clearCacheEntry(userOptions.getWorkflowId(), "findByOptionId", userOptions.getOptionId(), userOptions.getWorkflowId());
045 }
046 inner.deleteUserOptions(userOptions);
047 }
048
049 /**
050 * Checks the cache for results. On cache miss, delegates the call and caches the result before returning.
051 * @see org.kuali.rice.kew.useroptions.UserOptionsService#findByOptionId(java.lang.String, java.lang.String)
052 */
053 public UserOptions findByOptionId(String optionId, String principalId) {
054 Object result = null;
055 if (optionId != null && principalId != null) {
056 String methodName = "findByOptionId";
057 Object [] arguments = {optionId, principalId};
058
059 result = cache.getFromCache(principalId, methodName, arguments);
060 if (result == null) {
061 result = inner.findByOptionId(optionId, principalId);
062 cache.putInCache(principalId, methodName, (result != null) ? result : cache.NULL_OBJECT, arguments);
063 }
064 } else {
065 // just delegate on bad inputs:
066 result = inner.findByOptionId(optionId, principalId);
067 }
068
069 return (result == cache.NULL_OBJECT) ? null : (UserOptions) result;
070 }
071
072 /**
073 * This overridden method delegates the call.
074 * @see org.kuali.rice.kew.useroptions.UserOptionsService#findByOptionValue(java.lang.String, java.lang.String)
075 */
076 public Collection<UserOptions> findByOptionValue(String optionId,
077 String optionValue) {
078 // no caching for this method since it doesn't discriminate by user
079 return inner.findByOptionValue(optionId, optionValue);
080 }
081
082 /**
083 * This overridden method delegates the call.
084 * @see org.kuali.rice.kew.useroptions.UserOptionsService#findByUserQualified(java.lang.String, java.lang.String)
085 */
086 public List<UserOptions> findByUserQualified(String principalId,
087 String likeString) {
088 // no caching for this method since it could be pulling in saveRefreshUserOption generated UserOptions
089 return inner.findByUserQualified(principalId, likeString);
090 }
091
092 /**
093 * This overridden method delegates the call.
094 * @see org.kuali.rice.kew.useroptions.UserOptionsService#findByWorkflowUser(java.lang.String)
095 */
096 public Collection<UserOptions> findByWorkflowUser(String principalId) {
097 // no caching for this method since it could be pulling in saveRefreshUserOption generated UserOptions
098 return inner.findByWorkflowUser(principalId);
099 }
100
101 /**
102 * This overridden method ...
103 * @see org.kuali.rice.kew.useroptions.UserOptionsService#refreshActionList(java.lang.String)
104 */
105 public boolean refreshActionList(String principalId) {
106 // ignore the cache for this user option, as these are generated with unique numbers and shouldn't be
107 // retrieved more then once, as they are deleted once retrieved
108 return inner.refreshActionList(principalId);
109 }
110
111 /**
112 * This overridden method ...
113 *
114 * @see org.kuali.rice.kew.useroptions.UserOptionsService#save(java.lang.String, java.util.Map)
115 */
116 public void save(String principalId, Map<String, String> optionsMap) {
117 inner.save(principalId, optionsMap);
118 cache.clearCacheGroup(principalId);
119 }
120
121 /**
122 * This overridden method ...
123 *
124 * @see org.kuali.rice.kew.useroptions.UserOptionsService#save(java.lang.String, java.lang.String, java.lang.String)
125 */
126 public void save(String principalId, String optionId, String optionValue) {
127 inner.save(principalId, optionId, optionValue);
128 cache.clearCacheGroup(principalId);
129 }
130
131 /**
132 * This overridden method ...
133 *
134 * @see org.kuali.rice.kew.useroptions.UserOptionsService#save(org.kuali.rice.kew.useroptions.UserOptions)
135 */
136 public void save(UserOptions userOptions) {
137 inner.save(userOptions);
138 cache.clearCacheGroup(userOptions.getWorkflowId());
139 }
140
141 /**
142 * This overridden method ...
143 *
144 * @see org.kuali.rice.kew.useroptions.UserOptionsService#saveRefreshUserOption(java.lang.String)
145 */
146 public void saveRefreshUserOption(String principalId) {
147 // this shouldn't impact our simple cache since we're only caching single options queries, and this will
148 // generate a key that hasn't been used previously
149 inner.saveRefreshUserOption(principalId);
150 }
151
152 /**
153 * @param inner the decorated service
154 */
155 public void setInnerService(UserOptionsService inner) {
156 this.inner = inner;
157 }
158
159 /**
160 * a helper class to encapsulate cluster safe cache functionality
161 *
162 * @author Kuali Rice Team (rice.collab@kuali.org)
163 *
164 */
165 private static class ClusterSafeMethodCache {
166
167 public static final Object NULL_OBJECT = new Object();
168 public final String cachedServiceName;
169
170 /**
171 * This constructs a ClusterSaveMethodCache
172 *
173 * @param cachedServiceName a name for the service being cached
174 */
175 public ClusterSafeMethodCache(String cachedServiceName) {
176 this.cachedServiceName = cachedServiceName;
177 }
178
179 /**
180 * build a cache key based on the user, method name and the parameters
181 *
182 * @param methodName
183 * @param args
184 * @return
185 */
186 private String getCacheKey(String principalId, String methodName, Object ... args) {
187
188 StringBuilder sb = new StringBuilder(principalId);
189 sb.append("/");
190 sb.append(cachedServiceName);
191 sb.append(".");
192 sb.append(methodName);
193 sb.append(":");
194 boolean first = true;
195 for (Object arg : args) {
196 if (first) {
197 first = false;
198 } else {
199 sb.append(",");
200 }
201 sb.append((arg == null) ? "null" : arg.toString());
202 }
203
204 return sb.toString();
205 }
206
207 /**
208 * caches a method result
209 *
210 * @param principalId the principal for whom the call is being cached
211 * @param methodName the name of the method whose result is being cached
212 * @param value
213 * @param keySource the parameters to the method call that is being cached. These are used to build the cache key, so order is important.
214 */
215 @SuppressWarnings("unchecked")
216 public void putInCache(String principalId, String methodName, Object value, Object ... keySource) {
217 if (value != null) {
218 KsbApiServiceLocator.getCacheAdministrator().putInCache(getCacheKey(principalId, methodName, keySource), new SoftReference(value), getCacheGroup(principalId));
219 } else {
220 KsbApiServiceLocator.getCacheAdministrator().putInCache(getCacheKey(principalId, methodName, keySource), NULL_OBJECT, getCacheGroup(principalId));
221 }
222 }
223
224 /**
225 * retrieves a method result from the cache. If the static final NULL_OBJECT is returned then the cached result
226 * was null.
227 *
228 * @param principalId the principal for whom the call is being cached
229 * @param methodName the name of the method whose result is being cached
230 * @param keySource the parameters to the method call that is being cached. These are used to build the cache key, so order is important.
231 * @return
232 */
233 @SuppressWarnings("unchecked")
234 public Object getFromCache(String principalId, String methodName, Object ... keySource) {
235 Object result = null;
236 Object cacheReturned = KsbApiServiceLocator.getCacheAdministrator().getFromCache(getCacheKey(principalId, methodName, keySource));
237 if (cacheReturned == NULL_OBJECT) {
238 result = cacheReturned;
239 } else {
240 SoftReference reference = (SoftReference)cacheReturned;
241 if (reference != null) {
242 result = reference.get();
243 }
244 }
245 return result;
246 }
247
248 /**
249 * This method clears all cached calls for the given principal
250 *
251 * @param principalId the principal for whom the cache is being cleared
252 */
253 public void clearCacheGroup(String principalId) {
254 if (principalId != null) {
255 KsbApiServiceLocator.getCacheAdministrator().flushGroup(getCacheGroup(principalId));
256 }
257 }
258
259 /**
260 * This method clears a cache entry for a given method call / arguments combination
261 *
262 * @param principalId the principal for whom the cache entry is being cleared
263 * @param methodName the name of the method whose result is being cached
264 * @param keySource the parameters to the method call that is being cached. These are used to build the cache key, so order is important.
265 */
266 public void clearCacheEntry(String principalId, String methodName, Object ... keySource) {
267 KsbApiServiceLocator.getCacheAdministrator().flushEntry(getCacheKey(principalId, methodName, keySource));
268 }
269
270 /**
271 * This method gets the cache group name (an entire cache group can be cleared at once)
272 *
273 * @param principalId the principal whose cache group name is being generated
274 * @return the cache group name
275 */
276 private String getCacheGroup(String principalId) {
277 return principalId + "/" + cachedServiceName;
278 }
279 }
280
281 }