All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.qaware.tools.collectioncacheableforspring.CollectionCacheableCacheAnnotationParser Maven / Gradle / Ivy

/*-
 * #%L
 * Collection Cacheable for Spring :: Starter
 * %%
 * Copyright (C) 2020 QAware GmbH
 * %%
 * 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.
 * #L%
 */

package de.qaware.tools.collectioncacheableforspring;

import de.qaware.tools.collectioncacheableforspring.creator.CollectionCreator;
import de.qaware.tools.collectioncacheableforspring.returnvalue.ReturnValueConverter;
import org.springframework.cache.annotation.CacheAnnotationParser;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.interceptor.CacheOperation;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Strategy implementation for parsing Spring's
 * {@link CollectionCacheable}. Inspired by {@link
 * org.springframework.cache.annotation.SpringCacheAnnotationParser
 * Spring's internal implementation}.
 */
public class CollectionCacheableCacheAnnotationParser implements CacheAnnotationParser {

    private static final String MESSAGE_INVALID_COLLECTION_CACHEABLE_ANNOTATION_CONFIGURATION =
            "Invalid CollectionCacheable annotation configuration on '%s'.";

    private static final String MESSAGE_INVALID_CACHE_ANNOTATION_CONFIGURATION =
            "Invalid cache annotation configuration on '%s'.";

    private final Collection collectionCreators;
    private final Collection returnValueConverters;

    public CollectionCacheableCacheAnnotationParser(Collection collectionCreators, Collection returnValueConverters) {
        this.collectionCreators = collectionCreators;
        this.returnValueConverters = returnValueConverters;
    }

    @Override
    public Collection parseCacheAnnotations(Class type) {
        // @CollectionCacheable only makes sense on methods
        return Collections.emptyList();
    }

    @Override
    public Collection parseCacheAnnotations(Method method) {
        DefaultCacheConfig defaultConfig = new DefaultCacheConfig(method.getDeclaringClass());
        return parseCacheAnnotations(defaultConfig, method);
    }

    private Collection parseCacheAnnotations(DefaultCacheConfig cachingConfig, Method method) {
        Collection ops = parseCacheAnnotations(cachingConfig, method, false);
        if (ops != null && ops.size() > 1) {
            // More than one operation found -> local declarations override interface-declared ones...
            Collection localOps = parseCacheAnnotations(cachingConfig, method, true);
            if (localOps != null) {
                return localOps;
            }
        }
        return ops;
    }

    @Nullable
    private Collection parseCacheAnnotations(DefaultCacheConfig cachingConfig, Method method, boolean localOnly) {
        Collection annotations = (localOnly ?
                AnnotatedElementUtils.getAllMergedAnnotations(method, CollectionCacheable.class) :
                AnnotatedElementUtils.findAllMergedAnnotations(method, CollectionCacheable.class));
        if (annotations.isEmpty()) {
            return null;
        }
        return annotations.stream()
                .map(annotation -> parseCollectionCacheableAnnotation(method, cachingConfig, annotation))
                .collect(Collectors.toList());
    }

    private CollectionCacheableOperation parseCollectionCacheableAnnotation(
            Method method, DefaultCacheConfig defaultConfig, CollectionCacheable collectionCacheable) {

        boolean isFindAll = checkFindAll(method);
        if (!isFindAll) {
            validateMethodArguments(method);
            validateGenericMethodSignature(method);
        }

        CollectionCacheableOperation.Builder builder = new CollectionCacheableOperation.Builder();

        builder.setName(method.toString());
        builder.setCacheNames(collectionCacheable.cacheNames());
        builder.setCondition(collectionCacheable.condition());
        builder.setKey(collectionCacheable.key());
        builder.setKeyGenerator(collectionCacheable.keyGenerator());
        builder.setCacheManager(collectionCacheable.cacheManager());
        builder.setCacheResolver(collectionCacheable.cacheResolver());
        builder.setUnless(collectionCacheable.unless());
        builder.setFindAll(isFindAll);
        builder.setPutNull(collectionCacheable.putNull());
        if (!isFindAll) {
            builder.setCollectionCreator(findCollectionCreator(method));
        }
        builder.setReturnValueConverter(findReturnValueConverter(method));

        defaultConfig.applyDefault(builder);
        CollectionCacheableOperation op = builder.build();
        validateCollectionCacheableOperation(method, op);

        return op;
    }

    private ReturnValueConverter findReturnValueConverter(Method method) {
        Class returnType = method.getReturnType();
        return returnValueConverters.stream().filter(converter -> converter.canHandle(returnType))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("Cannot find appropriate return value converter for method " + method + ". Available are: " + returnValueConverters));
    }

    private CollectionCreator findCollectionCreator(Method method) {
        // non-isFindAll has exactly one collection-like parameter
        Class parameterType = method.getParameterTypes()[0];
        return collectionCreators.stream().filter(creator -> creator.canHandle(parameterType))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("Cannot find appropriate collection creator for method " + method + ". Available are: " + collectionCreators));
    }

    private boolean checkFindAll(Method method) {
        return method.getParameterTypes().length == 0;
    }

    private static void validateMethodArguments(Method method) {
        Class[] parameterTypes = method.getParameterTypes();
        if (parameterTypes.length != 1 || !Collection.class.isAssignableFrom(parameterTypes[0])) {
            throw new IllegalStateException(String.format(
                    MESSAGE_INVALID_COLLECTION_CACHEABLE_ANNOTATION_CONFIGURATION +
                            " Did not find exactly one Collection-like argument",
                    method));
        }
    }

    private static void validateGenericMethodSignature(Method method) {
        Type[] genericParameterTypes = method.getGenericParameterTypes();
        if (genericParameterTypes.length != 1 || !(genericParameterTypes[0] instanceof ParameterizedType)) {
            // assume method is not generic
            return;
        }
        if (!(method.getGenericReturnType() instanceof ParameterizedType)) {
            // assume method is not generic
            return;
        }
        ParameterizedType parameterizedCollection = (ParameterizedType) genericParameterTypes[0];
        if (parameterizedCollection.getActualTypeArguments().length != 1) {
            throw new IllegalStateException(String.format(
                    MESSAGE_INVALID_COLLECTION_CACHEABLE_ANNOTATION_CONFIGURATION +
                            " Parameterized collection does not have exactly one type argument.",
                    method));
        }
        if (!method.getReturnType().isAssignableFrom(Map.class)) {
            // assume that some custom ReturnValueConverter will handle this
            return;
        }
        ParameterizedType parameterizedMap = (ParameterizedType) method.getGenericReturnType();
        if (parameterizedMap.getActualTypeArguments().length != 2) {
            throw new IllegalStateException(String.format(
                    MESSAGE_INVALID_COLLECTION_CACHEABLE_ANNOTATION_CONFIGURATION +
                            " Parameterized map does not have exactly two type arguments.",
                    method));
        }
        if (!parameterizedMap.getActualTypeArguments()[0].equals(parameterizedCollection.getActualTypeArguments()[0])) {
            throw new IllegalStateException(String.format(
                    MESSAGE_INVALID_COLLECTION_CACHEABLE_ANNOTATION_CONFIGURATION +
                            " The Map key type should be equal to the collection type.",
                    method));
        }
    }

    private void validateCollectionCacheableOperation(AnnotatedElement ae, CollectionCacheableOperation operation) {
        if (StringUtils.hasText(operation.getCacheManager()) && StringUtils.hasText(operation.getCacheResolver())) {
            throw new IllegalStateException(String.format(
                    MESSAGE_INVALID_CACHE_ANNOTATION_CONFIGURATION +
                            " Both 'cacheManager' and 'cacheResolver' attributes have been set." +
                            " These attributes are mutually exclusive: the cache manager is used to configure a default cache resolver if none is set." +
                            " If a cache resolver is set, the cache manager won't be used.",
                    ae.toString()));
        }
        if (operation.isFindAll() && StringUtils.hasText(operation.getCondition())) {
            throw new IllegalStateException(String.format(
                    MESSAGE_INVALID_CACHE_ANNOTATION_CONFIGURATION +
                            " Cannot use 'condition' on 'findAll'-like methods.",
                    ae.toString()));
        }
        if (operation.isFindAll() && operation.isPutNull()) {
            throw new IllegalStateException(String.format(
                    MESSAGE_INVALID_CACHE_ANNOTATION_CONFIGURATION +
                            " 'putNull' has no effect on 'findAll'-like methods.",
                    ae.toString()));
        }
    }

    /**
     * Provides default settings for a given set of cache operations.
     */
    private static class DefaultCacheConfig {

        private final Class target;

        @Nullable
        private String[] cacheNames;

        @Nullable
        private String keyGenerator;

        @Nullable
        private String cacheManager;

        @Nullable
        private String cacheResolver;

        private boolean initialized = false;

        public DefaultCacheConfig(Class target) {
            this.target = target;
        }

        /**
         * Apply the defaults to the specified {@link CacheOperation.Builder}.
         *
         * @param builder the operation builder to update
         */
        public void applyDefault(CacheOperation.Builder builder) {
            initialize();
            if (builder.getCacheNames().isEmpty() && this.cacheNames != null) {
                builder.setCacheNames(this.cacheNames);
            }
            if (!StringUtils.hasText(builder.getKey()) && !StringUtils.hasText(builder.getKeyGenerator()) &&
                    StringUtils.hasText(this.keyGenerator)) {
                builder.setKeyGenerator(this.keyGenerator);
            }
            // One of these is set so we should not inherit anything
            if (!StringUtils.hasText(builder.getCacheManager()) && !StringUtils.hasText(builder.getCacheResolver())) {
                if (StringUtils.hasText(this.cacheResolver)) {
                    builder.setCacheResolver(this.cacheResolver);
                } else if (StringUtils.hasText(this.cacheManager)) {
                    builder.setCacheManager(this.cacheManager);
                }
            }
        }

        private void initialize() {
            if (this.initialized) {
                return;
            }
            CacheConfig annotation = AnnotatedElementUtils.findMergedAnnotation(this.target, CacheConfig.class);
            if (annotation != null) {
                this.cacheNames = annotation.cacheNames();
                this.keyGenerator = annotation.keyGenerator();
                this.cacheManager = annotation.cacheManager();
                this.cacheResolver = annotation.cacheResolver();
            }
            this.initialized = true;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy