diff --git a/plugin.xml b/plugin.xml
index cfeff2b..e027bf0 100644
--- a/plugin.xml
+++ b/plugin.xml
@@ -56,19 +56,23 @@
+
-
-
-
+
+
+
+
+
@@ -81,4 +85,4 @@
-
\ No newline at end of file
+
diff --git a/src/android/com/github/kevinsawicki/http/HttpRequest.java b/src/android/com/github/kevinsawicki/http/HttpRequest.java
index 237f236..935c972 100644
--- a/src/android/com/github/kevinsawicki/http/HttpRequest.java
+++ b/src/android/com/github/kevinsawicki/http/HttpRequest.java
@@ -88,6 +88,7 @@ import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
@@ -364,7 +365,7 @@ public class HttpRequest {
throws HttpRequestException {
try {
SSLContext context = SSLContext.getInstance("TLS");
- context.init(null, trustManagers, new SecureRandom());
+ context.init(keyManagers, trustManagers, new SecureRandom());
if (android.os.Build.VERSION.SDK_INT < 20) {
return new TLSSocketFactory(context);
diff --git a/src/android/com/github/kevinsawicki/http/OkConnectionFactory.java b/src/android/com/github/kevinsawicki/http/OkConnectionFactory.java
deleted file mode 100644
index fb529a6..0000000
--- a/src/android/com/github/kevinsawicki/http/OkConnectionFactory.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.github.kevinsawicki.http;
-
-import okhttp3.OkUrlFactory;
-import okhttp3.OkHttpClient;
-
-import java.net.URL;
-import java.net.HttpURLConnection;
-import java.net.URLStreamHandler;
-import java.net.Proxy;
-
-
-public class OkConnectionFactory implements HttpRequest.ConnectionFactory {
-
- protected OkHttpClient okHttpClient = new OkHttpClient();
-
- public HttpURLConnection create(URL url) {
- OkUrlFactory okUrlFactory = new OkUrlFactory(okHttpClient);
- return (HttpURLConnection) okUrlFactory.open(url);
- }
-
- public HttpURLConnection create(URL url, Proxy proxy) {
- OkHttpClient okHttpClientWithProxy = okHttpClient.newBuilder().proxy(proxy).build();
- OkUrlFactory okUrlFactory = new OkUrlFactory(okHttpClientWithProxy);
- return (HttpURLConnection) okUrlFactory.open(url);
- }
-}
\ No newline at end of file
diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpRequest.java b/src/android/com/silkimen/cordovahttp/CordovaHttpRequest.java
new file mode 100644
index 0000000..a8fe13f
--- /dev/null
+++ b/src/android/com/silkimen/cordovahttp/CordovaHttpRequest.java
@@ -0,0 +1,154 @@
+package com.silkimen.cordovahttp;
+
+import java.io.ByteArrayOutputStream;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+
+import javax.net.ssl.SSLHandshakeException;
+
+import com.silkimen.http.HttpBodyDecoder;
+import com.silkimen.http.HttpRequest;
+import com.silkimen.http.HttpRequest.HttpRequestException;
+import com.silkimen.http.HttpResponse;
+import com.silkimen.http.JsonUtils;
+
+import org.apache.cordova.CallbackContext;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.util.Log;
+
+public class CordovaHttpRequest implements Runnable {
+ private String method;
+ private String url;
+ private String serializer = "none";
+ private Object data;
+ private JSONObject params;
+ private JSONObject headers;
+ private int timeout;
+ private CallbackContext callbackContext;
+
+ public CordovaHttpRequest(String method, String url, String serializer, Object data, JSONObject headers,
+ int timeout, CallbackContext callbackContext) {
+
+ this.method = method;
+ this.url = url;
+ this.serializer = serializer;
+ this.data = data;
+ this.headers = headers;
+ this.timeout = timeout;
+ this.callbackContext = callbackContext;
+ }
+
+ public CordovaHttpRequest(String method, String url, JSONObject params, JSONObject headers, int timeout,
+ CallbackContext callbackContext) {
+
+ this.method = method;
+ this.url = url;
+ this.params = params;
+ this.headers = headers;
+ this.timeout = timeout;
+ this.callbackContext = callbackContext;
+ }
+
+ @Override
+ public void run() {
+ HttpResponse response = new HttpResponse();
+
+ try {
+ String processedUrl = HttpRequest.encode(HttpRequest.append(this.url, JsonUtils.getObjectMap(this.params)));
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ HttpRequest request = new HttpRequest(processedUrl, this.method)
+ .followRedirects(true /* @TODO */)
+ .readTimeout(this.timeout)
+ .acceptCharset("UTF-8")
+ .uncompress(true);
+
+ // setup content type before applying headers, so user can override it
+ this.setContentType(request)
+ .headers(JsonUtils.getStringMap(this.headers));
+
+ this.sendBody(request)
+ .receive(outputStream);
+
+ ByteBuffer rawOutput = ByteBuffer.wrap(outputStream.toByteArray());
+ String decodedBody = HttpBodyDecoder.decodeBody(rawOutput, request.charset());
+
+ response.setStatus(request.code());
+ response.setUrl(request.url().toString());
+ response.setHeaders(request.headers());
+
+ if (request.code() >= 200 && request.code() < 300) {
+ response.setBody(decodedBody);
+ this.callbackContext.success(response.toJSON());
+ } else {
+ response.setErrorMessage(decodedBody);
+ this.callbackContext.error(response.toJSON());
+ }
+ } catch (HttpRequestException e) {
+ if (e.getCause() instanceof SSLHandshakeException) {
+ response.setStatus(-2);
+ response.setErrorMessage("SSL handshake failed: " + e.getMessage());
+ Log.w("Cordova-Plugin-HTTP", "SSL handshake failed", e);
+ } else if (e.getCause() instanceof UnknownHostException) {
+ response.setStatus(-3);
+ response.setErrorMessage("Host could not be resolved: " + e.getMessage());
+ Log.w("Cordova-Plugin-HTTP", "Host could not be resolved", e);
+ } else if (e.getCause() instanceof SocketTimeoutException) {
+ response.setStatus(-4);
+ response.setErrorMessage("Request timed out: " + e.getMessage());
+ Log.w("Cordova-Plugin-HTTP", "Request timed out", e);
+ } else {
+ response.setStatus(-1);
+ response.setErrorMessage("There was an error with the request: " + e.getCause().getMessage());
+ Log.w("Cordova-Plugin-HTTP", "Generic request error", e);
+ }
+ } catch (Exception e) {
+ response.setStatus(-1);
+ response.setErrorMessage(e.getMessage());
+ Log.e("Cordova-Plugin-HTTP", "An unexpected error occured", e);
+ }
+
+ try {
+ if (response.hasFailed()) {
+ this.callbackContext.error(response.toJSON());
+ } else {
+ this.callbackContext.success(response.toJSON());
+ }
+ } catch (JSONException e) {
+ Log.e("Cordova-Plugin-HTTP", "An unexpected error occured while processing HTTP response", e);
+ }
+ }
+
+ private HttpRequest setContentType(HttpRequest request) {
+ switch(this.serializer) {
+ case "json":
+ return request.contentType("application/json", "UTF-8");
+ case "utf8":
+ return request.contentType("text/plain", "UTF-8");
+ case "urlencoded":
+ // intentionally left blank, because content type is set in HttpRequest.form()
+ }
+
+ return request;
+ }
+
+ private HttpRequest sendBody(HttpRequest request) throws JSONException {
+ if (this.data == null) {
+ return request;
+ }
+
+ switch (this.serializer) {
+ case "json":
+ return request.send(this.data.toString());
+ case "utf8":
+ return request.send(((JSONObject) this.data).getString("text"));
+ case "urlencoded":
+ return request.form(JsonUtils.getObjectMap((JSONObject) this.data));
+ }
+
+ return request;
+ }
+}
diff --git a/src/android/com/silkimen/http/HostnameVerifierFactory.java b/src/android/com/silkimen/http/HostnameVerifierFactory.java
new file mode 100644
index 0000000..8bc4fdc
--- /dev/null
+++ b/src/android/com/silkimen/http/HostnameVerifierFactory.java
@@ -0,0 +1,19 @@
+package com.silkimen.http;
+
+import javax.net.ssl.HostnameVerifier;
+
+public class HostnameVerfifierFactory {
+ private final HostnameVerifier noOpVerififer;
+
+ public HostnameVerifierFactory() {
+ this.noOpVerififer = new HostnameVerifier() {
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ };
+ }
+
+ public HostnameVerifier getNoOpVerifier() {
+ return this.noOpVerififer;
+ }
+}
diff --git a/src/android/com/silkimen/http/HttpBodyDecoder.java b/src/android/com/silkimen/http/HttpBodyDecoder.java
new file mode 100644
index 0000000..7aeddc3
--- /dev/null
+++ b/src/android/com/silkimen/http/HttpBodyDecoder.java
@@ -0,0 +1,48 @@
+package com.silkimen.http;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.MalformedInputException;
+
+public class HttpBodyDecoder {
+ private static final String[] ACCEPTED_CHARSETS = new String[] { "UTF-8", "ISO-8859-1" };
+
+ public static String decodeBody(ByteBuffer rawOutput, String charsetName)
+ throws CharacterCodingException, MalformedInputException {
+
+ if (charsetName == null) {
+ return tryDecodeByteBuffer(rawOutput);
+ }
+
+ return decodeByteBuffer(rawOutput, charsetName);
+ }
+
+ private static String tryDecodeByteBuffer(ByteBuffer rawOutput) throws CharacterCodingException, MalformedInputException {
+
+ for (int i = 0; i < ACCEPTED_CHARSETS.length - 1; i++) {
+ try {
+ return decodeByteBuffer(rawOutput, ACCEPTED_CHARSETS[i]);
+ } catch (MalformedInputException e) {
+ continue;
+ } catch (CharacterCodingException e) {
+ continue;
+ }
+ }
+
+ return decodeBody(rawOutput, ACCEPTED_CHARSETS[ACCEPTED_CHARSETS.length - 1]);
+ }
+
+ private static String decodeByteBuffer(ByteBuffer rawOutput, String charsetName)
+ throws CharacterCodingException, MalformedInputException {
+
+ return createCharsetDecoder(charsetName).decode(rawOutput).toString();
+ }
+
+ private static CharsetDecoder createCharsetDecoder(String charsetName) {
+ return Charset.forName(charsetName).newDecoder().onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT);
+ }
+}
diff --git a/src/android/com/silkimen/http/HttpRequest.java b/src/android/com/silkimen/http/HttpRequest.java
new file mode 100644
index 0000000..7e638bb
--- /dev/null
+++ b/src/android/com/silkimen/http/HttpRequest.java
@@ -0,0 +1,3095 @@
+/*
+ * Copyright (c) 2014 Kevin Sawicki
+ * modified by contributors of cordova-plugin-advanced-http
+ *
+ * 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.silkimen.http;
+
+import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
+import static java.net.HttpURLConnection.HTTP_CREATED;
+import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
+import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.Proxy.Type.HTTP;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.Flushable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.security.AccessController;
+import java.security.GeneralSecurityException;
+import java.security.PrivilegedAction;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.zip.GZIPInputStream;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * A fluid interface for making HTTP requests using an underlying
+ * {@link HttpURLConnection} (or sub-class).
+ *
+ * Each instance supports making a single request and cannot be reused for
+ * further requests.
+ */
+public class HttpRequest {
+
+ /**
+ * 'UTF-8' charset name
+ */
+ public static final String CHARSET_UTF8 = "UTF-8";
+
+ /**
+ * 'application/x-www-form-urlencoded' content type header value
+ */
+ public static final String CONTENT_TYPE_FORM = "application/x-www-form-urlencoded";
+
+ /**
+ * 'application/json' content type header value
+ */
+ public static final String CONTENT_TYPE_JSON = "application/json";
+
+ /**
+ * 'gzip' encoding header value
+ */
+ public static final String ENCODING_GZIP = "gzip";
+
+ /**
+ * 'Accept' header name
+ */
+ public static final String HEADER_ACCEPT = "Accept";
+
+ /**
+ * 'Accept-Charset' header name
+ */
+ public static final String HEADER_ACCEPT_CHARSET = "Accept-Charset";
+
+ /**
+ * 'Accept-Encoding' header name
+ */
+ public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
+
+ /**
+ * 'Authorization' header name
+ */
+ public static final String HEADER_AUTHORIZATION = "Authorization";
+
+ /**
+ * 'Cache-Control' header name
+ */
+ public static final String HEADER_CACHE_CONTROL = "Cache-Control";
+
+ /**
+ * 'Content-Encoding' header name
+ */
+ public static final String HEADER_CONTENT_ENCODING = "Content-Encoding";
+
+ /**
+ * 'Content-Length' header name
+ */
+ public static final String HEADER_CONTENT_LENGTH = "Content-Length";
+
+ /**
+ * 'Content-Type' header name
+ */
+ public static final String HEADER_CONTENT_TYPE = "Content-Type";
+
+ /**
+ * 'Date' header name
+ */
+ public static final String HEADER_DATE = "Date";
+
+ /**
+ * 'ETag' header name
+ */
+ public static final String HEADER_ETAG = "ETag";
+
+ /**
+ * 'Expires' header name
+ */
+ public static final String HEADER_EXPIRES = "Expires";
+
+ /**
+ * 'If-None-Match' header name
+ */
+ public static final String HEADER_IF_NONE_MATCH = "If-None-Match";
+
+ /**
+ * 'Last-Modified' header name
+ */
+ public static final String HEADER_LAST_MODIFIED = "Last-Modified";
+
+ /**
+ * 'Location' header name
+ */
+ public static final String HEADER_LOCATION = "Location";
+
+ /**
+ * 'Proxy-Authorization' header name
+ */
+ public static final String HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization";
+
+ /**
+ * 'Referer' header name
+ */
+ public static final String HEADER_REFERER = "Referer";
+
+ /**
+ * 'Server' header name
+ */
+ public static final String HEADER_SERVER = "Server";
+
+ /**
+ * 'User-Agent' header name
+ */
+ public static final String HEADER_USER_AGENT = "User-Agent";
+
+ /**
+ * 'DELETE' request method
+ */
+ public static final String METHOD_DELETE = "DELETE";
+
+ /**
+ * 'GET' request method
+ */
+ public static final String METHOD_GET = "GET";
+
+ /**
+ * 'HEAD' request method
+ */
+ public static final String METHOD_HEAD = "HEAD";
+
+ /**
+ * 'OPTIONS' options method
+ */
+ public static final String METHOD_OPTIONS = "OPTIONS";
+
+ /**
+ * 'POST' request method
+ */
+ public static final String METHOD_POST = "POST";
+
+ /**
+ * 'PUT' request method
+ */
+ public static final String METHOD_PUT = "PUT";
+
+ /**
+ * 'TRACE' request method
+ */
+ public static final String METHOD_TRACE = "TRACE";
+
+ /**
+ * 'charset' header value parameter
+ */
+ public static final String PARAM_CHARSET = "charset";
+
+ private static final String BOUNDARY = "00content0boundary00";
+
+ private static final String CONTENT_TYPE_MULTIPART = "multipart/form-data; boundary=" + BOUNDARY;
+
+ private static final String CRLF = "\r\n";
+
+ private static final String[] EMPTY_STRINGS = new String[0];
+
+ private static String getValidCharset(final String charset) {
+ if (charset != null && charset.length() > 0)
+ return charset;
+ else
+ return CHARSET_UTF8;
+ }
+
+ private static StringBuilder addPathSeparator(final String baseUrl, final StringBuilder result) {
+ // Add trailing slash if the base URL doesn't have any path segments.
+ //
+ // The following test is checking for the last slash not being part of
+ // the protocol to host separator: '://'.
+ if (baseUrl.indexOf(':') + 2 == baseUrl.lastIndexOf('/'))
+ result.append('/');
+ return result;
+ }
+
+ private static StringBuilder addParamPrefix(final String baseUrl, final StringBuilder result) {
+ // Add '?' if missing and add '&' if params already exist in base url
+ final int queryStart = baseUrl.indexOf('?');
+ final int lastChar = result.length() - 1;
+ if (queryStart == -1)
+ result.append('?');
+ else if (queryStart < lastChar && baseUrl.charAt(lastChar) != '&')
+ result.append('&');
+ return result;
+ }
+
+ private static StringBuilder addParam(final Object key, Object value, final StringBuilder result) {
+ if (value != null && value.getClass().isArray())
+ value = arrayToList(value);
+
+ if (value instanceof Iterable>) {
+ Iterator> iterator = ((Iterable>) value).iterator();
+ while (iterator.hasNext()) {
+ result.append(key);
+ result.append("[]=");
+ Object element = iterator.next();
+ if (element != null)
+ result.append(element);
+ if (iterator.hasNext())
+ result.append("&");
+ }
+ } else {
+ result.append(key);
+ result.append("=");
+ if (value != null)
+ result.append(value);
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates {@link HttpURLConnection HTTP connections} for {@link URL urls}.
+ */
+ public interface ConnectionFactory {
+ /**
+ * Open an {@link HttpURLConnection} for the specified {@link URL}.
+ *
+ * @throws IOException
+ */
+ HttpURLConnection create(URL url) throws IOException;
+
+ /**
+ * Open an {@link HttpURLConnection} for the specified {@link URL} and
+ * {@link Proxy}.
+ *
+ * @throws IOException
+ */
+ HttpURLConnection create(URL url, Proxy proxy) throws IOException;
+
+ /**
+ * A {@link ConnectionFactory} which uses the built-in
+ * {@link URL#openConnection()}
+ */
+ ConnectionFactory DEFAULT = new ConnectionFactory() {
+ public HttpURLConnection create(URL url) throws IOException {
+ return (HttpURLConnection) url.openConnection();
+ }
+
+ public HttpURLConnection create(URL url, Proxy proxy) throws IOException {
+ return (HttpURLConnection) url.openConnection(proxy);
+ }
+ };
+ }
+
+ private static ConnectionFactory CONNECTION_FACTORY = ConnectionFactory.DEFAULT;
+
+ /**
+ * Specify the {@link ConnectionFactory} used to create new requests.
+ */
+ public static void setConnectionFactory(final ConnectionFactory connectionFactory) {
+ if (connectionFactory == null)
+ CONNECTION_FACTORY = ConnectionFactory.DEFAULT;
+ else
+ CONNECTION_FACTORY = connectionFactory;
+ }
+
+ /**
+ * Callback interface for reporting upload progress for a request.
+ */
+ public interface UploadProgress {
+ /**
+ * Callback invoked as data is uploaded by the request.
+ *
+ * @param uploaded The number of bytes already uploaded
+ * @param total The total number of bytes that will be uploaded or -1 if the
+ * length is unknown.
+ */
+ void onUpload(long uploaded, long total);
+
+ UploadProgress DEFAULT = new UploadProgress() {
+ public void onUpload(long uploaded, long total) {
+ }
+ };
+ }
+
+ /**
+ *
+ * Encodes and decodes to and from Base64 notation.
+ *
+ *
+ * I am placing this code in the Public Domain. Do with it as you will. This
+ * software comes with no guarantees or warranties but with plenty of
+ * well-wishing instead! Please visit
+ * http://iharder.net/base64
+ * periodically to check for updates or to contribute improvements.
+ *
+ * Encodes up to three bytes of the array source and writes the
+ * resulting four Base64 bytes to destination. The source and
+ * destination arrays can be manipulated anywhere along their length by
+ * specifying srcOffset and destOffset. This method does
+ * not check to make sure your arrays are large enough to accomodate
+ * srcOffset + 3 for the source array or
+ * destOffset + 4 for the destination array. The actual
+ * number of significant bytes in your array is given by numSigBytes.
+ *
+ *
+ * This is the lowest level of the encoding methods with all possible
+ * parameters.
+ *
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param numSigBytes the number of significant bytes in your array
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @return the destination array
+ * @since 1.3
+ */
+ private static byte[] encode3to4(byte[] source, int srcOffset, int numSigBytes, byte[] destination,
+ int destOffset) {
+
+ byte[] ALPHABET = _STANDARD_ALPHABET;
+
+ int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
+ | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
+ | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
+
+ switch (numSigBytes) {
+ case 3:
+ destination[destOffset] = ALPHABET[(inBuff >>> 18)];
+ destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f];
+ return destination;
+
+ case 2:
+ destination[destOffset] = ALPHABET[(inBuff >>> 18)];
+ destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+
+ case 1:
+ destination[destOffset] = ALPHABET[(inBuff >>> 18)];
+ destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = EQUALS_SIGN;
+ destination[destOffset + 3] = EQUALS_SIGN;
+ return destination;
+
+ default:
+ return destination;
+ }
+ }
+
+ /**
+ * Encode string as a byte array in Base64 annotation.
+ *
+ * @param string
+ * @return The Base64-encoded data as a string
+ */
+ public static String encode(String string) {
+ byte[] bytes;
+ try {
+ bytes = string.getBytes(PREFERRED_ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ bytes = string.getBytes();
+ }
+ return encodeBytes(bytes);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source The data to convert
+ * @return The Base64-encoded data as a String
+ * @throws NullPointerException if source array is null
+ * @throws IllegalArgumentException if source array, offset, or length are
+ * invalid
+ * @since 2.0
+ */
+ public static String encodeBytes(byte[] source) {
+ return encodeBytes(source, 0, source.length);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @return The Base64-encoded data as a String
+ * @throws NullPointerException if source array is null
+ * @throws IllegalArgumentException if source array, offset, or length are
+ * invalid
+ * @since 2.0
+ */
+ public static String encodeBytes(byte[] source, int off, int len) {
+ byte[] encoded = encodeBytesToBytes(source, off, len);
+ try {
+ return new String(encoded, PREFERRED_ENCODING);
+ } catch (UnsupportedEncodingException uue) {
+ return new String(encoded);
+ }
+ }
+
+ /**
+ * Similar to {@link #encodeBytes(byte[], int, int)} but returns a byte array
+ * instead of instantiating a String. This is more efficient if you're working
+ * with I/O streams and have large data sets to encode.
+ *
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @return The Base64-encoded data as a String if there is an error
+ * @throws NullPointerException if source array is null
+ * @throws IllegalArgumentException if source array, offset, or length are
+ * invalid
+ * @since 2.3.1
+ */
+ public static byte[] encodeBytesToBytes(byte[] source, int off, int len) {
+
+ if (source == null)
+ throw new NullPointerException("Cannot serialize a null array.");
+
+ if (off < 0)
+ throw new IllegalArgumentException("Cannot have negative offset: " + off);
+
+ if (len < 0)
+ throw new IllegalArgumentException("Cannot have length offset: " + len);
+
+ if (off + len > source.length)
+ throw new IllegalArgumentException(String
+ .format("Cannot have offset of %d and length of %d with array of length %d", off, len, source.length));
+
+ // Bytes needed for actual encoding
+ int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0);
+
+ byte[] outBuff = new byte[encLen];
+
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ for (; d < len2; d += 3, e += 4)
+ encode3to4(source, d + off, 3, outBuff, e);
+
+ if (d < len) {
+ encode3to4(source, d + off, len - d, outBuff, e);
+ e += 4;
+ }
+
+ if (e <= outBuff.length - 1) {
+ byte[] finalOut = new byte[e];
+ System.arraycopy(outBuff, 0, finalOut, 0, e);
+ return finalOut;
+ } else
+ return outBuff;
+ }
+ }
+
+ /**
+ * HTTP request exception whose cause is always an {@link IOException}
+ */
+ public static class HttpRequestException extends RuntimeException {
+
+ private static final long serialVersionUID = -1170466989781746231L;
+
+ /**
+ * Create a new HttpRequestException with the given cause
+ *
+ * @param cause
+ */
+ public HttpRequestException(final IOException cause) {
+ super(cause);
+ }
+
+ /**
+ * Get {@link IOException} that triggered this request exception
+ *
+ * @return {@link IOException} cause
+ */
+ @Override
+ public IOException getCause() {
+ return (IOException) super.getCause();
+ }
+ }
+
+ /**
+ * Operation that handles executing a callback once complete and handling nested
+ * exceptions
+ *
+ * @param
+ */
+ protected static abstract class Operation implements Callable {
+
+ /**
+ * Run operation
+ *
+ * @return result
+ * @throws HttpRequestException
+ * @throws IOException
+ */
+ protected abstract V run() throws HttpRequestException, IOException;
+
+ /**
+ * Operation complete callback
+ *
+ * @throws IOException
+ */
+ protected abstract void done() throws IOException;
+
+ public V call() throws HttpRequestException {
+ boolean thrown = false;
+ try {
+ return run();
+ } catch (HttpRequestException e) {
+ thrown = true;
+ throw e;
+ } catch (IOException e) {
+ thrown = true;
+ throw new HttpRequestException(e);
+ } finally {
+ try {
+ done();
+ } catch (IOException e) {
+ if (!thrown)
+ throw new HttpRequestException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Class that ensures a {@link Closeable} gets closed with proper exception
+ * handling.
+ *
+ * @param
+ */
+ protected static abstract class CloseOperation extends Operation {
+
+ private final Closeable closeable;
+
+ private final boolean ignoreCloseExceptions;
+
+ /**
+ * Create closer for operation
+ *
+ * @param closeable
+ * @param ignoreCloseExceptions
+ */
+ protected CloseOperation(final Closeable closeable, final boolean ignoreCloseExceptions) {
+ this.closeable = closeable;
+ this.ignoreCloseExceptions = ignoreCloseExceptions;
+ }
+
+ @Override
+ protected void done() throws IOException {
+ if (closeable instanceof Flushable)
+ ((Flushable) closeable).flush();
+ if (ignoreCloseExceptions)
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ // Ignored
+ }
+ else
+ closeable.close();
+ }
+ }
+
+ /**
+ * Class that and ensures a {@link Flushable} gets flushed with proper exception
+ * handling.
+ *
+ * @param
+ */
+ protected static abstract class FlushOperation extends Operation {
+
+ private final Flushable flushable;
+
+ /**
+ * Create flush operation
+ *
+ * @param flushable
+ */
+ protected FlushOperation(final Flushable flushable) {
+ this.flushable = flushable;
+ }
+
+ @Override
+ protected void done() throws IOException {
+ flushable.flush();
+ }
+ }
+
+ /**
+ * Request output stream
+ */
+ public static class RequestOutputStream extends BufferedOutputStream {
+
+ private final CharsetEncoder encoder;
+
+ /**
+ * Create request output stream
+ *
+ * @param stream
+ * @param charset
+ * @param bufferSize
+ */
+ public RequestOutputStream(final OutputStream stream, final String charset, final int bufferSize) {
+ super(stream, bufferSize);
+
+ encoder = Charset.forName(getValidCharset(charset)).newEncoder();
+ }
+
+ /**
+ * Write string to stream
+ *
+ * @param value
+ * @return this stream
+ * @throws IOException
+ */
+ public RequestOutputStream write(final String value) throws IOException {
+ final ByteBuffer bytes = encoder.encode(CharBuffer.wrap(value));
+
+ super.write(bytes.array(), 0, bytes.limit());
+
+ return this;
+ }
+ }
+
+ /**
+ * Represents array of any type as list of objects so we can easily iterate over
+ * it
+ *
+ * @param array of elements
+ * @return list with the same elements
+ */
+ private static List