001/*
002 * Copyright 2002-2020 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 *      https://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
017package org.springframework.cache.interceptor;
018
019import java.lang.reflect.Method;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Map;
026import java.util.Optional;
027import java.util.concurrent.Callable;
028import java.util.concurrent.ConcurrentHashMap;
029
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032
033import org.springframework.aop.framework.AopProxyUtils;
034import org.springframework.beans.factory.BeanFactory;
035import org.springframework.beans.factory.BeanFactoryAware;
036import org.springframework.beans.factory.InitializingBean;
037import org.springframework.beans.factory.NoSuchBeanDefinitionException;
038import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
039import org.springframework.beans.factory.SmartInitializingSingleton;
040import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
041import org.springframework.cache.Cache;
042import org.springframework.cache.CacheManager;
043import org.springframework.context.ApplicationContext;
044import org.springframework.context.expression.AnnotatedElementKey;
045import org.springframework.expression.EvaluationContext;
046import org.springframework.lang.UsesJava8;
047import org.springframework.util.Assert;
048import org.springframework.util.ClassUtils;
049import org.springframework.util.CollectionUtils;
050import org.springframework.util.LinkedMultiValueMap;
051import org.springframework.util.MultiValueMap;
052import org.springframework.util.ObjectUtils;
053import org.springframework.util.ReflectionUtils;
054import org.springframework.util.StringUtils;
055
056/**
057 * Base class for caching aspects, such as the {@link CacheInterceptor} or an
058 * AspectJ aspect.
059 *
060 * <p>This enables the underlying Spring caching infrastructure to be used easily
061 * to implement an aspect for any aspect system.
062 *
063 * <p>Subclasses are responsible for calling relevant methods in the correct order.
064 *
065 * <p>Uses the <b>Strategy</b> design pattern. A {@link CacheOperationSource} is
066 * used for determining caching operations, a {@link KeyGenerator} will build the
067 * cache keys, and a {@link CacheResolver} will resolve the actual cache(s) to use.
068 *
069 * <p>Note: A cache aspect is serializable but does not perform any actual caching
070 * after deserialization.
071 *
072 * @author Costin Leau
073 * @author Juergen Hoeller
074 * @author Chris Beams
075 * @author Phillip Webb
076 * @author Sam Brannen
077 * @author Stephane Nicoll
078 * @since 3.1
079 */
080public abstract class CacheAspectSupport extends AbstractCacheInvoker
081                implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
082
083        private static Class<?> javaUtilOptionalClass = null;
084
085        static {
086                try {
087                        javaUtilOptionalClass =
088                                        ClassUtils.forName("java.util.Optional", CacheAspectSupport.class.getClassLoader());
089                }
090                catch (ClassNotFoundException ex) {
091                        // Java 8 not available - Optional references simply not supported then.
092                }
093        }
094
095        protected final Log logger = LogFactory.getLog(getClass());
096
097        private final Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache =
098                        new ConcurrentHashMap<CacheOperationCacheKey, CacheOperationMetadata>(1024);
099
100        private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator();
101
102        private CacheOperationSource cacheOperationSource;
103
104        private KeyGenerator keyGenerator = new SimpleKeyGenerator();
105
106        private CacheResolver cacheResolver;
107
108        private BeanFactory beanFactory;
109
110        private boolean initialized = false;
111
112
113        /**
114         * Set one or more cache operation sources which are used to find the cache
115         * attributes. If more than one source is provided, they will be aggregated
116         * using a {@link CompositeCacheOperationSource}.
117         */
118        public void setCacheOperationSources(CacheOperationSource... cacheOperationSources) {
119                Assert.notEmpty(cacheOperationSources, "At least 1 CacheOperationSource needs to be specified");
120                this.cacheOperationSource = (cacheOperationSources.length > 1 ?
121                                new CompositeCacheOperationSource(cacheOperationSources) : cacheOperationSources[0]);
122        }
123
124        /**
125         * Return the CacheOperationSource for this cache aspect.
126         */
127        public CacheOperationSource getCacheOperationSource() {
128                return this.cacheOperationSource;
129        }
130
131        /**
132         * Set the default {@link KeyGenerator} that this cache aspect should delegate to
133         * if no specific key generator has been set for the operation.
134         * <p>The default is a {@link SimpleKeyGenerator}.
135         */
136        public void setKeyGenerator(KeyGenerator keyGenerator) {
137                this.keyGenerator = keyGenerator;
138        }
139
140        /**
141         * Return the default {@link KeyGenerator} that this cache aspect delegates to.
142         */
143        public KeyGenerator getKeyGenerator() {
144                return this.keyGenerator;
145        }
146
147        /**
148         * Set the default {@link CacheResolver} that this cache aspect should delegate
149         * to if no specific cache resolver has been set for the operation.
150         * <p>The default resolver resolves the caches against their names and the
151         * default cache manager.
152         * @see #setCacheManager
153         * @see SimpleCacheResolver
154         */
155        public void setCacheResolver(CacheResolver cacheResolver) {
156                this.cacheResolver = cacheResolver;
157        }
158
159        /**
160         * Return the default {@link CacheResolver} that this cache aspect delegates to.
161         */
162        public CacheResolver getCacheResolver() {
163                return this.cacheResolver;
164        }
165
166        /**
167         * Set the {@link CacheManager} to use to create a default {@link CacheResolver}.
168         * Replace the current {@link CacheResolver}, if any.
169         * @see #setCacheResolver
170         * @see SimpleCacheResolver
171         */
172        public void setCacheManager(CacheManager cacheManager) {
173                this.cacheResolver = new SimpleCacheResolver(cacheManager);
174        }
175
176        /**
177         * Set the containing {@link BeanFactory} for {@link CacheManager} and other
178         * service lookups.
179         * @since 4.3
180         */
181        @Override
182        public void setBeanFactory(BeanFactory beanFactory) {
183                this.beanFactory = beanFactory;
184        }
185
186        /**
187         * @deprecated as of 4.3, in favor of {@link #setBeanFactory}
188         */
189        @Deprecated
190        public void setApplicationContext(ApplicationContext applicationContext) {
191                this.beanFactory = applicationContext;
192        }
193
194
195        @Override
196        public void afterPropertiesSet() {
197                Assert.state(getCacheOperationSource() != null, "The 'cacheOperationSources' property is required: " +
198                                "If there are no cacheable methods, then don't use a cache aspect.");
199                Assert.state(getErrorHandler() != null, "The 'errorHandler' property is required");
200        }
201
202        @Override
203        public void afterSingletonsInstantiated() {
204                if (getCacheResolver() == null) {
205                        // Lazily initialize cache resolver via default cache manager...
206                        try {
207                                setCacheManager(this.beanFactory.getBean(CacheManager.class));
208                        }
209                        catch (NoUniqueBeanDefinitionException ex) {
210                                throw new IllegalStateException("No CacheResolver specified, and no unique bean of type " +
211                                                "CacheManager found. Mark one as primary or declare a specific CacheManager to use.");
212                        }
213                        catch (NoSuchBeanDefinitionException ex) {
214                                throw new IllegalStateException("No CacheResolver specified, and no bean of type CacheManager found. " +
215                                                "Register a CacheManager bean or remove the @EnableCaching annotation from your configuration.");
216                        }
217                }
218                this.initialized = true;
219        }
220
221
222        /**
223         * Convenience method to return a String representation of this Method
224         * for use in logging. Can be overridden in subclasses to provide a
225         * different identifier for the given method.
226         * @param method the method we're interested in
227         * @param targetClass class the method is on
228         * @return log message identifying this method
229         * @see org.springframework.util.ClassUtils#getQualifiedMethodName
230         */
231        protected String methodIdentification(Method method, Class<?> targetClass) {
232                Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
233                return ClassUtils.getQualifiedMethodName(specificMethod);
234        }
235
236        protected Collection<? extends Cache> getCaches(
237                        CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) {
238
239                Collection<? extends Cache> caches = cacheResolver.resolveCaches(context);
240                if (caches.isEmpty()) {
241                        throw new IllegalStateException("No cache could be resolved for '" +
242                                        context.getOperation() + "' using resolver '" + cacheResolver +
243                                        "'. At least one cache should be provided per cache operation.");
244                }
245                return caches;
246        }
247
248        protected CacheOperationContext getOperationContext(
249                        CacheOperation operation, Method method, Object[] args, Object target, Class<?> targetClass) {
250
251                CacheOperationMetadata metadata = getCacheOperationMetadata(operation, method, targetClass);
252                return new CacheOperationContext(metadata, args, target);
253        }
254
255        /**
256         * Return the {@link CacheOperationMetadata} for the specified operation.
257         * <p>Resolve the {@link CacheResolver} and the {@link KeyGenerator} to be
258         * used for the operation.
259         * @param operation the operation
260         * @param method the method on which the operation is invoked
261         * @param targetClass the target type
262         * @return the resolved metadata for the operation
263         */
264        protected CacheOperationMetadata getCacheOperationMetadata(
265                        CacheOperation operation, Method method, Class<?> targetClass) {
266
267                CacheOperationCacheKey cacheKey = new CacheOperationCacheKey(operation, method, targetClass);
268                CacheOperationMetadata metadata = this.metadataCache.get(cacheKey);
269                if (metadata == null) {
270                        KeyGenerator operationKeyGenerator;
271                        if (StringUtils.hasText(operation.getKeyGenerator())) {
272                                operationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class);
273                        }
274                        else {
275                                operationKeyGenerator = getKeyGenerator();
276                        }
277                        CacheResolver operationCacheResolver;
278                        if (StringUtils.hasText(operation.getCacheResolver())) {
279                                operationCacheResolver = getBean(operation.getCacheResolver(), CacheResolver.class);
280                        }
281                        else if (StringUtils.hasText(operation.getCacheManager())) {
282                                CacheManager cacheManager = getBean(operation.getCacheManager(), CacheManager.class);
283                                operationCacheResolver = new SimpleCacheResolver(cacheManager);
284                        }
285                        else {
286                                operationCacheResolver = getCacheResolver();
287                        }
288                        metadata = new CacheOperationMetadata(operation, method, targetClass,
289                                        operationKeyGenerator, operationCacheResolver);
290                        this.metadataCache.put(cacheKey, metadata);
291                }
292                return metadata;
293        }
294
295        /**
296         * Return a bean with the specified name and type. Used to resolve services that
297         * are referenced by name in a {@link CacheOperation}.
298         * @param beanName the name of the bean, as defined by the operation
299         * @param expectedType type for the bean
300         * @return the bean matching that name
301         * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException if such bean does not exist
302         * @see CacheOperation#getKeyGenerator()
303         * @see CacheOperation#getCacheManager()
304         * @see CacheOperation#getCacheResolver()
305         */
306        protected <T> T getBean(String beanName, Class<T> expectedType) {
307                return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, expectedType, beanName);
308        }
309
310        /**
311         * Clear the cached metadata.
312         */
313        protected void clearMetadataCache() {
314                this.metadataCache.clear();
315                this.evaluator.clear();
316        }
317
318        protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
319                // Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
320                if (this.initialized) {
321                        Class<?> targetClass = getTargetClass(target);
322                        Collection<CacheOperation> operations = getCacheOperationSource().getCacheOperations(method, targetClass);
323                        if (!CollectionUtils.isEmpty(operations)) {
324                                return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass));
325                        }
326                }
327
328                return invoker.invoke();
329        }
330
331        /**
332         * Execute the underlying operation (typically in case of cache miss) and return
333         * the result of the invocation. If an exception occurs it will be wrapped in a
334         * {@link CacheOperationInvoker.ThrowableWrapper}: the exception can be handled
335         * or modified but it <em>must</em> be wrapped in a
336         * {@link CacheOperationInvoker.ThrowableWrapper} as well.
337         * @param invoker the invoker handling the operation being cached
338         * @return the result of the invocation
339         * @see CacheOperationInvoker#invoke()
340         */
341        protected Object invokeOperation(CacheOperationInvoker invoker) {
342                return invoker.invoke();
343        }
344
345        private Class<?> getTargetClass(Object target) {
346                Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
347                if (targetClass == null && target != null) {
348                        targetClass = target.getClass();
349                }
350                return targetClass;
351        }
352
353        private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
354                // Special handling of synchronized invocation
355                if (contexts.isSynchronized()) {
356                        CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
357                        if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
358                                Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
359                                Cache cache = context.getCaches().iterator().next();
360                                try {
361                                        return wrapCacheValue(method, cache.get(key, new Callable<Object>() {
362                                                @Override
363                                                public Object call() throws Exception {
364                                                        return unwrapReturnValue(invokeOperation(invoker));
365                                                }
366                                        }));
367                                }
368                                catch (Cache.ValueRetrievalException ex) {
369                                        // Directly propagate ThrowableWrapper from the invoker,
370                                        // or potentially also an IllegalArgumentException etc.
371                                        ReflectionUtils.rethrowRuntimeException(ex.getCause());
372                                }
373                        }
374                        else {
375                                // No caching required, only call the underlying method
376                                return invokeOperation(invoker);
377                        }
378                }
379
380
381                // Process any early evictions
382                processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
383                                CacheOperationExpressionEvaluator.NO_RESULT);
384
385                // Check if we have a cached item matching the conditions
386                Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
387
388                // Collect puts from any @Cacheable miss, if no cached item is found
389                List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
390                if (cacheHit == null) {
391                        collectPutRequests(contexts.get(CacheableOperation.class),
392                                        CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
393                }
394
395                Object cacheValue;
396                Object returnValue;
397
398                if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
399                        // If there are no put requests, just use the cache hit
400                        cacheValue = cacheHit.get();
401                        returnValue = wrapCacheValue(method, cacheValue);
402                }
403                else {
404                        // Invoke the method if we don't have a cache hit
405                        returnValue = invokeOperation(invoker);
406                        cacheValue = unwrapReturnValue(returnValue);
407                }
408
409                // Collect any explicit @CachePuts
410                collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
411
412                // Process any collected put requests, either from @CachePut or a @Cacheable miss
413                for (CachePutRequest cachePutRequest : cachePutRequests) {
414                        cachePutRequest.apply(cacheValue);
415                }
416
417                // Process any late evictions
418                processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
419
420                return returnValue;
421        }
422
423        private Object wrapCacheValue(Method method, Object cacheValue) {
424                if (method.getReturnType() == javaUtilOptionalClass &&
425                                (cacheValue == null || cacheValue.getClass() != javaUtilOptionalClass)) {
426                        return OptionalUnwrapper.wrap(cacheValue);
427                }
428                return cacheValue;
429        }
430
431        private Object unwrapReturnValue(Object returnValue) {
432                if (returnValue != null && returnValue.getClass() == javaUtilOptionalClass) {
433                        return OptionalUnwrapper.unwrap(returnValue);
434                }
435                return returnValue;
436        }
437
438        private boolean hasCachePut(CacheOperationContexts contexts) {
439                // Evaluate the conditions *without* the result object because we don't have it yet...
440                Collection<CacheOperationContext> cachePutContexts = contexts.get(CachePutOperation.class);
441                Collection<CacheOperationContext> excluded = new ArrayList<CacheOperationContext>();
442                for (CacheOperationContext context : cachePutContexts) {
443                        try {
444                                if (!context.isConditionPassing(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE)) {
445                                        excluded.add(context);
446                                }
447                        }
448                        catch (VariableNotAvailableException ex) {
449                                // Ignoring failure due to missing result, consider the cache put has to proceed
450                        }
451                }
452                // Check if all puts have been excluded by condition
453                return (cachePutContexts.size() != excluded.size());
454        }
455
456        private void processCacheEvicts(Collection<CacheOperationContext> contexts, boolean beforeInvocation, Object result) {
457                for (CacheOperationContext context : contexts) {
458                        CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation;
459                        if (beforeInvocation == operation.isBeforeInvocation() && isConditionPassing(context, result)) {
460                                performCacheEvict(context, operation, result);
461                        }
462                }
463        }
464
465        private void performCacheEvict(CacheOperationContext context, CacheEvictOperation operation, Object result) {
466                Object key = null;
467                for (Cache cache : context.getCaches()) {
468                        if (operation.isCacheWide()) {
469                                logInvalidating(context, operation, null);
470                                doClear(cache);
471                        }
472                        else {
473                                if (key == null) {
474                                        key = context.generateKey(result);
475                                }
476                                logInvalidating(context, operation, key);
477                                doEvict(cache, key);
478                        }
479                }
480        }
481
482        private void logInvalidating(CacheOperationContext context, CacheEvictOperation operation, Object key) {
483                if (logger.isTraceEnabled()) {
484                        logger.trace("Invalidating " + (key != null ? "cache key [" + key + "]" : "entire cache") +
485                                        " for operation " + operation + " on method " + context.metadata.method);
486                }
487        }
488
489        /**
490         * Find a cached item only for {@link CacheableOperation} that passes the condition.
491         * @param contexts the cacheable operations
492         * @return a {@link Cache.ValueWrapper} holding the cached item,
493         * or {@code null} if none is found
494         */
495        private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
496                Object result = CacheOperationExpressionEvaluator.NO_RESULT;
497                for (CacheOperationContext context : contexts) {
498                        if (isConditionPassing(context, result)) {
499                                Object key = generateKey(context, result);
500                                Cache.ValueWrapper cached = findInCaches(context, key);
501                                if (cached != null) {
502                                        return cached;
503                                }
504                                else {
505                                        if (logger.isTraceEnabled()) {
506                                                logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
507                                        }
508                                }
509                        }
510                }
511                return null;
512        }
513
514        /**
515         * Collect the {@link CachePutRequest} for all {@link CacheOperation} using
516         * the specified result item.
517         * @param contexts the contexts to handle
518         * @param result the result item (never {@code null})
519         * @param putRequests the collection to update
520         */
521        private void collectPutRequests(Collection<CacheOperationContext> contexts,
522                        Object result, Collection<CachePutRequest> putRequests) {
523
524                for (CacheOperationContext context : contexts) {
525                        if (isConditionPassing(context, result)) {
526                                Object key = generateKey(context, result);
527                                putRequests.add(new CachePutRequest(context, key));
528                        }
529                }
530        }
531
532        private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
533                for (Cache cache : context.getCaches()) {
534                        Cache.ValueWrapper wrapper = doGet(cache, key);
535                        if (wrapper != null) {
536                                if (logger.isTraceEnabled()) {
537                                        logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
538                                }
539                                return wrapper;
540                        }
541                }
542                return null;
543        }
544
545        private boolean isConditionPassing(CacheOperationContext context, Object result) {
546                boolean passing = context.isConditionPassing(result);
547                if (!passing && logger.isTraceEnabled()) {
548                        logger.trace("Cache condition failed on method " + context.metadata.method +
549                                        " for operation " + context.metadata.operation);
550                }
551                return passing;
552        }
553
554        private Object generateKey(CacheOperationContext context, Object result) {
555                Object key = context.generateKey(result);
556                if (key == null) {
557                        throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " +
558                                        "using named params on classes without debug info?) " + context.metadata.operation);
559                }
560                if (logger.isTraceEnabled()) {
561                        logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation);
562                }
563                return key;
564        }
565
566
567        private class CacheOperationContexts {
568
569                private final MultiValueMap<Class<? extends CacheOperation>, CacheOperationContext> contexts =
570                                new LinkedMultiValueMap<Class<? extends CacheOperation>, CacheOperationContext>();
571
572                private final boolean sync;
573
574                public CacheOperationContexts(Collection<? extends CacheOperation> operations, Method method,
575                                Object[] args, Object target, Class<?> targetClass) {
576
577                        for (CacheOperation operation : operations) {
578                                this.contexts.add(operation.getClass(), getOperationContext(operation, method, args, target, targetClass));
579                        }
580                        this.sync = determineSyncFlag(method);
581                }
582
583                public Collection<CacheOperationContext> get(Class<? extends CacheOperation> operationClass) {
584                        Collection<CacheOperationContext> result = this.contexts.get(operationClass);
585                        return (result != null ? result : Collections.<CacheOperationContext>emptyList());
586                }
587
588                public boolean isSynchronized() {
589                        return this.sync;
590                }
591
592                private boolean determineSyncFlag(Method method) {
593                        List<CacheOperationContext> cacheOperationContexts = this.contexts.get(CacheableOperation.class);
594                        if (cacheOperationContexts == null) {  // no @Cacheable operation at all
595                                return false;
596                        }
597                        boolean syncEnabled = false;
598                        for (CacheOperationContext cacheOperationContext : cacheOperationContexts) {
599                                if (((CacheableOperation) cacheOperationContext.getOperation()).isSync()) {
600                                        syncEnabled = true;
601                                        break;
602                                }
603                        }
604                        if (syncEnabled) {
605                                if (this.contexts.size() > 1) {
606                                        throw new IllegalStateException("@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'");
607                                }
608                                if (cacheOperationContexts.size() > 1) {
609                                        throw new IllegalStateException("Only one @Cacheable(sync=true) entry is allowed on '" + method + "'");
610                                }
611                                CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next();
612                                CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation();
613                                if (cacheOperationContext.getCaches().size() > 1) {
614                                        throw new IllegalStateException("@Cacheable(sync=true) only allows a single cache on '" + operation + "'");
615                                }
616                                if (StringUtils.hasText(operation.getUnless())) {
617                                        throw new IllegalStateException("@Cacheable(sync=true) does not support unless attribute on '" + operation + "'");
618                                }
619                                return true;
620                        }
621                        return false;
622                }
623        }
624
625
626        /**
627         * Metadata of a cache operation that does not depend on a particular invocation
628         * which makes it a good candidate for caching.
629         */
630        protected static class CacheOperationMetadata {
631
632                private final CacheOperation operation;
633
634                private final Method method;
635
636                private final Class<?> targetClass;
637
638                private final KeyGenerator keyGenerator;
639
640                private final CacheResolver cacheResolver;
641
642                public CacheOperationMetadata(CacheOperation operation, Method method, Class<?> targetClass,
643                                KeyGenerator keyGenerator, CacheResolver cacheResolver) {
644
645                        this.operation = operation;
646                        this.method = method;
647                        this.targetClass = targetClass;
648                        this.keyGenerator = keyGenerator;
649                        this.cacheResolver = cacheResolver;
650                }
651        }
652
653
654        /**
655         * A {@link CacheOperationInvocationContext} context for a {@link CacheOperation}.
656         */
657        protected class CacheOperationContext implements CacheOperationInvocationContext<CacheOperation> {
658
659                private final CacheOperationMetadata metadata;
660
661                private final Object[] args;
662
663                private final Object target;
664
665                private final Collection<? extends Cache> caches;
666
667                private final Collection<String> cacheNames;
668
669                private final AnnotatedElementKey methodCacheKey;
670
671                public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Object target) {
672                        this.metadata = metadata;
673                        this.args = extractArgs(metadata.method, args);
674                        this.target = target;
675                        this.caches = CacheAspectSupport.this.getCaches(this, metadata.cacheResolver);
676                        this.cacheNames = createCacheNames(this.caches);
677                        this.methodCacheKey = new AnnotatedElementKey(metadata.method, metadata.targetClass);
678                }
679
680                @Override
681                public CacheOperation getOperation() {
682                        return this.metadata.operation;
683                }
684
685                @Override
686                public Object getTarget() {
687                        return this.target;
688                }
689
690                @Override
691                public Method getMethod() {
692                        return this.metadata.method;
693                }
694
695                @Override
696                public Object[] getArgs() {
697                        return this.args;
698                }
699
700                private Object[] extractArgs(Method method, Object[] args) {
701                        if (!method.isVarArgs()) {
702                                return args;
703                        }
704                        Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]);
705                        Object[] combinedArgs = new Object[args.length - 1 + varArgs.length];
706                        System.arraycopy(args, 0, combinedArgs, 0, args.length - 1);
707                        System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length);
708                        return combinedArgs;
709                }
710
711                protected boolean isConditionPassing(Object result) {
712                        if (StringUtils.hasText(this.metadata.operation.getCondition())) {
713                                EvaluationContext evaluationContext = createEvaluationContext(result);
714                                return evaluator.condition(this.metadata.operation.getCondition(),
715                                                this.methodCacheKey, evaluationContext);
716                        }
717                        return true;
718                }
719
720                protected boolean canPutToCache(Object value) {
721                        String unless = "";
722                        if (this.metadata.operation instanceof CacheableOperation) {
723                                unless = ((CacheableOperation) this.metadata.operation).getUnless();
724                        }
725                        else if (this.metadata.operation instanceof CachePutOperation) {
726                                unless = ((CachePutOperation) this.metadata.operation).getUnless();
727                        }
728                        if (StringUtils.hasText(unless)) {
729                                EvaluationContext evaluationContext = createEvaluationContext(value);
730                                return !evaluator.unless(unless, this.methodCacheKey, evaluationContext);
731                        }
732                        return true;
733                }
734
735                /**
736                 * Compute the key for the given caching operation.
737                 * @return the generated key, or {@code null} if none can be generated
738                 */
739                protected Object generateKey(Object result) {
740                        if (StringUtils.hasText(this.metadata.operation.getKey())) {
741                                EvaluationContext evaluationContext = createEvaluationContext(result);
742                                return evaluator.key(this.metadata.operation.getKey(), this.methodCacheKey, evaluationContext);
743                        }
744                        return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
745                }
746
747                private EvaluationContext createEvaluationContext(Object result) {
748                        return evaluator.createEvaluationContext(this.caches, this.metadata.method, this.args,
749                                        this.target, this.metadata.targetClass, result, beanFactory);
750                }
751
752                protected Collection<? extends Cache> getCaches() {
753                        return this.caches;
754                }
755
756                protected Collection<String> getCacheNames() {
757                        return this.cacheNames;
758                }
759
760                private Collection<String> createCacheNames(Collection<? extends Cache> caches) {
761                        Collection<String> names = new ArrayList<String>();
762                        for (Cache cache : caches) {
763                                names.add(cache.getName());
764                        }
765                        return names;
766                }
767        }
768
769
770        private class CachePutRequest {
771
772                private final CacheOperationContext context;
773
774                private final Object key;
775
776                public CachePutRequest(CacheOperationContext context, Object key) {
777                        this.context = context;
778                        this.key = key;
779                }
780
781                public void apply(Object result) {
782                        if (this.context.canPutToCache(result)) {
783                                for (Cache cache : this.context.getCaches()) {
784                                        doPut(cache, this.key, result);
785                                }
786                        }
787                }
788        }
789
790
791        private static final class CacheOperationCacheKey implements Comparable<CacheOperationCacheKey> {
792
793                private final CacheOperation cacheOperation;
794
795                private final AnnotatedElementKey methodCacheKey;
796
797                private CacheOperationCacheKey(CacheOperation cacheOperation, Method method, Class<?> targetClass) {
798                        this.cacheOperation = cacheOperation;
799                        this.methodCacheKey = new AnnotatedElementKey(method, targetClass);
800                }
801
802                @Override
803                public boolean equals(Object other) {
804                        if (this == other) {
805                                return true;
806                        }
807                        if (!(other instanceof CacheOperationCacheKey)) {
808                                return false;
809                        }
810                        CacheOperationCacheKey otherKey = (CacheOperationCacheKey) other;
811                        return (this.cacheOperation.equals(otherKey.cacheOperation) &&
812                                        this.methodCacheKey.equals(otherKey.methodCacheKey));
813                }
814
815                @Override
816                public int hashCode() {
817                        return (this.cacheOperation.hashCode() * 31 + this.methodCacheKey.hashCode());
818                }
819
820                @Override
821                public String toString() {
822                        return this.cacheOperation + " on " + this.methodCacheKey;
823                }
824
825                @Override
826                public int compareTo(CacheOperationCacheKey other) {
827                        int result = this.cacheOperation.getName().compareTo(other.cacheOperation.getName());
828                        if (result == 0) {
829                                result = this.methodCacheKey.compareTo(other.methodCacheKey);
830                        }
831                        return result;
832                }
833        }
834
835
836        /**
837         * Inner class to avoid a hard dependency on Java 8.
838         */
839        @UsesJava8
840        private static class OptionalUnwrapper {
841
842                public static Object unwrap(Object optionalObject) {
843                        Optional<?> optional = (Optional<?>) optionalObject;
844                        if (!optional.isPresent()) {
845                                return null;
846                        }
847                        Object result = optional.get();
848                        Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported");
849                        return result;
850                }
851
852                public static Object wrap(Object value) {
853                        return Optional.ofNullable(value);
854                }
855        }
856
857}