/*
 * 
 * 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 com.sun.jini.jeri.internal.runtime;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.lang.ref.SoftReference;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.rmi.MarshalledObject;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.activation.ActivationID;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.WeakHashMap;
import java.net.InetAddress;
import java.rmi.server.ServerNotActiveException;
import javax.security.auth.Subject;
import net.jini.export.ServerContext;
import net.jini.io.context.ClientHost;
import net.jini.io.context.ClientSubject;
import net.jini.io.context.ContextPermission;
import net.jini.io.context.IntegrityEnforcement;
import net.jini.security.proxytrust.TrustEquivalence;

/**
 * Utility methods for implementing custom remote reference types.
 *
 * @author Sun Microsystems, Inc.
 * 
 */
public class Util {
    
    /** cache of tables mapping methods to hashes */
    private static TableCache methodToHash_TableCache = new TableCache(true);
    
    /** parameter types for activatable constructor or activate method */
    private static Class[] paramTypes = {
	ActivationID.class, MarshalledObject.class
    };

    /**
     * Appends the current thread's stack trace to the stack trace of the
     * given exception.
     *
     * This method is used for exceptions that have been unmarshalled as the
     * exceptional result of a remote method invocation, so that the
     * client-side stack trace gets recorded in the exception (while
     * preserving the server-side stack trace in the exception as well).
     *
     * Note that the (somewhat odd) names of this method and the method that
     * this one internally delegates to are significant because these methods
     * visually highlight the remote call boundary between the server-side
     * and the client-side portions to readers of the combined stack trace.
     */
    public static void exceptionReceivedFromServer(Throwable t) {
	__________EXCEPTION_RECEIVED_FROM_SERVER__________(t);
    }

    private static void __________EXCEPTION_RECEIVED_FROM_SERVER__________(
	Throwable t)
    {
	StackTraceElement[] serverTrace = t.getStackTrace();
	StackTraceElement[] clientTrace = (new Throwable()).getStackTrace();
	StackTraceElement[] combinedTrace =
	    new StackTraceElement[serverTrace.length + clientTrace.length];
	System.arraycopy(serverTrace, 0, combinedTrace, 0,
			 serverTrace.length);
	System.arraycopy(clientTrace, 0, combinedTrace, serverTrace.length,
			 clientTrace.length);
	t.setStackTrace(combinedTrace);
    }

    /**
     * Clear the stack trace of the given exception by replacing it with
     * an empty StackTraceElement array, and do the same for all of its
     * chained causative exceptions.
     *
     * This method is used when it is desired for the stack trace data of
     * an exception to be suppressed before the exception gets marshalled
     * to a remote virtual machine, perhaps for reasons of confidentiality
     * or performance.
     */
    public static void clearStackTraces(Throwable t) {
	StackTraceElement[] empty = new StackTraceElement[0];
	while (t != null) {
	    t.setStackTrace(empty);
	    t = t.getCause();
	}
    }

    /**
     * Marshals <code>value</code> to an <code>ObjectOutput</code> stream,
     * <code>out</code>, using RMI's serialization format for arguments or
     * return values.  For primitive types, the primitive type's class should
     * be specified (i.e., for the type <code>int</code>, specify
     * <code>int.class</code>), and the primitive value should be wrapped in
     * instances of the appropriate wrapper class, such as
     * <code>java.lang.Integer</code> or <code>java.lang.Boolean</code>.
     *
     * @param	type <code>Class</code> object for the value to be marshalled
     * @param	value value to marshal
     * @param	out stream to which the value is marshalled
     * @throws	IOException if an I/O error occurs marshalling
     *		the value to the output stream
     */
    public static void marshalValue(Class type, Object value, ObjectOutput out)
	throws IOException
    {
	if (type.isPrimitive()) {
	    if (type == int.class) {
		out.writeInt(((Integer) value).intValue());
	    } else if (type == boolean.class) {
		out.writeBoolean(((Boolean) value).booleanValue());
	    } else if (type == byte.class) {
		out.writeByte(((Byte) value).byteValue());
	    } else if (type == char.class) {
		out.writeChar(((Character) value).charValue());
	    } else if (type == short.class) {
		out.writeShort(((Short) value).shortValue());
	    } else if (type == long.class) {
		out.writeLong(((Long) value).longValue());
	    } else if (type == float.class) {
		out.writeFloat(((Float) value).floatValue());
	    } else if (type == double.class) {
		out.writeDouble(((Double) value).doubleValue());
	    } else {
		throw new AssertionError(
		    "Unrecognized primitive type: " + type);
	    }
	} else {
	    out.writeObject(value);
	}
    }

