All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xsocket.connection.http.client.HttpClientConnection Maven / Gradle / Ivy

The newest version!
/*
 *  Copyright (c) xsocket.org, 2006 - 2008. All rights reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Please refer to the LGPL license at: http://www.gnu.org/copyleft/lesser.txt
 * The latest copy of this software may be found on http://www.xsocket.org/
 */
package org.xsocket.connection.http.client;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.nio.BufferUnderflowException;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;


import org.xsocket.DataConverter;
import org.xsocket.Execution;
import org.xsocket.MaxReadSizeExceededException;
import org.xsocket.connection.INonBlockingConnection;
import org.xsocket.connection.NonBlockingConnection;
import org.xsocket.connection.http.AbstractHttpConnection;
import org.xsocket.connection.http.BodyDataSink;
import org.xsocket.connection.http.HttpResponse;
import org.xsocket.connection.http.HttpResponseHeader;
import org.xsocket.connection.http.HttpUtils;
import org.xsocket.connection.http.IBodyCompleteListener;
import org.xsocket.connection.http.IHttpConnectHandler;
import org.xsocket.connection.http.IHttpConnection;
import org.xsocket.connection.http.IHttpDisconnectHandler;
import org.xsocket.connection.http.IHttpHandler;
import org.xsocket.connection.http.IHttpRequest;
import org.xsocket.connection.http.IHttpRequestHandler;
import org.xsocket.connection.http.IHttpRequestHeader;
import org.xsocket.connection.http.IHttpResponse;
import org.xsocket.connection.http.IHttpResponseHandler;
import org.xsocket.connection.http.IHttpResponseTimeoutHandler;




/**
 * Represents the client side endpoint implementation of a http connection. The HttpClientConnection supports
 * constructors which accepts the remote address or a existing {@link INonBlockingConnection}.  A INonBlockingConnection
 * can become a HttpClientConnection at any time.
 *  
 * @author [email protected]
 */
public final class HttpClientConnection extends AbstractHttpConnection implements IHttpConnection, IHttpClientEndpoint {

	private static final Logger LOG = Logger.getLogger(HttpClientConnection.class.getName());
	
	private static final long MIN_WATCHDOG_PERIOD_MILLIS = 30 * 1000;
	
	private static String versionInfo = null;

	
	
	// timer
	private static final Timer TIMER = new Timer("xHttpClientTimer", true); 

	
	public static final Integer DEFAULT_RECEIVE_TIMEOUT_SEC = Integer.MAX_VALUE; 
	private int receiveTimeoutSec = DEFAULT_RECEIVE_TIMEOUT_SEC;

	
	
	private final ArrayList responseHandlers = new ArrayList();
	
	private IHttpHandler handler = null;
	private ConnectHandlerAdapter connectHandlerAdapter = null;
	private DisconnectHandlerAdapter disconnectHandlerAdapter = null;
	
	// close after response flag 
	private boolean isCloseAfterResponse = false; 
	
	// close handling (non persistent connection)
	private final ConnectionAutoCloseListener connectionAutoCloseListner = new ConnectionAutoCloseListener(); 
	
	
	// auto handle
	boolean isAutohandle100ContinueResponse = DEFAULT_AUTOHANDLE_100CONTINUE_RESPONSE;
	
	
	// response timeout support
	private WatchDogTask watchDogTask = null;
	
	// statistics
	private int countReceivedMessages = 0;
	private int countSendMessages = 0;
	
	
	/**
	 * constructor 
	 * 
	 * @param host            the remote host
	 * @param port            the remote port 
	 * @throws ConnectException if an error occurred while attempting to connect to a remote address and port. 
	 * @throws IOException    if an exception occurs
	 */
	public HttpClientConnection(String host, int port) throws IOException, ConnectException {
		this(newNonBlockingConnection(new InetSocketAddress(host, port)));
	}
	
	
	/**
	 * constructor 
	 * 
	 * @param host            the remote host
	 * @param port            the remote port 
	 * @param httpHandler     the handler (supported: {@link IHttpConnectHandler}, {@link IHttpDisconnectHandler}, {@link IHttpConnectionTimeoutHandler})
	 * @throws IOException    if an exception occurs
	 * @throws ConnectException if an error occurred while attempting to connect to a remote address and port. 
	 */
	public HttpClientConnection(String host, int port, IHttpHandler httpHandler) throws IOException, ConnectException {
		this(newNonBlockingConnection(new InetSocketAddress(host, port)), httpHandler);
	}

	
	
	/**
	 * constructor 
	 * 
	 * @param address the server address          
	 * @throws ConnectException if an error occurred while attempting to connect to a remote address and port.
	 * @throws IOException    if an exception occurs
	 */
	public HttpClientConnection(InetSocketAddress address) throws IOException, ConnectException {
		this(newNonBlockingConnection(address));
	}

	/**
	 * constructor 
	 * 
	 * @param connection    the underlying tcp connection
	 * @throws IOException    if an exception occurs
	 */
	public HttpClientConnection(INonBlockingConnection connection) throws IOException {
		this(connection, null);
	}

	
	private static INonBlockingConnection newNonBlockingConnection(InetSocketAddress address) throws ConnectException {
		try {
			return new NonBlockingConnection(address);
		} catch (IOException ioe) {
			throw new ConnectException(ioe.toString());
		}
	}
	
	
	/**
     * constructor 
	 * 
	 * @param connection    the underlying tcp connection
	 * @param handler       the handler (supported: {@link IHttpConnectHandler}, {@link IHttpDisconnectHandler}, {@link IHttpConnectionTimeoutHandler})
	 * @throws IOException    if an exception occurs
	 */
	public HttpClientConnection(INonBlockingConnection connection, IHttpHandler handler) throws IOException {
		super(connection);
		
		this.handler = handler;		
		
		if (HttpUtils.isConnectHandler(handler)) {
			connectHandlerAdapter = new ConnectHandlerAdapter(HttpUtils.isConnectHandlerMultithreaded(handler));	
		}
		
		if (HttpUtils.isDisconnectHandler(handler)) {
			disconnectHandlerAdapter = new DisconnectHandlerAdapter(HttpUtils.isDisconnectHandlerMultithreaded(handler));	
		}

		init();
		onConnect();
	}
	

	void setCloseAfterResponse(boolean isCloseAfterResponse) {
		this.isCloseAfterResponse = isCloseAfterResponse;
	}


	/**
	 * {@inheritDoc}
	 */
	public final void setAutohandle100ContinueResponse(boolean isAutohandle100ContinueResponse) {
		this.isAutohandle100ContinueResponse = isAutohandle100ContinueResponse;
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public boolean isAutohandle100ContinueResponse() {
		return isAutohandle100ContinueResponse;
	}



	/**
	 * {@inheritDoc}
	 */
	public void setResponseTimeoutSec(int receiveTimeoutSec) {
		
		if (this.receiveTimeoutSec != receiveTimeoutSec) {
			this.receiveTimeoutSec = receiveTimeoutSec;
			
			long watchdogPeriod = receiveTimeoutSec * 100;
			
			if (watchdogPeriod > MIN_WATCHDOG_PERIOD_MILLIS) {
				watchdogPeriod = MIN_WATCHDOG_PERIOD_MILLIS;
			}
		
			if (LOG.isLoggable(Level.FINE)) {
				LOG.fine("[" + getId() +"] response timeout set to " + receiveTimeoutSec + " sec. Updating wachdog tas to check period " + watchdogPeriod + " millis");
			}
			
			updateWatchDog(watchdogPeriod);
		}
	}
	
	
	private synchronized void updateWatchDog(long watchDogPeriod) {
		terminateWatchDogTask();

        watchDogTask = new WatchDogTask(this); 
        TIMER.schedule(watchDogTask, watchDogPeriod, watchDogPeriod);
	}

	
	private synchronized void terminateWatchDogTask() {
		if (watchDogTask != null) {
            watchDogTask.cancel();
        }
	}

	
	/**
	 * {@inheritDoc}
	 */
	public int getResponseTimeoutSec() {
		return receiveTimeoutSec;
	}
	
	
	
	
	protected void onMessage(INonBlockingConnection connection) throws BufferUnderflowException, IOException {
		
		assert (Thread.currentThread().getName().startsWith("xDispatcher"));
		
		HttpResponse response = null;
		
		
			
		// read and handle the header 
		if (isFullResponse(connection)) {
			
			// reading the header
			HttpResponseHeader responseHeader = HttpResponseHeader.readFrom(connection, MAX_HEADER_LENGTH);
			response = new HttpResponse(responseHeader);
			
			handleResponseHeader(responseHeader);

			// add body parser
			addBodyParserIfRequired(response, connection);

		// simple (header less) response 
		} else {
			// create empty header
			HttpResponseHeader responseHeader = newEmptyResponseHeader();
			response = new HttpResponse(responseHeader);

			handleResponseHeader(responseHeader);
			
			// add body parser 
			addConnectionTerminatedBodyParser(response, connection);
		}		
				
			
		// update statistics
		countReceivedMessages++;
		if (LOG.isLoggable(Level.FINE)) {
			if (response.getNonBlockingBody() == null) {
				LOG.fine("[" + connection.getId() + "] bodyless response received from " + connection.getRemoteAddress() + 
						":" + connection.getRemotePort() + 
						 " " + response.getResponseHeader().toString());

				
			} else {
				
				String body = "";
				
				String contentType = response.getContentType();
				if (contentType != null) {
					if (contentType.startsWith("application/x-www-form-urlencode")) {
						
						body = response.getNonBlockingBody().toString() + "\n";
					}
				} 
				
				LOG.fine("[" + connection.getId() + "] response received from " + connection.getRemoteAddress() + 
						 ":" + connection.getRemotePort() +
						 ": " + response.getResponseHeader().toString() + body);
			}
		}

		
		// handle 100 continue header
		if (isAutohandle100ContinueResponse() && (response.getStatus() == 100)) {
			return;
		}
			 
			
			
		// get response handler adapter
		ResponseHandlerAdapter adapter = null;
		synchronized (responseHandlers) {
			if (responseHandlers.isEmpty()) {
				// could occur in a timeout case
				if (LOG.isLoggable(Level.FINE)) {
					LOG.fine("response handler is null (may be a receive timeout has been occured");
				}
				return;
				
			} else {
				adapter = responseHandlers.remove(0);
			}
		}
		
		
		// ... and handle the response
		if (adapter != null) {

			final ResponseHandlerAdapter rsa = adapter;
			final HttpResponse res = response;
			IBodyCompleteListener completeListener = new IBodyCompleteListener() {
			
				@Execution(Execution.NONTHREADED)
				public final void onComplete() {
					
					rsa.onMessageCompleteReceived();
					
					// call handlers call back method if is HEADER_MESSAGE triggered
					if (rsa.isResponseHandlerInvokeOnMessageReceived()) {
						callOnResponseCallback(rsa, res);
					}
				}
			};
			
			
			// body containing message?
			if (response.hasBody()) {
				if (getResponseTimeoutSec() != Integer.MAX_VALUE) {
					// sets the body receive timeout
					response.getNonBlockingBody().setReceiveTimeoutSec(adapter.getRemainingResponseTimeSec(System.currentTimeMillis()));
				}

				response.getNonBlockingBody().addCompleteListener(connectionAutoCloseListner);
				response.getNonBlockingBody().addCompleteListener(completeListener);
					

			// ... no
			} else {
				completeListener.onComplete();
				connectionAutoCloseListner.onComplete();
			}
				
			
			// call handlers call back method if is HEADER_RECEIVED triggered
			if (!adapter.isResponseHandlerInvokeOnMessageReceived()) {
				callOnResponseCallback(adapter, response);
			}
		}
	}
	
	
	private void addBodyParserIfRequired(HttpResponse response, INonBlockingConnection connection) throws IOException {
		
		try {
			
			// TODO If a particular HTTP response is not allowed to have a body return (e.g. HEAD request)
			
			
			
			// body less response by definition  (304, 204 or 1xx response)
			int status = response.getStatus();
			if ((status == 304) || (status == 204) || (status < 199)) {
				return;
			}
			
			
			//  transfer encoding header is set with chunked -> chunked body
			String transferEncoding = response.getTransferEncoding();
			if ((transferEncoding != null) && (transferEncoding.equalsIgnoreCase("chunked"))) {
				addChunkedBodyParser(response);
				return;
			}
	
			
			// contains a non-zero Content-Length header  -> bound body 
			if ((response.getContentLength() != -1)) {
				if (response.getContentLength() > 0) {
					addBoundBodyParser(response);
				}
				return;
			}
			
			// no bound body, no chunked body, content type set -> connection terminated body
			if (response.getContentType() != null) {
				addConnectionTerminatedBodyParser(response, connection);
				return;
			}
			
		} catch (BufferUnderflowException ignore) { }
	}
	
	

	
	
	
	private void handleResponseHeader(HttpResponseHeader responseHeader) throws IOException {

		// if HTTP 1.1 -> connection is persistent by default
		if ((responseHeader.getProtocol() != null) && responseHeader.getProtocol().equals("HTTP/1.1")) {
			setPersistent(true);
		}

	
		// handle connection header
		handleConnectionHeaders(responseHeader);
	}

	
	
	private void handleConnectionHeaders(HttpResponseHeader responseHeader) throws IOException {	
		
		String keepAliveHeader = responseHeader.getHeader("Keep-Alive");
		if (keepAliveHeader != null) {
			String[] tokens = keepAliveHeader.split(",");
			for (String token : tokens) {
				handleKeepAlive(token);
			}
		} 
		
		
		//check if persistent connection
		String connectionHeader = responseHeader.getHeader("Connection");
		
		
		if (connectionHeader != null) {
			String[] values = connectionHeader.split(",");
			
			
			for (String value : values) {
				value = value.trim();
				
				if (value.equalsIgnoreCase("close")) {
					if (LOG.isLoggable(Level.FINER)) {
						LOG.finer("[" + getId() + " http client connection received 'connection: close' header. set isPersistent=false");
					}
					setPersistent(false);
				}
			}
		}
	}

	
	
	private void handleKeepAlive(String option) {
		
		
		try {
			if (option.toUpperCase().startsWith("TIMEOUT=")) {
				int timeoutSec = Integer.parseInt(option.substring("TIMEOUT=".length(), option.length()));
				
				if (LOG.isLoggable(Level.FINE)) {
					LOG.fine("response keep alive defines timout. set timeout " + timeoutSec + " sec");
				}
				setResponseTimeoutSec(timeoutSec);

				
			} else if (option.toUpperCase().startsWith("MAX=")) {
				int maxTransactions = Integer.parseInt(option.substring("MAX=".length(), option.length()));
				if (maxTransactions == 0) {
					setPersistent(false);
				}
				
			} else {
				Integer timeoutSec = Integer.parseInt(option);
				
				if (LOG.isLoggable(Level.FINE)) {
					LOG.fine("response keep alive defines timout. set timeout " + timeoutSec + " sec");
				}
				setResponseTimeoutSec(timeoutSec);
			}
		} catch (Exception e) {
			if (LOG.isLoggable(Level.FINE)) {
				LOG.fine("error occured by handling keep alive option " + option + " " + e.toString());
			}
		}
	}




	
	private void callOnResponseCallback(final ResponseHandlerAdapter adapter, final HttpResponse response) {
		
		Runnable task = new Runnable() {
			public void run() {
				adapter.performOnResponse(response);
			}
		};
		
		if (adapter.isResponseHandler()) {
			if (adapter.isResponseHandlerMultithreaded()) {
				processMultiThreaded(task);
				
			} else {
				processNonThreaded(task);			
			}
		} else {
			LOG.warning("[" + getId() + "] message received, but response handler is not set");
		}
	}
	
	
	
	
	private boolean isFullResponse(INonBlockingConnection connection) throws IOException {
		
		connection.markReadPosition();
		
		try {
			String statusLine = connection.readStringByDelimiter("\n", 999);
			if ((statusLine.length() > 0) && (statusLine.charAt(0) == '\r')) {
				statusLine = statusLine.substring(1, statusLine.length());
			}
		
			
			int pos1 = statusLine.indexOf(" ");
			int pos2 = statusLine.indexOf(" ", pos1 + 1);
			if (pos2 == -1) {
				pos2 = statusLine.length();
			}
			

			try {
				String protocol = statusLine.substring(0, pos1);
				if (!protocol.toUpperCase().startsWith("HTTP")) {
					return false;
				}
				
				// try to read status code to check if response is a full response 
				Integer.parseInt(statusLine.substring(pos1 + 1, pos2).trim());
			
				return true;
			} catch (Exception e) {
				return false;
			}
				
		} catch (BufferUnderflowException bue) {
			if (isOpen()) {
				throw bue;
				
			} else {
				return false;
			}
			
		} catch (MaxReadSizeExceededException mre) {
			return false;	
			
		} finally {
			connection.resetToReadMark();
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public IHttpResponse call(IHttpRequest request) throws IOException, SocketTimeoutException {

		// create response handler 
		BlockingResponseHandler responseHandler = new BlockingResponseHandler();
		
		
		// send request 
		send(request, responseHandler);
		return responseHandler.getResponse();
	}


	
	/**
	 * {@inheritDoc}
	 */
	public void send(IHttpRequest request, IHttpResponseHandler responseHandler) throws IOException {
		send(request, new ResponseHandlerAdapter(responseHandler, this, receiveTimeoutSec));
	}
	
	
	private void performPreSendActions(IHttpRequestHeader requestHeader) throws IOException {
		countSendMessages++;	
	}


	void send(IHttpRequest request, ResponseHandlerAdapter responseHandlerAdapter) throws IOException {
		

		if (isOpen()) {
			try {
				// body less request?
				if (request.getNonBlockingBody() == null) {
					sendBodyless(request.getRequestHeader(), responseHandlerAdapter);
					
					
				// no, request has body
				} else {
					if (request.getNonBlockingBody().getDataHandler() != null) {
						throw new IOException("a body handler is already assigned to the message body. sending such messages is not supported (remove data handler)"); 
					}
					
					
					BodyDataSink bodyDataSink = null;
					
					// send with plain body
					if (request.getRequestHeader().getContentLength() >= 0) {
						bodyDataSink = send(request.getRequestHeader(), request.getContentLength(), responseHandlerAdapter);
						
					// send with chunked body
					} else {
						bodyDataSink = send(request.getRequestHeader(), responseHandlerAdapter);   
					}
					
					if (LOG.isLoggable(Level.FINE)) {						
						String remoteAddr = "";
						InetAddress rAddr = getRemoteAddress();
						if (rAddr != null) {
							remoteAddr = rAddr.toString();
						}
						LOG.fine("[" + getId() + "] sending (" + bodyDataSink.getClass().getSimpleName() +
								 " body, target=" + remoteAddr + "): " + request.getRequestHeader());
					}
					
					sendMessageBody(bodyDataSink, request.getNonBlockingBody());
				}
							
			} catch (IOException ioe) {
				String msg = "can not send request \r\n " + request.toString() +
							 "\r\n\r\nhttpConnection:\r\n" + this.toString() +
				             "\r\n\r\nreason:\r\n" + ioe.toString(); 
				throw new IOException(msg);
			}
			
		} else {
			throw new ClosedChannelException();
		}
	}
	



	/**
	 * {@inheritDoc}
	 */
	public BodyDataSink send(IHttpRequestHeader requestHeader, IHttpResponseHandler responseHandler) throws IOException {
		return send(requestHeader, new ResponseHandlerAdapter(responseHandler, this, receiveTimeoutSec));
	}
	
	
	
	private BodyDataSink send(IHttpRequestHeader requestHeader, ResponseHandlerAdapter responseHandlerAdapter) throws IOException {
		if (requestHeader.getContentLength() != -1) {
			return sendPlain(requestHeader, requestHeader.getContentLength(), responseHandlerAdapter);
		}
		
		return sendChunked(requestHeader, responseHandlerAdapter);
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public BodyDataSink send(IHttpRequestHeader requestHeader, int contentLength, IHttpResponseHandler responseHandler) throws IOException {
		if (requestHeader.getMethod().equalsIgnoreCase("GET") || requestHeader.getMethod().equalsIgnoreCase("HEAD")) {
			throw new IOException(requestHeader.getMethod() + " is a bodyless request");
		}
		
		return send(requestHeader, contentLength, new ResponseHandlerAdapter(responseHandler, this, receiveTimeoutSec));
	}

	
	private BodyDataSink send(IHttpRequestHeader requestHeader, int contentLength, ResponseHandlerAdapter responseHandlerAdapter) throws IOException {
		
		if ((requestHeader.getTransferEncoding() != null) && (requestHeader.getTransferEncoding().equalsIgnoreCase("chunked"))) {
			LOG.warning("message header contains Transfer-Encoding: chunked. removing it because message should be sent with predefined length (Content-Length)");
			requestHeader.removeHeader("Transfer-Encoding");
		}
		
		if (requestHeader.getMethod().equalsIgnoreCase("GET") || requestHeader.getMethod().equalsIgnoreCase("HEAD")) {
			throw new IOException(requestHeader.getMethod() + " is a bodyless request");
		}
		
		return sendPlain(requestHeader, contentLength, responseHandlerAdapter);
	}
	
	
	
	private void sendBodyless(IHttpRequestHeader requestHeader, ResponseHandlerAdapter responseHandlerAdapter) throws IOException {
		
		performPreSendActions(requestHeader);
		
		synchronized (responseHandlers) {
			responseHandlers.add(responseHandlerAdapter);
		}
		
		enhanceHeader(requestHeader);

		
		
		if (LOG.isLoggable(Level.FINE)) {
			LOG.fine("[" + getId() + "] sending (bodyless, target=" + getRemoteAddress().toString() + "): " + requestHeader);
		}

		FlushMode savedFlushmode = getFlushmode();
		setFlushmode(FlushMode.ASYNC);
		
		writeHeader(requestHeader);
		flush();
		
		setFlushmode(savedFlushmode);
	}

	
	
	BodyDataSink sendPlain(IHttpRequestHeader requestHeader, int contentLength, ResponseHandlerAdapter responseHandlerAdapter) throws IOException {
		
		performPreSendActions(requestHeader);
		
		synchronized (responseHandlers) {
			responseHandlers.add(responseHandlerAdapter);
		}
		
		return writePlainRequest(requestHeader, contentLength);
	}
		
	
	BodyDataSink sendChunked(IHttpRequestHeader requestHeader, ResponseHandlerAdapter responseHandlerAdapter) throws IOException {
		
		performPreSendActions(requestHeader);
		
		synchronized (responseHandlers) {
			responseHandlers.add(responseHandlerAdapter);
		}
		
		return writeChunkedRequest(requestHeader);
	}




	private BodyDataSink writePlainRequest(IHttpRequestHeader header, int contentLength) throws IOException {
		if (contentLength > 0) {
			header.setContentLength(contentLength);
		}

		enhanceHeader(header);
		return newBoundBodyDataSink(header);
	}

	


	private BodyDataSink writeChunkedRequest(IHttpRequestHeader header) throws IOException {
		String transferEncoding = header.getTransferEncoding();
		if (transferEncoding == null) {
			header.setTransferEncoding("chunked");
		}

		enhanceHeader(header);
		return newChunkedBodyDataSink(header);
	}
	
	 

	
	private void enhanceHeader(IHttpRequestHeader header) throws IOException {
		String host = header.getHeader("HOST");
		if (host == null) {
			header.setHeader("Host", getRemoteHostInfo());
		}
		
		if (header.getScheme() == null) {
			if (isSecure()) {
				header.setScheme("https");
			} else {
				header.setScheme("http");
			}
				
		}
		
		String userAgent = header.getHeader("USER-AGENT");
		if (userAgent == null) {
			header.setHeader("User-Agent", getVersionInfo());
		}		
	}
	
	

	 
	private static String getVersionInfo() {
		if (versionInfo == null) {
			versionInfo = "xSocket-http/" + HttpUtils.getVersionInfo();
		}
		
		return versionInfo;
	}

	
	private String getRemoteHostInfo() throws IOException {
		InetAddress remoteAddress = getRemoteAddress();
		
		if (remoteAddress == null) {
			return "";
		}
		
		int remotePort = getRemotePort();
		
		return remoteAddress.getHostName() + ":" + remotePort;
	}
	
	
	
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public String toString() {
		String s = "HttpClientConnection (requests=" + countSendMessages + "; responses=" + countReceivedMessages + ") " + super.toString();
		return s;
	}

	
	

	private void onConnect() throws IOException {
		if (connectHandlerAdapter != null) {
			connectHandlerAdapter.callOnConnect();
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	@Override
	protected void onDisconnect() throws IOException {
			
		terminateWatchDogTask();
		
		ArrayList responseHandlersCopy = null;
		synchronized (responseHandlers) {
			responseHandlersCopy = (ArrayList) responseHandlers.clone();
		}
		
	
		for (ResponseHandlerAdapter responseHandlerAdapter : responseHandlersCopy) {
			callExceptionCallback(responseHandlerAdapter, new IOException("connection has been disconnected. " + this.toString()));
		} 

		
		if (disconnectHandlerAdapter != null) {
			disconnectHandlerAdapter.callOnDisconnect();
		} 		
		
		super.onDisconnect();
		
		if (LOG.isLoggable(Level.FINE)) {
			LOG.finer("[" + getId() + "] http client connection closed");
		}
	}
	
	
	

	@SuppressWarnings("unchecked")
	private void checkTimeouts() {
		try {
			List responseHandlersCopy = null;
			synchronized (responseHandlers) {
				responseHandlersCopy = (List) responseHandlers.clone();
			}
			
			long currentTimeMillis = System.currentTimeMillis();
			
			for (ResponseHandlerAdapter responseHandlerAdapter : responseHandlersCopy) {
				if (responseHandlerAdapter.isResponseTimeoutReached(currentTimeMillis)) {
					onResponseTimeout(responseHandlerAdapter);
				}
			}
		} catch (Exception e) {
			LOG.warning("exception occured by checking timouts. Reason: " + e.toString());
		}
	}

	
	private void onResponseTimeout(ResponseHandlerAdapter responseHandlerAdapter) {
		setPersistent(false);
		
		// and notify the response timeout handler
		callExceptionCallback(responseHandlerAdapter, new SocketTimeoutException("timeout " + responseHandlerAdapter.getReceiveTimeoutSec() + " sec reached"));
		
		// destroy the connection
		destroy();
	}
	
	
	private void callExceptionCallback(final ResponseHandlerAdapter responseHandlerAdapter, final IOException ioe) {
		
		synchronized (responseHandlers) {
			if (responseHandlers.contains(responseHandlerAdapter)) {
				responseHandlers.remove(responseHandlerAdapter);
			} else {
				return;
			}
		}
		
	
		if ((ioe instanceof SocketTimeoutException) &&  responseHandlerAdapter.isResponseTimeoutHandler()) {

			Runnable task = new Runnable() {
				public void run() {
					responseHandlerAdapter.performOnException((SocketTimeoutException) ioe);
				}
			};
				
			if (responseHandlerAdapter.isResponseTimeoutHandlerMultithreaded()) {
				processMultiThreaded(task);
						
			} else {
				processNonThreaded(task);			
			}

			return;
		}

		
		Runnable task = new Runnable() {
			public void run() {
				responseHandlerAdapter.performOnException(ioe);
			}
		};
				
		if (responseHandlerAdapter.isResponseExceptionHandlerMultithreaded()) {
			processMultiThreaded(task);
					
		} else {
			processNonThreaded(task);			
		}
	}
	 
	
	

	static class ResponseHandlerAdapter {
	
		private IHttpResponseHandler responseHandler = null;
		private boolean isResponseHandler = true;
		private boolean isResponseHandlerMultithreaded = true;
		private boolean isResponseExceptionHandlerMultithreaded = true;
		private boolean isResponseHandlerInvokeOnMessageReceived = false;
		private boolean isResponseTimeoutHandler = false;
		private boolean isResponseTimeoutHandlerMultithreaded = true;
		
		private HttpClientConnection httpConnection = null;

		private long creationTimeMillis = System.currentTimeMillis();
		private int receiveTimeoutSec = 0;
		
		
		ResponseHandlerAdapter(IHttpResponseHandler handler, HttpClientConnection httpConnection, int receiveTimeoutSec) throws IOException {
			this(handler, HttpUtils.isResponseHandler(handler), HttpUtils.isResponseExceptionHandlerMultithreaded(handler), HttpUtils.isResponseHandlerMultithreaded(handler), HttpUtils.isResponseTimeoutHandler(handler), HttpUtils.isRequestTimeoutHandlerMultithreaded(handler), HttpUtils.isResponseHandlerInvokeOnMessageReceived(handler), httpConnection, receiveTimeoutSec);
		}
		
		ResponseHandlerAdapter(IHttpResponseHandler responseHandler, boolean isResponseHandler, boolean isResponseExceptionHandlerMultithreaded, boolean isResponseHandlerMultithreaded, boolean isResponseTimeoutHandler, boolean isResponseTimeoutHandlerMultithreaded, boolean isResponseHandlerInvokeOnMessageReceived, HttpClientConnection httpConnection, int receiveTimeoutSec) throws IOException {
			this.responseHandler = responseHandler;
			this.httpConnection = httpConnection;
			this.receiveTimeoutSec = receiveTimeoutSec;
			
			this.isResponseHandler = isResponseHandler;
			this.isResponseExceptionHandlerMultithreaded = isResponseExceptionHandlerMultithreaded;
			this.isResponseHandlerMultithreaded = isResponseHandlerMultithreaded;
			this.isResponseTimeoutHandler = isResponseTimeoutHandler;
			this.isResponseTimeoutHandlerMultithreaded = isResponseTimeoutHandlerMultithreaded;
			this.isResponseHandlerInvokeOnMessageReceived = isResponseHandlerInvokeOnMessageReceived;
		}
		
	
		boolean isResponseTimeoutHandlerMultithreaded() {
			return isResponseTimeoutHandlerMultithreaded;
		}

		
		boolean isResponseTimeoutHandler() {
			return isResponseTimeoutHandler;
		}

		
		boolean isResponseHandlerInvokeOnMessageReceived() {
			return isResponseHandlerInvokeOnMessageReceived;
		}
		
		boolean isResponseHandler() {
			return isResponseHandler;
		}
		
		boolean isResponseHandlerMultithreaded() {
			return isResponseHandlerMultithreaded;
		}
		
		
		boolean isResponseExceptionHandlerMultithreaded() {
			return isResponseExceptionHandlerMultithreaded;
		}
		
		final HttpClientConnection getHttpConnection() {
			return httpConnection;
		}
		
		
		final boolean isResponseTimeoutReached(long currentTime) {
			if (getRemainingResponseTimeSec(currentTime) <= 0) {
				if (LOG.isLoggable(Level.FINE)) {
					LOG.fine("[" + httpConnection.getId() + "] response timeout " +
							 DataConverter.toFormatedDuration((long) receiveTimeoutSec * 1000) + " reached)");
				}
				return true;

			} else {
				return false;
			}
		}
		
		private int getRemainingResponseTimeSec(long currentTime) {
			return receiveTimeoutSec - getElapsedResponseTimeSec(currentTime);
		}
		
		private int getElapsedResponseTimeSec(long currentTimeMillis) {
			long elapsed = currentTimeMillis - creationTimeMillis;
			if (elapsed < 1000) {
				return 0;
			} else {
				return (int) elapsed / 1000;
			}
		}
		
		int getReceiveTimeoutSec() {
			return receiveTimeoutSec;
		}
		
		
		public void performOnResponse(HttpResponse response)  {
			try {
				responseHandler.onResponse(response);
			} catch (Exception e) {		
				if (LOG.isLoggable(Level.FINE)) {
					LOG.fine("[" + httpConnection.getId() + "] error occured by calling on response " + responseHandler + " " + response + " " + e.toString());
					LOG.fine("destroying connection " + httpConnection.getId());
				}
				
				httpConnection.destroy();
			}	
		}

		
		public void performOnException(IOException ioe)  {
			responseHandler.onException(ioe);
		}

		public void performOnException(SocketTimeoutException stoe)  {
			((IHttpResponseTimeoutHandler) responseHandler).onException(stoe);
		}
		
		public void onMessageCompleteReceived() { }
	}
	
	
	BodyDataSink newInterception(HttpClient httpClient, HttpClientConnection con, IHttpRequestHeader requestHeader, IHttpRequestHandler interceptor, boolean isInterceptorChain, IHttpResponseHandler responseHandler) throws IOException {
		return super.newClientInterception(httpClient, con, requestHeader, interceptor, isInterceptorChain, responseHandler);
	}
	

	

	void newExchange(HttpClient httpClient, HttpClientConnection con, IHttpRequest request, IHttpRequestHandler requestHandler, boolean isRequestHandlerChain, IHttpResponseHandler responseHandler) throws IOException {
		super.newClientExhange(httpClient, con, request, requestHandler, isRequestHandlerChain, responseHandler);
	}
	
	
	

	private final class ConnectionAutoCloseListener implements IBodyCompleteListener {
		
		@Execution(Execution.NONTHREADED)
		public void onComplete() throws IOException {

			// is not persistent?
			if (!isPersistent()) {
				if (LOG.isLoggable(Level.FINE)) {
					LOG.fine("[" + getId() + "] destroying connection because it is not persistent");
				}			
				
				// destroy it and return
				destroy();
				return;
				
			// .. it is persistent	
			} else {
				
				// close after response flag? 
				if (isCloseAfterResponse) {
					close();
				}
			}
		}
	}
	
	
	
	
	final static class BlockingResponseHandler implements IHttpResponseHandler {
		
		private final Object readLock = new Object();
		
		private final AtomicReference responseRef = new AtomicReference();
		private final AtomicReference ioExceptionRef = new AtomicReference();
		
		
		@Execution(Execution.NONTHREADED)
		public void onResponse(IHttpResponse response) throws IOException {
			responseRef.set(response);
			
			synchronized (readLock) {
				readLock.notifyAll();
			}
		}
	
		@Execution(Execution.NONTHREADED)
		public void onException(IOException ioe) {
			ioExceptionRef.set(ioe);
			
			synchronized (readLock) {
				readLock.notifyAll();
			}
		}
		
		
		IHttpResponse getResponse() throws IOException {
			

			do {
				synchronized (readLock) {
					if (ioExceptionRef.get() != null) {
						throw ioExceptionRef.get();
					}
					
					if (responseRef.get() == null) {
						try {
							readLock.wait();
						} catch (InterruptedException ignore) { }
					} else {
						return responseRef.get();
					}
				}
				
			} while (true);
		}
	}
	
	
	
	
	private final class ConnectHandlerAdapter implements Runnable {
		
		private boolean isMultithreaded = true;
		
		public ConnectHandlerAdapter(boolean isMultithreaded) {
			this.isMultithreaded = isMultithreaded;
		}
		
		private void callOnConnect() {
		
			if (isMultithreaded) {
				processMultiThreaded(this);
			} else {
				processNonThreaded(this);			
			}
		}
		
		public void run() {
			if (LOG.isLoggable(Level.FINE)) {
				LOG.fine("calling onConnect on " + handler);
			}
			
			try {
				((IHttpConnectHandler) handler).onConnect(HttpClientConnection.this);
			} catch (Exception e) {		
				if (LOG.isLoggable(Level.FINE)) {
					LOG.fine("[" + getId() + "] error occured by calling on connect " + handler + " " + e.toString());
				}
			}	
		}
	}
	
	
	private final class DisconnectHandlerAdapter implements Runnable {
		
		private boolean isMultithreaded = true;
		
		public DisconnectHandlerAdapter(boolean isMultithreaded) {
			this.isMultithreaded = isMultithreaded;
		}
		
		private void callOnDisconnect() {
		
			if (isMultithreaded) {
				processMultiThreaded(this);
			} else {
				processNonThreaded(this);			
			}
		}
		
		public void run() {
			if (LOG.isLoggable(Level.FINE)) {
				LOG.fine("calling onDisconnect on " + handler);
			}
			
			try {
				((IHttpDisconnectHandler) handler).onDisconnect(HttpClientConnection.this);
			} catch (Exception e) {		
				if (LOG.isLoggable(Level.FINE)) {
					LOG.fine("[" + getId() + "] error occured by calling on connect " + handler + " " + e.toString());
				}
			}	
		}
	}
	
	
	
	
	
	private static final class WatchDogTask extends TimerTask {
		
		private WeakReference httpClientConnectionRef = null;
		
		public WatchDogTask(HttpClientConnection httpClientConnection) {
			httpClientConnectionRef = new WeakReference(httpClientConnection);
		}
	
		
		@Override
		public void run() {
			HttpClientConnection httpClientConnection = httpClientConnectionRef.get();
			
			if (httpClientConnection == null)  {
				this.cancel();
				
			} else {
				httpClientConnection.checkTimeouts();
			}
		}		
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy