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 = (Named) annotation;
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 (A) annotation; }
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 (A) annotation;
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(currentEvicts) ? null : 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 (!(o instanceof Named)) {
459 return false;
460 }
461
462 Named other = (Named) o;
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 (!(o instanceof Typed)) {
499 return false;
500 }
501
502 Typed other = (Typed) o;
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 (!(o instanceof BindTo)) {
539 return false;
540 }
541
542 BindTo other = (BindTo) o;
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 }
|