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    }