/* * SK's Minecraft Launcher * Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors * Please see LICENSE.txt for license information. */ package com.skcraft.launcher.util; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.skcraft.concurrency.ProgressObservable; import lombok.Getter; import lombok.extern.java.Log; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import java.io.*; import java.net.*; import java.util.*; import static com.skcraft.launcher.LauncherUtils.checkInterrupted; import static org.apache.commons.io.IOUtils.closeQuietly; /** * A simple fluent interface for performing HTTP requests that uses * {@link java.net.HttpURLConnection} or {@link javax.net.ssl.HttpsURLConnection}. */ @Log public class HttpRequest implements Closeable, ProgressObservable { 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; @Getter 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 java.io.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. * <p/> * After execution, {@link #close()} should be called. * * @return this object * @throws java.io.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(); conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Java) SKMCLauncher"); 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 java.io.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 + " for " + url); } /** * Get the response code. * * @return the response code * @throws java.io.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 java.io.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 java.io.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 java.io.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; } @Override public double getProgress() { if (contentLength >= 0) { return readBytes / (double) contentLength; } else { return -1; } } @Override public String getStatus() { return null; } @Override public void close() throws IOException { if (conn != null) conn.disconnect(); } /** * Perform a GET request. * * @param url the URL * @return a new request object */ public static HttpRequest get(URL url) { return request("GET", url); } /** * Perform a POST request. * * @param url the URL * @return a new request object */ public static HttpRequest post(URL url) { return request("POST", url); } /** * Perform a request. * * @param method the method * @param url the URL * @return a new request object */ public static HttpRequest request(String method, URL url) { return new HttpRequest(method, url); } /** * Create a new {@link java.net.URL} and throw a {@link RuntimeException} if the URL * is not valid. * * @param url the url * @return a URL object * @throws RuntimeException if the URL is invalid */ public static URL url(String url) { try { return new URL(url); } catch (MalformedURLException e) { throw new RuntimeException(e); } } /** * URL may contain spaces and other nasties that will cause a failure. * * @param existing the existing URL to transform * @return the new URL, or old one if there was a failure */ 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; } } /** * Used with {@link #bodyForm(Form)}. */ public final static class Form { public final List<String> elements = new ArrayList<String>(); private Form() { } /** * Add a key/value to the form. * * @param key the key * @param value the value * @return this object */ 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(); } /** * Create a new form. * * @return a new form */ public static Form form() { return new Form(); } } /** * Used to buffer the response in memory. */ public class BufferedResponse { private final byte[] data; private BufferedResponse(byte[] data) { this.data = data; } /** * Return the result as bytes. * * @return the data */ public byte[] asBytes() { return data; } /** * Return the result as a string. * * @param encoding the encoding * @return the string * @throws java.io.IOException on I/O error */ public String asString(String encoding) throws IOException { return new String(data, encoding); } /** * Return the result as an instance of the given class that has been * deserialized from a JSON payload. * * @param cls the class * @return the object * @throws java.io.IOException on I/O error */ public <T> T asJson(Class<T> cls) throws IOException { return mapper.readValue(asString("UTF-8"), cls); } /** * Return the result as an instance of the given type that has been * deserialized from a JSON payload. * * @param type the type reference * @return the object * @throws java.io.IOException on I/O error */ public <T> T asJson(TypeReference type) throws IOException { return mapper.readValue(asString("UTF-8"), type); } /** * Return the result as an instance of the given class that has been * deserialized from a XML payload. * * @return the object * @throws java.io.IOException on I/O error */ @SuppressWarnings("unchecked") 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); } } /** * Save the result to a file. * * @param file the file * @return this object * @throws java.io.IOException on I/O error * @throws InterruptedException on interruption */ 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; } /** * Save the result to an output stream. * * @param out the output stream * @return this object * @throws java.io.IOException on I/O error * @throws InterruptedException on interruption */ public BufferedResponse saveContent(OutputStream out) throws IOException, InterruptedException { out.write(data); return this; } } }