001    /*
002     * Copyright 2002-2004 the original author or authors.
003     *
004     * Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0
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     * Code obtained from http://opensource.atlassian.com/confluence/spring/display/DISC/Caching+the+result+of+methods+using+Spring+and+EHCache
017     * and modified for use within Kuali
018     */
019     
020    // begin Kuali Foundation modification
021    package org.kuali.rice.kns.util.cache;
022    
023    // Kuali Foundation modification: changed some imports
024    import java.io.Serializable;
025    import java.util.Collection;
026    import java.util.Collections;
027    import java.util.List;
028    import java.util.Map;
029    import java.util.Set;
030    import java.util.SortedMap;
031    import java.util.SortedSet;
032    
033    import org.aopalliance.intercept.MethodInterceptor;
034    import org.aopalliance.intercept.MethodInvocation;
035    import org.apache.log4j.Logger;
036    import org.springframework.beans.factory.InitializingBean;
037    import org.springframework.util.Assert;
038    
039    import com.opensymphony.oscache.base.Cache;
040    import com.opensymphony.oscache.base.NeedsRefreshException;
041    
042    /**
043     * begin Kuali Foundation modification
044     * This class implements org.aopalliance.intercept.MethodInterceptor. This interceptor builds the cache key for the method and
045     * checks if an earlier result was cached with that key. If so, the cached result is returned; otherwise, the intercepted method is
046     * called and the result cached for future use.
047     * end Kuali Foundation modification
048     * 
049     * @author Kuali Rice Team (rice.collab@kuali.org)
050     * @since 2004.10.07
051     */
052    public class MethodCacheNoCopyInterceptor implements MethodInterceptor, InitializingBean {
053        private static final Logger LOG = Logger.getLogger(MethodCacheNoCopyInterceptor.class);
054    
055        private Cache cache;
056        // begin Kuali Foundation modification
057        private int expirationTimeInSeconds = 1000;
058        // end Kuali Foundation modification
059    
060        /**
061         * begin Kuali Foundation modification
062         * @param cache name of cache to be used
063         * end Kuali Foundation modification
064         */
065        public void setCache(Cache cache) {
066            this.cache = cache;
067        }
068    
069        // begin Kuali Foundation modification
070        /**
071         * Entries older than this will have their contents replaced by the return value from a call to the appropriate method
072         * 
073         * @param expirationTimeInSeconds
074         */
075        public void setExpirationTimeInSeconds(int expirationTimeInSeconds) {
076            this.expirationTimeInSeconds = expirationTimeInSeconds;
077        }
078    
079        // end Kuali Foundation modification
080    
081    
082        /**
083         * Checks if required attributes are provided.
084         * 
085         * begin Kuali Foundation modification
086         * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
087         * end Kuali Foundation modification
088         */
089        public void afterPropertiesSet() throws Exception {
090            Assert.notNull(cache, "A cache is required. Use setCache(Cache) to provide one.");
091        }
092    
093        /**
094         * begin Kuali Foundation modification
095         * Caches method results, if possible.
096         * <p>
097         * Results must be Serializable to be cached. Method with unSerializable results will never have their results cached, and will
098         * log error messages complaining about that fact.
099         * 
100         * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
101         * end Kuali Foundation modification
102         */
103        @SuppressWarnings("unchecked")
104            public Object invoke(MethodInvocation invocation) throws Throwable {
105            // begin Kuali Foundation modification
106            boolean cancelUpdate = true;
107    
108            Object methodResult = null;
109            String cacheKey = buildCacheKey(invocation);
110    
111            // lookup result in cache
112            if (LOG.isDebugEnabled()) {
113                LOG.debug("looking for method result for invocation '" + cacheKey + "'");
114            }
115            try {
116                methodResult = cache.getFromCache(cacheKey, expirationTimeInSeconds);
117                if (LOG.isDebugEnabled()) {
118                    LOG.debug("using cached result invocation '" + cacheKey + "'");
119                }
120    
121                cancelUpdate = false;
122            }
123            catch (NeedsRefreshException e) {
124                // call intercepted method
125                try {
126                    if (LOG.isDebugEnabled()) {
127                        LOG.debug("calling intercepted method for invocation '" + cacheKey + "'");
128                    }
129                    methodResult = invocation.proceed();
130                }
131                catch (Exception invocationException) {
132                    LOG.warn("unable to cache methodResult: caught exception invoking intercepted method: '" + invocationException);
133                    throw invocationException;
134                }
135    
136                // cache method result, if possible
137                if ( methodResult == null || Serializable.class.isAssignableFrom(methodResult.getClass() ) ) {
138                    try {
139                        if (LOG.isDebugEnabled()) {
140                            LOG.debug("caching results for invocation '" + cacheKey + "'");
141                        }
142                        // ensure that items like lists/maps/collections are not modified once returned
143                        if ( methodResult != null ) {
144                            if ( methodResult instanceof SortedMap ) {
145                                    methodResult = Collections.unmodifiableSortedMap((SortedMap)methodResult);
146                            } else if ( methodResult instanceof Map ) {
147                                    methodResult = Collections.unmodifiableMap((Map)methodResult);
148                            } else if ( methodResult instanceof List ) {
149                                    methodResult = Collections.unmodifiableList((List)methodResult);
150                            } else if ( methodResult instanceof SortedSet ) {
151                                    methodResult = Collections.unmodifiableSortedSet((SortedSet)methodResult);
152                            } else if ( methodResult instanceof Set ) {
153                                    methodResult = Collections.unmodifiableSet((Set)methodResult);
154                            } else if ( methodResult instanceof Collection ) {
155                                    methodResult = Collections.unmodifiableCollection((Collection)methodResult);
156                            }
157                        }
158                        cache.putInCache(cacheKey, methodResult);
159        
160                        // adding, not updating
161                        cancelUpdate = false;
162                    } catch (Exception cacheException) {
163                        LOG.error("unable to cache methodResult: caught exception invoking putInCache: '" + cacheException);
164                        throw cacheException;
165                    }
166                }
167            }
168            finally {
169                // it is imperative that you call cancelUpdate if you aren't going to update the cache entry
170                if (cancelUpdate) {
171                    cache.cancelUpdate(cacheKey);
172                }
173            }
174    
175    
176            return methodResult;
177            // end Kuali Foundation modification
178        }
179    
180        // begin Kuali Foundation modification
181        /**
182         * @param invocation MethodInvocation being handled
183         * @return cache key: className.methodName(paramClass=argValue[,paramClass=argValue...])
184         */
185        private String buildCacheKey(MethodInvocation invocation) {
186            return buildCacheKey(invocation.getStaticPart().toString(), invocation.getArguments());
187        }
188    
189    
190        /**
191         * @param className
192         * @param methodName
193         * @param paramTypes
194         * @param argValues
195         * @return cache key: className.methodName(paramClass=argValue[,paramClass=argValue...])
196         */
197        public String buildCacheKey(String methodSignature, Object[] argValues) {
198            StringBuffer cacheKey = new StringBuffer(methodSignature);
199            cacheKey.append(": ");
200            if (argValues != null) {
201                for (int i = 0; i < argValues.length; i++) {
202                    if (i > 0) {
203                        cacheKey.append(",");
204                    }
205                    // handle weird cache bug:
206                    // if you call a one-arg method foo with a null arg i.e. foo(null),
207                    // and then call it with an argument whose toString evaluates to "null",
208                    // OSCache gets stuck in an infinite wait() call because it somehow thinks
209                    // another thread is already updating this cache entry
210                    //
211                    // workaround: change so that args which are actually null literal have
212                    // some weird, unlikely-to-be-encountered String representation
213                    if (argValues[i] == null) {
214                        cacheKey.append("<literal null>");
215                    }
216                    else {
217                        cacheKey.append(argValues[i]);
218                    }
219                }
220            }
221            return cacheKey.toString();
222        }
223    
224    
225        /**
226         * @param key
227         * @return true if the cache contains an entry with the given key
228         */
229        public boolean containsCacheKey(String key) {
230            boolean contains = false;
231    
232            try {
233                cache.getFromCache(key);
234                contains = true;
235            }
236            catch (NeedsRefreshException e) {
237                // it is imperative that you call cancelUpdate if you aren't going to update the cache entry that caused the
238                // NeedsRefreshException above
239                cache.cancelUpdate(key);
240                contains = false;
241            }
242    
243            return contains;
244        }
245        /** 
246         * Removes a method cache if one exists for the given key.
247         * @param cacheKey - key for method signature and parameters - see buildCacheKey
248         */
249        public void removeCacheKey(String cacheKey) {
250          if (!containsCacheKey(cacheKey)) {
251              return;
252          }
253          
254          if ( LOG.isDebugEnabled() ) {
255              LOG.debug("removing method cache for key: " + cacheKey);
256          }
257          cache.cancelUpdate(cacheKey);
258          cache.flushEntry(cacheKey);
259        }
260        
261        // Kuali Foundation modification: removed getCacheKey(String, String, Object[])
262        // end Kuali Foundation modification
263    }