/*
* Copyright (c) 2013 Andreas Wohlén
* MIT License
*/
package se.wollan.httpwrapper;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.PasswordAuthentication;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Lightweight wrapper class for {@link HttpURLConnection}.
*
* @see https://bitbucket.org/wollan/httpwrapper
*/
/*
* TODO:
* - Multiple encodings. Including read charset from header?
* - Optimize for android: http://developer.android.com/reference/java/net/HttpURLConnection.html
* - respLength() for number of bytes
* - srcFile, srcStream as well.
* - more efficient string reading
*/
public class Http {
private final static String CHARSET = "UTF-8";
/** Convenience method for simply getting an URL. */
public static String get(String url) throws IOException {
return new Http(url).exec();
}
//request params
private String url;
private String method;
private int timeout;
private List<String> reqHeaderFieldNames;
private List<String> reqHeaderFieldValues;
private String reqData;
private Map<String, String> reqForm;
//destinations
private File destFile, destErrorFile;
private OutputStream destStream, destErrorStream;
//response data
private int respCode;
private String respData;
private Map<String, List<String>> respHeaders;
private IOException ioException;
/** Create an empty http object. */
public Http() {
clear();
}
/** Same as <code>new Http().url(myurl)</code>. */
public Http(String urlToExecute) {
clear();
url = urlToExecute;
}
/** Resets both req and resp to default values effectively recycling this instance. */
public Http clear() {
clearReq();
clearResp();
return this;
}
/** Reset req parameters to default values. */
public Http clearReq() {
url = null;
method = null;
timeout = -1;
reqHeaderFieldNames = null;
reqHeaderFieldValues = null;
reqData = null;
reqForm = null;
return this;
}
/** Removes latest response releasing potentially large resources like respData. */
public Http clearResp() {
respCode = -1;
respData = null;
respHeaders = null;
ioException = null;
return this;
}
/** Set the url. Wrapper for {@link URL#URL(String)}. */
public Http url(String urlToExecute) {
url = urlToExecute;
return this;
}
/** Wrapper for {@link HttpURLConnection#getURL()} */
public String url() {
return url;
}
/** Wrapper for {@link HttpURLConnection#setRequestMethod(String)}.
* Default is "GET". */
public Http method(String httpMethodToUse) {
method = httpMethodToUse;
return this;
}
/** Wrapper for {@link HttpURLConnection#getRequestMethod()} */
public String method() {
return method;
}
/** Wrapper for {@link HttpURLConnection#setConnectTimeout(int)}. */
public Http timeout(int newTimeoutMillis) {
timeout = newTimeoutMillis;
return this;
}
/** Wrapper for {@link HttpURLConnection#getConnectTimeout()} */
public int timeout() {
return timeout;
}
/** Adds a request header. Wrapper for
* {@link HttpURLConnection#addRequestProperty(String, String)}. */
public Http header(String fieldName, String fieldValue) {
if(reqHeaderFieldNames == null) {
reqHeaderFieldNames = new ArrayList<String>();
reqHeaderFieldValues = new ArrayList<String>();
}
reqHeaderFieldNames.add(fieldName);
reqHeaderFieldValues.add(fieldValue);
return this;
}
/** Set default basic authentication. */
public Http basicAuth(final String username, final String password) {
Authenticator.setDefault(new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username,
(password == null ? "" : password).toCharArray());
}
});
return this;
}
/**
* Add a cookie to request headers. About allowed
* characters: http://stackoverflow.com/a/1969339/1593797.
* No cookie session management.
*/
public Http cookie(String name, String value) {
if(name != null && value != null) {
header("Cookie", name + "=" + value);
}
return this;
}
/**
* Set arbitrary text data to send with this request.
* Any form data will be deleted, see {@link #form(String, String)}.
*
* @param dataToSend null disables sending
* @return this to allow chaining
*/
public Http data(String dataToSend) {
reqData = dataToSend;
if(dataToSend != null) {
reqForm = null;
}
return this;
}
/** @return request data set by {@link #data(String)}. */
public String data() {
return reqData;
}
/**
* Set a HTML form name-value-pair to send with this request
* assuming UTF-8.<br>
* Call multiple times to set a whole form.<br>
* Any data set by {@link #data(String)} will be deleted,
* as it's not possible to send both form and arbitrary data
* with the same request.<br>
* The Content-Type <tt>application/x-www-form-urlencoded</tt> is not
* set automatically. Do so using {@link #header(String, String)} if
* necessary.
*
* @param fieldName null fieldName disables the whole form
* @param fieldValue to disable one previously set field, call this
* with same fieldName and fieldValue=null
* @return this to allow chaining
*/
public Http form(String fieldName, String fieldValue) {
if(fieldName == null) {
reqForm = null;
return this;
}
if(reqForm == null) {
reqForm = new HashMap<String, String>();
}
reqForm.put(fieldName, fieldValue);
reqData = null;
return this;
}
/** @return request form data set by {@link #form(String, String)}. */
public Map<String, String> form() {
return reqForm;
}
/**
* Set the destination file.
*
* <p>The response bytes will be written to this file during execution, creating file if it doesn't exist.
* Any <code>IOException</code> related file operations will be thrown later when executed.
* This is for status codes below 400, for error codes use {@link #destErrorFile(File)}.
*
* <p><b>Note:</b> This does not interfere with any other <code>dest*()</code>-method
* (multiple outputs are allowed), but it will disable the default string decoder.
* Meaning {@link #exec()} will henceforth return <code>null</code>.
*/
public Http destFile(File file) {
destFile = file;
return this;
}
/** Same as calling {@link #destFile(File)} with <code>new File(filePath)</code>. */
public Http destFile(String filePath) {
return destFile(new File(filePath));
}
/** @return the destination file set by {@link #destFile(File)}. */
public File destFile() {
return destFile;
}
/** Same as {@link #destFile(File)} but for error responses, ie. http codes >= 400. May be same file. */
public Http destErrorFile(File errorFile) {
destErrorFile = errorFile;
return this;
}
/** @return the destination error file set by {@link #destErrorFile(File)}. */
public File destErrorFile() {
return destErrorFile;
}
/**
* Set the destination stream.
*
* <p>The response bytes will be written to this output stream during execution, closing it when done.
* Any relating <code>IOException</code> will be thrown later when executed.
* This is for status codes below 400, for error codes use {@link #destErrorStream(OutputStream)}.
*
* <p><b>Note:</b> This does not interfere with any other <code>dest*()</code>-method
* (multiple outputs are allowed), but it will disable the default string decoder.
* Meaning {@link #exec()} will henceforth return <code>null</code>.
*/
public Http destStream(OutputStream stream) {
destStream = stream;
return this;
}
/** @return the destination stream set by {@link #destStream(OutputStream)}. */
public OutputStream destStream() {
return destStream;
}
/** Same as {@link #destStream(OutputStream)} but for error responses, ie. http codes >= 400. May be same stream. */
public Http destErrorStream(OutputStream errorStream) {
destErrorStream = errorStream;
return this;
}
/** @return the destination error stream set by {@link #destErrorStream(OutputStream)}. */
public OutputStream destErrorStream() {
return destErrorStream;
}
/**
* Execute request silently. Check exceptions
* by {@link #ioException()}.
*
* @return the response data including error data.
* (Same as {@link #respData()}.)
*/
public String execSilently() {
try {
httpExec();
return respData;
} catch (IOException e) {
ioException = e;
return respData;
}
}
/**
* Execute request.
*
* @return the response data on a successful request.
* (Retrieve response data for http codes above 400 by {@link #respData()}
* after caught exception).
* @throws IOException
*/
public String exec() throws IOException {
httpExec();
return respData;
}
/** Checks whether we got a response code from server.
* If false, check {@link #ioException}. */
public boolean gotResp() {
return respCode != -1;
}
/** Wrapper for {@link HttpURLConnection#getResponseCode()}. */
public int respCode() {
return respCode;
}
/** @return The latest response from server including error data
* (eg. http 404 response). */
public String respData() {
return respData;
}
/** Wrapper for {@link URLConnection#getHeaderFields()}. */
public Map<String, List<String>> respHeaders() {
return respHeaders;
}
/** @return saved exception from {@link #execSilently()}. */
public IOException ioException() {
return ioException;
}
private void httpExec() throws IOException {
clearResp();
final HttpURLConnection httpConn = (HttpURLConnection)
new URL(url).openConnection();
try {
setRequestParams(httpConn);
sendRequestData(httpConn);
respCode = httpConn.getResponseCode();
respHeaders = httpConn.getHeaderFields();
receiveResponse(httpConn);
} finally {
httpConn.disconnect();
}
}
/**
* Called during execution. Sets request parameters prior to actual connection.
* Override to add custom properties to the {@link HttpURLConnection}
* not present as public methods.
*
* @param httpConn the current instance
*/
protected void setRequestParams(HttpURLConnection httpConn) throws ProtocolException {
if(method != null) {
httpConn.setRequestMethod(method);
}
if(timeout >= 0) httpConn.setConnectTimeout(timeout);
if(reqHeaderFieldNames != null) {
for(int i=0; i<reqHeaderFieldNames.size(); i++) {
httpConn.addRequestProperty(reqHeaderFieldNames.get(i), reqHeaderFieldValues.get(i));
}
}
}
/**
* Called during execution. Override to write the
* {@link HttpURLConnection#getOutputStream()} yourself.
*
* @param httpConn the current instance
* @return the raw data to send
* @throws UnsupportedEncodingException
*/
protected void sendRequestData(HttpURLConnection httpConn) throws IOException {
final String outData = encodeRequestData();
if(outData != null) {
httpConn.setDoOutput(true);
writeStream(httpConn.getOutputStream(), outData);
}
}
/**
* Called during execution when receiving response. Override to
* read the {@link HttpURLConnection#getInputStream()} and
* {@link HttpURLConnection#getErrorStream()} yourself.
*
* @param httpConn the current instance
* @throws IOException
*/
protected void receiveResponse(HttpURLConnection httpConn) throws IOException {
try {
decodeRespInputStream(respCode(), httpConn.getInputStream());
} catch (IOException e) {
decodeRespErrorStream(respCode(), httpConn.getErrorStream());
throw e;
}
}
/**
* Called during execution when decoding {@link HttpURLConnection#getInputStream()}
* (status codes below 400). Override this can be useful if you need to switch on
* status code and decode some responses differently.
* Don't forget to close the stream when done.
*
* @see #destStream(OutputStream)
*/
protected void decodeRespInputStream(int statusCode, InputStream inputStream) throws IOException {
decodeRespStream(inputStream, destFile, destStream);
}
/**
* Same as {@link #decodeRespInputStream(InputStream)} but for
* {@link HttpURLConnection#getErrorStream()} (codes >=400).
*
* @see #destErrorStream(OutputStream)
*/
protected void decodeRespErrorStream(int statusCode, InputStream errorStream) throws IOException {
decodeRespStream(errorStream, destErrorFile, destErrorStream);
}
private void decodeRespStream(InputStream is, File f, OutputStream os) throws IOException {
//use default string decoder if there are no destinations
if(f == null && os == null) {
respData = readStream(is);
return;
}
//create outputs
OutputStream fos = f != null ? new FileOutputStream(f) : null;
try {
//stream data to all non-null outputs
byte[] buffer = new byte[8192]; //same size as bufferedInputStream
int n;
while((n = is.read(buffer)) >= 0) {
if(fos != null) {
fos.write(buffer, 0, n);
}
if(os != null) {
os.write(buffer, 0, n);
}
}
} finally {
//close all inputs and outputs
is.close();
if(fos != null) fos.close();
if(os != null) os.close();
}
}
/**
* Write an output stream to a string assuming UTF-8.
* It will internally wrap the stream with a {@link BufferedOutputStream}
* and close it when done.
*/
public static void writeStream(OutputStream os, String data) throws IOException {
final OutputStreamWriter out = new OutputStreamWriter(
new BufferedOutputStream(os), CHARSET);
try {
out.write(data);
} finally {
out.close();
}
}
/**
* Read an input stream into a string assuming UTF-8 and close it when done.
*/
public static String readStream(InputStream is) throws IOException {
final StringBuilder sb = new StringBuilder(is.available());
final byte[] buffer = new byte[8192];
int n;
try {
while((n = is.read(buffer)) >= 0) {
sb.append(new String(buffer, 0, n, CHARSET));
}
return sb.toString();
} finally {
is.close();
}
}
/**
* @return a hexadecimal md5 hash of the provided string assuming UTF-8.
*/
public static String md5(String toHash) {
try {
return new BigInteger(1,
MessageDigest.getInstance("MD5").digest(
String.valueOf(toHash).getBytes(CHARSET)))
.toString(16);
} catch (UnsupportedEncodingException e) {
} catch (NoSuchAlgorithmException e) {}
return null; //will never happen
}
private String encodeRequestData() throws UnsupportedEncodingException {
if(reqData != null) {
return reqData;
}
if(reqForm != null) {
//URL encode the form data
final StringBuilder result = new StringBuilder();
boolean first = true;
for(Map.Entry<String, String> entry : reqForm.entrySet()) {
if(entry.getValue() != null) {
if(first) {
first = false;
} else {
result.append("&");
}
result.append(URLEncoder.encode(entry.getKey(), CHARSET));
result.append("=");
result.append(URLEncoder.encode(entry.getValue(), CHARSET));
}
}
return result.toString();
}
return null;
}
}