package com.laytonsmith.PureUtilities.ClassLoading.ClassMirror; import com.laytonsmith.PureUtilities.Common.ClassUtils; import com.laytonsmith.PureUtilities.Common.StringUtils; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** * Represents an Annotation. Most features available to annotations are available here, * though finding the default value of an annotation does require loading the annotation. */ public class AnnotationMirror implements Serializable { private static final long serialVersionUID = 1L; private final ClassReferenceMirror type; private final boolean visible; private final List<AnnotationValue> values; /** * Creates a new AnnotationMirror based an a loaded {@link Annotation}. * @param annotation */ public AnnotationMirror(Annotation annotation){ this.type = ClassReferenceMirror.fromClass(annotation.annotationType()); this.visible = true; values = new ArrayList<>(); for(Method m : annotation.annotationType().getDeclaredMethods()){ try { values.add(new AnnotationValue(m.getName(), m.invoke(annotation))); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { throw new RuntimeException(ex); } } } /* package */ AnnotationMirror(ClassReferenceMirror type, boolean visible){ this.type = type; this.visible = visible; this.values = new ArrayList<>(); } /* package */ void addAnnotationValue(String name, Object value){ values.add(new AnnotationValue(name, value)); } /** * Returns the value for this annotation. Note that this won't resolve * default annotations, as that requires actually loading the annotation class * into memory. See {@link #getValueWithDefault} if you are ok with loading * the annotation class into memory. Null is returned if this value doesn't exist. * @param forName * @return */ public Object getValue(String forName){ for(AnnotationValue value : values){ if(value.name.equals(forName)){ return value.value; } } return null; } /** * Returns the list of defined values in this annotation. Note that this * won't resolve default annotations, as that requires actually loading the * annotation class into memory. See {@link #getDefinedValuesWithDefault} if you * are ok with loading the annotation class into memory. * @return */ public List<String> getDefinedValues(){ List<String> list = new ArrayList<>(); for(AnnotationValue value : values){ list.add(value.name); } return list; } /** * Gets the value of this annotation. If the value wasn't defined * in this annotation, the default is returned by loading the annotation * Class into memory, and finding the default, and returning that. Calling * this method doesn't guarantee that the class will be loaded, however. * If the value doesn't exist, at all, this will return null. * @param forName * @return * @throws java.lang.ClassNotFoundException */ public Object getValueWithDefault(String forName) throws ClassNotFoundException{ Object value = getValue(forName); if(value != null){ return value; } //Nope, have to load it. Class c = type.loadClass(); try { Method m = c.getMethod(forName); return m.getDefaultValue(); } catch (NoSuchMethodException ex) { return null; } catch (SecurityException ex) { throw new RuntimeException(ex); } } /** * Loads the class into memory, and returns all the annotation value names. * This includes default values that weren't specified by the actual instance * of the annotation. The values returned from here must be used with * {@link #getValueWithDefault} to ensure they will return a value properly. * @return * @throws ClassNotFoundException */ public List<String> getDefinedValuesWithDefault() throws ClassNotFoundException{ List<String> ret = new ArrayList<>(); Class c = type.loadClass(); for(Method m : c.getDeclaredMethods()){ ret.add(m.getName()); } return ret; } /** * Returns the type of this annotation. * @return */ public ClassReferenceMirror getType(){ return type; } /** * Returns true if this annotation is visible. * @return */ public boolean isVisible(){ return visible; } /** * Gets a proxy annotation. When retrieving the annotation value, * getValueWithDefault is called, and the annotation's Class will for sure have * already been loaded. * * This allows for annotation values to be read from an element without having * to actually load that element (just the annotation Class is loaded), and * allowing the type safe checks of compile time. * @param <T> * @param type * @return * @throws IllegalArgumentException If AnnotationMirror doesn't represent the type * requested. */ public <T extends Annotation> T getProxy(Class<T> type) throws IllegalArgumentException { if(!this.type.getJVMName().equals(ClassUtils.getJVMName(type))){ throw new IllegalArgumentException(); } return (T) Proxy.newProxyInstance(AnnotationMirror.class.getClassLoader(), new Class[]{type}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if(("equals".equals(method.getName()) && matches(args, Object.class)) || ("hashCode".equals(method.getName()) && matches(args)) || ("toString".equals(method.getName()) && matches(args)) || ("wait".equals(method.getName()) && matches(args)) || ("wait".equals(method.getName()) && matches(args, long.class)) || ("wait".equals(method.getName()) && matches(args, long.class, int.class)) || ("getClass".equals(method.getName()) && matches(args)) || ("notify".equals(method.getName()) && matches(args)) || ("notifyAll".equals(method.getName()) && matches(args)) || ("finalize".equals(method.getName()) && matches(args)) || ("clone".equals(method.getName()) && matches(args)) ){ // Currently, we just throw an exception, because they are // actual methods defined in Object, not annotation values. // I don't know how to make this work correctly yet. throw new RuntimeException("The " + method.getName() + " method cannot be called on Annotation Proxies yet."); } return getValueWithDefault(method.getName()); } }); } private static boolean matches(Object[] args, Class ... types){ if(args.length != types.length){ return false; } for(int i = 0; i < args.length; i++){ //Can't just use == here, since the class might be a subclass if(!types[i].isAssignableFrom(args[i].getClass())){ return false; } } return true; } @Override public String toString() { return "@" + type + "(" + StringUtils.Join(values, ", ") + ")"; } @Override public int hashCode() { int hash = 7; hash = 37 * hash + Objects.hashCode(this.type); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final AnnotationMirror other = (AnnotationMirror) obj; if (!Objects.equals(this.type, other.type)) { return false; } return true; } private static class AnnotationValue implements Serializable { private static final long serialVersionUID = 1L; private String name; private Object value; public AnnotationValue(String name, Object value){ this.name = name; this.value = value; } @Override public String toString() { if(value instanceof String){ return name + " = " + StringUtils.toCodeString(value.toString()); } else { return name + " = " + value.toString(); } } @Override public int hashCode() { int hash = 7; hash = 29 * hash + Objects.hashCode(this.name); hash = 29 * hash + Objects.hashCode(this.value); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final AnnotationValue other = (AnnotationValue) obj; if (!Objects.equals(this.name, other.name)) { return false; } if (!Objects.equals(this.value, other.value)) { return false; } return true; } } }