/*
* Copyright 2008-2017 the original author or authors.
*
* 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.
*/
package griffon.util;
import griffon.inject.BindTo;
import griffon.inject.DependsOn;
import griffon.inject.Evicts;
import griffon.inject.Typed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Named;
import javax.inject.Qualifier;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static griffon.util.GriffonClassUtils.requireState;
import static griffon.util.GriffonNameUtils.getLogicalPropertyName;
import static griffon.util.GriffonNameUtils.getPropertyName;
import static griffon.util.GriffonNameUtils.isBlank;
import static griffon.util.GriffonNameUtils.uncapitalize;
import static java.util.Objects.requireNonNull;
/**
* @author Andres Almiray
* @since 2.0.0
*/
public class AnnotationUtils {
private static final Logger LOG = LoggerFactory.getLogger(AnnotationUtils.class);
private static final String ERROR_CLASS_NULL = "Argument 'class' must not be null";
private static final String ERROR_SUFFIX_NULL = "Argument 'suffix' must not be null";
private static final String ERROR_INSTANCE_NULL = "Argument 'instance' must not be null";
private static final String ERROR_ANNOTATION_TYPE_NULL = "Argument 'annotationType' must not be null";
private static final String ERROR_FIELD_NULL = "Argument 'field' must not be null";
private static final String ERROR_SETTER_METHOD_NULL = "Argument 'setterMethod' must not be null";
private AnnotationUtils() {
}
@Nonnull
public static List<Annotation> harvestQualifiers(@Nonnull Class<?> klass) {
requireNonNull(klass, ERROR_CLASS_NULL);
List<Annotation> list = new ArrayList<>();
Annotation[] annotations = klass.getAnnotations();
for (Annotation annotation : annotations) {
if (AnnotationUtils.isAnnotatedWith(annotation, Qualifier.class)) {
// special case @BindTo is only used during tests
if (BindTo.class.isAssignableFrom(annotation.getClass())) {
continue;
}
// special case for @Named
if (Named.class.isAssignableFrom(annotation.getClass())) {
Named named = (Named) annotation;
if (isBlank(named.value())) {
list.add(named(getPropertyName(klass)));
continue;
}
}
list.add(annotation);
}
}
return list;
}
@Nullable
public static <A extends Annotation> A findAnnotation(@Nonnull Class<?> klass, @Nonnull Class<A> annotationType) {
requireNonNull(klass, ERROR_CLASS_NULL);
requireNonNull(annotationType, ERROR_ANNOTATION_TYPE_NULL);
while (klass != null) {
Annotation annotation = findAnnotation(klass.getAnnotations(), annotationType);
if (annotation != null) { return (A) annotation; }
klass = klass.getSuperclass();
}
return null;
}
@Nullable
public static <A extends Annotation> A findAnnotation(@Nonnull Annotation[] annotations, @Nonnull Class<A> annotationType) {
requireNonNull(annotations, "Argument 'annotations' must not be null");
requireNonNull(annotationType, ERROR_ANNOTATION_TYPE_NULL);
for (Annotation annotation : annotations) {
if (annotationType.isAssignableFrom(annotation.getClass())) {
return (A) annotation;
}
}
return null;
}
public static boolean isAnnotatedWith(@Nonnull Object instance, @Nonnull Class<? extends Annotation> annotationType) {
return isAnnotatedWith(requireNonNull(instance, ERROR_INSTANCE_NULL).getClass(), annotationType);
}
public static boolean isAnnotatedWith(@Nonnull Class<?> clazz, @Nonnull Class<? extends Annotation> annotationType) {
requireNonNull(clazz, ERROR_CLASS_NULL);
requireNonNull(annotationType, ERROR_ANNOTATION_TYPE_NULL);
//noinspection ConstantConditions
while (clazz != null) {
for (Annotation annotation : clazz.getAnnotations()) {
if (annotationType.equals(annotation.annotationType())) {
return true;
}
}
for (Class<?> iface : clazz.getInterfaces()) {
if (isAnnotatedWith(iface, annotationType)) {
return true;
}
}
clazz = clazz.getSuperclass();
}
return false;
}
@Nonnull
public static <T> T requireAnnotation(@Nonnull T instance, @Nonnull Class<? extends Annotation> annotationType) {
if (!isAnnotatedWith(instance, annotationType)) {
throw new IllegalArgumentException("Instance of " + instance.getClass() + " is not annotated with " + annotationType.getName());
}
return instance;
}
@Nonnull
public static <T> Class<T> requireAnnotation(@Nonnull Class<T> klass, @Nonnull Class<? extends Annotation> annotationType) {
if (!isAnnotatedWith(klass, annotationType)) {
throw new IllegalArgumentException("Class " + klass.getName() + " is not annotated with " + annotationType.getName());
}
return klass;
}
@Nonnull
public static String[] getDependsOn(@Nonnull Object instance) {
requireNonNull(instance, ERROR_INSTANCE_NULL);
DependsOn dependsOn = instance.getClass().getAnnotation(DependsOn.class);
return dependsOn != null ? dependsOn.value() : new String[0];
}
@Nonnull
public static String getEvicts(@Nonnull Object instance) {
requireNonNull(instance, ERROR_INSTANCE_NULL);
Evicts evicts = instance.getClass().getAnnotation(Evicts.class);
return evicts != null ? evicts.value() : "";
}
@Nonnull
public static String nameFor(@Nonnull Object instance) {
requireNonNull(instance, ERROR_INSTANCE_NULL);
Named annotation = instance.getClass().getAnnotation(Named.class);
if (annotation != null && !isBlank(annotation.value())) {
return annotation.value();
} else {
return instance.getClass().getName();
}
}
@Nonnull
public static String nameFor(@Nonnull Field field) {
requireNonNull(field, ERROR_FIELD_NULL);
Named annotation = field.getAnnotation(Named.class);
if (annotation != null && !isBlank(annotation.value())) {
return annotation.value();
} else {
return field.getType().getName();
}
}
@Nonnull
public static String[] namesFor(@Nonnull Field field) {
requireNonNull(field, ERROR_FIELD_NULL);
List<String> names = new ArrayList<>();
Named annotation = field.getAnnotation(Named.class);
if (annotation != null && !isBlank(annotation.value())) {
names.add(annotation.value());
} else {
names.add(field.getName());
}
names.add(field.getType().getName());
return names.toArray(new String[names.size()]);
}
@Nonnull
public static String nameFor(@Nonnull Method setterMethod) {
requireNonNull(setterMethod, ERROR_SETTER_METHOD_NULL);
Class<?>[] parameterTypes = setterMethod.getParameterTypes();
requireState(parameterTypes != null && parameterTypes.length > 0, "Argument 'setterMethod' must have at least one parameter. " + MethodDescriptor.forMethod(setterMethod));
Named annotation = findAnnotation(annotationsOfMethodParameter(setterMethod, 0), Named.class);
if (annotation != null && !isBlank(annotation.value())) {
return annotation.value();
} else {
return parameterTypes[0].getName();
}
}
@Nonnull
public static String[] namesFor(@Nonnull Method setterMethod) {
requireNonNull(setterMethod, ERROR_SETTER_METHOD_NULL);
Class<?>[] parameterTypes = setterMethod.getParameterTypes();
requireState(parameterTypes != null && parameterTypes.length > 0, "Argument 'setterMethod' must have at least one parameter. " + MethodDescriptor.forMethod(setterMethod));
List<String> names = new ArrayList<>();
Named annotation = findAnnotation(annotationsOfMethodParameter(setterMethod, 0), Named.class);
if (annotation != null && !isBlank(annotation.value())) {
names.add(annotation.value());
} else {
if (GriffonClassUtils.isSetterMethod(setterMethod)) {
names.add(uncapitalize(setterMethod.getName().substring(3)));
} else {
names.add(uncapitalize(setterMethod.getName()));
}
}
names.add(parameterTypes[0].getName());
return names.toArray(new String[names.size()]);
}
@Nonnull
public static Annotation[] annotationsOfMethodParameter(@Nonnull Method method, int paramIndex) {
requireNonNull(method, "Argument 'method' must not be null");
Class<?>[] parameterTypes = method.getParameterTypes();
requireState(parameterTypes != null && parameterTypes.length > paramIndex, "Index " + paramIndex + " is out of bounds");
return method.getParameterAnnotations()[paramIndex];
}
@Nonnull
public static <T> Map<String, T> mapInstancesByName(@Nonnull Collection<T> instances, @Nonnull String suffix) {
Map<String, T> map = new LinkedHashMap<>();
for (T instance : instances) {
map.put(getLogicalPropertyName(nameFor(instance), suffix), instance);
}
return map;
}
@Nonnull
public static <T> Map<String, T> mapInstancesByName(@Nonnull Collection<T> instances, @Nonnull String suffix, @Nonnull String type) {
Map<String, T> map = new LinkedHashMap<>();
for (T instance : instances) {
String currentEvicts = getEvicts(instance);
String name = getLogicalPropertyName(nameFor(instance), suffix);
Object evictedInstance = isBlank(currentEvicts) ? null : map.get(currentEvicts);
if (evictedInstance != null) {
String evictedEvicts = getEvicts(evictedInstance);
if (!isBlank(evictedEvicts)) {
throw new IllegalArgumentException(type + " " + name + " has an eviction conflict between " + instance + " and " + evictedInstance);
} else {
name = currentEvicts;
LOG.info("{} {} with instance {} evicted by {}", type, name, evictedInstance, instance);
}
} else {
name = isBlank(currentEvicts) ? name : currentEvicts;
Object previousInstance = map.get(name);
if (previousInstance != null) {
if (isBlank(getEvicts(previousInstance))) {
throw new IllegalArgumentException(type + " " + name + " neither " + instance + " nor " + previousInstance + " is marked with @Evict");
} else {
LOG.info("{} {} with instance {} evicted by {}", type, name, instance, previousInstance);
}
}
}
map.put(name, instance);
}
return map;
}
@Nonnull
public static <T> Map<String, T> sortByDependencies(@Nonnull Collection<T> instances, @Nonnull String suffix, @Nonnull String type) {
return sortByDependencies(instances, suffix, type, Collections.<String>emptyList());
}
@Nonnull
public static <T> Map<String, T> sortByDependencies(@Nonnull Collection<T> instances, @Nonnull String suffix, @Nonnull String type, @Nonnull List<String> order) {
requireNonNull(instances, "Argument 'instances' must not be null");
requireNonNull(suffix, ERROR_SUFFIX_NULL);
requireNonNull(type, "Argument 'type' must not be null");
requireNonNull(order, "Argument 'order' must not be null");
Map<String, T> instancesByName = mapInstancesByName(instances, suffix, type);
Map<String, T> map = new LinkedHashMap<>();
map.putAll(instancesByName);
if (!order.isEmpty()) {
Map<String, T> tmp1 = new LinkedHashMap<>(instancesByName);
Map<String, T> tmp2 = new LinkedHashMap<>();
//noinspection ConstantConditions
for (String name : order) {
if (tmp1.containsKey(name)) {
tmp2.put(name, tmp1.remove(name));
}
}
tmp2.putAll(tmp1);
map.clear();
map.putAll(tmp2);
}
List<T> sorted = new ArrayList<>();
Set<String> instanceDeps = new LinkedHashSet<>();
while (!map.isEmpty()) {
int processed = 0;
LOG.debug("Current {} order is {}", type, instancesByName.keySet());
for (Iterator<Map.Entry<String, T>> iter = map.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry<String, T> entry = iter.next();
String instanceName = entry.getKey();
String[] dependsOn = getDependsOn(entry.getValue());
LOG.trace("Processing {} '{}'", type, instanceName);
LOG.trace(" depends on '{}'", Arrays.toString(dependsOn));
if (dependsOn.length != 0) {
LOG.trace(" checking {} '{}' dependencies ({})", type, instanceName, dependsOn.length);
boolean failedDep = false;
for (String dep : dependsOn) {
LOG.trace(" checking {} '{}' dependencies: ", type, instanceName, dep);
if (!instanceDeps.contains(dep)) {
// dep not in the list yet, we need to skip adding this to the list for now
LOG.trace(" skipped {} '{}', since dependency '{}' not yet added", type, instanceName, dep);
failedDep = true;
break;
} else {
LOG.trace(" {} '{}' dependency '{}' already added", type, instanceName, dep);
}
}
if (failedDep) {
// move on to next dependency
continue;
}
}
LOG.trace(" adding {} '{}', since all dependencies have been added", type, instanceName);
sorted.add(entry.getValue());
instanceDeps.add(instanceName);
iter.remove();
processed++;
}
if (processed == 0) {
// we have a cyclical dependency, warn the user and load in the order they appeared originally
LOG.warn(" unresolved {} dependencies detected", type);
LOG.warn(" continuing with original {} order", type);
for (Map.Entry<String, T> entry : map.entrySet()) {
String instanceName = entry.getKey();
String[] dependsOn = getDependsOn(entry.getValue());
// display this as a cyclical dep
LOG.warn(" {} {} ", type, instanceName);
if (dependsOn.length != 0) {
for (String dep : dependsOn) {
LOG.warn(" depends on {}", dep);
}
} else {
// we should only have items left in the list with deps, so this should never happen
// but a wise man once said...check for true, false and otherwise...just in case
LOG.warn(" problem while resolving dependencies.");
LOG.warn(" unable to resolve dependency hierarchy.");
}
}
break;
// if we have processed all the instances, we are done
} else if (sorted.size() == instancesByName.size()) {
LOG.trace("{} dependency ordering complete", type);
break;
}
}
instancesByName = mapInstancesByName(sorted, suffix);
LOG.debug("computed {} order is {}", type, instancesByName.keySet());
return instancesByName;
}
@Nonnull
public static Named named(@Nonnull String name) {
return new NamedImpl(requireNonNull(name, "Argument 'name' must not be null"));
}
@Nonnull
public static Typed typed(@Nonnull Class<?> clazz) {
return new TypedImpl(requireNonNull(clazz, ERROR_CLASS_NULL));
}
@Nonnull
public static BindTo bindto(@Nonnull Class<?> clazz) {
return new BindToImpl(requireNonNull(clazz, ERROR_CLASS_NULL));
}
/**
* @author Andres Almiray
* @since 2.0.0
*/
@SuppressWarnings("ClassExplicitlyAnnotation")
private static class NamedImpl implements Named, Serializable {
private static final long serialVersionUID = 0;
private final String value;
public NamedImpl(String value) {
this.value = requireNonNull(value, "value");
}
public String value() {
return this.value;
}
public int hashCode() {
// This is specified in java.lang.Annotation.
return (127 * "value".hashCode()) ^ value.hashCode();
}
public boolean equals(Object o) {
if (!(o instanceof Named)) {
return false;
}
Named other = (Named) o;
return value.equals(other.value());
}
public String toString() {
return "@" + Named.class.getName() + "(value=" + value + ")";
}
public Class<? extends Annotation> annotationType() {
return Named.class;
}
}
/**
* @author Andres Almiray
* @since 2.0.0
*/
@SuppressWarnings("ClassExplicitlyAnnotation")
private static class TypedImpl implements Typed, Serializable {
private static final long serialVersionUID = 0;
private final Class<?> value;
public TypedImpl(Class<?> value) {
this.value = requireNonNull(value, "value");
}
public Class<?> value() {
return this.value;
}
public int hashCode() {
// This is specified in java.lang.Annotation.
return (127 * "value".hashCode()) ^ value.hashCode();
}
public boolean equals(Object o) {
if (!(o instanceof Typed)) {
return false;
}
Typed other = (Typed) o;
return value.equals(other.value());
}
public String toString() {
return "@" + Typed.class.getName() + "(value=" + value + ")";
}
public Class<? extends Annotation> annotationType() {
return Typed.class;
}
}
/**
* @author Andres Almiray
* @since 2.0.0
*/
@SuppressWarnings("ClassExplicitlyAnnotation")
private static class BindToImpl implements BindTo, Serializable {
private static final long serialVersionUID = 0;
private final Class<?> value;
public BindToImpl(Class<?> value) {
this.value = requireNonNull(value, "value");
}
public Class<?> value() {
return this.value;
}
public int hashCode() {
// This is specified in java.lang.Annotation.
return (127 * "value".hashCode()) ^ value.hashCode();
}
public boolean equals(Object o) {
if (!(o instanceof BindTo)) {
return false;
}
BindTo other = (BindTo) o;
return value.equals(other.value());
}
public String toString() {
return "@" + BindTo.class.getName() + "(value=" + value + ")";
}
public Class<? extends Annotation> annotationType() {
return BindTo.class;
}
}
}