/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.openejb.util.proxy;

import org.apache.openejb.util.Debug;
import org.apache.xbean.asm5.ClassWriter;
import org.apache.xbean.asm5.Label;
import org.apache.xbean.asm5.MethodVisitor;
import org.apache.xbean.asm5.Opcodes;
import org.apache.xbean.asm5.Type;

import javax.ejb.EJBException;
import java.io.Serializable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

public class LocalBeanProxyFactory implements Opcodes {

    public static final InvocationHandler NON_BUSINESS_HANDLER = new NonBusinessHandler();

    private static final String BUSSINESS_HANDLER_NAME = "businessHandler";
    private static final String NON_BUSINESS_HANDLER_NAME = "nonBusinessHandler";
    private static final ReentrantLock LOCK = new ReentrantLock();

    public static Object newProxyInstance(final ClassLoader classLoader, final InvocationHandler handler, final Class classToSubclass, final Class... interfaces) throws IllegalArgumentException {
        try {
            final Class proxyClass = createProxy(classToSubclass, classLoader, interfaces);
            return constructProxy(proxyClass, handler);
        } catch (final Throwable e) {
            throw new InternalError("LocalBeanProxyFactory.newProxyInstance: " + Debug.printStackTrace(e));
        }
    }

    public static void setInvocationHandler(final Object proxy, final InvocationHandler invocationHandler) {
        try {
            final Field field = proxy.getClass().getDeclaredField(BUSSINESS_HANDLER_NAME);
            field.setAccessible(true);
            try {
                field.set(proxy, invocationHandler);
            } finally {
                field.setAccessible(false);
            }
        } catch (final NoSuchFieldException | IllegalAccessException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static InvocationHandler getInvocationHandler(final Object proxy) {
        try {
            final Field field = proxy.getClass().getDeclaredField(BUSSINESS_HANDLER_NAME);
            field.setAccessible(true);
            try {
                return (InvocationHandler) field.get(proxy);
            } finally {
                field.setAccessible(false);
            }
        } catch (final NoSuchFieldException | IllegalAccessException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static Object constructProxy(final Class clazz, final InvocationHandler handler) throws IllegalStateException {

        final Object instance = Unsafe.allocateInstance(clazz);

        Unsafe.setValue(getDeclaredField(clazz, BUSSINESS_HANDLER_NAME), instance, handler);
        Unsafe.setValue(getDeclaredField(clazz, NON_BUSINESS_HANDLER_NAME), instance, NON_BUSINESS_HANDLER);

        return instance;
    }

    private static Field getDeclaredField(final Class clazz, final String fieldName) {
        try {
            return clazz.getDeclaredField(fieldName);
        } catch (final NoSuchFieldException e) {
            final String message = String.format("Proxy class does not contain expected field \"%s\": %s", fieldName, clazz.getName());
            throw new IllegalStateException(message, e);
        }
    }

    public static boolean isProxy(final Class<?> clazz) {
        return clazz.isAnnotationPresent(Proxy.class);
    }

    public static Class createProxy(final Class<?> classToProxy, final ClassLoader cl, final String proxyName, final Class... interfaces) {
        final String classFileName = proxyName.replace('.', '/');

        try {
            return cl.loadClass(proxyName);
        } catch (final Exception e) {
            // no-op
        }

        final ReentrantLock lock = LocalBeanProxyFactory.LOCK;
        lock.lock();

        try {

            try { // Try it again, another thread may have beaten this one...
                return cl.loadClass(proxyName);
            } catch (final Exception e) {
                // no-op
            }

            final byte[] proxyBytes = generateProxy(classToProxy, classFileName, interfaces);
            return Unsafe.defineClass(cl, classToProxy, proxyName, proxyBytes);

        } catch (final Exception e) {
            throw new InternalError("LocalBeanProxyFactory.createProxy: " + Debug.printStackTrace(e));
        } finally {
            lock.unlock();
        }
    }

    public static Class createProxy(final Class<?> classToProxy, final ClassLoader cl, final Class... interfaces) {
        return createProxy(classToProxy, cl, classToProxy.getName() + "$$LocalBeanProxy", interfaces);
    }

    public static byte[] generateProxy(final Class<?> classToProxy, final String proxyName, final Class<?>... interfaces) throws ProxyGenerationException {
        final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        final String proxyClassFileName = proxyName.replace('.', '/');
        final String classFileName = classToProxy.getName().replace('.', '/');

        // push class signature
        final String[] interfaceNames = new String[interfaces.length];
        for (int i = 0; i < interfaces.length; i++) {
            final Class<?> anInterface = interfaces[i];
            interfaceNames[i] = anInterface.getName().replace('.', '/');
        }

        cw.visit(V1_5, ACC_PUBLIC + ACC_SUPER, proxyClassFileName, null, classFileName, interfaceNames);
        cw.visitSource(classFileName + ".java", null);

        cw.visitAnnotation("L" + Proxy.class.getName().replace('.', '/') + ";", true).visitEnd();

        // push InvocationHandler fields
        cw.visitField(ACC_FINAL + ACC_PRIVATE, BUSSINESS_HANDLER_NAME, "Ljava/lang/reflect/InvocationHandler;", null, null).visitEnd();
        cw.visitField(ACC_FINAL + ACC_PRIVATE, NON_BUSINESS_HANDLER_NAME, "Ljava/lang/reflect/InvocationHandler;", null, null).visitEnd();

        final Map<String, List<Method>> methodMap = new HashMap<String, List<Method>>();

        getNonPrivateMethods(classToProxy, methodMap);

        for (final Class<?> anInterface : interfaces) {
            getNonPrivateMethods(anInterface, methodMap);
        }

        // Iterate over the public methods
        for (final Map.Entry<String, List<Method>> entry : methodMap.entrySet()) {

            for (final Method method : entry.getValue()) {
                final String name = method.getName();

                if (Modifier.isPublic(method.getModifiers())
                    || method.getParameterTypes().length == 0 && ("finalize".equals(name)
                    || "clone".equals(name))) {
                    // forward invocations of any public methods or 
                    // finalize/clone methods to businessHandler 
                    processMethod(cw, method, proxyClassFileName, BUSSINESS_HANDLER_NAME);
                } else {
                    // forward invocations of any other methods to nonBusinessHandler
                    processMethod(cw, method, proxyClassFileName, NON_BUSINESS_HANDLER_NAME);
                }
            }
        }

        return cw.toByteArray();
    }

    private static void getNonPrivateMethods(Class<?> clazz, final Map<String, List<Method>> methodMap) {
        while (clazz != null) {
            for (final Method method : clazz.getDeclaredMethods()) {
                if (method.isBridge()) {
                    continue;
                }

                final int modifiers = method.getModifiers();
                if (Modifier.isFinal(modifiers)
                    || Modifier.isPrivate(modifiers)
                    || Modifier.isStatic(modifiers)) {
                    continue;
                }

                List<Method> methods = methodMap.get(method.getName());
                if (methods == null) {
                    methods = new ArrayList<Method>();
                    methods.add(method);
                    methodMap.put(method.getName(), methods);
                } else {
                    if (!isOverridden(methods, method)) {
                        // method is not overridden, so add it
                        methods.add(method);
                    } // else method is overridden in superclass, so do nothing
                }
            }

            clazz = clazz.getSuperclass();
        }
    }

    private static boolean isOverridden(final List<Method> methods, final Method method) {
        for (final Method m : methods) {
            if (Arrays.equals(m.getParameterTypes(), method.getParameterTypes())) {
                return true;
            }
        }
        return false;
    }

    public static void processMethod(final ClassWriter cw, final Method method, final String proxyName, final String handlerName) throws ProxyGenerationException {
        if ("<init>".equals(method.getName())) {
            return;
        }

        visit(cw, method, proxyName, handlerName).visitEnd();
    }

    public static MethodVisitor visit(final ClassWriter cw, final Method method, final String proxyName, final String handlerName) throws ProxyGenerationException {
        final Class<?> returnType = method.getReturnType();
        final Class<?>[] parameterTypes = method.getParameterTypes();
        final Class<?>[] exceptionTypes = method.getExceptionTypes();
        final int modifiers = method.getModifiers();

        // push the method definition
        int modifier = 0;
        if (Modifier.isPublic(modifiers)) {
            modifier = ACC_PUBLIC;
        } else if (Modifier.isProtected(modifiers)) {
            modifier = ACC_PROTECTED;
        }

        final MethodVisitor mv = cw.visitMethod(modifier, method.getName(), getMethodSignatureAsString(returnType, parameterTypes), null, null);
        mv.visitCode();

        // push try/catch block, to catch declared exceptions, and to catch java.lang.Throwable
        final Label l0 = new Label();
        final Label l1 = new Label();
        final Label l2 = new Label();

        if (exceptionTypes.length > 0) {
            mv.visitTryCatchBlock(l0, l1, l2, "java/lang/reflect/InvocationTargetException");
        }

        // push try code
        mv.visitLabel(l0);
        final String classNameToOverride = method.getDeclaringClass().getName().replace('.', '/');
        mv.visitLdcInsn(Type.getType("L" + classNameToOverride + ";"));

        // the following code generates the bytecode for this line of Java:
        // Method method = <proxy>.class.getMethod("add", new Class[] { <array of function argument classes> });

        // get the method name to invoke, and push to stack
        mv.visitLdcInsn(method.getName());

        // create the Class[]
        createArrayDefinition(mv, parameterTypes.length, Class.class);

        int length = 1;

        // push parameters into array
        for (int i = 0; i < parameterTypes.length; i++) {
            // keep copy of array on stack
            mv.visitInsn(DUP);

            final Class<?> parameterType = parameterTypes[i];

            // push number onto stack
            pushIntOntoStack(mv, i);

            if (parameterType.isPrimitive()) {
                final String wrapperType = getWrapperType(parameterType);
                mv.visitFieldInsn(GETSTATIC, wrapperType, "TYPE", "Ljava/lang/Class;");
            } else {
                mv.visitLdcInsn(Type.getType(getAsmTypeAsString(parameterType, true)));
            }

            mv.visitInsn(AASTORE);

            if (Long.TYPE.equals(parameterType) || Double.TYPE.equals(parameterType)) {
                length += 2;
            } else {
                length++;
            }
        }

        // invoke getMethod() with the method name and the array of types
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getDeclaredMethod", "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;", false);

        // store the returned method for later
        mv.visitVarInsn(ASTORE, length);

        // the following code generates bytecode equivalent to:
        // return ((<returntype>) invocationHandler.invoke(this, method, new Object[] { <function arguments }))[.<primitive>Value()];

        final Label l4 = new Label();
        mv.visitLabel(l4);
        mv.visitVarInsn(ALOAD, 0);

        // get the invocationHandler field from this class
        mv.visitFieldInsn(GETFIELD, proxyName, handlerName, "Ljava/lang/reflect/InvocationHandler;");

        // we want to pass "this" in as the first parameter
        mv.visitVarInsn(ALOAD, 0);

        // and the method we fetched earlier
        mv.visitVarInsn(ALOAD, length);

        // need to construct the array of objects passed in

        // create the Object[]
        createArrayDefinition(mv, parameterTypes.length, Object.class);

        int index = 1;
        // push parameters into array
        for (int i = 0; i < parameterTypes.length; i++) {
            // keep copy of array on stack
            mv.visitInsn(DUP);

            final Class<?> parameterType = parameterTypes[i];

            // push number onto stack
            pushIntOntoStack(mv, i);

            if (parameterType.isPrimitive()) {
                final String wrapperType = getWrapperType(parameterType);
                mv.visitVarInsn(getVarInsn(parameterType), index);

                mv.visitMethodInsn(INVOKESTATIC, wrapperType, "valueOf", "(" + getPrimitiveLetter(parameterType) + ")L" + wrapperType + ";", false);
                mv.visitInsn(AASTORE);

                if (Long.TYPE.equals(parameterType) || Double.TYPE.equals(parameterType)) {
                    index += 2;
                } else {
                    index++;
                }
            } else {
                mv.visitVarInsn(ALOAD, index);
                mv.visitInsn(AASTORE);
                index++;
            }
        }

        // invoke the invocationHandler
        mv.visitMethodInsn(INVOKEINTERFACE, "java/lang/reflect/InvocationHandler", "invoke", "(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;", true);

        // cast the result
        mv.visitTypeInsn(CHECKCAST, getCastType(returnType));

        if (returnType.isPrimitive() && !Void.TYPE.equals(returnType)) {
            // get the primitive value
            mv.visitMethodInsn(INVOKEVIRTUAL, getWrapperType(returnType), getPrimitiveMethod(returnType), "()" + getPrimitiveLetter(returnType), false);
        }

        // push return
        mv.visitLabel(l1);
        if (!Void.TYPE.equals(returnType)) {
            mv.visitInsn(getReturnInsn(returnType));
        } else {
            mv.visitInsn(POP);
            mv.visitInsn(RETURN);
        }

        // catch InvocationTargetException
        if (exceptionTypes.length > 0) {
            mv.visitLabel(l2);
            mv.visitVarInsn(ASTORE, length);

            final Label l5 = new Label();
            mv.visitLabel(l5);

            for (int i = 0; i < exceptionTypes.length; i++) {
                final Class<?> exceptionType = exceptionTypes[i];

                mv.visitLdcInsn(Type.getType("L" + exceptionType.getName().replace('.', '/') + ";"));
                mv.visitVarInsn(ALOAD, length);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/reflect/InvocationTargetException", "getCause", "()Ljava/lang/Throwable;", false);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "equals", "(Ljava/lang/Object;)Z", false);

                final Label l6 = new Label();
                mv.visitJumpInsn(IFEQ, l6);

                final Label l7 = new Label();
                mv.visitLabel(l7);

                mv.visitVarInsn(ALOAD, length);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/reflect/InvocationTargetException", "getCause", "()Ljava/lang/Throwable;", false);
                mv.visitTypeInsn(CHECKCAST, exceptionType.getName().replace('.', '/'));
                mv.visitInsn(ATHROW);
                mv.visitLabel(l6);

                if (i == exceptionTypes.length - 1) {
                    mv.visitTypeInsn(NEW, "java/lang/reflect/UndeclaredThrowableException");
                    mv.visitInsn(DUP);
                    mv.visitVarInsn(ALOAD, length);
                    mv.visitMethodInsn(INVOKESPECIAL, "java/lang/reflect/UndeclaredThrowableException", "<init>", "(Ljava/lang/Throwable;)V", false);
                    mv.visitInsn(ATHROW);
                }
            }
        }

        // finish this method
        mv.visitMaxs(0, 0);
        return mv;
    }

    /**
     * Gets the appropriate bytecode instruction for RETURN, according to what type we need to return
     *
     * @param type Type the needs to be returned
     * @return The matching bytecode instruction
     */
    private static int getReturnInsn(final Class<?> type) {
        if (type.isPrimitive()) {
            if (Integer.TYPE.equals(type)) {
                return IRETURN;
            } else if (Boolean.TYPE.equals(type)) {
                return IRETURN;
            } else if (Character.TYPE.equals(type)) {
                return IRETURN;
            } else if (Byte.TYPE.equals(type)) {
                return IRETURN;
            } else if (Short.TYPE.equals(type)) {
                return IRETURN;
            } else if (Float.TYPE.equals(type)) {
                return FRETURN;
            } else if (Long.TYPE.equals(type)) {
                return LRETURN;
            } else if (Double.TYPE.equals(type)) {
                return DRETURN;
            }
        }

        return ARETURN;
    }

    /**
     * Returns the appropriate bytecode instruction to load a value from a variable to the stack
     *
     * @param type Type to load
     * @return Bytecode instruction to use
     */
    private static int getVarInsn(final Class<?> type) {
        if (type.isPrimitive()) {
            if (Integer.TYPE.equals(type)) {
                return ILOAD;
            } else if (Boolean.TYPE.equals(type)) {
                return ILOAD;
            } else if (Character.TYPE.equals(type)) {
                return ILOAD;
            } else if (Byte.TYPE.equals(type)) {
                return ILOAD;
            } else if (Short.TYPE.equals(type)) {
                return ILOAD;
            } else if (Float.TYPE.equals(type)) {
                return FLOAD;
            } else if (Long.TYPE.equals(type)) {
                return LLOAD;
            } else if (Double.TYPE.equals(type)) {
                return DLOAD;
            }
        }

        throw new IllegalStateException("Type: " + type.getCanonicalName() + " is not a primitive type");
    }

    /**
     * Returns the name of the Java method to call to get the primitive value from an Object - e.g. intValue for java.lang.Integer
     *
     * @param type Type whose primitive method we want to lookup
     * @return The name of the method to use
     */
    private static String getPrimitiveMethod(final Class<?> type) {
        if (Integer.TYPE.equals(type)) {
            return "intValue";
        } else if (Boolean.TYPE.equals(type)) {
            return "booleanValue";
        } else if (Character.TYPE.equals(type)) {
            return "charValue";
        } else if (Byte.TYPE.equals(type)) {
            return "byteValue";
        } else if (Short.TYPE.equals(type)) {
            return "shortValue";
        } else if (Float.TYPE.equals(type)) {
            return "floatValue";
        } else if (Long.TYPE.equals(type)) {
            return "longValue";
        } else if (Double.TYPE.equals(type)) {
            return "doubleValue";
        }

        throw new IllegalStateException("Type: " + type.getCanonicalName() + " is not a primitive type");
    }

    /**
     * Gets the string to use for CHECKCAST instruction, returning the correct value for any type, including primitives and arrays
     *
     * @param returnType The type to cast to with CHECKCAST
     * @return CHECKCAST parameter
     */
    static String getCastType(final Class<?> returnType) {
        if (returnType.isPrimitive()) {
            return getWrapperType(returnType);
        } else {
            return getAsmTypeAsString(returnType, false);
        }
    }

    /**
     * Returns the wrapper type for a primitive, e.g. java.lang.Integer for int
     */
    private static String getWrapperType(final Class<?> type) {
        if (Integer.TYPE.equals(type)) {
            return Integer.class.getCanonicalName().replace('.', '/');
        } else if (Boolean.TYPE.equals(type)) {
            return Boolean.class.getCanonicalName().replace('.', '/');
        } else if (Character.TYPE.equals(type)) {
            return Character.class.getCanonicalName().replace('.', '/');
        } else if (Byte.TYPE.equals(type)) {
            return Byte.class.getCanonicalName().replace('.', '/');
        } else if (Short.TYPE.equals(type)) {
            return Short.class.getCanonicalName().replace('.', '/');
        } else if (Float.TYPE.equals(type)) {
            return Float.class.getCanonicalName().replace('.', '/');
        } else if (Long.TYPE.equals(type)) {
            return Long.class.getCanonicalName().replace('.', '/');
        } else if (Double.TYPE.equals(type)) {
            return Double.class.getCanonicalName().replace('.', '/');
        } else if (Void.TYPE.equals(type)) {
            return Void.class.getCanonicalName().replace('.', '/');
        }

        throw new IllegalStateException("Type: " + type.getCanonicalName() + " is not a primitive type");
    }

    /**
     * Invokes the most appropriate bytecode instruction to put a number on the stack
     */
    private static void pushIntOntoStack(final MethodVisitor mv, final int i) {
        if (i == 0) {
            mv.visitInsn(ICONST_0);
        } else if (i == 1) {
            mv.visitInsn(ICONST_1);
        } else if (i == 2) {
            mv.visitInsn(ICONST_2);
        } else if (i == 3) {
            mv.visitInsn(ICONST_3);
        } else if (i == 4) {
            mv.visitInsn(ICONST_4);
        } else if (i == 5) {
            mv.visitInsn(ICONST_5);
        } else if (i > 5 && i <= 255) {
            mv.visitIntInsn(BIPUSH, i);
        } else {
            mv.visitIntInsn(SIPUSH, i);
        }
    }

    /**
     * pushes an array of the specified size to the method visitor. The generated bytecode will leave
     * the new array at the top of the stack.
     *
     * @param mv   MethodVisitor to use
     * @param size Size of the array to create
     * @param type Type of array to create
     * @throws ProxyGenerationException
     */
    private static void createArrayDefinition(final MethodVisitor mv, final int size, final Class<?> type) throws ProxyGenerationException {
        // create a new array of java.lang.class (2)

        if (size < 0) {
            throw new ProxyGenerationException("Array size cannot be less than zero");
        }

        pushIntOntoStack(mv, size);

        mv.visitTypeInsn(ANEWARRAY, type.getCanonicalName().replace('.', '/'));
    }

    static String getMethodSignatureAsString(final Class<?> returnType, final Class<?>[] parameterTypes) {
        final StringBuilder builder = new StringBuilder();
        builder.append("(");
        for (final Class<?> parameterType : parameterTypes) {
            builder.append(getAsmTypeAsString(parameterType, true));
        }

        builder.append(")");
        builder.append(getAsmTypeAsString(returnType, true));

        return builder.toString();
    }

    /**
     * Returns the single letter that matches the given primitive in bytecode instructions
     */
    private static String getPrimitiveLetter(final Class<?> type) {
        if (Integer.TYPE.equals(type)) {
            return "I";
        } else if (Void.TYPE.equals(type)) {
            return "V";
        } else if (Boolean.TYPE.equals(type)) {
            return "Z";
        } else if (Character.TYPE.equals(type)) {
            return "C";
        } else if (Byte.TYPE.equals(type)) {
            return "B";
        } else if (Short.TYPE.equals(type)) {
            return "S";
        } else if (Float.TYPE.equals(type)) {
            return "F";
        } else if (Long.TYPE.equals(type)) {
            return "J";
        } else if (Double.TYPE.equals(type)) {
            return "D";
        }

        throw new IllegalStateException("Type: " + type.getCanonicalName() + " is not a primitive type");
    }

    /**
     * Converts a class to a String suitable for ASM.
     *
     * @param parameterType Class to convert
     * @param wrap          True if a non-array object should be wrapped with L and ; - e.g. Ljava/lang/Integer;
     * @return String to use for ASM
     */
    public static String getAsmTypeAsString(final Class<?> parameterType, final boolean wrap) {
        if (parameterType.isArray()) {
            if (parameterType.getComponentType().isPrimitive()) {
                final Class<?> componentType = parameterType.getComponentType();
                return "[" + getPrimitiveLetter(componentType);
            } else {
                return "[" + getAsmTypeAsString(parameterType.getComponentType(), true);
            }
        } else {
            if (parameterType.isPrimitive()) {
                return getPrimitiveLetter(parameterType);
            } else {
                String className = parameterType.getCanonicalName();

                if (parameterType.isMemberClass()) {
                    final int lastDot = className.lastIndexOf(".");
                    className = className.substring(0, lastDot) + "$" + className.substring(lastDot + 1);
                }

                if (wrap) {
                    return "L" + className.replace('.', '/') + ";";
                } else {
                    return className.replace('.', '/');
                }
            }
        }
    }

    static class NonBusinessHandler implements InvocationHandler, Serializable {

        @Override
        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
            throw new EJBException("Calling non-public methods of a local bean without any interfaces is not allowed");
        }

    }

    /**
     * The methods of this class model sun.misc.Unsafe which is used reflectively
     */
    public static class Unsafe {

        // sun.misc.Unsafe
        private static final Object unsafe;
        private static final Method defineClass;
        private static final Method allocateInstance;
        private static final Method putObject;
        private static final Method objectFieldOffset;

        static {
            final Class<?> unsafeClass;
            try {
                unsafeClass = AccessController.doPrivileged(new PrivilegedAction<Class<?>>() {
                    @Override
                    public Class<?> run() {
                        try {
                            return Thread.currentThread().getContextClassLoader().loadClass("sun.misc.Unsafe");
                        } catch (final Exception e) {
                            try {
                                return ClassLoader.getSystemClassLoader().loadClass("sun.misc.Unsafe");
                            } catch (final ClassNotFoundException e1) {
                                throw new IllegalStateException("Cannot get sun.misc.Unsafe", e);
                            }
                        }
                    }
                });
            } catch (final Exception e) {
                throw new IllegalStateException("Cannot get sun.misc.Unsafe class", e);
            }

            unsafe = AccessController.doPrivileged(new PrivilegedAction<Object>() {
                @Override
                public Object run() {
                    try {
                        final Field field = unsafeClass.getDeclaredField("theUnsafe");
                        field.setAccessible(true);
                        return field.get(null);
                    } catch (final Exception e) {
                        throw new IllegalStateException("Cannot get sun.misc.Unsafe", e);
                    }
                }
            });
            allocateInstance = AccessController.doPrivileged(new PrivilegedAction<Method>() {
                @Override
                public Method run() {
                    try {
                        final Method mtd = unsafeClass.getDeclaredMethod("allocateInstance", Class.class);
                        mtd.setAccessible(true);
                        return mtd;
                    } catch (final Exception e) {
                        throw new IllegalStateException("Cannot get sun.misc.Unsafe.allocateInstance", e);
                    }
                }
            });
            objectFieldOffset = AccessController.doPrivileged(new PrivilegedAction<Method>() {
                @Override
                public Method run() {
                    try {
                        final Method mtd = unsafeClass.getDeclaredMethod("objectFieldOffset", Field.class);
                        mtd.setAccessible(true);
                        return mtd;
                    } catch (final Exception e) {
                        throw new IllegalStateException("Cannot get sun.misc.Unsafe.objectFieldOffset", e);
                    }
                }
            });
            putObject = AccessController.doPrivileged(new PrivilegedAction<Method>() {
                @Override
                public Method run() {
                    try {
                        final Method mtd = unsafeClass.getDeclaredMethod("putObject", Object.class, long.class, Object.class);
                        mtd.setAccessible(true);
                        return mtd;
                    } catch (final Exception e) {
                        throw new IllegalStateException("Cannot get sun.misc.Unsafe.putObject", e);
                    }
                }
            });
            defineClass = AccessController.doPrivileged(new PrivilegedAction<Method>() {
                @Override
                public Method run() {
                    try {
                        final Method mtd = unsafeClass.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class);
                        mtd.setAccessible(true);
                        return mtd;
                    } catch (final Exception e) {
                        throw new IllegalStateException("Cannot get sun.misc.Unsafe.defineClass", e);
                    }
                }
            });
        }

        private static Object allocateInstance(final Class clazz) {
            try {
                return allocateInstance.invoke(unsafe, clazz);
            } catch (final IllegalAccessException e) {
                throw new IllegalStateException("Failed to allocateInstance of Proxy class " + clazz.getName(), e);
            } catch (final InvocationTargetException e) {
                final Throwable throwable = e.getTargetException() != null ? e.getTargetException() : e;
                throw new IllegalStateException("Failed to allocateInstance of Proxy class " + clazz.getName(), throwable);
            }
        }

        private static void setValue(final Field field, final Object object, final Object value) {
            final long offset;
            try {
                offset = (Long) objectFieldOffset.invoke(unsafe, field);
            } catch (final Exception e) {
                throw new IllegalStateException("Failed getting offset for: field=" + field.getName() + "  class=" + field.getDeclaringClass().getName(), e);
            }

            try {
                putObject.invoke(unsafe, object, offset, value);
            } catch (final Exception e) {
                throw new IllegalStateException("Failed putting field=" + field.getName() + "  class=" + field.getDeclaringClass().getName(), e);
            }
        }

        // it is super important to pass a classloader as first parameter otherwise if API class is in a "permanent" classloader then it will leak
        public static Class defineClass(final ClassLoader loader, final Class<?> clsToProxy, final String proxyName, final byte[] proxyBytes) throws IllegalAccessException, InvocationTargetException {
            return (Class<?>) defineClass.invoke(unsafe, proxyName, proxyBytes, 0, proxyBytes.length, loader, clsToProxy.getProtectionDomain());
        }
    }

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    private @interface Proxy {

    }
}
