All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dorkbox.network.rmi.CachedMethod Maven / Gradle / Ivy

/*
 * Copyright 2010 dorkbox, llc
 *
 * Licensed 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.
 *
 * Copyright (c) 2008, Nathan Sweet
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided with the distribution.
 * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package dorkbox.network.rmi;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.Serializer;
import com.esotericsoftware.kryo.util.IdentityMap;
import com.esotericsoftware.kryo.util.Util;
import com.esotericsoftware.reflectasm.MethodAccess;
import dorkbox.network.connection.Connection;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.connection.KryoExtra;
import dorkbox.network.util.RMISerializationManager;
import dorkbox.util.ClassHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public
class CachedMethod {
    private static final Logger logger = LoggerFactory.getLogger(CachedMethod.class);

    private static final Comparator METHOD_COMPARATOR = new Comparator() {
        @Override
        public
        int compare(Method o1, Method o2) {
            // Methods are sorted so they can be represented as an index.
            int diff = o1.getName()
                         .compareTo(o2.getName());
            if (diff != 0) {
                return diff;
            }

            Class[] argTypes1 = o1.getParameterTypes();
            Class[] argTypes2 = o2.getParameterTypes();
            if (argTypes1.length > argTypes2.length) {
                return 1;
            }

            if (argTypes1.length < argTypes2.length) {
                return -1;
            }

            for (int i = 0; i < argTypes1.length; i++) {
                diff = argTypes1[i].getName()
                                   .compareTo(argTypes2[i].getName());
                if (diff != 0) {
                    return diff;
                }
            }
            throw new RuntimeException("Two methods with same signature!"); // Impossible, should never happen
        }
    };

    // the purpose of the method cache, is to accelerate looking up methods for specific class
    private static final Map, CachedMethod[]> methodCache = new ConcurrentHashMap, CachedMethod[]>(EndPoint.DEFAULT_THREAD_POOL_SIZE);
    private static final OverriddenMethods overriddenMethods = OverriddenMethods.INSTANCE();


    // type will be likely be the interface
    public static
    CachedMethod[] getMethods(final Kryo kryo, final Class type) {
        CachedMethod[] cachedMethods = methodCache.get(type);
        if (cachedMethods != null) {
            return cachedMethods;
        }

        cachedMethods = getCachedMethods(kryo, type);
        methodCache.put(type, cachedMethods);

        return cachedMethods;
    }


    // type will be likely be the interface
    public static
    CachedMethod[] getMethods(final RMISerializationManager serializationManager, final Class type) {
        CachedMethod[] cachedMethods = methodCache.get(type);
        if (cachedMethods != null) {
            return cachedMethods;
        }

        final KryoExtra kryo = serializationManager.takeKryo();
        try {
            cachedMethods = getCachedMethods(kryo, type);
            methodCache.put(type, cachedMethods);
        } finally {
            serializationManager.returnKryo(kryo);
        }

        return cachedMethods;
    }

    private static
    CachedMethod[] getCachedMethods(final Kryo kryo, final Class type) {
        // race-conditions are OK, because we just recreate the same thing.
        final ArrayList methods = getMethods(type);
        final int size = methods.size();
        final CachedMethod[] cachedMethods = new CachedMethod[size];

        // In situations where we want to pass in the Connection (to an RMI method), we have to be able to override method A, with method B.
        // This is to support calling RMI methods from an interface (that does pass the connection reference) to
        // an implType, that DOES pass the connection reference. The remote side (that initiates the RMI calls), MUST use
        // the interface, and the implType may override the method, so that we add the connection as the first in
        // the list of parameters.
        //
        // for example:
        // Interface: foo(String x)
        //      Impl: foo(Connection c, String x)
        //
        // The implType (if it exists, with the same name, and with the same signature+connection) will be called from the interface.
        // This MUST hold valid for both remote and local connection types.

        // To facilitate this functionality, for methods with the same name, the "overriding" method is the one that inherits the Connection
        // interface as the first parameter, and  .registerRemote(ifaceClass, implClass)  must be called.
        final IdentityMap overriddenMethods = getOverriddenMethods(type, methods);
        final boolean asmEnabled = kryo.getAsmEnabled();

        MethodAccess methodAccess = null;
        // reflectASM can't get any method from the 'Object' object, and it MUST be public
        if (asmEnabled && type != Object.class && !Util.isAndroid && Modifier.isPublic(type.getModifiers())) {
            methodAccess = MethodAccess.get(type);
            if (methodAccess.getMethodNames().length == 0 && methodAccess.getParameterTypes().length == 0 &&
                methodAccess.getReturnTypes().length == 0) {

                // there was NOTHING reflectASM found, so trying to use it doesn't do us any good
                methodAccess = null;
            }
        }

        for (int i = 0; i < size; i++) {
            final Method origMethod = methods.get(i);
            Method method = origMethod; // copy because one or more can be overridden
            MethodAccess localMethodAccess = methodAccess; // copy because one or more can be overridden
            Class[] parameterTypes = method.getParameterTypes();
            Class[] asmParameterTypes = parameterTypes;

            if (overriddenMethods != null) {
                Method overriddenMethod = overriddenMethods.remove(method);
                if (overriddenMethod != null) {
                    // we can override the details of this method BECAUSE (and only because) our kryo registration override will return
                    // the correct object for this overridden method to be called on.
                    method = overriddenMethod;

                    Class overrideType = method.getDeclaringClass();
                    if (asmEnabled && !Util.isAndroid && Modifier.isPublic(overrideType.getModifiers())) {
                        localMethodAccess = MethodAccess.get(overrideType);
                        asmParameterTypes = method.getParameterTypes();
                    }
                }
            }

            CachedMethod cachedMethod = null;
            if (localMethodAccess != null) {
                try {
                    final int index = localMethodAccess.getIndex(method.getName(), asmParameterTypes);

                    AsmCachedMethod asmCachedMethod = new AsmCachedMethod();
                    asmCachedMethod.methodAccessIndex = index;
                    asmCachedMethod.methodAccess = localMethodAccess;
                    cachedMethod = asmCachedMethod;
                } catch (Exception e) {
                    if (logger.isTraceEnabled()) {
                        logger.trace("Unable to use ReflectAsm for {}.{}", method.getDeclaringClass(), method.getName(), e);
                    }
                }
            }

            if (cachedMethod == null) {
                cachedMethod = new CachedMethod();
            }
            cachedMethod.method = method;
            cachedMethod.origMethod = origMethod;
            cachedMethod.methodClassID = kryo.getRegistration(method.getDeclaringClass()).getId();
            cachedMethod.methodIndex = i;

            // Store the serializer for each final parameter.
            // ONLY for the ORIGINAL method, not he overridden one.
            cachedMethod.serializers = new Serializer[parameterTypes.length];
            for (int ii = 0, nn = parameterTypes.length; ii < nn; ii++) {
                if (kryo.isFinal(parameterTypes[ii])) {
                    cachedMethod.serializers[ii] = kryo.getSerializer(parameterTypes[ii]);
                }
            }

            cachedMethods[i] = cachedMethod;
        }

        return cachedMethods;
    }

    private static
    IdentityMap getOverriddenMethods(final Class type, final ArrayList origMethods) {
        final Class implType = overriddenMethods.get(type);

        if (implType != null) {
            final ArrayList implMethods = getMethods(implType);
            final IdentityMap overrideMap = new IdentityMap(implMethods.size());

            for (Method origMethod : origMethods) {
                String name = origMethod.getName();
                Class[] origTypes = origMethod.getParameterTypes();
                int origLength = origTypes.length + 1;

                METHOD_CHECK:
                for (Method implMethod : implMethods) {
                    String checkName = implMethod.getName();
                    Class[] checkTypes = implMethod.getParameterTypes();
                    int checkLength = checkTypes.length;

                    if (origLength != checkLength || !(name.equals(checkName))) {
                        continue;
                    }

                    // checkLength > 0
                    Class shouldBeConnectionType = checkTypes[0];
                    if (ClassHelper.hasInterface(dorkbox.network.connection.Connection.class, shouldBeConnectionType)) {
                        // now we check to see if our "check" method is equal to our "cached" method + Connection
                        if (checkLength == 1) {
                            overrideMap.put(origMethod, implMethod);
                            break;
                        }
                        else {
                            for (int k = 1; k < checkLength; k++) {
                                if (origTypes[k - 1] == checkTypes[k]) {
                                    overrideMap.put(origMethod, implMethod);
                                    break METHOD_CHECK;
                                }
                            }
                        }
                    }
                }
            }

            return overrideMap;
        }
        else {
            return null;
        }
    }

    private static
    ArrayList getMethods(final Class type) {
        final ArrayList allMethods = new ArrayList();

        Class nextClass = type;
        while (nextClass != null) {
            Collections.addAll(allMethods, nextClass.getDeclaredMethods());
            nextClass = nextClass.getSuperclass();
            if (nextClass == Object.class) {
                break;
            }
        }

        final ArrayList methods = new ArrayList(Math.max(1, allMethods.size()));
        for (int i = 0, n = allMethods.size(); i < n; i++) {
            Method method = allMethods.get(i);
            int modifiers = method.getModifiers();
            if (Modifier.isStatic(modifiers)) {
                continue;
            }
            if (Modifier.isPrivate(modifiers)) {
                continue;
            }
            if (method.isSynthetic()) {
                continue;
            }
            methods.add(method);
        }

        Collections.sort(methods, METHOD_COMPARATOR);
        return methods;
    }

    /**
     * Called by the SerializationManager, so that RMI classes that are overridden for serialization purposes, can check to see if certain
     * methods need to be overridden.
     */
    public static
    void registerOverridden(final Class ifaceClass, final Class implClass) {
        overriddenMethods.set(ifaceClass, implClass);
    }

    public Method method;
    public int methodClassID;
    public int methodIndex;

    /**
     * in some cases, we want to override the cached method, with one that supports passing 'Connection' as the first argument. This is
     * completely OPTIONAL, however - greatly adds functionality to RMI methods.
     */
    public transient Method origMethod;

    @SuppressWarnings("rawtypes")
    public Serializer[] serializers;

    public
    Object invoke(final Connection connection, Object target, Object[] args) throws IllegalAccessException, InvocationTargetException {
        // did we override our cached method?
        if (method == origMethod) {
            return this.method.invoke(target, args);
        }
        else {
            int length = args.length;
            Object[] newArgs = new Object[length + 1];
            newArgs[0] = connection;
            System.arraycopy(args, 0, newArgs, 1, length);

            return this.method.invoke(target, newArgs);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy