/*
 * 
 * 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.ssl;

import com.sun.jini.jeri.internal.connection.BasicConnManagerFactory;
import com.sun.jini.jeri.internal.connection.ConnManager;
import com.sun.jini.jeri.internal.connection.ConnManagerFactory;
import com.sun.jini.jeri.internal.runtime.Util;
import com.sun.jini.logging.Levels;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.cert.CertPath;
import java.security.cert.X509Certificate;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.SocketFactory;
import javax.security.auth.Subject;
import javax.security.auth.x500.X500Principal;
import javax.security.auth.x500.X500PrivateCredential;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.io.UnsupportedConstraintException;
import net.jini.jeri.Endpoint;
import net.jini.jeri.OutboundRequest;
import net.jini.jeri.OutboundRequestIterator;
import net.jini.jeri.connection.Connection;
import net.jini.jeri.connection.ConnectionEndpoint;
import net.jini.jeri.connection.OutboundRequestHandle;
import net.jini.security.AuthenticationPermission;

/**
 * Provides the implementation of SslEndpoint so that the implementation can be
 * inherited by HttpsEndpoint without revealing the inheritance in the public
 * API.
 *
 * @author Sun Microsystems, Inc.
 */
class SslEndpointImpl extends Utilities implements ConnectionEndpoint {

    /* -- Fields -- */

    /** Client logger */
    static final Logger logger = clientLogger;

    /**
     * Weak key map that maps connection endpoints to weak references to the
     * associated ConnManager.  The weak values insure that the keys will not
     * be strongly held, since the connection manager has a strong reference to
     * the associated connection endpoint.
     */
    private static final Map connectionMgrs = new WeakHashMap();

    /** The size of the connection context cache. */
    private static final int CACHE_SIZE = 4;
    
    /** The factory for default connection managers. */
    private static final ConnManagerFactory connectionManagerFactory =
	new BasicConnManagerFactory();

    /** The associated endpoint. */
    final Endpoint endpoint;

    /** The name of the server host. */
    final String serverHost;

    /** The server port. */
    final int port;

    /** The factory for creating sockets, or null to use default sockets. */
    final SocketFactory socketFactory;

    /**
     * Whether to disable calling Socket.connect -- set when used by discovery
     * providers.
     */
    boolean disableSocketConnect;

    /** A cache for recently computed connection contexts. */
    private ConnectionContextCache[] connectionContextCache =
	new ConnectionContextCache[CACHE_SIZE];

    /** Next index for a connectionContextCache miss; counts down, not up. */
    private int cacheNext;

    /** The connection manager for this endpoint or null if not yet set. */
    ConnManager connectionManager;

    /* -- Constructors -- */

    /** Creates an instance of this class. */
    SslEndpointImpl(Endpoint endpoint,
		    String serverHost,
		    int port,
		    SocketFactory socketFactory)
    {
	this.endpoint = endpoint;
	if (serverHost == null) {
	    throw new NullPointerException("serverHost is null");
	}
	this.serverHost = serverHost;
	if (port <= 0 || port > 0xFFFF) {
	    throw new IllegalArgumentException("Invalid port: " + port);
	}
	this.port = port;
	this.socketFactory = socketFactory;
    }

    /* -- Methods -- */

    /** Returns a string representation of this object. */
    public String toString() {
	return getClassName(this) + fieldsToString();
    }

    /** Returns a string representation of the fields of this object. */
    final String fieldsToString() {
	return "[" + serverHost + ":" + port + 
	    (socketFactory != null ? ", " + socketFactory : "") + "]";
    }

    /* -- Implement Endpoint -- */

    /** Returns a hash code value for this object. */
    public int hashCode() {
	return getClass().hashCode()
	    ^ serverHost.hashCode()
	    ^ port
	    ^ (socketFactory != null ? socketFactory.hashCode() : 0);
    }

