View Javadoc

1   /*
2    * Copyright 2002-2004 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   * 
16   * Code obtained from http://opensource.atlassian.com/confluence/spring/display/DISC/Caching+the+result+of+methods+using+Spring+and+EHCache
17   * and modified for use within Kuali
18   */
19   
20  // begin Kuali Foundation modification
21  package org.kuali.rice.kns.util.cache;
22  
23  // Kuali Foundation modification: changed some imports
24  import java.io.Serializable;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  import java.util.SortedMap;
31  import java.util.SortedSet;
32  
33  import org.aopalliance.intercept.MethodInterceptor;
34  import org.aopalliance.intercept.MethodInvocation;
35  import org.apache.log4j.Logger;
36  import org.springframework.beans.factory.InitializingBean;
37  import org.springframework.util.Assert;
38  
39  import com.opensymphony.oscache.base.Cache;
40  import com.opensymphony.oscache.base.NeedsRefreshException;
41  
42  /**
43   * begin Kuali Foundation modification
44   * This class implements org.aopalliance.intercept.MethodInterceptor. This interceptor builds the cache key for the method and
45   * checks if an earlier result was cached with that key. If so, the cached result is returned; otherwise, the intercepted method is
46   * called and the result cached for future use.
47   * end Kuali Foundation modification
48   * 
49   * @author Kuali Rice Team (rice.collab@kuali.org)
50   * @since 2004.10.07
51   */
52  public class MethodCacheNoCopyInterceptor implements MethodInterceptor, InitializingBean {
53      private static final Logger LOG = Logger.getLogger(MethodCacheNoCopyInterceptor.class);
54  
55      private Cache cache;
56      // begin Kuali Foundation modification
57      private int expirationTimeInSeconds = 1000;
58      // end Kuali Foundation modification
59  
60      /**
61       * begin Kuali Foundation modification
62       * @param cache name of cache to be used
63       * end Kuali Foundation modification
64       */
65      public void setCache(Cache cache) {
66          this.cache = cache;
67      }
68  
69      // begin Kuali Foundation modification
70      /**
71       * Entries older than this will have their contents replaced by the return value from a call to the appropriate method
72       * 
73       * @param expirationTimeInSeconds
74       */
75      public void setExpirationTimeInSeconds(int expirationTimeInSeconds) {
76          this.expirationTimeInSeconds = expirationTimeInSeconds;
77      }
78  
79      // end Kuali Foundation modification
80  
81  
82      /**
83       * Checks if required attributes are provided.
84       * 
85       * begin Kuali Foundation modification
86       * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
87       * end Kuali Foundation modification
88       */
89      public void afterPropertiesSet() throws Exception {
90          Assert.notNull(cache, "A cache is required. Use setCache(Cache) to provide one.");
91      }
92  
93      /**
94       * begin Kuali Foundation modification
95       * Caches method results, if possible.
96       * <p>
97       * Results must be Serializable to be cached. Method with unSerializable results will never have their results cached, and will
98       * log error messages complaining about that fact.
99       * 
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 }