BeanAnnotationFinder.java
package dev.orne.beans;
/*-
* #%L
* Orne Beans
* %%
* Copyright (C) 2020 Orne Developments
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Lesser Public License for more details.
*
* You should have received a copy of the GNU General Lesser Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/lgpl-3.0.html>.
* #L%
*/
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.WeakHashMap;
import javax.validation.constraints.NotNull;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>Type level annotation finder. Detects annotations in classes,
* directly implemented interfaces, super classes and inherited
* interfaces. Supports detection of annotation list annotations.</p>
*
* <p>Examples:</p>
*
* <p>To search for type level annotations:</p>
* <pre>
* Set<?> annotations = new BeanAnnotationFinder<>(annotationType)
* .find(beanType);
* // Real case
* Set<NotNull> annotations = new BeanAnnotationFinder<>(NotNull.class)
* .find(beanType);
* </pre>
*
* <p>To search for type level annotations with annotation list support:</p>
* <pre>
* Set<?> annotations = new BeanAnnotationFinder<>(
* annotationType,
* annotationListType,
* annotationListExtractor)
* .find(beanType);
* // Real case
* Set<NotNull> annotations = new BeanAnnotationFinder<>(
* NotNull.class,
* NotNull.List.class,
* NotNull.List::values)
* .find(beanType);
* </pre>
*
* <p>Instances are reusable and thread-safe.</p>
*
* @author <a href="mailto:wamphiry@orne.dev">(w) Iker Hernaez</a>
* @version 1.0, 2020-05
* @param <T> The supported annotation type
* @param <L> The supported annotation list type
* @since 0.1
*/
@API(status=Status.INTERNAL, since="0.1")
public class BeanAnnotationFinder<
T extends Annotation,
L extends Annotation> {
/** The class logger. */
private static final Logger LOG = LoggerFactory.getLogger(BeanAnnotationFinder.class);
/** The type level annotations shared cache. */
private static final Cache SHARED_CACHE = new WeakHashMapCache();
/** The searched annotation type. */
private final @NotNull Class<T> annotationType;
/** The searched annotation list type. */
private final Class<L> annotationListType;
/** The searched annotation list extractor. */
private final AnnotationListExtractor<L, T> extractor;
/**
* The type level annotations cache for this instance. By default shared
* between all instances.
*/
private @NotNull Cache cache = SHARED_CACHE;
/**
* Creates a new instance.
*
* @param annotationType The searched annotation type
*/
public BeanAnnotationFinder(
final @NotNull Class<T> annotationType) {
this(annotationType, null, null);
}
/**
* Creates a new instance. To support annotation list both
* {@code annotationListType} and {@code extractor} must be supplied.
*
* @param annotationType The searched annotation type
* @param annotationListType The searched annotation list type
* @param extractor The searched annotation list extractor
*/
public BeanAnnotationFinder(
final @NotNull Class<T> annotationType,
final Class<L> annotationListType,
final AnnotationListExtractor<L, T> extractor) {
super();
Validate.notNull(annotationType, "Annotation type is required.");
this.annotationType = annotationType;
if ((annotationListType == null) != (extractor == null)) {
LOG.warn(
"To support annotation lists both 'annotationListType'" +
" and 'extractor' are required." +
" Annotation list support is disabled." +
" Passed values are: '{}' and '{}'",
annotationListType,
extractor);
}
this.annotationListType = annotationListType;
this.extractor = extractor;
}
/**
* Returns the searched annotation type.
*
* @return The searched annotation type
*/
public @NotNull Class<T> getAnnotationType() {
return this.annotationType;
}
/**
* Returns the searched annotation list type.
*
* @return The searched annotation list type
*/
public Class<L> getAnnotationListType() {
return this.annotationListType;
}
/**
* Returns the searched annotation list extractor.
*
* @return The searched annotation list extractor
*/
public AnnotationListExtractor<L, T> getExtractor() {
return this.extractor;
}
/**
* Returns the cache to be used by this instance.
*
* @return The cache to be used by this instance
*/
protected @NotNull Cache getCache() {
return this.cache;
}
/**
* <p>Sets the type level annotations cache for this instance.
* If {@code cache} is {@code null} shared cache will be used.</p>
*
* @param cache The cache to be used by this instance
* @return This instance for method chaining
*/
protected BeanAnnotationFinder<T, L> setCache(
final Cache cache) {
if (cache == null) {
this.cache = SHARED_CACHE;
} else {
this.cache = cache;
}
return this;
}
/**
* Finds the type level annotations this instance supports in the specified
* type.
*
* @param type The type to search for supported annotations
* @return The type level annotations of the type
*/
public @NotNull Set<T> find(
final @NotNull Class<?> type) {
Validate.notNull(type, "Type is required.");
return findAnnotations(type, new HashSet<>());
}
/**
* Finds type level annotations of the supported annotation type in the
* specified type. If the type has been already processed returns an
* empty set. Found annotations are cached for future uses.
*
* @param type The type to found annotations on
* @param visitedTypes The visited types record to prevent loops
* and repeated searches
* @return The found annotations
*/
protected @NotNull Set<T> findAnnotations(
final @NotNull Class<?> type,
final @NotNull Set<Class<?>> visitedTypes) {
final Set<T> annotations = new HashSet<>(0);
if (!visitedTypes.contains(type)) {
synchronized (this.cache) {
final CacheEntryKey<T> cacheKey = new CacheEntryKey<>(
type,
this.annotationType);
final Set<T> cachedDefinitions = this.cache.get(cacheKey);
if (cachedDefinitions == null) {
final Set<T> foundAnnotatios = findAllAnnotations(type);
annotations.addAll(foundAnnotatios);
LOG.debug(
"Caching annotations for type {}: {}",
type,
annotations);
this.cache.put(cacheKey, foundAnnotatios);
} else {
annotations.addAll(cachedDefinitions);
}
}
visitedTypes.add(type);
}
return annotations;
}
/**
* Finds type level annotations of the supported annotation type in the
* specified type. Finds direct annotations (detecting annotation lists if
* configured) and annotations in super class and implemented interfaces.
*
* @param type The type to found annotations on
* @return The found annotations
*/
protected @NotNull Set<T> findAllAnnotations(
final @NotNull Class<?> type) {
final Set<T> annotations = new HashSet<>(0);
final Set<Class<?>> visitedTypes = new HashSet<>();
visitedTypes.add(type);
addDirectAnnotation(type, annotations);
addDirectAnnotationsList(type, annotations);
addInterfacesAnnotations(type, annotations, visitedTypes);
addSuperclassAnnotations(type, annotations, visitedTypes);
return annotations;
}
/**
* Finds direct type level annotations of the supported annotation type in
* the specified type.
*
* @param type The type to found annotations on
* @param annotations The set to add the found annotations on
*/
protected void addDirectAnnotation(
final @NotNull Class<?> type,
final @NotNull Set<T> annotations) {
final T annotation = type.getAnnotation(this.annotationType);
if (annotation != null) {
annotations.add(annotation);
}
}
/**
* Finds direct type level annotations of the supported annotation list
* type in the specified type, if configured. If {@code annotationListType}
* or {@code extractor} are {@code null} no searching is done.
*
* @param type The type to found annotations on
* @param annotations The set to add the found annotations on
*/
protected void addDirectAnnotationsList(
final @NotNull Class<?> type,
final @NotNull Set<T> annotations) {
if (this.annotationListType != null && this.extractor != null) {
final L annotationsList = type.getAnnotation(
this.annotationListType);
if (annotationsList != null) {
annotations.addAll(Arrays.asList(
this.extractor.extract(annotationsList)));
}
}
}
/**
* Finds type level annotations of the supported annotation type in
* the super class of the specified type, if any.
*
* @param type The type to found annotations on
* @param annotations The set to add the found annotations on
* @param visitedTypes The visited types record to prevent loops
* and repeated searches
*/
protected void addSuperclassAnnotations(
final @NotNull Class<?> type,
final @NotNull Set<T> annotations,
final @NotNull Set<Class<?>> visitedTypes) {
if (type.getSuperclass() != null) {
annotations.addAll(findAnnotations(
type.getSuperclass(),
visitedTypes));
}
}
/**
* Finds type level annotations of the supported annotation type in
* the interfaces implemented by the specified type, if any.
*
* @param type The type to found annotations on
* @param annotations The set to add the found annotations on
* @param visitedTypes The visited types record to prevent loops
* and repeated searches
*/
protected void addInterfacesAnnotations(
final @NotNull Class<?> type,
final @NotNull Set<T> annotations,
final @NotNull Set<Class<?>> visitedTypes) {
for (final Class<?> iface : type.getInterfaces()) {
annotations.addAll(findAnnotations(iface, visitedTypes));
}
}
/**
* <p>Functional interface for extractor of individual annotations from
* annotation list annotations.</p>
*
* @param <T> The annotation type
* @param <L> The annotation list type
*/
@API(status=Status.INTERNAL, since="0.1")
@FunctionalInterface
public static interface AnnotationListExtractor<
L extends Annotation,
T extends Annotation> {
/**
* Extracts the nested annotations from the list annotation
* passed as argument.
*
* @param list The annotation list annotation
* @return The nested annotations
*/
@NotNull T[] extract(L list);
}
/**
* Class level annotations cache entry. This class is immutable.
*
* @param <T> The annotation type
*/
@API(status=Status.INTERNAL, since="0.1")
protected static final class CacheEntryKey<T extends Annotation> {
/** The analyzed class. */
private final @NotNull Class<?> type;
/** The annotation type searched. */
private final @NotNull Class<T> annotationType;
/**
* Creates a new instance.
*
* @param type The analyzed class
* @param annotationType The annotation type searched
*/
public CacheEntryKey(
final @NotNull Class<?> type,
final @NotNull Class<T> annotationType) {
super();
this.type = type;
this.annotationType = annotationType;
}
/**
* Returns the analyzed class.
*
* @return The analyzed class
*/
public @NotNull Class<?> getType() {
return this.type;
}
/**
* Returns the annotation type searched.
*
* @return The annotation type searched
*/
public @NotNull Class<T> getAnnotationType() {
return this.annotationType;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return new HashCodeBuilder()
.append(getClass())
.append(this.type)
.append(this.annotationType)
.toHashCode();
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(final Object obj) {
if (obj == null) { return false; }
if (obj == this) { return true; }
if (obj.getClass() != getClass()) {
return false;
}
final CacheEntryKey<?> other = (CacheEntryKey<?>) obj;
return new EqualsBuilder()
.append(this.type, other.type)
.append(this.annotationType, other.annotationType)
.isEquals();
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return new ToStringBuilder(this)
.append("type", this.type)
.append("annotationType", this.annotationType)
.toString();
}
}
/**
* Interface for type level annotations cache.
*/
@API(status=Status.INTERNAL, since="0.1")
protected static interface Cache {
/**
* Returns {@code true} if this instance contains an entry for
* the specified key.
*
* @param key The cache entry key
* @return If this instance contains an entry for the key
*/
boolean contains(
@NotNull CacheEntryKey<?> key);
/**
* Returns the cached found annotations for the specified key, if any.
*
* @param <T> The type of annotations
* @param key The cache entry key
* @return The annotations found, or {@code null} if not cached o cache expired
*/
<T extends Annotation> Set<T> get(
@NotNull CacheEntryKey<T> key);
/**
* Puts the specified found annotations for the specified key.
*
* @param <T> The type of annotations
* @param key The cache entry key
* @param value The annotations found
*/
<T extends Annotation> void put(
@NotNull CacheEntryKey<T> key,
@NotNull Set<T> value);
}
/**
* Implementation of {@code Cache} based on {@code WeakHashMap}.
*
* @see Cache
* @see WeakHashMap
*/
@API(status=Status.INTERNAL, since="0.1")
protected static class WeakHashMapCache
implements Cache {
/** The cache entries. */
private final WeakHashMap<CacheEntryKey<?>, Set<? extends Annotation>> entries =
new WeakHashMap<>();
/**
* Creates a new instance.
*/
public WeakHashMapCache() {
super();
}
/**
* {@inheritDoc}
*/
@Override
public synchronized boolean contains(
final @NotNull CacheEntryKey<?> key) {
return this.entries.containsKey(key);
}
/**
* {@inheritDoc}
*/
@Override
public synchronized @NotNull <T extends Annotation> void put(
final @NotNull CacheEntryKey<T> key,
final @NotNull Set<T> value) {
this.entries.put(key, value);
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public synchronized <T extends Annotation> Set<T> get(
final @NotNull CacheEntryKey<T> key) {
return (Set<T>) this.entries.get(key);
}
}
}