You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ia...@apache.org on 2013/04/29 15:39:52 UTC

[3/7] [CB-2431] Switch to OkHttp for FileTransfer connections

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/553a25ce/framework/src/com/squareup/okhttp/internal/http/HttpResponseCache.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpResponseCache.java b/framework/src/com/squareup/okhttp/internal/http/HttpResponseCache.java
new file mode 100644
index 0000000..8735166
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpResponseCache.java
@@ -0,0 +1,608 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.OkResponseCache;
+import com.squareup.okhttp.ResponseSource;
+import com.squareup.okhttp.internal.Base64;
+import com.squareup.okhttp.internal.DiskLruCache;
+import com.squareup.okhttp.internal.StrictLineReader;
+import com.squareup.okhttp.internal.Util;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FilterInputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.ResponseCache;
+import java.net.SecureCacheResponse;
+import java.net.URI;
+import java.net.URLConnection;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+import static com.squareup.okhttp.internal.Util.US_ASCII;
+import static com.squareup.okhttp.internal.Util.UTF_8;
+
+/**
+ * Cache responses in a directory on the file system. Most clients should use
+ * {@code android.net.HttpResponseCache}, the stable, documented front end for
+ * this.
+ */
+public final class HttpResponseCache extends ResponseCache implements OkResponseCache {
+  private static final char[] DIGITS =
+      { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+  // TODO: add APIs to iterate the cache?
+  private static final int VERSION = 201105;
+  private static final int ENTRY_METADATA = 0;
+  private static final int ENTRY_BODY = 1;
+  private static final int ENTRY_COUNT = 2;
+
+  private final DiskLruCache cache;
+
+  /* read and write statistics, all guarded by 'this' */
+  private int writeSuccessCount;
+  private int writeAbortCount;
+  private int networkCount;
+  private int hitCount;
+  private int requestCount;
+
+  public HttpResponseCache(File directory, long maxSize) throws IOException {
+    cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize);
+  }
+
+  private String uriToKey(URI uri) {
+    try {
+      MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+      byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8"));
+      return bytesToHexString(md5bytes);
+    } catch (NoSuchAlgorithmException e) {
+      throw new AssertionError(e);
+    } catch (UnsupportedEncodingException e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  private static String bytesToHexString(byte[] bytes) {
+    char[] digits = DIGITS;
+    char[] buf = new char[bytes.length * 2];
+    int c = 0;
+    for (byte b : bytes) {
+      buf[c++] = digits[(b >> 4) & 0xf];
+      buf[c++] = digits[b & 0xf];
+    }
+    return new String(buf);
+  }
+
+  @Override public CacheResponse get(URI uri, String requestMethod,
+      Map<String, List<String>> requestHeaders) {
+    String key = uriToKey(uri);
+    DiskLruCache.Snapshot snapshot;
+    Entry entry;
+    try {
+      snapshot = cache.get(key);
+      if (snapshot == null) {
+        return null;
+      }
+      entry = new Entry(snapshot.getInputStream(ENTRY_METADATA));
+    } catch (IOException e) {
+      // Give up because the cache cannot be read.
+      return null;
+    }
+
+    if (!entry.matches(uri, requestMethod, requestHeaders)) {
+      snapshot.close();
+      return null;
+    }
+
+    return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot)
+        : new EntryCacheResponse(entry, snapshot);
+  }
+
+  @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+    if (!(urlConnection instanceof HttpURLConnection)) {
+      return null;
+    }
+
+    HttpURLConnection httpConnection = (HttpURLConnection) urlConnection;
+    String requestMethod = httpConnection.getRequestMethod();
+    String key = uriToKey(uri);
+
+    if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals(
+        "DELETE")) {
+      try {
+        cache.remove(key);
+      } catch (IOException ignored) {
+        // The cache cannot be written.
+      }
+      return null;
+    } else if (!requestMethod.equals("GET")) {
+      // Don't cache non-GET responses. We're technically allowed to cache
+      // HEAD requests and some POST requests, but the complexity of doing
+      // so is high and the benefit is low.
+      return null;
+    }
+
+    HttpEngine httpEngine = getHttpEngine(httpConnection);
+    if (httpEngine == null) {
+      // Don't cache unless the HTTP implementation is ours.
+      return null;
+    }
+
+    ResponseHeaders response = httpEngine.getResponseHeaders();
+    if (response.hasVaryAll()) {
+      return null;
+    }
+
+    RawHeaders varyHeaders =
+        httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
+    Entry entry = new Entry(uri, varyHeaders, httpConnection);
+    DiskLruCache.Editor editor = null;
+    try {
+      editor = cache.edit(key);
+      if (editor == null) {
+        return null;
+      }
+      entry.writeTo(editor);
+      return new CacheRequestImpl(editor);
+    } catch (IOException e) {
+      abortQuietly(editor);
+      return null;
+    }
+  }
+
+  /**
+   * Handles a conditional request hit by updating the stored cache response
+   * with the headers from {@code httpConnection}. The cached response body is
+   * not updated. If the stored response has changed since {@code
+   * conditionalCacheHit} was returned, this does nothing.
+   */
+  @Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection)
+      throws IOException {
+    HttpEngine httpEngine = getHttpEngine(httpConnection);
+    URI uri = httpEngine.getUri();
+    ResponseHeaders response = httpEngine.getResponseHeaders();
+    RawHeaders varyHeaders =
+        httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields());
+    Entry entry = new Entry(uri, varyHeaders, httpConnection);
+    DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse)
+        ? ((EntryCacheResponse) conditionalCacheHit).snapshot
+        : ((EntrySecureCacheResponse) conditionalCacheHit).snapshot;
+    DiskLruCache.Editor editor = null;
+    try {
+      editor = snapshot.edit(); // returns null if snapshot is not current
+      if (editor != null) {
+        entry.writeTo(editor);
+        editor.commit();
+      }
+    } catch (IOException e) {
+      abortQuietly(editor);
+    }
+  }
+
+  private void abortQuietly(DiskLruCache.Editor editor) {
+    // Give up because the cache cannot be written.
+    try {
+      if (editor != null) {
+        editor.abort();
+      }
+    } catch (IOException ignored) {
+    }
+  }
+
+  private HttpEngine getHttpEngine(URLConnection httpConnection) {
+    if (httpConnection instanceof HttpURLConnectionImpl) {
+      return ((HttpURLConnectionImpl) httpConnection).getHttpEngine();
+    } else if (httpConnection instanceof HttpsURLConnectionImpl) {
+      return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine();
+    } else {
+      return null;
+    }
+  }
+
+  public DiskLruCache getCache() {
+    return cache;
+  }
+
+  public synchronized int getWriteAbortCount() {
+    return writeAbortCount;
+  }
+
+  public synchronized int getWriteSuccessCount() {
+    return writeSuccessCount;
+  }
+
+  public synchronized void trackResponse(ResponseSource source) {
+    requestCount++;
+
+    switch (source) {
+      case CACHE:
+        hitCount++;
+        break;
+      case CONDITIONAL_CACHE:
+      case NETWORK:
+        networkCount++;
+        break;
+    }
+  }
+
+  public synchronized void trackConditionalCacheHit() {
+    hitCount++;
+  }
+
+  public synchronized int getNetworkCount() {
+    return networkCount;
+  }
+
+  public synchronized int getHitCount() {
+    return hitCount;
+  }
+
+  public synchronized int getRequestCount() {
+    return requestCount;
+  }
+
+  private final class CacheRequestImpl extends CacheRequest {
+    private final DiskLruCache.Editor editor;
+    private OutputStream cacheOut;
+    private boolean done;
+    private OutputStream body;
+
+    public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
+      this.editor = editor;
+      this.cacheOut = editor.newOutputStream(ENTRY_BODY);
+      this.body = new FilterOutputStream(cacheOut) {
+        @Override public void close() throws IOException {
+          synchronized (HttpResponseCache.this) {
+            if (done) {
+              return;
+            }
+            done = true;
+            writeSuccessCount++;
+          }
+          super.close();
+          editor.commit();
+        }
+
+        @Override
+        public void write(byte[] buffer, int offset, int length) throws IOException {
+          // Since we don't override "write(int oneByte)", we can write directly to "out"
+          // and avoid the inefficient implementation from the FilterOutputStream.
+          out.write(buffer, offset, length);
+        }
+      };
+    }
+
+    @Override public void abort() {
+      synchronized (HttpResponseCache.this) {
+        if (done) {
+          return;
+        }
+        done = true;
+        writeAbortCount++;
+      }
+      Util.closeQuietly(cacheOut);
+      try {
+        editor.abort();
+      } catch (IOException ignored) {
+      }
+    }
+
+    @Override public OutputStream getBody() throws IOException {
+      return body;
+    }
+  }
+
+  private static final class Entry {
+    private final String uri;
+    private final RawHeaders varyHeaders;
+    private final String requestMethod;
+    private final RawHeaders responseHeaders;
+    private final String cipherSuite;
+    private final Certificate[] peerCertificates;
+    private final Certificate[] localCertificates;
+
+    /**
+     * Reads an entry from an input stream. A typical entry looks like this:
+     * <pre>{@code
+     *   http://google.com/foo
+     *   GET
+     *   2
+     *   Accept-Language: fr-CA
+     *   Accept-Charset: UTF-8
+     *   HTTP/1.1 200 OK
+     *   3
+     *   Content-Type: image/png
+     *   Content-Length: 100
+     *   Cache-Control: max-age=600
+     * }</pre>
+     *
+     * <p>A typical HTTPS file looks like this:
+     * <pre>{@code
+     *   https://google.com/foo
+     *   GET
+     *   2
+     *   Accept-Language: fr-CA
+     *   Accept-Charset: UTF-8
+     *   HTTP/1.1 200 OK
+     *   3
+     *   Content-Type: image/png
+     *   Content-Length: 100
+     *   Cache-Control: max-age=600
+     *
+     *   AES_256_WITH_MD5
+     *   2
+     *   base64-encoded peerCertificate[0]
+     *   base64-encoded peerCertificate[1]
+     *   -1
+     * }</pre>
+     * The file is newline separated. The first two lines are the URL and
+     * the request method. Next is the number of HTTP Vary request header
+     * lines, followed by those lines.
+     *
+     * <p>Next is the response status line, followed by the number of HTTP
+     * response header lines, followed by those lines.
+     *
+     * <p>HTTPS responses also contain SSL session information. This begins
+     * with a blank line, and then a line containing the cipher suite. Next
+     * is the length of the peer certificate chain. These certificates are
+     * base64-encoded and appear each on their own line. The next line
+     * contains the length of the local certificate chain. These
+     * certificates are also base64-encoded and appear each on their own
+     * line. A length of -1 is used to encode a null array.
+     */
+    public Entry(InputStream in) throws IOException {
+      try {
+        StrictLineReader reader = new StrictLineReader(in, US_ASCII);
+        uri = reader.readLine();
+        requestMethod = reader.readLine();
+        varyHeaders = new RawHeaders();
+        int varyRequestHeaderLineCount = reader.readInt();
+        for (int i = 0; i < varyRequestHeaderLineCount; i++) {
+          varyHeaders.addLine(reader.readLine());
+        }
+
+        responseHeaders = new RawHeaders();
+        responseHeaders.setStatusLine(reader.readLine());
+        int responseHeaderLineCount = reader.readInt();
+        for (int i = 0; i < responseHeaderLineCount; i++) {
+          responseHeaders.addLine(reader.readLine());
+        }
+
+        if (isHttps()) {
+          String blank = reader.readLine();
+          if (!blank.isEmpty()) {
+            throw new IOException("expected \"\" but was \"" + blank + "\"");
+          }
+          cipherSuite = reader.readLine();
+          peerCertificates = readCertArray(reader);
+          localCertificates = readCertArray(reader);
+        } else {
+          cipherSuite = null;
+          peerCertificates = null;
+          localCertificates = null;
+        }
+      } finally {
+        in.close();
+      }
+    }
+
+    public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection)
+        throws IOException {
+      this.uri = uri.toString();
+      this.varyHeaders = varyHeaders;
+      this.requestMethod = httpConnection.getRequestMethod();
+      this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true);
+
+      if (isHttps()) {
+        HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
+        cipherSuite = httpsConnection.getCipherSuite();
+        Certificate[] peerCertificatesNonFinal = null;
+        try {
+          peerCertificatesNonFinal = httpsConnection.getServerCertificates();
+        } catch (SSLPeerUnverifiedException ignored) {
+        }
+        peerCertificates = peerCertificatesNonFinal;
+        localCertificates = httpsConnection.getLocalCertificates();
+      } else {
+        cipherSuite = null;
+        peerCertificates = null;
+        localCertificates = null;
+      }
+    }
+
+    public void writeTo(DiskLruCache.Editor editor) throws IOException {
+      OutputStream out = editor.newOutputStream(ENTRY_METADATA);
+      Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+
+      writer.write(uri + '\n');
+      writer.write(requestMethod + '\n');
+      writer.write(Integer.toString(varyHeaders.length()) + '\n');
+      for (int i = 0; i < varyHeaders.length(); i++) {
+        writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n');
+      }
+
+      writer.write(responseHeaders.getStatusLine() + '\n');
+      writer.write(Integer.toString(responseHeaders.length()) + '\n');
+      for (int i = 0; i < responseHeaders.length(); i++) {
+        writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n');
+      }
+
+      if (isHttps()) {
+        writer.write('\n');
+        writer.write(cipherSuite + '\n');
+        writeCertArray(writer, peerCertificates);
+        writeCertArray(writer, localCertificates);
+      }
+      writer.close();
+    }
+
+    private boolean isHttps() {
+      return uri.startsWith("https://");
+    }
+
+    private Certificate[] readCertArray(StrictLineReader reader) throws IOException {
+      int length = reader.readInt();
+      if (length == -1) {
+        return null;
+      }
+      try {
+        CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+        Certificate[] result = new Certificate[length];
+        for (int i = 0; i < result.length; i++) {
+          String line = reader.readLine();
+          byte[] bytes = Base64.decode(line.getBytes("US-ASCII"));
+          result[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(bytes));
+        }
+        return result;
+      } catch (CertificateException e) {
+        throw new IOException(e);
+      }
+    }
+
+    private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException {
+      if (certificates == null) {
+        writer.write("-1\n");
+        return;
+      }
+      try {
+        writer.write(Integer.toString(certificates.length) + '\n');
+        for (Certificate certificate : certificates) {
+          byte[] bytes = certificate.getEncoded();
+          String line = Base64.encode(bytes);
+          writer.write(line + '\n');
+        }
+      } catch (CertificateEncodingException e) {
+        throw new IOException(e);
+      }
+    }
+
+    public boolean matches(URI uri, String requestMethod,
+        Map<String, List<String>> requestHeaders) {
+      return this.uri.equals(uri.toString())
+          && this.requestMethod.equals(requestMethod)
+          && new ResponseHeaders(uri, responseHeaders).varyMatches(varyHeaders.toMultimap(false),
+          requestHeaders);
+    }
+  }
+
+  /**
+   * Returns an input stream that reads the body of a snapshot, closing the
+   * snapshot when the stream is closed.
+   */
+  private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) {
+    return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) {
+      @Override public void close() throws IOException {
+        snapshot.close();
+        super.close();
+      }
+    };
+  }
+
+  static class EntryCacheResponse extends CacheResponse {
+    private final Entry entry;
+    private final DiskLruCache.Snapshot snapshot;
+    private final InputStream in;
+
+    public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
+      this.entry = entry;
+      this.snapshot = snapshot;
+      this.in = newBodyInputStream(snapshot);
+    }
+
+    @Override public Map<String, List<String>> getHeaders() {
+      return entry.responseHeaders.toMultimap(true);
+    }
+
+    @Override public InputStream getBody() {
+      return in;
+    }
+  }
+
+  static class EntrySecureCacheResponse extends SecureCacheResponse {
+    private final Entry entry;
+    private final DiskLruCache.Snapshot snapshot;
+    private final InputStream in;
+
+    public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) {
+      this.entry = entry;
+      this.snapshot = snapshot;
+      this.in = newBodyInputStream(snapshot);
+    }
+
+    @Override public Map<String, List<String>> getHeaders() {
+      return entry.responseHeaders.toMultimap(true);
+    }
+
+    @Override public InputStream getBody() {
+      return in;
+    }
+
+    @Override public String getCipherSuite() {
+      return entry.cipherSuite;
+    }
+
+    @Override public List<Certificate> getServerCertificateChain()
+        throws SSLPeerUnverifiedException {
+      if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
+        throw new SSLPeerUnverifiedException(null);
+      }
+      return Arrays.asList(entry.peerCertificates.clone());
+    }
+
+    @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+      if (entry.peerCertificates == null || entry.peerCertificates.length == 0) {
+        throw new SSLPeerUnverifiedException(null);
+      }
+      return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal();
+    }
+
+    @Override public List<Certificate> getLocalCertificateChain() {
+      if (entry.localCertificates == null || entry.localCertificates.length == 0) {
+        return null;
+      }
+      return Arrays.asList(entry.localCertificates.clone());
+    }
+
+    @Override public Principal getLocalPrincipal() {
+      if (entry.localCertificates == null || entry.localCertificates.length == 0) {
+        return null;
+      }
+      return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal();
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/553a25ce/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java b/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java
new file mode 100644
index 0000000..dd7a38d
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.internal.Util;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheRequest;
+import java.net.ProtocolException;
+import java.net.Socket;
+
+import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+
+public final class HttpTransport implements Transport {
+  /**
+   * The maximum number of bytes to buffer when sending headers and a request
+   * body. When the headers and body can be sent in a single write, the
+   * request completes sooner. In one WiFi benchmark, using a large enough
+   * buffer sped up some uploads by half.
+   */
+  private static final int MAX_REQUEST_BUFFER_LENGTH = 32768;
+
+  /**
+   * The timeout to use while discarding a stream of input data. Since this is
+   * used for connection reuse, this timeout should be significantly less than
+   * the time it takes to establish a new connection.
+   */
+  private static final int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
+
+  public static final int DEFAULT_CHUNK_LENGTH = 1024;
+
+  private final HttpEngine httpEngine;
+  private final InputStream socketIn;
+  private final OutputStream socketOut;
+
+  /**
+   * This stream buffers the request headers and the request body when their
+   * combined size is less than MAX_REQUEST_BUFFER_LENGTH. By combining them
+   * we can save socket writes, which in turn saves a packet transmission.
+   * This is socketOut if the request size is large or unknown.
+   */
+  private OutputStream requestOut;
+
+  public HttpTransport(HttpEngine httpEngine, OutputStream outputStream, InputStream inputStream) {
+    this.httpEngine = httpEngine;
+    this.socketOut = outputStream;
+    this.requestOut = outputStream;
+    this.socketIn = inputStream;
+  }
+
+  @Override public OutputStream createRequestBody() throws IOException {
+    boolean chunked = httpEngine.requestHeaders.isChunked();
+    if (!chunked
+        && httpEngine.policy.getChunkLength() > 0
+        && httpEngine.connection.getHttpMinorVersion() != 0) {
+      httpEngine.requestHeaders.setChunked();
+      chunked = true;
+    }
+
+    // Stream a request body of unknown length.
+    if (chunked) {
+      int chunkLength = httpEngine.policy.getChunkLength();
+      if (chunkLength == -1) {
+        chunkLength = DEFAULT_CHUNK_LENGTH;
+      }
+      writeRequestHeaders();
+      return new ChunkedOutputStream(requestOut, chunkLength);
+    }
+
+    // Stream a request body of a known length.
+    int fixedContentLength = httpEngine.policy.getFixedContentLength();
+    if (fixedContentLength != -1) {
+      httpEngine.requestHeaders.setContentLength(fixedContentLength);
+      writeRequestHeaders();
+      return new FixedLengthOutputStream(requestOut, fixedContentLength);
+    }
+
+    // Buffer a request body of a known length.
+    int contentLength = httpEngine.requestHeaders.getContentLength();
+    if (contentLength != -1) {
+      writeRequestHeaders();
+      return new RetryableOutputStream(contentLength);
+    }
+
+    // Buffer a request body of an unknown length. Don't write request
+    // headers until the entire body is ready; otherwise we can't set the
+    // Content-Length header correctly.
+    return new RetryableOutputStream();
+  }
+
+  @Override public void flushRequest() throws IOException {
+    requestOut.flush();
+    requestOut = socketOut;
+  }
+
+  @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
+    requestBody.writeToSocket(requestOut);
+  }
+
+  /**
+   * Prepares the HTTP headers and sends them to the server.
+   *
+   * <p>For streaming requests with a body, headers must be prepared
+   * <strong>before</strong> the output stream has been written to. Otherwise
+   * the body would need to be buffered!
+   *
+   * <p>For non-streaming requests with a body, headers must be prepared
+   * <strong>after</strong> the output stream has been written to and closed.
+   * This ensures that the {@code Content-Length} header field receives the
+   * proper value.
+   */
+  public void writeRequestHeaders() throws IOException {
+    httpEngine.writingRequestHeaders();
+    int contentLength = httpEngine.requestHeaders.getContentLength();
+    RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders();
+    byte[] bytes = headersToSend.toBytes();
+
+    if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) {
+      requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength);
+    }
+
+    requestOut.write(bytes);
+  }
+
+  @Override public ResponseHeaders readResponseHeaders() throws IOException {
+    RawHeaders headers = RawHeaders.fromBytes(socketIn);
+    httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion());
+    httpEngine.receiveHeaders(headers);
+    return new ResponseHeaders(httpEngine.uri, headers);
+  }
+
+  public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
+      InputStream responseBodyIn) {
+    if (streamCancelled) {
+      return false;
+    }
+
+    // We cannot reuse sockets that have incomplete output.
+    if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) {
+      return false;
+    }
+
+    // If the request specified that the connection shouldn't be reused, don't reuse it.
+    if (httpEngine.requestHeaders.hasConnectionClose()) {
+      return false;
+    }
+
+    // If the response specified that the connection shouldn't be reused, don't reuse it.
+    if (httpEngine.responseHeaders != null && httpEngine.responseHeaders.hasConnectionClose()) {
+      return false;
+    }
+
+    if (responseBodyIn instanceof UnknownLengthHttpInputStream) {
+      return false;
+    }
+
+    if (responseBodyIn != null) {
+      return discardStream(httpEngine, responseBodyIn);
+    }
+
+    return true;
+  }
+
+  /**
+   * Discards the response body so that the connection can be reused. This
+   * needs to be done judiciously, since it delays the current request in
+   * order to speed up a potential future request that may never occur.
+   */
+  private static boolean discardStream(HttpEngine httpEngine, InputStream responseBodyIn) {
+    Connection connection = httpEngine.connection;
+    if (connection == null) return false;
+    Socket socket = connection.getSocket();
+    if (socket == null) return false;
+    try {
+      int socketTimeout = socket.getSoTimeout();
+      socket.setSoTimeout(DISCARD_STREAM_TIMEOUT_MILLIS);
+      try {
+        Util.skipAll(responseBodyIn);
+        return true;
+      } finally {
+        socket.setSoTimeout(socketTimeout);
+      }
+    } catch (IOException e) {
+      return false;
+    }
+  }
+
+  @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
+    if (!httpEngine.hasResponseBody()) {
+      return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, 0);
+    }
+
+    if (httpEngine.responseHeaders.isChunked()) {
+      return new ChunkedInputStream(socketIn, cacheRequest, this);
+    }
+
+    if (httpEngine.responseHeaders.getContentLength() != -1) {
+      return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine,
+          httpEngine.responseHeaders.getContentLength());
+    }
+
+    // Wrap the input stream from the connection (rather than just returning
+    // "socketIn" directly here), so that we can control its use after the
+    // reference escapes.
+    return new UnknownLengthHttpInputStream(socketIn, cacheRequest, httpEngine);
+  }
+
+  /** An HTTP body with a fixed length known in advance. */
+  private static final class FixedLengthOutputStream extends AbstractHttpOutputStream {
+    private final OutputStream socketOut;
+    private int bytesRemaining;
+
+    private FixedLengthOutputStream(OutputStream socketOut, int bytesRemaining) {
+      this.socketOut = socketOut;
+      this.bytesRemaining = bytesRemaining;
+    }
+
+    @Override public void write(byte[] buffer, int offset, int count) throws IOException {
+      checkNotClosed();
+      checkOffsetAndCount(buffer.length, offset, count);
+      if (count > bytesRemaining) {
+        throw new ProtocolException("expected " + bytesRemaining + " bytes but received " + count);
+      }
+      socketOut.write(buffer, offset, count);
+      bytesRemaining -= count;
+    }
+
+    @Override public void flush() throws IOException {
+      if (closed) {
+        return; // don't throw; this stream might have been closed on the caller's behalf
+      }
+      socketOut.flush();
+    }
+
+    @Override public void close() throws IOException {
+      if (closed) {
+        return;
+      }
+      closed = true;
+      if (bytesRemaining > 0) {
+        throw new ProtocolException("unexpected end of stream");
+      }
+    }
+  }
+
+  /**
+   * An HTTP body with alternating chunk sizes and chunk bodies. Chunks are
+   * buffered until {@code maxChunkLength} bytes are ready, at which point the
+   * chunk is written and the buffer is cleared.
+   */
+  private static final class ChunkedOutputStream extends AbstractHttpOutputStream {
+    private static final byte[] CRLF = { '\r', '\n' };
+    private static final byte[] HEX_DIGITS = {
+        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+    };
+    private static final byte[] FINAL_CHUNK = new byte[] { '0', '\r', '\n', '\r', '\n' };
+
+    /** Scratch space for up to 8 hex digits, and then a constant CRLF. */
+    private final byte[] hex = { 0, 0, 0, 0, 0, 0, 0, 0, '\r', '\n' };
+
+    private final OutputStream socketOut;
+    private final int maxChunkLength;
+    private final ByteArrayOutputStream bufferedChunk;
+
+    private ChunkedOutputStream(OutputStream socketOut, int maxChunkLength) {
+      this.socketOut = socketOut;
+      this.maxChunkLength = Math.max(1, dataLength(maxChunkLength));
+      this.bufferedChunk = new ByteArrayOutputStream(maxChunkLength);
+    }
+
+    /**
+     * Returns the amount of data that can be transmitted in a chunk whose total
+     * length (data+headers) is {@code dataPlusHeaderLength}. This is presumably
+     * useful to match sizes with wire-protocol packets.
+     */
+    private int dataLength(int dataPlusHeaderLength) {
+      int headerLength = 4; // "\r\n" after the size plus another "\r\n" after the data
+      for (int i = dataPlusHeaderLength - headerLength; i > 0; i >>= 4) {
+        headerLength++;
+      }
+      return dataPlusHeaderLength - headerLength;
+    }
+
+    @Override public synchronized void write(byte[] buffer, int offset, int count)
+        throws IOException {
+      checkNotClosed();
+      checkOffsetAndCount(buffer.length, offset, count);
+
+      while (count > 0) {
+        int numBytesWritten;
+
+        if (bufferedChunk.size() > 0 || count < maxChunkLength) {
+          // fill the buffered chunk and then maybe write that to the stream
+          numBytesWritten = Math.min(count, maxChunkLength - bufferedChunk.size());
+          // TODO: skip unnecessary copies from buffer->bufferedChunk?
+          bufferedChunk.write(buffer, offset, numBytesWritten);
+          if (bufferedChunk.size() == maxChunkLength) {
+            writeBufferedChunkToSocket();
+          }
+        } else {
+          // write a single chunk of size maxChunkLength to the stream
+          numBytesWritten = maxChunkLength;
+          writeHex(numBytesWritten);
+          socketOut.write(buffer, offset, numBytesWritten);
+          socketOut.write(CRLF);
+        }
+
+        offset += numBytesWritten;
+        count -= numBytesWritten;
+      }
+    }
+
+    /**
+     * Equivalent to, but cheaper than writing Integer.toHexString().getBytes()
+     * followed by CRLF.
+     */
+    private void writeHex(int i) throws IOException {
+      int cursor = 8;
+      do {
+        hex[--cursor] = HEX_DIGITS[i & 0xf];
+      } while ((i >>>= 4) != 0);
+      socketOut.write(hex, cursor, hex.length - cursor);
+    }
+
+    @Override public synchronized void flush() throws IOException {
+      if (closed) {
+        return; // don't throw; this stream might have been closed on the caller's behalf
+      }
+      writeBufferedChunkToSocket();
+      socketOut.flush();
+    }
+
+    @Override public synchronized void close() throws IOException {
+      if (closed) {
+        return;
+      }
+      closed = true;
+      writeBufferedChunkToSocket();
+      socketOut.write(FINAL_CHUNK);
+    }
+
+    private void writeBufferedChunkToSocket() throws IOException {
+      int size = bufferedChunk.size();
+      if (size <= 0) {
+        return;
+      }
+
+      writeHex(size);
+      bufferedChunk.writeTo(socketOut);
+      bufferedChunk.reset();
+      socketOut.write(CRLF);
+    }
+  }
+
+  /** An HTTP body with a fixed length specified in advance. */
+  private static class FixedLengthInputStream extends AbstractHttpInputStream {
+    private int bytesRemaining;
+
+    public FixedLengthInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine,
+        int length) throws IOException {
+      super(is, httpEngine, cacheRequest);
+      bytesRemaining = length;
+      if (bytesRemaining == 0) {
+        endOfInput(false);
+      }
+    }
+
+    @Override public int read(byte[] buffer, int offset, int count) throws IOException {
+      checkOffsetAndCount(buffer.length, offset, count);
+      checkNotClosed();
+      if (bytesRemaining == 0) {
+        return -1;
+      }
+      int read = in.read(buffer, offset, Math.min(count, bytesRemaining));
+      if (read == -1) {
+        unexpectedEndOfInput(); // the server didn't supply the promised content length
+        throw new ProtocolException("unexpected end of stream");
+      }
+      bytesRemaining -= read;
+      cacheWrite(buffer, offset, read);
+      if (bytesRemaining == 0) {
+        endOfInput(false);
+      }
+      return read;
+    }
+
+    @Override public int available() throws IOException {
+      checkNotClosed();
+      return bytesRemaining == 0 ? 0 : Math.min(in.available(), bytesRemaining);
+    }
+
+    @Override public void close() throws IOException {
+      if (closed) {
+        return;
+      }
+      if (bytesRemaining != 0 && !discardStream(httpEngine, this)) {
+        unexpectedEndOfInput();
+      }
+      closed = true;
+    }
+  }
+
+  /** An HTTP body with alternating chunk sizes and chunk bodies. */
+  private static class ChunkedInputStream extends AbstractHttpInputStream {
+    private static final int NO_CHUNK_YET = -1;
+    private final HttpTransport transport;
+    private int bytesRemainingInChunk = NO_CHUNK_YET;
+    private boolean hasMoreChunks = true;
+
+    ChunkedInputStream(InputStream is, CacheRequest cacheRequest, HttpTransport transport)
+        throws IOException {
+      super(is, transport.httpEngine, cacheRequest);
+      this.transport = transport;
+    }
+
+    @Override public int read(byte[] buffer, int offset, int count) throws IOException {
+      checkOffsetAndCount(buffer.length, offset, count);
+      checkNotClosed();
+
+      if (!hasMoreChunks) {
+        return -1;
+      }
+      if (bytesRemainingInChunk == 0 || bytesRemainingInChunk == NO_CHUNK_YET) {
+        readChunkSize();
+        if (!hasMoreChunks) {
+          return -1;
+        }
+      }
+      int read = in.read(buffer, offset, Math.min(count, bytesRemainingInChunk));
+      if (read == -1) {
+        unexpectedEndOfInput(); // the server didn't supply the promised chunk length
+        throw new IOException("unexpected end of stream");
+      }
+      bytesRemainingInChunk -= read;
+      cacheWrite(buffer, offset, read);
+      return read;
+    }
+
+    private void readChunkSize() throws IOException {
+      // read the suffix of the previous chunk
+      if (bytesRemainingInChunk != NO_CHUNK_YET) {
+        Util.readAsciiLine(in);
+      }
+      String chunkSizeString = Util.readAsciiLine(in);
+      int index = chunkSizeString.indexOf(";");
+      if (index != -1) {
+        chunkSizeString = chunkSizeString.substring(0, index);
+      }
+      try {
+        bytesRemainingInChunk = Integer.parseInt(chunkSizeString.trim(), 16);
+      } catch (NumberFormatException e) {
+        throw new ProtocolException("Expected a hex chunk size but was " + chunkSizeString);
+      }
+      if (bytesRemainingInChunk == 0) {
+        hasMoreChunks = false;
+        RawHeaders rawResponseHeaders = httpEngine.responseHeaders.getHeaders();
+        RawHeaders.readHeaders(transport.socketIn, rawResponseHeaders);
+        httpEngine.receiveHeaders(rawResponseHeaders);
+        endOfInput(false);
+      }
+    }
+
+    @Override public int available() throws IOException {
+      checkNotClosed();
+      if (!hasMoreChunks || bytesRemainingInChunk == NO_CHUNK_YET) {
+        return 0;
+      }
+      return Math.min(in.available(), bytesRemainingInChunk);
+    }
+
+    @Override public void close() throws IOException {
+      if (closed) {
+        return;
+      }
+      if (hasMoreChunks && !discardStream(httpEngine, this)) {
+        unexpectedEndOfInput();
+      }
+      closed = true;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/553a25ce/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
new file mode 100644
index 0000000..25e8c3c
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
@@ -0,0 +1,502 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.internal.Util;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CookieHandler;
+import java.net.HttpRetryException;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.ResponseCache;
+import java.net.SocketPermission;
+import java.net.URL;
+import java.security.Permission;
+import java.security.cert.CertificateException;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSocketFactory;
+
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
+
+/**
+ * This implementation uses HttpEngine to send requests and receive responses.
+ * This class may use multiple HttpEngines to follow redirects, authentication
+ * retries, etc. to retrieve the final response body.
+ *
+ * <h3>What does 'connected' mean?</h3>
+ * This class inherits a {@code connected} field from the superclass. That field
+ * is <strong>not</strong> used to indicate not whether this URLConnection is
+ * currently connected. Instead, it indicates whether a connection has ever been
+ * attempted. Once a connection has been attempted, certain properties (request
+ * header fields, request method, etc.) are immutable. Test the {@code
+ * connection} field on this class for null/non-null to determine of an instance
+ * is currently connected to a server.
+ */
+public class HttpURLConnectionImpl extends HttpURLConnection {
+
+  /** Numeric status code, 307: Temporary Redirect. */
+  static final int HTTP_TEMP_REDIRECT = 307;
+
+  /**
+   * How many redirects should we follow? Chrome follows 21; Firefox, curl,
+   * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
+   */
+  private static final int MAX_REDIRECTS = 20;
+
+  private final boolean followProtocolRedirects;
+
+  /** The proxy requested by the client, or null for a proxy to be selected automatically. */
+  final Proxy requestedProxy;
+
+  final ProxySelector proxySelector;
+  final CookieHandler cookieHandler;
+  final ResponseCache responseCache;
+  final ConnectionPool connectionPool;
+  /* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */
+  SSLSocketFactory sslSocketFactory;
+  HostnameVerifier hostnameVerifier;
+
+  private final RawHeaders rawRequestHeaders = new RawHeaders();
+
+  private int redirectionCount;
+
+  protected IOException httpEngineFailure;
+  protected HttpEngine httpEngine;
+
+  public HttpURLConnectionImpl(URL url, OkHttpClient client) {
+    super(url);
+    this.followProtocolRedirects = client.getFollowProtocolRedirects();
+    this.requestedProxy = client.getProxy();
+    this.proxySelector = client.getProxySelector();
+    this.cookieHandler = client.getCookieHandler();
+    this.responseCache = client.getResponseCache();
+    this.connectionPool = client.getConnectionPool();
+    this.sslSocketFactory = client.getSslSocketFactory();
+    this.hostnameVerifier = client.getHostnameVerifier();
+  }
+
+  @Override public final void connect() throws IOException {
+    initHttpEngine();
+    boolean success;
+    do {
+      success = execute(false);
+    } while (!success);
+  }
+
+  @Override public final void disconnect() {
+    // Calling disconnect() before a connection exists should have no effect.
+    if (httpEngine != null) {
+      // We close the response body here instead of in
+      // HttpEngine.release because that is called when input
+      // has been completely read from the underlying socket.
+      // However the response body can be a GZIPInputStream that
+      // still has unread data.
+      if (httpEngine.hasResponse()) {
+        Util.closeQuietly(httpEngine.getResponseBody());
+      }
+      httpEngine.release(true);
+    }
+  }
+
+  /**
+   * Returns an input stream from the server in the case of error such as the
+   * requested file (txt, htm, html) is not found on the remote server.
+   */
+  @Override public final InputStream getErrorStream() {
+    try {
+      HttpEngine response = getResponse();
+      if (response.hasResponseBody() && response.getResponseCode() >= HTTP_BAD_REQUEST) {
+        return response.getResponseBody();
+      }
+      return null;
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Returns the value of the field at {@code position}. Returns null if there
+   * are fewer than {@code position} headers.
+   */
+  @Override public final String getHeaderField(int position) {
+    try {
+      return getResponse().getResponseHeaders().getHeaders().getValue(position);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Returns the value of the field corresponding to the {@code fieldName}, or
+   * null if there is no such field. If the field has multiple values, the
+   * last value is returned.
+   */
+  @Override public final String getHeaderField(String fieldName) {
+    try {
+      RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders();
+      return fieldName == null ? rawHeaders.getStatusLine() : rawHeaders.get(fieldName);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  @Override public final String getHeaderFieldKey(int position) {
+    try {
+      return getResponse().getResponseHeaders().getHeaders().getFieldName(position);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  @Override public final Map<String, List<String>> getHeaderFields() {
+    try {
+      return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  @Override public final Map<String, List<String>> getRequestProperties() {
+    if (connected) {
+      throw new IllegalStateException(
+          "Cannot access request header fields after connection is set");
+    }
+    return rawRequestHeaders.toMultimap(false);
+  }
+
+  @Override public final InputStream getInputStream() throws IOException {
+    if (!doInput) {
+      throw new ProtocolException("This protocol does not support input");
+    }
+
+    HttpEngine response = getResponse();
+
+    // if the requested file does not exist, throw an exception formerly the
+    // Error page from the server was returned if the requested file was
+    // text/html this has changed to return FileNotFoundException for all
+    // file types
+    if (getResponseCode() >= HTTP_BAD_REQUEST) {
+      throw new FileNotFoundException(url.toString());
+    }
+
+    InputStream result = response.getResponseBody();
+    if (result == null) {
+      throw new ProtocolException("No response body exists; responseCode=" + getResponseCode());
+    }
+    return result;
+  }
+
+  @Override public final OutputStream getOutputStream() throws IOException {
+    connect();
+
+    OutputStream result = httpEngine.getRequestBody();
+    if (result == null) {
+      throw new ProtocolException("method does not support a request body: " + method);
+    } else if (httpEngine.hasResponse()) {
+      throw new ProtocolException("cannot write request body after response has been read");
+    }
+
+    return result;
+  }
+
+  @Override public final Permission getPermission() throws IOException {
+    String hostName = getURL().getHost();
+    int hostPort = Util.getEffectivePort(getURL());
+    if (usingProxy()) {
+      InetSocketAddress proxyAddress = (InetSocketAddress) requestedProxy.address();
+      hostName = proxyAddress.getHostName();
+      hostPort = proxyAddress.getPort();
+    }
+    return new SocketPermission(hostName + ":" + hostPort, "connect, resolve");
+  }
+
+  @Override public final String getRequestProperty(String field) {
+    if (field == null) {
+      return null;
+    }
+    return rawRequestHeaders.get(field);
+  }
+
+  private void initHttpEngine() throws IOException {
+    if (httpEngineFailure != null) {
+      throw httpEngineFailure;
+    } else if (httpEngine != null) {
+      return;
+    }
+
+    connected = true;
+    try {
+      if (doOutput) {
+        if (method.equals("GET")) {
+          // they are requesting a stream to write to. This implies a POST method
+          method = "POST";
+        } else if (!method.equals("POST") && !method.equals("PUT")) {
+          // If the request method is neither POST nor PUT, then you're not writing
+          throw new ProtocolException(method + " does not support writing");
+        }
+      }
+      httpEngine = newHttpEngine(method, rawRequestHeaders, null, null);
+    } catch (IOException e) {
+      httpEngineFailure = e;
+      throw e;
+    }
+  }
+
+  protected HttpURLConnection getHttpConnectionToCache() {
+    return this;
+  }
+
+  private HttpEngine newHttpEngine(String method, RawHeaders requestHeaders,
+      Connection connection, RetryableOutputStream requestBody) throws IOException {
+    if (url.getProtocol().equals("http")) {
+      return new HttpEngine(this, method, requestHeaders, connection, requestBody);
+    } else if (url.getProtocol().equals("https")) {
+      return new HttpsURLConnectionImpl.HttpsEngine(
+          this, method, requestHeaders, connection, requestBody);
+    } else {
+      throw new AssertionError();
+    }
+  }
+
+  /**
+   * Aggressively tries to get the final HTTP response, potentially making
+   * many HTTP requests in the process in order to cope with redirects and
+   * authentication.
+   */
+  private HttpEngine getResponse() throws IOException {
+    initHttpEngine();
+
+    if (httpEngine.hasResponse()) {
+      return httpEngine;
+    }
+
+    while (true) {
+      if (!execute(true)) {
+        continue;
+      }
+
+      Retry retry = processResponseHeaders();
+      if (retry == Retry.NONE) {
+        httpEngine.automaticallyReleaseConnectionToPool();
+        return httpEngine;
+      }
+
+      // The first request was insufficient. Prepare for another...
+      String retryMethod = method;
+      OutputStream requestBody = httpEngine.getRequestBody();
+
+      // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM
+      // redirect should keep the same method, Chrome, Firefox and the
+      // RI all issue GETs when following any redirect.
+      int responseCode = getResponseCode();
+      if (responseCode == HTTP_MULT_CHOICE
+          || responseCode == HTTP_MOVED_PERM
+          || responseCode == HTTP_MOVED_TEMP
+          || responseCode == HTTP_SEE_OTHER) {
+        retryMethod = "GET";
+        requestBody = null;
+      }
+
+      if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
+        throw new HttpRetryException("Cannot retry streamed HTTP body",
+            httpEngine.getResponseCode());
+      }
+
+      if (retry == Retry.DIFFERENT_CONNECTION) {
+        httpEngine.automaticallyReleaseConnectionToPool();
+      }
+
+      httpEngine.release(false);
+
+      httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
+          (RetryableOutputStream) requestBody);
+    }
+  }
+
+  /**
+   * Sends a request and optionally reads a response. Returns true if the
+   * request was successfully executed, and false if the request can be
+   * retried. Throws an exception if the request failed permanently.
+   */
+  private boolean execute(boolean readResponse) throws IOException {
+    try {
+      httpEngine.sendRequest();
+      if (readResponse) {
+        httpEngine.readResponse();
+      }
+      return true;
+    } catch (IOException e) {
+      RouteSelector routeSelector = httpEngine.routeSelector;
+      if (routeSelector != null && httpEngine.connection != null) {
+        routeSelector.connectFailed(httpEngine.connection, e);
+      }
+      if (routeSelector == null && httpEngine.connection == null) {
+        throw e; // If we failed before finding a route or a connection, give up.
+      }
+
+      // The connection failure isn't fatal if there's another route to attempt.
+      OutputStream requestBody = httpEngine.getRequestBody();
+      if ((routeSelector == null || routeSelector.hasNext()) && isRecoverable(e) && (requestBody
+          == null || requestBody instanceof RetryableOutputStream)) {
+        httpEngine.release(true);
+        httpEngine =
+            newHttpEngine(method, rawRequestHeaders, null, (RetryableOutputStream) requestBody);
+        httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
+        return false;
+      }
+      httpEngineFailure = e;
+      throw e;
+    }
+  }
+
+  private boolean isRecoverable(IOException e) {
+    // If the problem was a CertificateException from the X509TrustManager,
+    // do not retry, we didn't have an abrupt server initiated exception.
+    boolean sslFailure =
+        e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException;
+    boolean protocolFailure = e instanceof ProtocolException;
+    return !sslFailure && !protocolFailure;
+  }
+
+  HttpEngine getHttpEngine() {
+    return httpEngine;
+  }
+
+  enum Retry {
+    NONE,
+    SAME_CONNECTION,
+    DIFFERENT_CONNECTION
+  }
+
+  /**
+   * Returns the retry action to take for the current response headers. The
+   * headers, proxy and target URL or this connection may be adjusted to
+   * prepare for a follow up request.
+   */
+  private Retry processResponseHeaders() throws IOException {
+    Proxy selectedProxy = httpEngine.connection != null
+        ? httpEngine.connection.getProxy()
+        : requestedProxy;
+    final int responseCode = getResponseCode();
+    switch (responseCode) {
+      case HTTP_PROXY_AUTH:
+        if (selectedProxy.type() != Proxy.Type.HTTP) {
+          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
+        }
+        // fall-through
+      case HTTP_UNAUTHORIZED:
+        boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(),
+            httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, selectedProxy, url);
+        return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE;
+
+      case HTTP_MULT_CHOICE:
+      case HTTP_MOVED_PERM:
+      case HTTP_MOVED_TEMP:
+      case HTTP_SEE_OTHER:
+      case HTTP_TEMP_REDIRECT:
+        if (!getInstanceFollowRedirects()) {
+          return Retry.NONE;
+        }
+        if (++redirectionCount > MAX_REDIRECTS) {
+          throw new ProtocolException("Too many redirects: " + redirectionCount);
+        }
+        if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) {
+          // "If the 307 status code is received in response to a request other than GET or HEAD,
+          // the user agent MUST NOT automatically redirect the request"
+          return Retry.NONE;
+        }
+        String location = getHeaderField("Location");
+        if (location == null) {
+          return Retry.NONE;
+        }
+        URL previousUrl = url;
+        url = new URL(previousUrl, location);
+        if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) {
+          return Retry.NONE; // Don't follow redirects to unsupported protocols.
+        }
+        boolean sameProtocol = previousUrl.getProtocol().equals(url.getProtocol());
+        if (!sameProtocol && !followProtocolRedirects) {
+          return Retry.NONE; // This client doesn't follow redirects across protocols.
+        }
+        boolean sameHost = previousUrl.getHost().equals(url.getHost());
+        boolean samePort = getEffectivePort(previousUrl) == getEffectivePort(url);
+        if (sameHost && samePort && sameProtocol) {
+          return Retry.SAME_CONNECTION;
+        } else {
+          return Retry.DIFFERENT_CONNECTION;
+        }
+
+      default:
+        return Retry.NONE;
+    }
+  }
+
+  /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */
+  final int getFixedContentLength() {
+    return fixedContentLength;
+  }
+
+  /** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */
+  final int getChunkLength() {
+    return chunkLength;
+  }
+
+  @Override public final boolean usingProxy() {
+    return (requestedProxy != null && requestedProxy.type() != Proxy.Type.DIRECT);
+  }
+
+  @Override public String getResponseMessage() throws IOException {
+    return getResponse().getResponseHeaders().getHeaders().getResponseMessage();
+  }
+
+  @Override public final int getResponseCode() throws IOException {
+    return getResponse().getResponseCode();
+  }
+
+  @Override public final void setRequestProperty(String field, String newValue) {
+    if (connected) {
+      throw new IllegalStateException("Cannot set request property after connection is made");
+    }
+    if (field == null) {
+      throw new NullPointerException("field == null");
+    }
+    rawRequestHeaders.set(field, newValue);
+  }
+
+  @Override public final void addRequestProperty(String field, String value) {
+    if (connected) {
+      throw new IllegalStateException("Cannot add request property after connection is made");
+    }
+    if (field == null) {
+      throw new NullPointerException("field == null");
+    }
+    rawRequestHeaders.add(field, value);
+  }
+}

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/553a25ce/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
new file mode 100644
index 0000000..c224270
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
@@ -0,0 +1,458 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.TunnelRequest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.ProtocolException;
+import java.net.SecureCacheResponse;
+import java.net.URL;
+import java.security.Permission;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
+
+public final class HttpsURLConnectionImpl extends HttpsURLConnection {
+
+  /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
+  private final HttpUrlConnectionDelegate delegate;
+
+  public HttpsURLConnectionImpl(URL url, OkHttpClient client) {
+    super(url);
+    delegate = new HttpUrlConnectionDelegate(url, client);
+  }
+
+  @Override public String getCipherSuite() {
+    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
+    if (cacheResponse != null) {
+      return cacheResponse.getCipherSuite();
+    }
+    SSLSocket sslSocket = getSslSocket();
+    if (sslSocket != null) {
+      return sslSocket.getSession().getCipherSuite();
+    }
+    return null;
+  }
+
+  @Override public Certificate[] getLocalCertificates() {
+    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
+    if (cacheResponse != null) {
+      List<Certificate> result = cacheResponse.getLocalCertificateChain();
+      return result != null ? result.toArray(new Certificate[result.size()]) : null;
+    }
+    SSLSocket sslSocket = getSslSocket();
+    if (sslSocket != null) {
+      return sslSocket.getSession().getLocalCertificates();
+    }
+    return null;
+  }
+
+  @Override public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
+    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
+    if (cacheResponse != null) {
+      List<Certificate> result = cacheResponse.getServerCertificateChain();
+      return result != null ? result.toArray(new Certificate[result.size()]) : null;
+    }
+    SSLSocket sslSocket = getSslSocket();
+    if (sslSocket != null) {
+      return sslSocket.getSession().getPeerCertificates();
+    }
+    return null;
+  }
+
+  @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
+    if (cacheResponse != null) {
+      return cacheResponse.getPeerPrincipal();
+    }
+    SSLSocket sslSocket = getSslSocket();
+    if (sslSocket != null) {
+      return sslSocket.getSession().getPeerPrincipal();
+    }
+    return null;
+  }
+
+  @Override public Principal getLocalPrincipal() {
+    SecureCacheResponse cacheResponse = delegate.getSecureCacheResponse();
+    if (cacheResponse != null) {
+      return cacheResponse.getLocalPrincipal();
+    }
+    SSLSocket sslSocket = getSslSocket();
+    if (sslSocket != null) {
+      return sslSocket.getSession().getLocalPrincipal();
+    }
+    return null;
+  }
+
+  HttpEngine getHttpEngine() {
+    return delegate.getHttpEngine();
+  }
+
+  private SSLSocket getSslSocket() {
+    if (delegate.httpEngine == null || delegate.httpEngine.sentRequestMillis == -1) {
+      throw new IllegalStateException("Connection has not yet been established");
+    }
+    return delegate.httpEngine instanceof HttpsEngine
+        ? ((HttpsEngine) delegate.httpEngine).sslSocket
+        : null; // Not HTTPS! Probably an https:// to http:// redirect.
+  }
+
+  @Override
+  public void disconnect() {
+    delegate.disconnect();
+  }
+
+  @Override
+  public InputStream getErrorStream() {
+    return delegate.getErrorStream();
+  }
+
+  @Override
+  public String getRequestMethod() {
+    return delegate.getRequestMethod();
+  }
+
+  @Override
+  public int getResponseCode() throws IOException {
+    return delegate.getResponseCode();
+  }
+
+  @Override
+  public String getResponseMessage() throws IOException {
+    return delegate.getResponseMessage();
+  }
+
+  @Override
+  public void setRequestMethod(String method) throws ProtocolException {
+    delegate.setRequestMethod(method);
+  }
+
+  @Override
+  public boolean usingProxy() {
+    return delegate.usingProxy();
+  }
+
+  @Override
+  public boolean getInstanceFollowRedirects() {
+    return delegate.getInstanceFollowRedirects();
+  }
+
+  @Override
+  public void setInstanceFollowRedirects(boolean followRedirects) {
+    delegate.setInstanceFollowRedirects(followRedirects);
+  }
+
+  @Override
+  public void connect() throws IOException {
+    connected = true;
+    delegate.connect();
+  }
+
+  @Override
+  public boolean getAllowUserInteraction() {
+    return delegate.getAllowUserInteraction();
+  }
+
+  @Override
+  public Object getContent() throws IOException {
+    return delegate.getContent();
+  }
+
+  @SuppressWarnings("unchecked") // Spec does not generify
+  @Override
+  public Object getContent(Class[] types) throws IOException {
+    return delegate.getContent(types);
+  }
+
+  @Override
+  public String getContentEncoding() {
+    return delegate.getContentEncoding();
+  }
+
+  @Override
+  public int getContentLength() {
+    return delegate.getContentLength();
+  }
+
+  @Override
+  public String getContentType() {
+    return delegate.getContentType();
+  }
+
+  @Override
+  public long getDate() {
+    return delegate.getDate();
+  }
+
+  @Override
+  public boolean getDefaultUseCaches() {
+    return delegate.getDefaultUseCaches();
+  }
+
+  @Override
+  public boolean getDoInput() {
+    return delegate.getDoInput();
+  }
+
+  @Override
+  public boolean getDoOutput() {
+    return delegate.getDoOutput();
+  }
+
+  @Override
+  public long getExpiration() {
+    return delegate.getExpiration();
+  }
+
+  @Override
+  public String getHeaderField(int pos) {
+    return delegate.getHeaderField(pos);
+  }
+
+  @Override
+  public Map<String, List<String>> getHeaderFields() {
+    return delegate.getHeaderFields();
+  }
+
+  @Override
+  public Map<String, List<String>> getRequestProperties() {
+    return delegate.getRequestProperties();
+  }
+
+  @Override
+  public void addRequestProperty(String field, String newValue) {
+    delegate.addRequestProperty(field, newValue);
+  }
+
+  @Override
+  public String getHeaderField(String key) {
+    return delegate.getHeaderField(key);
+  }
+
+  @Override
+  public long getHeaderFieldDate(String field, long defaultValue) {
+    return delegate.getHeaderFieldDate(field, defaultValue);
+  }
+
+  @Override
+  public int getHeaderFieldInt(String field, int defaultValue) {
+    return delegate.getHeaderFieldInt(field, defaultValue);
+  }
+
+  @Override
+  public String getHeaderFieldKey(int position) {
+    return delegate.getHeaderFieldKey(position);
+  }
+
+  @Override
+  public long getIfModifiedSince() {
+    return delegate.getIfModifiedSince();
+  }
+
+  @Override
+  public InputStream getInputStream() throws IOException {
+    return delegate.getInputStream();
+  }
+
+  @Override
+  public long getLastModified() {
+    return delegate.getLastModified();
+  }
+
+  @Override
+  public OutputStream getOutputStream() throws IOException {
+    return delegate.getOutputStream();
+  }
+
+  @Override
+  public Permission getPermission() throws IOException {
+    return delegate.getPermission();
+  }
+
+  @Override
+  public String getRequestProperty(String field) {
+    return delegate.getRequestProperty(field);
+  }
+
+  @Override
+  public URL getURL() {
+    return delegate.getURL();
+  }
+
+  @Override
+  public boolean getUseCaches() {
+    return delegate.getUseCaches();
+  }
+
+  @Override
+  public void setAllowUserInteraction(boolean newValue) {
+    delegate.setAllowUserInteraction(newValue);
+  }
+
+  @Override
+  public void setDefaultUseCaches(boolean newValue) {
+    delegate.setDefaultUseCaches(newValue);
+  }
+
+  @Override
+  public void setDoInput(boolean newValue) {
+    delegate.setDoInput(newValue);
+  }
+
+  @Override
+  public void setDoOutput(boolean newValue) {
+    delegate.setDoOutput(newValue);
+  }
+
+  @Override
+  public void setIfModifiedSince(long newValue) {
+    delegate.setIfModifiedSince(newValue);
+  }
+
+  @Override
+  public void setRequestProperty(String field, String newValue) {
+    delegate.setRequestProperty(field, newValue);
+  }
+
+  @Override
+  public void setUseCaches(boolean newValue) {
+    delegate.setUseCaches(newValue);
+  }
+
+  @Override
+  public void setConnectTimeout(int timeoutMillis) {
+    delegate.setConnectTimeout(timeoutMillis);
+  }
+
+  @Override
+  public int getConnectTimeout() {
+    return delegate.getConnectTimeout();
+  }
+
+  @Override
+  public void setReadTimeout(int timeoutMillis) {
+    delegate.setReadTimeout(timeoutMillis);
+  }
+
+  @Override
+  public int getReadTimeout() {
+    return delegate.getReadTimeout();
+  }
+
+  @Override
+  public String toString() {
+    return delegate.toString();
+  }
+
+  @Override
+  public void setFixedLengthStreamingMode(int contentLength) {
+    delegate.setFixedLengthStreamingMode(contentLength);
+  }
+
+  @Override
+  public void setChunkedStreamingMode(int chunkLength) {
+    delegate.setChunkedStreamingMode(chunkLength);
+  }
+
+  @Override public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
+    delegate.hostnameVerifier = hostnameVerifier;
+  }
+
+  @Override public HostnameVerifier getHostnameVerifier() {
+    return delegate.hostnameVerifier;
+  }
+
+  @Override public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
+    delegate.sslSocketFactory = sslSocketFactory;
+  }
+
+  @Override public SSLSocketFactory getSSLSocketFactory() {
+    return delegate.sslSocketFactory;
+  }
+
+  private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
+    private HttpUrlConnectionDelegate(URL url, OkHttpClient client) {
+      super(url, client);
+    }
+
+    @Override protected HttpURLConnection getHttpConnectionToCache() {
+      return HttpsURLConnectionImpl.this;
+    }
+
+    public SecureCacheResponse getSecureCacheResponse() {
+      return httpEngine instanceof HttpsEngine
+          ? (SecureCacheResponse) httpEngine.getCacheResponse()
+          : null;
+    }
+  }
+
+  public static final class HttpsEngine extends HttpEngine {
+    /**
+     * Stash of HttpsEngine.connection.socket to implement requests like
+     * {@link #getCipherSuite} even after the connection has been recycled.
+     */
+    private SSLSocket sslSocket;
+
+    /**
+     * @param policy the HttpURLConnectionImpl with connection configuration
+     */
+    public HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
+        Connection connection, RetryableOutputStream requestBody)
+        throws IOException {
+      super(policy, method, requestHeaders, connection, requestBody);
+      this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
+    }
+
+    @Override protected void connected(Connection connection) {
+      this.sslSocket = (SSLSocket) connection.getSocket();
+    }
+
+    @Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) {
+      return cacheResponse instanceof SecureCacheResponse;
+    }
+
+    @Override protected boolean includeAuthorityInRequestLine() {
+      // Even if there is a proxy, it isn't involved. Always request just the file.
+      return false;
+    }
+
+    @Override protected TunnelRequest getTunnelConfig() {
+      String userAgent = requestHeaders.getUserAgent();
+      if (userAgent == null) {
+        userAgent = getDefaultUserAgent();
+      }
+
+      URL url = policy.getURL();
+      return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent,
+          requestHeaders.getProxyAuthorization());
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/cordova-android/blob/553a25ce/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java
----------------------------------------------------------------------
diff --git a/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java b/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java
new file mode 100644
index 0000000..c121abc
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java
@@ -0,0 +1,429 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.ProtocolException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * The HTTP status and unparsed header fields of a single HTTP message. Values
+ * are represented as uninterpreted strings; use {@link RequestHeaders} and
+ * {@link ResponseHeaders} for interpreted headers. This class maintains the
+ * order of the header fields within the HTTP message.
+ *
+ * <p>This class tracks fields line-by-line. A field with multiple comma-
+ * separated values on the same line will be treated as a field with a single
+ * value by this class. It is the caller's responsibility to detect and split
+ * on commas if their field permits multiple values. This simplifies use of
+ * single-valued fields whose values routinely contain commas, such as cookies
+ * or dates.
+ *
+ * <p>This class trims whitespace from values. It never returns values with
+ * leading or trailing whitespace.
+ */
+public final class RawHeaders {
+  private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
+    // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
+    @Override public int compare(String a, String b) {
+      if (a == b) {
+        return 0;
+      } else if (a == null) {
+        return -1;
+      } else if (b == null) {
+        return 1;
+      } else {
+        return String.CASE_INSENSITIVE_ORDER.compare(a, b);
+      }
+    }
+  };
+
+  private final List<String> namesAndValues = new ArrayList<String>(20);
+  private String requestLine;
+  private String statusLine;
+  private int httpMinorVersion = 1;
+  private int responseCode = -1;
+  private String responseMessage;
+
+  public RawHeaders() {
+  }
+
+  public RawHeaders(RawHeaders copyFrom) {
+    namesAndValues.addAll(copyFrom.namesAndValues);
+    requestLine = copyFrom.requestLine;
+    statusLine = copyFrom.statusLine;
+    httpMinorVersion = copyFrom.httpMinorVersion;
+    responseCode = copyFrom.responseCode;
+    responseMessage = copyFrom.responseMessage;
+  }
+
+  /** Sets the request line (like "GET / HTTP/1.1"). */
+  public void setRequestLine(String requestLine) {
+    requestLine = requestLine.trim();
+    this.requestLine = requestLine;
+  }
+
+  /** Sets the response status line (like "HTTP/1.0 200 OK"). */
+  public void setStatusLine(String statusLine) throws IOException {
+    // H T T P / 1 . 1   2 0 0   T e m p o r a r y   R e d i r e c t
+    // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
+    if (this.responseMessage != null) {
+      throw new IllegalStateException("statusLine is already set");
+    }
+    // We allow empty message without leading white space since some servers
+    // do not send the white space when the message is empty.
+    boolean hasMessage = statusLine.length() > 13;
+    if (!statusLine.startsWith("HTTP/1.")
+        || statusLine.length() < 12
+        || statusLine.charAt(8) != ' '
+        || (hasMessage && statusLine.charAt(12) != ' ')) {
+      throw new ProtocolException("Unexpected status line: " + statusLine);
+    }
+    int httpMinorVersion = statusLine.charAt(7) - '0';
+    if (httpMinorVersion < 0 || httpMinorVersion > 9) {
+      throw new ProtocolException("Unexpected status line: " + statusLine);
+    }
+    int responseCode;
+    try {
+      responseCode = Integer.parseInt(statusLine.substring(9, 12));
+    } catch (NumberFormatException e) {
+      throw new ProtocolException("Unexpected status line: " + statusLine);
+    }
+    this.responseMessage = hasMessage ? statusLine.substring(13) : "";
+    this.responseCode = responseCode;
+    this.statusLine = statusLine;
+    this.httpMinorVersion = httpMinorVersion;
+  }
+
+  public void computeResponseStatusLineFromSpdyHeaders() throws IOException {
+    String status = null;
+    String version = null;
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      String name = namesAndValues.get(i);
+      if (":status".equals(name)) {
+        status = namesAndValues.get(i + 1);
+      } else if (":version".equals(name)) {
+        version = namesAndValues.get(i + 1);
+      }
+    }
+    if (status == null || version == null) {
+      throw new ProtocolException("Expected ':status' and ':version' headers not present");
+    }
+    setStatusLine(version + " " + status);
+  }
+
+  /**
+   * @param method like "GET", "POST", "HEAD", etc.
+   * @param path like "/foo/bar.html"
+   * @param version like "HTTP/1.1"
+   * @param host like "www.android.com:1234"
+   * @param scheme like "https"
+   */
+  public void addSpdyRequestHeaders(String method, String path, String version, String host,
+      String scheme) {
+    // TODO: populate the statusLine for the client's benefit?
+    add(":method", method);
+    add(":scheme", scheme);
+    add(":path", path);
+    add(":version", version);
+    add(":host", host);
+  }
+
+  public String getStatusLine() {
+    return statusLine;
+  }
+
+  /**
+   * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0
+   * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown.
+   */
+  public int getHttpMinorVersion() {
+    return httpMinorVersion != -1 ? httpMinorVersion : 1;
+  }
+
+  /** Returns the HTTP status code or -1 if it is unknown. */
+  public int getResponseCode() {
+    return responseCode;
+  }
+
+  /** Returns the HTTP status message or null if it is unknown. */
+  public String getResponseMessage() {
+    return responseMessage;
+  }
+
+  /**
+   * Add an HTTP header line containing a field name, a literal colon, and a
+   * value.
+   */
+  public void addLine(String line) {
+    int index = line.indexOf(":");
+    if (index == -1) {
+      add("", line);
+    } else {
+      add(line.substring(0, index), line.substring(index + 1));
+    }
+  }
+
+  /** Add a field with the specified value. */
+  public void add(String fieldName, String value) {
+    if (fieldName == null) {
+      throw new IllegalArgumentException("fieldName == null");
+    }
+    if (value == null) {
+      // Given null values, the RI sends a malformed field line like
+      // "Accept\r\n". For platform compatibility and HTTP compliance, we
+      // print a warning and ignore null values.
+      Platform.get()
+          .logW("Ignoring HTTP header field '" + fieldName + "' because its value is null");
+      return;
+    }
+    namesAndValues.add(fieldName);
+    namesAndValues.add(value.trim());
+  }
+
+  public void removeAll(String fieldName) {
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
+        namesAndValues.remove(i); // field name
+        namesAndValues.remove(i); // value
+      }
+    }
+  }
+
+  public void addAll(String fieldName, List<String> headerFields) {
+    for (String value : headerFields) {
+      add(fieldName, value);
+    }
+  }
+
+  /**
+   * Set a field with the specified value. If the field is not found, it is
+   * added. If the field is found, the existing values are replaced.
+   */
+  public void set(String fieldName, String value) {
+    removeAll(fieldName);
+    add(fieldName, value);
+  }
+
+  /** Returns the number of field values. */
+  public int length() {
+    return namesAndValues.size() / 2;
+  }
+
+  /** Returns the field at {@code position} or null if that is out of range. */
+  public String getFieldName(int index) {
+    int fieldNameIndex = index * 2;
+    if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) {
+      return null;
+    }
+    return namesAndValues.get(fieldNameIndex);
+  }
+
+  /** Returns the value at {@code index} or null if that is out of range. */
+  public String getValue(int index) {
+    int valueIndex = index * 2 + 1;
+    if (valueIndex < 0 || valueIndex >= namesAndValues.size()) {
+      return null;
+    }
+    return namesAndValues.get(valueIndex);
+  }
+
+  /** Returns the last value corresponding to the specified field, or null. */
+  public String get(String fieldName) {
+    for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
+      if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
+        return namesAndValues.get(i + 1);
+      }
+    }
+    return null;
+  }
+
+  /** @param fieldNames a case-insensitive set of HTTP header field names. */
+  public RawHeaders getAll(Set<String> fieldNames) {
+    RawHeaders result = new RawHeaders();
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      String fieldName = namesAndValues.get(i);
+      if (fieldNames.contains(fieldName)) {
+        result.add(fieldName, namesAndValues.get(i + 1));
+      }
+    }
+    return result;
+  }
+
+  /** Returns bytes of a request header for sending on an HTTP transport. */
+  public byte[] toBytes() throws UnsupportedEncodingException {
+    StringBuilder result = new StringBuilder(256);
+    result.append(requestLine).append("\r\n");
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      result.append(namesAndValues.get(i))
+          .append(": ")
+          .append(namesAndValues.get(i + 1))
+          .append("\r\n");
+    }
+    result.append("\r\n");
+    return result.toString().getBytes("ISO-8859-1");
+  }
+
+  /** Parses bytes of a response header from an HTTP transport. */
+  public static RawHeaders fromBytes(InputStream in) throws IOException {
+    RawHeaders headers;
+    do {
+      headers = new RawHeaders();
+      headers.setStatusLine(Util.readAsciiLine(in));
+      readHeaders(in, headers);
+    } while (headers.getResponseCode() == HttpEngine.HTTP_CONTINUE);
+    return headers;
+  }
+
+  /** Reads headers or trailers into {@code out}. */
+  public static void readHeaders(InputStream in, RawHeaders out) throws IOException {
+    // parse the result headers until the first blank line
+    String line;
+    while ((line = Util.readAsciiLine(in)).length() != 0) {
+      out.addLine(line);
+    }
+  }
+
+  /**
+   * Returns an immutable map containing each field to its list of values. The
+   * status line is mapped to null.
+   */
+  public Map<String, List<String>> toMultimap(boolean response) {
+    Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      String fieldName = namesAndValues.get(i);
+      String value = namesAndValues.get(i + 1);
+
+      List<String> allValues = new ArrayList<String>();
+      List<String> otherValues = result.get(fieldName);
+      if (otherValues != null) {
+        allValues.addAll(otherValues);
+      }
+      allValues.add(value);
+      result.put(fieldName, Collections.unmodifiableList(allValues));
+    }
+    if (response && statusLine != null) {
+      result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine)));
+    } else if (requestLine != null) {
+      result.put(null, Collections.unmodifiableList(Collections.singletonList(requestLine)));
+    }
+    return Collections.unmodifiableMap(result);
+  }
+
+  /**
+   * Creates a new instance from the given map of fields to values. If
+   * present, the null field's last element will be used to set the status
+   * line.
+   */
+  public static RawHeaders fromMultimap(Map<String, List<String>> map, boolean response)
+      throws IOException {
+    if (!response) throw new UnsupportedOperationException();
+    RawHeaders result = new RawHeaders();
+    for (Entry<String, List<String>> entry : map.entrySet()) {
+      String fieldName = entry.getKey();
+      List<String> values = entry.getValue();
+      if (fieldName != null) {
+        result.addAll(fieldName, values);
+      } else if (!values.isEmpty()) {
+        result.setStatusLine(values.get(values.size() - 1));
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Returns a list of alternating names and values. Names are all lower case.
+   * No names are repeated. If any name has multiple values, they are
+   * concatenated using "\0" as a delimiter.
+   */
+  public List<String> toNameValueBlock() {
+    Set<String> names = new HashSet<String>();
+    List<String> result = new ArrayList<String>();
+    for (int i = 0; i < namesAndValues.size(); i += 2) {
+      String name = namesAndValues.get(i).toLowerCase(Locale.US);
+      String value = namesAndValues.get(i + 1);
+
+      // TODO: promote this check to where names and values are created
+      if (name.length() == 0
+          || value.length() == 0
+          || name.indexOf('\0') != -1
+          || value.indexOf('\0') != -1) {
+        throw new IllegalArgumentException("Unexpected header: " + name + ": " + value);
+      }
+
+      // Drop headers that are forbidden when layering HTTP over SPDY.
+      if (name.equals("connection")
+          || name.equals("host")
+          || name.equals("keep-alive")
+          || name.equals("proxy-connection")
+          || name.equals("transfer-encoding")) {
+        continue;
+      }
+
+      // If we haven't seen this name before, add the pair to the end of the list...
+      if (names.add(name)) {
+        result.add(name);
+        result.add(value);
+        continue;
+      }
+
+      // ...otherwise concatenate the existing values and this value.
+      for (int j = 0; j < result.size(); j += 2) {
+        if (name.equals(result.get(j))) {
+          result.set(j + 1, result.get(j + 1) + "\0" + value);
+          break;
+        }
+      }
+    }
+    return result;
+  }
+
+  public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) {
+    if (nameValueBlock.size() % 2 != 0) {
+      throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock);
+    }
+    RawHeaders result = new RawHeaders();
+    for (int i = 0; i < nameValueBlock.size(); i += 2) {
+      String name = nameValueBlock.get(i);
+      String values = nameValueBlock.get(i + 1);
+      for (int start = 0; start < values.length(); ) {
+        int end = values.indexOf('\0', start);
+        if (end == -1) {
+          end = values.length();
+        }
+        result.namesAndValues.add(name);
+        result.namesAndValues.add(values.substring(start, end));
+        start = end + 1;
+      }
+    }
+    return result;
+  }
+}