/*
* Copyright (c) xlightweb.org, 2008 - 2009. 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.xlightweb.org/
*/
package org.xlightweb.client;
import java.io.Closeable;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import org.xlightweb.BodyDataSink;
import org.xlightweb.FutureResponseHandler;
import org.xlightweb.HttpRequest;
import org.xlightweb.HttpUtils;
import org.xlightweb.IFutureResponse;
import org.xlightweb.IHttpConnectHandler;
import org.xlightweb.IHttpRequest;
import org.xlightweb.IHttpRequestHandler;
import org.xlightweb.IHttpRequestHeader;
import org.xlightweb.IHttpResponse;
import org.xlightweb.IHttpResponseHandler;
import org.xlightweb.RequestHandlerChain;
import org.xlightweb.client.HttpClientConnection.ClientExchange;
import org.xlightweb.client.TransactionMonitor.Transaction;
import org.xlightweb.client.TransactionMonitor.TransactionLog;
import org.xsocket.ILifeCycle;
import org.xsocket.connection.IConnectionPool;
/**
* Higher level client-side abstraction of the client side endpoint. Internally, the HttpClient uses a pool
* of {@link HttpClientConnection} to perform the requests. Example:
*
* <pre>
* HttpClient httpClient = new HttpClient();
*
* // set some properties
* httpClient.setFollowsRedirect(true);
* httpClient.setAutoHandleCookies(false);
* // ...
*
* // perform a synchronous call
* IHttpResponse response = httpClient.call(new GetRequest("http://www.gmx.com/index.html"));
* System.out.println(response.getStatus());
*
* BlockingBodyDataSource bodyChannel = response.getBlockingBody();
* System.out.println(bodyChannel.readString());
*
*
* // perform an asynchronous request
* MyResponseHandler respHdl = new MyResponseHandler();
* httpClient.send(new HttpRequestHeader("GET", "http://www.gmx.com/index.html"), respHdl);
*
* //..
*
* httpClient.close();
* </pre>
*
* @author grro@xlightweb.org
*/
public class HttpClient implements IHttpClientEndpoint, IConnectionPool, Closeable {
private static final Logger LOG = Logger.getLogger(HttpClient.class.getName());
public static final int DEFAULT_CREATION_MAX_WAIT_TIMEOUT = 60 * 1000;
public static final int DEFAULT_POOLED_LIFE_TIMEOUT_MILLIS = 30 * 1000;
public static final int DEFAULT_POOLED_IDLE_TIMEOUT_MILLIS = 3 * 1000;
public static final int DEFAULT_MAX_REDIRECTS = 5;
public static final boolean DEFAULT_TREAT_302_REDIRECT_AS_303 = false;
public static final Long DEFAULT_RESPONSE_TIMEOUT_SEC = Long.MAX_VALUE;
private int maxRedirects = DEFAULT_MAX_REDIRECTS;
private boolean isTreat302RedirectAs303 = DEFAULT_TREAT_302_REDIRECT_AS_303;
// pool
private boolean isPooled = true;
private final HttpClientConnectionPool pool;
// auto supported handlers
public static final boolean DEFAULT_FOLLOWS_REDIRECT = false;
private boolean isFollowsRedirect = DEFAULT_FOLLOWS_REDIRECT;
private final AutoRedirectHandler redirectHandler = new AutoRedirectHandler(HttpClient.this);
public static final boolean DEFAULT_AUTOHANDLING_COOKIES = true;
private boolean isAutohandlingCookies = DEFAULT_AUTOHANDLING_COOKIES;
private final CookieHandler cookiesHandler = new CookieHandler();
public static final boolean DEFAULT_PROXY_ACTIVATED = false;
private boolean isProxyActivated = DEFAULT_PROXY_ACTIVATED;
private final ProxyHandler proxyHandler = new ProxyHandler();
public static final int DEFAULT_CACHE_SIZE = 0;
private final CacheHandler cacheHandler = new CacheHandler(this, DEFAULT_CACHE_SIZE);
private boolean isShowCache = false;
private final RequestHandlerChain chain = new RequestHandlerChain();
// the assigned session manager
private SessionManager sessionManager = null;
// statistics
private long lastTimeRequestSentMillis = System.currentTimeMillis();
// transaction monitor
private TransactionMonitor transactionMonitor = null;
private final TransactionLog transactionLog = new TransactionLog(0);
/**
* constructor
*/
public HttpClient() {
this(null, new IHttpRequestHandler[0]);
}
/**
* constructor
*
* @param interceptors interceptor
*/
public HttpClient(IHttpRequestHandler... interceptors) {
this(null, interceptors);
}
/**
* constructor
*
* @param sslCtx the ssl context to use
*/
public HttpClient(SSLContext sslCtx) {
this(sslCtx, new IHttpRequestHandler[0]);
}
/**
* constructor
*
* @param sslCtx the ssl context to use
* @param interceptors the interceptors
*/
public HttpClient(SSLContext sslCtx, IHttpRequestHandler... interceptors) {
if (sslCtx != null) {
pool = new HttpClientConnectionPool(sslCtx);
} else {
pool = new HttpClientConnectionPool();
}
setCreationMaxWaitMillis(DEFAULT_CREATION_MAX_WAIT_TIMEOUT);
setPooledMaxIdleTimeMillis(DEFAULT_POOLED_IDLE_TIMEOUT_MILLIS);
setPooledMaxLifeTimeMillis(DEFAULT_POOLED_LIFE_TIMEOUT_MILLIS);
sessionManager = new SessionManager();
proxyHandler.setSSLContext(sslCtx);
for (IHttpRequestHandler interceptor : interceptors) {
addInterceptor(interceptor);
}
resetChain();
chain.onInit();
}
/**
* adds an interceptor. Example:
*
* <pre>
* HttpClient httpClient = new HttpClient();
*
* LoadBalancerRequestInterceptor lbInterceptor = new LoadBalancerRequestInterceptor();
* lbInterceptor.addVirtualServer("http://customerService", "srv1:8030", "srv2:8030");
* httpClient.addInterceptor(lbInterceptor);
*
* // ...
* GetRequest request = new GetRequest("http://customerService/price?id=2336&amount=5656");
* IHttpResponse response = httpClient.call(request);
* //...
*
* </pre>
*
* @param interceptor the interceptor to add
*/
public void addInterceptor(IHttpRequestHandler interceptor) {
if (interceptor instanceof ILifeCycle) {
((ILifeCycle) interceptor).onInit();
}
if (!HttpUtils.isConnectHandlerWarningIsSuppressed() && (interceptor instanceof IHttpConnectHandler)) {
LOG.warning("only IHttpRequestHandler is supported. The onConnect(...) method will not be called. (suppress this warning by setting system property org.xlightweb.httpConnectHandler.suppresswarning=true)");
}
chain.addLast(interceptor);
resetChain();
}
private void resetChain() {
chain.remove(cacheHandler);
chain.remove(cookiesHandler);
chain.remove(redirectHandler);
chain.remove(proxyHandler);
if (cacheHandler.getMaxCacheSizeBytes() > 0) {
chain.addFirst(cacheHandler);
}
if (isFollowsRedirect == true) {
chain.addFirst(redirectHandler);
}
if (isAutohandlingCookies == true) {
chain.addFirst(cookiesHandler);
}
if (isProxyActivated) {
chain.addLast(proxyHandler);
}
}
/**
* sets if redirects should be followed
*
* @param isFollowsRedirect true, if redirects should be followed
*/
public void setFollowsRedirect(boolean isFollowsRedirect) {
if (this.isFollowsRedirect == isFollowsRedirect) {
return;
}
this.isFollowsRedirect = isFollowsRedirect;
resetChain();
}
/**
* returns true, if redirects should be followed
* @return true, if redirects should be followed
*/
public boolean getFollowsRedirect() {
return isFollowsRedirect;
}
/**
* sets if cookies should be auto handled
*
* @param isAutohandlingCookies true, if cookies should be auto handled
*/
public void setAutoHandleCookies(boolean isAutohandlingCookies) {
if (this.isAutohandlingCookies == isAutohandlingCookies) {
return;
}
this.isAutohandlingCookies = isAutohandlingCookies;
resetChain();
}
/**
* sets the cache size (in kilo bytes)
*
* @param maxSizeKb the max cache size in bytes or 0 to deactivate caching
*/
public void setCacheMaxSizeKB(int maxSizeKb) {
cacheHandler.setMaxCacheSizeBytes(maxSizeKb * 1024);
resetChain();
}
/**
* returns the max cache size
*
* @return the max cache size
*/
public int getCacheMaxSizeKB() {
return (cacheHandler.getMaxCacheSizeBytes() / 1024);
}
/**
* returns the cache size
*
* @return the cache size
*/
public float getCacheSizeKB() {
return ((float) cacheHandler.getCurrentCacheSizeBytes() / 1000);
}
/**
* sets if the cache is shared between users
*
* @param isSharedCache true, if the cache is shared between users
*/
public void setCacheShared(boolean isSharedCache) {
cacheHandler.setSharedCache(isSharedCache);
}
/**
* returns true, if the cache is shared between users
*
* @return true, if the cache is shared between users
*/
public boolean isCacheShared() {
return cacheHandler.isSharedCache();
}
/**
* sets the proxy host to use. Example:
*
* <pre>
* HttpClient httpClient = new HttpClient();
*
* // sets the proxy adress
* httpClient.setProxyHost(host);
* httpClient.setProxyPort(port);
*
* // set auth params (only necessary if proxy authentication is required)
* httpClient.setProxyUser(user);
* httpClient.setProxyPassword(pwd);
*
* // calling through the proxy
* IHttpResponse resp = httpClient.call(new GetRequest("http://www.gmx.com/");
* // ...
* </pre>
*
* @param proxyHost the proxy host or <null>
*/
public void setProxyHost(String proxyHost) {
proxyHandler.setProxyHost(proxyHost);
if ((proxyHost != null) && (proxyHost.length() > 1)) {
isProxyActivated = true;
}
resetChain();
}
/**
* sets the proxy port. Default is 80. For an example see {@link HttpClient#setProxyHost(String)}
*
* @param proxyPort the proxy port
*/
public void setProxyPort(int proxyPort) {
proxyHandler.setProxyPort(proxyPort);
}
/**
* sets the secured proxy host. Example:
*
* <pre>
* // SSL context has to be set to support SSL
* HttpClient httpClient = new HttpClient(SSLContext.getDefault());
*
* // sets the secured proxy adress
* httpClient.setProxySecuredHost(host);
* httpClient.setProxySecuredPort(port);
*
* // calling through the proxy
* IHttpResponse resp = httpClient.call(new GetRequest("https://www.gmx.com/");
* // ...
* </pre>
*
*
*
* @param proxyHost the secured proxy host or <null>
*/
public void setProxySecuredHost(String proxyHost) {
proxyHandler.setSecuredProxyHost(proxyHost);
if ((proxyHost != null) && (proxyHost.length() > 1)) {
isProxyActivated = true;
}
resetChain();
}
/**
* sets the secured proxy port. Default is 443. For an example see {@link HttpClient#setProxySecuredHost(String)}
*
* @param proxyPort the proxy port
*/
public void setProxySecuredPort(int proxyPort) {
proxyHandler.setSecuredProxyPort(proxyPort);
}
/**
* sets the user name for proxy authentification
* @param proxyUser the user name
*/
public void setProxyUser(String proxyUser) {
proxyHandler.setProxyUser(proxyUser);
}
/**
* sets the user password for proxy authentification
*
* @param proxyPassword the user password
*/
public void setProxyPassword(String proxyPassword) {
proxyHandler.setProxyPassword(proxyPassword);
}
/**
* returns if cookies should be auto handled
* @return true, if cookies should be auto handled
*/
public boolean isAutohandleCookies() {
return isAutohandlingCookies;
}
/**
* returns the session manager
*
* @return the session manager
*/
SessionManager getSessionManager() {
return sessionManager;
}
/**
* set the max redirects
*
* @param maxRedirects the max redirects
*/
public void setMaxRedirects(int maxRedirects) {
this.maxRedirects = maxRedirects;
}
/**
* get the max redirects
*
* @return the max redirects
*/
public int getMaxRedirects() {
return maxRedirects;
}
/**
* sets if a 302 response should be treat as a 303 response
*
* @param isTreat303RedirectAs302 true, if a 303 response should be treat a a 303 response
*/
public void setTreat302RedirectAs303(boolean isTreat303RedirectAs302) {
this.isTreat302RedirectAs303 = isTreat303RedirectAs302;
}
/**
* gets if a 302 response should be treat as a 303 response
*
* @return true, if a 302 response should be treat as a 303 response
*/
public boolean isTreat302RedirectAs303() {
return isTreat302RedirectAs303;
}
/**
* get the max size of the transaction log
*
* @return the max size of the transaction log
*/
int getTransactionLogMaxSize() {
return transactionLog.getMaxSize();
}
/**
* returns the number of pending transactions
*
* @return the number of pending transactions
*/
Integer getTransactionsPending() {
if (transactionMonitor != null) {
return transactionMonitor.getPendingTransactions();
} else {
return null;
}
}
/**
* sets the max size of the transaction log
*
* @param maxSize the max size of the transaction log
*/
void setTransactionLogMaxSize(int maxSize) {
transactionLog.setMaxSize(maxSize);
if (maxSize == 0) {
transactionMonitor = null;
} else {
transactionMonitor = new TransactionMonitor(transactionLog);
}
pool.setTranactionMonitor(transactionMonitor);
}
/**
* set the worker pool which will be assigned to the connections for call back handling
*
* @param workerpool the worker pool
*/
public void setWorkerpool(Executor workerpool) {
pool.setWorkerpool(workerpool);
}
/**
* returns the assigned worker pool
*
* @return the assigned worker pool
*/
Executor getWorkerpool() {
return pool.getWorkerpool();
}
/**
* @deprecated
*/
public boolean isPooled() {
return isPooled;
}
/**
* @deprecated
*/
public void setPooled(boolean isPooled) {
if (!isPooled) {
LOG.warning("isPooled is deprecated and will be ignored");
}
this.isPooled = isPooled;
}
/**
* {@inheritDoc}
*/
public void setResponseTimeoutMillis(long responseTimeoutMillis) {
pool.setResponseTimeoutMillis(responseTimeoutMillis);
}
/**
* {@inheritDoc}
*/
public long getResponseTimeoutMillis() {
return pool.getResponseTimeoutMillis();
}
/**
* {@inheritDoc}
*/
public final void setBodyDataReceiveTimeoutMillis(long bodyDataReceiveTimeoutMillis) {
pool.setBodyDataReceiveTimeoutMillis(bodyDataReceiveTimeoutMillis);
}
/**
* {@inheritDoc}
*/
public void close() throws IOException {
pool.close();
chain.onDestroy();
sessionManager.close();
sessionManager = null;
}
/**
* {@inheritDoc}
*/
public boolean isOpen() {
return pool.isOpen();
}
/**
* returns a unique id
*
* @return the id
*/
public String getId() {
return Integer.toString(this.hashCode());
}
/**
* {@inheritDoc}
*/
public void addListener(ILifeCycle listener) {
pool.addListener(listener);
}
/**
* {@inheritDoc}
*/
public boolean removeListener(ILifeCycle listener) {
return pool.removeListener(listener);
}
/**
* {@inheritDoc}
*/
public void setPooledMaxIdleTimeMillis(int idleTimeoutMillis) {
pool.setPooledMaxIdleTimeMillis(idleTimeoutMillis);
}
/**
* {@inheritDoc}
*/
public int getPooledMaxIdleTimeMillis() {
return pool.getPooledMaxIdleTimeMillis();
}
/**
* {@inheritDoc}
*/
public void setPooledMaxLifeTimeMillis(int lifeTimeoutMillis) {
pool.setPooledMaxLifeTimeMillis(lifeTimeoutMillis);
}
/**
* {@inheritDoc}
*/
public int getPooledMaxLifeTimeMillis() {
return pool.getPooledMaxLifeTimeMillis();
}
/**
* {@inheritDoc}
*/
public long getCreationMaxWaitMillis() {
return pool.getCreationMaxWaitMillis();
}
/**
* {@inheritDoc}
*/
public void setCreationMaxWaitMillis(long maxWaitMillis) {
pool.setCreationMaxWaitMillis(maxWaitMillis);
}
/**
* {@inheritDoc}
*/
public void setMaxIdle(int maxIdle) {
pool.setMaxIdle(maxIdle);
}
/**
* {@inheritDoc}
*/
public int getMaxIdle() {
return pool.getMaxIdle();
}
/**
* {@inheritDoc}
*/
public void setMaxActive(int maxActive) {
pool.setMaxActive(maxActive);
}
/**
* {@inheritDoc}
*/
public int getMaxActive() {
return pool.getMaxActive();
}
/**
* {@inheritDoc}
*/
public int getNumActive() {
return pool.getNumActive();
}
/**
* {@inheritDoc}
*/
public int getNumIdle() {
return pool.getNumIdle();
}
/**
* {@inheritDoc}
*/
int getNumPendingGet() {
return pool.getNumPendingGet();
}
/**
* {@inheritDoc}
*/
public int getNumCreated() {
return pool.getNumCreated();
}
/**
* {@inheritDoc}
*/
public int getNumDestroyed() {
return pool.getNumDestroyed();
}
/**
* get the number of creation errors
*
* @return the number of creation errors
*/
int getNumCreationError() {
return pool.getNumCreationError();
}
/**
* {@inheritDoc}
*/
public int getNumTimeoutPooledMaxIdleTime() {
return pool.getNumTimeoutPooledMaxIdleTime();
}
/**
* {@inheritDoc}
*/
public int getNumTimeoutPooledMaxLifeTime() {
return pool.getNumTimeoutPooledMaxLifeTime();
}
int getNumCacheHit() {
return cacheHandler.getCountCacheHit();
}
int getNumCacheMiss() {
return cacheHandler.getCountCacheMiss();
}
/**
* {@inheritDoc}
*/
public List<String> getActiveConnectionInfos() {
return pool.getActiveConnectionInfos();
}
/**
* {@inheritDoc}
*/
public List<String> getIdleConnectionInfos() {
return pool.getIdleConnectionInfos();
}
boolean isCacheInfoDisplay() {
return isShowCache;
}
void setCacheInfoDisplay(boolean isShowCache) {
this.isShowCache = isShowCache;
}
List<String> getCacheInfo() {
if (isShowCache) {
return cacheHandler.getCacheInfo();
} else {
return null;
}
}
/**
* returns the transaction log
* @return the transaction log
*/
List<String> getTransactionInfos() {
List<String> result = new ArrayList<String>();
for (Transaction transaction : transactionLog.getTransactions()) {
result.add(transaction.toString());
}
return result;
}
/**
* {@inheritDoc}
*/
public IHttpResponse call(IHttpRequest request) throws IOException, SocketTimeoutException {
try {
IFutureResponse futureResponse = send(request);
return futureResponse.getResponse();
} catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
/**
* {@inheritDoc}
*/
public IFutureResponse send(IHttpRequest request) throws IOException, ConnectException {
FutureResponseHandler responseHandler = new FutureResponseHandler();
send(request, responseHandler);
return responseHandler;
}
/**
* {@inheritDoc}
*/
public void send(IHttpRequest request, IHttpResponseHandler responseHandler) throws IOException, ConnectException {
lastTimeRequestSentMillis = System.currentTimeMillis();
// log trace if activated
if (transactionMonitor != null) {
transactionMonitor.register(request.getRequestHeader());
}
ClientExchange clientExchange = new ClientExchange(pool, sessionManager, responseHandler, request);
chain.onRequest(clientExchange);
}
/**
* {@inheritDoc}
*/
public BodyDataSink send(IHttpRequestHeader requestHeader, int contentLength, IHttpResponseHandler responseHandler) throws IOException, ConnectException {
requestHeader.setContentLength(contentLength);
return sendInternal(requestHeader, responseHandler);
}
/**
* {@inheritDoc}
*/
public BodyDataSink send(IHttpRequestHeader requestHeader, IHttpResponseHandler responseHandler) throws IOException, ConnectException {
requestHeader.setTransferEncoding("chunked");
return sendInternal(requestHeader, responseHandler);
}
private BodyDataSink sendInternal(IHttpRequestHeader requestHeader, IHttpResponseHandler responseHandler) throws IOException, ConnectException {
lastTimeRequestSentMillis = System.currentTimeMillis();
BodyDataSink dataSink = HttpClientConnection.newInMemoryBodyDataSink(requestHeader.getCharacterEncoding(), pool.getWorkerpool());
IHttpRequest request = new HttpRequest(requestHeader, HttpClientConnection.getDataSourceOfInMemoryBodyDataSink(dataSink));
send(request, responseHandler);
return dataSink;
}
/**
* gets the time when the last request has been sent
*
* @return the time when the last request has been sent
*/
long getLastTimeRequestSentMillis() {
return lastTimeRequestSentMillis;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder(super.toString());
sb.append("\r\nnumCreatedConnections " + getNumCreated());
sb.append("\r\nnumCreationError " + getNumCreationError());
sb.append("\r\nnumDestroyedConnections " + getNumDestroyed());
List<String> active = getActiveConnectionInfos();
if (active.isEmpty()) {
sb.append("\r\nnumActiveConnections 0");
} else {
sb.append("\r\n" + active.size() + " active connections:");
for (String connectionInfo : getActiveConnectionInfos()) {
sb.append("\r\n " + connectionInfo);
}
}
List<String> idle = getActiveConnectionInfos();
if (idle.isEmpty()) {
sb.append("\r\nnumIdleConnections 0");
} else {
sb.append("\r\n" + idle.size() + " idle connections:");
for (String connectionInfo : getIdleConnectionInfos()) {
sb.append("\r\n " + connectionInfo);
}
}
sb.append("\r\ntransaction log:");
for (String transactionInfo : getTransactionInfos()) {
sb.append("\r\n " + transactionInfo);
}
return sb.toString();
}
}