    /**
     * Two instances of this class are equal if they have the same actual
     * class; have the same values for server host and port; and have socket
     * factories that are either both null, or have the same actual class and
     * are equal.
     */
    public boolean equals(Object object) {
	if (object == null || object.getClass() != getClass()) {
	    return false;
	}
	SslEndpointImpl other = (SslEndpointImpl) object;
	return serverHost.equals(other.serverHost)
	    && port == other.port
	    && Util.sameClassAndEquals(socketFactory, other.socketFactory);
    }

    /** Implements Endpoint.newRequest */
    final OutboundRequestIterator newRequest(
	InvocationConstraints constraints)
    {
	if (constraints == null) {
	    throw new NullPointerException("Constraints cannot be null");
	}
	try {
	    return newRequest(getCallContext(constraints));
	} catch (UnsupportedConstraintException e) {
	    return new ExceptionOutboundRequestIterator(e);
	} catch (SecurityException e) {
	    return new ExceptionOutboundRequestIterator(e);
	}
    }

    /**
     * An outbound request iterator that throws an IOException or a
     * SecurityException.
     */
    private static final class ExceptionOutboundRequestIterator
	implements OutboundRequestIterator
    {
	private final Exception exception;
	private boolean done = false;

	ExceptionOutboundRequestIterator(Exception exception) {
	    this.exception = exception;
	}

	public synchronized boolean hasNext() {
	    return !done;
	}

	public synchronized OutboundRequest next() throws IOException {
	    if (done) {
		throw new NoSuchElementException();
	    }
	    done = true;
	    if (exception instanceof SecurityException) {
		throw (SecurityException) exception;
	    } else {
		throw (IOException) exception;
	    }
	}
    }

    /** Implements Endpoint.newRequest when the constraints are supported. */
    OutboundRequestIterator newRequest(CallContext callContext) {
	return getConnectionManager().newRequest(callContext);
    }

    /** Returns the connection manager for this endpoint. */
    private ConnManager getConnectionManager() {
	synchronized (connectionMgrs) {
	    if (connectionManager == null) {
		Reference ref = (Reference) connectionMgrs.get(this);
		connectionManager =
		    (ref != null) ? (ConnManager) ref.get() : null;
		if (connectionManager == null) {
		    connectionManager = connectionManagerFactory.create(this);
		    connectionMgrs.put(
			this, new WeakReference(connectionManager));
		}
	    }
	    return connectionManager;
	}
    }

