package co.forsaken.projectindigo.utils;
import static org.apache.commons.io.IOUtils.closeQuietly;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimerTask;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import lombok.extern.java.Log;
import org.codehaus.jackson.map.ObjectMapper;
import co.forsaken.projectindigo.util.concurrent.WorkUnit;
public class HttpRequest extends WorkUnit implements Closeable {
private static final int READ_TIMEOUT = 1000 * 60 * 10;
private static final int READ_BUFFER_SIZE = 1024 * 8;
private final ObjectMapper mapper = new ObjectMapper();
private final Map<String, String> headers = new HashMap<String, String>();
private final String method;
private final URL url;
private String contentType;
private byte[] body;
private HttpURLConnection conn;
private InputStream inputStream;
private long contentLength = -1;
private long readBytes = 0;
/**
* Create a new HTTP request.
*
* @param method
* the method
* @param url
* the URL
*/
private HttpRequest(String method, URL url) {
this.method = method;
this.url = url;
}
/**
* Set the content body to a JSON object with the content type of
* "application/json".
*
* @param object
* the object to serialize as JSON
* @return this object
* @throws IOException
* if the object can't be mapped
*/
public HttpRequest bodyJson(Object object) throws IOException {
contentType = "application/json";
body = mapper.writeValueAsBytes(object);
return this;
}
/**
* Submit form data.
*
* @param form
* the form
* @return this object
*/
public HttpRequest bodyForm(Form form) {
contentType = "application/x-www-form-urlencoded";
body = form.toString().getBytes();
return this;
}
/**
* Add a header.
*
* @param key
* the header key
* @param value
* the header value
* @return this object
*/
public HttpRequest header(String key, String value) {
headers.put(key, value);
return this;
}
/**
* Execute the request.
*
* After execution, {@link #close()} should be called.
*
* @return this object
* @throws IOException
* on I/O error
*/
public HttpRequest execute() throws IOException {
boolean successful = false;
try {
if (conn != null) { throw new IllegalArgumentException("Connection already executed"); }
conn = (HttpURLConnection) reformat(url).openConnection();
if (body != null) {
conn.setRequestProperty("Content-Type", contentType);
conn.setRequestProperty("Content-Length", Integer.toString(body.length));
conn.setDoInput(true);
}
for (Map.Entry<String, String> entry : headers.entrySet()) {
conn.setRequestProperty(entry.getKey(), entry.getValue());
}
conn.setRequestMethod(method);
conn.setUseCaches(false);
conn.setDoOutput(true);
conn.setReadTimeout(READ_TIMEOUT);
conn.connect();
if (body != null) {
DataOutputStream out = new DataOutputStream(conn.getOutputStream());
out.write(body);
out.flush();
out.close();
}
inputStream = conn.getResponseCode() == HttpURLConnection.HTTP_OK ? conn.getInputStream() : conn.getErrorStream();
successful = true;
} finally {
if (!successful) {
close();
}
}
return this;
}
/**
* Require that the response code is one of the given response codes.
*
* @param codes
* a list of codes
* @return this object
* @throws IOException
* if there is an I/O error or the response code is not expected
*/
public HttpRequest expectResponseCode(int... codes) throws IOException {
int responseCode = getResponseCode();
for (int code : codes) {
if (code == responseCode) { return this; }
}
close();
throw new IOException("Did not get expected response code, got " + responseCode);
}
/**
* Get the response code.
*
* @return the response code
* @throws IOException
* on I/O error
*/
public int getResponseCode() throws IOException {
if (conn == null) { throw new IllegalArgumentException("No connection has been made"); }
return conn.getResponseCode();
}
/**
* Get the input stream.
*
* @return the input stream
*/
public InputStream getInputStream() {
return inputStream;
}
/**
* Buffer the returned response.
*
* @return the buffered response
* @throws IOException
* on I/O error
* @throws InterruptedException
* on interruption
*/
public BufferedResponse returnContent() throws IOException, InterruptedException {
if (inputStream == null) { throw new IllegalArgumentException("No input stream available"); }
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int b = 0;
while ((b = inputStream.read()) != -1) {
checkInterrupted();
bos.write(b);
}
return new BufferedResponse(bos.toByteArray());
} finally {
close();
}
}
/**
* Save the result to a file.
*
* @param file
* the file
* @return this object
* @throws IOException
* on I/O error
* @throws InterruptedException
* on interruption
*/
public HttpRequest saveContent(File file) throws IOException, InterruptedException {
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try {
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos);
saveContent(bos);
} finally {
closeQuietly(bos);
closeQuietly(fos);
}
return this;
}
/**
* Save the result to an output stream.
*
* @param out
* the output stream
* @return this object
* @throws IOException
* on I/O error
* @throws InterruptedException
* on interruption
*/
public HttpRequest saveContent(OutputStream out) throws IOException, InterruptedException {
BufferedInputStream bis;
try {
String field = conn.getHeaderField("Content-Length");
if (field != null) {
long len = Long.parseLong(field);
if (len >= 0) { // Let's just not deal with really big numbers
contentLength = len;
}
}
} catch (NumberFormatException e) {}
try {
bis = new BufferedInputStream(inputStream);
byte[] data = new byte[READ_BUFFER_SIZE];
int len = 0;
while ((len = bis.read(data, 0, READ_BUFFER_SIZE)) >= 0) {
out.write(data, 0, len);
readBytes += len;
checkInterrupted();
}
} finally {
close();
}
return this;
}
public void updateProgress() {
double progress = -1;
if (contentLength >= 0) {
progress = readBytes / (double) contentLength;
}
push(progress, url.toString());
}
public void close() throws IOException {
if (conn != null) conn.disconnect();
}
public static HttpRequest get(URL url) {
return request("GET", url);
}
public static HttpRequest post(URL url) {
return request("POST", url);
}
public static HttpRequest request(String method, URL url) {
return new HttpRequest(method, url);
}
public static URL url(String url) {
try {
return new URL(url);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
private static URL reformat(URL existing) {
try {
URL url = new URL(existing.toString());
URI uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef());
url = uri.toURL();
return url;
} catch (MalformedURLException e) {
return existing;
} catch (URISyntaxException e) {
return existing;
}
}
public final static class Form {
public final List<String> elements = new ArrayList<String>();
private Form() {}
public Form add(String key, String value) {
try {
elements.add(URLEncoder.encode(key, "UTF-8") + "=" + URLEncoder.encode(value, "UTF-8"));
return this;
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override public String toString() {
StringBuilder builder = new StringBuilder();
boolean first = true;
for (String element : elements) {
if (first) {
first = false;
} else {
builder.append("&");
}
builder.append(element);
}
return builder.toString();
}
public static Form form() {
return new Form();
}
}
public class BufferedResponse {
private final byte[] data;
private BufferedResponse(byte[] data) {
this.data = data;
}
public byte[] asBytes() {
return data;
}
public String asString(String encoding) throws IOException {
return new String(data, encoding);
}
public <T> T asJson(Class<T> cls) throws IOException {
return mapper.readValue(asString("UTF-8"), cls);
}
public <T> T asXml(Class<T> cls) throws IOException {
try {
JAXBContext context = JAXBContext.newInstance(cls);
Unmarshaller um = context.createUnmarshaller();
return (T) um.unmarshal(new ByteArrayInputStream(data));
} catch (JAXBException e) {
throw new IOException(e);
}
}
public BufferedResponse saveContent(File file) throws IOException, InterruptedException {
FileOutputStream fos = null;
BufferedOutputStream bos = null;
file.getParentFile().mkdirs();
try {
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos);
saveContent(bos);
} finally {
closeQuietly(bos);
closeQuietly(fos);
}
return this;
}
public BufferedResponse saveContent(OutputStream out) throws IOException, InterruptedException {
out.write(data);
return this;
}
}
public static void checkInterrupted() throws InterruptedException {
if (Thread.interrupted()) { throw new InterruptedException(); }
}
}