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.util;
017    
018    import java.lang.ref.SoftReference;
019    import java.util.HashSet;
020    import java.util.Set;
021    
022    import org.aopalliance.intercept.MethodInterceptor;
023    import org.aopalliance.intercept.MethodInvocation;
024    import org.apache.commons.logging.Log;
025    import org.apache.commons.logging.LogFactory;
026    import org.kuali.rice.kew.service.KEWServiceLocator;
027    
028    /**
029     * <p>This is a generic caching proxy for KEW, using the cache administrator service to store results.  It's inteded
030     * for caching query results for a DAO, but it likely has broader use.
031     * <p>It will cache the results of calls matching method names which are set in {@link #setCacheForMethods}.  
032     * It will clear this "cache group" for calls with matching method names set in {@link #setClearCacheOnMethods}.
033     * <p>The cache group name (see {@link #setCacheGroupName(String)}) in most instances should unique for each class that 
034     * is proxied.
035     * <p>Configuration should be done through Spring, probably using BeanNameAutoProxyCreator.
036     * <p>NOTE: One important assumption here is that the arguments for the methods being cached will all have 
037     * {@link Object#toString()} implementations such that o1.equals(o2) iff o1.toString().equals(o2.toString()).
038     * @author Kuali Rice Team (rice.collab@kuali.org)
039     */
040    public class CachingInterceptor implements MethodInterceptor {
041            
042            private static Log LOG = LogFactory.getLog(CachingInterceptor.class);
043            
044            private Set<String> clearCacheOnMethods = new HashSet<String>(50);
045            private Set<String> cacheForMethods = new HashSet<String>(50);
046            private String cacheGroupName = "CachingInterceptor";
047            
048            private static final Object NULL_OBJECT = new Object();
049            
050            public void setClearCacheOnMethods(String [] methods) {
051                    for (String method : methods) {
052                            clearCacheOnMethods.add(method);
053                    }
054            }
055            
056            public void setCacheForMethods(String [] methods) {
057                    for (String method : methods) {
058                            cacheForMethods.add(method);
059                    }
060            }
061            
062            public void setCacheGroupName(String name) {
063                    cacheGroupName = name;
064            }
065            
066            /**
067             * This overridden method intercepts calls, and
068             * <ul>
069             * <li>if the intercepted method is in clearCacheOnMethods
070             * <ul><li>the cache is purged, then the method is called and any results returned.</ul>
071             * <li>if the intercepted method is in cacheForMethods
072             * <ul><li>the cache is checked.  On a hit, cached results are returned.  On a miss, the invocation
073             * occurrs and the results are cached.</ul>
074             * <li>otherwise
075             * <ul><li>the method is called and any results returned</ul>
076             * </ul> 
077             * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
078             */
079            @SuppressWarnings("unchecked")
080            public Object invoke(MethodInvocation invocation) throws Throwable {
081    
082                    Object results = null;
083                    
084                    String methodName = invocation.getMethod().getName();
085    
086                    if (clearCacheOnMethods.contains(methodName)) {
087                            if (LOG.isTraceEnabled()) { LOG.trace("clearing cache group " +cacheGroupName+" on " + methodName); }
088                            // clear the cache
089                            KEWServiceLocator.getCacheAdministrator().flushGroup(cacheGroupName);
090                            results = invocation.proceed();
091    
092                    } else if (cacheForMethods.contains(methodName)) {
093                            boolean gotCachedResults = false;
094                            // attempt to use the cache;
095                            String cacheKey = getCacheKey(methodName, invocation.getArguments());
096                            if (cacheKey != null) {
097                                    SoftReference<Object> resultRef = 
098                                            (SoftReference<Object>) KEWServiceLocator.getCacheAdministrator().getFromCache(cacheKey);
099                                    
100                                    if (resultRef != null) { results = resultRef.get(); }
101                                    if (null != results) {
102                                            if (LOG.isTraceEnabled()) { LOG.trace("cache hit in group " +cacheGroupName+" on " + methodName); }
103                                            gotCachedResults = true;
104                                    }
105                            }
106                            
107                            if (!gotCachedResults) {
108                                    if (LOG.isTraceEnabled()) { LOG.trace("cache miss in group " +cacheGroupName+" on " + methodName); }
109    
110                                    results = invocation.proceed();
111                                    if (results == null) { results = NULL_OBJECT; /* we'll cache a null result too */ }
112            
113                                    // add to the cache
114                                    if (cacheKey != null) {
115                                            KEWServiceLocator.getCacheAdministrator().
116                                                putInCache(cacheKey, new SoftReference<Object>(results), cacheGroupName);
117                                    }
118                            }
119                    } else {
120                            if (LOG.isTraceEnabled()) { LOG.trace("no caching enabled in group " +cacheGroupName+" on " + methodName); }
121                            results = invocation.proceed();
122                    }
123                    
124                    return (NULL_OBJECT == results) ? null : results;
125            }
126    
127            /**
128             * build a cache key based on the method name and the parameters
129             * 
130             * @param methodName
131             * @param args
132             * @return
133             */
134            private String getCacheKey(String methodName, Object [] args) {
135                    StringBuilder sb = new StringBuilder(cacheGroupName);
136                    sb.append(".");
137                    sb.append(methodName);
138                    sb.append(":");
139                    boolean first = true;
140                    for (Object arg : args) {
141                            if (first) {
142                                    first = false;
143                            } else {
144                                    sb.append(",");
145                            }
146                            sb.append((arg == null) ? "null" : arg.toString());
147                    }
148                    
149                    return sb.toString();
150            }
151    }