/*
 * ====================================================================
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.http.localserver;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocketFactory;

import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.HttpResponseFactory;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpServerConnection;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.params.SyncBasicHttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpExpectationVerifier;
import org.apache.http.protocol.HttpProcessor;
import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.protocol.HttpRequestHandlerRegistry;
import org.apache.http.protocol.HttpService;
import org.apache.http.protocol.ImmutableHttpProcessor;
import org.apache.http.protocol.ResponseConnControl;
import org.apache.http.protocol.ResponseContent;
import org.apache.http.protocol.ResponseDate;
import org.apache.http.protocol.ResponseServer;

/**
 * Local HTTP server for tests that require one. Based on the
 * <code>ElementalHttpServer</code> example in HttpCore.
 */
public class LocalTestServer
{
	/**
	 * The local address to bind to. The host is an IP number rather than
	 * "localhost" to avoid surprises on hosts that map "localhost" to an IPv6
	 * address or something else. The port is 0 to let the system pick one.
	 */
	public final static InetSocketAddress TEST_SERVER_ADDR = new InetSocketAddress( "127.0.0.1", 0 );

	/** The request handler registry. */
	private final HttpRequestHandlerRegistry handlerRegistry;

	private final HttpService httpservice;

	/** Optional SSL context */
	private final SSLContext sslcontext;

	/** The server socket, while being served. */
	private volatile ServerSocket servicedSocket;

	/** The request listening thread, while listening. */
	private volatile ListenerThread listenerThread;

	/** Set of active worker threads */
	private final Set<Worker> workers;

	/** The number of connections this accepted. */
	private final AtomicInteger acceptedConnections = new AtomicInteger( 0 );

	/**
	 * Creates a new test server.
	 * 
	 * @param proc
	 *           the HTTP processors to be used by the server, or
	 *           <code>null</code> to use a {@link #newProcessor default}
	 *           processor
	 * @param reuseStrat
	 *           the connection reuse strategy to be used by the server, or
	 *           <code>null</code> to use {@link #newConnectionReuseStrategy()
	 *           default} strategy.
	 * @param params
	 *           the parameters to be used by the server, or <code>null</code> to
	 *           use {@link #newDefaultParams default} parameters
	 * @param sslcontext
	 *           optional SSL context if the server is to leverage SSL/TLS
	 *           transport security
	 */
	public LocalTestServer( final BasicHttpProcessor proc, final ConnectionReuseStrategy reuseStrat,
			final HttpResponseFactory responseFactory, final HttpExpectationVerifier expectationVerifier,
			final HttpParams params, final SSLContext sslcontext )
	{
		super();
		this.handlerRegistry = new HttpRequestHandlerRegistry();
		this.workers = Collections.synchronizedSet( new HashSet<Worker>() );
		this.httpservice = new HttpService( proc != null ? proc : newProcessor(), reuseStrat != null ? reuseStrat
				: newConnectionReuseStrategy(), responseFactory != null ? responseFactory : newHttpResponseFactory(),
				handlerRegistry, expectationVerifier, params != null ? params : newDefaultParams() );
		this.sslcontext = sslcontext;
	}

	/**
	 * Creates a new test server with SSL/TLS encryption.
	 * 
	 * @param sslcontext
	 *           SSL context
	 */
	public LocalTestServer( final SSLContext sslcontext )
	{
		this( null, null, null, null, null, sslcontext );
	}

	/**
	 * Creates a new test server.
	 * 
	 * @param proc
	 *           the HTTP processors to be used by the server, or
	 *           <code>null</code> to use a {@link #newProcessor default}
	 *           processor
	 * @param params
	 *           the parameters to be used by the server, or <code>null</code> to
	 *           use {@link #newDefaultParams default} parameters
	 */
	public LocalTestServer( BasicHttpProcessor proc, HttpParams params )
	{
		this( proc, null, null, null, params, null );
	}

	/**
	 * Obtains an HTTP protocol processor with default interceptors.
	 * 
	 * @return a protocol processor for server-side use
	 */
	protected HttpProcessor newProcessor()
	{
		return new ImmutableHttpProcessor( new HttpResponseInterceptor[] { new ResponseDate(), new ResponseServer(),
				new ResponseContent(), new ResponseConnControl() } );
	}

	/**
	 * Obtains a set of reasonable default parameters for a server.
	 * 
	 * @return default parameters
	 */
	protected HttpParams newDefaultParams()
	{
		HttpParams params = new SyncBasicHttpParams();
		params.setIntParameter( CoreConnectionPNames.SO_TIMEOUT, 60000 )
				.setIntParameter( CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024 )
				.setBooleanParameter( CoreConnectionPNames.STALE_CONNECTION_CHECK, false )
				.setBooleanParameter( CoreConnectionPNames.TCP_NODELAY, true )
				.setParameter( CoreProtocolPNames.ORIGIN_SERVER, "LocalTestServer/1.1" );
		return params;
	}

	protected ConnectionReuseStrategy newConnectionReuseStrategy()
	{
		return new DefaultConnectionReuseStrategy();
	}

	protected HttpResponseFactory newHttpResponseFactory()
	{
		return new DefaultHttpResponseFactory();
	}

	/**
	 * Returns the number of connections this test server has accepted.
	 */
	public int getAcceptedConnectionCount()
	{
		return acceptedConnections.get();
	}

	/**
	 * {@link #register Registers} a set of default request handlers.
	 * 
	 * <pre>
	 * URI pattern      Handler
	 * -----------      -------
	 * /echo/*          {@link EchoHandler EchoHandler}
	 * /random/*        {@link RandomHandler RandomHandler}
	 * </pre>
	 */
	public void registerDefaultHandlers()
	{
		handlerRegistry.register( "/echo/*", new EchoHandler() );
		handlerRegistry.register( "/random/*", new RandomHandler() );
	}

	/**
	 * Registers a handler with the local registry.
	 * 
	 * @param pattern
	 *           the URL pattern to match
	 * @param handler
	 *           the handler to apply
	 */
	public void register( String pattern, HttpRequestHandler handler )
	{
		handlerRegistry.register( pattern, handler );
	}

	/**
	 * Unregisters a handler from the local registry.
	 * 
	 * @param pattern
	 *           the URL pattern
	 */
	public void unregister( String pattern )
	{
		handlerRegistry.unregister( pattern );
	}

	/**
	 * Starts this test server.
	 */
	public void start() throws Exception
	{
		if( servicedSocket != null )
		{
			throw new IllegalStateException( this.toString() + " already running" );
		}
		ServerSocket ssock;
		if( sslcontext != null )
		{
			SSLServerSocketFactory sf = sslcontext.getServerSocketFactory();
			ssock = sf.createServerSocket();
		}
		else
		{
			ssock = new ServerSocket();
		}

		ssock.setReuseAddress( true ); // probably pointless for port '0'
		ssock.bind( TEST_SERVER_ADDR );
		servicedSocket = ssock;

		listenerThread = new ListenerThread();
		listenerThread.setDaemon( false );
		listenerThread.start();
	}

	/**
	 * Stops this test server.
	 */
	public void stop() throws Exception
	{
		if( servicedSocket == null )
		{
			return; // not running
		}
		ListenerThread t = listenerThread;
		if( t != null )
		{
			t.shutdown();
		}
		synchronized( workers )
		{
			for( Iterator<Worker> it = workers.iterator(); it.hasNext(); )
			{
				Worker worker = it.next();
				worker.shutdown();
			}
		}
	}

	public void awaitTermination( long timeMs ) throws InterruptedException
	{
		if( listenerThread != null )
		{
			listenerThread.join( timeMs );
		}
	}

	@Override
	public String toString()
	{
		ServerSocket ssock = servicedSocket; // avoid synchronization
		StringBuilder sb = new StringBuilder( 80 );
		sb.append( "LocalTestServer/" );
		if( ssock == null )
			sb.append( "stopped" );
		else
			sb.append( ssock.getLocalSocketAddress() );
		return sb.toString();
	}

	/**
	 * Obtains the local address the server is listening on
	 * 
	 * @return the service address
	 */
	public InetSocketAddress getServiceAddress()
	{
		ServerSocket ssock = servicedSocket; // avoid synchronization
		if( ssock == null )
		{
			throw new IllegalStateException( "not running" );
		}
		return ( InetSocketAddress )ssock.getLocalSocketAddress();
	}

	/**
	 * The request listener. Accepts incoming connections and launches a service
	 * thread.
	 */
	class ListenerThread extends Thread
	{

		private volatile Exception exception;

		ListenerThread()
		{
			super();
		}

		@Override
		public void run()
		{
			try
			{
				while( !interrupted() )
				{
					Socket socket = servicedSocket.accept();
					acceptedConnections.incrementAndGet();
					DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
					conn.bind( socket, httpservice.getParams() );
					// Start worker thread
					Worker worker = new Worker( conn );
					workers.add( worker );
					worker.setDaemon( true );
					worker.start();
				}
			}
			catch( Exception ex )
			{
				this.exception = ex;
			}
			finally
			{
				try
				{
					servicedSocket.close();
				}
				catch( IOException ignore )
				{
				}
			}
		}

		public void shutdown()
		{
			interrupt();
			try
			{
				servicedSocket.close();
			}
			catch( IOException ignore )
			{
			}
		}

		public Exception getException()
		{
			return this.exception;
		}

	}

	class Worker extends Thread
	{

		private final HttpServerConnection conn;

		private volatile Exception exception;

		public Worker( final HttpServerConnection conn )
		{
			this.conn = conn;
		}

		@Override
		public void run()
		{
			HttpContext context = new BasicHttpContext();
			try
			{
				while( this.conn.isOpen() && !Thread.interrupted() )
				{
					httpservice.handleRequest( this.conn, context );
				}
			}
			catch( Exception ex )
			{
				this.exception = ex;
			}
			finally
			{
				workers.remove( this );
				try
				{
					this.conn.shutdown();
				}
				catch( IOException ignore )
				{
				}
			}
		}

		public void shutdown()
		{
			interrupt();
			try
			{
				this.conn.shutdown();
			}
			catch( IOException ignore )
			{
			}
		}

		public Exception getException()
		{
			return this.exception;
		}

	}
}
