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