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 }