package com.laytonsmith.PureUtilities.ClassLoading.ClassMirror; import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscovery; import com.laytonsmith.PureUtilities.Common.ClassUtils; import com.laytonsmith.PureUtilities.Common.StringUtils; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Attribute; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; /** * This class gathers information about a class, without actually loading * the class into memory. Most of the methods in {@link java.lang.Class} are * available in this class (or have an equivalent Mirror version). * @param <T> */ public class ClassMirror<T> implements Serializable { private static final long serialVersionUID = 1L; private final ClassInfo info = new ClassInfo(); //Transient, because it's only used during construction private final transient org.objectweb.asm.ClassReader reader; /** * If this is just a wrapper for an already loaded Class, this will * be non-null, and should override all the existing methods with * the wrapped return. */ private final Class underlyingClass; /** * The original URL that houses this class. **/ private final URL originalURL; /** * Creates a ClassMirror object for a given input stream representing * a class file. * @param is * @param container * @throws IOException */ public ClassMirror(InputStream is, URL container) throws IOException { reader = new org.objectweb.asm.ClassReader(is); underlyingClass = null; originalURL = container; parse(); } /** * Creates a ClassMirror object for a given class file. * @param file * @throws FileNotFoundException * @throws IOException */ public ClassMirror(File file) throws FileNotFoundException, IOException { this(new FileInputStream(file), file.toURI().toURL()); } /** * Creates a ClassMirror object from an already loaded Class. While this * obviously defeats the purpose of not loading the Class into PermGen, this * does allow already loaded classes to fit into the ClassMirror ecosystem. * Essentially, calls to the ClassMirror are simply forwarded to the Class and * the return re-wrapped in sub mirror types. Some operations are not possible, * namely non-runtime annotation processing, but in general, all other operations * work the same. * @param c */ public ClassMirror(Class c){ this.underlyingClass = c; reader = null; originalURL = ClassDiscovery.GetClassContainer(c); } private void parse(){ reader.accept(info, org.objectweb.asm.ClassReader.SKIP_CODE | org.objectweb.asm.ClassReader.SKIP_DEBUG | org.objectweb.asm.ClassReader.SKIP_FRAMES); } /** * Return the container that houses this class. * @return */ public URL getContainer() { return originalURL; } /** * Returns the modifiers on this class. * @return */ public ModifierMirror getModifiers(){ if(underlyingClass != null){ return new ModifierMirror(underlyingClass.getModifiers()); } return info.modifiers; } /** * Returns the name of this class as recognized by the JVM, not the * common class name. Use {@link #getClassName()} instead, if you want * the common name. * @return */ public String getJVMClassName(){ if(underlyingClass != null){ return ClassUtils.getJVMName(underlyingClass); } return "L" + info.name + ";"; } /** * Returns the class name of this class. This is the "normal" name, that * is, what you would type in code to reference a class, without / or $. * @return */ public String getClassName(){ if(underlyingClass != null){ return underlyingClass.getName().replace("$", "."); } return info.name.replaceAll("[/$]", "."); } /** * Returns true if this class is an enum * @return */ public boolean isEnum(){ if(underlyingClass != null){ return underlyingClass.isEnum(); } return info.isEnum; } /** * Returns true if this class is an interface. * @return */ public boolean isInterface(){ if(underlyingClass != null){ return underlyingClass.isInterface(); } return info.isInterface; } /** * Returns true iff the underlying class is an abstract class (not an interface). * @return */ public boolean isAbstract() { if(underlyingClass != null){ return (underlyingClass.getModifiers() & Modifier.ABSTRACT) > 0; } return info.modifiers.isAbstract(); } /** * Returns a {@link ClassReferenceMirror} to the class's superclass. * @return */ public ClassReferenceMirror getSuperClass(){ if(underlyingClass != null){ return ClassReferenceMirror.fromClass(underlyingClass.getSuperclass()); } return new ClassReferenceMirror("L" + info.superClass + ";"); } /** * Returns a list of {@link ClassReferenceMirror}s of all the interfaces that * this implements. * @return */ public List<ClassReferenceMirror> getInterfaces(){ List<ClassReferenceMirror> l = new ArrayList<>(); if(underlyingClass != null){ for(Class inter : underlyingClass.getInterfaces()){ l.add(ClassReferenceMirror.fromClass(inter)); } } else { for(String inter : info.interfaces){ l.add(new ClassReferenceMirror("L" + inter + ";")); } } return l; } /** * Returns true if this class contains the annotation specified. * @param annotation * @return */ public boolean hasAnnotation(Class<? extends Annotation> annotation){ if(underlyingClass != null){ return underlyingClass.getAnnotation(annotation) != null; } String name = ClassUtils.getJVMName(annotation); for(AnnotationMirror a : info.annotations){ if(a.getType().getJVMName().equals(name)){ return true; } } return false; } /** * Because ClassMirror works with annotations that were declared as * either {@link RetentionPolicy#CLASS} or {@link RetentionPolicy#RUNTIME}, * you may also want to check visibility. If this returns false, then the * class does have the annotation, but {@link Class#getAnnotation(java.lang.Class)} * would return false. If the class doesn't have the annotation, null is returned. * Note that if this ClassMirror was initialized from a loaded Class object, this * may not return correct information, because it essentially will be returning * the result of {@link #hasAnnotation(java.lang.Class)}, since there is no way * to tell if an annotation is anything but runtime. * @param annotation * @return */ public Boolean isAnnotationVisible(Class<? extends Annotation> annotation){ if(underlyingClass != null){ return hasAnnotation(annotation); } String name = ClassUtils.getJVMName(annotation); for(AnnotationMirror a : info.annotations){ if(a.getType().getJVMName().equals(name)){ return a.isVisible(); } } return null; } /** * Returns the annotation defined on this class. * @param clazz * @return */ public AnnotationMirror getAnnotation(Class<? extends Annotation> clazz){ if(underlyingClass != null){ Annotation ann = underlyingClass.getAnnotation(clazz); if(ann == null){ return null; } return new AnnotationMirror(ann); } String name = ClassUtils.getJVMName(clazz); for(AnnotationMirror a : info.annotations){ if(a.getType().getJVMName().equals(name)){ return a; } } return null; } /** * Returns a list of annotations on this class. * @return */ public List<AnnotationMirror> getAnnotations(){ if(underlyingClass != null){ List<AnnotationMirror> list = new ArrayList<>(); for(Annotation a : underlyingClass.getAnnotations()){ list.add(new AnnotationMirror(ClassReferenceMirror.fromClass(a.annotationType()), true)); } return list; } return new ArrayList<>(info.annotations); } /** * Loads the corresponding Annotation type for this field * or method. This actually loads the Annotation class into memory. * This is equivalent to getAnnotation(type).getProxy(type), however * this checks for null first, and returns null instead of causing a NPE. * In the case that this is a wrapper for a real Class object, this simply * returns the real Annotation object (or null). * @param <T> * @param type * @return */ public <T extends Annotation> T loadAnnotation(Class<T> type) { if(underlyingClass != null){ return (T) underlyingClass.getAnnotation(type); } AnnotationMirror mirror = getAnnotation(type); if(mirror == null){ return null; } return mirror.getProxy(type); } /** * Returns the fields in this class. This works like * {@link Class#getDeclaredFields()}, as only the methods in * this class are loaded. * @return */ public FieldMirror[] getFields(){ if(underlyingClass != null){ FieldMirror[] fields = new FieldMirror[this.underlyingClass.getDeclaredFields().length]; for(int i = 0; i < fields.length; i++){ Field f = this.underlyingClass.getDeclaredFields()[i]; fields[i] = new FieldMirror(f); } return fields; } return info.fields.toArray(new FieldMirror[info.fields.size()]); } /** * Returns the field, given by name. This does not traverse the * Object hierarchy, unlike {@link Class#getField(java.lang.String)}. * @param name * @return * @throws java.lang.NoSuchFieldException */ public FieldMirror getField(String name) throws NoSuchFieldException { for(FieldMirror m : getFields()){ if(m.getName().equals(name)){ return m; } } throw new NoSuchFieldException("The field \"" + name + "\" was not found."); } /** * Returns the methods in this class. This traverses the parent Object * heirarchy if the methods are apart of the visible interface, as well as * private methods in this class itself. * @return */ public MethodMirror[] getMethods(){ if(underlyingClass != null){ MethodMirror[] mirrors = new MethodMirror[underlyingClass.getDeclaredMethods().length]; for(int i = 0; i < mirrors.length; i++){ mirrors[i] = new MethodMirror(underlyingClass.getDeclaredMethods()[i]); } return mirrors; } return info.methods.toArray(new MethodMirror[info.methods.size()]); } /** * Returns the method, given by name. This traverses the parent Object heirarchy * if the methods are apart of the visible interface, as well as private methods * in this class itself. * @param name * @param params * @return * @throws java.lang.NoSuchMethodException */ public MethodMirror getMethod(String name, Class...params) throws NoSuchMethodException{ ClassReferenceMirror mm [] = new ClassReferenceMirror[params.length]; for(int i = 0; i < params.length; i++){ mm[i] = new ClassReferenceMirror(ClassUtils.getJVMName(params[i])); } return getMethod(name, mm); } /** * Returns the method, given by name. This traverses the parent Object heirarchy * if the methods are apart of the visible interface, as well as private methods * in this class itself. * @param name * @param params * @return * @throws NoSuchMethodException */ public MethodMirror getMethod(String name, ClassReferenceMirror... params) throws NoSuchMethodException { List<ClassReferenceMirror> crmParams = new ArrayList<>(); crmParams.addAll(Arrays.asList(params)); for(MethodMirror m : getMethods()){ if(m.getName().equals(name) && m.getParams().equals(crmParams)){ return m; } } throw new NoSuchMethodException("No method matching the signature " + name + "(" + StringUtils.Join(crmParams, ", ") + ") was found."); } /** * Loads the class into memory and returns the class object. For this * call to succeed, the class must otherwise be on the class path. The standard * class loader is used, and the class is initialized. If this is a wrapper * for an already loaded Class object, that object is simply returned. * @return */ public Class<T> loadClass() throws NoClassDefFoundError { if(underlyingClass != null){ return underlyingClass; } try{ return (Class<T>)info.classReferenceMirror.loadClass(); } catch(ClassNotFoundException ex){ throw new NoClassDefFoundError(); } } /** * Loads the class into memory and returns the class object. For this * call to succeed, the classloader specified must be able to find the class. * If this is a wrapper for an already loaded Class object, that object is simply * returned. * @param loader * @param initialize * @return */ public Class<T> loadClass(ClassLoader loader, boolean initialize) throws NoClassDefFoundError { if(underlyingClass != null){ return underlyingClass; } try{ return info.classReferenceMirror.loadClass(loader, initialize); } catch(ClassNotFoundException ex){ throw new NoClassDefFoundError(ex.getMessage()); } } /** * Returns true if this class either extends or implements the class * specified, or is the same as that class. Note that if it transiently * extends from this class, it can't necessarily find that information without * actually loading the intermediate class, so this is a less useful method * than {@link Class#isAssignableFrom(java.lang.Class)}, however, in combination * with a system that is aware of all classes in a class ecosystem, this can * be used to piece together that information without actually loading the * classes. * @param superClass * @return */ public boolean directlyExtendsFrom(Class superClass){ if(underlyingClass != null){ return (underlyingClass.getSuperclass() == superClass); } String name = superClass.getName().replace(".", "/"); if(info.superClass.equals(name)){ return true; } for(String in : info.interfaces){ if(in.equals(name)){ return true; } } return false; } /** * Returns the Package this class is in. If this is not in a package, * null is returned. * @return */ public PackageMirror getPackage(){ if(underlyingClass != null){ return new PackageMirror(underlyingClass.getPackage().getName()); } String[] split = getClassName().split("\\."); if(split.length == 1){ return null; } StringBuilder b = new StringBuilder(); for(int i = 0; i < split.length - 1; i++){ if(i != 0){ b.append("."); } b.append(split[i]); } return new PackageMirror(b.toString()); } /** * Returns the simple name of this class. I.e. for java.lang.String, "String" is * returned. * @return */ public String getSimpleName(){ if(underlyingClass != null){ return underlyingClass.getSimpleName(); } String[] split = getClassName().split("\\."); return split[split.length - 1]; } /** * Returns a string representation of this object. The string will match the toString * that would be generated by that of the Class object. * @return */ @Override public String toString() { return (isInterface()? "interface": (isEnum()?"enum":"class")) + " " + getClassName(); } /** * Returns a {@link ClassReferenceMirror} to the object. This is useful * for classes that may not exist, as it doesn't require an actual known * reference to the class to exist. * @return */ public ClassReferenceMirror getClassReference() { if(underlyingClass != null){ return ClassReferenceMirror.fromClass(underlyingClass); } return new ClassReferenceMirror(getJVMClassName()); } @Override public int hashCode() { int hash = 5; hash = 97 * hash + Objects.hashCode(this.getJVMClassName()); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final ClassMirror<?> other = (ClassMirror<?>) obj; return Objects.equals(this.getJVMClassName(), other.getJVMClassName()); } private static class ClassInfo implements ClassVisitor, Serializable { private static final long serialVersionUID = 1L; public ModifierMirror modifiers; public String name; public String superClass; public String[] interfaces; public List<AnnotationMirror> annotations = new ArrayList<>(); public boolean isInterface = false; public boolean isEnum = false; public ClassReferenceMirror classReferenceMirror; public List<FieldMirror> fields = new ArrayList<>(); public List<MethodMirror> methods = new ArrayList<>(); @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { if((access & Opcodes.ACC_ENUM) > 0){ isEnum = true; } if((access & Opcodes.ACC_INTERFACE) > 0){ isInterface = true; } this.modifiers = new ModifierMirror(ModifierMirror.Type.CLASS, access); this.name = name; //We know we aren't an array or a primitive, so we just add L...; to make //the binary name, which is what ClassReferenceMirror expects. this.classReferenceMirror = new ClassReferenceMirror("L" + name + ";"); this.superClass = superName; this.interfaces = interfaces; } @Override public void visitSource(String source, String debug) { //Ignored } @Override public void visitOuterClass(String owner, String name, String desc) { //Ignored } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { AnnotationMirror am = new AnnotationMirror(new ClassReferenceMirror(desc), visible); annotations.add(am); return new AnnotationV(am); } @Override public void visitAttribute(Attribute attr) { } @Override public void visitInnerClass(String name, String outerName, String innerName, int access) { } @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { final FieldMirror fm = new FieldMirror(classReferenceMirror, new ModifierMirror(ModifierMirror.Type.FIELD, access), new ClassReferenceMirror(desc), name, value); fields.add(fm); return new FieldVisitor() { @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { AnnotationMirror m = new AnnotationMirror(new ClassReferenceMirror(desc), visible); fm.addAnnotation(m); return new AnnotationV(m); } @Override public void visitAttribute(Attribute attr) { } @Override public void visitEnd() { } }; } private static transient final Pattern METHOD_SIGNATURE_PATTERN = Pattern.compile("^\\((.*)\\)(.*)$"); @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if("<init>".equals(name) || "<clinit>".equals(name)){ //For now, we aren't interested in constructors or static initializers return null; } Matcher m = METHOD_SIGNATURE_PATTERN.matcher(desc); if(!m.find()){ //The desc type didn't match? throw new Error("No match found for " + desc); } String inner = m.group(1); String ret = m.group(2); List<ClassReferenceMirror> params = new ArrayList<ClassReferenceMirror>(); //Parsing the params list is a bit more complicated than it should be. StringBuilder b = new StringBuilder(); boolean inObject = false; for(char c : inner.toCharArray()){ b.append(c); if(inObject){ if(c == ';'){ inObject = false; params.add(new ClassReferenceMirror(b.toString())); b = new StringBuilder(); } } else { if(c == 'L'){ inObject = true; } else if(c != '['){ params.add(new ClassReferenceMirror(b.toString())); b = new StringBuilder(); } //otherwise, it's an array, continue. } } final MethodMirror mm = new MethodMirror(classReferenceMirror, new ModifierMirror(ModifierMirror.Type.METHOD, access), new ClassReferenceMirror(ret), name, params, (access & Opcodes.ACC_VARARGS) > 0, (access & Opcodes.ACC_SYNTHETIC) > 0); methods.add(mm); return new MethodVisitor() { @Override public AnnotationVisitor visitAnnotationDefault() { return null; } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { AnnotationMirror am = new AnnotationMirror(new ClassReferenceMirror(desc), visible); mm.addAnnotation(am); return new AnnotationV(am); } @Override public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { return null; } @Override public void visitAttribute(Attribute attr) { } @Override public void visitCode() { } @Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { } @Override public void visitInsn(int opcode) { } @Override public void visitIntInsn(int opcode, int operand) { } @Override public void visitVarInsn(int opcode, int var) { } @Override public void visitTypeInsn(int opcode, String type) { } @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc) { } @Override public void visitJumpInsn(int opcode, Label label) { } @Override public void visitLabel(Label label) { } @Override public void visitLdcInsn(Object cst) { } @Override public void visitIincInsn(int var, int increment) { } @Override public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { } @Override public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { } @Override public void visitMultiANewArrayInsn(String desc, int dims) { } @Override public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { } @Override public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) { } @Override public void visitLineNumber(int line, Label start) { } @Override public void visitMaxs(int maxStack, int maxLocals) { } @Override public void visitEnd() { } }; } @Override public void visitEnd() { } } private static class AnnotationV implements AnnotationVisitor { private final AnnotationMirror mirror; public AnnotationV(AnnotationMirror mirror){ this.mirror = mirror; } @Override public void visit(String name, Object value) { if(value instanceof org.objectweb.asm.Type){ //Type can't serialize, so we need to store a reference to it. //This will only happen if it's a class type, so a ClassReferenceMirror //is what we need anyways. org.objectweb.asm.Type type = (org.objectweb.asm.Type) value; value = new ClassReferenceMirror(type.getDescriptor()); } mirror.addAnnotationValue(name, value); } @Override public void visitEnum(String name, String desc, String value) { } @Override public AnnotationVisitor visitAnnotation(String name, String desc) { return null; } @Override public AnnotationVisitor visitArray(String name) { return null; } @Override public void visitEnd() { } } }