/*
* Copyright (C) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package interactivespaces.util.web;
import interactivespaces.InteractiveSpacesException;
import interactivespaces.SimpleInteractiveSpacesException;
import com.google.common.base.Charsets;
import com.google.common.io.Closeables;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.AbstractContentBody;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.InputStreamBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.util.EntityUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Map.Entry;
/**
* An {@link HttpContentCopier} which uses Apache HttpClient.
*
* @author Keith M. Hughes
*/
public class HttpClientHttpContentCopier implements HttpContentCopier {
/**
* The default number of total connections.
*/
public static final int TOTAL_CONNECTIONS_ALLOWED_DEFAULT = 20;
/**
* Number of bytes in the copy buffer.
*/
private static final int BUFFER_SIZE = 4096;
/**
* The HTTPClient instance which does the actual transfer.
*/
private HttpClient httpClient;
/**
* Connection manager for the client.
*/
private ThreadSafeClientConnManager httpConnectionManager;
/**
* The total number of connections allowed.
*/
private final int totalConnectionsAllowed;
/**
* Construct a copier which allows a maximum of {@link #TOTAL_CONNECTIONS_ALLOWED_DEFAULT} connections.
*/
public HttpClientHttpContentCopier() {
this(TOTAL_CONNECTIONS_ALLOWED_DEFAULT);
}
/**
* Construct a copier with a specified maximum number of connections.
*
* @param totalConnectionsAllowed
* the maximum total number of connections allowed
*/
public HttpClientHttpContentCopier(int totalConnectionsAllowed) {
this.totalConnectionsAllowed = totalConnectionsAllowed;
}
@Override
public void startup() {
httpConnectionManager = new ThreadSafeClientConnManager();
httpConnectionManager.setDefaultMaxPerRoute(totalConnectionsAllowed);
httpConnectionManager.setMaxTotal(totalConnectionsAllowed);
httpClient = new DefaultHttpClient(httpConnectionManager);
}
@Override
public void shutdown() {
if (httpConnectionManager != null) {
httpConnectionManager.shutdown();
httpConnectionManager = null;
httpClient = null;
}
}
@Override
public String getContentAsString(String sourceUri) throws InteractiveSpacesException {
return getContentAsString(sourceUri, Charsets.UTF_8);
}
@Override
public String getContentAsString(String sourceUri, Charset charset) throws InteractiveSpacesException {
StringHttpEntityCopier copier = new StringHttpEntityCopier(charset);
copier.retrieveRemoteContent(sourceUri);
return copier.getContentString();
}
@Override
public void copy(String sourceUri, File destinationFile) {
new FileHttpEntityCopier(destinationFile).retrieveRemoteContent(sourceUri);
}
@Override
public void copyTo(String destinationUri, File source, String sourceParameterName, Map<String, String> params) {
FileBody contentBody = new FileBody(source);
doCopyTo(destinationUri, sourceParameterName, params, contentBody);
}
@Override
public void copyTo(String destinationUri, InputStream source, String sourceFileName, String sourceParameterName,
Map<String, String> params) {
InputStreamBody contentBody = new InputStreamBody(source, sourceFileName);
doCopyTo(destinationUri, sourceParameterName, params, contentBody);
}
@Override
public int getTotalConnectionsAllowed() {
return totalConnectionsAllowed;
}
/**
* Perform the actual content copy.
*
* @param destinationUri
* URI for the destination
* @param sourceParameterName
* the parameter name in the HTTP form post for the content
* @param params
* the parameters to be included, can be {@code null}
* @param contentBody
* the content to be sent
*/
private void doCopyTo(String destinationUri, String sourceParameterName, Map<String, String> params,
AbstractContentBody contentBody) {
HttpEntity entity = null;
try {
HttpPost httpPost = new HttpPost(destinationUri);
MultipartEntity mpEntity = new MultipartEntity();
mpEntity.addPart(sourceParameterName, contentBody);
if (params != null) {
for (Entry<String, String> entry : params.entrySet()) {
mpEntity.addPart(entry.getKey(), new StringBody(entry.getValue()));
}
}
httpPost.setEntity(mpEntity);
HttpResponse response = httpClient.execute(httpPost);
entity = response.getEntity();
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
throw new SimpleInteractiveSpacesException(String.format(
"Server returned bad status code %d for source URI %s during HTTP copy", statusCode, destinationUri));
}
} catch (InteractiveSpacesException e) {
throw e;
} catch (Exception e) {
throw new InteractiveSpacesException(String.format("Could not send file to destination URI %s during HTTP copy",
destinationUri), e);
} finally {
if (entity != null) {
try {
EntityUtils.consume(entity);
} catch (IOException e) {
throw new InteractiveSpacesException(String.format(
"Could not consume entity content for %s during HTTP copy", destinationUri), e);
}
}
}
}
/**
* An entity copier that copies to a String.
*
* @author Keith M. Hughes
*/
private class StringHttpEntityCopier extends HttpResponseCopier {
/**
* Output stream for the copier.
*/
private ByteArrayOutputStream outputStream;
/**
* The charset for the bytes being read.
*/
private Charset charset;
/**
* Construct a string copier.
*
* @param charset
* the charset the string is expected in
*/
public StringHttpEntityCopier(Charset charset) {
this.charset = charset;
}
/**
* Get the string from the result in the required charset.
*
* @return the content of the result
*/
public String getContentString() {
return new String(outputStream.toByteArray(), charset);
}
@Override
protected OutputStream getOutputStream() throws IOException {
outputStream = new ByteArrayOutputStream();
return outputStream;
}
@Override
protected String getDestinationDescription() {
return "string result";
}
}
/**
* An entity copier that copies to a file.
*
* @author Keith M. Hughes
*/
private class FileHttpEntityCopier extends HttpResponseCopier {
/**
* The file to transfer the content to.
*/
private File destinationFile;
/**
* Construct a file copier.
*
* @param destinationFile
* the destination file
*/
public FileHttpEntityCopier(File destinationFile) {
this.destinationFile = destinationFile;
}
@Override
protected OutputStream getOutputStream() throws IOException {
return new FileOutputStream(destinationFile);
}
@Override
protected String getDestinationDescription() {
return destinationFile.getAbsolutePath();
}
}
/**
* The copier for HTTP responses. Subclasses decide the ultimate destination.
*
* @author Keith M. Hughes
*/
private abstract class HttpResponseCopier {
/**
* Create the output stream needed for the copier.
*
* @return the output steam to write to
*
* @throws IOException
* an exception happened when obtaining the stream
*/
protected abstract OutputStream getOutputStream() throws IOException;
/**
* Get a description of the destination for error reporting.
*
* @return a description of the destination for error reporting
*/
protected abstract String getDestinationDescription();
/**
* Get the remote content from the source URI.
*
* @param sourceUri
* the URI for the source content
*/
public void retrieveRemoteContent(String sourceUri) {
HttpEntity entity = null;
try {
HttpGet httpGet = new HttpGet(sourceUri);
HttpResponse response = httpClient.execute(httpGet);
entity = response.getEntity();
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
if (entity != null) {
InputStream in = entity.getContent();
try {
transferFile(in);
in.close();
in = null;
} catch (IOException e) {
throw new InteractiveSpacesException(String.format("Exception during copy of HTTP resource %s to %s",
sourceUri, getDestinationDescription()), e);
} finally {
Closeables.closeQuietly(in);
}
}
} else {
throw new SimpleInteractiveSpacesException(String.format(
"Server returned bad status code %d for source URI %s during HTTP copy", statusCode, sourceUri));
}
} catch (InteractiveSpacesException e) {
throw e;
} catch (Exception e) {
throw new InteractiveSpacesException(String.format("Could not read source URI %s during HTTP copy", sourceUri),
e);
} finally {
if (entity != null) {
try {
EntityUtils.consume(entity);
} catch (IOException e) {
throw new InteractiveSpacesException(String.format(
"Could not consume entity content for %s during HTTP copy", sourceUri), e);
}
}
}
}
/**
* Transfer the content from the HTTP input stream to the destination file.
*
* @param in
* the HTTP result
*
* @throws IOException
* something bad happened during IO operations
*/
private void transferFile(InputStream in) throws IOException {
OutputStream out = null;
try {
out = getOutputStream();
byte[] buffer = new byte[BUFFER_SIZE];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
out.flush();
out.close();
out = null;
} finally {
if (out != null) {
out.close();
}
}
}
}
}