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 }