/*
* 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.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xlightweb.BodyDataSink;
import org.xlightweb.IBodyDestroyListener;
import org.xlightweb.IHttpExchange;
import org.xlightweb.IHttpResponse;
import org.xlightweb.IHttpResponseHeader;
import org.xlightweb.IHttpResponseHandler;
import org.xlightweb.IHttpRequest;
import org.xlightweb.HttpRequest;
import org.xlightweb.IHttpRequestHeader;
import org.xlightweb.IHttpRequestHandler;
import org.xlightweb.ProtocolException;
import org.xlightweb.Supports100Continue;
import org.xlightweb.client.DuplicatingBodyForwarder.ISink;
import org.xlightweb.client.DuplicatingBodyForwarder.BodyDataSinkAdapter;
import org.xsocket.Execution;
import org.xsocket.connection.IConnection.FlushMode;
/*
* Retry handler
*
* @author grro@xlightweb.org
*/
@Supports100Continue
final class RetryHandler implements IHttpRequestHandler {
/**
* RetryHandler is unsynchronized by config. See HttpUtils$RequestHandlerInfo
*/
private static final Logger LOG = Logger.getLogger(RetryHandler.class.getName());
static final String RETRY_KEY = "org.xlightweb.client.RetryHandler.retry";
private static final String RETRY_COUNT_KEY = "org.xlightweb.client.RetryHandler.countTrials";
private final HttpClient httpClient;
// statistics
private int countRetries = 0;
public RetryHandler(HttpClient httpClient) {
this.httpClient = httpClient;
}
int getCountRetries() {
return countRetries;
}
static boolean isRetryable(IOException ioe) {
if ((ProtocolException.class.isAssignableFrom(ioe.getClass())) &&
(!isErrorStatusCodeRecevied((ProtocolException) ioe))) {
return true;
} else {
return false;
}
}
private static boolean isErrorStatusCodeRecevied(ProtocolException pe) {
IHttpResponseHeader header = (IHttpResponseHeader) HttpClientConnection.getReceviedHeader(pe);
if ((header != null) && (header.getStatus() >= 400)) {
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
public void onRequest(final IHttpExchange exchange) throws IOException {
IHttpRequest request = exchange.getRequest();
Boolean retry = (Boolean) request.getAttribute(RETRY_KEY);
if (retry == null) {
retry = true;
}
if (retry) {
// handle GET and Delete request
if (request.getMethod().equalsIgnoreCase("GET") || request.getMethod().equalsIgnoreCase("DELETE")) {
BodylessRetryResponseHandler retryHandler = new BodylessRetryResponseHandler(exchange, request.getRequestHeader().copy());
exchange.forward(request, retryHandler);
return;
// handle PUT
} else if (request.getMethod().equalsIgnoreCase("PUT")) {
BodyRetryResponseHandler retryHandler = new BodyRetryResponseHandler(exchange, request.getRequestHeader().copy());
final BodyDataSink dataSink = exchange.forward(request.getRequestHeader(), retryHandler);
dataSink.setFlushmode(FlushMode.ASYNC);
// BodyDataSink
DuplicatingBodyForwarder forwarder = new DuplicatingBodyForwarder(request.getNonBlockingBody(), new BodyDataSinkAdapter(dataSink), retryHandler);
request.getNonBlockingBody().setDataHandler(forwarder);
return;
}
}
exchange.forward(request);
}
/**
* BodyRetryResponseHandler is unsynchronized by config. See HttpUtils$RequestHandlerInfo
*/
private final class BodyRetryResponseHandler implements IHttpResponseHandler, ISink {
private final IHttpExchange exchange;
private final IHttpRequestHeader requestHeader;
private final InMemorySink inMemorySink = new InMemorySink();
private Integer countTrials;
BodyRetryResponseHandler(IHttpExchange exchange, IHttpRequestHeader requestHeader) {
this.exchange = exchange;
this.requestHeader = requestHeader;
countTrials = (Integer) requestHeader.getAttribute(RETRY_COUNT_KEY);
if (countTrials == null) {
countTrials = 0;
}
}
public void onData(ByteBuffer data) throws IOException {
inMemorySink.onData(data);
}
public void close() throws IOException {
inMemorySink.close();
}
public void destroy() {
inMemorySink.destroy();
}
public void setDestroyListener(IBodyDestroyListener destroyListener) {
inMemorySink.setDestroyListener(destroyListener);
}
public String getId() {
return inMemorySink.getId();
}
public void onResponse(IHttpResponse response) throws IOException {
exchange.send(response);
}
public void onException(IOException ioe) throws IOException {
if (isRetryable(ioe) && (countTrials < httpClient.getMaxRetries())) {
countRetries++;
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("try to retrying request (retry num " + countRetries + "). I/O exception caught when processing request " + ioe.toString());
}
sendRetry(ioe);
} else {
exchange.sendError(ioe);
}
}
private void sendRetry(final IOException ioe) throws IOException {
Runnable task = new Runnable() {
public void run() {
try {
requestHeader.setAttribute(RETRY_COUNT_KEY, ++countTrials);
requestHeader.setAttribute(CookieHandler.COOKIE_WARNING_KEY, false);
IHttpResponseHandler respHdl = new IHttpResponseHandler() {
@Execution(Execution.NONTHREADED)
public void onResponse(IHttpResponse response) throws IOException {
exchange.send(response);
}
@Execution(Execution.NONTHREADED)
public void onException(IOException ioe) throws IOException {
exchange.sendError(ioe);
}
};
BodyDataSink ds = httpClient.send(requestHeader, respHdl);
ds.setFlushmode(FlushMode.ASYNC);
boolean isForwarding = inMemorySink.forwardTo(new BodyDataSinkAdapter(ds));
if (!isForwarding) {
exchange.sendError(ioe);
}
} catch (IOException ioe) {
exchange.sendError(ioe);
}
}
};
httpClient.getWorkerpool().execute(task);
}
}
private static final class InMemorySink implements ISink {
/*
* TODO: improve synchronization (make it less expensive)
*/
private List<ByteBuffer> buffers = new ArrayList<ByteBuffer>();
private IBodyDestroyListener destroyListener = null;
private boolean isDestroyed = false;
private boolean isClosed = false;
private ISink forwardSink = null;
public synchronized void onData(ByteBuffer data) throws IOException {
if (forwardSink == null) {
buffers.add(data);
} else {
forwardSink.onData(data);
}
}
public synchronized void close() throws IOException {
if (forwardSink == null) {
isClosed = true;
} else {
forwardSink.close();
}
}
public synchronized void destroy() {
if (forwardSink == null) {
isDestroyed = true;
} else {
forwardSink.destroy();
}
}
public String getId() {
return "<unset>";
}
public synchronized void setDestroyListener(IBodyDestroyListener destroyListener) {
if (forwardSink == null) {
this.destroyListener = destroyListener;
} else {
forwardSink.setDestroyListener(destroyListener);
}
}
public synchronized boolean forwardTo(ISink sink) throws IOException {
if (isDestroyed) {
return false;
}
forwardSink = sink;
if (destroyListener != null) {
sink.setDestroyListener(destroyListener);
}
for (ByteBuffer buffer : buffers) {
onData(buffer);
}
if (isClosed) {
close();
}
return true;
}
}
/**
* BodyRetryResponseHandler is unsynchronized by config. See HttpUtils$RequestHandlerInfo
*/
private final class BodylessRetryResponseHandler implements IHttpResponseHandler {
private final AtomicBoolean isHandled = new AtomicBoolean(false);
private final IHttpExchange exchange;
private final IHttpRequestHeader requestHeader;
private Integer countTrials;
BodylessRetryResponseHandler(IHttpExchange exchange, IHttpRequestHeader requestHeader) {
this.exchange = exchange;
this.requestHeader = requestHeader;
countTrials = (Integer) requestHeader.getAttribute(RETRY_COUNT_KEY);
if (countTrials == null) {
countTrials = 0;
}
}
public void onResponse(IHttpResponse response) throws IOException {
if (!isHandled.getAndSet(true)) {
exchange.send(response);
}
}
public void onException(IOException ioe) throws IOException {
if (!isHandled.getAndSet(true)) {
if (isRetryable(ioe) && (countTrials < httpClient.getMaxRetries())) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("I/O exception caught when processing request " + ioe.toString() + " retrying request");
}
countRetries++;
sendRetry();
} else {
exchange.sendError(ioe);
}
}
}
private void sendRetry() throws IOException {
Runnable task = new Runnable() {
public void run() {
try {
requestHeader.setAttribute(RETRY_COUNT_KEY, ++countTrials);
requestHeader.setAttribute(CookieHandler.COOKIE_WARNING_KEY, false);
IHttpResponseHandler respHdl = new IHttpResponseHandler() {
@Execution(Execution.NONTHREADED)
public void onResponse(IHttpResponse response) throws IOException {
exchange.send(response);
}
@Execution(Execution.NONTHREADED)
public void onException(IOException ioe) throws IOException {
exchange.sendError(ioe);
}
};
httpClient.send(new HttpRequest(requestHeader), respHdl);
} catch (IOException ioe) {
exchange.sendError(ioe);
}
}
};
httpClient.getWorkerpool().execute(task);
}
}
}