    /**
     * Returns a context for making a remote call with the specified
     * constraints and the current subject. This method does not perform
     * communication with the remote server. Throws a SecurityException if lack
     * of authentication permissions needed for requirements prevent the use of
     * all contexts.  Throws UnsupportedConstraintException if some
     * requirements cannot be satisfied.  Returns a CallContext if throwing an
     * exception or returning null would reveal information about the current
     * subject that the caller does not have permission to know.
     */
    private CallContext getCallContext(InvocationConstraints constraints)
	throws UnsupportedConstraintException
    {
	final AccessControlContext acc = AccessController.getContext();
	Subject clientSubject = (Subject) AccessController.doPrivileged(
	    new PrivilegedAction() {
		public Object run() {
		    return Subject.getSubject(acc);
		}
	    });
	Set clientPrincipals = getClientPrincipals(constraints.requirements());
	boolean requiredClient = clientPrincipals != null;
	boolean constrainedServer = getServerPrincipals(constraints) != null;
	Boolean getSubject = null;
	if (!requiredClient) {
	    /* Try using principals from Subject instead */
	    if (clientSubject == null) {
		clientPrincipals = Collections.EMPTY_SET;
	    } else {
		/*
		 * XXX: Work around BugID 4892841, Subject.getPrincipals(Class)
		 * not thread-safe against changes to principals.
		 * -tjb[18.Jul.2003]
		 */
		synchronized (clientSubject.getPrincipals()) {
		    clientPrincipals =
			clientSubject.getPrincipals(X500Principal.class);
		}
	    }
	    if (clientPrincipals.isEmpty()) {
		getSubject = getSubjectPermitted();
		if (getSubject == Boolean.FALSE) {
		    /* Don't reveal that there are no client principals */
		    clientPrincipals =
			Collections.singleton(UNKNOWN_PRINCIPAL);
		}
	    }
	}
	List contexts = new CopyOnRemoveList(
	    getConnectionContexts(constraints, clientPrincipals));
	if (constrainedServer) {
	    /* Check authentication permissions */
	    try {
		contexts = checkAuthenticationPermissions(contexts);
	    } catch (SecurityException e) {
		if (getSubject == null) {
		    getSubject = getSubjectPermitted();
		}
		if (getSubject == Boolean.TRUE) {
		    if (logger.isLoggable(Levels.FAILED)) {
			logThrow(
			    logger, Levels.FAILED,
			    SslEndpointImpl.class, "getCallContext",
			    "new request for {0}\nwith {1}\nand {2}\nthrows",
			    new Object[] { endpoint, constraints,
					   subjectString(clientSubject) },
			    e);
		    }
		    throw e;
		} else {
		    /* No access to subject -- don't reveal principals */
		    CallContext result = createCallContext(
			getConnectionContexts(
			    constraints,
			    Collections.singleton(UNKNOWN_PRINCIPAL)),
			null);
		    if (logger.isLoggable(Levels.FAILED)) {
			logThrow(
			    logger, Levels.FAILED,
			    SslEndpointImpl.class, "getCallContext",
			    "new request for {0}\nwith {1}\nand {2}\n" +
			    "will fail but cannot throw " +
			    "because caller has no subject access\n" +
			    "returns {3}\n" +
			    "caught exception",
			    new Object[] { endpoint, constraints,
					   subjectString(clientSubject),
					   result },
			    e);
		    }
		    return result;
		}
	    }
	}
	UnsupportedConstraintException unsupported = null;
	if (contexts.isEmpty()) {
	    unsupported = new UnsupportedConstraintException(
		"Constraints not supported: " + constraints);
	} else {
	    boolean checkSubject;
	    if (constrainedServer) {
		checkSubject = true;
	    } else {
		if (getSubject == null) {
		    getSubject = getSubjectPermitted();
		}
		checkSubject = (getSubject == Boolean.TRUE);
	    }
	    if (checkSubject) {
		/* Check subject if caller has any access */
		try {
		    contexts = checkSubject(contexts, clientSubject,
					    constrainedServer, constraints);
		} catch (UnsupportedConstraintException e) {
		    unsupported = e;
		}
	    }
	}
	if (unsupported != null) {
	    if (logger.isLoggable(Levels.FAILED)) {
		logThrow(logger, Levels.FAILED,
			 SslEndpointImpl.class, "getCallContext",
			 "new request for {0}\nwith {1}\nand {2}\nthrows",
			 new Object[] { endpoint, constraints,
					subjectString(clientSubject) },
			 unsupported);
	    }
	    throw unsupported;
	} else {
	    CallContext result = createCallContext(contexts, clientSubject);
	    if (logger.isLoggable(Level.FINE)) {
		logger.log(Level.FINE,
			   "new request for {0}\nwith {1}\nand {2}\n" +
			   "returns {3}",
			   new Object[] {
			       endpoint, constraints,
			       subjectString(clientSubject), result
			   });
	    }
	    return result;
	}
    }

    /** Convert connection contexts to a call context */
    private CallContext createCallContext(List contexts,
					  Subject clientSubject)
    {
	boolean clientAuthRequired = true;
	boolean clientAuthPermitted = false;
	Set clientPrincipals = null;
	Set serverPrincipals = null;
	List suites = new ArrayList();
	boolean integrityRequired = false;
	boolean integrityPreferred = false;
	long connectionTimeout = -1;
	int max = contexts.size();
	for (int i = 0; i < max; i++) {
	    ConnectionContext context = (ConnectionContext) contexts.get(i);
	    if (context.client == null) {
		clientAuthRequired = false;
	    } else {
		clientAuthPermitted = true;
		if (clientPrincipals == null) {
		    clientPrincipals = new HashSet();
		}
		/*
		 * Don't include the unknown principal, but still should record
		 * that principals were specified by making clientPrincipals
		 * non-null.
		 */
		if (context.client != UNKNOWN_PRINCIPAL) {
		    clientPrincipals.add(context.client);
		}
	    }
	    if (context.server != null &&
		context.server != UNKNOWN_PRINCIPAL)
	    {
		if (serverPrincipals == null) {
		    serverPrincipals = new HashSet();
		}
		serverPrincipals.add(context.server);
	    }
	    if (!suites.contains(context.cipherSuite)) {
		suites.add(context.cipherSuite);
	    }
	    if (context.getIntegrityRequired()) {
		integrityRequired = true;
	    } else if (context.getIntegrityPreferred()) {
		integrityPreferred = true;
	    }
	    if (context.getConnectionTime() != -1 &&
		(connectionTimeout == -1 ||
		 connectionTimeout > context.getConnectionTime()))
	    {
		connectionTimeout = context.getConnectionTime();
	    }
	}
	return new CallContext(
	    endpoint, this,
	    clientAuthPermitted ? clientSubject : null,
	    clientAuthRequired, clientPrincipals, serverPrincipals, suites,
	    integrityRequired, integrityPreferred, connectionTimeout);
    }

    /**
     * Returns a list of the contexts which are supported by principals and
     * credentials in the Subject.  Throws an UnsupportedConstraintException if
     * none of the contexts are supported, otherwise returns a non-empty list.
     */

    private static List checkSubject(List contexts,
				     Subject clientSubject,
				     boolean constrainedServer,
				     InvocationConstraints constraints)
	throws UnsupportedConstraintException
    {
	Map publicCreds = getPublicCredentials(clientSubject);
	X500PrivateCredential[] privateCreds =
	    (clientSubject != null && constrainedServer)
	    ? (X500PrivateCredential[]) AccessController.doPrivileged(
		new SubjectCredentials.GetAllPrivateCredentialsAction(
		    clientSubject))
	    : new X500PrivateCredential[0];
	Set missingPublic = new HashSet();
	Set missingPrivate = new HashSet();
	/* Check principals and credentials */
      top:
	for (int i = contexts.size(); --i >= 0; ) {
	    ConnectionContext context = (ConnectionContext) contexts.get(i);
	    if (context.client == null) {
		continue;			/* Anonymous client OK */
	    }
	    Collection certs = (Collection) publicCreds.get(context.client);
	    if (certs == null) {
		logger.log(Levels.HANDLED,
			   "missing principal or public credentials: {0}",
			   context.client);
		contexts.remove(i);
		missingPublic.add(context.client);
	    } else if (constrainedServer) {
		/*
		 * Checked authentication permissions, so OK to check private
		 * credentials.
		 */
		for (int j = privateCreds.length; --j >= 0; ) {
		    X509Certificate cert = privateCreds[j].getCertificate();
		    if (cert != null) {		/* null if destroyed */
			if (certs.contains(cert)) {
			    continue top;	/* Private credentials OK */
			}
		    }
		}
		logger.log(Levels.HANDLED, "missing private credentials: {0}",
			   context.client);
		contexts.remove(i);
		missingPrivate.add(context.client);
	    }
	}
	if (!contexts.isEmpty()) {
	    return contexts;
	} else {
	    throw new UnsupportedConstraintException(
		"Constraints not supported: " + constraints + ";" +
		(missingPublic.isEmpty() ? ""
		 : ("\nmissing principals or public credentials: " +
		    missingPublic)) +
		(missingPrivate.isEmpty() ? ""
		 : "\nmissing private credentials: " + missingPrivate));
	}
    }

