/*
* 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.internal;
import com.google.common.collect.Lists;
import org.grouplens.grapht.InvalidBindingException;
import org.grouplens.grapht.reflect.Desire;
import org.grouplens.grapht.reflect.InjectionPoint;
import org.grouplens.grapht.reflect.Satisfaction;
import org.grouplens.grapht.util.ClassProxy;
import org.grouplens.grapht.util.Preconditions;
import org.grouplens.grapht.util.Types;
import javax.inject.Inject;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.*;
import java.util.*;
/**
* ReflectionDesire is an implementation of desire that contains all necessary
* implementation to represent a desire, except that the point of injection is
* abstracted by an {@link InjectionPoint}.
*
* @author <a href="http://grouplens.org">GroupLens Research</a>
*/
public class ReflectionDesire implements Desire, Serializable {
private static final long serialVersionUID = -1L;
/**
* Return a list of desires that must satisfied in order to instantiate the
* given type.
*
* @param type The class type whose dependencies will be queried
* @return The dependency desires for the given type
* @throws NullPointerException if the type is null
*/
public static List<Desire> getDesires(Class<?> type) {
List<Desire> desires = Lists.newArrayList();
boolean ctorFound = false;
for (Constructor<?> ctor: type.getDeclaredConstructors()) {
if (ctor.getAnnotation(Inject.class) != null) {
if (!ctorFound) {
ctorFound = true;
for (int i = 0; i < ctor.getParameterTypes().length; i++) {
desires.add(new ReflectionDesire(new ConstructorParameterInjectionPoint(ctor, i)));
}
} else {
// at the moment there can only be one injectable constructor
throw new InvalidBindingException(type, "More than one constructor with @Inject is not allowed");
}
}
}
// JSR 330 mandates that super class methods are injected first, so we
// collect method injection points into a separate list and then reverse
// it to get the ordering correct.
List<Desire> groupDesires = Lists.newArrayList();
// Must also keep track of methods overridden in the subtypes.
Set<Signature> visitedMethods = new HashSet<Signature>();
while(type != null) {
for (Method m: type.getDeclaredMethods()) {
Signature s = new Signature(m);
if (!visitedMethods.contains(s) && m.getAnnotation(Inject.class) != null
&& !Modifier.isStatic(m.getModifiers())) {
// have not seen this signature, and its an injection point
if (m.getParameterTypes().length > 0) {
for (int i = 0; i < m.getParameterTypes().length; i++) {
groupDesires.add(new ReflectionDesire(new SetterInjectionPoint(m, i)));
}
} else {
// hack to invoke no-argument injectable methods required by JSR 330
groupDesires.add(new ReflectionDesire(new NoArgumentInjectionPoint(m)));
}
}
// always add signature, because a subclass without @Inject
// overrides any @Inject on the superclass's method declaration
visitedMethods.add(s);
}
for (Field f: type.getDeclaredFields()) {
if (f.getAnnotation(Inject.class) != null && !Modifier.isStatic(f.getModifiers())) {
// have not seen this field
groupDesires.add(new ReflectionDesire(new FieldInjectionPoint(f)));
}
}
type = type.getSuperclass();
}
// after reversing this list, fields will be injected
// before methods as required
Collections.reverse(groupDesires);
desires.addAll(groupDesires);
return Collections.unmodifiableList(desires);
}
private final transient Class<?> desiredType;
private final transient InjectionPoint injectPoint;
private final transient Satisfaction satisfaction;
/**
* Create a ReflectionDesire that immediately wraps the given
* InjectionPoint. The desired type equals the type declared by the
* injection point. The created desire will have a satisfaction if the
* injection point's type is satisfiable.
*
* @param injectPoint The injection point to wrap
* @throws NullPointerException if injectPoint is null
*/
public ReflectionDesire(InjectionPoint injectPoint) {
this(injectPoint.getErasedType(), injectPoint, null);
}
/**
* Create a ReflectionDesire that represents the dependency for
* <tt>desiredType</tt> that will be injected into the given InjectionPoint.
* The optional satisfaction will satisfy this desire. If null is provided,
* and the desired type instantiable, a ClassSatisfaction is created.
*
* @param desiredType The desired type of the dependency
* @param injectPoint The injection point of the desire
* @param satisfaction The satisfaction satisfying this desire, if there is
* one
* @throws NullPointerException if desiredType, injectPoint, or dfltSource is null
* @throws IllegalArgumentException if desiredType is not assignable to the
* type of the injection point, or if the satisfaction's type is
* not assignable to the desired type
*/
public ReflectionDesire(Class<?> desiredType, InjectionPoint injectPoint,
Satisfaction satisfaction) {
Preconditions.notNull("desired type", desiredType);
Preconditions.notNull("injection point", injectPoint);
desiredType = Types.box(desiredType);
Preconditions.isAssignable(injectPoint.getErasedType(), desiredType);
if (satisfaction != null) {
Preconditions.isAssignable(desiredType, satisfaction.getErasedType());
}
// try and find a satisfaction
if (satisfaction == null) {
if (Types.shouldBeInstantiable(desiredType)) {
if (Types.isInstantiable(desiredType)) {
satisfaction = new ClassSatisfaction(desiredType);
} // else don't satisfy this, even if the injection point is null
} else if (injectPoint.isNullable()) {
// we only default to null if the injection point depended on
// an interface. if they ask for a concrete type, it's a bug
// for that type not to be injectable
satisfaction = new NullSatisfaction(desiredType);
}
}
this.desiredType = desiredType;
this.injectPoint = injectPoint;
this.satisfaction = satisfaction;
}
@Override
public Class<?> getDesiredType() {
return desiredType;
}
@Override
public InjectionPoint getInjectionPoint() {
return injectPoint;
}
@Override
public boolean isInstantiable() {
return satisfaction != null;
}
@Override
public Satisfaction getSatisfaction() {
return satisfaction;
}
@Override
public Desire restrict(Class<?> type) {
return new ReflectionDesire(type, injectPoint, null);
}
@Override
public Desire restrict(Satisfaction satis) {
return new ReflectionDesire(satis.getErasedType(), injectPoint, satis);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ReflectionDesire)) {
return false;
}
ReflectionDesire r = (ReflectionDesire) o;
return (r.desiredType.equals(desiredType) &&
r.injectPoint.equals(injectPoint) &&
(r.satisfaction == null ? satisfaction == null : r.satisfaction.equals(satisfaction)));
}
@Override
public int hashCode() {
return desiredType.hashCode() ^ injectPoint.hashCode() ^ (satisfaction == null ? 0 : satisfaction.hashCode());
}
@Override
public String toString() {
return "Desire(" + desiredType.getSimpleName() + ", " + injectPoint + ")";
}
private Object writeReplace() {
return new SerialProxy(desiredType, injectPoint, satisfaction);
}
private void readObject(ObjectInputStream stream) throws ObjectStreamException {
throw new InvalidObjectException("must use serialization proxy");
}
private static class SerialProxy implements Serializable {
private static final long serialVersionUID = 1L;
private final InjectionPoint injectionPoint;
private final ClassProxy desiredType;
private final Satisfaction satisfaction;
public SerialProxy(Class<?> type, InjectionPoint ip, Satisfaction sat) {
injectionPoint = ip;
desiredType = ClassProxy.of(type);
satisfaction = sat;
}
@SuppressWarnings("unchecked")
private Object readResolve() throws ObjectStreamException {
try {
return new ReflectionDesire(desiredType.resolve(),
injectionPoint,
satisfaction);
} catch (ClassNotFoundException e) {
InvalidObjectException ex = new InvalidObjectException("cannot resolve " + desiredType);
ex.initCause(e);
throw ex;
} catch (InvalidBindingException e) {
InvalidObjectException ex = new InvalidObjectException("invalid binding");
ex.initCause(e);
throw ex;
}
}
}
/*
* Internal class to track a methods signature. Java's default reflection
* doesn't give us a convenient way to record just this information.
*
* FIXME Document why we need this class more clearly
*/
public static class Signature {
private final String name;
private final Type[] args;
public Signature(Method m) {
// FIXME Make it clearer what this code is supposed to do
int mods = m.getModifiers();
if (Modifier.isPublic(mods) || Modifier.isProtected(mods)) {
// method overrides depends solely on method name
name = m.getName();
} else if (Modifier.isPrivate(mods)) {
// method overrides depend on method name and class name
name = m.getName() + m.getDeclaringClass().getCanonicalName();
} else {
// method overrides depend on method name and package,
// since it is package-private
Package pkg = m.getDeclaringClass().getPackage();
if (pkg != null) {
name = m.getName() + pkg.getName();
} else {
name = m.getName();
}
}
args = m.getGenericParameterTypes();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Signature)) {
return false;
}
Signature s = (Signature) o;
return s.name.equals(name) && Arrays.equals(args, s.args);
}
@Override
public int hashCode() {
return (name.hashCode() ^ Arrays.hashCode(args));
}
}
}