WebbUtils.java

  1. package com.goebl.david;

  2. import org.json.JSONArray;
  3. import org.json.JSONException;
  4. import org.json.JSONObject;

  5. import java.io.ByteArrayOutputStream;
  6. import java.io.File;
  7. import java.io.IOException;
  8. import java.io.InputStream;
  9. import java.io.OutputStream;
  10. import java.io.UnsupportedEncodingException;
  11. import java.net.HttpURLConnection;
  12. import java.net.URLEncoder;
  13. import java.text.DateFormat;
  14. import java.text.SimpleDateFormat;
  15. import java.util.Calendar;
  16. import java.util.Date;
  17. import java.util.Locale;
  18. import java.util.Map;
  19. import java.util.TimeZone;
  20. import java.util.zip.GZIPInputStream;
  21. import java.util.zip.GZIPOutputStream;
  22. import java.util.zip.Inflater;
  23. import java.util.zip.InflaterInputStream;

  24. /**
  25.  * Static utility method and tools for HTTP traffic parsing and encoding.
  26.  *
  27.  * @author hgoebl
  28.  */
  29. public class WebbUtils {

  30.     protected WebbUtils() {}

  31.     /**
  32.      * Convert a Map to a query string.
  33.      * @param values the map with the values
  34.      *               <code>null</code> will be encoded as empty string, all other objects are converted to
  35.      *               String by calling its <code>toString()</code> method.
  36.      * @return e.g. "key1=value&amp;key2=&amp;email=max%40example.com"
  37.      */
  38.     public static String queryString(Map<String, Object> values) {
  39.         StringBuilder sbuf = new StringBuilder();
  40.         String separator = "";

  41.         for (Map.Entry<String, Object> entry : values.entrySet()) {
  42.             Object entryValue = entry.getValue();
  43.             if (entryValue instanceof Object[]) {
  44.                 for (Object value : (Object[]) entryValue) {
  45.                     appendParam(sbuf, separator, entry.getKey(), value);
  46.                     separator = "&";
  47.                 }
  48.             } else if (entryValue instanceof Iterable) {
  49.                 for (Object multiValue : (Iterable) entryValue) {
  50.                     appendParam(sbuf, separator, entry.getKey(), multiValue);
  51.                     separator = "&";
  52.                 }
  53.             } else {
  54.                 appendParam(sbuf, separator, entry.getKey(), entryValue);
  55.                 separator = "&";
  56.             }
  57.         }

  58.         return sbuf.toString();
  59.     }

  60.     private static void appendParam(StringBuilder sbuf, String separator, String entryKey, Object value) {
  61.         String sValue = value == null ? "" : String.valueOf(value);
  62.         sbuf.append(separator);
  63.         sbuf.append(urlEncode(entryKey));
  64.         sbuf.append('=');
  65.         sbuf.append(urlEncode(sValue));
  66.     }

  67.     /**
  68.      * Convert a byte array to a JSONObject.
  69.      * @param bytes a UTF-8 encoded string representing a JSON object.
  70.      * @return the parsed object
  71.      * @throws WebbException in case of error (usually a parsing error due to invalid JSON)
  72.      */
  73.     public static JSONObject toJsonObject(byte[] bytes) {
  74.         String json;
  75.         try {
  76.             json = new String(bytes, Const.UTF8);
  77.             return new JSONObject(json);
  78.         } catch (UnsupportedEncodingException e) {
  79.             throw new WebbException(e);
  80.         } catch (JSONException e) {
  81.             throw new WebbException("payload is not a valid JSON object", e);
  82.         }
  83.     }

  84.     /**
  85.      * Convert a byte array to a JSONArray.
  86.      * @param bytes a UTF-8 encoded string representing a JSON array.
  87.      * @return the parsed JSON array
  88.      * @throws WebbException in case of error (usually a parsing error due to invalid JSON)
  89.      */
  90.     public static JSONArray toJsonArray(byte[] bytes) {
  91.         String json;
  92.         try {
  93.             json = new String(bytes, Const.UTF8);
  94.             return new JSONArray(json);
  95.         } catch (UnsupportedEncodingException e) {
  96.             throw new WebbException(e);
  97.         } catch (JSONException e) {
  98.             throw new WebbException("payload is not a valid JSON array", e);
  99.         }
  100.     }

  101.     /**
  102.      * Read an <code>InputStream</code> into <code>byte[]</code> until EOF.
  103.      * <br>
  104.      * Does not close the InputStream!
  105.      *
  106.      * @param is the stream to read the bytes from
  107.      * @return all read bytes as an array
  108.      * @throws IOException when read or write operation fails
  109.      */
  110.     public static byte[] readBytes(InputStream is) throws IOException {
  111.         if (is == null) {
  112.             return null;
  113.         }
  114.         ByteArrayOutputStream baos = new ByteArrayOutputStream();
  115.         copyStream(is, baos);
  116.         return baos.toByteArray();
  117.     }

  118.     /**
  119.      * Copy complete content of <code>InputStream</code> to <code>OutputStream</code> until EOF.
  120.      * <br>
  121.      * Does not close the InputStream nor OutputStream!
  122.      *
  123.      * @param input the stream to read the bytes from
  124.      * @param output the stream to write the bytes to
  125.      * @throws IOException when read or write operation fails
  126.      */
  127.     public static void copyStream(InputStream input, OutputStream output) throws IOException {
  128.         byte[] buffer = new byte[1024];
  129.         int count;
  130.         while ((count = input.read(buffer)) != -1) {
  131.             output.write(buffer, 0, count);
  132.         }
  133.     }

  134.     /**
  135.      * Creates a new instance of a <code>DateFormat</code> for RFC1123 compliant dates.
  136.      * <br>
  137.      * Should be stored for later use but be aware that this DateFormat is not Thread-safe!
  138.      * <br>
  139.      * If you have to deal with dates in this format with JavaScript, it's easy, because the JavaScript
  140.      * Date object has a constructor for strings formatted this way.
  141.      * @return a new instance
  142.      */
  143.     public static DateFormat getRfc1123DateFormat() {
  144.         DateFormat format = new SimpleDateFormat(
  145.                 "EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH);
  146.         format.setLenient(false);
  147.         format.setTimeZone(TimeZone.getTimeZone("UTC"));
  148.         return format;
  149.     }

  150.     static String urlEncode(String value) {
  151.         try {
  152.             return URLEncoder.encode(value, "UTF-8");
  153.         } catch (UnsupportedEncodingException e) {
  154.             return value;
  155.         }
  156.     }

  157.     static void addRequestProperties(HttpURLConnection connection, Map<String, Object> map) {
  158.         if (map == null || map.isEmpty()) {
  159.             return;
  160.         }
  161.         for (Map.Entry<String, Object> entry : map.entrySet()) {
  162.             addRequestProperty(connection, entry.getKey(), entry.getValue());
  163.         }
  164.     }

  165.     static void addRequestProperty(HttpURLConnection connection, String name, Object value) {
  166.         if (name == null || name.length() == 0 || value == null) {
  167.             throw new IllegalArgumentException("name and value must not be empty");
  168.         }

  169.         String valueAsString;
  170.         if (value instanceof Date) {
  171.             valueAsString = getRfc1123DateFormat().format((Date) value);
  172.         } else if (value instanceof Calendar) {
  173.             valueAsString = getRfc1123DateFormat().format(((Calendar) value).getTime());
  174.         } else {
  175.             valueAsString = value.toString();
  176.         }

  177.         connection.addRequestProperty(name, valueAsString);
  178.     }

  179.     static void ensureRequestProperty(HttpURLConnection connection, String name, Object value) {
  180.         if (!connection.getRequestProperties().containsKey(name)) {
  181.             addRequestProperty(connection, name, value);
  182.         }
  183.     }

  184.     static byte[] getPayloadAsBytesAndSetContentType(
  185.             HttpURLConnection connection,
  186.             Request request,
  187.             boolean compress,
  188.             int jsonIndentFactor) throws JSONException, UnsupportedEncodingException {

  189.         byte[] requestBody = null;
  190.         String bodyStr = null;

  191.         if (request.params != null) {
  192.             WebbUtils.ensureRequestProperty(connection, Const.HDR_CONTENT_TYPE, Const.APP_FORM);
  193.             bodyStr = WebbUtils.queryString(request.params);
  194.         } else if (request.payload == null) {
  195.             return null;
  196.         } else if (request.payload instanceof JSONObject) {
  197.             WebbUtils.ensureRequestProperty(connection, Const.HDR_CONTENT_TYPE, Const.APP_JSON);
  198.             bodyStr = jsonIndentFactor >= 0
  199.                     ? ((JSONObject) request.payload).toString(jsonIndentFactor)
  200.                     : request.payload.toString();
  201.         } else if (request.payload instanceof JSONArray) {
  202.             WebbUtils.ensureRequestProperty(connection, Const.HDR_CONTENT_TYPE, Const.APP_JSON);
  203.             bodyStr = jsonIndentFactor >= 0
  204.                     ? ((JSONArray) request.payload).toString(jsonIndentFactor)
  205.                     : request.payload.toString();
  206.         } else if (request.payload instanceof byte[]) {
  207.             WebbUtils.ensureRequestProperty(connection, Const.HDR_CONTENT_TYPE, Const.APP_BINARY);
  208.             requestBody = (byte[]) request.payload;
  209.         } else {
  210.             WebbUtils.ensureRequestProperty(connection, Const.HDR_CONTENT_TYPE, Const.TEXT_PLAIN);
  211.             bodyStr = request.payload.toString();
  212.         }
  213.         if (bodyStr != null) {
  214.             requestBody = bodyStr.getBytes(Const.UTF8);
  215.         }

  216.         if (requestBody == null) {
  217.             throw new IllegalStateException();
  218.         }

  219.         // only compress if the new body is smaller than uncompressed body
  220.         if (compress && requestBody.length > Const.MIN_COMPRESSED_ADVANTAGE) {
  221.             byte[] compressedBody = gzip(requestBody);
  222.             if (requestBody.length - compressedBody.length > Const.MIN_COMPRESSED_ADVANTAGE) {
  223.                 requestBody = compressedBody;
  224.                 connection.setRequestProperty(Const.HDR_CONTENT_ENCODING, "gzip");
  225.             }
  226.         }

  227.         connection.setFixedLengthStreamingMode(requestBody.length);

  228.         return requestBody;
  229.     }

  230.     static void setContentTypeAndLengthForStreaming(
  231.             HttpURLConnection connection,
  232.             Request request,
  233.             boolean compress) {

  234.         long length;

  235.         if (request.payload instanceof File) {
  236.             length = compress ? -1L : ((File) request.payload).length();
  237.         } else if (request.payload instanceof InputStream) {
  238.             length = -1L;
  239.         } else {
  240.             throw new IllegalStateException();
  241.         }

  242.         if (length > Integer.MAX_VALUE) {
  243.             length = -1L; // use chunked streaming mode
  244.         }

  245.         WebbUtils.ensureRequestProperty(connection, Const.HDR_CONTENT_TYPE, Const.APP_BINARY);
  246.         if (length < 0) {
  247.             connection.setChunkedStreamingMode(-1); // use default chunk size
  248.             if (compress) {
  249.                 connection.setRequestProperty(Const.HDR_CONTENT_ENCODING, "gzip");
  250.             }
  251.         } else {
  252.             connection.setFixedLengthStreamingMode((int) length);
  253.         }
  254.     }

  255.     static byte[] gzip(byte[] input) {
  256.         GZIPOutputStream gzipOS = null;
  257.         try {
  258.             ByteArrayOutputStream byteArrayOS = new ByteArrayOutputStream();
  259.             gzipOS = new GZIPOutputStream(byteArrayOS);
  260.             gzipOS.write(input);
  261.             gzipOS.flush();
  262.             gzipOS.close();
  263.             gzipOS = null;
  264.             return byteArrayOS.toByteArray();
  265.         } catch (Exception e) {
  266.             throw new WebbException(e);
  267.         } finally {
  268.             if (gzipOS != null) {
  269.                 try { gzipOS.close(); } catch (Exception ignored) {}
  270.             }
  271.         }
  272.     }

  273.     static InputStream wrapStream(String contentEncoding, InputStream inputStream) throws IOException {
  274.         if (contentEncoding == null || "identity".equalsIgnoreCase(contentEncoding)) {
  275.             return inputStream;
  276.         }
  277.         if ("gzip".equalsIgnoreCase(contentEncoding)) {
  278.             return new GZIPInputStream(inputStream);
  279.         }
  280.         if ("deflate".equalsIgnoreCase(contentEncoding)) {
  281.             return new InflaterInputStream(inputStream, new Inflater(false), 512);
  282.         }
  283.         throw new WebbException("unsupported content-encoding: " + contentEncoding);
  284.     }

  285.     static <T> void parseResponseBody(Class<T> clazz, Response<T> response, InputStream responseBodyStream)
  286.             throws UnsupportedEncodingException, IOException {

  287.         if (responseBodyStream == null || clazz == Void.class) {
  288.             return;
  289.         } else if (clazz == InputStream.class) {
  290.             response.setBody(responseBodyStream);
  291.             return;
  292.         }

  293.         byte[] responseBody = WebbUtils.readBytes(responseBodyStream);
  294.         // we are ignoring headers describing the content type of the response, instead
  295.         // try to force the content based on the type the client is expecting it (clazz)
  296.         if (clazz == String.class) {
  297.             response.setBody(new String(responseBody, Const.UTF8));
  298.         } else if (clazz == Const.BYTE_ARRAY_CLASS) {
  299.             response.setBody(responseBody);
  300.         } else if (clazz == JSONObject.class) {
  301.             response.setBody(WebbUtils.toJsonObject(responseBody));
  302.         } else if (clazz == JSONArray.class) {
  303.             response.setBody(WebbUtils.toJsonArray(responseBody));
  304.         }
  305.     }

  306.     static <T> void parseErrorResponse(Class<T> clazz, Response<T> response, InputStream responseBodyStream)
  307.             throws UnsupportedEncodingException, IOException {

  308.         if (responseBodyStream == null) {
  309.             return;
  310.         } else if (clazz == InputStream.class) {
  311.             response.errorBody = responseBodyStream;
  312.             return;
  313.         }

  314.         byte[] responseBody = WebbUtils.readBytes(responseBodyStream);
  315.         String contentType = response.connection.getContentType();
  316.         if (contentType == null || contentType.startsWith(Const.APP_BINARY) || clazz == Const.BYTE_ARRAY_CLASS) {
  317.             response.errorBody = responseBody;
  318.             return;
  319.         }

  320.         if (contentType.startsWith(Const.APP_JSON) && clazz == JSONObject.class) {
  321.             try {
  322.                 response.errorBody = WebbUtils.toJsonObject(responseBody);
  323.                 return;
  324.             } catch (Exception ignored) {
  325.                 // ignored - was just a try!
  326.             }
  327.         }

  328.         // fallback to String if bytes are valid UTF-8 characters ...
  329.         try {
  330.             response.errorBody = new String(responseBody, Const.UTF8);
  331.             return;
  332.         } catch (Exception ignored) {
  333.             // ignored - was just a try!
  334.         }

  335.         // last fallback - return error object as byte[]
  336.         response.errorBody = responseBody;
  337.     }
  338. }