/*
 * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package sun.rmi.server;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.DataOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.lang.reflect.Method;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.StubNotFoundException;
import java.rmi.registry.Registry;
import java.rmi.server.LogStream;
import java.rmi.server.ObjID;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.rmi.server.RemoteRef;
import java.rmi.server.RemoteStub;
import java.rmi.server.Skeleton;
import java.rmi.server.SkeletonNotFoundException;
import java.security.AccessController;
import java.security.MessageDigest;
import java.security.DigestOutputStream;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import sun.rmi.registry.RegistryImpl;
import sun.rmi.runtime.Log;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import sun.security.action.GetBooleanAction;
import sun.security.action.GetPropertyAction;

/**
 * A utility class with static methods for creating stubs/proxies and
 * skeletons for remote objects.
 */
@SuppressWarnings("deprecation")
public final class Util {

    /** "server" package log level */
    static final int logLevel = LogStream.parseLevel(
        AccessController.doPrivileged(
            new GetPropertyAction("sun.rmi.server.logLevel")));

    /** server reference log */
    public static final Log serverRefLog =
        Log.getLog("sun.rmi.server.ref", "transport", Util.logLevel);

    /** cached value of property java.rmi.server.ignoreStubClasses */
    private static final boolean ignoreStubClasses =
        AccessController.doPrivileged(
            new GetBooleanAction("java.rmi.server.ignoreStubClasses")).
            booleanValue();

    /** cache of  impl classes that have no corresponding stub class */
    private static final Map<Class<?>, Void> withoutStubs =
        Collections.synchronizedMap(new WeakHashMap<Class<?>, Void>(11));

    /** parameter types for stub constructor */
    private static final Class<?>[] stubConsParamTypes = { RemoteRef.class };

    private Util() {
    }