    /**
     * Unmarshals a value of the specified <code>type</code> from the
     * <code>ObjectInput</code> stream, <code>in</code>, using RMI's
     * serialization format for arguments or return values and returns the
     * result.  For primitive types, the primitive type's class should be
     * specified (i.e., for the primitive type <code>int</code>, specify
     * <code>int.class</code>).
     *
     * @param	type <code>Class</code> object for the value to be unmarshalled
     * @param	in stream from which the value is unmarshalled
     * @return	value unmarshalled from the input stream
     * @throws	IOException if an I/O error occurs marshalling
     *		the value to the output stream
     * @throws	ClassNotFoundException if the <code>type</code>'s
     *		class could not	be found
     */
    public static Object unmarshalValue(Class type, ObjectInput in)
	throws IOException, ClassNotFoundException
    {
	if (type.isPrimitive()) {
	    if (type == int.class) {
		return new Integer(in.readInt());
	    } else if (type == boolean.class) {
		return new Boolean(in.readBoolean());
	    } else if (type == byte.class) {
		return new Byte(in.readByte());
	    } else if (type == char.class) {
		return new Character(in.readChar());
	    } else if (type == short.class) {
		return new Short(in.readShort());
	    } else if (type == long.class) {
		return new Long(in.readLong());
	    } else if (type == float.class) {
		return new Float(in.readFloat());
	    } else if (type == double.class) {
		return new Double(in.readDouble());
	    } else {
		throw new AssertionError(
		    "Unrecognized primitive type: " + type);
	    }
	} else {
	    return in.readObject();
	}
    }

    /**
     * Computes the "method hash" of a remote method, <code>m</code>.  The
     * method hash is a <code>long</code> containing the first 64 bits of the
     * SHA digest from the UTF encoded string of the method name followed by
     * its "method descriptor".  See section 4.3.3 of The Java(TM) Virtual
     * Machine Specification for the definition of a "method descriptor".
     *
     * @param	m remote method
     * @return	the method hash
     */
    private 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);
	    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;
    }

    /**
     * Returns the method hash for the method <code>m</code>.  Subsequent
     * calls to <code>getMethodHash</code> passing the same method argument
     * should be faster since this method caches internally the result of the
     * method to method hash mapping.  The method hash is calculated using the
     * <code>computeMethodHash</code> method.
     *
     * @param	m the remote method
     * @return	the method hash for the method <code>m</code>
     */
    public static long getMethodHash(Method m) {
	Map table = methodToHash_TableCache.getTable(m.getDeclaringClass());
	Long hash = (Long) table.get(m);
	return hash.longValue();
    }

    private static class TableCache extends WeakHashMap {
	/**
	 * if true, the tables map methods to method hashes; if false,
	 * the tables map method hashes to methods
	 */
	private boolean mapsMethodToHash;

	public TableCache(boolean mapsMethodToHash) {
	    super();
	    this.mapsMethodToHash = mapsMethodToHash;
	}
	
	public Map getTable(Class remoteClass) {
	    SoftReference[] tableRef;

	    /*
	     * Method tables for remote classes are cached in a hash table
	     * using weak references to hold the Class object keys, so that
	     * the cache does not prevent the class from being unloaded, and
	     * using soft references to hold the values, so that the computed
	     * method tables will generally persist while no objects of the
	     * remote class are exported, but their storage may be reclaimed
	     * if necessary, and accidental reachability of the remote class
	     * through its interfaces is avoided.
	     */
	    synchronized (this) {
		/*
		 * Look up class in cache; add entry if not found.
		 */
		tableRef = (SoftReference[]) get(remoteClass);
		if (tableRef == null) {
		    tableRef = new SoftReference[] { null };
		    put(remoteClass, tableRef);
		}
	    }

	    /*
	     * Check cached reference to method table for this class;
	     * if it is null, go and create the table.
	     */
	    synchronized (tableRef) {
		Map table = null;
		if (tableRef[0] != null) {
		    table = (Map) tableRef[0].get();
		}
		if (table == null) {
		    if (mapsMethodToHash) {
			/*
			 * REMIND: if we hand out this table directly, via a
			 * public API, we need to make this map "unmodifiable".
			 */
			table = new LazyMethodToHash_Map();
		    } else {
			throw new UnsupportedOperationException();
		    }
		    tableRef[0] = new SoftReference(table);
		}
		return table;
	    }
	}
    }


    /**
     * 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);
    }
    
    private static class LazyMethodToHash_Map extends WeakHashMap {

	public LazyMethodToHash_Map() {
	    super();
	}

	public synchronized Object get(Object key) {
	    Object hash = super.get(key);
	    if (hash == null) {
		Method method = (Method) key;
		hash = new Long(computeMethodHash(method));
		put(method, hash);
	    }
	    return (Long) hash;
	}
    }
	    
    /*
     * The following static methods are related to the creation of the
     * "method table" for a remote class, which maps method hashes to
     * the appropriate Method objects for the class's remote methods.
     */

    /**
     * Returns 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(TM) Virtual Machine Specification for
     * the definition of a "method descriptor".
     */
    public 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();
    }

    /**
     * Returns the descriptor of a particular type, as appropriate for either
     * a parameter type 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 virtual machine 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 an array containing the remote interfaces implemented
     * by the given class.
     *
     * @throws	IllegalArgumentException if remoteClass implements
     * 		any illegal remote interfaces
     * @throws	NullPointerException if remoteClass is null
     */
    public static Class[] getRemoteInterfaces(Class remoteClass) {
	ArrayList list = new ArrayList();
	getRemoteInterfaces(list, remoteClass);
	return (Class []) 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 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);
		}
	    }
	}
    }

    /**
     * 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);
    }

    /**
     * Returns true either if both arguments are null or if an
     * invocation of Object.equals on "subject" with "object" as the
     * argument returns true; returns false otherwise;
     **/
    public static boolean equals(Object subject, Object object) {
	return subject == null ? object == null : subject.equals(object);
    }

    /**
     * Returns true either if both arguments are null or if both
     * arguments refer to objects of the same class and an invocation
     * of Object.equals on "subject" with "object" as the argument
     * returns true; returns false otherwise.
     *
     * This method is used to compare to possibly-null references for
     * object equality when neither object's class is trusted, with
     * the restriction that only objects of the same class can be
     * considered equal.
     **/
    public static boolean sameClassAndEquals(Object subject, Object object) {
	return subject == null ? object == null :
	    object != null &&
	    subject.getClass() == object.getClass() &&
	    subject.equals(object);
    }

    /**
     * Returns true either if both arguments are null of if "subject"
     * is an instance of TrustEquivalence and an invocation of
     * TrustEquivalence.checkTrustEquivalence on "subject" with
     * "object" as the argument returns true; returns false otherwise.
     **/
    public static boolean checkTrustEquivalence(Object subject,
						Object object)
    {
	return subject == null ? object == null :
	    subject instanceof TrustEquivalence &&
	    ((TrustEquivalence) subject).checkTrustEquivalence(object);
    }

    /**
     * Returns true if proxy2 is a generated Proxy (proxy1 is assumed to
     * be one) and the classes of both proxies implement the same ordered
     * list of interfaces, and returns false otherwise.
     */
    public static boolean sameProxyClass(Object proxy1, Object proxy2) {
	return (proxy1.getClass() == proxy2.getClass() ||
		(Proxy.isProxyClass(proxy2.getClass()) &&
		 equalInterfaces(proxy1, proxy2)));
    }

    /**
     * Returns true if the interfaces implemented by obj1's class
     * are the same (and in the same order) as obj2's class.
     */
    public static boolean equalInterfaces(Object obj1, Object obj2) {
	Class[] intf1 = obj1.getClass().getInterfaces();
	Class[] intf2 = obj2.getClass().getInterfaces();
	if (intf1.length != intf2.length) {
	    return false;
	} else {
	    for (int i = 0; i < intf1.length; i++) {
		if (intf1[i] != intf2[i]) {
		    return false;
		}
	    }
	    return true;
	}
    }
    
    public static void populateContext(Collection context, InetAddress addr) {
	if (context == null) {
	    throw new NullPointerException("context is null");
	}
	if (addr != null) {
	    context.add(new ClientHostImpl(addr));
	}
    }
    
    public static void populateContext(Collection context, Subject s) {
	if (context == null) {
	    throw new NullPointerException("context is null");
	}
	context.add(new ClientSubjectImpl(s));
    }

    public static void populateContext(Collection context, boolean integrity) {
	if (context == null) {
	    throw new NullPointerException("context is null");
	}
	context.add(new IntegrityEnforcementImpl(integrity));
    }

    private static class ClientHostImpl
    	implements ClientHost
    {
	private final InetAddress addr;
	public ClientHostImpl(InetAddress addr) { this.addr = addr; }
	public InetAddress getClientHost() { return addr; }
    }

    private static class ClientSubjectImpl
    	implements ClientSubject
    {
	private final Subject s;
	private static final Permission getClientSubjectPerm =
	    new ContextPermission("net.jini.io.context.ClientSubject.getClientSubject");

	public ClientSubjectImpl(Subject s) { this.s = s; }
	public Subject getClientSubject() {
	    SecurityManager sm = System.getSecurityManager();
	    if (sm != null) {
		sm.checkPermission(getClientSubjectPerm);
	    }
	    return s;
	}
    }

    private static class IntegrityEnforcementImpl
    	implements IntegrityEnforcement
    {
	private final boolean integrity;
	public IntegrityEnforcementImpl(boolean integrity) {
	    this.integrity = integrity;
	}
	public boolean integrityEnforced() { return integrity; }
    }

    public static InetAddress getClientHost() throws ServerNotActiveException {
	ClientHost ch = (ClientHost)
	    ServerContext.getServerContextElement(ClientHost.class);
	return (ch != null) ? ch.getClientHost() : null;
    }

    public static String getClientHostString()
	throws ServerNotActiveException
    {
	InetAddress addr = getClientHost();
	return (addr != null) ? addr.toString() : null;
    }

    public static Subject getClientSubject() throws ServerNotActiveException {
	ClientSubject cs = (ClientSubject)
	    ServerContext.getServerContextElement(ClientSubject.class);
	return (cs != null) ? cs.getClientSubject() : null;
    }

    /**
     * Check for permission to access the package of the specified class.
     *
     * @throws SecurityException if a security manager exists and invoking
     * its <code>checkPackageAccess</code> method with the package name of
     * the specified class throws a <code>SecurityException</code>
     */
    public static void checkPackageAccess(Class type) {
	SecurityManager security = System.getSecurityManager();
	if (security != null) {
	    String name = type.getName();
	    int i = name.lastIndexOf('.');
	    if (i != -1) {
		security.checkPackageAccess(name.substring(0, i));
	    }
	}
    }
}
