package org.jboss.resteasy.client.core.executors;
import org.apache.commons.io.output.DeferredFileOutputStream;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.entity.FileEntity;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.jboss.resteasy.client.ClientExecutor;
import org.jboss.resteasy.client.ClientRequest;
import org.jboss.resteasy.client.ClientResponse;
import org.jboss.resteasy.client.core.BaseClientResponse;
import org.jboss.resteasy.client.core.BaseClientResponse.BaseClientResponseStreamFactory;
import org.jboss.resteasy.client.core.SelfExpandingBufferredInputStream;
import org.jboss.resteasy.client.exception.mapper.ApacheHttpClient4ExceptionMapper;
import org.jboss.resteasy.client.exception.mapper.ClientExceptionMapper;
import org.jboss.resteasy.resteasy_jaxrs.i18n.LogMessages;
import org.jboss.resteasy.resteasy_jaxrs.i18n.Messages;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.util.CaseInsensitiveMap;
import org.jboss.resteasy.util.Types;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriBuilder;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ApacheHttpClient4Executor implements ClientExecutor
{
public static final String BYTE_MEMORY_UNIT = "BY";
public static final String KILOBYTE_MEMORY_UNIT = "KB";
public static final String MEGABYTE_MEMORY_UNIT = "MB";
public static final String GIGABYTE_MEMORY_UNIT = "GB";
/**
* Used to build temp file prefix.
*/
private static String processId = null;
static
{
ApacheHttpClient4Executor.processId = ManagementFactory.getRuntimeMXBean().getName().replaceAll("[^0-9a-zA-Z]", "");
}
static synchronized private void checkClientExceptionMapper()
{
ResteasyProviderFactory factory = ResteasyProviderFactory.getInstance();
if (factory instanceof org.jboss.resteasy.spi.old.ResteasyProviderFactory)
{
org.jboss.resteasy.spi.old.ResteasyProviderFactory f = (org.jboss.resteasy.spi.old.ResteasyProviderFactory)factory;
if (f.getClientExceptionMapper(Exception.class) == null)
{
Type exceptionType = Types.getActualTypeArgumentsOfAnInterface(ApacheHttpClient4ExceptionMapper.class,
ClientExceptionMapper.class)[0];
f.addClientExceptionMapper(new ApacheHttpClient4ExceptionMapper(), exceptionType);
}
}
}
protected HttpClient httpClient;
protected boolean createdHttpClient;
protected HttpContext httpContext;
protected boolean closed;
/**
* For uploading File's over JAX-RS framework, this property, together with {@link #fileUploadMemoryUnit},
* defines the maximum File size allowed in memory. If fileSize exceeds this size, it will be stored to
* {@link #fileUploadTempFileDir}. <br>
* <br>
* Defaults to 1 MB
*/
private int fileUploadInMemoryThresholdLimit = 1;
/**
* The unit for {@link #fileUploadInMemoryThresholdLimit}. <br>
* <br>
* Defaults to MB.
*
* @see MemoryUnit
*/
private MemoryUnit fileUploadMemoryUnit = MemoryUnit.MB;
/**
* Temp directory to write output request stream to. Any file to be uploaded has to be written out to the
* output request stream to be sent to the service and when the File is too huge the output request stream is
* written out to the disk rather than to memory. <br>
* <br>
* Defaults to JVM temp directory.
*/
private File fileUploadTempFileDir = new File(System.getProperty("java.io.tmpdir"));
protected int responseBufferSize = 8192;
public ApacheHttpClient4Executor()
{
this(new DefaultHttpClient(), null);
this.createdHttpClient = true;
}
public ApacheHttpClient4Executor(HttpClient httpClient)
{
this(httpClient, null);
}
public ApacheHttpClient4Executor(HttpClient httpClient, HttpContext httpContext)
{
this.httpClient = httpClient;
this.httpContext = httpContext;
checkClientExceptionMapper();
}
/**
* Response stream is wrapped in a BufferedInputStream. Default is 8192. Value of 0 will not wrap it.
* Value of -1 will use a SelfExpandingBufferedInputStream
*
* @return
*/
public int getResponseBufferSize()
{
return responseBufferSize;
}
/**
* Response stream is wrapped in a BufferedInputStream. Default is 8192. Value of 0 will not wrap it.
* Value of -1 will use a SelfExpandingBufferedInputStream
*
* @param responseBufferSize
*/
public void setResponseBufferSize(int responseBufferSize)
{
this.responseBufferSize = responseBufferSize;
}
public HttpClient getHttpClient()
{
return httpClient;
}
public HttpContext getHttpContext()
{
return httpContext;
}
public void setHttpContext(HttpContext httpContext)
{
this.httpContext = httpContext;
}
public static CaseInsensitiveMap<String> extractHeaders(
HttpResponse response)
{
final CaseInsensitiveMap<String> headers = new CaseInsensitiveMap<String>();
for (Header header : response.getAllHeaders())
{
headers.add(header.getName(), header.getValue());
}
return headers;
}
public ClientRequest createRequest(String uriTemplate)
{
return new ClientRequest(uriTemplate, this);
}
public ClientRequest createRequest(UriBuilder uriBuilder)
{
return new ClientRequest(uriBuilder, this);
}
static class ResponseStream extends SelfExpandingBufferredInputStream
{
BaseClientResponse response;
public ResponseStream(InputStream in, BaseClientResponse response)
{
super(in);
// Keep a reference to the response object to prevent it being finalized prematurely
this.response = response;
}
public synchronized void close() throws IOException
{
super.close();
// Response object is no longer needed and can be finalized
response = null;
}
}
protected InputStream createBufferedStream(InputStream is)
{
if (responseBufferSize == 0)
{
return is;
}
if (responseBufferSize < 0)
{
return new SelfExpandingBufferredInputStream(is);
}
BufferedInputStream bis = new BufferedInputStream(is, responseBufferSize);
// mark read limit
bis.mark(responseBufferSize);
return bis;
}
@SuppressWarnings("unchecked")
public ClientResponse execute(ClientRequest request) throws Exception
{
String uri = request.getUri();
final HttpRequestBase httpMethod = createHttpMethod(uri, request.getHttpMethod());
try
{
loadHttpMethod(request, httpMethod);
final HttpResponse res = httpClient.execute(httpMethod, httpContext);
final BaseClientResponse response = new BaseClientResponse(null, this);
BaseClientResponseStreamFactory sf = new BaseClientResponseStreamFactory()
{
InputStream stream;
public InputStream getInputStream() throws IOException
{
if (stream == null)
{
HttpEntity entity = res.getEntity();
if (entity == null) return null;
// stream = new SelfExpandingBufferredInputStream(entity.getContent());
stream = createBufferedStream(entity.getContent());
}
return stream;
}
public void performReleaseConnection()
{
// Apache Client 4 is stupid, You have to get the InputStream and close it if there is an entity
// otherwise the connection is never released. There is, of course, no close() method on response
// to make this easier.
try
{
if (stream != null)
{
stream.close();
}
else
{
InputStream is = getInputStream();
if (is != null)
{
is.close();
}
}
}
catch (Exception ignore)
{
}
}
};
response.setStreamFactory(sf);
response.setAttributes(request.getAttributes());
response.setStatus(res.getStatusLine().getStatusCode());
response.setHeaders(extractHeaders(res));
response.setProviderFactory(request.getProviderFactory());
return response;
}
finally
{
cleanUpAfterExecute(httpMethod);
}
}
/**
* If passed httpMethod is of type HttpPost then obtain its entity. If the entity has an enclosing File then
* delete it by invoking this method after the request has completed. The entity will have an enclosing File
* only if it was too huge to fit into memory.
*
* @param httpMethod - the httpMethod to clean up.
* @see #writeRequestBodyToOutputStream(ClientRequest)
*/
protected void cleanUpAfterExecute(final HttpRequestBase httpMethod)
{
if (httpMethod != null && httpMethod instanceof HttpPost)
{
HttpPost postMethod = (HttpPost) httpMethod;
HttpEntity entity = postMethod.getEntity();
if (entity != null && entity instanceof FileExposingFileEntity)
{
File tempRequestFile = ((FileExposingFileEntity) entity).getFile();
try
{
boolean isDeleted = tempRequestFile.delete();
if (!isDeleted)
{
handleFileNotDeletedError(tempRequestFile, null);
}
}
catch (Exception ex)
{
handleFileNotDeletedError(tempRequestFile, ex);
}
}
}
}
private HttpRequestBase createHttpMethod(String url, String restVerb)
{
if ("GET".equals(restVerb))
{
return new HttpGet(url);
}
else if ("POST".equals(restVerb))
{
return new HttpPost(url);
}
else
{
final String verb = restVerb;
return new HttpPost(url)
{
@Override
public String getMethod()
{
return verb;
}
};
}
}
public void loadHttpMethod(final ClientRequest request, HttpRequestBase httpMethod) throws Exception
{
if (httpMethod instanceof HttpGet && request.followRedirects())
{
HttpClientParams.setRedirecting(httpMethod.getParams(), true);
}
else
{
HttpClientParams.setRedirecting(httpMethod.getParams(), false);
}
if (request.getBody() != null && !request.getFormParameters().isEmpty())
throw new RuntimeException(Messages.MESSAGES.cannotSendFormParametersAndEntity());
if (!request.getFormParameters().isEmpty())
{
commitHeaders(request, httpMethod);
HttpPost post = (HttpPost) httpMethod;
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
for (Map.Entry<String, List<String>> formParam : request.getFormParameters().entrySet())
{
List<String> values = formParam.getValue();
for (String value : values)
{
formparams.add(new BasicNameValuePair(formParam.getKey(), value));
}
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(entity);
}
else if (request.getBody() != null)
{
if (httpMethod instanceof HttpGet) throw new RuntimeException(Messages.MESSAGES.getRequestCannotHaveBody());
try
{
HttpEntity entity = buildEntity(request);
HttpPost post = (HttpPost) httpMethod;
commitHeaders(request, httpMethod);
post.setEntity(entity);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
else // no body
{
commitHeaders(request, httpMethod);
}
}
/**
* Build the HttpEntity to be sent to the Service as part of (POST) request. Creates a off-memory
* {@link FileExposingFileEntity} or a regular in-memory {@link ByteArrayEntity} depending on if the request
* OutputStream fit into memory when built by calling {@link #writeRequestBodyToOutputStream(ClientRequest)}.
*
* @param request -
* @return - the built HttpEntity
* @throws IOException -
*/
protected HttpEntity buildEntity(final ClientRequest request) throws IOException
{
HttpEntity entityToBuild = null;
DeferredFileOutputStream memoryManagedOutStream = writeRequestBodyToOutputStream(request);
if (memoryManagedOutStream.isInMemory())
{
ByteArrayEntity entityToBuildByteArray = new ByteArrayEntity(memoryManagedOutStream.getData());
entityToBuildByteArray.setContentType(new BasicHeader(HTTP.CONTENT_TYPE, request.getBodyContentType().toString()));
entityToBuild = entityToBuildByteArray;
}
else
{
File requestBodyFile = memoryManagedOutStream.getFile();
requestBodyFile.deleteOnExit();
entityToBuild = new FileExposingFileEntity(memoryManagedOutStream.getFile(), request.getBodyContentType().toString());
}
return entityToBuild;
}
/**
* Creates the request OutputStream, to be sent to the end Service invoked, as a
* <a href="http://commons.apache.org/io/api-release/org/apache/commons/io/output/DeferredFileOutputStream.html"
* >DeferredFileOutputStream</a>.
*
* @param request -
* @return - DeferredFileOutputStream with the ClientRequest written out per HTTP specification.
* @throws IOException -
*/
private DeferredFileOutputStream writeRequestBodyToOutputStream(final ClientRequest request) throws IOException
{
DeferredFileOutputStream memoryManagedOutStream =
new DeferredFileOutputStream(this.fileUploadInMemoryThresholdLimit * getMemoryUnitMultiplier(),
getTempfilePrefix(), ".tmp", this.fileUploadTempFileDir);
request.writeRequestBody(request.getHeadersAsObjects(), memoryManagedOutStream);
memoryManagedOutStream.close();
return memoryManagedOutStream;
}
/**
* @return - the constant to multiply {@link #fileUploadInMemoryThresholdLimit} with based on
* {@link #fileUploadMemoryUnit} enumeration value.
*/
private int getMemoryUnitMultiplier()
{
switch (this.fileUploadMemoryUnit)
{
case BY:
return 1;
case KB:
return 1024;
case MB:
return 1024 * 1024;
case GB:
return 1024 * 1024 * 1024;
}
return 1;
}
/**
* Use context information, which will include node name, to avoid conflicts in case of multiple VMS using same
* temp directory location.
*
* @return -
*/
protected String getTempfilePrefix()
{
return ApacheHttpClient4Executor.processId;
}
/**
* Log that the file did not get deleted but prevent the request from failing by eating the exception. The file
* has been registered to delete on exit, so it will get deleted eventually.
*
* @param tempRequestFile -
* @param ex - a null may be passed in which case this param gets ignored.
*/
private void handleFileNotDeletedError(File tempRequestFile, Exception ex)
{
LogMessages.LOGGER.couldNotDeleteFile(tempRequestFile.getAbsolutePath(), ex);
}
/**
* Setter for the {@link HttpClient} to which this class delegates the actual HTTP call. Note that this class
* acts as the adapter between RestEasy and Apache HTTP Component library.
*
* @param pHttpClient -
*/
void setHttpClient(HttpClient pHttpClient)
{
this.httpClient = pHttpClient;
}
/**
* Setter for {@link #fileUploadInMemoryThresholdLimit}
*
* @param pInMemoryThresholdLimit - the inMemoryThresholdLimitMB to set
*/
public void setFileUploadInMemoryThresholdLimit(int pInMemoryThresholdLimit)
{
this.fileUploadInMemoryThresholdLimit = pInMemoryThresholdLimit;
}
/**
* Setter for {@link #fileUploadTempFileDir}
*
* @param pTempFileDir the tempFileDir to set
*/
public void setFileUploadTempFileDir(File pTempFileDir)
{
this.fileUploadTempFileDir = pTempFileDir;
}
/**
* Setter for {@link #fileUploadMemoryUnit}
*
* @param pMemoryUnit the memoryUnit to set
*/
public void setFileUploadMemoryUnit(String pMemoryUnit)
{
this.fileUploadMemoryUnit = MemoryUnit.valueOf(pMemoryUnit);
}
public void commitHeaders(ClientRequest request, HttpRequestBase httpMethod)
{
MultivaluedMap<String, String> headers = request.getHeaders();
for (Map.Entry<String, List<String>> header : headers.entrySet())
{
List<String> values = header.getValue();
for (String value : values)
{
// System.out.println(String.format("setting %s = %s", header.getKey(), value));
httpMethod.addHeader(header.getKey(), value);
}
}
}
public void close()
{
if (closed)
return;
if (createdHttpClient && httpClient != null)
{
ClientConnectionManager manager = httpClient.getConnectionManager();
if (manager != null)
{
manager.shutdown();
}
}
closed = true;
}
public boolean isClosed()
{
return closed;
}
public void finalize() throws Throwable
{
close();
super.finalize();
}
/**
* We use {@link FileEntity} as the {@link HttpEntity} implementation when the request OutputStream has been
* saved to a File on disk (because it was too large to fit into memory see
* {@link RestCFHttpClientExecutor#writeRequestBodyToOutputStream(ClientRequest)}); however, we have to delete
* the File supporting the <code>FileEntity</code>, otherwise the disk will soon run out of space - remember
* that there can be very huge files, in GB range, processed on a regular basis - and FileEntity exposes its
* content File as a protected field. For the enclosing parent class ( {@link ApacheHttpClient4Executor} ) to be
* able to get a handle to this content File and delete it, this class expose the content File.<br>
* This class is private scoped to prevent access to this content File outside of the parent class.
*
* @author <a href="mailto:stikoo@digitalriver.com">Sandeep Tikoo</a>
*/
private static class FileExposingFileEntity extends FileEntity
{
/**
* @param pFile -
* @param pContentType -
*/
public FileExposingFileEntity(File pFile, String pContentType)
{
super(pFile, pContentType);
}
/**
* @return - the content File enclosed by this FileEntity.
*/
File getFile()
{
return this.file;
}
}
/**
* Enumeration to represent memory units.
*/
private static enum MemoryUnit
{
/**
* Bytes
*/
BY,
/**
* Killo Bytes
*/
KB,
/**
* Mega Bytes
*/
MB,
/**
* Giga Bytes
*/
GB
}
}