/* * Grapht, an open source dependency injector. * Copyright 2014-2015 various contributors (see CONTRIBUTORS.txt) * Copyright 2010-2014 Regents of the University of Minnesota * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.grouplens.grapht.reflect; import com.google.common.collect.Sets; import org.grouplens.grapht.annotation.AliasFor; import org.grouplens.grapht.annotation.AllowDefaultMatch; import org.grouplens.grapht.annotation.AllowUnqualifiedMatch; import org.grouplens.grapht.util.ClassProxy; import org.grouplens.grapht.util.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.inject.Qualifier; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectStreamException; import java.io.Serializable; import java.lang.annotation.Annotation; import java.util.Set; /** * Utilities related to Qualifier implementations. * * @author <a href="http://grouplens.org">GroupLens Research</a> */ public final class Qualifiers { private static final Logger logger = LoggerFactory.getLogger(Qualifiers.class); private Qualifiers() { } /** * Return true or false whether or not the annotation type represents a * {@link Qualifier} * * @param type The annotation type * @return True if the annotation is a {@link Qualifier} or parameter * @throws NullPointerException if the type is null */ public static boolean isQualifier(Class<? extends Annotation> type) { return type.getAnnotation(javax.inject.Qualifier.class) != null; } /** * Resolve qualifier aliases, returning the target qualifier. Aliases are resolved * recursively. * * @param type The annotation type. * @return The annotation type for which this type is an alias, or {@code type} if it is not an * alias. * @throws java.lang.IllegalArgumentException if there is a problem with the type, such as a * circular alias reference. */ @Nonnull public static Class<? extends Annotation> resolveAliases(@Nonnull Class<? extends Annotation> type) { Preconditions.notNull("qualifier type", type); Set<Class<? extends Annotation>> seen = Sets.newHashSet(); seen.add(type); Class<? extends Annotation> result = type; AliasFor alias; while ((alias = result.getAnnotation(AliasFor.class)) != null) { if (result.getDeclaredMethods().length > 0) { throw new IllegalArgumentException("aliased qualifier cannot have parameters"); } result = alias.value(); if (!result.isAnnotationPresent(Qualifier.class)) { throw new IllegalArgumentException("alias target " + type + " is not a qualifier"); } if (!seen.add(result)) { throw new IllegalArgumentException("Circular alias reference starting with " + type); } } return result; } /** * The default qualifier matcher. This matches the null qualifier and any qualifier * @return A QualifierMatcher that matches using the default policy. */ public static QualifierMatcher matchDefault() { return new DefaultMatcher(); } /** * @return A QualifierMatcher that matches any qualifier */ public static QualifierMatcher matchAny() { return new AnyMatcher(); } /** * @return A QualifierMatcher that matches only the null qualifier */ public static QualifierMatcher matchNone() { return new NullMatcher(); } /** * @param annotType Annotation type class to match; {@code null} to match only the lack of a * qualifier. * @return A QualifierMatcher that matches any annotation of the given class * type. */ public static QualifierMatcher match(Class<? extends Annotation> annotType) { if (annotType == null) { return matchNone(); } else { return new AnnotationClassMatcher(annotType); } } /** * @param annot Annotation instance to match, or {@code null} to match only the lack of a qualifier. * @return A QualifierMatcher that matches annotations equaling annot */ public static QualifierMatcher match(Annotation annot) { if (annot == null) { return matchNone(); } else if (annot.annotationType().getDeclaredMethods().length == 0) { logger.debug("using type matcher for nullary annotation {}", annot); // Instances of the same nullary annotation are all equal to each other, so just do // type checking. This makes aliasing work with annotation value matchers, b/c we // do not allow aliases to have parameters. The matcher still has value priority. return new AnnotationClassMatcher(annot.annotationType(), DefaultMatcherPriority.MATCH_VALUE); } else { return new AnnotationMatcher(annot); } } private enum DefaultMatcherPriority { MATCH_VALUE, MATCH_TYPE, MATCH_ANY, MATCH_DEFAULT } private abstract static class AbstractMatcher implements QualifierMatcher { private static final long serialVersionUID = 1L; private final DefaultMatcherPriority priority; AbstractMatcher(DefaultMatcherPriority prio) { priority = prio; } @Override public final int getPriority() { return priority.ordinal(); } @Override @Deprecated public boolean matches(Annotation q) { return apply(q); } @Override public int compareTo(QualifierMatcher o) { if (o == null) { // other type is unknown, so extend it to the front return 1; } else { // lower priorities sort lower (higher precedence) return getPriority() - o.getPriority(); } } } private static class DefaultMatcher extends AbstractMatcher { private static final long serialVersionUID = 1L; DefaultMatcher() { super(DefaultMatcherPriority.MATCH_DEFAULT); } @Override @SuppressWarnings("deprecation") public boolean apply(Annotation q) { if (q == null) { return true; } else { Class<? extends Annotation> atype = q.annotationType(); return atype.isAnnotationPresent(AllowDefaultMatch.class) || atype.isAnnotationPresent(AllowUnqualifiedMatch.class); } } @Override public boolean equals(Object o) { return o instanceof DefaultMatcher; } @Override public int hashCode() { return DefaultMatcher.class.hashCode(); } @Override public String toString() { return "%"; } } private static class AnyMatcher extends AbstractMatcher { private static final long serialVersionUID = 1L; AnyMatcher() { super(DefaultMatcherPriority.MATCH_ANY); } @Override public boolean apply(Annotation q) { return true; } @Override public boolean equals(Object o) { return o instanceof AnyMatcher; } @Override public int hashCode() { return AnyMatcher.class.hashCode(); } @Override public String toString() { return "*"; } } private static class NullMatcher extends AbstractMatcher { private static final long serialVersionUID = 1L; NullMatcher() { super(DefaultMatcherPriority.MATCH_VALUE); } @Override public boolean apply(Annotation q) { return q == null; } @Override public boolean equals(Object o) { return o instanceof NullMatcher; } @Override public int hashCode() { return NullMatcher.class.hashCode(); } @Override public String toString() { return "-"; } } static class AnnotationClassMatcher extends AbstractMatcher { private static final long serialVersionUID = -1L; private final Class<? extends Annotation> type; private final Class<? extends Annotation> actual; public AnnotationClassMatcher(Class<? extends Annotation> type) { this(type, DefaultMatcherPriority.MATCH_TYPE); } public AnnotationClassMatcher(Class<? extends Annotation> type, DefaultMatcherPriority prio) { super(prio); Preconditions.notNull("type", type); Preconditions.isQualifier(type); this.type = type; // find the actual type to match (resolving aliases) actual = resolveAliases(type); } @Override public boolean apply(Annotation q) { // We test if the alias-resolved types match. Class<? extends Annotation> qtype = (q == null ? null : q.annotationType()); if (qtype == null) { return false; } else { Class<? extends Annotation> qact = resolveAliases(qtype); return actual.equals(qact); } } @Override public boolean equals(Object o) { return o instanceof AnnotationClassMatcher && ((AnnotationClassMatcher) o).actual.equals(actual); } @Override public int hashCode() { return actual.hashCode(); } @Override public String toString() { if (type.equals(actual)) { return type.toString(); } else { return type.toString() + "( alias of " + actual.toString() + ")"; } } private Object writeReplace() { // We just serialize the type. If its alias status changes, that is fine. return new SerialProxy(type); } private void readObject(ObjectInputStream stream) throws ObjectStreamException { throw new InvalidObjectException("must use serialization proxy"); } static class SerialProxy implements Serializable { private static final long serialVersionUID = 1L; private final ClassProxy type; public SerialProxy(Class<?> cls) { type = ClassProxy.of(cls); } private Object readResolve() throws ObjectStreamException { try { return new AnnotationClassMatcher(type.resolve().asSubclass(Annotation.class)); } catch (ClassNotFoundException e) { InvalidObjectException ex = new InvalidObjectException("cannot resolve " + type); ex.initCause(e); throw ex; } catch (ClassCastException e) { InvalidObjectException ex = new InvalidObjectException("class " + type + " not an annotation"); ex.initCause(e); throw ex; } } } } private static class AnnotationMatcher extends AbstractMatcher implements Serializable { private static final long serialVersionUID = 1L; @SuppressWarnings("squid:S1948") // serializable warning; annotations are serializable private final Annotation annotation; public AnnotationMatcher(Annotation annot) { super(DefaultMatcherPriority.MATCH_VALUE); Preconditions.notNull("annotation", annot); Preconditions.isQualifier(annot.annotationType()); annotation = annot; } @Override public boolean apply(Annotation q) { return annotation.equals(q); } @Override public boolean equals(Object o) { return (o instanceof AnnotationMatcher) && ((AnnotationMatcher) o).annotation.equals(annotation); } @Override public int hashCode() { return annotation.hashCode(); } @Override public String toString() { return annotation.toString(); } } }