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

import com.sun.jini.jeri.internal.http.ConnectionTimer;
import com.sun.jini.jeri.internal.http.HttpClientConnection;
import com.sun.jini.jeri.internal.http.HttpClientManager;
import com.sun.jini.jeri.internal.http.HttpClientSocketFactory;
import com.sun.jini.jeri.internal.http.HttpSettings;
import com.sun.jini.jeri.internal.runtime.Util;
import com.sun.jini.logging.Levels;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Collection;
import java.util.HashSet;
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.LogRecord;
import java.util.logging.Logger;
import javax.net.SocketFactory;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.jeri.Endpoint;
import net.jini.jeri.OutboundRequest;
import net.jini.jeri.OutboundRequestIterator;
import net.jini.security.proxytrust.TrustEquivalence;

/**
 * An implementation of the {@link Endpoint} abstraction that uses HTTP
 * messages sent over TCP sockets (instances of {@link Socket}) for the
 * underlying communication mechanism.
 *
 * <p><code>HttpEndpoint</code> instances contain a host name and a
 * TCP port number, as well as an optional {@link SocketFactory} for
 * customizing the type of <code>Socket</code> to use.  The host name
 * and port number are used as the remote address to connect to when
 * making socket connections.  Note that constructing an
 * <code>HttpEndpoint</code> with a <code>SocketFactory</code>
 * instance that produces SSL sockets does not result in an endpoint
 * that is fully HTTPS capable.
 *
 * <p><code>HttpEndpoint</code> instances map outgoing requests to HTTP
 * request/response messages; when possible, underlying TCP connections are
 * reused for multiple non-overlapping outgoing requests.  Outbound request
 * data is sent as the <code>entity-body</code> of an HTTP POST request;
 * inbound response data is received as the <code>entity-body</code> of the
 * corresponding HTTP response message. For information on HTTP, refer to <a
 * href="http://www.ietf.org/rfc/rfc2616.txt">RFC 2616</a>.
 * 
 * <p><code>HttpEndpoint</code> can be configured via system properties to send
 * HTTP messages through an intermediary HTTP proxy server.  It also supports
 * basic and digest HTTP authentication, specified in <a
 * href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>.  The mechanisms
 * involved in configuring each of these features are the same as those used by 
 * {@link java.net.HttpURLConnection}; for details, see the 
 * {@link net.jini.jeri.http} package documentation.
 *
 * <p>A <code>SocketFactory</code> used with an
 * <code>HttpEndpoint</code> should be serializable and must implement
 * {@link Object#equals Object.equals} to obey the guidelines that are
 * specified for <code>equals</code> methods of {@link Endpoint}
 * instances.
 *
 * @author Sun Microsystems, Inc.
 * @see HttpServerEndpoint
 * @since 2.0
 **/
