/*
* Created on Nov 10, 2004
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* Copyright @2007 the original author or authors.
*/
package org.springmodules.cache.interceptor.caching;
import java.beans.PropertyEditor;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springmodules.cache.CachingModel;
import org.springmodules.cache.FatalCacheException;
import org.springmodules.cache.key.CacheKeyGenerator;
import org.springmodules.cache.key.HashCodeCacheKeyGenerator;
import org.springmodules.cache.provider.CacheModelValidator;
import org.springmodules.cache.provider.CacheProviderFacade;
/**
* Template for advises that store in a cache the return value of intercepted
* methods.
*
* @author Omar Irbouh
* @author Alex Ruiz
* @author Antony Stubbs
*/
public abstract class AbstractCachingInterceptor implements MethodInterceptor,
InitializingBean {
protected final Log logger = LogFactory.getLog(getClass());
public static final NullObject NULL_ENTRY = new NullObject();
protected CacheProviderFacade cache;
protected CacheKeyGenerator keyGenerator;
private CachingListener[] listeners;
private Map modelMap;
public final void afterPropertiesSet() throws FatalCacheException {
validateCache();
if (modelMap instanceof Properties)
setCachingModels(propertiesToModels());
validateModels();
if (keyGenerator == null) setCacheKeyGenerator(defaultKeyGenerator());
onAfterPropertiesSet();
}
public final CacheKeyGenerator cacheKeyGenerator() {
return keyGenerator;
}
public Object invoke(MethodInvocation mi) throws Throwable {
Method method = mi.getMethod();
if (!CachingUtils.isCacheable(method))
return methodNotCacheable(mi, method);
CachingModel model = model(mi);
if (model == null) return noModelFound(mi, method);
Serializable key = keyGenerator.generateKey(mi);
Object cached = cache.getFromCache(key, model);
if (null == cached) return cachedValueFromSource(mi, key, model);
return unmaskNull(cached);
}
public final void setCacheKeyGenerator(CacheKeyGenerator k) {
keyGenerator = k;
}
public final void setCacheProviderFacade(CacheProviderFacade c) {
cache = c;
}
public final void setCachingListeners(CachingListener[] l) {
listeners = l;
}
public final void setCachingModels(Map m) {
modelMap = m;
}
protected abstract CachingModel model(MethodInvocation mi);
protected final Map models() {
return modelMap;
}
protected void onAfterPropertiesSet() throws FatalCacheException {
// no implementation.
}
/**
* Takes care of looking up the caches value from the proxied method, and
* putting the return value into the cache.
*
* @param mi the proxied method to invoke
* @param key the key for the cache
* @param m the backing {@link CachingModel}
* @return the looked up value
* @throws Throwable
*/
protected Object cachedValueFromSource(MethodInvocation mi, Serializable key,
CachingModel m) throws Throwable {
boolean successful = true;
try {
Object value = mi.proceed();
putInCache(key, m, value);
return value;
} catch (Throwable t) {
successful = false;
logger.debug("method " + mi.getMethod().getName() + " throwed a exception", t);
throw t;
} finally {
if (!successful) cache.cancelCacheUpdate(key);
}
}
private CacheKeyGenerator defaultKeyGenerator() {
return new HashCodeCacheKeyGenerator(true);
}
private Object logAndProceed(String message, MethodInvocation mi)
throws Throwable {
logger.debug(message);
return mi.proceed();
}
private Object maskNull(Object o) {
return o != null ? o : NULL_ENTRY;
}
protected Object methodNotCacheable(MethodInvocation mi, Method m)
throws Throwable {
return logAndProceed("Unable to perform caching. Intercepted method <"
+ m + "> does not return a value", mi);
}
protected Object noModelFound(MethodInvocation mi, Method m) throws Throwable {
return logAndProceed("Unable to perform caching. "
+ "No model is associated to the method <" + m + ">", mi);
}
private void notifyListeners(Serializable key, Object cachedObject,
CachingModel m) {
if (ObjectUtils.isEmpty(listeners)) return;
for (int i = 0; i < listeners.length; i++)
listeners[i].onCaching(key, cachedObject, m);
}
private Map propertiesToModels() {
PropertyEditor editor = cache.getCachingModelEditor();
Properties properties = (Properties) modelMap;
Map m = new HashMap();
for (Iterator i = properties.keySet().iterator(); i.hasNext();) {
String id = (String) i.next();
editor.setAsText(properties.getProperty(id));
m.put(id, editor.getValue());
}
return m;
}
protected void putInCache(Serializable key, CachingModel m, Object o) {
cache.putInCache(key, m, maskNull(o));
notifyListeners(key, o, m);
}
protected Object unmaskNull(Object obj) {
return NULL_ENTRY.equals(obj) ? null : obj;
}
private void validateCache() throws FatalCacheException {
if (cache == null)
throw new FatalCacheException(
"The cache provider facade should not be null");
}
private void validateModels() throws FatalCacheException {
if (CollectionUtils.isEmpty(modelMap))
throw new FatalCacheException(
"The map of caching models should not be empty");
CacheModelValidator validator = cache.modelValidator();
String id = null;
try {
for (Iterator i = modelMap.keySet().iterator(); i.hasNext();) {
id = (String) i.next();
validator.validateCachingModel(modelMap.get(id));
}
} catch (Exception exception) {
throw new FatalCacheException("Unable to validate caching model with id "
+ StringUtils.quote(id), exception);
}
}
}