/** * Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org> * * 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. */ package org.onebusaway.container.cache; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import net.sf.ehcache.Cache; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.onebusaway.collections.PropertyPathExpression; import org.springframework.stereotype.Component; /** * Support class that determines the caching policy for a particular method by * producing a {@link CacheableMethodKeyFactory} for that method. We make use of * {@link Cacheable}, {@link CacheableArgument}, and {@link CacheableKey} * annotations to allow customization of the default key factory behavior, as * well as directly setting behavior using methods such as * {@link #setCacheKeyFactories(Map)} and * {@link #putCacheRefreshIndicatorArgumentIndexForMethod(Method, int)}. * * @author bdferris * @see CacheableMethodKeyFactory * @see Cacheable * @see CacheableArgument * @see CacheableKey */ @Component public class CacheableMethodKeyFactoryManager { private Map<Class<?>, CacheableObjectKeyFactory> _keyFactories = new HashMap<Class<?>, CacheableObjectKeyFactory>(); private Map<Method, Integer> _cacheRefreshIndicatorArgumentIndexByMethod = new HashMap<Method, Integer>(); public void addCacheableObjectKeyFactory(Class<?> className, CacheableObjectKeyFactory keyFactory) { _keyFactories.put(className, keyFactory); } public void setCacheKeyFactories(Map<Object, Object> keyFactories) { for (Map.Entry<Object, Object> entry : keyFactories.entrySet()) { Class<?> className = getObjectAsClass(entry.getKey()); CacheableObjectKeyFactory keyFactory = getObjectAsObjectKeyFactory(entry.getValue()); addCacheableObjectKeyFactory(className, keyFactory); } } public void putCacheRefreshIndicatorArgumentIndexForMethodSignature( String methodName, int argumentIndex) { Method method = getMethodForSignature(methodName); _cacheRefreshIndicatorArgumentIndexByMethod.put(method, argumentIndex); } public void putCacheRefreshIndicatorArgumentIndexForMethod(Method method, int argumentIndex) { _cacheRefreshIndicatorArgumentIndexByMethod.put(method, argumentIndex); } public CacheableMethodKeyFactory getCacheableMethodKeyFactoryForJoinPoint( ProceedingJoinPoint pjp, Method method) { return getCacheableMethodKeyFactoryForMethod(method); } public CacheableMethodKeyFactory getCacheableMethodKeyFactoryForMethod( Method m) { /** * Try looking for a @Cacheable annotation first and using that first for * the CacheableMethodKeyFactory if specified */ Cacheable cacheableAnnotation = m.getAnnotation(Cacheable.class); if (cacheableAnnotation != null) { Class<? extends CacheableMethodKeyFactory> keyFactoryType = cacheableAnnotation.keyFactory(); if (!keyFactoryType.equals(CacheableMethodKeyFactory.class)) try { return keyFactoryType.newInstance(); } catch (Exception ex) { throw new IllegalStateException( "error instantiating CacheableKeyFactory: " + keyFactoryType.getName(), ex); } } /** * Revert to default cacheable method key factory behavior */ Class<?>[] parameters = m.getParameterTypes(); Annotation[][] annotations = m.getParameterAnnotations(); int cacheRefreshParameterIndex = -1; if (_cacheRefreshIndicatorArgumentIndexByMethod.containsKey(m)) cacheRefreshParameterIndex = _cacheRefreshIndicatorArgumentIndexByMethod.get(m); CacheableObjectKeyFactory[] keyFactories = new CacheableObjectKeyFactory[parameters.length]; for (int i = 0; i < parameters.length; i++) { boolean cacheRefreshIndicator = (i == cacheRefreshParameterIndex); CacheableArgument cacheableArgumentAnnotation = getCacheableArgumentAnnotation(annotations[i]); if (cacheableArgumentAnnotation != null) { keyFactories[i] = getKeyFactoryForCacheableArgumentAnnotation( parameters[i], cacheableArgumentAnnotation, cacheRefreshIndicator); } else { keyFactories[i] = getKeyFactoryForParameterType(parameters[i], cacheRefreshIndicator); } } return new DefaultCacheableKeyFactory(keyFactories); } public Method getMatchingMethodForJoinPoint(ProceedingJoinPoint pjp) { List<Method> methods = getMatchingMethodsForJoinPoint(pjp); if (methods.size() == 1) { return methods.get(0); } else if (methods.size() == 0) { throw new IllegalArgumentException("method not found: pjp=" + pjp.getSignature()); } else { throw new IllegalArgumentException("multiple methods found: pjp=" + pjp.getSignature()); } } public List<Method> getMatchingMethodsForJoinPoint(ProceedingJoinPoint pjp) { Signature sig = pjp.getSignature(); Object target = pjp.getTarget(); Class<?> type = target.getClass(); List<Method> matches = new ArrayList<Method>(); for (Method m : type.getDeclaredMethods()) { if (!m.getName().equals(sig.getName())) continue; // if (m.getModifiers() != sig.getModifiers()) // continue; Object[] args = pjp.getArgs(); Class<?>[] types = m.getParameterTypes(); if (args.length != types.length) continue; boolean miss = false; for (int i = 0; i < args.length; i++) { Object arg = args[i]; Class<?> argType = types[i]; if (argType.isPrimitive()) { if (argType.equals(Double.TYPE) && !arg.getClass().equals(Double.class)) miss = true; } else { if (arg != null && !argType.isInstance(arg)) miss = true; } } if (miss) continue; matches.add(m); } return matches; } /*************************************************************************** * Private Methods **************************************************************************/ protected CacheableArgument getCacheableArgumentAnnotation( Annotation[] annotations) { for (Annotation annotation : annotations) { if (annotation instanceof CacheableArgument) return (CacheableArgument) annotation; } return null; } protected CacheableObjectKeyFactory getKeyFactoryForCacheableArgumentAnnotation( Class<?> type, CacheableArgument cacheableArgumentAnnotation, boolean cacheRefreshIndicator) { String keyProperty = cacheableArgumentAnnotation.keyProperty(); cacheRefreshIndicator |= cacheableArgumentAnnotation.cacheRefreshIndicator(); if (!(keyProperty == null || keyProperty.equals(""))) { PropertyPathExpression expression = new PropertyPathExpression( keyProperty); type = expression.initialize(type); CacheableObjectKeyFactory factory = getKeyFactoryForParameterType(type, cacheRefreshIndicator); return new PropertyPathExpressionCacheableObjectKeyFactory(expression, factory); } // Nothing interesting defined in the annotation? Apply the default behavior return getKeyFactoryForParameterType(type, cacheRefreshIndicator); } protected CacheableObjectKeyFactory getKeyFactoryForParameterType( Class<?> type, boolean cacheRefreshIndicator) { if (_keyFactories.containsKey(type)) return _keyFactories.get(type); for (Map.Entry<Class<?>, CacheableObjectKeyFactory> entry : _keyFactories.entrySet()) { Class<?> argumentType = entry.getKey(); if (argumentType.isAssignableFrom(type)) return entry.getValue(); } Class<?> checkType = type; while (checkType != null && !checkType.equals(Object.class)) { CacheableKey annotation = checkType.getAnnotation(CacheableKey.class); if (annotation != null) { Class<? extends CacheableObjectKeyFactory> keyFactoryType = annotation.keyFactory(); try { return keyFactoryType.newInstance(); } catch (Exception ex) { throw new IllegalStateException( "error instantiating CacheableObjectKeyFactory [type=" + keyFactoryType.getName() + "] from CacheableKey [type=" + checkType.getName() + "]", ex); } } checkType = type.getSuperclass(); } DefaultCacheableObjectKeyFactory factory = new DefaultCacheableObjectKeyFactory(); factory.setCacheRefreshCheck(cacheRefreshIndicator); return factory; } protected Cache createCache(ProceedingJoinPoint pjp, String name) { return null; } /**** * Private Methods ****/ private Class<?> getObjectAsClass(Object object) { if (object instanceof Class<?>) return (Class<?>) object; if (object instanceof String) { try { return Class.forName((String) object); } catch (ClassNotFoundException e) { throw new IllegalArgumentException(e); } } throw new IllegalArgumentException("unable to convert object to class: " + object); } private CacheableObjectKeyFactory getObjectAsObjectKeyFactory(Object value) { if (value instanceof CacheableObjectKeyFactory) return (CacheableObjectKeyFactory) value; Class<?> classType = getObjectAsClass(value); if (!CacheableObjectKeyFactory.class.isAssignableFrom(classType)) throw new IllegalArgumentException(classType + " is not assignable to " + CacheableObjectKeyFactory.class); try { return (CacheableObjectKeyFactory) classType.newInstance(); } catch (Exception ex) { throw new IllegalStateException("error instantiating " + classType, ex); } } private Method getMethodForSignature(String methodSignature) { int index = methodSignature.lastIndexOf('.'); if (index == -1) throw new IllegalArgumentException( "invalid method signature: expected=package.ClassName.methodName actual=" + methodSignature); String className = methodSignature.substring(0, index); String methodName = methodSignature.substring(index + 1); Class<?> clazz = null; try { clazz = Class.forName(className); } catch (ClassNotFoundException ex) { throw new IllegalStateException(ex); } List<Method> methods = new ArrayList<Method>(); for (Method method : clazz.getMethods()) { if (method.getName().equals(methodName)) methods.add(method); } if (methods.size() == 1) return methods.get(0); else if (methods.size() == 0) throw new IllegalArgumentException("no method found for signature: " + methodSignature); else throw new IllegalArgumentException( "multiple methods found for signature: " + methodSignature); } }