/*
* Copyright (c) 2013 Andreas Wohlén
* MIT License
*/
package se.wollan.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.3
* <p>
* Wrapper class for {@link HttpURLConnection}.
* Use directly to send/receive text data (assuming UTF-8 everywhere)
* 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>
* <p>
* Example:<br>
* <code>String resp = new Http("http://www.google.com").exec();<code>
*/
/*
* TODO:
* - Multiple encodings. Including read charset from header?
*/
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;
//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), CHARSET);
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), CHARSET);
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(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;
}
}