    /**
     * Checks if the caller has permission to get the current subject,
     * returning Boolean.TRUE or FALSE.
     */
    private static Boolean getSubjectPermitted() {
	SecurityManager sm = System.getSecurityManager();
	if (sm != null) {
	    try {
		sm.checkPermission(getSubjectPermission);
	    } catch (SecurityException e) {
		return Boolean.FALSE;
	    }
	}
	return Boolean.TRUE;
    }

    /**
     * Removes the contexts for which the client does not have authentication
     * permission.  Throws SecurityException if lack of permission prevents any
     * contexts from being used.
     */
    private static List checkAuthenticationPermissions(List contexts) {
	if (contexts.isEmpty()) {
	    return contexts;
	}
	SecurityManager sm = System.getSecurityManager();
	if (sm == null) {
	    return contexts;
	}
	Map perms = new HashMap();
	Set exceptions = new HashSet();
	for (int i = contexts.size(); -- i >= 0; ) {
	    ConnectionContext context = (ConnectionContext) contexts.get(i);
	    if (context.client == null) {
		/* Client anonymous -- OK */
		continue;
	    } else if (context.server == UNKNOWN_PRINCIPAL) {
		/* Server not known -- can't check any permissions */
		break;
	    }
	    AuthenticationPermission p = new AuthenticationPermission(
		Collections.singleton(context.client),
		Collections.singleton(context.server),
		"connect");
	    Object value = perms.get(p);
	    if (Boolean.FALSE.equals(value)) {
		contexts.remove(i);
	    } else if (!Boolean.TRUE.equals(value)) {
		try {
		    sm.checkPermission(p);
		    perms.put(p, Boolean.TRUE);
		} catch (SecurityException e) {
		    logger.log(
			Levels.HANDLED,
			"check authentication permission caught exception",
			e);
		    perms.put(p, Boolean.FALSE);
		    exceptions.add(e);
		    contexts.remove(i);
		}
	    }
	}
	if (!contexts.isEmpty()) {
	    return contexts;
	} else if (exceptions.size() == 1) {
	    throw (SecurityException) exceptions.iterator().next();
	} else {
	    throw new SecurityException(exceptions.toString());
	}
    }

    /**
     * Returns a map that maps each principal in the subject to a set of the
     * associated X.500 public credentials.
     */
    private static Map getPublicCredentials(Subject subject) {
	Map publicCreds = new HashMap();
	List certPaths = SubjectCredentials.getCertificateChains(subject);
	if (certPaths != null) {
	    for (int i = certPaths.size(); --i >= 0; ) {
		CertPath chain = (CertPath) certPaths.get(i);
		X509Certificate cert = SubjectCredentials.firstX509Cert(chain);
		X500Principal p =
		    SubjectCredentials.getPrincipal(subject, cert);
		if (p != null) {
		    Collection certs = (Collection) publicCreds.get(p);
		    if (certs == null) {
			certs = new ArrayList(1);
			publicCreds.put(p, certs);
		    }
		    certs.add(cert);
		}
	    }
	}
	return publicCreds;
    }

    /**
     * A List that supports removing items by making a copy of the underlying
     * list.
     */
    private static final class CopyOnRemoveList extends AbstractList {
	private List list;
	private boolean modified;

	CopyOnRemoveList(List list) {
	    this.list = list;
	}

	public Object get(int index) {
	    return list.get(index);
	}

	public int size() {
	    return list.size();
	}

	public Object remove(int index) {
	    if (!modified) {
		list = new ArrayList(list);
		modified = true;
	    }
	    return list.remove(index);
	}
    }

    /**
     * Defines a structure to cache a ConnectionContexts for specific
     * constraints and client principals.
     */
    private static final class ConnectionContextCache {
	final InvocationConstraints constraints;
	final Set clientPrincipals;
	final List connectionContexts;

