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

name.remal.annotation.AnnotationUtils Maven / Gradle / Ivy

package name.remal.annotation;

import static java.lang.String.format;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PACKAGE;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.ElementType.TYPE_PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.reflect.Modifier.isStatic;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static name.remal.ArrayUtils.arrayEquals;
import static name.remal.ArrayUtils.arrayHashCode;
import static name.remal.ArrayUtils.arrayToString;
import static name.remal.ArrayUtils.contains;
import static name.remal.PrimitiveTypeUtils.unwrap;
import static name.remal.PrimitiveTypeUtils.wrap;
import static name.remal.SneakyThrow.sneakyThrow;
import static name.remal.UncheckedCast.uncheckedCast;
import static name.remal.reflection.HierarchyUtils.getHierarchy;
import static name.remal.reflection.HierarchyUtils.getPackageHierarchy;

import com.google.common.base.Joiner;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.AnnotatedTypeVariable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import name.remal.gradle_plugins.api.RelocateClasses;
import name.remal.proxy.CompositeInvocationHandler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class AnnotationUtils {

    public static boolean isLangCoreAnnotation(@NotNull Class type) {
        String typeName = type.getName();
        if (typeName.startsWith("java.lang.")) return true;
        if (typeName.startsWith("kotlin.")) return true;
        if (typeName.startsWith("groovy.")) return true;
        if (typeName.startsWith("scala.")) return true;
        return false;
    }

    @NotNull
    public static List<@NotNull Method> getAttributeMethods(@NotNull Class type) {
        List result = new ArrayList<>();
        for (Method method : type.getDeclaredMethods()) {
            if (isStatic(method.getModifiers())) continue;
            if (method.isSynthetic()) continue;
            if (0 != method.getParameterCount()) continue;
            if (Void.TYPE == method.getReturnType()) continue;
            result.add(method);
        }
        return result;
    }

    @NotNull
    @RelocateClasses(Joiner.class)
    public static  T newAnnotationInstance(@NotNull Class type, @Nullable Map attributes) {
        List attributeMethods = getAttributeMethods(type);
        attributeMethods.forEach(method -> method.setAccessible(true));

        Map fullAttributes = new HashMap<>();
        attributeMethods.forEach(attributeMethod -> {
            String attributeName = attributeMethod.getName();
            Object value = attributes != null ? attributes.get(attributeName) : null;
            if (value == null) value = attributeMethod.getDefaultValue();
            if (value == null) throw new IllegalArgumentException("Missing value for " + attributeName);
            if (!wrap(attributeMethod.getReturnType()).isInstance(value)) {
                throw new ErrorCreatingAnnotationInstanceException(type, format(
                    "Attribute %s must be %s, but %s provided",
                    attributeName,
                    unwrap(attributeMethod.getReturnType()),
                    unwrap(value.getClass())
                ));
            }
            fullAttributes.put(attributeName, value);
        });

        if (attributes != null) {
            Set unknownAttrs = new LinkedHashSet<>();
            for (String attr : attributes.keySet()) {
                if (!fullAttributes.containsKey(attr)) unknownAttrs.add(attr);
            }
            if (!unknownAttrs.isEmpty()) {
                if (1 == unknownAttrs.size()) {
                    throw new ErrorCreatingAnnotationInstanceException(type, "Unknown attribute: " + unknownAttrs.iterator().next());
                } else {
                    throw new ErrorCreatingAnnotationInstanceException(type, "Unknown attribute: " + Joiner.on(", ").join(unknownAttrs));
                }
            }
        }

        final int hashCode;
        {
            int value = 0;
            for (Entry entry : fullAttributes.entrySet()) {
                value += 127 * entry.getKey().hashCode() ^ arrayHashCode(entry.getValue());
            }
            hashCode = value;
        }

        final String stringRepresentation;
        {
            StringBuilder sb = new StringBuilder();
            sb.append('@').append(type.getName()).append('(');
            boolean isFirst = true;
            for (Entry entry : fullAttributes.entrySet()) {
                if (isFirst) isFirst = false;
                else sb.append(", ");
                sb.append(entry.getKey()).append('=').append(arrayToString(entry.getValue()));
            }
            sb.append(')');
            stringRepresentation = sb.toString();
        }

        return uncheckedCast(Proxy.newProxyInstance(
            type.getClassLoader(),
            new Class[]{type},
            new CompositeInvocationHandler()
                .appendEqualsHandler((proxy, other) -> {
                    if (!type.isInstance(other)) return false;
                    for (Method attributeMethod : attributeMethods) {
                        if (!arrayEquals(
                            fullAttributes.get(attributeMethod.getName()),
                            attributeMethod.invoke(other)
                        )) {
                            return false;
                        }
                    }
                    return true;
                })
                .appendHashCodeHandler(hashCode)
                .appendToStringHandler(stringRepresentation)
                .appendConstMethodHandler(
                    method -> 0 == method.getParameterCount() && "annotationType".equals(method.getName()),
                    type
                )
                .appendMethodHandler(
                    method -> 0 == method.getParameterCount() && fullAttributes.containsKey(method.getName()),
                    (proxy, method, args) -> fullAttributes.get(method.getName())
                )
        ));
    }

    @NotNull
    public static  Map<@NotNull String, @NotNull Object> getAttributes(@NotNull T annotation) {
        Map attrs = new LinkedHashMap<>();
        for (Method method : getAttributeMethods(annotation.annotationType())) {
            method.setAccessible(true);
            try {
                attrs.put(method.getName(), method.invoke(annotation));
            } catch (Throwable throwable) {
                throw sneakyThrow(throwable);
            }
        }
        return attrs;
    }

    @NotNull
    public static  T withAttributes(@NotNull T annotation, @Nullable Map attributes) {
        if (attributes == null || attributes.isEmpty()) return annotation;

        Map fullAttributes = new HashMap<>(getAttributes(annotation));
        fullAttributes.putAll(attributes);
        return newAnnotationInstance(uncheckedCast(annotation.annotationType()), fullAttributes);
    }

    public static  boolean canAnnotate(@NotNull Class type, @NotNull ElementType elementType) {
        Target target = type.getDeclaredAnnotation(Target.class);
        return target == null || contains(target.value(), elementType);
    }

    public static boolean canAnnotate(@NotNull Class type, @NotNull AnnotatedElement annotatedElement) {
        if (annotatedElement instanceof Annotation) return canAnnotate(type, ANNOTATION_TYPE);
        if (annotatedElement instanceof Class) return canAnnotate(type, TYPE);
        if (annotatedElement instanceof Field) return canAnnotate(type, FIELD);
        if (annotatedElement instanceof Method) return canAnnotate(type, METHOD);
        if (annotatedElement instanceof Parameter) return canAnnotate(type, PARAMETER);
        if (annotatedElement instanceof Constructor) return canAnnotate(type, CONSTRUCTOR);
        if (annotatedElement instanceof Package) return canAnnotate(type, PACKAGE);
        if (annotatedElement instanceof AnnotatedTypeVariable) return canAnnotate(type, TYPE_PARAMETER) || canAnnotate(type, TYPE_USE);
        if (annotatedElement instanceof AnnotatedType) return canAnnotate(type, TYPE_USE);
        return false;
    }

    @Nullable
    public static  T getMetaAnnotation(@NotNull AnnotatedElement annotatedElement, @NotNull Class type) {
        List annotations = getMetaAnnotationsImpl(annotatedElement, type, true);
        if (!annotations.isEmpty()) return annotations.get(0);
        return null;
    }

    @NotNull
    public static  List<@NotNull T> getMetaAnnotations(@NotNull AnnotatedElement annotatedElement, @NotNull Class type) {
        return getMetaAnnotationsImpl(annotatedElement, type, false);
    }

    @NotNull
    @SuppressFBWarnings({"CAIL_POSSIBLE_CONSTANT_ALLOCATION_IN_LOOP", "PCAIL_POSSIBLE_CONSTANT_ALLOCATION_IN_LOOP"})
    private static  List<@NotNull T> getMetaAnnotationsImpl(@NotNull AnnotatedElement rootAnnotatedElement, @NotNull Class type, boolean doReturnOnlyFirst) {
        Set result = null;

        boolean canAnnotateAnnotations = canAnnotate(type, ANNOTATION_TYPE);

        Set processedMetaAnnotationScanContexts = new HashSet<>();
        Queue annotatedElementsQueue = new LinkedList<>();
        annotatedElementsQueue.add(new MetaAnnotationScanContext(rootAnnotatedElement, null));
        while (true) {
            MetaAnnotationScanContext metaAnnotationScanContext = annotatedElementsQueue.poll();
            if (metaAnnotationScanContext == null) break;
            if (!processedMetaAnnotationScanContexts.add(metaAnnotationScanContext)) continue;

            AnnotatedElement annotatedElement = metaAnnotationScanContext.getAnnotatedElement();
            List attributes = metaAnnotationScanContext.getAttributes();

            {
                // Get directly declared annotation:
                T annotation = annotatedElement.getDeclaredAnnotation(type);
                if (annotation != null) {
                    T resultAnnotation = withAttributes(annotation, attributes);
                    if (doReturnOnlyFirst) return singletonList(resultAnnotation);
                    if (result == null) result = new LinkedHashSet<>();
                    result.add(resultAnnotation);
                }
            }

            List otherDeclaredAnnotations = new ArrayList<>();
            for (Annotation annotation : annotatedElement.getDeclaredAnnotations()) {
                Class annotationType = annotation.annotationType();
                if (type != annotationType && !isLangCoreAnnotation(annotationType)) {
                    otherDeclaredAnnotations.add(annotation);
                }
            }

            // Parse repeatable-container annotations:
            for (Annotation annotation : otherDeclaredAnnotations) {
                List attributeMethods = getAttributeMethods(annotation.annotationType());
                if (1 != attributeMethods.size()) continue;
                Method valueMethod = attributeMethods.get(0);
                if (!valueMethod.getReturnType().isArray() || type != valueMethod.getReturnType().getComponentType()) continue;
                if (!"value".equals(valueMethod.getName())) continue;
                valueMethod.setAccessible(true);
                final T[] values;
                try {
                    values = uncheckedCast(valueMethod.invoke(annotation));
                } catch (Throwable throwable) {
                    throw sneakyThrow(throwable);
                }
                for (T value : values) {
                    T resultAnnotation = withAttributes(value, attributes);
                    if (doReturnOnlyFirst) return singletonList(resultAnnotation);
                    if (result == null) result = new LinkedHashSet<>();
                    result.add(resultAnnotation);
                }
            }

            // Parse annotations on annotations:
            if (canAnnotateAnnotations) {
                for (Annotation annotation : otherDeclaredAnnotations) {
                    Class annotationType = annotation.annotationType();
                    List innerAttributes = null;
                    for (Method attributeMethod : getAttributeMethods(annotationType)) {
                        for (AnnotationAttributeAlias alias : attributeMethod.getDeclaredAnnotationsByType(AnnotationAttributeAlias.class)) {
                            if (innerAttributes == null) {
                                innerAttributes = new ArrayList<>();
                                if (attributes != null) innerAttributes.addAll(attributes);
                            }

                            Object value = null;
                            if (attributes != null) {
                                for (AnnotationAttribute attr : attributes) {
                                    if (annotationType == attr.getAnnotationType() && Objects.equals(attributeMethod.getName(), attr.getName())) {
                                        value = attr.getValue();
                                        break;
                                    }
                                }
                            }
                            if (value == null) {
                                attributeMethod.setAccessible(true);
                                try {
                                    value = attributeMethod.invoke(annotation);
                                } catch (Throwable throwable) {
                                    throw sneakyThrow(throwable);
                                }
                            }

                            innerAttributes.add(new AnnotationAttribute(
                                alias.annotationClass(),
                                alias.attributeName(),
                                value
                            ));
                        }
                    }
                    annotatedElementsQueue.add(new MetaAnnotationScanContext(
                        annotationType,
                        innerAttributes != null ? innerAttributes : attributes
                    ));
                }
            }

            // Scan inheritance hierarchy for root annotated element:
            if (rootAnnotatedElement != annotatedElement) continue;
            if (type.getDeclaredAnnotation(Inherited.class) == null) continue;

            if (annotatedElement instanceof Parameter) {
                List hierarchy = getHierarchy((Parameter) annotatedElement);
                if (2 <= hierarchy.size()) {
                    for (AnnotatedElement curAnnotatedElement : hierarchy.subList(1, hierarchy.size())) {
                        annotatedElementsQueue.add(new MetaAnnotationScanContext(curAnnotatedElement, attributes));
                    }
                }

            } else if (annotatedElement instanceof Method) {
                List hierarchy = getHierarchy((Method) annotatedElement);
                if (2 <= hierarchy.size()) {
                    for (AnnotatedElement curAnnotatedElement : hierarchy.subList(1, hierarchy.size())) {
                        annotatedElementsQueue.add(new MetaAnnotationScanContext(curAnnotatedElement, attributes));
                    }
                }

            } else if (annotatedElement instanceof Class) {
                List> hierarchy = uncheckedCast(getHierarchy((Class) annotatedElement));
                if (2 <= hierarchy.size()) {
                    for (Class curAnnotatedElement : hierarchy.subList(1, hierarchy.size())) {
                        if (Object.class == curAnnotatedElement) continue;
                        annotatedElementsQueue.add(new MetaAnnotationScanContext(curAnnotatedElement, attributes));
                    }
                }
                if (canAnnotate(type, PACKAGE)) {
                    for (Class curClass : hierarchy) {
                        for (Package curPackage : getPackageHierarchy(curClass)) {
                            annotatedElementsQueue.add(new MetaAnnotationScanContext(curPackage, attributes));
                        }
                    }
                }
            }
        }

        return result == null ? emptyList() : new ArrayList<>(result);
    }

    @NotNull
    private static  T withAttributes(@NotNull T annotation, @Nullable Collection attributes) {
        if (attributes == null || attributes.isEmpty()) return annotation;

        Map attrsMap = new HashMap<>();
        Class annotationType = uncheckedCast(annotation.annotationType());
        for (AnnotationAttribute attr : attributes) {
            if (annotationType == attr.getAnnotationType()) {
                attrsMap.putIfAbsent(attr.getName(), attr.getValue());
            }
        }
        getAttributes(annotation).forEach(attrsMap::putIfAbsent);
        return newAnnotationInstance(annotationType, attrsMap);
    }

}


final class AnnotationAttribute {

    @NotNull
    private final Class annotationType;

    @NotNull
    private final String name;

    @Nullable
    private final Object value;

    public AnnotationAttribute(@NotNull Class annotationType, @NotNull String name, @Nullable Object value) {
        this.annotationType = annotationType;
        this.name = name;
        this.value = value;
    }

    @NotNull
    public Class getAnnotationType() {
        return annotationType;
    }

    @NotNull
    public String getName() {
        return name;
    }

    @Nullable
    public Object getValue() {
        return value;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) return true;
        if (!(object instanceof AnnotationAttribute)) return false;
        AnnotationAttribute other = (AnnotationAttribute) object;
        return Objects.equals(annotationType, other.annotationType)
            && Objects.equals(name, other.name)
            && arrayEquals(value, other.value);
    }

    @Override
    public int hashCode() {
        int result = 1;
        result = 31 * result + annotationType.hashCode();
        result = 31 * result + name.hashCode();
        result = 31 * result + arrayHashCode(value);
        return result;
    }

    @NotNull
    @Override
    public String toString() {
        return AnnotationAttribute.class.getSimpleName() + '('
            + "annotationType=" + annotationType
            + ", name='" + name + '\''
            + ", value=" + arrayToString(value)
            + '}';
    }

}


final class MetaAnnotationScanContext {

    @NotNull
    private final AnnotatedElement annotatedElement;

    @Nullable
    private final List<@NotNull AnnotationAttribute> attributes;

    public MetaAnnotationScanContext(@NotNull AnnotatedElement annotatedElement, @Nullable List<@NotNull AnnotationAttribute> attributes) {
        this.annotatedElement = annotatedElement;
        this.attributes = attributes;
    }

    @NotNull
    public AnnotatedElement getAnnotatedElement() {
        return annotatedElement;
    }

    @Nullable
    public List<@NotNull AnnotationAttribute> getAttributes() {
        return attributes;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof MetaAnnotationScanContext)) return false;
        MetaAnnotationScanContext other = (MetaAnnotationScanContext) obj;
        return Objects.equals(annotatedElement, other.annotatedElement) && Objects.equals(attributes, other.attributes);
    }

    @Override
    public int hashCode() {
        return Objects.hash(annotatedElement, attributes);
    }

    @Override
    public String toString() {
        return MetaAnnotationScanContext.class.getSimpleName() + "{"
            + "annotatedElement=" + annotatedElement
            + ", attributes=" + attributes
            + '}';
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy