/*
 * 
 * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source
 * License v3.0/Jini Technology Specific Attachment v 1.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://wwws.sun.com/software/jini/licensing/SCSL3_JiniTSA1.html.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Reference Code is Jini Technology Core Platform code, v2.0. The
 * Developer of the Reference Code is Sun Microsystems, Inc.
 * 
 * Contributor(s): Sun Microsystems, Inc.
 * 
 * The contents of this file comply with the Jini Technology Core
 * Platform Compatibility Kit, v2.0A.
 * 
 * Tester(s): Sun Microsystems, Inc.
 * 
 * Test Platform(s):
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Solaris(TM)SPARC/x86
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Linux (Intel x86)
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Windows (Intel Platform)
 * 
 */

package net.jini.jeri;

import com.sun.jini.jeri.internal.runtime.Util;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.server.ExportException;
import java.security.Permission;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import net.jini.core.constraint.RemoteMethodControl;
import net.jini.export.ExportPermission;
import net.jini.security.Security;

/**
 * An abstract implementation of {@link InvocationLayerFactory} that
 * provides a convenient way for subclasses to create proxies and
 * invocation dispatchers for remote objects.  A subclass must provide an
 * implementation for at least the {@link #createInvocationHandler
 * createInvocationHandler} and {@link #createInvocationDispatcher
 * createInvocationDispatcher} methods.  A subclass can override the
 * {@link #getProxyInterfaces getProxyInterfaces} method if its proxies
 * need to implement a different set of interfaces than the default
 * set (all remote interfaces of the remote object).
 *
 * @author	Sun Microsystems, Inc.
 * @since 2.0
 **/
public abstract class AbstractILFactory implements InvocationLayerFactory {

    /** Cached getClassLoader permission */
    private static final Permission getClassLoaderPermission =
	new RuntimePermission("getClassLoader");

    /** The class loader to define proxy classes in */
    private final ClassLoader loader;
    
    /**
     * Constructs an <code>AbstractILFactory</code> instance with a
     * <code>null</code> class loader.
     **/
    protected AbstractILFactory() {
	this(null);
    }

    /**
     * Constructs an <code>AbstractILFactory</code> instance with the
     * specified class loader.  The {@link #createInstances
     * createInstances} method uses the specified loader to define a proxy
     * class.
     *
     * @param	loader the class loader, or <code>null</code> 
     **/
    protected AbstractILFactory(ClassLoader loader) {
	this.loader = loader;
    }

    /**
     * Returns a concatenated array of the interfaces in <code>i1</code>
     * followed by the interfaces in <code>i2</code> with duplicate
     * interfaces (that is, duplicates after the first occurence of an
     * interface) eliminated. 
     *
     * @param	i1 an array of interfaces
     * @param	i2 an array of interfaces
     * @return 	a concatenated array of the interfaces with duplicates
     *		eliminated
     * @throws	NullPointerException if any element in <code>i1</code> or
     *		<code>i2</code> is null
     **/
    private static Class[] combineInterfaces(Class[] i1, Class[] i2) {
	ArrayList list = new ArrayList(i1.length + i2.length);
	addInterfaces(list, i1);
	addInterfaces(list, i2);
	return (Class[]) list.toArray(new Class[list.size()]);
    }

    /**
     * Adds non-duplicate interfaces in the array to the list.
     *
     * @throws	NullPointerException if the interfaces array contains a
     *		<code>null</code> element 
     **/
    private static void addInterfaces(ArrayList list, Class[] interfaces) {
	for (int i = 0; i < interfaces.length; i++) {
	    Class cl = interfaces[i];
	    if (cl == null) {
		throw new NullPointerException("array contains null element");
	    }
	    if (!list.contains(cl)) {
		list.add(cl);
	    }
	}
    }

    /**
     * Returns the class loader specified during construction.
     * @return the class loader
     */
    protected final ClassLoader getClassLoader() {
	return loader;
    }
    
    /**
     * Returns a new array containing the interfaces for the proxy to
     * implement.
     *
     * <p><code>AbstractILFactory</code> implements this method to return
     * an array containing all of the interfaces obtained by passing
     * <code>impl</code> to the {@link #getRemoteInterfaces
     * getRemoteInterfaces} method plus the interfaces obtained by calling
     * the {@link #getExtraProxyInterfaces getExtraProxyInterfaces} method,
     * in that order, with duplicate interfaces (that is, duplicates after
     * the first occurrence of an interface) eliminated.
     *
     * <p>A subclass can override this method if its proxies need to
     * implement a different set of interfaces than the default.
     *
     * @param	impl the remote object
     * @return	the proxy interfaces
     * @throws	ExportException if there is a problem obtaining the
     *		proxy interfaces or if <code>impl</code> does not satisfy
     *		the requirements of this factory
     * @throws	NullPointerException if <code>impl</code> is <code>null</code>
     **/
    protected Class[] getProxyInterfaces(Remote impl)
	throws ExportException
    {
	return combineInterfaces(getRemoteInterfaces(impl),
				 getExtraProxyInterfaces(impl));
    }
    
    /**
     * Returns a new array containing the remote interfaces that should be
     * implemented by the proxy.  All of the methods of a remote interface
     * are implemented as remote invocations, so all of the methods must
     * declare {@link java.rmi.RemoteException} or one of its superclasses
     * in their <code>throws</code> clauses.
     *
     * <p><code>AbstractILFactory</code> implements this method to return
     * an array containing the following ordered list of interfaces:
     *
     * <ul><li>for each superclass of <code>impl's</code> class starting
     * with <code>java.lang.Object</code> and following with each direct
     * subclass to the direct superclass of <code>impl</code>'s class, all
     * of the direct superinterfaces of the given superclass that extend
     * {@link Remote} and that do not appear previously in the list, in
     * declaration order (the order in which they are declared in the
     * class's <code>implements</code> clause), followed by
     * 
     * <li>all of the direct superinterfaces of <code>impl</code>'s
     * class that extend {@link Remote} and that do not appear previously
     * in the list, in declaration order.
     * </ul>
     *
     * and throws an <code>ExportException</code> if any method of those
     * interfaces does not have a conforming <code>throws</code> clause.
     *
     * <p>A subclass can override this method if its proxies need a
     * set of remote interfaces other than the default.
     *
     * @param	impl the remote object
     * @return	the remote interfaces implemented by <code>impl</code>
     * @throws	NullPointerException if <code>impl</code> is <code>null</code>
     * @throws	ExportException if there is a problem obtaining the remote
     * 		interfaces or if <code>impl</code> does not satisfy the
     *		requirements of this factory
     **/
    protected Class[] getRemoteInterfaces(Remote impl) throws ExportException {
	if (impl == null) {
	    throw new NullPointerException("impl is null");
	}
	try {
	    return Util.getRemoteInterfaces(impl.getClass());
	} catch (IllegalArgumentException e) {
	    throw new ExportException("cannot get proxy interfaces", e);
	}
    }
    
    /**
     * Returns a new array containing any additional interfaces that the
     * proxy should implement, beyond the interfaces obtained by passing
     * <code>impl</code> to the {@link #getRemoteInterfaces
     * getRemoteInterfaces} method.
     *
     * <p><code>AbstractILFactory</code> implements this method to return
     * an array containing the {@link RemoteMethodControl} interface.
     *
     * <p>A subclass can override this method if its proxies need
     * to implement a different set of extra interfaces than the default.
     *
     * @param	impl the remote object
     * @return	the extra proxy interfaces
     * @throws	NullPointerException if <code>impl</code> is <code>null</code>
     * @throws	ExportException if there is a problem obtaining the additional
     *		interfaces or if <code>impl</code> does not satisfy the
     *		requirements of this factory
     **/
    protected Class[] getExtraProxyInterfaces(Remote impl)
	throws ExportException
    {
	if (impl == null) {
	    throw new NullPointerException("impl is null");
	}
	return new Class[]{RemoteMethodControl.class};
    }

    /**
     * Returns a new, modifiable collection of {@link Method} objects,
     * containing all remote methods for which the invocation
     * dispatcher should accept incoming remote calls.
     *
     * <p><code>AbstractILFactory</code> implements this method to return a
     * {@link Set} containing all of the methods of the interfaces obtained
     * by passing <code>impl</code> to the {@link #getRemoteInterfaces
     * getRemoteInterfaces} method and satisfying the following
     * requirements:
     *
     * <ul>
     * <li>No duplicate methods with the same signature and return
     * type are contained in the set returned.  If the interfaces contain
     * more than one method with the same signature and return type, the
     * method in the returned collection will be a member of the foremost
     * of the interfaces that contains a method with that signature and
     * return type.
     *
     * <li>For each interface, if a security manager exists, its
     * <code>checkPackageAccess</code> method is invoked with the package
     * name of the interface; this invocation may throw a
     * <code>SecurityException</code>.
     *
     * <li>For each interface, if the interface is non-public and a
     * security manager exists, the security manager's
     * <code>checkPermission</code> method is invoked with the permission
     * (@link ExportPermission} constructed with the string
     * "exportRemoteInterface." concatenated with the fully qualified
     * interface name; this invocation may throw a
     * <code>SecurityException</code>.  If the security check passes, each
     * <code>Method</code> object of the non-public interface has its
     * accessibility flag set to suppress language access checks.
     * </ul>
     *
     * <p>A subclass can override this method if it needs to control the
     * selection of the set of methods for the dispatcher to handle, or if
     * it needs to control the implementation of the collection returned.
     *
     * @param	impl the remote object
     * @return	the remote methods
     * @throws	NullPointerException if <code>impl</code> is <code>null</code>
     * @throws	ExportException if there is a problem obtaining the remote
     *		methods or if <code>impl</code> does not satisfy the
     *		requirements of this factory
     **/
    protected Collection getInvocationDispatcherMethods(Remote impl)
	throws ExportException
    {
	Class[] interfaces = getRemoteInterfaces(impl);
	Map methodMap = new HashMap();

	for (int i = interfaces.length-1; i >= 0; i--) {
	    Class intf = interfaces[i];
	    boolean nonpublic = checkNonPublicInterface(intf);
	    Util.checkPackageAccess(intf);
	    Method interfaceMethods[] = intf.getMethods();
	    for (int j = interfaceMethods.length-1; j >=0; j--) {
		final Method m = interfaceMethods[j];
		/*
		 * If the interface is non-public, set this Method object
		 * to override language access checks so that the
		 * dispatcher can invoke methods from non-public remote
		 * interfaces.  This is okay because the
		 * checkNonPublicInterface method checked a permission for
		 * this action.
		 */
		if (nonpublic) {
		    Security.doPrivileged(new PrivilegedAction() {
			public Object run() {
			    m.setAccessible(true);
			    return null;
			}
		    });
		}
		methodMap.put(Util.getMethodNameAndDescriptor(m), m);
	    }
	}
	
	Collection methods = new HashSet();
	methods.addAll(methodMap.values());
	return methods;
    }

    /**
     * Returns <code>true</code> if and only if the specified interface is
     * non-public.
     *
     * <p>If the interface is non-public and a security manager exists, the
     * security manager's <code>checkPermission</code> method is invoked
     * with the permission (@link ExportPermission} constructed with the
     * string "exportRemoteInterface." concatenated with the fully
     * qualified interface name; this invocation may throw a
     * <code>SecurityException</code>.
     *
     * @returns <code>true</code> if the specified interface is non-public;
     * 		otherwise returns <code>false</code> 
     * @throws  SecurityException if the specified interface is non-public
     *		and the security check fails
     **/
    private static boolean checkNonPublicInterface(Class intf) {
	if (!Modifier.isPublic(intf.getModifiers())) {
	    SecurityManager sm = System.getSecurityManager();
	    if (sm != null) {
		String pString = "exportRemoteInterface." + intf.getName();
		sm.checkPermission(new ExportPermission(pString));
	    }
	    return true;
	}
	return false;
    }
    
   /**
     * Returns an invocation handler to use with a {@link Proxy} instance
     * implementing the specified interfaces, communicating with the
     * specified remote object using the specified object endpoint.
     *
     * <p>A subclass must override this method to create an
     * <code>InvocationHandler</code> for the specified interfaces, remote
     * object, and object endpoint.
     *
     * @param	interfaces an array of proxy interfaces
     * @param	impl a remote object this invocation handler
     *		is being created for
     * @param	oe an object endpoint used to communicate with
     *		the remote object
     * @return	the invocation handler for the remote object's proxy
     * @throws	ExportException if there is a problem creating the
     * 		invocation handler
     * @throws	NullPointerException if any argument is <code>null</code>,
     *		or if <code>interfaces</code> contains a <code>null</code>
     *		element
     **/
    protected abstract InvocationHandler
	createInvocationHandler(Class[] interfaces,
				Remote impl,
				ObjectEndpoint oe)
	throws ExportException;

    /**
     * Returns an invocation dispatcher to receive incoming remote calls
     * for the specified methods to the specified remote object, for a
     * server and transport with the specified capabilities.
     *
     * <p>A subclass must override this method to create an
     * <code>InvocationDispatcher</code> for the specified methods, remote
     * object, and server capabilities.
     *
     * @param	methods a collection of {@link Method} instances for the
     *		remote methods
     * @param	impl a remote object that the dispatcher is being created for
     * @param	caps the transport capabilities of the server
     * @return	the invocation dispatcher for the remote object
     * @throws	ExportException if there is a problem creating the
     *		dispatcher 
     * @throws	IllegalArgumentException if <code>methods</code> contains
     *		an element that is not a <code>Method</code> instance
     * @throws	NullPointerException if any argument is <code>null</code>,
     *		or if <code>methods</code> contains a <code>null</code> element
     **/
    protected abstract InvocationDispatcher
	createInvocationDispatcher(Collection methods,
				   Remote impl,
				   ServerCapabilities caps)
	throws ExportException;

 
    /**
     * {@inheritDoc}
     *
     * <p><code>AbstractILFactory</code> implements this method to return a
     * {@link Proxy} instance where:
     * <ul>
     *
     * <li>If the class loader specified during construction is not
     * <code>null</code>, the proxy's class is defined by the specified
     * loader.  Otherwise, if a security manager exists, its {@link
     * SecurityManager#checkPermission checkPermission} method is invoked
     * with the <code>{@link RuntimePermission}("getClassLoader")</code>
     * permission; this invocation may throw a
     * <code>SecurityException</code>.  If the above security check
     * succeeds, the proxy's class is defined by the class loader of
     * <code>impl</code>'s class.
     *
     * <li>The proxy implements the set of interfaces obtained by calling
     * the {@link #getProxyInterfaces getProxyInterfaces} method, passing
     * <code>impl</code> as the argument.  If a security manager exists,
     * for each interface returned from the <code>getProxyInterfaces</code>
     * method,  the security manager's {@link
     * SecurityManager#checkPackageAccess checkPackageAccess} method is
     * invoked with the package name of the interface. Such an invocation
     * may throw a <code>SecurityException</code>.
     *
     * <li>The invocation handler of the proxy instance is obtained by calling
     * the {@link #createInvocationHandler createInvocationHandler} method,
     * passing the proxy interfaces (as above), <code>impl</code>, and
     * <code>oe</code> as arguments. 
     * </ul>
     *
     * <p>The returned invocation dispatcher is obtained by calling the
     * {@link #createInvocationDispatcher createInvocationDispatcher}
     * method, passing a collection of methods, <code>impl</code>, and
     * <code>caps</code> as arguments. The collection of methods is
     * obtained by calling the {@link #getInvocationDispatcherMethods
     * getInvocationDispatcherMethods} method, passing <code>impl</code> as
     * the argument.
     *
     * @throws	NullPointerException {@inheritDoc}
     **/
    public Instances createInstances(Remote impl,
				     ObjectEndpoint oe,
				     ServerCapabilities caps)
    	throws ExportException
    {
	if (impl == null || oe == null || caps == null) {
	    throw new NullPointerException();
	}
 	Class[] interfaces = getProxyInterfaces(impl);
	InvocationHandler handler =
	    createInvocationHandler(interfaces, impl, oe);
	
	ClassLoader proxyLoader;
	if (loader != null) {
	    proxyLoader = loader;
	} else {
	    SecurityManager security = System.getSecurityManager();
	    if (security != null) {
		security.checkPermission(getClassLoaderPermission);
	    }
	    proxyLoader = impl.getClass().getClassLoader();
	}

	for (int i = 0; i < interfaces.length; i++) {
	    Util.checkPackageAccess(interfaces[i].getClass());
	}
	
	Remote proxy;
	try {
	    proxy = (Remote) Proxy.newProxyInstance(proxyLoader,
						    interfaces,
						    handler);
	} catch (IllegalArgumentException e) {
	    throw new ExportException("unable to create proxy", e);
	}
	InvocationDispatcher dispatcher =
	    createInvocationDispatcher(getInvocationDispatcherMethods(impl),
				       impl, caps);
	return new Instances(proxy, dispatcher);
    }
    
    /**
     * Returns a hash code value for this factory.
     **/
    public int hashCode() {
	return getClass().hashCode();
    }

    /**
     * Compares the specified object with this invocation layer factory for
     * equality.
     *
     * <p><code>AbstractILFactory</code> implements this method to return
     * <code>true</code> if and only if the specified object has the same
     * class as this object and the loader in the specified object is equal
     * to the loader in this object.
     *
     * <p>A subclass should override this method if it adds instance state
     * that affects equality.
     **/
    public boolean equals(Object obj) {
	return (obj == this ||
		(obj != null &&
		 obj.getClass() == getClass() &&
		 ((AbstractILFactory) obj).loader == loader));
    }

    /**
     * Returns a string representation for this factory.
     **/
    public String toString() {
	String name = getClass().getName();
	return name.substring(name.lastIndexOf('.') + 1);
    }
}
