AnnotationUtils.java
001 /*
002  * Copyright 2008-2017 the original author or authors.
003  *
004  * Licensed under the Apache License, Version 2.0 (the "License");
005  * you may not use this file except in compliance with the License.
006  * You may obtain a copy of the License at
007  *
008  *     http://www.apache.org/licenses/LICENSE-2.0
009  *
010  * Unless required by applicable law or agreed to in writing, software
011  * distributed under the License is distributed on an "AS IS" BASIS,
012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013  * See the License for the specific language governing permissions and
014  * limitations under the License.
015  */
016 package griffon.util;
017 
018 import griffon.inject.BindTo;
019 import griffon.inject.DependsOn;
020 import griffon.inject.Evicts;
021 import griffon.inject.Typed;
022 import org.slf4j.Logger;
023 import org.slf4j.LoggerFactory;
024 
025 import javax.annotation.Nonnull;
026 import javax.annotation.Nullable;
027 import javax.inject.Named;
028 import javax.inject.Qualifier;
029 import java.io.Serializable;
030 import java.lang.annotation.Annotation;
031 import java.lang.reflect.Field;
032 import java.lang.reflect.Method;
033 import java.util.ArrayList;
034 import java.util.Arrays;
035 import java.util.Collection;
036 import java.util.Collections;
037 import java.util.Iterator;
038 import java.util.LinkedHashMap;
039 import java.util.LinkedHashSet;
040 import java.util.List;
041 import java.util.Map;
042 import java.util.Set;
043 
044 import static griffon.util.GriffonClassUtils.requireState;
045 import static griffon.util.GriffonNameUtils.getLogicalPropertyName;
046 import static griffon.util.GriffonNameUtils.getPropertyName;
047 import static griffon.util.GriffonNameUtils.isBlank;
048 import static griffon.util.GriffonNameUtils.uncapitalize;
049 import static java.util.Objects.requireNonNull;
050 
051 /**
052  @author Andres Almiray
053  @since 2.0.0
054  */
055 public class AnnotationUtils {
056     private static final Logger LOG = LoggerFactory.getLogger(AnnotationUtils.class);
057     private static final String ERROR_CLASS_NULL = "Argument 'class' must not be null";
058     private static final String ERROR_SUFFIX_NULL = "Argument 'suffix' must not be null";
059     private static final String ERROR_INSTANCE_NULL = "Argument 'instance' must not be null";
060     private static final String ERROR_ANNOTATION_TYPE_NULL = "Argument 'annotationType' must not be null";
061     private static final String ERROR_FIELD_NULL = "Argument 'field' must not be null";
062     private static final String ERROR_SETTER_METHOD_NULL = "Argument 'setterMethod' must not be null";
063 
064     private AnnotationUtils() {
065 
066     }
067 
068     @Nonnull
069     public static List<Annotation> harvestQualifiers(@Nonnull Class<?> klass) {
070         requireNonNull(klass, ERROR_CLASS_NULL);
071         List<Annotation> list = new ArrayList<>();
072         Annotation[] annotations = klass.getAnnotations();
073         for (Annotation annotation : annotations) {
074             if (AnnotationUtils.isAnnotatedWith(annotation, Qualifier.class)) {
075                 // special case @BindTo is only used during tests
076                 if (BindTo.class.isAssignableFrom(annotation.getClass())) {
077                     continue;
078                 }
079                 // special case for @Named
080                 if (Named.class.isAssignableFrom(annotation.getClass())) {
081                     Named named = (Namedannotation;
082                     if (isBlank(named.value())) {
083                         list.add(named(getPropertyName(klass)));
084                         continue;
085                     }
086                 }
087                 list.add(annotation);
088             }
089         }
090         return list;
091     }
092 
093     @Nullable
094     public static <A extends Annotation> A findAnnotation(@Nonnull Class<?> klass, @Nonnull Class<A> annotationType) {
095         requireNonNull(klass, ERROR_CLASS_NULL);
096         requireNonNull(annotationType, ERROR_ANNOTATION_TYPE_NULL);
097 
098         while (klass != null) {
099             Annotation annotation = findAnnotation(klass.getAnnotations(), annotationType);
100             if (annotation != null) { return (Aannotation; }
101             klass = klass.getSuperclass();
102         }
103         return null;
104     }
105 
106     @Nullable
107     public static <A extends Annotation> A findAnnotation(@Nonnull Annotation[] annotations, @Nonnull Class<A> annotationType) {
108         requireNonNull(annotations, "Argument 'annotations' must not be null");
109         requireNonNull(annotationType, ERROR_ANNOTATION_TYPE_NULL);
110         for (Annotation annotation : annotations) {
111             if (annotationType.isAssignableFrom(annotation.getClass())) {
112                 return (Aannotation;
113             }
114         }
115         return null;
116     }
117 
118     public static boolean isAnnotatedWith(@Nonnull Object instance, @Nonnull Class<? extends Annotation> annotationType) {
119         return isAnnotatedWith(requireNonNull(instance, ERROR_INSTANCE_NULL).getClass(), annotationType);
120     }
121 
122     public static boolean isAnnotatedWith(@Nonnull Class<?> clazz, @Nonnull Class<? extends Annotation> annotationType) {
123         requireNonNull(clazz, ERROR_CLASS_NULL);
124         requireNonNull(annotationType, ERROR_ANNOTATION_TYPE_NULL);
125 
126         //noinspection ConstantConditions
127         while (clazz != null) {
128             for (Annotation annotation : clazz.getAnnotations()) {
129                 if (annotationType.equals(annotation.annotationType())) {
130                     return true;
131                 }
132             }
133             for (Class<?> iface : clazz.getInterfaces()) {
134                 if (isAnnotatedWith(iface, annotationType)) {
135                     return true;
136                 }
137             }
138 
139             clazz = clazz.getSuperclass();
140         }
141         return false;
142     }
143 
144     @Nonnull
145     public static <T> T requireAnnotation(@Nonnull T instance, @Nonnull Class<? extends Annotation> annotationType) {
146         if (!isAnnotatedWith(instance, annotationType)) {
147             throw new IllegalArgumentException("Instance of " + instance.getClass() " is not annotated with " + annotationType.getName());
148         }
149         return instance;
150     }
151 
152     @Nonnull
153     public static <T> Class<T> requireAnnotation(@Nonnull Class<T> klass, @Nonnull Class<? extends Annotation> annotationType) {
154         if (!isAnnotatedWith(klass, annotationType)) {
155             throw new IllegalArgumentException("Class " + klass.getName() " is not annotated with " + annotationType.getName());
156         }
157         return klass;
158     }
159 
160     @Nonnull
161     public static String[] getDependsOn(@Nonnull Object instance) {
162         requireNonNull(instance, ERROR_INSTANCE_NULL);
163 
164         DependsOn dependsOn = instance.getClass().getAnnotation(DependsOn.class);
165         return dependsOn != null ? dependsOn.value() new String[0];
166     }
167 
168     @Nonnull
169     public static String getEvicts(@Nonnull Object instance) {
170         requireNonNull(instance, ERROR_INSTANCE_NULL);
171 
172         Evicts evicts = instance.getClass().getAnnotation(Evicts.class);
173         return evicts != null ? evicts.value() "";
174     }
175 
176     @Nonnull
177     public static String nameFor(@Nonnull Object instance) {
178         requireNonNull(instance, ERROR_INSTANCE_NULL);
179 
180         Named annotation = instance.getClass().getAnnotation(Named.class);
181         if (annotation != null && !isBlank(annotation.value())) {
182             return annotation.value();
183         else {
184             return instance.getClass().getName();
185         }
186     }
187 
188     @Nonnull
189     public static String nameFor(@Nonnull Field field) {
190         requireNonNull(field, ERROR_FIELD_NULL);
191 
192         Named annotation = field.getAnnotation(Named.class);
193         if (annotation != null && !isBlank(annotation.value())) {
194             return annotation.value();
195         else {
196             return field.getType().getName();
197         }
198     }
199 
200     @Nonnull
201     public static String[] namesFor(@Nonnull Field field) {
202         requireNonNull(field, ERROR_FIELD_NULL);
203 
204         List<String> names = new ArrayList<>();
205         Named annotation = field.getAnnotation(Named.class);
206         if (annotation != null && !isBlank(annotation.value())) {
207             names.add(annotation.value());
208         else {
209             names.add(field.getName());
210         }
211         names.add(field.getType().getName());
212         return names.toArray(new String[names.size()]);
213     }
214 
215     @Nonnull
216     public static String nameFor(@Nonnull Method setterMethod) {
217         requireNonNull(setterMethod, ERROR_SETTER_METHOD_NULL);
218 
219         Class<?>[] parameterTypes = setterMethod.getParameterTypes();
220         requireState(parameterTypes != null && parameterTypes.length > 0"Argument 'setterMethod' must have at least one parameter. " + MethodDescriptor.forMethod(setterMethod));
221 
222         Named annotation = findAnnotation(annotationsOfMethodParameter(setterMethod, 0), Named.class);
223         if (annotation != null && !isBlank(annotation.value())) {
224             return annotation.value();
225         else {
226             return parameterTypes[0].getName();
227         }
228     }
229 
230     @Nonnull
231     public static String[] namesFor(@Nonnull Method setterMethod) {
232         requireNonNull(setterMethod, ERROR_SETTER_METHOD_NULL);
233 
234         Class<?>[] parameterTypes = setterMethod.getParameterTypes();
235         requireState(parameterTypes != null && parameterTypes.length > 0"Argument 'setterMethod' must have at least one parameter. " + MethodDescriptor.forMethod(setterMethod));
236 
237 
238         List<String> names = new ArrayList<>();
239         Named annotation = findAnnotation(annotationsOfMethodParameter(setterMethod, 0), Named.class);
240         if (annotation != null && !isBlank(annotation.value())) {
241             names.add(annotation.value());
242         else {
243             if (GriffonClassUtils.isSetterMethod(setterMethod)) {
244                 names.add(uncapitalize(setterMethod.getName().substring(3)));
245             else {
246                 names.add(uncapitalize(setterMethod.getName()));
247             }
248         }
249         names.add(parameterTypes[0].getName());
250         return names.toArray(new String[names.size()]);
251     }
252 
253     @Nonnull
254     public static Annotation[] annotationsOfMethodParameter(@Nonnull Method method, int paramIndex) {
255         requireNonNull(method, "Argument 'method' must not be null");
256 
257         Class<?>[] parameterTypes = method.getParameterTypes();
258         requireState(parameterTypes != null && parameterTypes.length > paramIndex, "Index " + paramIndex + " is out of bounds");
259 
260         return method.getParameterAnnotations()[paramIndex];
261     }
262 
263     @Nonnull
264     public static <T> Map<String, T> mapInstancesByName(@Nonnull Collection<T> instances, @Nonnull String suffix) {
265         Map<String, T> map = new LinkedHashMap<>();
266 
267         for (T instance : instances) {
268             map.put(getLogicalPropertyName(nameFor(instance), suffix), instance);
269         }
270 
271         return map;
272     }
273 
274     @Nonnull
275     public static <T> Map<String, T> mapInstancesByName(@Nonnull Collection<T> instances, @Nonnull String suffix, @Nonnull String type) {
276         Map<String, T> map = new LinkedHashMap<>();
277 
278         for (T instance : instances) {
279             String currentEvicts = getEvicts(instance);
280             String name = getLogicalPropertyName(nameFor(instance), suffix);
281             Object evictedInstance = isBlank(currentEvictsnull : map.get(currentEvicts);
282 
283             if (evictedInstance != null) {
284                 String evictedEvicts = getEvicts(evictedInstance);
285                 if (!isBlank(evictedEvicts)) {
286                     throw new IllegalArgumentException(type + " " + name + " has an eviction conflict between " + instance + " and " + evictedInstance);
287                 else {
288                     name = currentEvicts;
289                     LOG.info("{} {} with instance {} evicted by {}", type, name, evictedInstance, instance);
290                 }
291             else {
292                 name = isBlank(currentEvicts? name : currentEvicts;
293                 Object previousInstance = map.get(name);
294                 if (previousInstance != null) {
295                     if (isBlank(getEvicts(previousInstance))) {
296                         throw new IllegalArgumentException(type + " " + name + " neither " + instance + " nor " + previousInstance + " is marked with @Evict");
297                     else {
298                         LOG.info("{} {} with instance {} evicted by {}", type, name, instance, previousInstance);
299                     }
300                 }
301             }
302 
303             map.put(name, instance);
304         }
305 
306         return map;
307     }
308 
309     @Nonnull
310     public static <T> Map<String, T> sortByDependencies(@Nonnull Collection<T> instances, @Nonnull String suffix, @Nonnull String type) {
311         return sortByDependencies(instances, suffix, type, Collections.<String>emptyList());
312     }
313 
314     @Nonnull
315     public static <T> Map<String, T> sortByDependencies(@Nonnull Collection<T> instances, @Nonnull String suffix, @Nonnull String type, @Nonnull List<String> order) {
316         requireNonNull(instances, "Argument 'instances' must not be null");
317         requireNonNull(suffix, ERROR_SUFFIX_NULL);
318         requireNonNull(type, "Argument 'type' must not be null");
319         requireNonNull(order, "Argument 'order' must not be null");
320 
321         Map<String, T> instancesByName = mapInstancesByName(instances, suffix, type);
322 
323         Map<String, T> map = new LinkedHashMap<>();
324         map.putAll(instancesByName);
325 
326         if (!order.isEmpty()) {
327             Map<String, T> tmp1 = new LinkedHashMap<>(instancesByName);
328             Map<String, T> tmp2 = new LinkedHashMap<>();
329             //noinspection ConstantConditions
330             for (String name : order) {
331                 if (tmp1.containsKey(name)) {
332                     tmp2.put(name, tmp1.remove(name));
333                 }
334             }
335             tmp2.putAll(tmp1);
336             map.clear();
337             map.putAll(tmp2);
338         }
339 
340         List<T> sorted = new ArrayList<>();
341         Set<String> instanceDeps = new LinkedHashSet<>();
342 
343         while (!map.isEmpty()) {
344             int processed = 0;
345 
346             LOG.debug("Current {} order is {}", type, instancesByName.keySet());
347 
348             for (Iterator<Map.Entry<String, T>> iter = map.entrySet().iterator(); iter.hasNext()) {
349                 Map.Entry<String, T> entry = iter.next();
350                 String instanceName = entry.getKey();
351                 String[] dependsOn = getDependsOn(entry.getValue());
352 
353                 LOG.trace("Processing {} '{}'", type, instanceName);
354                 LOG.trace("  depends on '{}'", Arrays.toString(dependsOn));
355 
356                 if (dependsOn.length != 0) {
357                     LOG.trace("  checking {} '{}' dependencies ({})", type, instanceName, dependsOn.length);
358 
359                     boolean failedDep = false;
360                     for (String dep : dependsOn) {
361                         LOG.trace("  checking {} '{}' dependencies: ", type, instanceName, dep);
362                         if (!instanceDeps.contains(dep)) {
363                             // dep not in the list yet, we need to skip adding this to the list for now
364                             LOG.trace("  skipped {} '{}', since dependency '{}' not yet added", type, instanceName, dep);
365                             failedDep = true;
366                             break;
367                         else {
368                             LOG.trace("  {} '{}' dependency '{}' already added", type, instanceName, dep);
369                         }
370                     }
371 
372                     if (failedDep) {
373                         // move on to next dependency
374                         continue;
375                     }
376                 }
377 
378                 LOG.trace("  adding {} '{}', since all dependencies have been added", type, instanceName);
379                 sorted.add(entry.getValue());
380                 instanceDeps.add(instanceName);
381                 iter.remove();
382                 processed++;
383             }
384 
385             if (processed == 0) {
386                 // we have a cyclical dependency, warn the user and load in the order they appeared originally
387                 LOG.warn("  unresolved {} dependencies detected", type);
388                 LOG.warn("  continuing with original {} order", type);
389                 for (Map.Entry<String, T> entry : map.entrySet()) {
390                     String instanceName = entry.getKey();
391                     String[] dependsOn = getDependsOn(entry.getValue());
392 
393                     // display this as a cyclical dep
394                     LOG.warn("  {} {} ", type, instanceName);
395                     if (dependsOn.length != 0) {
396                         for (String dep : dependsOn) {
397                             LOG.warn("    depends on {}", dep);
398                         }
399                     else {
400                         // we should only have items left in the list with deps, so this should never happen
401                         // but a wise man once said...check for true, false and otherwise...just in case
402                         LOG.warn("  problem while resolving dependencies.");
403                         LOG.warn("  unable to resolve dependency hierarchy.");
404                     }
405                 }
406                 break;
407                 // if we have processed all the instances, we are done
408             else if (sorted.size() == instancesByName.size()) {
409                 LOG.trace("{} dependency ordering complete", type);
410                 break;
411             }
412         }
413 
414         instancesByName = mapInstancesByName(sorted, suffix);
415         LOG.debug("computed {} order is {}", type, instancesByName.keySet());
416 
417         return instancesByName;
418     }
419 
420     @Nonnull
421     public static Named named(@Nonnull String name) {
422         return new NamedImpl(requireNonNull(name, "Argument 'name' must not be null"));
423     }
424 
425     @Nonnull
426     public static Typed typed(@Nonnull Class<?> clazz) {
427         return new TypedImpl(requireNonNull(clazz, ERROR_CLASS_NULL));
428     }
429 
430     @Nonnull
431     public static BindTo bindto(@Nonnull Class<?> clazz) {
432         return new BindToImpl(requireNonNull(clazz, ERROR_CLASS_NULL));
433     }
434 
435     /**
436      @author Andres Almiray
437      @since 2.0.0
438      */
439     @SuppressWarnings("ClassExplicitlyAnnotation")
440     private static class NamedImpl implements Named, Serializable {
441         private static final long serialVersionUID = 0;
442         private final String value;
443 
444         public NamedImpl(String value) {
445             this.value = requireNonNull(value, "value");
446         }
447 
448         public String value() {
449             return this.value;
450         }
451 
452         public int hashCode() {
453             // This is specified in java.lang.Annotation.
454             return (127 "value".hashCode()) ^ value.hashCode();
455         }
456 
457         public boolean equals(Object o) {
458             if (!(instanceof Named)) {
459                 return false;
460             }
461 
462             Named other = (Namedo;
463             return value.equals(other.value());
464         }
465 
466         public String toString() {
467             return "@" + Named.class.getName() "(value=" + value + ")";
468         }
469 
470         public Class<? extends Annotation> annotationType() {
471             return Named.class;
472         }
473     }
474 
475     /**
476      @author Andres Almiray
477      @since 2.0.0
478      */
479     @SuppressWarnings("ClassExplicitlyAnnotation")
480     private static class TypedImpl implements Typed, Serializable {
481         private static final long serialVersionUID = 0;
482         private final Class<?> value;
483 
484         public TypedImpl(Class<?> value) {
485             this.value = requireNonNull(value, "value");
486         }
487 
488         public Class<?> value() {
489             return this.value;
490         }
491 
492         public int hashCode() {
493             // This is specified in java.lang.Annotation.
494             return (127 "value".hashCode()) ^ value.hashCode();
495         }
496 
497         public boolean equals(Object o) {
498             if (!(instanceof Typed)) {
499                 return false;
500             }
501 
502             Typed other = (Typedo;
503             return value.equals(other.value());
504         }
505 
506         public String toString() {
507             return "@" + Typed.class.getName() "(value=" + value + ")";
508         }
509 
510         public Class<? extends Annotation> annotationType() {
511             return Typed.class;
512         }
513     }
514 
515     /**
516      @author Andres Almiray
517      @since 2.0.0
518      */
519     @SuppressWarnings("ClassExplicitlyAnnotation")
520     private static class BindToImpl implements BindTo, Serializable {
521         private static final long serialVersionUID = 0;
522         private final Class<?> value;
523 
524         public BindToImpl(Class<?> value) {
525             this.value = requireNonNull(value, "value");
526         }
527 
528         public Class<?> value() {
529             return this.value;
530         }
531 
532         public int hashCode() {
533             // This is specified in java.lang.Annotation.
534             return (127 "value".hashCode()) ^ value.hashCode();
535         }
536 
537         public boolean equals(Object o) {
538             if (!(instanceof BindTo)) {
539                 return false;
540             }
541 
542             BindTo other = (BindToo;
543             return value.equals(other.value());
544         }
545 
546         public String toString() {
547             return "@" + BindTo.class.getName() "(value=" + value + ")";
548         }
549 
550         public Class<? extends Annotation> annotationType() {
551             return BindTo.class;
552         }
553     }
554 }