All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.keeber.http.Rest Maven / Gradle / Ivy

package org.keeber.http;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

public abstract class Rest {
  protected String url;
  protected Map headers = new HashMap();
  protected static final String AUTH = "Authorization";

  /**
   * Create a client instance with the given endpoint.
   * 
   * @param url the endpoint (typically starting with http:// or https://)
   * @return Client
   */
  public static Client newClient(String url) {
    return new Client(url);
  }

  protected Rest(String url) {
    this.url = url;
  }

  private Gson serializer;

  Gson serializer() {
    return this.serializer == null ? serializer = new GsonBuilder().setPrettyPrinting().create() : serializer;
  }

  /**
   * Set the Gson serializer for object operations.
   * 
   * @param serializer Gson instance to use.
   * @return Rest
   */
  public T serializer(Gson serializer) {
    this.serializer = serializer;
    return getThis();
  }

  public T header(String key, String value) {
    headers.put(key, value);
    return getThis();
  }

  protected abstract T getThis();

  /**
   * Removed the designated header from this client (already created requests will be unaffected).
   * 
   * @param key header key to remove eg: "User-agent"
   * @return Rest
   */
  public T clear(String key) {
    headers.remove(key);
    return getThis();
  }

  /**
   * Clear the basic authorization for this client.
   * 
   * @return Rest
   */
  public T nobasic() {
    return clear(AUTH);
  }

  /**
   * Set basic authorization for this client.
   * 
   * @param username User name.
   * @param password Password.
   * @return Rest
   */
  public T basic(String username, String password) {
    return header(AUTH, "Basic " + utils.encode(username + ":" + password));
  }

  /**
   * Sets headers for JSON communication (Accept & Content-Type).
   * 
   * @return Rest
   */
  public T json() {
    header("Accept", "application/json");
    header("Content-Type", "application/json");
    return getThis();
  }

  private enum Method {
    POST, GET, PUT, DELETE, HEAD;
  }

  public static class Client extends Rest {

