| HTTPServiceCaller.java |
/*
* $Id: HTTPServiceCaller.java,v 1.141 2013/01/23 11:36:37 agoubard Exp $
*
* See the COPYRIGHT file for redistribution and use restrictions.
*/
package org.xins.common.http;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.NoRouteToHostException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpTrace;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.log4j.NDC;
import org.xins.common.FormattedParameters;
import org.xins.common.Log;
import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.TimeOutException;
import org.xins.common.Utils;
import org.xins.common.service.CallConfig;
import org.xins.common.service.CallException;
import org.xins.common.service.CallExceptionList;
import org.xins.common.service.CallRequest;
import org.xins.common.service.CallResult;
import org.xins.common.service.ConnectionRefusedCallException;
import org.xins.common.service.ConnectionTimeOutCallException;
import org.xins.common.service.Descriptor;
import org.xins.common.service.GenericCallException;
import org.xins.common.service.IOCallException;
import org.xins.common.service.NoRouteToHostCallException;
import org.xins.common.service.ServiceCaller;
import org.xins.common.service.SocketTimeOutCallException;
import org.xins.common.service.TargetDescriptor;
import org.xins.common.service.TotalTimeOutCallException;
import org.xins.common.service.UnexpectedExceptionCallException;
import org.xins.common.service.UnknownHostCallException;
import org.xins.common.service.UnsupportedProtocolException;
import org.xins.common.text.TextUtils;
import org.xins.common.text.URLEncoding;
/**
* HTTP service caller. This class can be used to perform a call to an HTTP
* server and fail-over to other HTTP servers if the first one fails.
*
*
* <h2>Supported protocols</h2>
*
* <p>This service caller supports both HTTP and HTTPS (which is HTTP
* tunneled over a secure SSL connection). If a {@link TargetDescriptor} is
* passed to the constructor with a protocol other than <code>"http"</code>
* or <code>"https"</code>, then an {@link UnsupportedProtocolException} is
* thrown.
*
* <h2>Load-balancing and fail-over</h2>
*
* <p>To perform an HTTP call using a
* <code>HTTPServiceCaller</code> use {@link #call(HTTPCallRequest)}.
*
* <p>How load-balancing is done depends on the {@link Descriptor} passed to
* the {@link #HTTPServiceCaller(Descriptor)} constructor:
*
* <ul>
* <li>if it is a {@link TargetDescriptor}, then only this single target
* service is called and no load-balancing is performed;
* <li>if it is a {@link org.xins.common.service.GroupDescriptor}, then the
* configuration of the <code>GroupDescriptor</code> determines how the
* load-balancing is done.
* </ul>
*
* <p>A <code>GroupDescriptor</code> is a recursive data structure, which allows
* for fairly advanced load-balancing algorithms.
*
* <p>If a call attempt fails and there are more available target services,
* then the <code>HTTPServiceCaller</code> may or may not fail-over to a next
* target. If the request was not accepted by the target service, then
* fail-over is considered acceptable and will be performed. This includes
* the following situations:
*
* <ul>
* <li>if the <em>failOverAllowed</em> property is set to <code>true</code>
* for the active {@link HTTPCallConfig};
* <li>if the connection cannot be established (e.g. due to connection
* refusal, a DNS error, connection time-out, etc.)
* <li>if an HTTP status code other than 200-299 is returned;
* </ul>
*
* <p>If none of these conditions holds, then fail-over is not considered
* acceptable and will not be performed.
*
* <h2>Thread-safety</h2>
*
* <p>Instances of this class can safely be used from multiple threads at the
* same time.
*
*
* <h2>Example code</h2>
*
* <p>The following example code snippet constructs an
* <code>HTTPServiceCaller</code> instance:
*
* <blockquote><pre>// Initialize properties for the services. Normally these
// properties would come from a configuration source, like a file.
{@link java.util.Map} properties = new {@link java.util.HashMap#HashMap() HashMap}();
properties.{@link java.util.Map#put(Object,Object) put}("myapi", "group, random, server1, server2");
properties.{@link java.util.Map#put(Object,Object) put}("myapi.server1", "service, http://server1/myapi, 10000");
properties.{@link java.util.Map#put(Object,Object) put}("myapi.server2", "service, http://server2/myapi, 12000");
// Construct a descriptor and an HTTPServiceCaller instance
{@link Descriptor Descriptor} descriptor = {@link org.xins.common.service.DescriptorBuilder DescriptorBuilder}.{@link org.xins.common.service.DescriptorBuilder#build(Map,String) build}(properties, "myapi");
HTTPServiceCaller caller = new {@link #HTTPServiceCaller(Descriptor) HTTPServiceCaller}(descriptor);</pre></blockquote>
*
* <p>Then the following code snippet uses this <code>HTTPServiceCaller</code>
* to perform an HTTP GET call:
*
* <blockquote><pre>{@link java.util.Map} params = new {@link java.util.HashMap HashMap}();
params.{@link java.util.Map#put(Object,Object) put}("street", "Broadband Avenue");
params.{@link java.util.Map#put(Object,Object) put}("houseNumber", "12");
{@link HTTPCallRequest} request = new {@link HTTPCallRequest#HTTPCallRequest(HTTPMethod,Map) HTTPCallRequest}({@link HTTPMethod}.{@link HTTPMethod#GET GET}, params);
{@link HTTPCallResult} result = caller.{@link #call(HTTPCallRequest) call}(request);</pre></blockquote>
*
* @version $Revision: 1.141 $ $Date: 2013/01/23 11:36:37 $
* @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
* @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
*
* @since XINS 1.0.0
*/
public class HTTPServiceCaller extends ServiceCaller {
/**
* Charset for the error messages.
*/
private static final Charset UTF_CHARSET = Charset.forName("UTF-8");
/**
* Fully-qualified name of this class.
*/
private static final String CLASSNAME = HTTPServiceCaller.class.getName();
/**
* Fully-qualified name of the inner class <code>CallExecutor</code>.
*/
private static final String EXECUTOR_CLASSNAME = HTTPServiceCaller.CallExecutor.class.getName();
/**
* The number of constructed call executors.
*/
private static AtomicInteger CALL_EXECUTOR_COUNT = new AtomicInteger();
/**
* HTTP retry handler that does not allow any retries.
*/
private static HttpRequestRetryHandler NO_RETRIES = new DefaultHttpRequestRetryHandler(0, false);
/**
* Constructs a new <code>HTTPServiceCaller</code> object with the
* specified descriptor and call configuration.
*
* @param descriptor
* the descriptor of the service, cannot be <code>null</code>.
*
* @param callConfig
* the call configuration, or <code>null</code> if a default one should
* be used.
*
* @throws IllegalArgumentException
* if <code>descriptor == null</code>.
*
* @throws UnsupportedProtocolException
* if <code>descriptor</code> is or contains a {@link TargetDescriptor}
* with an unsupported protocol.
*
* @since XINS 1.1.0
*/
public HTTPServiceCaller(Descriptor descriptor,
HTTPCallConfig callConfig)
throws IllegalArgumentException, UnsupportedProtocolException {
// Call superclass constructor
super(descriptor, callConfig);
}
/**
* Constructs a new <code>HTTPServiceCaller</code> object.
*
* @param descriptor
* the descriptor of the service, cannot be <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>descriptor == null</code>.
*
* @throws UnsupportedProtocolException
* if <code>descriptor</code> is or contains a {@link TargetDescriptor}
* with an unsupported protocol (<em>since XINS 1.1.0</em>).
*/
public HTTPServiceCaller(Descriptor descriptor)
throws IllegalArgumentException, UnsupportedProtocolException {
this(descriptor, null);
}
/**
* Returns the {@link HttpClient} to use to contact the given target.
*
* @param config
* the HTTP configuration of the service.
*
* @param target
* the target of the service.
*
* @return
* the HttpClient shared instance.
*/
private static HttpClient getHttpClient(HTTPCallConfig config, TargetDescriptor target) {
HttpClient httpClient = config.getHttpClient();
HttpParams httpParams = httpClient.getParams();
int connectionTimeOut = target.getConnectionTimeOut();
int socketTimeOut = target.getSocketTimeOut();
// Configure connection time-out and socket time-out
httpParams.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, connectionTimeOut);
httpParams.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, socketTimeOut);
// Redirection handling
httpParams.setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, config.getFollowRedirect());
// Retry stategy if not already set
if (httpClient instanceof AbstractHttpClient
&& ((AbstractHttpClient) httpClient).getHttpRequestRetryHandler() == null) {
((AbstractHttpClient) httpClient).setHttpRequestRetryHandler(NO_RETRIES);
}
return httpClient;
}
/**
* Creates an appropriate <code>HttpRequestBase</code> object for the
* specified URL.
*
* @param url
* the URL for which to create an {@link HttpRequestBase} object, should
* not be <code>null</code>.
*
* @param request
* the HTTP call request, not <code>null</code>.
*
* @param callConfig
* the HTTP call configuration object, not <code>null</code>.
*
* @return
* the constructed {@link HttpRequestBase} object, not <code>null</code>.
*/
private static HttpRequestBase createMethod(String url,
HTTPCallRequest request,
HTTPCallConfig callConfig) {
// Get the HTTP method (like GET and POST) and parameters
HTTPMethod method = callConfig.getMethod();
Map<String, String> parameters = request.getParameters();
// HTTP POST, PUT requests
if (method == HTTPMethod.POST || method == HTTPMethod.PUT) {
List<BasicNameValuePair> httpParameters = new ArrayList<BasicNameValuePair>();
// Loop through the parameters
for (Map.Entry<String, String> param : parameters.entrySet()) {
httpParameters.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
HttpEntityEnclosingRequestBase httpMethod = null;
if (method == HTTPMethod.POST) {
httpMethod = new HttpPost(url);
} else {
httpMethod = new HttpPut(url);
}
try {
httpMethod.setEntity(new UrlEncodedFormEntity(httpParameters, "UTF-8"));
} catch (UnsupportedEncodingException ex) {
throw new IllegalStateException("UTF-8 not supported on this platform: " + ex.getMessage());
}
return httpMethod;
// HTTP GET, DELETE, HEAD, OPTIONS, TRACE requests
} else {
// Loop through the parameters
StringBuffer query = new StringBuffer(255);
for (Map.Entry<String, String> param : parameters.entrySet()) {
String key = param.getKey();
String value = param.getValue();
if (value == null) {
value = "";
}
// Add this parameter key/value combination.
if (key != null) {
if (query.length() > 0) {
query.append("&");
}
query.append(URLEncoding.encode(key));
query.append("=");
query.append(URLEncoding.encode(value));
}
}
if (query.length() > 0) {
url += "?" + query;
}
HttpRequestBase httpMethod = null;
if (method == HTTPMethod.GET) {
httpMethod = new HttpGet(url);
} else if (method == HTTPMethod.DELETE) {
httpMethod = new HttpDelete(url);
} else if (method == HTTPMethod.HEAD) {
httpMethod = new HttpHead(url);
} else if (method == HTTPMethod.OPTIONS) {
httpMethod = new HttpOptions(url);
} else if (method == HTTPMethod.TRACE) {
httpMethod = new HttpTrace(url);
}
return httpMethod;
}
}
/**
* Checks if the specified protocol is supported (implementation method).
* The protocol is the part in a URL before the string <code>"://"</code>).
*
* <p>This method should only ever be called from the
* {@link #isProtocolSupported(String)} method.
*
* <p>The implementation of this method in class
* <code>HTTPServiceCaller</code> throws an
* {@link UnsupportedOperationException} unless the protocol equals either
* <code>"http"</code> or <code>"https</code> (ignoring case)..
*
* @param protocol
* the protocol, guaranteed not to be <code>null</code>.
*
* @return
* <code>true</code> if the specified protocol is supported, or
* <code>false</code> if it is not.
*
* @since XINS 1.2.0
*/
protected boolean isProtocolSupportedImpl(String protocol) {
return "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol);
}
/**
* Returns a default <code>CallConfig</code> object. This method is called
* by the <code>ServiceCaller</code> constructor if no
* <code>CallConfig</code> object was given.
*
* <p>The implementation of this method in class {@link HTTPServiceCaller}
* returns a standard {@link HTTPCallConfig} object which has unconditional
* fail-over disabled and the HTTP method set to
* {@link HTTPMethod#POST POST}.
*
* @return
* a new {@link HTTPCallConfig} instance, never <code>null</code>.
*/
protected CallConfig getDefaultCallConfig() {
return new HTTPCallConfig();
}
/**
* Sets the <code>HTTPCallConfig</code> associated with this HTTP service
* caller.
*
* @param config
* the fall-back {@link HTTPCallConfig} object for this service caller,
* cannot be <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>config == null</code>.
*
* @since XINS 1.2.0
*/
protected void setHTTPCallConfig(HTTPCallConfig config)
throws IllegalArgumentException {
super.setCallConfig(config);
}
/**
* Returns the <code>HTTPCallConfig</code> associated with this service
* caller.
*
* <p>This method is the type-safe equivalent of {@link #getCallConfig()}.
*
* @return
* the fall-back {@link HTTPCallConfig} object for this HTTP service
* caller, never <code>null</code>.
*
* @since XINS 1.2.0
*/
public HTTPCallConfig getHTTPCallConfig() {
return (HTTPCallConfig) getCallConfig();
}
/**
* Executes a request towards the specified target. If the call succeeds,
* then a {@link HTTPCallResult} object is returned, otherwise a
* {@link CallException} is thrown.
*
* @param request
* the call request to be executed, must be an instance of class
* {@link HTTPCallRequest}, cannot be <code>null</code>.
*
* @param callConfig
* the call configuration, never <code>null</code> and should always be
* an instance of class {@link HTTPCallConfig}.
*
* @param target
* the target to call, cannot be <code>null</code>.
*
* @return
* the result, if and only if the call succeeded, always an instance of
* class {@link HTTPCallResult}, never <code>null</code>.
*
* @throws ClassCastException
* if the specified <code>request</code> object is not <code>null</code>
* and not an instance of class {@link HTTPCallRequest}.
*
* @throws IllegalArgumentException
* if <code>target == null || request == null</code>.
*
* @throws CallException
* if the call to the specified target failed.
*
* @since XINS 1.1.0
*/
public Object doCallImpl(CallRequest request,
CallConfig callConfig,
TargetDescriptor target)
throws ClassCastException, IllegalArgumentException, CallException {
// Delegate to method with more specialized interface
Object ret = call((HTTPCallRequest) request,
(HTTPCallConfig) callConfig,
target);
return ret;
}
/**
* Performs the specified request towards the HTTP service. If the call
* succeeds with one of the targets, then a {@link HTTPCallResult} object
* is returned, that combines the HTTP status code and the data returned.
* Otherwise, if none of the targets could successfully be called, a
* {@link CallException} is thrown.
*
* @param request
* the call request, not <code>null</code>.
*
* @param callConfig
* the call configuration to use, or <code>null</code>.
*
* @return
* the result of the call, cannot be <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>request == null</code>.
*
* @throws GenericCallException
* if the first call attempt failed due to a generic reason and all the
* other call attempts failed as well.
*
* @throws HTTPCallException
* if the first call attempt failed due to an HTTP-related reason and
* all the other call attempts failed as well.
*
* @since XINS 1.1.0
*/
public HTTPCallResult call(HTTPCallRequest request,
HTTPCallConfig callConfig)
throws IllegalArgumentException, GenericCallException, HTTPCallException {
// Check preconditions
MandatoryArgumentChecker.check("request", request);
// Perform the call
CallResult callResult;
try {
callResult = doCall(request, callConfig);
// Allow GenericCallException, HTTPCallException and Error to proceed,
// but block other kinds of exceptions and throw an Error instead.
} catch (GenericCallException exception) {
throw exception;
} catch (HTTPCallException exception) {
throw exception;
} catch (Exception exception) {
throw Utils.logProgrammingError(exception);
}
return (HTTPCallResult) callResult;
}
/**
* Performs the specified request towards the HTTP service. If the call
* succeeds with one of the targets, then a {@link HTTPCallResult} object
* is returned, that combines the HTTP status code and the data returned.
* Otherwise, if none of the targets could successfully be called, a
* {@link CallException} is thrown.
*
* @param request
* the call request, not <code>null</code>.
*
* @return
* the result of the call, cannot be <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>request == null</code>.
*
* @throws GenericCallException
* if the first call attempt failed due to a generic reason and all the
* other call attempts failed as well.
*
* @throws HTTPCallException
* if the first call attempt failed due to an HTTP-related reason and
* all the other call attempts failed as well.
*/
public HTTPCallResult call(HTTPCallRequest request)
throws IllegalArgumentException,
GenericCallException,
HTTPCallException {
return call(request, (HTTPCallConfig) null);
}
/**
* Executes the specified HTTP call request on the specified target with
* the specified call configuration. If the call fails in any way, then a
* {@link CallException} is thrown.
*
* @param request
* the call request to execute, cannot be <code>null</code>.
*
* @param callConfig
* the (optional) call configuration, or <code>null</code> if it should
* be determined at a lower level.
*
* @param target
* the service target on which to execute the request, cannot be
* <code>null</code>.
*
* @return
* the call result, never <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>target == null || request == null</code>.
*
* @throws GenericCallException
* if the first call attempt failed due to a generic reason and all the
* other call attempts failed as well.
*
* @throws HTTPCallException
* if the first call attempt failed due to an HTTP-related reason and
* all the other call attempts failed as well.
*
* @since XINS 1.1.0
*/
private HTTPCallResult call(HTTPCallRequest request,
HTTPCallConfig callConfig,
TargetDescriptor target)
throws IllegalArgumentException,
GenericCallException,
HTTPCallException {
// Get the parameters for logging
Map<String, String> p = request.getParameters();
FormattedParameters params = new FormattedParameters(p, null, "", "?", 160);
// Prepare a thread for execution of the call
// NOTE: Preconditions are checked by the CallExecutor constructor
CallExecutor executor = new CallExecutor(request, callConfig, target, NDC.peek());
// Get URL and time-out values
String url = target.getURL();
int totalTimeOut = target.getTotalTimeOut();
int connectionTimeOut = target.getConnectionTimeOut();
int socketTimeOut = target.getSocketTimeOut();
// About to make an HTTP call
Log.log_1100(url, params);
// Perform the HTTP call
long start = System.currentTimeMillis();
long duration;
try {
controlTimeOut(executor, target);
// 2.1 code: TimeoutController.execute(executor, totalTimeOut);
// Total time-out exceeded
} catch (TimeOutException exception) {
duration = System.currentTimeMillis() - start;
Log.log_1106(url, params, duration, totalTimeOut);
executor.dispose();
throw new TotalTimeOutCallException(request, target, duration);
}
// Determine the call duration
duration = System.currentTimeMillis() - start;
// Log that the HTTP call is done
Log.log_1101(url, params, duration);
// Check for exceptions
Throwable exception = executor.getException();
if (exception != null) {
String exceptionName = exception.getClass().getName();
// Unknown host
if (exception instanceof UnknownHostException) {
Log.log_1102(url, params, duration);
executor.dispose();
throw new UnknownHostCallException(request, target, duration);
// No route to host
} else if (exception instanceof NoRouteToHostException) {
Log.log_1110(url, params, duration);
executor.dispose();
throw new NoRouteToHostCallException(request, target, duration);
// Connection refusal
} else if (exception instanceof ConnectException) {
Log.log_1103(url, params, duration);
executor.dispose();
throw new ConnectionRefusedCallException(request, target, duration);
// Connection time-out
} else if (exception instanceof ConnectTimeoutException) {
Log.log_1104(url, params, duration, connectionTimeOut);
executor.dispose();
throw new ConnectionTimeOutCallException(request, target, duration);
// Socket time-out
} else if (exception instanceof SocketTimeoutException) {
Log.log_1105(url, params, duration, socketTimeOut);
executor.dispose();
throw new SocketTimeOutCallException(request, target, duration);
// Unspecific I/O error
} else if (exception instanceof IOException) {
Log.log_1109(exception, url, params, duration);
executor.dispose();
throw new IOCallException(request, target, duration,
(IOException) exception);
// Unrecognized kind of exception caught
} else {
String thisMethod = "call(HTTPCallREquest, HTTPCallConfig, TargetDescriptor)";
String subjectClass = executor.getThrowingClass();
String subjectMethod = executor.getThrowingMethod();
Log.log_1052(exception, CLASSNAME, thisMethod, subjectClass, subjectMethod, null);
executor.dispose();
throw new UnexpectedExceptionCallException(request, target, duration, null, exception);
}
}
// Retrieve the data returned from the HTTP call
HTTPCallResultData data = executor.getData();
// Determine the HTTP status code
int code = data.getStatusCode();
HTTPStatusCodeVerifier verifier = request.getStatusCodeVerifier();
// Status code is considered acceptable
if (verifier == null || verifier.isAcceptable(code)) {
Log.log_1107(url, params, duration, code);
// Status code is considered unacceptable
} else {
Log.log_1108(url, params, duration, code);
// TODO: Pass down body as well. Perhaps just pass down complete
// HTTPCallResult object and add getter for the body to the
// StatusCodeHTTPCallException class.
executor.dispose();
String details = data.getData() == null ? null : new String(data.getData(), UTF_CHARSET);
throw new StatusCodeHTTPCallException(request, target, duration, code, details);
}
executor.dispose();
return new HTTPCallResult(request, target, duration, null, data);
}
/**
* Constructs an appropriate <code>CallResult</code> object for a
* successful call attempt. This method is called from
* {@link #doCall(CallRequest,CallConfig)}.
*
* <p>The implementation of this method in class
* {@link HTTPServiceCaller} expects an {@link HTTPCallRequest} and
* returns an {@link HTTPCallResult}.
*
* @param request
* the {@link CallRequest} that was to be executed, never
* <code>null</code> when called from {@link #doCall(CallRequest,CallConfig)};
* should be an instance of class {@link HTTPCallRequest}.
*
* @param succeededTarget
* the {@link TargetDescriptor} for the service that was successfully
* called, never <code>null</code> when called from
* {@link #doCall(CallRequest,CallConfig)}.
*
* @param duration
* the call duration in milliseconds, must be a non-negative number.
*
* @param exceptions
* the list of {@link CallException} instances, or <code>null</code> if
* there were no call failures.
*
* @param result
* the result from the call, which is the object returned by
* {@link #doCallImpl(CallRequest,CallConfig,TargetDescriptor)},
* always an instance of class {@link HTTPCallResult}, never <code>null</code>; .
*
* @return
* an {@link HTTPCallResult} instance, never <code>null</code>.
*
* @throws ClassCastException
* if either <code>request</code> or <code>result</code> is not of the
* correct class.
*/
protected CallResult createCallResult(CallRequest request,
TargetDescriptor succeededTarget,
long duration,
List<CallException> exceptions,
Object result)
throws ClassCastException {
return new HTTPCallResult((HTTPCallRequest) request,
succeededTarget,
duration,
exceptions,
(HTTPCallResultData) result);
}
/**
* Determines whether a call should fail-over to the next selected target
* based on a request, call configuration and exception list.
* This method should only be called from
* {@link #doCall(CallRequest,CallConfig)}.
*
* <p>The implementation of this method in class {@link HTTPServiceCaller}
* returns <code>true</code> if and only if one of the following conditions
* holds true:
*
* <ul>
* <li><code>super.shouldFailOver(request, callConfig, exceptions)</code>
* <li><code>exceptions.{@link CallExceptionList#last() last()}
* instanceof {@link StatusCodeHTTPCallException}</code> and for
* that exception
* {@link StatusCodeHTTPCallException#getStatusCode() getStatusCode()} is not in the
* range 200-299.
* </ul>
*
* @param request
* the request for the call, as passed to {@link #doCall(CallRequest,CallConfig)},
* should not be <code>null</code>.
*
* @param callConfig
* the call config that is currently in use, never <code>null</code>.
*
* @param exceptions
* the current list of {@link CallException}s; never
* <code>null</code>; get the most recent one by calling
* <code>exceptions.</code>{@link CallExceptionList#last() last()}.
*
* @return
* <code>true</code> if the call should fail-over to the next target, or
* <code>false</code> if it should not.
*/
protected boolean shouldFailOver(CallRequest request,
CallConfig callConfig,
List<CallException> exceptions) {
CallException last = exceptions.get(exceptions.size() - 1);
// Let the superclass do it's job
if (super.shouldFailOver(request, callConfig, exceptions)) {
return true;
// A non-2xx HTTP status code indicates the request was not handled
} else if (last instanceof StatusCodeHTTPCallException) {
int code = ((StatusCodeHTTPCallException) last).getStatusCode();
return (code < 200 || code > 299) && code != 304;
// Otherwise do not fail over
} else {
return false;
}
}
/**
* Executor of calls to an API.
*
* @version $Revision: 1.141 $ $Date: 2013/01/23 11:36:37 $
* @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
*/
private static final class CallExecutor extends Thread {
/**
* Constructs a new <code>CallExecutor</code> for the specified call to
* an HTTP service.
*
* <p>A <em>Nested Diagnostic Context identifier</em> (NDC) may be
* specified, which will be set for the new thread when it is executed.
* If the NDC is <code>null</code>, then it will be left unchanged. See
* the {@link NDC} class.
*
* @param request
* the call request to execute, cannot be <code>null</code>.
*
* @param callConfig
* the call configuration, never <code>null</code>.
*
* @param target
* the service target on which to execute the request, cannot be
* <code>null</code>.
*
* @param context
* the <em>Nested Diagnostic Context identifier</em> (NDC), or
*/
private CallExecutor(HTTPCallRequest request,
HTTPCallConfig callConfig,
TargetDescriptor target,
String context) {
// Store data for later use in the run() method
_request = request;
_callConfig = callConfig;
_target = target;
_context = context;
// Determine the unique ID of this instance
int instanceID = CALL_EXECUTOR_COUNT.incrementAndGet();
// Set the name of this thread
String name = EXECUTOR_CLASSNAME + " #" + instanceID;
setName(name);
}
/**
* The call request to execute. Never <code>null</code>.
*/
private HTTPCallRequest _request;
/**
* The call configuration. Never <code>null</code>.
*/
private HTTPCallConfig _callConfig;
/**
* The service target on which to execute the request. Never
* <code>null</code>.
*/
private TargetDescriptor _target;
/**
* The <em>Nested Diagnostic Context identifier</em> (NDC). Is set to
* <code>null</code> if it should be left unchanged.
*/
private String _context;
/**
* The exception caught while executing the call. If there was no
* exception, then this field is <code>null</code>.
*/
private Throwable _exception;
/**
* The name of the class that threw the exception. This field is
* <code>null</code> if the call was performed successfully.
*/
private String _throwingClass;
/**
* The name of the method that threw the exception. This field is
* <code>null</code> if the call was performed successfully.
*/
private String _throwingMethod;
/**
* The result from the call. The value of this field is
* <code>null</code> if the call was unsuccessful or if it was not
* executed yet.
*/
private HTTPCallResultData _result;
/**
* Runs this thread (wrapper method). It will call the HTTP service. If that call was
* successful, then the result is stored in this object. Otherwise
* there is an exception, in which case that exception is stored in this
* object instead.
*/
public void run() {
// XXX: Note that performance could be improved by using local
// variables for _target and _request
// Activate the diagnostic context ID
if (_context != null) {
NDC.push(_context);
}
// Get the HttpClient object
HttpClient client = getHttpClient(_callConfig, _target);
// Determine URL and time-outs
String url = _target.getURL();
// Construct the method object
HttpRequestBase method = createMethod(url, _request, _callConfig);
// Set the user agent, if specified.
String userAgent = _callConfig.getUserAgent();
if (! TextUtils.isEmpty(userAgent)) {
client.getParams().setParameter(CoreProtocolPNames.USER_AGENT, userAgent);
}
// Perform the HTTP call
try {
// Execute call
_throwingClass = client.getClass().getName();
_throwingMethod = "execute(" + method.getClass().getName() + ')';
HttpResponse response = client.execute(method);
int statusCode = response.getStatusLine().getStatusCode();
// Get response body
_throwingClass = method.getClass().getName();
_throwingMethod = "getContent()";
HttpEntity responseEntity = response.getEntity();
byte[] body = null;
if (responseEntity != null) {
InputStream in = responseEntity.getContent();
if (in != null) {
_throwingMethod = "getResponseContentLength()";
int contentLength = (int) response.getEntity().getContentLength();
// Create byte array output stream
int size = contentLength > 0 ? contentLength : 4096;
ByteArrayOutputStream out = new ByteArrayOutputStream(size);
byte[] buffer = new byte[4096];
// Copy from the input stream to the byte array
String inClass = in.getClass().getName();
String outClass = "java.io.ByteArrayOutputStream";
_throwingClass = inClass;
_throwingMethod = "read(byte[])";
for (int len = in.read(buffer); len > 0; ) {
_throwingClass = outClass;
_throwingMethod = "write(byte[],int,int)";
out.write(buffer, 0, len);
_throwingClass = inClass;
_throwingMethod = "read(byte[])";
len = in.read(buffer);
}
_throwingClass = outClass;
_throwingMethod = "toByteArray()";
body = out.toByteArray();
}
}
// Store the result
_throwingClass = HTTPCallResultDataHandler.class.getName();
_throwingMethod = "<init>(int,byte[])";
_result = new HTTPCallResultDataHandler(statusCode, body);
// No exception thrown, reset _throwingXXXX fields
_throwingClass = null;
_throwingMethod = null;
// If an exception is thrown, store it for processing at later stage
} catch (Throwable exception) {
_exception = exception;
// Release the HTTP connection immediately
} finally {
method.abort();
}
// Remove the diagnostic context ID
if (_context != null) {
NDC.pop();
}
NDC.remove();
// Set objects to null for garbage collection
_callConfig = null;
_context = null;
_request = null;
_target = null;
}
/**
* Gets the exception if any generated when calling the method.
*
* @return
* the invocation exception or <code>null</code> if the call was
* performed successfully.
*/
private Throwable getException() {
return _exception;
}
/**
* Gets the name of the class that threw the exception.
*
* @return
* the name of the class that threw the exception or
* <code>null</code> if the call was performed successfully.
*/
private String getThrowingClass() {
return _throwingClass;
}
/**
* Gets the name of the method that threw the exception.
*
* @return
* the name of the method that threw the exception or
* <code>null</code> if the call was performed successfully.
*/
private String getThrowingMethod() {
return _throwingMethod;
}
/**
* Returns the result if the call was successful. If the call was
* unsuccessful, then <code>null</code> is returned.
*
* @return
* the result from the call, or <code>null</code> if it was
* unsuccessful.
*/
private HTTPCallResultData getData() {
return _result;
}
/**
* Disposes the result variables, so that the variables could be
* garbage collected.
*/
private void dispose() {
_exception = null;
_throwingClass = null;
_throwingMethod = null;
_result = null;
}
}
/**
* Container of the data part of an HTTP call result.
*
* @version $Revision: 1.141 $ $Date: 2013/01/23 11:36:37 $
* @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
*
* @since XINS 1.0.0
*/
private static final class HTTPCallResultDataHandler
implements HTTPCallResultData {
/**
* Constructs a new <code>HTTPCallResultDataHandler</code> object.
*
* @param code
* the HTTP status code.
*
* @param data
* the data returned from the call, as a set of bytes.
*/
HTTPCallResultDataHandler(int code, byte[] data) {
_code = code;
_data = data;
}
/**
* The HTTP status code.
*/
private final int _code;
/**
* The data returned.
*/
private final byte[] _data;
/**
* Returns the HTTP status code.
*
* @return
* the HTTP status code.
*/
public int getStatusCode() {
return _code;
}
/**
* Returns the result data as a byte array. Note that this is not a copy or
* clone of the internal data structure, but it is a link to the actual
* data structure itself.
*
* @return
* a byte array of the result data, never <code>null</code>.
*/
public byte[] getData() {
return _data;
}
}
/**
* Post method that encode the Unicode characters above 255 as %uxxxx
* where xxxx is the hexadecimal value of the character.
*
* @version $Revision: 1.141 $ $Date: 2013/01/23 11:36:37 $
* @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
*
* @since XINS 1.4.0
*/
/*private static class UnicodePostMethod extends PostMethod {
public UnicodePostMethod(String url) {
super(url);
// Disable retries
getParams().setParameter(HttpMethodParams.RETRY_HANDLER, NO_RETRIES);
}
protected RequestEntity generateRequestEntity() {
NameValuePair[] params = getParameters();
int paramsCount = params.length;
if (paramsCount == 0) {
return super.generateRequestEntity();
} else {
StringBuffer queryString = new StringBuffer();
for (int i = 0; i < paramsCount; i++) {
if (i > 0) {
queryString.append('&');
}
queryString.append(URLEncoding.encode(params[i].getName()));
queryString.append('=');
queryString.append(URLEncoding.encode(params[i].getValue()));
}
try {
return new StringRequestEntity(queryString.toString(),
"application/x-www-form-urlencoded", "UTF-8");
} catch (UnsupportedEncodingException ueex) {
// Should never happen
throw Utils.logProgrammingError(ueex);
}
}
}
}*/
}