public final class HttpEndpoint
    implements Endpoint, TrustEquivalence, Serializable
{
    private static final long serialVersionUID = -7094180943307123931L;

    /** set of canonical instances */
    private static final Map internTable = new WeakHashMap();

    /** HTTP client manager */
    private static final HttpClientManager clientManager;
    /** idle connection timer */
    private static final ConnectionTimer connTimer;
    static {
	HttpSettings hs = getHttpSettings();
	clientManager = new HttpClientManager(hs.getResponseAckTimeout(15000));
	connTimer = new ConnectionTimer(hs.getConnectionTimeout(15000));
    }

    /** client transport logger */
    private static final Logger logger =
	Logger.getLogger("net.jini.jeri.http.client");

    /**
     * The host that this <code>HttpEndpoint</code> connects to.
     *
     * @serial
     **/
    private final String host;

    /**
     * The TCP port that this <code>HttpEndpoint</code> connects to.
     *
     * @serial
     **/
    private final int port;

    /**
     * The socket factory that this <code>HttpEndpoint</code> uses to
     * create {@link Socket} objects.
     *
     * @serial
     **/
    private final SocketFactory sf;
    
    /** idle connection cache */
    private transient Set connections;
    /** current proxy host, or empty string if not proxied */
    private transient String proxyHost;
    /** current proxy port, or -1 if not proxied */
    private transient int proxyPort;

    /**
     * Returns an <code>HttpEndpoint</code> instance for the given host name
     * and TCP port number.  Note that if HTTP proxying is in effect, then an
     * explicit host name or IP address (i.e., not "localhost") must be
     * provided, or else the returned <code>HttpEndpoint</code> will be unable
     * to properly send requests through the proxy.
     *
     * <p>The {@link SocketFactory} contained in the returned
     * <code>HttpEndpoint</code> will be <code>null</code>, indicating
     * that this endpoint will create {@link Socket} objects directly.
     *
     * @param host the host for the endpoint to connect to
     *
     * @param port the TCP port on the given host for the endpoint to
     * connect to
     * 
     * @return an <code>HttpEndpoint</code> instance
     *
     * @throws IllegalArgumentException if the port number is out of
     * the range <code>1</code> to <code>65535</code> (inclusive)
     *
     * @throws NullPointerException if <code>host</code> is
     * <code>null</code>
     **/
    public static HttpEndpoint getInstance(String host, int port) {
	return intern(new HttpEndpoint(host, port, null));
    }

    /**
     * Returns an <code>HttpEndpoint</code> instance for the given
     * host name and TCP port number that contains the given {@link
     * SocketFactory}.  Note that if HTTP proxying is in effect, then
     * an explicit host name or IP address (i.e., not "localhost")
     * must be provided, or else the returned
     * <code>HttpEndpoint</code> will be unable to properly send
     * requests through the proxy.
     *
     * <p>If the socket factory argument is <code>null</code>, then
     * this endpoint will create {@link Socket} objects directly.
     *
     * @param host the host for the endpoint to connect to
     *
     * @param port the TCP port on the given host for the endpoint to
     * connect to
     *
     * @param sf the <code>SocketFactory</code> to use for this
     * <code>HttpEndpoint</code>, or <code>null</code>
     *
     * @return an <code>HttpEndpoint</code> instance
     *
     * @throws IllegalArgumentException if the port number is out of
     * the range <code>1</code> to <code>65535</code> (inclusive)
     *
     * @throws NullPointerException if <code>host</code> is
     * <code>null</code>
     **/
    public static HttpEndpoint getInstance(String host, int port,
					   SocketFactory sf)
    {
	return intern(new HttpEndpoint(host, port, sf));
    }

    /**
     * Returns canonical instance equivalent to given instance.
     **/
    private static HttpEndpoint intern(HttpEndpoint endpoint) {
	synchronized (internTable) {
	    Reference ref = (SoftReference) internTable.get(endpoint);
	    if (ref != null) {
		HttpEndpoint canonical = (HttpEndpoint) ref.get();
		if (canonical != null) {
		    return canonical;
		}
	    }
	    endpoint.init();
	    internTable.put(endpoint, new SoftReference(endpoint));
	    return endpoint;
	}
    }

    /**
     * Constructs a new (not fully initialized) instance.
     **/
    private HttpEndpoint(String host, int port, SocketFactory sf) {
	if (host == null) {
	    throw new NullPointerException();
	}
	if (port < 1 || port > 0xFFFF) {
	    throw new IllegalArgumentException(
	        "port number out of range: " + port);
	}

	this.host = host;
	this.port = port;
	this.sf = sf;
    }

    /*
     * [This is not a doc comment to prevent its appearance in
     * HttpEndpoint's serialized form specification.]
     *
     * Resolves deserialized instance to equivalent canonical instance.
     */
    private Object readResolve() {
	return intern(this);
    }

    /**
     * Initializes new instance obtained either from private constructor or
     * deserialization.
     **/
    private void init() {
	connections = new HashSet(5);
	proxyHost = "";
	proxyPort = -1;
    }

    /**
     * Returns the host that this <code>HttpEndpoint</code> connects to.
     *
     * @return the host that this endpoint connects to
     **/
    public String getHost() {
	return host;
    }

    /**
     * Returns the TCP port that this <code>HttpEndpoint</code> connects to.
     *
     * @return the TCP port that this endpoint connects to
     **/
    public int getPort() {
	return port;
    }

    /**
     * Returns the {@link SocketFactory} that this endpoint uses to
     * create {@link Socket} objects.
     *
     * @return the socket factory that this endpoint uses to create
     * sockets, or <code>null</code> if this endpoint creates sockets
     * directly
     **/
    public SocketFactory getSocketFactory() {
	return sf;
    }

    /**
     * {@inheritDoc}
     *
     * <p>The returned <code>OutboundRequestIterator</code>'s {@link
     * OutboundRequestIterator#next next} method behaves as follows:
     *
     * <blockquote>
     *
     * Initiates an attempt to communicate the request to this remote
     * endpoint.
     *
     * <p>When the implementation of this method needs to create a new
     * <code>Socket</code>, it will do so by invoking one of the
     * <code>createSocket</code> methods on the
     * <code>SocketFactory</code> of the <code>HttpEndpoint</code>
     * that produced this iterator if non-<code>null</code>, or it
     * will create a <code>Socket</code> directly otherwise.
     *
     * <p>If there is a security manager, its {@link
     * SecurityManager#checkConnect(String,int) checkConnect} method
     * will be invoked with the remote IP address and port of the
     * <code>Socket</code> to be used for the communication; this
     * could result in a <code>SecurityException</code>.
     *
     * <p>Throws {@link NoSuchElementException} if this iterator does
     * not support making another attempt to communicate the request
     * (that is, if <code>hasNext</code> would return
     * <code>false</code>).
     *
     * <p>Throws {@link ConnectException} if a failure occurs while
     * connecting a socket to the remote host and port, such as if the
     * connection was refused (no entity is listening on that port or
     * the host is too busy).
     *
     * <p>Throws {@link IOException} if an I/O exception occurs while
     * performing this operation.
     *
     * <p>Throws {@link SecurityException} if there is a security
     * manager and the invocation of its <code>checkConnect</code>
     * method fails.
     *
     * </blockquote>
     *
     * @throws NullPointerException {@inheritDoc}
     **/
    public OutboundRequestIterator
	newRequest(final InvocationConstraints constraints)
    {
	if (constraints == null) {
	    throw new NullPointerException();
	}
	return new OutboundRequestIterator() {
	    private boolean nextCalled = false;
	    private OutboundRequest currentRequest;
	    public OutboundRequest next() throws IOException {
		if (!hasNext()) {
		    throw new NoSuchElementException();
		}
		nextCalled = true;
		currentRequest = nextRequest(constraints);
		return currentRequest;
	    }
	    public boolean hasNext() {
		// NYI: determine if HTTP failure suggests retry
		return !nextCalled;
	    }
	};
    }

    private OutboundRequest nextRequest(InvocationConstraints constraints)
	throws IOException
    {
	final Constraints.Distilled distilled =
	    Constraints.distill(constraints, false);

	final OutboundRequest request = nextRequest(distilled);

	// must wrap to provide getUnfulfilledConstraints implementation
	return new OutboundRequest() {
	    public void populateContext(Collection context) {
		request.populateContext(context);
	    }
	    public InvocationConstraints getUnfulfilledConstraints() {
		return distilled.getUnfulfilledConstraints();
	    }
	    public OutputStream getRequestOutputStream() {
		return request.getRequestOutputStream();
	    }
	    public InputStream getResponseInputStream() {
		return request.getResponseInputStream();
	    }
	    public boolean getDeliveryStatus() {
		return request.getDeliveryStatus();
	    }
	    public void abort() { request.abort(); }
	};
    }

    private OutboundRequest nextRequest(final Constraints.Distilled distilled)
	throws IOException
    {
	SecurityManager sm = System.getSecurityManager();
	if (sm != null) {
	    sm.checkConnect(host, port);
	}

	HttpSettings settings = getHttpSettings();
	String phost = settings.getHttpProxyHost("");
	if (!phost.equals("") && settings.nonProxied(host, false)) {
	    phost = "";
	}
	int pport = phost.equals("") ? -1 : settings.getHttpProxyPort(80);
	Connection conn;

	synchronized (connections) {
	    if (!(proxyHost.equals(phost) && proxyPort == pport)) {
		proxyHost = phost;
		proxyPort = pport;
		shedConnections();
	    }

	    while (!connections.isEmpty()) {
		conn = (Connection) connections.iterator().next();
		connections.remove(conn);
		if (connTimer.cancelTimeout(conn)) {
		    try { 
			return conn.newRequest(); 
		    } catch (IOException ex) {
			if (logger.isLoggable(Levels.HANDLED)) {
			    LogRecord lr = new LogRecord(
				Levels.HANDLED, "newRequest on {0} throws");
			    lr.setLoggerName(logger.getName());
			    lr.setSourceClassName(this.getClass().getName());
			    lr.setSourceMethodName("newRequest");
			    lr.setParameters(new Object[]{ this });
			    lr.setThrown(ex);
			    logger.log(lr);
			}
		    }
		}
		conn.shutdown(true);
	    }
	}

	try {
	    // REMIND: ok to mask proxy connect permission?
	    conn = (Connection) AccessController.doPrivileged(
		new PrivilegedExceptionAction() {
		    public Object run() throws IOException {
			return proxyHost.equals("") ?
			    new Connection(host, port, distilled) :
			    new Connection(host, port, proxyHost, proxyPort,
					   distilled);
		    }
		}
	    );
	} catch (PrivilegedActionException e) {
	    throw (IOException) e.getCause();
	}
	return conn.newRequest();
    }
    
    /**
     * Closes all idle connections cached by this HTTP endpoint.
     **/
    private void shedConnections() {
	synchronized (connections) {
	    Object[] conns = connections.toArray();
	    connections.clear();
	    for (int i = 0; i < conns.length; i++) {
		((Connection) conns[i]).shutdown(true);
	    }
	}
    }

    /**
     * Returns the hash code value for this <code>HttpEndpoint</code>.
     *
     * @return the hash code value for this <code>HttpEndpoint</code>
     **/
    public int hashCode() {
	return host.hashCode() ^ port ^
	    (sf != null ? sf.hashCode() : 0);
    }

    /**
     * Compares the specified object with this
     * <code>HttpEndpoint</code> for equality.
     *
     * <p>This method returns <code>true</code> if and only if
     *
     * <ul>
     *
     * <li>the specified object is also an <code>HttpEndpoint</code>,
     *
     * <li>the host and port in the specified object are equal to the
     * host and port in this object, and
     *
     * <li>either this object and the specified object both have no
     * <code>SocketFactory</code> or the <code>SocketFactory</code> in
     * the specified object has the same class and is equal to the one
     * in this object.
     *
     * </ul>
     *
     * @param obj the object to compare with
     * 
     * @return <code>true</code> if <code>obj</code> is equivalent to
     * this object; <code>false</code> otherwise
     **/ 
    public boolean equals(Object obj) {
	if (obj == this) {
	    return true;
	} else if (!(obj instanceof HttpEndpoint)) {
	    return false;
	}
	HttpEndpoint other = (HttpEndpoint) obj;
	return
	    host.equals(other.host) &&
	    port == other.port &&
	    Util.sameClassAndEquals(sf, other.sf);
    }

    /**
     * Returns <code>true</code> if the specified object (which is not
     * yet known to be trusted) is equivalent in trust, content, and
     * function to this known trusted object, and <code>false</code>
     * otherwise.
     *
     * <p>This method returns <code>true</code> if and only if
     *
     * <ul>
     *
     * <li>the specified object is also an <code>HttpEndpoint</code>,
     *
     * <li>the host and port in the specified object are equal to the
     * host and port in this object, and
     *
     * <li>either this object and the specified object both have no
     * <code>SocketFactory</code> or the <code>SocketFactory</code> in
     * the specified object has the same class and is equal to the one
     * in this object.
     *
     * </ul>
     **/
    public boolean checkTrustEquivalence(Object obj) {
	if (obj == this) {
	    return true;
	} else if (!(obj instanceof HttpEndpoint)) {
	    return false;
	}
	HttpEndpoint other = (HttpEndpoint) obj;
	return
	    host.equals(other.host) &&
	    port == other.port &&
	    Util.sameClassAndEquals(sf, other.sf);
    }

    /**
     * Returns a string representation of this
     * <code>HttpEndpoint</code>.
     *
     * @return a string representation of this
     * <code>HttpEndpoint</code>
     **/
    public String toString() {
	return "HttpEndpoint[" + host + ":" + port + 
	    (sf != null ? "," + sf : "") + "]";
    }

    /**
     * @throws InvalidObjectException if the host name is
     * <code>null</code> or if the port number is out of the range
     * <code>1</code> to <code>65535</code> (inclusive)
     **/
    private void readObject(ObjectInputStream in)
	throws IOException, ClassNotFoundException
    {
	in.defaultReadObject();
	if (host == null) {
	    throw new InvalidObjectException("null host");
	}
	if (port < 1 || port > 0xFFFF) {
	    throw new InvalidObjectException(
	        "port number out of range: " + port);
	}
    }

    /**
     * Returns current HTTP system property settings.
     **/
    static HttpSettings getHttpSettings() {
	return (HttpSettings) AccessController.doPrivileged(
	    new PrivilegedAction() {
		public Object run() { return HttpSettings.getHttpSettings(); }
	    });
    }

    /**
     * SocketFactory -> HttpClientSocketFactory adapter.
     **/
    private static final class SocketFactoryAdapter
	implements HttpClientSocketFactory
    {
	private final SocketFactory sf;
	private final Constraints.Distilled distilled;

	SocketFactoryAdapter(SocketFactory sf,
			     Constraints.Distilled distilled)
	{
	    this.sf = sf;
	    this.distilled = distilled;
	}
	
	public Socket createSocket(String host, int port) throws IOException {
	    Socket socket;
	    if (sf != null) {
		socket = sf.createSocket();
	    } else {
		socket = new Socket();
	    }

	    int timeout;
	    if (distilled.hasConnectDeadline()) {
		long now = System.currentTimeMillis();
		long deadline = distilled.getConnectDeadline();
		if (deadline <= now) {
		    throw new SocketTimeoutException(
			"deadline past before connect attempt");
		}
		assert now > 0; // if not, we could overflow
		long delta = deadline - now;
		// assume that no socket connect will last over 24 days
		timeout = (delta > Integer.MAX_VALUE ? 0 : (int) delta);
	    } else {
		timeout = 0;
	    }

	    socket.connect(new InetSocketAddress(host, port), timeout);

	    if (logger.isLoggable(Level.FINE)) {
		logger.log(Level.FINE, 
			   "created socket {0}" +
			   (sf != null ? " using factory {1}" : ""),
			   new Object[]{ socket, sf });
	    }

	    try {
		socket.setTcpNoDelay(true);
	    } catch (SocketException e) {
		// ignore possible failures and proceed anyway
		if (logger.isLoggable(Levels.HANDLED)) {
		    LogRecord lr = new LogRecord(Levels.HANDLED,
			"exception setting TCP_NODELAY on socket {0}");
		    lr.setLoggerName(logger.getName());
		    lr.setSourceClassName(this.getClass().getName());
		    lr.setSourceMethodName("connect");
		    lr.setParameters(new Object[]{ socket });
		    lr.setThrown(e);
		    logger.log(lr);
		}
	    }
	    try {
		socket.setKeepAlive(true);
	    } catch (SocketException e) {
		// ignore possible failures and proceed anyway
		if (logger.isLoggable(Levels.HANDLED)) {
		    LogRecord lr = new LogRecord(Levels.HANDLED,
			"exception setting SO_KEEPALIVE on socket {0}");
		    lr.setLoggerName(logger.getName());
		    lr.setSourceClassName(this.getClass().getName());
		    lr.setSourceMethodName("connect");
		    lr.setParameters(new Object[]{ socket });
		    lr.setThrown(e);
		    logger.log(lr);
		}
	    }
	    
	    return socket;
	}
	
	public Socket createTunnelSocket(Socket s) throws IOException {
	    // proxy tunneling never used
	    throw new UnsupportedOperationException();
	}
    }
    
    /**
     * HTTP connection for sending requests.
     **/
    private final class Connection extends HttpClientConnection {
	
	private final String proxyHost;
	private final int proxyPort;

	/**
	 * Creates a direct connection to given host/port.
	 **/
	Connection(String host, int port, Constraints.Distilled distilled)
	    throws IOException
	{
	    super(host, port,
		  new SocketFactoryAdapter(sf, distilled), clientManager);
	    proxyHost = "";
	    proxyPort = -1;
	}

	/**
	 * Creates a proxied connection to given host/port.
	 **/
	Connection(String host,
		   int port,
		   String proxyHost,
		   int proxyPort,
		   Constraints.Distilled distilled) 
	    throws IOException
	{
	    super(host, port, proxyHost, proxyPort, false, 
		  new SocketFactoryAdapter(sf, distilled), clientManager);
	    this.proxyHost = proxyHost;
	    this.proxyPort = proxyPort;
	}
	
	/**
	 * Adds connection to idle connection cache, schedules timeout.
	 **/
	protected void idle() {
	    synchronized (connections) {
		if (proxyHost.equals(HttpEndpoint.this.proxyHost) &&
		    proxyPort == HttpEndpoint.this.proxyPort)
		{
		    connections.add(this);
		    connTimer.scheduleTimeout(this, false);
		} else {
		    super.shutdown(true);
		}
	    }
	}
	
	/**
	 * Attempts to close connection.
	 **/
	public boolean shutdown(boolean force) {
	    Socket sock = getSocket();
	    if (super.shutdown(force)) {
		synchronized (connections) {
		    connections.remove(this);
		    connTimer.cancelTimeout(this);
		}
		if (logger.isLoggable(Level.FINE)) {
		    logger.log(Level.FINE, "closed socket {0}", 
			new Object[]{ sock });
		}
		return true;
	    }
	    return false;
	}
    }
}
