
name.remal.annotation.AnnotationUtils Maven / Gradle / Ivy
package name.remal.annotation;
import com.google.common.base.Joiner;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import name.remal.gradle_plugins.api.RelocateClasses;
import name.remal.proxy.CompositeInvocationHandler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.*;
import java.util.Map.Entry;
import static java.lang.String.format;
import static java.lang.annotation.ElementType.*;
import static java.lang.reflect.Modifier.isStatic;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static name.remal.ArrayUtils.*;
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;
public class AnnotationUtils {
public static boolean isLangCoreAnnotation(@NotNull Class extends Annotation> 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 extends Annotation> 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 extends Annotation> 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 extends Annotation> 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 extends Annotation> 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 extends Annotation> annotationType;
@NotNull
private final String name;
@Nullable
private final Object value;
public AnnotationAttribute(@NotNull Class extends Annotation> annotationType, @NotNull String name, @Nullable Object value) {
this.annotationType = annotationType;
this.name = name;
this.value = value;
}
@NotNull
public Class extends Annotation> 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