package org.limewire.inspection;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;
import org.limewire.inject.MoreScopes;
import org.limewire.util.OSUtils;
import com.google.inject.Binding;
import com.google.inject.Injector;
import com.google.inject.Scopes;
import com.google.inject.spi.BindingScopingVisitor;
import com.google.inject.spi.BindingTargetVisitor;
import com.google.inject.spi.ConstructorBinding;
import com.google.inject.spi.ConvertedConstantBinding;
import com.google.inject.spi.ExposedBinding;
import com.google.inject.spi.InstanceBinding;
import com.google.inject.spi.LinkedKeyBinding;
import com.google.inject.spi.ProviderBinding;
import com.google.inject.spi.ProviderInstanceBinding;
import com.google.inject.spi.ProviderKeyBinding;
import com.google.inject.spi.UntargettedBinding;
/**
* Gets the value of an object that implements {@link Inspectable} or an object
* with an annotation of {@link InspectableForSize @InspectableForSize} or
* {@link InspectablePrimitive @InspectablePrimitive}.
* <p>
* See the Lime Wire Wiki for sample code using the <a href="http://www.limewire.org/wiki/index.php?title=Org.limewire.inspection">
* org.limewire.inspection</a> package.
*
*/
public class InspectionUtils {
private static class InspectionData {
String encodedField;
boolean isStatic;
Field field;
List<Annotation> annotations;
Class<?> lookupClass;
Class<?> actualClass;
Object fieldContainerInstance;
Object fieldValue;
}
/**
*
* Inspects a field and returns a representation of that field. The field
* must:
* <p>
* a) implement the <code>Inspectable</code> interface, in which case the
* return value of the <code>inspect</code> method is returned, or else
* <p>
* b) be annotated with <code>@InspectablePrimitive</code>, in which case
* the <code>String.valueOf</code> is returned, or else
* <p>
* c) be annotated with {@link InspectableForSize @InspectableForSize} and
* have a <code>size</code> method in which case the return value of the
* <code>size</code> method call is returned.
*
* @param encodedField - name of the field we want to get, starting with a
* fully qualified class name, and followed by comma-separated field
* names that will help us reach the target field.
*
* @return the object the field represents, or an Exception object if such
* was thrown trying to get it.
*/
public static Object inspectValue(String encodedField, Injector injector, boolean collectUsageData)
throws InspectionException {
try {
InspectionData data = createInspectionData(encodedField);
validateField(data, collectUsageData);
setFieldContainerInstance(data, injector);
setFieldValue(data);
return inspect(data);
} catch (Throwable e) {
if (e instanceof InspectionException) {
throw (InspectionException) e;
} else {
throw new InspectionException(e);
}
}
}
private static InspectionData createInspectionData(String encodedField) throws Throwable {
InspectionData data;
if(encodedField.contains(":")) {
data = getStaticField(encodedField);
} else {
data = getInjectedField(encodedField);
}
data.annotations = Arrays.asList(data.field.getAnnotations());
return data;
}
private static InspectionData getStaticField(String encodedField) throws Throwable {
StringTokenizer t = new StringTokenizer(encodedField, ":");
if (t.countTokens() != 2) {
throw new InspectionException("invalid encoded field: " + encodedField);
}
Class clazz = Class.forName(t.nextToken());
Field field = clazz.getDeclaredField(t.nextToken());
field.setAccessible(true);
InspectionData data = new InspectionData();
data.encodedField = encodedField;
data.isStatic = true;
data.fieldValue = clazz;
data.field = field;
return data;
}
private static InspectionData getInjectedField(String encodedField) throws Throwable {
InspectionData data = new InspectionData();
Class lookupClass = null;
if(encodedField.contains("|")) {
StringTokenizer tokenizer = new StringTokenizer(encodedField, "|");
if(tokenizer.countTokens() != 2) {
throw new InspectionException("invalid encoded field: " + encodedField);
}
lookupClass = Class.forName(tokenizer.nextToken());
encodedField = tokenizer.nextToken();
}
StringTokenizer t = new StringTokenizer(encodedField, ",");
if(t.countTokens() != 2) {
throw new InspectionException("invalid encoded field: " + encodedField);
}
Class clazz = Class.forName(t.nextToken());
Field field = clazz.getDeclaredField(t.nextToken());
field.setAccessible(true);
data.encodedField = encodedField;
data.lookupClass = lookupClass;
data.actualClass = clazz;
data.field = field;
return data;
}
private static void validateField(InspectionData data, boolean collectUsageData) throws InspectionException {
boolean valid = false;
for(Annotation annotation : data.annotations) {
if(annotation.annotationType() == InspectionPoint.class) {
validateLimitations(((InspectionPoint)annotation).requires(),
((InspectionPoint)annotation).category(), data, collectUsageData);
valid = true;
break;
} else if(annotation.annotationType() == InspectablePrimitive.class) {
validateLimitations(((InspectablePrimitive)annotation).requires(),
((InspectablePrimitive)annotation).category(), data, collectUsageData);
valid = true;
break;
} else if(annotation.annotationType() == InspectableForSize.class) {
validateLimitations(((InspectableForSize)annotation).requires(),
((InspectableForSize)annotation).category(), data, collectUsageData);
valid = true;
break;
}
}
if(!valid) {
throw new InspectionException("field not annotated for inspection: " + data.field);
}
}
private static void validateLimitations(InspectionRequirement[] limitations,
DataCategory category, InspectionData data,
boolean collectUsageData) throws InspectionException {
validateOSLimitations(limitations, data);
validateDataCategoryLimitations(category, data, collectUsageData);
}
private static void validateDataCategoryLimitations(DataCategory category, InspectionData data, boolean collectUsageData) throws InspectionException {
if(category == DataCategory.USAGE && !collectUsageData) {
throw new InspectionException("field " + data.field + " is usage data, but usage data collection not allowed");
}
}
private static void validateOSLimitations(InspectionRequirement[] limitations, InspectionData data) throws InspectionException {
boolean valid = false;
if(limitations != null && limitations.length > 0) {
for(InspectionRequirement limitation : limitations) {
switch(limitation) {
case OS_LINUX:
valid |= OSUtils.isLinux();
break;
case OS_OSX:
valid |= OSUtils.isMacOSX();
break;
case OS_WINDOWS:
valid |= OSUtils.isWindows();
break;
}
}
} else {
valid = true;
}
if(!valid) {
List<InspectionRequirement> requires = Arrays.asList(limitations);
throw new InspectionException("invalid limitations: " + requires + " on field: " + data.field, requires);
}
}
private static void setFieldContainerInstance(InspectionData data, Injector injector) throws Throwable {
// no container instance for static data.
if(!data.isStatic) {
if(data.lookupClass == null) {
// if the container had an enclosing class and it's not static, make sure the lookup
// points to the enclosing class.
if(data.actualClass.getEnclosingClass() != null && !Modifier.isStatic(data.actualClass.getModifiers())) {
data.lookupClass = data.actualClass.getEnclosingClass();
}
}
// check if this is an enclosed class
if (data.lookupClass == null) {
Binding<?> binding = injector.getBinding(data.actualClass);
validateBindingIsSingleton(injector, binding);
data.fieldContainerInstance = binding.getProvider().get();
} else {
// inner classes must be annotated properly
Binding<?> binding = injector.getBinding(data.lookupClass);
validateBindingIsSingleton(injector, binding);
Object lookupObj = binding.getProvider().get();
// If this was an enclosed class, validate that it has InspectableContainer.
if(data.actualClass.getEnclosingClass() != null && !Modifier.isStatic(data.actualClass.getModifiers())) {
if (data.actualClass.getAnnotation(InspectableContainer.class) == null) {
throw new InspectionException("container must be annotated with InspectableContainer");
}
Constructor[] constructors = data.actualClass.getDeclaredConstructors();
if (constructors.length != 1) {
throw new InspectionException("wrong constructors length: " + constructors.length);
}
Class[] parameters = constructors[0].getParameterTypes();
if (parameters.length != 1 || !data.lookupClass.isAssignableFrom(parameters[0])) {
throw new InspectionException("wrong parameter count or type for constructor");
}
constructors[0].setAccessible(true);
data.fieldContainerInstance = constructors[0].newInstance(lookupObj);
} else {
data.fieldContainerInstance = lookupObj;
}
}
}
}
private static void setFieldValue(InspectionData data) throws Throwable {
if (data.isStatic) {
data.fieldValue = data.field.get(null);
} else {
data.fieldValue = data.field.get(data.fieldContainerInstance);
}
}
private static void validateBindingIsSingleton(Injector injector, Binding<?> binding) throws InspectionException {
binding = resolveBinding(injector, binding);
boolean singleton = binding.acceptScopingVisitor(new BindingScopingVisitor<Boolean>() {
public Boolean visitEagerSingleton() { return true; }
public Boolean visitNoScoping() { return false; }
public Boolean visitScope(com.google.inject.Scope scope) { return scope == Scopes.SINGLETON || scope == MoreScopes.LAZY_SINGLETON || scope == MoreScopes.EAGER_SINGLETON; }
public Boolean visitScopeAnnotation(java.lang.Class<? extends Annotation> scopeAnnotation) { return false; }
});
if(!singleton) {
throw new InspectionException("must be singleton or lazysingleton or eagerSingleton annotation or be interface!");
}
}
/** Resolves a binding to the ultimate destination, ensuring we can check the scope of the proper link. */
private static Binding<?> resolveBinding(final Injector injector, final Binding<?> binding) {
return binding.acceptTargetVisitor(new BindingTargetVisitor<Object, Binding<?>>() {
@Override
public Binding<?> visit(InstanceBinding<? extends Object> link) {
return binding;
}
@Override
public Binding<?> visit(ProviderInstanceBinding<? extends Object> link) {
return binding;
}
@Override
public Binding<?> visit(ProviderKeyBinding<? extends Object> link) {
return resolveBinding(injector, injector.getBinding(link.getProviderKey()));
}
@Override
public Binding<?> visit(LinkedKeyBinding<? extends Object> link) {
return resolveBinding(injector, injector.getBinding(link.getLinkedKey()));
}
@Override
public Binding<?> visit(ExposedBinding<? extends Object> link) {
return binding;
}
@Override
public Binding<?> visit(UntargettedBinding<? extends Object> link) {
return binding;
}
@Override
public Binding<?> visit(ConstructorBinding<? extends Object> link) {
return binding;
}
@Override
public Binding<?> visit(ConvertedConstantBinding<? extends Object> link) {
return binding;
}
@Override
public Binding<?> visit(ProviderBinding<? extends Object> link) {
return resolveBinding(injector, injector.getBinding(link.getProvidedKey()));
}
});
}
/**
* Gets a string representation of an object.
*
* @param o the object to be inspected
* @param annotations annotations that were found in the last field traversed
* while looking for this object
* @return a String representation taken either from Inspectable.inspect(),
* String.valueOf or size() depending on the annotation or type of the field.
*/
private static Object inspect(InspectionData data) throws Exception {
if (data.fieldValue instanceof Inspectable) {
Inspectable i = (Inspectable)data.fieldValue;
return i.inspect();
}
for (Annotation a : data.annotations) {
if (a instanceof InspectablePrimitive)
return String.valueOf(data.fieldValue);
if (a instanceof InspectableForSize) {
Method m = data.fieldValue.getClass().getMethod("size", new Class[0]);
m.setAccessible(true);
return m.invoke(data.fieldValue).toString();
}
}
throw new InspectionException("no valid Inspectable annotation!");
}
}