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 }