/**
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2013 Andreas Wohlén
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
*/
package httpwrapper;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
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.HttpURLConnection;
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;
import java.util.Scanner;
import javax.xml.bind.DatatypeConverter;
/**
* Version 1.2
* <p>
* Wrapper class for {@link HttpURLConnection}.
* Use directly to send/receive text data
* supporting common request parameters without streaming, or
* extend class to access to the current {@link HttpURLConnection} instance.
* Override {@link #setRequestParams(HttpURLConnection)} to set additional params,
* {@link #sendRequestData(HttpURLConnection)} to write output stream or
* {@link #receiveResponse(HttpURLConnection)} to read input stream.
* <p>
* Usage:
* <ol>
* <li>Format the request: <code>Http conn =
* new Http().method("POST").url("http://localhost/").data("My post data!");</code></li>
* <li>Execute request: <code>String respData = conn.exec();</code></li>
* <li>Optional. Check response: <code>conn.respCode(), conn.respHeaders(), ...</code></li>
* <li>Optional. Clear data: <code>conn.clear(), conn.clearReq(), conn.clearResp()</code></li>
* </ol>
*/
/*
* TODO:
* - Multiple encodings. Including read charset from header?
*/
public class Http {
/** 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;
//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;
}
/** Add a basic authentication header. */
public Http basicAuth(String username, String password) {
if(username != null && password != null) {
header("Authorization", "Basic " +
DatatypeConverter.printBase64Binary(
(username+":"+username).getBytes()));
}
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;
}
/**
* 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()} yourself.
*
* @param httpConn the current instance
* @throws IOException
*/
protected void receiveResponse(HttpURLConnection httpConn) throws IOException {
try {
respData = readStream(httpConn.getInputStream());
} catch (IOException e) {
respData = readStream(httpConn.getErrorStream());
throw e;
}
}
/**
* 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), "UTF-8");
try {
out.write(data);
} finally {
out.close();
}
}
/**
* Read an input stream into a string assuming UTF-8.
* It will internally wrap the stream with a {@link BufferedInputStream}
* and close it when done.
*/
public static String readStream(InputStream is) throws IOException {
if(is == null) return null;
final Scanner scanner = new Scanner(new BufferedInputStream(is), "UTF-8");
scanner.useDelimiter("\\A");
final String content = scanner.hasNext() ? scanner.next() : "";
final IOException readException = scanner.ioException();
scanner.close();
if(readException != null) {
throw new IOException(readException);
}
return content;
}
/**
* @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("UTF-8")))
.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;
final String enc = "UTF-8";
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(), enc));
result.append("=");
result.append(URLEncoder.encode(entry.getValue(), enc));
}
}
return result.toString();
}
return null;
}
}