    /**
     * Returns a proxy for the specified implClass.
     *
     * If both of the following criteria is satisfied, a dynamic proxy for
     * the specified implClass is returned (otherwise a RemoteStub instance
     * for the specified implClass is returned):
     *
     *    a) either the property java.rmi.server.ignoreStubClasses is true or
     *       a pregenerated stub class does not exist for the impl class, and
     *    b) forceStubUse is false.
     *
     * If the above criteria are satisfied, this method constructs a
     * dynamic proxy instance (that implements the remote interfaces of
     * implClass) constructed with a RemoteObjectInvocationHandler instance
     * constructed with the clientRef.
     *
     * Otherwise, this method loads the pregenerated stub class (which
     * extends RemoteStub and implements the remote interfaces of
     * implClass) and constructs an instance of the pregenerated stub
     * class with the clientRef.
     *
     * @param implClass the class to obtain remote interfaces from
     * @param clientRef the remote ref to use in the invocation handler
     * @param forceStubUse if true, forces creation of a RemoteStub
     * @throws IllegalArgumentException if implClass implements illegal
     * remote interfaces
     * @throws StubNotFoundException if problem locating/creating stub or
     * creating the dynamic proxy instance
     **/
    public static Remote createProxy(Class<?> implClass,
                                     RemoteRef clientRef,
                                     boolean forceStubUse)
        throws StubNotFoundException
    {
        Class<?> remoteClass;

        try {
            remoteClass = getRemoteClass(implClass);
        } catch (ClassNotFoundException ex ) {
            throw new StubNotFoundException(
                "object does not implement a remote interface: " +
                implClass.getName());
        }

        if (forceStubUse ||
            !(ignoreStubClasses || !stubClassExists(remoteClass)))
        {
            return createStub(remoteClass, clientRef);
        }

        final ClassLoader loader = implClass.getClassLoader();
        final Class<?>[] interfaces = getRemoteInterfaces(implClass);
        final InvocationHandler handler =
            new RemoteObjectInvocationHandler(clientRef);

        /* REMIND: private remote interfaces? */

        try {
            return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
                public Remote run() {
                    return (Remote) Proxy.newProxyInstance(loader,
                                                           interfaces,
                                                           handler);
                }});
        } catch (IllegalArgumentException e) {
            throw new StubNotFoundException("unable to create proxy", e);
        }
    }

    /**
     * Returns true if a stub class for the given impl class can be loaded,
     * otherwise returns false.
     *
     * @param remoteClass the class to obtain remote interfaces from
     */
    private static boolean stubClassExists(Class<?> remoteClass) {
        if (!withoutStubs.containsKey(remoteClass)) {
            try {
                Class.forName(remoteClass.getName() + "_Stub",
                              false,
                              remoteClass.getClassLoader());
                return true;

            } catch (ClassNotFoundException cnfe) {
                withoutStubs.put(remoteClass, null);
            }
        }
        return false;
    }

    /*
     * Returns the class/superclass that implements the remote interface.
     * @throws ClassNotFoundException if no class is found to have a
     * remote interface
     */
    private static Class<?> getRemoteClass(Class<?> cl)
        throws ClassNotFoundException
    {
        while (cl != null) {
            Class<?>[] interfaces = cl.getInterfaces();
            for (int i = interfaces.length -1; i >= 0; i--) {
                if (Remote.class.isAssignableFrom(interfaces[i]))
                    return cl;          // this class implements remote object
            }
            cl = cl.getSuperclass();
        }
        throw new ClassNotFoundException(
                "class does not implement java.rmi.Remote");
    }

    /**
     * Returns an array containing the remote interfaces implemented
     * by the given class.
     *
     * @param   remoteClass the class to obtain remote interfaces from
     * @throws  IllegalArgumentException if remoteClass implements
     *          any illegal remote interfaces
     * @throws  NullPointerException if remoteClass is null
     */
    private static Class<?>[] getRemoteInterfaces(Class<?> remoteClass) {
        ArrayList<Class<?>> list = new ArrayList<>();
        getRemoteInterfaces(list, remoteClass);
        return list.toArray(new Class<?>[list.size()]);
    }

    /**
     * Fills the given array list with the remote interfaces implemented
     * by the given class.
     *
     * @throws  IllegalArgumentException if the specified class implements
     *          any illegal remote interfaces
     * @throws  NullPointerException if the specified class or list is null
     */
    private static void getRemoteInterfaces(ArrayList<Class<?>> list, Class<?> cl) {
        Class<?> superclass = cl.getSuperclass();
        if (superclass != null) {
            getRemoteInterfaces(list, superclass);
        }

        Class<?>[] interfaces = cl.getInterfaces();
        for (int i = 0; i < interfaces.length; i++) {
            Class<?> intf = interfaces[i];
            /*
             * If it is a remote interface (if it extends from
             * java.rmi.Remote) and is not already in the list,
             * then add the interface to the list.
             */
            if (Remote.class.isAssignableFrom(intf)) {
                if (!(list.contains(intf))) {
                    Method[] methods = intf.getMethods();
                    for (int j = 0; j < methods.length; j++) {
                        checkMethod(methods[j]);
                    }
                    list.add(intf);
                }
            }
        }
    }

    /**
     * Verifies that the supplied method has at least one declared exception
     * type that is RemoteException or one of its superclasses.  If not,
     * then this method throws IllegalArgumentException.
     *
     * @throws IllegalArgumentException if m is an illegal remote method
     */
    private static void checkMethod(Method m) {
        Class<?>[] ex = m.getExceptionTypes();
        for (int i = 0; i < ex.length; i++) {
            if (ex[i].isAssignableFrom(RemoteException.class))
                return;
        }
        throw new IllegalArgumentException(
            "illegal remote method encountered: " + m);
    }

    /**
     * Creates a RemoteStub instance for the specified class, constructed
     * with the specified RemoteRef.  The supplied class must be the most
     * derived class in the remote object's superclass chain that
     * implements a remote interface.  The stub class name is the name of
     * the specified remoteClass with the suffix "_Stub".  The loading of
     * the stub class is initiated from class loader of the specified class
     * (which may be the bootstrap class loader).
     **/
    private static RemoteStub createStub(Class<?> remoteClass, RemoteRef ref)
        throws StubNotFoundException
    {
        String stubname = remoteClass.getName() + "_Stub";

        /* Make sure to use the local stub loader for the stub classes.
         * When loaded by the local loader the load path can be
         * propagated to remote clients, by the MarshalOutputStream/InStream
         * pickle methods
         */
        try {
            Class<?> stubcl =
                Class.forName(stubname, false, remoteClass.getClassLoader());
            Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);
            return (RemoteStub) cons.newInstance(new Object[] { ref });

        } catch (ClassNotFoundException e) {
            throw new StubNotFoundException(
                "Stub class not found: " + stubname, e);
        } catch (NoSuchMethodException e) {
            throw new StubNotFoundException(
                "Stub class missing constructor: " + stubname, e);
        } catch (InstantiationException e) {
            throw new StubNotFoundException(
                "Can't create instance of stub class: " + stubname, e);
        } catch (IllegalAccessException e) {
            throw new StubNotFoundException(
                "Stub class constructor not public: " + stubname, e);
        } catch (InvocationTargetException e) {
            throw new StubNotFoundException(
                "Exception creating instance of stub class: " + stubname, e);
        } catch (ClassCastException e) {
            throw new StubNotFoundException(
                "Stub class not instance of RemoteStub: " + stubname, e);
        }
    }

    /**
     * Locate and return the Skeleton for the specified remote object
     */
    static Skeleton createSkeleton(Remote object)
        throws SkeletonNotFoundException
    {
        Class<?> cl;
        try {
            cl = getRemoteClass(object.getClass());
        } catch (ClassNotFoundException ex ) {
            throw new SkeletonNotFoundException(
                "object does not implement a remote interface: " +
                object.getClass().getName());
        }

        // now try to load the skeleton based ont he name of the class
        String skelname = cl.getName() + "_Skel";
        try {
            Class<?> skelcl = Class.forName(skelname, false, cl.getClassLoader());

            return (Skeleton)skelcl.newInstance();
        } catch (ClassNotFoundException ex) {
            throw new SkeletonNotFoundException("Skeleton class not found: " +
                                                skelname, ex);
        } catch (InstantiationException ex) {
            throw new SkeletonNotFoundException("Can't create skeleton: " +
                                                skelname, ex);
        } catch (IllegalAccessException ex) {
            throw new SkeletonNotFoundException("No public constructor: " +
                                                skelname, ex);
        } catch (ClassCastException ex) {
            throw new SkeletonNotFoundException(
                "Skeleton not of correct class: " + skelname, ex);
        }
    }

    /**
     * Compute the "method hash" of a remote method.  The method hash
     * is a long containing the first 64 bits of the SHA digest from
     * the UTF encoded string of the method name and descriptor.
     */
    public static long computeMethodHash(Method m) {
        long hash = 0;
        ByteArrayOutputStream sink = new ByteArrayOutputStream(127);
        try {
            MessageDigest md = MessageDigest.getInstance("SHA");
            DataOutputStream out = new DataOutputStream(
                new DigestOutputStream(sink, md));

            String s = getMethodNameAndDescriptor(m);
            if (serverRefLog.isLoggable(Log.VERBOSE)) {
                serverRefLog.log(Log.VERBOSE,
                    "string used for method hash: \"" + s + "\"");
            }
            out.writeUTF(s);

            // use only the first 64 bits of the digest for the hash
            out.flush();
            byte hasharray[] = md.digest();
            for (int i = 0; i < Math.min(8, hasharray.length); i++) {
                hash += ((long) (hasharray[i] & 0xFF)) << (i * 8);
            }
        } catch (IOException ignore) {
            /* can't happen, but be deterministic anyway. */
            hash = -1;
        } catch (NoSuchAlgorithmException complain) {
            throw new SecurityException(complain.getMessage());
        }
        return hash;
    }

    /**
     * Return a string consisting of the given method's name followed by
     * its "method descriptor", as appropriate for use in the computation
     * of the "method hash".
     *
     * See section 4.3.3 of The Java Virtual Machine Specification for
     * the definition of a "method descriptor".
     */
    private static String getMethodNameAndDescriptor(Method m) {
        StringBuffer desc = new StringBuffer(m.getName());
        desc.append('(');
        Class<?>[] paramTypes = m.getParameterTypes();
        for (int i = 0; i < paramTypes.length; i++) {
            desc.append(getTypeDescriptor(paramTypes[i]));
        }
        desc.append(')');
        Class<?> returnType = m.getReturnType();
        if (returnType == void.class) { // optimization: handle void here
            desc.append('V');
        } else {
            desc.append(getTypeDescriptor(returnType));
        }
        return desc.toString();
    }

    /**
     * Get the descriptor of a particular type, as appropriate for either
     * a parameter or return type in a method descriptor.
     */
    private static String getTypeDescriptor(Class<?> type) {
        if (type.isPrimitive()) {
            if (type == int.class) {
                return "I";
            } else if (type == boolean.class) {
                return "Z";
            } else if (type == byte.class) {
                return "B";
            } else if (type == char.class) {
                return "C";
            } else if (type == short.class) {
                return "S";
            } else if (type == long.class) {
                return "J";
            } else if (type == float.class) {
                return "F";
            } else if (type == double.class) {
                return "D";
            } else if (type == void.class) {
                return "V";
            } else {
                throw new Error("unrecognized primitive type: " + type);
            }
        } else if (type.isArray()) {
            /*
             * According to JLS 20.3.2, the getName() method on Class does
             * return the VM type descriptor format for array classes (only);
             * using that should be quicker than the otherwise obvious code:
             *
             *     return "[" + getTypeDescriptor(type.getComponentType());
             */
            return type.getName().replace('.', '/');
        } else {
            return "L" + type.getName().replace('.', '/') + ";";
        }
    }

    /**
     * Returns the binary name of the given type without package
     * qualification.  Nested types are treated no differently from
     * top-level types, so for a nested type, the returned name will
     * still be qualified with the simple name of its enclosing
     * top-level type (and perhaps other enclosing types), the
     * separator will be '$', etc.
     **/
    public static String getUnqualifiedName(Class<?> c) {
        String binaryName = c.getName();
        return binaryName.substring(binaryName.lastIndexOf('.') + 1);
    }
}