	ConnectionContextCache(InvocationConstraints constraints,
			       Set clientPrincipals,
			       List connectionContexts)
	{
	    this.constraints = constraints;
	    this.clientPrincipals = clientPrincipals;
	    this.connectionContexts = connectionContexts;
	}
    }

    /**
     * Gets an unmodifiable list of the ConnectionContexts for the
     * specified constraints and client principals.
     */
    private List getConnectionContexts(InvocationConstraints constraints,
				       Set clientPrincipals)
    {
	synchronized (connectionContextCache) {
	    for (int i = CACHE_SIZE; --i >= 0; ) {
		ConnectionContextCache cache = connectionContextCache[i];
		if (cache != null &&
		    cache.constraints.equals(constraints) &&
		    cache.clientPrincipals.equals(clientPrincipals))
		{
		    logger.log(Level.FINEST, "used connection cache");
		    return cache.connectionContexts;
		}
	    }
	}
	Set serverPrincipals = getServerPrincipals(constraints);
	if (serverPrincipals == null) {
	    serverPrincipals = Collections.singleton(UNKNOWN_PRINCIPAL);
	}
	List contexts = Collections.unmodifiableList(
	    computeConnectionContexts(
		getSupportedCipherSuites(), clientPrincipals, serverPrincipals,
		constraints));
	synchronized (connectionContextCache) {
	    connectionContextCache[cacheNext] =
		new ConnectionContextCache(
		    constraints, clientPrincipals, contexts);
	    if (cacheNext == 0) {
		cacheNext = CACHE_SIZE;
	    }
	    cacheNext--;
	}
	return contexts;
    }

    /** Used for sorting ConnectionContexts by preferences and suite order. */
    private static final class ComparableConnectionContext
	implements Comparable
    {
	final ConnectionContext context;
	private final int suiteIndex;

	ComparableConnectionContext(ConnectionContext context,
				    int suiteIndex)
	{
	    this.context = context;
	    this.suiteIndex = suiteIndex;
	}

	public int compareTo(Object object) {
	    ComparableConnectionContext other =
		(ComparableConnectionContext) object;
	    /* Lower value for more preferences */
	    int result =
		other.context.getPreferences() - context.getPreferences();
	    if (result == 0) {
		/* Lower value for lower index */
		result = suiteIndex - other.suiteIndex;
	    }
	    return result;
	}

	public String toString() {
	    StringBuffer sb = new StringBuffer("ComparableConnectionContext[");
	    context.fieldsToString(sb);
	    sb.append(", index: ").append(suiteIndex);
	    sb.append("]");
	    return sb.toString();
	}
    }

    /**
     * Computes a list of ConnectionContexts for the specified set of
     * suites, client and server principals, and constraints, sorted by
     * preferences and suite order.
     */
    private static List computeConnectionContexts(
	String[] suites,
	Set clients,
	Set servers,
	InvocationConstraints constraints)
    {
	List result = new ArrayList(
	    suites.length * (clients.size() + 1) * (servers.size() + 1));
	for (int suiteIndex = suites.length; --suiteIndex >= 0; ) {
	    String suite = suites[suiteIndex];
	    Iterator cIter = clients.iterator();
	    Principal client;
	    do {
		if (cIter.hasNext()) {
		    client = (Principal) cIter.next();
		    assert client != null;
		} else {
		    client = null;
		}
		Iterator sIter = servers.iterator();
		Principal server;
		do {
		    if (sIter.hasNext()) {
			server = (Principal) sIter.next();
			assert server != null;
		    } else {
			server = null;
		    }
		    for (int i = 2; --i >= 0; ) {
			boolean integrity = i == 0;
			ConnectionContext context =
			    ConnectionContext.getInstance(
				suite, client, server, integrity,
				true /* clientSide */, constraints);
			if (context != null) {
			    result.add(
				new ComparableConnectionContext(
				    context, suiteIndex));
			}
		    }
		} while (server != null);
	    } while (client != null);
	}
	Collections.sort(result);
	logger.log(Level.FINEST, "compute connection contexts produces {0}",
		   result);
	for (int i = result.size(); --i >= 0; ) {
	    ComparableConnectionContext ccc =
		(ComparableConnectionContext) result.get(i);
	    result.set(i, ccc.context);
	}
	return result;
    }

    /* -- Implement ConnectionEndpoint -- */

    /** Creates a new connection. */
    public Connection connect(OutboundRequestHandle handle) throws IOException {
	SslConnection connection = new SslConnection(
	    CallContext.coerce(handle, endpoint), serverHost, port,
	    socketFactory);
	connection.establishCallContext();
	return connection;
    }

    /** Chooses a connection from existing connections. */
    public Connection connect(OutboundRequestHandle handle,
			      Collection active,
			      Collection idle)
    {
	CallContext context = CallContext.coerce(handle, endpoint);
	if (active == null || idle == null) {
	    throw new NullPointerException("Arguments cannot be null");
	}
	Connection result = null;
	/*
	 * Choose the first connection with an appropriate subject, an active
	 * suite that is one of the requested suites, and one for which all the
	 * requested suites better than the active one were also better for its
	 * call context.  That insures that we've gotten the best connection we
	 * can, assuming a new handshake were to make similar decisions.
	 */
	for (Iterator iter = new ConnectionsIterator(endpoint, active, idle);
	     iter.hasNext(); )
	{
	    SslConnection connection = (SslConnection) iter.next();
	    if (connection.useFor(context)) {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
		    try {
			sm.checkConnect(serverHost, port);
		    } catch (SecurityException e) {
			if (logger.isLoggable(Levels.FAILED)) {
			    logThrow(logger, Levels.FAILED,
				     SslEndpointImpl.class, "connect",
				     "choose connection for {0}\nthrows",
				     new Object[] { this }, e);
			}
			throw e;
		    }
		}
		result = connection;
		break;
	    }
	}
	if (logger.isLoggable(Level.FINE)) {
	    logger.log(Level.FINE,
		       "choose connection for {0}\nwith active {1}\n" +
		       "and idle {2}\nreturns {3}",
		       new Object[] { handle, active, idle, result });
	}
	return result;
    }

    /**
     * Defines an iterator over active and idle connections which performs
     * error checking on connections.
     */
    private static final class ConnectionsIterator implements Iterator {
	private final Endpoint endpoint;
	private Collection active;	/* Set to null when exhausted */
	private final Collection idle;
	private Iterator iter;

	ConnectionsIterator(Endpoint endpoint,
			    Collection active,
			    Collection idle)
	{
	    this.endpoint = endpoint;
	    this.active = active;
	    this.idle = idle;
	    iter = active.iterator();
	    if (!iter.hasNext()) {
		this.active = null;
		iter = idle.iterator();
	    }
	}

	public boolean hasNext() {
	    return iter.hasNext();
	}

	public Object next() {
	    if (!hasNext()) {
		throw new NoSuchElementException();
	    }
	    Object next = iter.next();
	    if (next == null) {
		throw new NullPointerException("Connection cannot be null");
	    } else if (!(next instanceof SslConnection)) {
		throw new IllegalArgumentException(
		    "Connection must be of type SslConnection: " + next);
	    }
	    SslConnection result = (SslConnection) next;
	    if (!endpoint.equals(result.callContext.endpoint)) {
		throw new IllegalArgumentException(
		    "Connection has wrong endpoint: found " +
		    result.callContext.endpoint + ", expected " + endpoint);
	    }
	    if (!iter.hasNext() && active != null) {
		active = null;
		iter = idle.iterator();
	    }
	    return result;
	}

	public void remove() {
	    throw new UnsupportedOperationException();
	}
    }
}
