org.xsocket.connection.http.client.HttpClient Maven / Gradle / Ivy
/*
* 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.Closeable;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import org.xsocket.ILifeCycle;
import org.xsocket.connection.IConnectionPool;
import org.xsocket.connection.INonBlockingConnection;
import org.xsocket.connection.NonBlockingConnection;
import org.xsocket.connection.NonBlockingConnectionPool;
import org.xsocket.connection.http.BodyDataSink;
import org.xsocket.connection.http.NonBlockingBodyDataSource;
import org.xsocket.connection.http.Request;
import org.xsocket.connection.http.RequestHeader;
import org.xsocket.connection.http.Response;
import org.xsocket.connection.http.ResponseHeader;
import org.xsocket.connection.http.client.HttpClientConnection.BlockingResponseHandler;
import org.xsocket.connection.http.client.HttpClientConnection.ResponseHandlerAdapter;
/**
* Higher level client-side abstraction of the client side endpoint. The HttpClient uses am internal pool
* of {@link HttpClientConnection} to perform the requests.
*
* @author [email protected]
*/
public final class HttpClient implements IHttpClientEndpoint, IConnectionPool, Closeable {
private static final Logger LOG = Logger.getLogger(HttpClient.class.getName());
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
private static final boolean DEFAULT_IS_TRANSACTION_LOG = true;
public static final int DEFAULT_MAX_REDIRECTS = 5;
public static final boolean DEFAULT_TREAT_302_REDIRECT_AS_303 = false;
public static final Integer DEFAULT_RECEIVE_TIMEOUT_MILLIS = Integer.MAX_VALUE;
private int maxRedirects = DEFAULT_MAX_REDIRECTS;
private boolean isTreat302RedirectAs303 = DEFAULT_TREAT_302_REDIRECT_AS_303;
private int receiveTimeoutMillis = DEFAULT_RECEIVE_TIMEOUT_MILLIS;
private boolean isTransactionLog = DEFAULT_IS_TRANSACTION_LOG;
private SSLContext sslCtx = null;
private NonBlockingConnectionPool pool = null;
private boolean isPooled = true;
private boolean isSSLSupported = false;
// statistics
private TransactionLog transactionLog = new TransactionLog(20);
/**
* constructor
*/
public HttpClient() {
this(null);
}
/**
* constructor
*
* @param sslCtx the ssl context to use
*/
public HttpClient(SSLContext sslCtx) {
if (sslCtx != null) {
this.sslCtx = sslCtx;
pool = new NonBlockingConnectionPool(sslCtx);
isSSLSupported = true;
} else {
pool = new NonBlockingConnectionPool();
isSSLSupported = false;
}
}
/**
* 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;
}
/**
* activates the transaction log
*
* @param isTransactionLog true, if transaction log sholud be activated
*/
void setTransactionLog(boolean isTransactionLog) {
this.isTransactionLog = isTransactionLog;
}
/**
* returns if the transaction log is activated
*
* @return true, if the transaction log is activated
*/
boolean isTransactionLog() {
return isTransactionLog;
}
/**
* get the max size of the transaction log
*
* @return the max size of the transaction log
*/
int getTransactionLogMaxSize() {
return transactionLog.getMaxSize();
}
/**
* sets the max size of the transaction log
*
* @param maxSize the max size of the transaction log
*/
void setTransactionLogMaxSize(int maxSize) {
transactionLog.setMaxSize(maxSize);
}
/**
* 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 is pooling is used
*
* @return true, if pooling is used
*/
public boolean isPooled() {
return isPooled;
}
/**
* sets if pooling is used
*
* @param isPooled true, if pooling is used
*/
public void setPooled(boolean isPooled) {
this.isPooled = isPooled;
}
/**
* {@inheritDoc}
*/
public void setResponseTimeoutMillis(int receiveTimeoutMillis) {
this.receiveTimeoutMillis = receiveTimeoutMillis;
}
/**
* {@inheritDoc}
*/
public int getResponseTimeoutMillis() {
return receiveTimeoutMillis;
}
/**
* {@inheritDoc}
*/
public void close() throws IOException {
pool.close();
}
/**
* {@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 setPooledIdleTimeoutSec(int idleTimeoutSec) {
pool.setPooledIdleTimeoutSec(idleTimeoutSec);
}
/**
* {@inheritDoc}
*/
public int getPooledIdleTimeoutSec() {
return pool.getPooledIdleTimeoutSec();
}
/**
* {@inheritDoc}
*/
public void setPooledLifeTimeoutSec(int lifeTimeoutSec) {
pool.setPooledLifeTimeoutSec(lifeTimeoutSec);
}
/**
* {@inheritDoc}
*/
public int getPooledLifeTimeoutSec() {
return pool.getPooledLifeTimeoutSec();
}
/**
* {@inheritDoc}
*/
public long getCreationMaxWaitMillis() {
return pool.getCreationMaxWaitMillis();
}
/**
* {@inheritDoc}
*/
public void setCreationMaxWaitMillis(long maxWaitMillis) {
pool.setCreationMaxWaitMillis(maxWaitMillis);
}
/**
* {@inheritDoc}
*/
public void setMaxIdlePooled(int maxIdle) {
pool.setMaxIdlePooled(maxIdle);
}
/**
* {@inheritDoc}
*/
public int getMaxIdlePooled() {
return pool.getMaxIdlePooled();
}
/**
* {@inheritDoc}
*/
public void setMaxActivePooled(int maxActive) {
pool.setMaxActivePooled(maxActive);
}
/**
* {@inheritDoc}
*/
public int getMaxActivePooled() {
return pool.getMaxActivePooled();
}
/**
* {@inheritDoc}
*/
public int getNumPooledActive() {
return pool.getNumPooledActive();
}
/**
* {@inheritDoc}
*/
public int getNumPooledIdle() {
return pool.getNumPooledIdle();
}
/**
* {@inheritDoc}
*/
int getNumPendingGet() {
return pool.getNumPendingGet();
}
/**
* {@inheritDoc}
*/
public int getNumCreated() {
return pool.getNumCreated();
}
/**
* {@inheritDoc}
*/
public int getNumDestroyed() {
return pool.getNumDestroyed();
}
/**
* {@inheritDoc}
*/
public int getNumTimeoutPooledIdle() {
return pool.getNumTimeoutPooledIdle();
}
/**
* {@inheritDoc}
*/
public int getNumTimeoutPooledLifetime() {
return pool.getNumTimeoutPooledLifetime();
}
/**
* {@inheritDoc}
*/
public List getActiveConnectionInfos() {
return pool.getActiveConnectionInfos();
}
/**
* {@inheritDoc}
*/
public List getIdleConnectionInfos() {
return pool.getIdleConnectionInfos();
}
/**
* returns the transaction log
* @return the transaction log
*/
List getTransactionInfos() {
return transactionLog.getTransactions();
}
/**
* {@inheritDoc}
*/
public Response call(Request request) throws IOException, SocketTimeoutException {
// get connection
HttpClientConnection con = getConnection(request.isSecure(), request.getRemoteHost(), request.getRemotePort());
// create a response handler
BlockingAutoCloseResponseHandler responseHandler = new BlockingAutoCloseResponseHandler(con, getResponseTimeoutMillis());
// send the request and wait for the response
con.send(request, responseHandler);
Response response = responseHandler.getResponse();
addTransactionInfo(request, response);
return response;
}
/**
* performs a request, by handling redirects
*
* @param request the request
* @return the response
* @throws IOException if an exception occurs
* @throws SocketTimeoutException if the received timeout is exceed
*/
public Response callFollowRedirects(Request request) throws IOException, SocketTimeoutException {
return callFollowRedirects(request, 0);
}
private Response callFollowRedirects(Request request, int redirectCounter) throws IOException, SocketTimeoutException {
if (redirectCounter > maxRedirects) {
throw new IOException("max redirects " + maxRedirects + " reached");
}
Response response = null;
// duplicate body is redirect is activated
NonBlockingBodyDataSource copiedBody = null;
if (request.hasBody()) {
copiedBody = request.getNonBlockingBody().duplicate();
}
// get connection and perform call
HttpClientConnection con = getConnection(request.isSecure(), request.getRemoteHost(), request.getRemotePort());
response = con.call(request);
addTransactionInfo(request, response);
if (isRedirectResponse(request.getRequestHeader(), response.getResponseHeader())) {
URL newLocation = getRedirectURI(response, request.isSecure(), request.getRemoteHost(), request.getRemotePort());
RequestHeader newRequestHeader = new RequestHeader(request.getMethod(), newLocation.toExternalForm());
newRequestHeader.copyHeaderFrom(request.getRequestHeader(), "HOST", "CONTENT-LENGTH");
Request newRequest = null;
if ((response.getStatus() == 303) || ((response.getStatus() == 302) && isTreat302RedirectAs303)) {
newRequest = new Request(newRequestHeader);
newRequest.setMethod("GET");
} else {
newRequest = new Request(newRequestHeader, copiedBody);
}
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Sending redirect ");
}
response = callFollowRedirects(newRequest, ++redirectCounter);
}
return response;
}
/**
* {@inheritDoc}
*/
public void send(Request request, IResponseHandler responseHandler) throws IOException {
// get connection
HttpClientConnection con = getConnection(request.isSecure(), request.getRemoteHost(), request.getRemotePort());
// perform call
AutoCloseResponseHandlerAdapter responseHandlerAdapter = new AutoCloseResponseHandlerAdapter(responseHandler, con, getResponseTimeoutMillis());
con.send(request, responseHandlerAdapter);
}
/**
* {@inheritDoc}
*/
public BodyDataSink send(RequestHeader requestHeader, int contentLength, IResponseHandler responseHandler) throws IOException {
// get connection
HttpClientConnection con = getConnection(requestHeader.isSecure(), requestHeader.getRemoteHost(), requestHeader.getRemotePort());
// perform call
AutoCloseResponseHandlerAdapter responseHandlerAdapter = new AutoCloseResponseHandlerAdapter(responseHandler, con, getResponseTimeoutMillis());
return con.sendPlain(requestHeader, contentLength, responseHandlerAdapter);
}
/**
* {@inheritDoc}
*/
public BodyDataSink send(RequestHeader requestHeader, IResponseHandler responseHandler) throws IOException {
// get connection
HttpClientConnection con = getConnection(requestHeader.isSecure(), requestHeader.getRemoteHost(), requestHeader.getRemotePort());
// perform call
return con.sendChunked(requestHeader, new AutoCloseResponseHandlerAdapter(responseHandler, con, getResponseTimeoutMillis()));
}
/**
* sends a request, by following redirects
*
* @param request the request
* @param responseHandler the response handler
* @throws IOException if an exception occurs
*/
public void sendFollowRedirects(Request request, IResponseHandler responseHandler) throws IOException {
sendFollowRedirects(request, responseHandler, 0);
}
private void sendFollowRedirects(Request request, IResponseHandler responseHandler, int redirectCounter) throws IOException {
// duplicate body and create RedirectAdapter is redirect is activated
NonBlockingBodyDataSource copiedBody = null;
if (request.hasBody()) {
copiedBody = request.getNonBlockingBody().duplicate();
}
// get connection
HttpClientConnection con = getConnection(request.isSecure(), request.getRemoteHost(), request.getRemotePort());
// perform call
AutoCloseRedirectResponseHandlerAdapter responseHandlerAdapter = new AutoCloseRedirectResponseHandlerAdapter(responseHandler, con, request.getRequestHeader(), copiedBody, redirectCounter, getResponseTimeoutMillis());
con.send(request, responseHandlerAdapter);
}
private void addTransactionInfo(Request request, Response response) {
if (isTransactionLog) {
String info = null;
if (request.getQueryString() != null) {
info = "[" + DATE_FORMAT.format(new Date()) + "] " + request.getRemoteHost() + ":" + request.getRemotePort() +
" " + request.getMethod() + " " + request.getRequestURI() + request.getQueryString() +
" -> " + response.getStatus() + " " + response.getReason();
} else {
info = "[" + DATE_FORMAT.format(new Date()) + "] " + request.getRemoteHost() + ":" + request.getRemotePort() +
" " + request.getMethod() + " " + request.getRequestURI() +
" -> " + response.getStatus() + " " + response.getReason();
}
if (response.containsHeader("connection")) {
info = info + " (connection: " + response.getHeader("connection") + ")";
}
transactionLog.add(info);
}
}
private HttpClientConnection getConnection(boolean isSSL, String host, int port) throws IOException {
if ((isSSL == true) && !isSSLSupported) {
throw new IOException("ssl connection are not supported (use pool sslContext parameter constructor)");
}
INonBlockingConnection tcpConnection = null;
if (isPooled) {
try {
tcpConnection = pool.getNonBlockingConnection(host, port, isSSL);
} catch (IOException ioe) {
throw new IOException("could not retrieve a pooled tcp connection to " + host + ":" + port + " reason: " + ioe.toString());
}
} else {
try {
if (sslCtx != null) {
tcpConnection = new NonBlockingConnection(host, port, sslCtx, true);
((NonBlockingConnection) tcpConnection).setWorkerpool(pool.getWorkerpool());
} else {
tcpConnection = new NonBlockingConnection(host, port);
((NonBlockingConnection) tcpConnection).setWorkerpool(pool.getWorkerpool());
}
} catch (IOException ioe) {
throw new IOException("could not establish new tcp connection to " + host + ":" + port + " reason: " + ioe.toString());
}
}
HttpClientConnection httpConnection = new HttpClientConnection(tcpConnection);
httpConnection.setResponseTimeoutMillis(receiveTimeoutMillis);
return httpConnection;
}
private boolean isRedirectResponse(RequestHeader requestHeader, ResponseHeader responseHeader) {
switch (responseHeader.getStatus()) {
// 300 Multiple choices
case 300:
return false;
// 301 Moved permanently
case 301:
if (requestHeader.getMethod().equalsIgnoreCase("GET") || requestHeader.getMethod().equalsIgnoreCase("HEAD")) {
return true;
}
return false;
// 302 found
case 302:
if (isTreat302RedirectAs303) {
return true;
}
if (requestHeader.getMethod().equalsIgnoreCase("GET") || requestHeader.getMethod().equalsIgnoreCase("HEAD")) {
return true;
}
return false;
// 303 See other
case 303:
return true;
// 304 Not modified
case 304:
return false;
// 305 Use proxy
case 305:
return false;
// 306 (unused)
case 306:
return false;
// 307 temporary redirect
case 307:
return false;
default:
return false;
}
}
private static final URL getRedirectURI(Response response, boolean isSSL, String originalHost, int originalPort) {
if (response.getStatus() == 302) {
String location = response.getHeader("Location");
// absolute URL?
try {
return new URL(location);
} catch (MalformedURLException mue) {
// no
try {
if (isSSL) {
return new URL("https://" + originalHost + ":" + originalPort + location);
} else {
return new URL("http://" + originalHost + ":" + originalPort + location);
}
} catch (MalformedURLException e) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("could not create relocation url . reason " + e.toString());
}
}
}
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder(super.toString());
sb.append("\r\nactive connections:");
for (String connectionInfo : getActiveConnectionInfos()) {
sb.append("\r\n " + connectionInfo);
}
sb.append("\r\nidle 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();
}
static class BlockingAutoCloseResponseHandler extends BlockingResponseHandler {
public BlockingAutoCloseResponseHandler(HttpClientConnection httpConnection, long maxReceiveTime) {
super(ClientUtils.RESPONSE_HANDLER_INFO_NONTHREADED_MESSAGE_RECEIVED, httpConnection, maxReceiveTime);
}
@Override
public void onMessageCompleteReceived() {
closeConnection();
}
}
static class AutoCloseResponseHandlerAdapter extends ResponseHandlerAdapter {
public AutoCloseResponseHandlerAdapter(IResponseHandler responseHandler, HttpClientConnection httpConnection, long maxReceiveTime) throws IOException {
super(responseHandler, httpConnection, maxReceiveTime);
}
@Override
public void onMessageCompleteReceived() {
closeConnection();
}
}
class AutoCloseRedirectResponseHandlerAdapter extends ResponseHandlerAdapter {
private IResponseHandler responseHandler = null;
// redirect support
private int currentRedirects = 0;
private RequestHeader originalRequestHeader = null;
private NonBlockingBodyDataSource originalRequestBody = null;
public AutoCloseRedirectResponseHandlerAdapter(IResponseHandler responseHandler, HttpClientConnection httpConnection, RequestHeader originalRequestHeader, NonBlockingBodyDataSource originalRequestBody, int currentRedirects, long maxReceiveTime) throws IOException {
super(responseHandler, httpConnection, maxReceiveTime);
this.responseHandler = responseHandler;
this.originalRequestHeader = originalRequestHeader;
this.originalRequestBody = originalRequestBody;
this.currentRedirects = currentRedirects;
}
@Override
public void performOnResponse(Response response) {
try {
// redirect handling
if (isRedirectResponse(originalRequestHeader, response.getResponseHeader())) {
URL newLocation = getRedirectURI(response, originalRequestHeader.isSecure(), originalRequestHeader.getRemoteHost(), originalRequestHeader.getRemotePort());
RequestHeader newRequestHeader = new RequestHeader(originalRequestHeader.getMethod(), newLocation.toExternalForm());
newRequestHeader.copyHeaderFrom(originalRequestHeader, "HOST", "CONTENT-LENGTH");
Request newRequest = null;
if ((response.getStatus() == 303) || ((response.getStatus() == 302) && isTreat302RedirectAs303)) {
newRequest = new Request(newRequestHeader);
newRequest.setMethod("GET");
} else {
newRequest = new Request(newRequestHeader, originalRequestBody);
}
sendFollowRedirects(newRequest, responseHandler, ++currentRedirects);
} else {
super.performOnResponse(response);
}
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by calling on response " + ioe.toString());
}
}
}
@Override
public void onMessageCompleteReceived() {
closeConnection();
}
}
@SuppressWarnings("unchecked")
private static final class TransactionLog {
private LinkedList transactions = new LinkedList();
private int maxSize = 0;
TransactionLog(int maxSize) {
this.maxSize = maxSize;
}
void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}
int getMaxSize() {
return maxSize;
}
void add(String transactionInfo) {
transactions.add(transactionInfo);
if (transactions.size() > maxSize) {
try {
transactions.removeFirst();
} catch (Exception e) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by removing list entry " + e.toString());
}
}
}
}
public List getTransactions() {
return (List) transactions.clone();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy