blob: 466fc4182e3890c9e405d3d2f7452176359ea161 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2014 Stuart McCulloch
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Stuart McCulloch - initial API and implementation
*******************************************************************************/
package org.eclipse.sisu.peaberry.internal;
import static java.lang.reflect.Modifier.ABSTRACT;
import static java.lang.reflect.Modifier.FINAL;
import static java.lang.reflect.Modifier.NATIVE;
import static java.lang.reflect.Modifier.PRIVATE;
import static java.lang.reflect.Modifier.PUBLIC;
import static java.lang.reflect.Modifier.STATIC;
import static java.lang.reflect.Modifier.SYNCHRONIZED;
import static java.util.Collections.addAll;
import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS;
import static org.objectweb.asm.Opcodes.ACONST_NULL;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ASTORE;
import static org.objectweb.asm.Opcodes.ATHROW;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.IFNONNULL;
import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.IRETURN;
import static org.objectweb.asm.Opcodes.ISTORE;
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.PUTFIELD;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.objectweb.asm.Opcodes.V1_5;
import static org.objectweb.asm.Type.VOID;
import static org.objectweb.asm.Type.getArgumentTypes;
import static org.objectweb.asm.Type.getDescriptor;
import static org.objectweb.asm.Type.getInternalName;
import static org.objectweb.asm.Type.getMethodDescriptor;
import static org.objectweb.asm.Type.getReturnType;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.sisu.peaberry.Import;
import org.eclipse.sisu.peaberry.ServiceUnavailableException;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
/**
* Around-advice glue, specifically optimized for the {@link Import} concept.
*
* @author mcculls@gmail.com (Stuart McCulloch)
*/
final class ImportGlue {
// instances not allowed
private ImportGlue() {}
private static final String UNAVAILABLE_NAME = getInternalName(ServiceUnavailableException.class);
private static final String EXCEPTION_NAME = getInternalName(Exception.class);
private static final String IMPORT_NAME = getInternalName(Import.class);
private static final String OBJECT_NAME = getInternalName(Object.class);
private static final String VOID_DESC = "()V";
private static final String IMPORT_DESC = getDescriptor(Import.class);
private static final String OBJECT_DESC = getDescriptor(Object.class);
private static final Method[] OBJECT_METHODS = Object.class.getMethods();
private static final String PROXY_SUFFIX = "$pbryglu";
private static final String PROXY_HANDLE = "__pbry__";
static String getProxyName(final String clazzName) {
final StringBuilder tmpName = new StringBuilder();
// support proxy of java.* interfaces by changing the package space
if (clazzName.startsWith("java.") || clazzName.startsWith("java/")) {
tmpName.append('$');
}
return tmpName.append(clazzName).append(PROXY_SUFFIX).toString();
}
static String getClazzName(final String proxyName) {
final int head = '$' == proxyName.charAt(0) ? 1 : 0;
final int tail = proxyName.lastIndexOf(PROXY_SUFFIX);
if (tail > 0) {
return proxyName.substring(head, tail);
}
return proxyName;
}
static byte[] generateProxy(final Class<?> clazz) {
final String clazzName = getInternalName(clazz);
final String proxyName = getProxyName(clazzName);
final String superName;
final String[] interfaceNames;
if (clazz.isInterface()) {
superName = OBJECT_NAME;
interfaceNames = new String[]{clazzName};
} else {
superName = clazzName;
interfaceNames = getInternalNames(clazz.getInterfaces());
}
final ClassWriter cw = new ClassWriter(COMPUTE_MAXS);
cw.visit(V1_5, PUBLIC | FINAL, proxyName, null, superName, interfaceNames);
// single Import<T> constructor
init(cw, superName, proxyName);
// for the moment only proxy the public API...
final Method[] publicAPI = clazz.getMethods();
final List<Method> methods = new ArrayList<Method>(publicAPI.length + OBJECT_METHODS.length);
addAll(methods, publicAPI);
if (clazz.isInterface()) {
// patch in any missing Object methods...
for (final Method m : OBJECT_METHODS) {
if (missingMethod(publicAPI, m)) {
methods.add(m);
}
}
}
for (final Method m : methods) {
// we cannot proxy any static or final methods
if ((m.getModifiers() & (STATIC | FINAL)) == 0) {
wrap(cw, proxyName, m);
}
}
cw.visitEnd();
return cw.toByteArray();
}
private static void init(final ClassWriter cw, final String superName, final String proxyName) {
cw.visitField(PRIVATE | FINAL, PROXY_HANDLE, IMPORT_DESC, null, null).visitEnd();
final MethodVisitor v = cw.visitMethod(PUBLIC, "<init>", '(' + IMPORT_DESC + ")V", null, null);
v.visitCode();
// store Import<T> handle
v.visitVarInsn(ALOAD, 0);
v.visitInsn(DUP);
v.visitVarInsn(ALOAD, 1);
v.visitFieldInsn(PUTFIELD, proxyName, PROXY_HANDLE, IMPORT_DESC);
v.visitMethodInsn(INVOKESPECIAL, superName, "<init>", VOID_DESC);
v.visitInsn(RETURN);
v.visitMaxs(0, 0);
v.visitEnd();
}
@SuppressWarnings("PMD.ExcessiveMethodLength")
private static void wrap(final ClassWriter cw, final String proxyName, final Method method) {
final String methodName = method.getName();
final String descriptor = getMethodDescriptor(method);
final String[] exceptions = getInternalNames(method.getExceptionTypes());
// simple delegating proxy, so don't need synchronization on wrapper method
final int modifiers = method.getModifiers() & ~(ABSTRACT | NATIVE | SYNCHRONIZED);
final MethodVisitor v = cw.visitMethod(modifiers, methodName, descriptor, null, exceptions);
final Label start = new Label();
final Label invoke = new Label();
final Label end = new Label();
final Label ungetR = new Label();
final Label finalR = new Label();
final Label catchX = new Label();
final Label ungetX = new Label();
final Label finalX = new Label();
v.visitCode();
// support try{ get(); } finally { unget(); } model
v.visitTryCatchBlock(start, ungetR, catchX, null);
v.visitTryCatchBlock(ungetR, finalR, finalR, EXCEPTION_NAME);
v.visitTryCatchBlock(ungetX, finalX, finalX, EXCEPTION_NAME);
// store handle as "this"
v.visitVarInsn(ALOAD, 0);
v.visitFieldInsn(GETFIELD, proxyName, PROXY_HANDLE, IMPORT_DESC);
v.visitInsn(DUP);
v.visitVarInsn(ASTORE, 0);
v.visitLabel(start);
// dereference handle to get actual service instance
v.visitMethodInsn(INVOKEINTERFACE, IMPORT_NAME, "get", "()" + OBJECT_DESC);
v.visitInsn(DUP);
// null => ServiceUnavailableException
v.visitJumpInsn(IFNONNULL, invoke);
v.visitTypeInsn(NEW, UNAVAILABLE_NAME);
v.visitInsn(DUP);
v.visitMethodInsn(INVOKESPECIAL, UNAVAILABLE_NAME, "<init>", "()V");
v.visitInsn(ATHROW);
v.visitLabel(invoke);
final Class<?> clazz = method.getDeclaringClass();
final String subjectName = getInternalName(clazz);
if (!clazz.isInterface()) {
v.visitTypeInsn(CHECKCAST, subjectName);
}
int i = 1;
for (final Type t : getArgumentTypes(method)) {
v.visitVarInsn(t.getOpcode(ILOAD), i);
i = i + t.getSize();
}
// delegate to real method
if (clazz.isInterface()) {
v.visitMethodInsn(INVOKEINTERFACE, subjectName, methodName, descriptor);
} else {
v.visitMethodInsn(INVOKEVIRTUAL, subjectName, methodName, descriptor);
}
final Type returnType = getReturnType(method);
if (VOID != returnType.getSort()) {
v.visitVarInsn(returnType.getOpcode(ISTORE), 1);
}
v.visitLabel(ungetR);
// unget on return
v.visitVarInsn(ALOAD, 0);
v.visitMethodInsn(INVOKEINTERFACE, IMPORT_NAME, "unget", VOID_DESC);
v.visitInsn(ACONST_NULL);
v.visitLabel(finalR);
if (VOID != returnType.getSort()) {
v.visitVarInsn(returnType.getOpcode(ILOAD), 1);
}
v.visitInsn(returnType.getOpcode(IRETURN));
// cache initial exception
v.visitLabel(catchX);
v.visitVarInsn(ASTORE, 1);
v.visitLabel(ungetX);
// unget on exception
v.visitVarInsn(ALOAD, 0);
v.visitMethodInsn(INVOKEINTERFACE, IMPORT_NAME, "unget", VOID_DESC);
v.visitInsn(ACONST_NULL);
v.visitLabel(finalX);
// restore initial exception
v.visitVarInsn(ALOAD, 1);
v.visitInsn(ATHROW);
v.visitLabel(end);
v.visitMaxs(0, 0);
v.visitEnd();
}
private static String[] getInternalNames(final Class<?>... clazzes) {
final String[] names = new String[clazzes.length];
for (int i = 0; i < names.length; i++) {
names[i] = getInternalName(clazzes[i]);
}
return names;
}
private static boolean missingMethod(final Method[] methods, final Method method) {
final String sig = Arrays.toString(method.getParameterTypes());
final String name = method.getName();
for (final Method m : methods) {
// just filter on method name and signature, ignore the declaring class...
if (name.equals(m.getName()) && sig.equals(Arrays.toString(m.getParameterTypes()))) {
return false;
}
}
return true;
}
}