/******************************************************************************* * Copyright (c) 2008, 2009 Brian Ballantine and Bug Labs, Inc. * * MIT License * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. *******************************************************************************/ package com.buglabs.util.simplerestclient; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import com.buglabs.util.Base64; /** * Class for dealing RESTfully with HTTP Requests. * * Example Usage: * HttpRequest req = new HttpRequest(myConnectionProvider) * HttpResponse resp = req.get("http://some.url") * System.out.println(resp.getString()); * * @author Brian * * Revisions * 09-03-2008 AK added a Map header parameter to "put" and "post" to support http header * * */ public class HTTPRequest { private static final String METHOD_POST = "POST"; private static final String HEADER_CONTENT_LENGTH = "Content-Length"; private static final String HEADER_CONTENT_TYPE = "Content-Type"; private static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded"; /** * Implementors can configure the http connection before every call is made. * Useful for setting headers that always need to be present in every WS call to a given server. * * @author kgilmer * */ public interface HTTPConnectionInitializer { /** * @param connection HttpURLConnection */ void initialize(HttpURLConnection connection); } //////////////////////////////////////////////// HTTP REQUEST METHODS private static final String HEADER_TYPE = HEADER_CONTENT_TYPE; private static final String HEADER_PARA = "Content-Disposition: form-data"; private static final String CONTENT_TYPE = "multipart/form-data"; private static final String LINE_ENDING = "\r\n"; private static final String BOUNDARY = "boundary="; private static final String PARA_NAME = "name"; private static final String FILE_NAME = "filename"; private static final int COPY_BUFFER_SIZE = 1024 * 4; private List<HTTPConnectionInitializer> configurators; private IConnectionProvider _connectionProvider; private boolean debugMode = false; private boolean throwHTTPErrorResponses = true; /** * Constructor where client provides connectionProvider. * * @param connectionProvider IConnectionProvider */ public HTTPRequest(IConnectionProvider connectionProvider) { this._connectionProvider = connectionProvider; } /** * @param connectionProvider IConnectionProvider * @param debugMode if true debug mode will be enabled, printing method calls and times. */ public HTTPRequest(IConnectionProvider connectionProvider, boolean debugMode) { this(connectionProvider); this.debugMode = debugMode; } /** * @param connectionProvider * @param debugMode if true debug mode will be enabled, printing method calls and times. */ public HTTPRequest(boolean debugMode) { this(); this.debugMode = debugMode; } /** * constructor that uses default connection provider. */ public HTTPRequest() { _connectionProvider = new DefaultConnectionProvider(); } /** * @return true if HTTP client will throw HttpExceptions on 4xx HTTP responses. */ public boolean getThrowsHTTPErrorsAsExceptions() { return throwHTTPErrorResponses; } /** * @param value if true, client will throw HttpException if 4xx codes are returned from server */ public void setThrowsHTTPErrorsAsExceptions(boolean value) { this.throwHTTPErrorResponses = value; } /** * Do an authenticated HTTP GET from url. * * @param url String URL to connect to * @return HttpURLConnection ready with response data * @throws IOException on I/O error */ public HTTPResponse get(String url) throws IOException { HttpURLConnection conn = getAndConfigureConnection(url); conn.setDoInput(true); conn.setDoOutput(false); if (debugMode) debugMessage("GET", url, conn); return connect(conn); } /** * @param url url of host * @return HttpURLConnection * @throws IOException on I/O error */ private HttpURLConnection getAndConfigureConnection(String url) throws IOException { url = guardUrl(url); HttpURLConnection connection = _connectionProvider.getConnection(url); if (configurators == null) return connection; for (HTTPConnectionInitializer c : configurators) c.initialize(connection); return connection; } /** * Do an authenticated HTTP GET from url. * * @param url String URL to connect to * @param headers Map of <String, String> of headers to process * @return HttpURLConnection ready with response data * @throws IOException on I/O error */ public HTTPResponse get(String url, Map<String, String> headers) throws IOException { HttpURLConnection conn = getAndConfigureConnection(url); conn.setDoInput(true); conn.setDoOutput(false); for (Entry<String, String> e : headers.entrySet()) conn.addRequestProperty(e.getKey(), e.getValue()); if (debugMode) debugMessage("GET", url, conn); return connect(conn); } /** * Do an HTTP POST to url. * * @param url String URL to connect to * @param data String data to post * @return HttpURLConnection ready with response data * @throws IOException on I/O error */ public HTTPResponse post(String url, String data) throws IOException { return post(url, data, null); } /** * Do an HTTP POST to url w/ extra http headers. * * @param url url of host * @param data data to pass as body of message * @param headers HTTP Headers * @return HTTPResponse * @throws IOException on I/O error */ public HTTPResponse post(String url, String data, Map<String, String> headers) throws IOException { HttpURLConnection conn = getAndConfigureConnection(url); if (headers != null) for (Entry<String, String> e : headers.entrySet()) conn.setRequestProperty(e.getKey(), e.getValue()); if (debugMode) debugMessage(METHOD_POST, url + " data: " + data, conn); conn.setDoOutput(true); OutputStreamWriter osr = new OutputStreamWriter(conn.getOutputStream()); osr.write(data); osr.flush(); osr.close(); return connect(conn); } /** * Do an HTTP POST to url. * * @param url String URL to connect to * @param stream InputStream data to post * @return HttpURLConnection ready with response data * @throws IOException on I/O error */ public HTTPResponse post(String url, InputStream stream) throws IOException { byte[] buff = streamToByteArray(stream); String data = Base64.encodeBytes(buff); return post(url, data); } /** * Posts a Map of key, value pair properties, like a web form. * * @param url url of host * @param properties map of properties to pass in post * @return HTTPResponse * @throws IOException on I/O error */ public HTTPResponse post(String url, Map<String, String> properties) throws IOException { String data = propertyString(properties); HashMap<String, String> headers = new HashMap<String, String>(); headers.put(HEADER_CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED); return post(url, data, headers); } /** * Posts a Map of key, value pair properties, like a web form. * * @param url url of host * @param properties map of properties to pass in post * @param headers HTTP headers to pass in post * @return HTTPResponse * @throws IOException on I/O error */ public HTTPResponse post(String url, Map<String, String> properties , Map<String, String> headers) throws IOException { String data = propertyString(properties); headers.put(HEADER_CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED); return post(url, data, headers); } /** * Post byte data to a url. * * @param url url of host * @param data message body as byte array * @return HTTPResponse * @throws IOException on I/O error */ public HTTPResponse post(String url, byte[] data) throws IOException { HttpURLConnection conn = getAndConfigureConnection(url); conn.setRequestProperty(HEADER_CONTENT_LENGTH, String.valueOf(data.length)); conn.setRequestMethod(METHOD_POST); conn.setDoOutput(true); if (debugMode) debugMessage(METHOD_POST, url, conn); OutputStream os = conn.getOutputStream(); os.write(data); return connect(conn); } /** * Does a multipart post which is different than a regular post * mostly use this one if you're posting files. * * @param url url of host * @param parameters Key-Value pairs in map. Keys are always string. Values can be string or IFormFile * @return HTTPResponse * @throws IOException on I/O error */ public HTTPResponse postMultipart(String url, Map<String, Object> parameters) throws IOException { HttpURLConnection conn = getAndConfigureConnection(url); conn.setRequestMethod(METHOD_POST); String boundary = createMultipartBoundary(); conn.setRequestProperty(HEADER_TYPE, CONTENT_TYPE + "; " + BOUNDARY + boundary); conn.setDoOutput(true); if (debugMode) debugMessage(METHOD_POST, url, conn); // write things out to connection OutputStream os = conn.getOutputStream(); // add parameters Object [] elems = parameters.keySet().toArray(); StringBuffer buf; // lil helper IFormFile file; for (int i = 0; i < elems.length; i++) { String key = (String) elems[i]; Object obj = parameters.get(key); //System.out.println("--" + key); buf = new StringBuffer(); if (obj instanceof IFormFile) { file = (IFormFile) obj; buf.append("--" + boundary + LINE_ENDING); buf.append(HEADER_PARA); buf.append("; " + PARA_NAME + "=\"" + key + "\""); buf.append("; " + FILE_NAME + "=\"" + file.getFilename() + "\"" + LINE_ENDING); buf.append(HEADER_TYPE + ": " + file.getContentType() + ";"); buf.append(LINE_ENDING); buf.append(LINE_ENDING); os.write(buf.toString().getBytes()); os.write(file.getBytes()); } else if (obj != null) { buf.append("--" + boundary + LINE_ENDING); buf.append(HEADER_PARA); buf.append("; " + PARA_NAME + "=\"" + key + "\""); buf.append(LINE_ENDING); buf.append(LINE_ENDING); buf.append(obj.toString()); os.write(buf.toString().getBytes()); } os.write(LINE_ENDING.getBytes()); } os.write(("--" + boundary + "--" + LINE_ENDING).getBytes()); return connect(conn); } /** * Do an HTTP PUT to url. * * @param url String URL to connect to * @param data String data to post * @return HttpURLConnection ready with response data * @throws IOException on I/O error */ public HTTPResponse put(String url, String data) throws IOException { return put(url, data, null); } /** * Do an HTTP PUT to url with extra headers. * * @param url url of host * @param data data as a string for content body * @param headers HTTP headers * @return HTTPResponse * @throws IOException on I/O error */ public HTTPResponse put(String url, String data, Map<String, String> headers) throws IOException { HttpURLConnection connection = getAndConfigureConnection(url); if (headers != null) for (Entry<String, String> e : headers.entrySet()) connection.setRequestProperty(e.getKey(), e.getValue()); if (debugMode) debugMessage("PUT", url, connection); connection.setDoOutput(true); connection.setRequestMethod("PUT"); OutputStreamWriter osr = new OutputStreamWriter(connection.getOutputStream()); osr.write(data); osr.flush(); osr.close(); return connect(connection); } /** * Do an HTTP PUT to url. * * @param url String URL to connect to * @param stream InputStream data to put * @return HttpURLConnection ready with response data * @throws IOException on I/O error */ public HTTPResponse put(String url, InputStream stream) throws IOException { byte[] buff = streamToByteArray(stream); String data = Base64.encodeBytes(buff); return put(url, data); } /** * Do an HTTP DELETE to url. * * @param url url of host * @return HTTPResponse * @throws IOException on I/O error */ public HTTPResponse delete(String url) throws IOException { HttpURLConnection connection = getAndConfigureConnection(url); connection.setDoInput(true); connection.setRequestMethod("DELETE"); if (debugMode) debugMessage("DELETE", url, connection); return connect(connection); } /** * Do an HTTP DELETE to url. * * @param url url of host * @param headers HTTP headers as <String, String> Map * @return HTTPResponse * @throws IOException on I/O error */ public HTTPResponse delete(String url, Map<String, String> headers) throws IOException { HttpURLConnection connection = getAndConfigureConnection(url); if (headers != null) for (Entry<String, String> e : headers.entrySet()) connection.setRequestProperty(e.getKey(), e.getValue()); if (debugMode) debugMessage("DELETE", url, connection); connection.setDoInput(true); connection.setRequestMethod("DELETE"); return connect(connection); } /** * Puts a Map of key, value pair properties, like a web form. * * @param url url of host * @param properties map of properties for request body * @return HTTPResponse * @throws IOException on I/O error */ public HTTPResponse put(String url, Map<String, String> properties) throws IOException { String data = propertyString(properties); HashMap<String, String> headers = new HashMap<String, String>(); headers.put(HEADER_CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED); return put(url, data, headers); } /** * Puts a Map of key, value pair properties, like a web form. * * @param url url of host * @param properties properties in request body * @param headers HTTP headers for request * @return HTTPResponse * @throws IOException on I/O error */ public HTTPResponse put(String url, Map<String, String> properties, Map<String, String> headers) throws IOException { String data = propertyString(properties); headers.put(HEADER_CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED); return put(url, data, headers); } /** * Do an HTTP HEAD to url. * * @param url String URL to connect to * @return HttpURLConnection ready with response data * @throws IOException on I/O error */ public HTTPResponse head(String url) throws IOException { HttpURLConnection connection = getAndConfigureConnection(url); connection.setDoOutput(true); connection.setRequestMethod("HEAD"); if (debugMode) debugMessage("HEAD", url, connection); return connect(connection); } ////////////////////////////////////////////////////////////// THESE HELP /** * Connect to server, check the status, and return the new HTTPResponse. * * @param connection HttpURLConnection * @return HTTPResponse * @throws IOException on I/O error */ private HTTPResponse connect(HttpURLConnection connection) throws IOException { long timestamp = 0; if (debugMode) timestamp = System.currentTimeMillis(); HTTPResponse response = new HTTPResponse(connection); if (throwHTTPErrorResponses) response.checkStatus(); if (debugMode) debugMessage(timestamp, connection.getURL().toString()); return response; } /** * Create a byte array from the contents of an input stream. * * @param in InputStream to turn into a byte array * @return byte array (byte[]) w/ contents of input stream * @throws IOException on I/O error */ public static byte[] streamToByteArray(InputStream in) throws IOException { ByteArrayOutputStream os = new ByteArrayOutputStream(); int read = 0; byte[] buff = new byte[COPY_BUFFER_SIZE]; while ((read = in.read(buff)) > 0) { os.write(buff, 0, read); } return os.toByteArray(); } /** * turns a map into a key=value property string. * * @param props * @return * @throws IOException */ public static String propertyString(Map<String, String> props) throws IOException { String propstr = new String(); String key; for (Iterator<String> i = props.keySet().iterator(); i.hasNext();) { key = i.next(); propstr = propstr + URLEncoder.encode(key, "UTF-8") + "=" + URLEncoder.encode((String) props.get(key), "UTF-8"); if (i.hasNext()) { propstr = propstr + "&"; } } return propstr; } /** * helper to create multipart form boundary * * @return */ private static String createMultipartBoundary() { StringBuffer buf = new StringBuffer(); buf.append("---------------------------"); for (int i=0; i < 15; i++) { double rand = Math.random() * 35; if (rand < 10) { buf.append((int)rand); } else { int ascii = 87 + (int)rand; char symbol = (char)ascii; buf.append(symbol); } } return buf.toString(); } /** * Add a initializer that will be called for each http operation before the call is made. * @param c */ public void addConfigurator(HTTPConnectionInitializer c) { if (configurators == null) configurators = new ArrayList<HTTPRequest.HTTPConnectionInitializer>(); if (!configurators.contains(c)) configurators.add(c); } /** * Remove a initializer. * @param c */ public void removeConfigurator(HTTPConnectionInitializer c) { if (configurators == null) return; configurators.remove(c); if (configurators.size() == 0) configurators = null; } /** * Print debug messages * @param httpMethod * @param url * @param conn */ private void debugMessage(String httpMethod, String url, HttpURLConnection conn) { System.out.println("HTTPRequest DEBUG " + System.currentTimeMillis() + ": [" + httpMethod + "] " + url + " ~ " + conn.getRequestProperties()); } /** * Print debug messages with ws time info * @param time * @param url */ private void debugMessage(long time, String url) { System.out.println("HTTPRequest DEBUG time (" + (System.currentTimeMillis() - time) + " ms): " + url); } /** * Check for null and handle protocol-less type. * @param url */ private String guardUrl(String url) { if (url == null) throw new RuntimeException("URL passed in was null."); //If no protocol defined in url, assume HTTP. if (!url.toLowerCase().trim().startsWith("http")) return "http://" + url; return url; } }