package openmods.include; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Throwables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; import openmods.Log; import openmods.asm.StopTransforming; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.commons.Method; public class IncludingClassVisitor extends ClassVisitor { private class AnnotatedFieldFinder extends FieldVisitor implements IIncludedMethodBuilder { public final String fieldName; public final Type fieldType; public AnnotatedFieldFinder(String fieldName, Type fieldType, FieldVisitor fv) { super(Opcodes.ASM5, fv); this.fieldName = fieldName; this.fieldType = fieldType; } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { AnnotationVisitor av = super.visitAnnotation(desc, visible); Type t = Type.getType(desc); if (INCLUDE_INTERFACE.equals(t)) return new IncludeAnnotationVisitor(av, this); return av; } @Override public Type getInterfaceType(Type annotationHint) { return Objects.firstNonNull(annotationHint, fieldType); } @Override public MethodAdder createMethod(Type wrappedInterface) { return new MethodAdder(wrappedInterface) { @Override public void visitInterfaceAccess(MethodVisitor target) { target.visitFieldInsn(Opcodes.GETFIELD, clsName, fieldName, fieldType.getDescriptor()); } }; } } private class AnnotatedMethodFinder extends MethodVisitor implements IIncludedMethodBuilder { private final Method method; public AnnotatedMethodFinder(Method method, MethodVisitor mv) { super(Opcodes.ASM5, mv); this.method = method; } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { AnnotationVisitor av = super.visitAnnotation(desc, visible); Type t = Type.getType(desc); if (INCLUDE_INTERFACE.equals(t)) return new IncludeAnnotationVisitor(av, this); if (INCLUDE_OVERRIDE.equals(t)) overrides.add(method); return av; } @Override public Type getInterfaceType(Type annotationHint) { return Objects.firstNonNull(annotationHint, method.getReturnType()); } @Override public MethodAdder createMethod(Type wrappedInterface) { return new MethodAdder(wrappedInterface) { @Override public void visitInterfaceAccess(MethodVisitor target) { target.visitMethodInsn(Opcodes.INVOKEVIRTUAL, clsName, method.getName(), method.getDescriptor(), false); } }; } } private interface IIncludedMethodBuilder { public Type getInterfaceType(Type annotationHint); public MethodAdder createMethod(Type wrappedInterface); } private class IncludeAnnotationVisitor extends AnnotationVisitor { private Type classParam; private final IIncludedMethodBuilder builder; public IncludeAnnotationVisitor(AnnotationVisitor av, IIncludedMethodBuilder builder) { super(Opcodes.ASM5, av); this.builder = builder; } @Override public void visit(String name, Object value) { if ("value".equals(name) && value instanceof Type) classParam = (Type)value; super.visit(name, value); } @Override public void visitEnd() { addInterfaceImplementations(classParam, builder); } } private abstract static class MethodAdder { public final Type intf; private MethodAdder(Type intf) { this.intf = intf; } public abstract void visitInterfaceAccess(MethodVisitor target); public void addMethod(ClassVisitor target, Method method) { MethodVisitor mv = target.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, method.getName(), method.getDescriptor(), null, null); mv.visitVarInsn(Opcodes.ALOAD, 0); visitInterfaceAccess(mv); // should have interface reference on stack // checkcast just to be safe mv.visitTypeInsn(Opcodes.CHECKCAST, intf.getInternalName()); Type[] args = method.getArgumentTypes(); for (int i = 0; i < args.length; i++) mv.visitVarInsn(args[i].getOpcode(Opcodes.ILOAD), i + 1); mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, intf.getInternalName(), method.getName(), method.getDescriptor(), true); Type returnType = method.getReturnType(); mv.visitInsn(returnType.getOpcode(Opcodes.IRETURN)); mv.visitMaxs(args.length + 1, args.length + 1); mv.visitEnd(); } } public static final Type INCLUDE_INTERFACE = Type.getObjectType("openmods/include/IncludeInterface"); public static final Type INCLUDE_OVERRIDE = Type.getObjectType("openmods/include/IncludeOverride"); private final Set<Method> existingMethods = Sets.newHashSet(); private final Set<Method> overrides = Sets.newHashSet(); private final Map<Method, MethodAdder> methodsToAdd = Maps.newHashMap(); private String clsName; private int version; private int access; private String signature; private String superName; private final Set<String> interfaces = Sets.newHashSet(); public IncludingClassVisitor(ClassVisitor cv) { super(Opcodes.ASM5, cv); } @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { FieldVisitor parent = super.visitField(access, name, desc, signature, value); return new AnnotatedFieldFinder(name, Type.getType(desc), parent); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { Method method = new Method(name, desc); existingMethods.add(method); MethodVisitor parent = super.visitMethod(access, name, desc, signature, exceptions); return new AnnotatedMethodFinder(method, parent); } @Override public void visitEnd() { final Set<Method> conflicts = Sets.intersection(existingMethods, methodsToAdd.keySet()); Set<Method> nonMarked = Sets.difference(conflicts, overrides); Preconditions.checkState(nonMarked.isEmpty(), "%s implements interface methods %s, but they are not marked with @IncludeOverride", clsName, nonMarked); Set<Method> markedButNotImplemented = Sets.difference(overrides, methodsToAdd.keySet()); Preconditions.checkState(markedButNotImplemented.isEmpty(), "%s marks methods %s with @IncludeOverride, but no interface implements it", clsName, markedButNotImplemented); Map<Method, MethodAdder> filtered = Maps.filterKeys(methodsToAdd, new Predicate<Method>() { @Override public boolean apply(Method m) { return !conflicts.contains(m); } }); for (Map.Entry<Method, MethodAdder> m : filtered.entrySet()) m.getValue().addMethod(cv, m.getKey()); // risky, but should work, since we are only replacing interfaces super.visit(version, access, clsName, signature, superName, interfaces.toArray(new String[interfaces.size()])); super.visitEnd(); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { if (Modifier.isInterface(access)) throw new StopTransforming(); this.clsName = name; this.access = access; this.version = version; this.signature = signature; this.superName = superName; this.interfaces.addAll(Arrays.asList(interfaces)); super.visit(version, access, clsName, signature, superName, interfaces); } private List<Method> getInterfaceMethods(Type intf) { Preconditions.checkState(intf.getSort() == Type.OBJECT, "%s is not interface (including class = %s)", intf, clsName); try { Class<?> loaded = Class.forName(intf.getClassName()); Preconditions.checkArgument(loaded.isInterface(), "%s is not interface (including class = %s)", loaded, clsName); List<Method> result = Lists.newArrayList(); for (java.lang.reflect.Method m : loaded.getMethods()) result.add(Method.getMethod(m)); return result; } catch (Throwable t) { Log.severe(t, "Error while searching for interface '%s'", intf); throw Throwables.propagate(t); } } private void addInterfaceImplementations(Type annotationHint, IIncludedMethodBuilder builder) { Type wrappedInterface = builder.getInterfaceType(annotationHint); boolean notYetDeclared = interfaces.add(wrappedInterface.getInternalName()); Preconditions.checkState(notYetDeclared, "%s already implements interface %s", clsName, wrappedInterface); Log.debug("Adding interface %s to %s", wrappedInterface.getInternalName(), clsName); for (Method m : getInterfaceMethods(wrappedInterface)) { MethodAdder prev = methodsToAdd.put(m, builder.createMethod(wrappedInterface)); if (prev != null) Preconditions.checkState(overrides.contains(m), "Included method '%s' conflict, interfaces = %s,%s", m, wrappedInterface, prev.intf); } } }