    /**
     * Create a request from this client. The provided URL is appended to the clients endpoint - it
     * may also include route params. Headers from the client (including auth) will be used on
     * execution.
     * 
     * 

* Requests executions (get(), post(), etc... methods) are thread safe. * * @param url the endpoint. * @return Request */ public Request newRequest(String url) { return new Request(url); } /** * Create a request from this client. Using the base URL for the client. Headers from the client * (including auth) will be used on execution. * *

* Requests executions (get(), post(), etc... methods) are thread safe. * * @param url the endpoint. * @return Request */ public Request newRequest() { return new Request(""); } private Client(String url) { super(url); } public class Request extends Rest { private Request(String url) { super(url); } private String query; @Override protected Request getThis() { return this; } /** * Add query parameters to this request - this method will perform URL encoding. * *

* Query parameters are cumulative (ie: they do not use an underlying map) - however them may * contain (or consist entirely of) route parameters in the form of {myparam}. * * @param name of the parameter * @param value of the parameter * @return Request */ public Request query(String name, String value) { try { query = (query == null ? "?" : query + "&") + name + "=" + URLEncoder.encode(value, utils.UTF_8); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e.getMessage(), e); } return this; } /** * Execute a GET with the specified route params. * *

* To replace the route param {id} in the URL (or query string) the route params * "id","myidvalue" would be provided. * * @param routes optional string list of routes. * @return Response * @throws RestException */ public Response get(String... routes) throws RestException { return execute(this, null, Method.GET, routes); } /** * Execute a HEAD with the specified route params. * * @param routes optional string list of routes. * @return Response * @throws RestException */ public Response head(String... routes) throws RestException { return execute(this, null, Method.HEAD, routes); } /** * Execute a POST with the provided object as the body with the optional route params. * * @param body for the post - can be a Rest.Form object for a multi-part post. * @param routes optional string list of routes. * @return Response * @throws RestException */ public Response post(Object body, String... routes) throws RestException { return execute(this, body, Method.POST, routes); } /** * Execute a PUT with the provided object as the body with the optional route params. * * @param body for the post - can be a Rest.Form object for a multi-part post. * @param type expected return type or class. * @param routes optional string list of routes. * @return Response * @throws RestException */ public Response put(Object body, String... routes) throws RestException { return execute(this, body, Method.PUT, routes); } /** * Execute a DELETE with the optional route params. * * @param type expected return type or class. * @param routes optional string list of routes. * @return Response * @throws RestException */ public Response delete(String... routes) throws RestException { return execute(this, null, Method.DELETE, routes); } } /** * An HTTP response class. * * @author Jason Keeber * * @param */ public class Response { private Object result; private int code, length; private String message, contentType; /** * Does this response contain a non-null value for type T? * * @return true is the result is present. */ public boolean hasResult() { return result != null; } public JsonObject asJsonObject() throws RestException { return as(JsonObject.class); } public String asString() throws RestException { return as(String.class); } public InputStream asStream() throws RestException { return as(InputStream.class); } /** * Converts the result (if present) to the requested type (if necessary). Possible types are * String (for text responses), JsonElement (for json content types), and InputStream (for all * other types). * *

* JsonElement types can be converted to other types as appropriate. * * @param type * @return * @throws RestException */ @SuppressWarnings("unchecked") public T as(Type type) throws RestException { if (result == null) { return null; } if (result instanceof InputStream) { if (type.equals(InputStream.class)) { return (T) result; } throw new RestException("Cannot convert streaming response type [" + type.getTypeName() + "]"); } if (result instanceof JsonElement) { if (type.equals(String.class)) { return (T) serializer().toJson(result); } if (type.equals(JsonArray.class)) { return (T) ((JsonElement) result).getAsJsonArray(); } if (type.equals(JsonObject.class)) { return (T) ((JsonElement) result).getAsJsonObject(); } if (type.equals(JsonElement.class)) { return (T) result; } return serializer().fromJson((JsonElement) result, type); } else { if (type.equals(String.class)) { return (T) result.toString(); } return serializer().fromJson(result.toString(), type); } } /** * The code of the response ie: 200 == OK * * @return HTTP response code. */ public int getCode() { return code; } /** * The contents length (if available). * * @return the content length. */ public int getLength() { return length; } /** * The response message. * * @return eg: OK */ public String getMessage() { return message; } /** * The content type of the response. * * @return */ public String getContentType() { return contentType; } } @Override protected Client getThis() { return this; } protected Response execute(Request request, Object body, Method method, String... routes) throws RestException { Response response = new Response(); HttpURLConnection connection = null; boolean multipart = body != null && body instanceof Form, streaming = false; try { // Swap the route params into the url String furl = new StringBuilder(url).append(request.url).append(request.query == null ? "" : request.query).toString(); if (routes.length % 2 != 0) { throw new RestException("Even number of route paramaters expected [" + routes.length + "]"); } for (int i = 0; i < routes.length; i += 2) { furl = furl.replaceAll("(%7B|\\{)" + routes[i] + "(%7D|\\})", URLEncoder.encode(routes[i + 1], utils.UTF_8)); } // Create the connection & set the method connection = (HttpURLConnection) new URL(furl).openConnection(); connection.setRequestMethod(method.toString()); // Set the headers { for (Entry header : headers.entrySet()) { connection.setRequestProperty(header.getKey(), header.getValue()); } for (Entry header : request.headers.entrySet()) { connection.setRequestProperty(header.getKey(), header.getValue()); } } // String boundary = null; if (multipart) { connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + (boundary = "==" + System.currentTimeMillis() + "==")); } // connection.setDoInput(true); connection.setDoOutput(body != null); connection.connect(); // Request BODY if (body != null) { if (multipart) { Form form = (Form) body; OutputStream os; PrintWriter wt = new PrintWriter(new OutputStreamWriter(os = new BufferedOutputStream(connection.getOutputStream()), utils.UTF_8), true); for (Entry entry : form.data.entrySet()) { if (entry.getValue() instanceof Form.FileEntry) { Form.FileEntry fileEntry = (Form.FileEntry) entry.getValue(); wt.append("--" + boundary).append(utils.LF); wt.append("Content-Disposition: form-data; name=\"").append(entry.getKey()).append("\"; filename=\"").append(fileEntry.filename).append("\"").append(utils.LF); wt.append("Content-Type: ").append(HttpURLConnection.guessContentTypeFromName(fileEntry.filename)).append(utils.LF); wt.append("Content-Transfer-Encoding: binary").append(utils.LF).append(utils.LF); wt.flush(); utils.copy(fileEntry.stream, os, false); utils.close(fileEntry.stream); wt.append(utils.LF); wt.flush(); } else { wt.append("--" + boundary).append(utils.LF); wt.append("Content-Disposition: form-data; name=\"").append(entry.getKey()).append("\"").append(utils.LF); wt.append("Content-Type: text/plain; charset=").append(utils.UTF_8).append(utils.LF).append(utils.LF); wt.append(entry.getValue() instanceof String ? entry.getValue() + "" : request.serializer().toJson(entry.getValue())); wt.append(utils.LF).flush(); } } wt.append("--" + boundary + "--").append(utils.LF).flush(); wt.close(); } else { InputStream is; if (body instanceof InputStream) { is = (InputStream) body; } else { is = new ByteArrayInputStream((body instanceof String ? (String) body : request.serializer().toJson(body)).getBytes(utils.UTF_8)); } utils.copy(is, connection.getOutputStream(), true); } } response.code = connection.getResponseCode(); response.message = connection.getResponseMessage(); response.length = connection.getContentLength(); response.contentType = connection.getContentType(); if (response.code >= 200 && response.code < 400) { if (response.contentType.matches(".*(text|json).*")) { String result = utils.asString(connection.getInputStream()); response.result = (response.contentType.contains("text") ? result : request.serializer().fromJson(result, JsonElement.class)); } else { response.result = new AutocloseConnectionStream(connection, connection.getInputStream()); streaming = true; } } } catch (ProtocolException e) { throw new RestException(e.getMessage(), e); } catch (MalformedURLException e) { throw new RestException(e.getMessage(), e); } catch (IOException e) { throw new RestException(e.getMessage(), e); } finally { if (!streaming && connection != null) { connection.disconnect(); } } return response; } } /** * A stream that disconnection the Http URL Connection when it is closed. * * @author Jason Keeber * */ private static class AutocloseConnectionStream extends FilterInputStream { private transient HttpURLConnection connection; public AutocloseConnectionStream(HttpURLConnection connection, InputStream in) { super(in); this.connection = connection; } @Override public void close() throws IOException { super.close(); connection.disconnect(); } } /** * Oh noes...it haz a went wrong. * * @author Jason Keeber * */ public static class RestException extends Exception { public RestException(String message, Throwable cause) { super(message, cause); } public RestException(String message) { super(message); } } /** * Internal utility class. * * @author Jason Keeber * */ public static class utils { private static final String UTF_8 = "UTF-8"; private static final String LF = "\r\n"; public static void copy(InputStream is, OutputStream os, boolean close) throws IOException { try { byte[] buffer = new byte[1024 * 16]; int len; while ((len = is.read(buffer)) > 0) { os.write(buffer, 0, len); } os.flush(); } finally { if (close) { utils.close(is); utils.close(os); } } } public static String asString(InputStream is) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); copy(is, bos, true); return bos.toString(UTF_8); } private static void close(Closeable stream) { if (stream != null) { try { stream.close(); } catch (IOException e) { // Ignore (that is the only function of this method. } } } private static String encode(String content) { byte[] data; try { data = content.getBytes(UTF_8); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e.getMessage(), e); } char[] tbl = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}; StringBuilder buffer = new StringBuilder(); int pad = 0; for (int i = 0; i < data.length; i += 3) { int b = ((data[i] & 0xFF) << 16) & 0xFFFFFF; if (i + 1 < data.length) { b |= (data[i + 1] & 0xFF) << 8; } else { pad++; } if (i + 2 < data.length) { b |= (data[i + 2] & 0xFF); } else { pad++; } for (int j = 0; j < 4 - pad; j++) { int c = (b & 0xFC0000) >> 18; buffer.append(tbl[c]); b <<= 6; } } for (int j = 0; j < pad; j++) { buffer.append("="); } return buffer.toString(); } } /** * A HTTP Form representation. * * @author Jason Keeber * */ public static class Form { private Map data = new HashMap(); /** * Add the object to this form with the provided key. None-string objects will be serialized * (using the Rest client serializer) to JSON objects when the form is POSTed. * * @param key the form name. * @param value the form value. * @return Form */ public Form put(String key, Object value) { data.put(key, value); return this; } /** * Add the provided stream as a file to the Form. * * @param key the form name. * @param filename the file upload name. * @param stream the file content. * @return Form */ public Form put(String key, String filename, InputStream stream) { data.put(key, new FileEntry(filename, stream)); return this; } /** * Add the provided string content as a file upload to the Form. * * @param key the form name. * @param filename the file upload name. * @param filecontent string file content. * @return Form */ public Form put(String key, String filename, String filecontent) { try { data.put(key, new FileEntry(filename, new ByteArrayInputStream(filecontent.getBytes(utils.UTF_8)))); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } return this; } private static class FileEntry { private InputStream stream; private String filename; public FileEntry(String filename, InputStream stream) { this.filename = filename; this.stream = stream; } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy