> 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;
+ }
+ }
+
+ private 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;
+ }
+ }
+
+ /**
+ * Closes the cache and deletes all of its stored values. This will delete
+ * all files in the cache directory including files that weren't created by
+ * the cache.
+ */
+ public void delete() throws IOException {
+ cache.delete();
+ }
+
+ public synchronized int getWriteAbortCount() {
+ return writeAbortCount;
+ }
+
+ public synchronized int getWriteSuccessCount() {
+ return writeSuccessCount;
+ }
+
+ private synchronized void trackResponse(ResponseSource source) {
+ requestCount++;
+
+ switch (source) {
+ case CACHE:
+ hitCount++;
+ break;
+ case CONDITIONAL_CACHE:
+ case NETWORK:
+ networkCount++;
+ break;
+ }
+ }
+
+ private 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:
+ * {@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
+ * }
+ *
+ * A typical HTTPS file looks like this:
+ *
{@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
+ * }
+ * 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.
+ *
+ * Next is the response status line, followed by the number of HTTP
+ * response header lines, followed by those lines.
+ *
+ *
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.length() > 0) {
+ 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.getMessage());
+ }
+ }
+
+ 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.getMessage());
+ }
+ }
+
+ public boolean matches(URI uri, String requestMethod,
+ Map> 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> 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> getHeaders() {
+ return entry.responseHeaders.toMultimap(true);
+ }
+
+ @Override public InputStream getBody() {
+ return in;
+ }
+
+ @Override public String getCipherSuite() {
+ return entry.cipherSuite;
+ }
+
+ @Override public List 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 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();
+ }
+ }
+}
diff --git a/framework/src/com/squareup/okhttp/OkHttpClient.java b/framework/src/com/squareup/okhttp/OkHttpClient.java
index d21cdb71..7834bd6b 100644
--- a/framework/src/com/squareup/okhttp/OkHttpClient.java
+++ b/framework/src/com/squareup/okhttp/OkHttpClient.java
@@ -17,12 +17,17 @@ package com.squareup.okhttp;
import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
+import com.squareup.okhttp.internal.http.OkResponseCache;
+import com.squareup.okhttp.internal.http.OkResponseCacheAdapter;
import java.net.CookieHandler;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.ResponseCache;
import java.net.URL;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
@@ -30,6 +35,7 @@ import javax.net.ssl.SSLSocketFactory;
/** Configures and creates HTTP connections. */
public final class OkHttpClient {
private Proxy proxy;
+ private Set failedRoutes = Collections.synchronizedSet(new LinkedHashSet());
private ProxySelector proxySelector;
private CookieHandler cookieHandler;
private ResponseCache responseCache;
@@ -102,6 +108,16 @@ public final class OkHttpClient {
return responseCache;
}
+ private OkResponseCache okResponseCache() {
+ if (responseCache instanceof HttpResponseCache) {
+ return ((HttpResponseCache) responseCache).okResponseCache;
+ } else if (responseCache != null) {
+ return new OkResponseCacheAdapter(responseCache);
+ } else {
+ return null;
+ }
+ }
+
/**
* Sets the socket factory used to secure HTTPS connections.
*
@@ -166,22 +182,24 @@ public final class OkHttpClient {
public HttpURLConnection open(URL url) {
String protocol = url.getProtocol();
+ OkHttpClient copy = copyWithDefaults();
if (protocol.equals("http")) {
- return new HttpURLConnectionImpl(url, copyWithDefaults());
+ return new HttpURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
} else if (protocol.equals("https")) {
- return new HttpsURLConnectionImpl(url, copyWithDefaults());
+ return new HttpsURLConnectionImpl(url, copy, copy.okResponseCache(), copy.failedRoutes);
} else {
throw new IllegalArgumentException("Unexpected protocol: " + protocol);
}
}
/**
- * Returns a copy of this OkHttpClient that uses the system-wide default for
+ * Returns a shallow copy of this OkHttpClient that uses the system-wide default for
* each field that hasn't been explicitly configured.
*/
private OkHttpClient copyWithDefaults() {
OkHttpClient result = new OkHttpClient();
result.proxy = proxy;
+ result.failedRoutes = failedRoutes;
result.proxySelector = proxySelector != null ? proxySelector : ProxySelector.getDefault();
result.cookieHandler = cookieHandler != null ? cookieHandler : CookieHandler.getDefault();
result.responseCache = responseCache != null ? responseCache : ResponseCache.getDefault();
diff --git a/framework/src/com/squareup/okhttp/Route.java b/framework/src/com/squareup/okhttp/Route.java
new file mode 100644
index 00000000..6968c604
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/Route.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * 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;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+/** Represents the route used by a connection to reach an endpoint. */
+public class Route {
+ final Address address;
+ final Proxy proxy;
+ final InetSocketAddress inetSocketAddress;
+ final boolean modernTls;
+
+ public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
+ boolean modernTls) {
+ if (address == null) throw new NullPointerException("address == null");
+ if (proxy == null) throw new NullPointerException("proxy == null");
+ if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null");
+ this.address = address;
+ this.proxy = proxy;
+ this.inetSocketAddress = inetSocketAddress;
+ this.modernTls = modernTls;
+ }
+
+ /** Returns the {@link Address} of this route. */
+ public Address getAddress() {
+ return address;
+ }
+
+ /**
+ * Returns the {@link Proxy} of this route.
+ *
+ * Warning: This may be different than the proxy returned
+ * by {@link #getAddress}! That is the proxy that the user asked to be
+ * connected to; this returns the proxy that they were actually connected
+ * to. The two may disagree when a proxy selector selects a different proxy
+ * for a connection.
+ */
+ public Proxy getProxy() {
+ return proxy;
+ }
+
+ /** Returns the {@link InetSocketAddress} of this route. */
+ public InetSocketAddress getSocketAddress() {
+ return inetSocketAddress;
+ }
+
+ /** Returns true if this route uses modern tls. */
+ public boolean isModernTls() {
+ return modernTls;
+ }
+
+ /** Returns a copy of this route with flipped tls mode. */
+ public Route flipTlsMode() {
+ return new Route(address, proxy, inetSocketAddress, !modernTls);
+ }
+
+ @Override public boolean equals(Object obj) {
+ if (obj instanceof Route) {
+ Route other = (Route) obj;
+ return (address.equals(other.address)
+ && proxy.equals(other.proxy)
+ && inetSocketAddress.equals(other.inetSocketAddress)
+ && modernTls == other.modernTls);
+ }
+ return false;
+ }
+
+ @Override public int hashCode() {
+ int result = 17;
+ result = 31 * result + address.hashCode();
+ result = 31 * result + proxy.hashCode();
+ result = 31 * result + inetSocketAddress.hashCode();
+ result = result + (modernTls ? (31 * result) : 0);
+ return result;
+ }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java b/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java
new file mode 100644
index 00000000..78c9691e
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/AbstractOutputStream.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An output stream for an HTTP request body.
+ *
+ * Since a single socket's output stream may be used to write multiple HTTP
+ * requests to the same server, subclasses should not close the socket stream.
+ */
+public abstract class AbstractOutputStream extends OutputStream {
+ protected boolean closed;
+
+ @Override public final void write(int data) throws IOException {
+ write(new byte[] { (byte) data });
+ }
+
+ protected final void checkNotClosed() throws IOException {
+ if (closed) {
+ throw new IOException("stream closed");
+ }
+ }
+
+ /** Returns true if this stream was closed locally. */
+ public boolean isClosed() {
+ return closed;
+ }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/DiskLruCache.java b/framework/src/com/squareup/okhttp/internal/DiskLruCache.java
index 00fe2f18..f7fcb1ed 100644
--- a/framework/src/com/squareup/okhttp/internal/DiskLruCache.java
+++ b/framework/src/com/squareup/okhttp/internal/DiskLruCache.java
@@ -23,7 +23,6 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
-import java.io.FileWriter;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -32,23 +31,22 @@ import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
-
-import static com.squareup.okhttp.internal.Util.UTF_8;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* A cache that uses a bounded amount of space on a filesystem. Each cache
- * entry has a string key and a fixed number of values. Values are byte
- * sequences, accessible as streams or files. Each value must be between {@code
- * 0} and {@code Integer.MAX_VALUE} bytes in length.
+ * entry has a string key and a fixed number of values. Each key must match
+ * the regex [a-z0-9_-]{1,64}. Values are byte sequences,
+ * accessible as streams or files. Each value must be between {@code 0} and
+ * {@code Integer.MAX_VALUE} bytes in length.
*
*
The cache stores its data in a directory on the filesystem. This
* directory must be exclusive to the cache; the cache may delete or overwrite
@@ -66,12 +64,12 @@ import static com.squareup.okhttp.internal.Util.UTF_8;
* entry may have only one editor at one time; if a value is not available to be
* edited then {@link #edit} will return null.
*
- * - When an entry is being created it is necessary to
- * supply a full set of values; the empty value should be used as a
- * placeholder if necessary.
- *
- When an entry is being edited, it is not necessary
- * to supply data for every value; values default to their previous
- * value.
+ *
- When an entry is being created it is necessary to
+ * supply a full set of values; the empty value should be used as a
+ * placeholder if necessary.
+ *
- When an entry is being edited, it is not necessary
+ * to supply data for every value; values default to their previous
+ * value.
*
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
@@ -89,58 +87,63 @@ import static com.squareup.okhttp.internal.Util.UTF_8;
*/
public final class DiskLruCache implements Closeable {
static final String JOURNAL_FILE = "journal";
- static final String JOURNAL_FILE_TMP = "journal.tmp";
+ static final String JOURNAL_FILE_TEMP = "journal.tmp";
+ static final String JOURNAL_FILE_BACKUP = "journal.bkp";
static final String MAGIC = "libcore.io.DiskLruCache";
static final String VERSION_1 = "1";
static final long ANY_SEQUENCE_NUMBER = -1;
+ static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}");
private static final String CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE";
private static final String READ = "READ";
- // This cache uses a journal file named "journal". A typical journal file
- // looks like this:
- // libcore.io.DiskLruCache
- // 1
- // 100
- // 2
- //
- // CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
- // DIRTY 335c4c6028171cfddfbaae1a9c313c52
- // CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
- // REMOVE 335c4c6028171cfddfbaae1a9c313c52
- // DIRTY 1ab96a171faeeee38496d8b330771a7a
- // CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
- // READ 335c4c6028171cfddfbaae1a9c313c52
- // READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
- //
- // The first five lines of the journal form its header. They are the
- // constant string "libcore.io.DiskLruCache", the disk cache's version,
- // the application's version, the value count, and a blank line.
- //
- // Each of the subsequent lines in the file is a record of the state of a
- // cache entry. Each line contains space-separated values: a state, a key,
- // and optional state-specific values.
- // o DIRTY lines track that an entry is actively being created or updated.
- // Every successful DIRTY action should be followed by a CLEAN or REMOVE
- // action. DIRTY lines without a matching CLEAN or REMOVE indicate that
- // temporary files may need to be deleted.
- // o CLEAN lines track a cache entry that has been successfully published
- // and may be read. A publish line is followed by the lengths of each of
- // its values.
- // o READ lines track accesses for LRU.
- // o REMOVE lines track entries that have been deleted.
- //
- // The journal file is appended to as cache operations occur. The journal may
- // occasionally be compacted by dropping redundant lines. A temporary file named
- // "journal.tmp" will be used during compaction; that file should be deleted if
- // it exists when the cache is opened.
+ /*
+ * This cache uses a journal file named "journal". A typical journal file
+ * looks like this:
+ * libcore.io.DiskLruCache
+ * 1
+ * 100
+ * 2
+ *
+ * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
+ * DIRTY 335c4c6028171cfddfbaae1a9c313c52
+ * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
+ * REMOVE 335c4c6028171cfddfbaae1a9c313c52
+ * DIRTY 1ab96a171faeeee38496d8b330771a7a
+ * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
+ * READ 335c4c6028171cfddfbaae1a9c313c52
+ * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
+ *
+ * The first five lines of the journal form its header. They are the
+ * constant string "libcore.io.DiskLruCache", the disk cache's version,
+ * the application's version, the value count, and a blank line.
+ *
+ * Each of the subsequent lines in the file is a record of the state of a
+ * cache entry. Each line contains space-separated values: a state, a key,
+ * and optional state-specific values.
+ * o DIRTY lines track that an entry is actively being created or updated.
+ * Every successful DIRTY action should be followed by a CLEAN or REMOVE
+ * action. DIRTY lines without a matching CLEAN or REMOVE indicate that
+ * temporary files may need to be deleted.
+ * o CLEAN lines track a cache entry that has been successfully published
+ * and may be read. A publish line is followed by the lengths of each of
+ * its values.
+ * o READ lines track accesses for LRU.
+ * o REMOVE lines track entries that have been deleted.
+ *
+ * The journal file is appended to as cache operations occur. The journal may
+ * occasionally be compacted by dropping redundant lines. A temporary file named
+ * "journal.tmp" will be used during compaction; that file should be deleted if
+ * it exists when the cache is opened.
+ */
private final File directory;
private final File journalFile;
private final File journalFileTmp;
+ private final File journalFileBackup;
private final int appVersion;
- private final long maxSize;
+ private long maxSize;
private final int valueCount;
private long size = 0;
private Writer journalWriter;
@@ -156,13 +159,13 @@ public final class DiskLruCache implements Closeable {
private long nextSequenceNumber = 0;
/** This cache uses a single background thread to evict entries. */
- private final ExecutorService executorService =
+ final ThreadPoolExecutor executorService =
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue());
private final Callable cleanupCallable = new Callable() {
- @Override public Void call() throws Exception {
+ public Void call() throws Exception {
synchronized (DiskLruCache.this) {
if (journalWriter == null) {
- return null; // closed
+ return null; // Closed.
}
trimToSize();
if (journalRebuildRequired()) {
@@ -178,7 +181,8 @@ public final class DiskLruCache implements Closeable {
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
- this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
+ this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
+ this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
}
@@ -201,26 +205,35 @@ public final class DiskLruCache implements Closeable {
throw new IllegalArgumentException("valueCount <= 0");
}
- // prefer to pick up where we left off
+ // If a bkp file exists, use it instead.
+ File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
+ if (backupFile.exists()) {
+ File journalFile = new File(directory, JOURNAL_FILE);
+ // If journal file also exists just delete backup file.
+ if (journalFile.exists()) {
+ backupFile.delete();
+ } else {
+ renameTo(backupFile, journalFile, false);
+ }
+ }
+
+ // Prefer to pick up where we left off.
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
- cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true));
+ cache.journalWriter = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
return cache;
} catch (IOException journalIsCorrupt) {
- Platform.get()
- .logW("DiskLruCache "
- + directory
- + " is corrupt: "
- + journalIsCorrupt.getMessage()
- + ", removing");
+ Platform.get().logW("DiskLruCache " + directory + " is corrupt: "
+ + journalIsCorrupt.getMessage() + ", removing");
cache.delete();
}
}
- // create a new empty cache
+ // Create a new empty cache.
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
@@ -235,42 +248,47 @@ public final class DiskLruCache implements Closeable {
String appVersionString = reader.readLine();
String valueCountString = reader.readLine();
String blank = reader.readLine();
- if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(appVersion)
- .equals(appVersionString) || !Integer.toString(valueCount).equals(valueCountString) || !""
- .equals(blank)) {
- throw new IOException("unexpected journal header: ["
- + magic
- + ", "
- + version
- + ", "
- + valueCountString
- + ", "
- + blank
- + "]");
+ if (!MAGIC.equals(magic)
+ || !VERSION_1.equals(version)
+ || !Integer.toString(appVersion).equals(appVersionString)
+ || !Integer.toString(valueCount).equals(valueCountString)
+ || !"".equals(blank)) {
+ throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
+ + valueCountString + ", " + blank + "]");
}
+ int lineCount = 0;
while (true) {
try {
readJournalLine(reader.readLine());
+ lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
+ redundantOpCount = lineCount - lruEntries.size();
} finally {
Util.closeQuietly(reader);
}
}
private void readJournalLine(String line) throws IOException {
- String[] parts = line.split(" ");
- if (parts.length < 2) {
+ int firstSpace = line.indexOf(' ');
+ if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
- String key = parts[1];
- if (parts[0].equals(REMOVE) && parts.length == 2) {
- lruEntries.remove(key);
- return;
+ int keyBegin = firstSpace + 1;
+ int secondSpace = line.indexOf(' ', keyBegin);
+ final String key;
+ if (secondSpace == -1) {
+ key = line.substring(keyBegin);
+ if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
+ lruEntries.remove(key);
+ return;
+ }
+ } else {
+ key = line.substring(keyBegin, secondSpace);
}
Entry entry = lruEntries.get(key);
@@ -279,14 +297,15 @@ public final class DiskLruCache implements Closeable {
lruEntries.put(key, entry);
}
- if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
+ if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
+ String[] parts = line.substring(secondSpace + 1).split(" ");
entry.readable = true;
entry.currentEditor = null;
- entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length));
- } else if (parts[0].equals(DIRTY) && parts.length == 2) {
+ entry.setLengths(parts);
+ } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
entry.currentEditor = new Editor(entry);
- } else if (parts[0].equals(READ) && parts.length == 2) {
- // this work was already done by calling lruEntries.get()
+ } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
+ // This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
@@ -324,32 +343,53 @@ public final class DiskLruCache implements Closeable {
journalWriter.close();
}
- Writer writer = new BufferedWriter(new FileWriter(journalFileTmp));
- writer.write(MAGIC);
- writer.write("\n");
- writer.write(VERSION_1);
- writer.write("\n");
- writer.write(Integer.toString(appVersion));
- writer.write("\n");
- writer.write(Integer.toString(valueCount));
- writer.write("\n");
- writer.write("\n");
+ Writer writer = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
+ try {
+ writer.write(MAGIC);
+ writer.write("\n");
+ writer.write(VERSION_1);
+ writer.write("\n");
+ writer.write(Integer.toString(appVersion));
+ writer.write("\n");
+ writer.write(Integer.toString(valueCount));
+ writer.write("\n");
+ writer.write("\n");
- for (Entry entry : lruEntries.values()) {
- if (entry.currentEditor != null) {
- writer.write(DIRTY + ' ' + entry.key + '\n');
- } else {
- writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ for (Entry entry : lruEntries.values()) {
+ if (entry.currentEditor != null) {
+ writer.write(DIRTY + ' ' + entry.key + '\n');
+ } else {
+ writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ }
}
+ } finally {
+ writer.close();
}
- writer.close();
- journalFileTmp.renameTo(journalFile);
- journalWriter = new BufferedWriter(new FileWriter(journalFile, true));
+ if (journalFile.exists()) {
+ renameTo(journalFile, journalFileBackup, true);
+ }
+ renameTo(journalFileTmp, journalFile, false);
+ journalFileBackup.delete();
+
+ journalWriter = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}
private static void deleteIfExists(File file) throws IOException {
- file.delete();
+ if (file.exists() && !file.delete()) {
+ throw new IOException();
+ }
+ }
+
+ private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
+ if (deleteDestination) {
+ deleteIfExists(to);
+ }
+ if (!from.renameTo(to)) {
+ throw new IOException();
+ }
}
/**
@@ -378,7 +418,14 @@ public final class DiskLruCache implements Closeable {
ins[i] = new FileInputStream(entry.getCleanFile(i));
}
} catch (FileNotFoundException e) {
- // a file must have been deleted manually!
+ // A file must have been deleted manually!
+ for (int i = 0; i < valueCount; i++) {
+ if (ins[i] != null) {
+ Util.closeQuietly(ins[i]);
+ } else {
+ break;
+ }
+ }
return null;
}
@@ -388,7 +435,7 @@ public final class DiskLruCache implements Closeable {
executorService.submit(cleanupCallable);
}
- return new Snapshot(key, entry.sequenceNumber, ins);
+ return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
}
/**
@@ -405,19 +452,19 @@ public final class DiskLruCache implements Closeable {
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
- return null; // snapshot is stale
+ return null; // Snapshot is stale.
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
- return null; // another edit is in progress
+ return null; // Another edit is in progress.
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
- // flush the journal before creating files to prevent file leaks
+ // Flush the journal before creating files to prevent file leaks.
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
@@ -432,10 +479,19 @@ public final class DiskLruCache implements Closeable {
* Returns the maximum number of bytes that this cache should use to store
* its data.
*/
- public long maxSize() {
+ public long getMaxSize() {
return maxSize;
}
+ /**
+ * Changes the maximum number of bytes the cache can store and queues a job
+ * to trim the existing store, if necessary.
+ */
+ public synchronized void setMaxSize(long maxSize) {
+ this.maxSize = maxSize;
+ executorService.submit(cleanupCallable);
+ }
+
/**
* Returns the number of bytes currently being used to store the values in
* this cache. This may be greater than the max size if a background
@@ -451,7 +507,7 @@ public final class DiskLruCache implements Closeable {
throw new IllegalStateException();
}
- // if this edit is creating the entry for the first time, every index must have a value
+ // If this edit is creating the entry for the first time, every index must have a value.
if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!editor.written[i]) {
@@ -460,7 +516,6 @@ public final class DiskLruCache implements Closeable {
}
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
- Platform.get().logW("DiskLruCache: Newly created entry doesn't have file for index " + i);
return;
}
}
@@ -494,6 +549,7 @@ public final class DiskLruCache implements Closeable {
lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
}
+ journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
@@ -506,7 +562,8 @@ public final class DiskLruCache implements Closeable {
*/
private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
- return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size();
+ return redundantOpCount >= redundantOpCompactThreshold //
+ && redundantOpCount >= lruEntries.size();
}
/**
@@ -564,7 +621,7 @@ public final class DiskLruCache implements Closeable {
/** Closes this cache. Stored values will remain on the filesystem. */
public synchronized void close() throws IOException {
if (journalWriter == null) {
- return; // already closed
+ return; // Already closed.
}
for (Entry entry : new ArrayList(lruEntries.values())) {
if (entry.currentEditor != null) {
@@ -594,14 +651,14 @@ public final class DiskLruCache implements Closeable {
}
private void validateKey(String key) {
- if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
- throw new IllegalArgumentException(
- "keys must not contain spaces or newlines: \"" + key + "\"");
+ Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\"");
}
}
private static String inputStreamToString(InputStream in) throws IOException {
- return Util.readFully(new InputStreamReader(in, UTF_8));
+ return Util.readFully(new InputStreamReader(in, Util.UTF_8));
}
/** A snapshot of the values for an entry. */
@@ -609,11 +666,13 @@ public final class DiskLruCache implements Closeable {
private final String key;
private final long sequenceNumber;
private final InputStream[] ins;
+ private final long[] lengths;
- private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
+ private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.ins = ins;
+ this.lengths = lengths;
}
/**
@@ -635,18 +694,31 @@ public final class DiskLruCache implements Closeable {
return inputStreamToString(getInputStream(index));
}
- @Override public void close() {
+ /** Returns the byte length of the value for {@code index}. */
+ public long getLength(int index) {
+ return lengths[index];
+ }
+
+ public void close() {
for (InputStream in : ins) {
Util.closeQuietly(in);
}
}
}
+ private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ // Eat all writes silently. Nom nom.
+ }
+ };
+
/** Edits the values for an entry. */
public final class Editor {
private final Entry entry;
private final boolean[] written;
private boolean hasErrors;
+ private boolean committed;
private Editor(Entry entry) {
this.entry = entry;
@@ -665,7 +737,11 @@ public final class DiskLruCache implements Closeable {
if (!entry.readable) {
return null;
}
- return new FileInputStream(entry.getCleanFile(index));
+ try {
+ return new FileInputStream(entry.getCleanFile(index));
+ } catch (FileNotFoundException e) {
+ return null;
+ }
}
}
@@ -693,7 +769,21 @@ public final class DiskLruCache implements Closeable {
if (!entry.readable) {
written[index] = true;
}
- return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
+ File dirtyFile = entry.getDirtyFile(index);
+ FileOutputStream outputStream;
+ try {
+ outputStream = new FileOutputStream(dirtyFile);
+ } catch (FileNotFoundException e) {
+ // Attempt to recreate the cache directory.
+ directory.mkdirs();
+ try {
+ outputStream = new FileOutputStream(dirtyFile);
+ } catch (FileNotFoundException e2) {
+ // We are unable to recover. Silently eat the writes.
+ return NULL_OUTPUT_STREAM;
+ }
+ }
+ return new FaultHidingOutputStream(outputStream);
}
}
@@ -701,7 +791,7 @@ public final class DiskLruCache implements Closeable {
public void set(int index, String value) throws IOException {
Writer writer = null;
try {
- writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
+ writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
writer.write(value);
} finally {
Util.closeQuietly(writer);
@@ -715,10 +805,11 @@ public final class DiskLruCache implements Closeable {
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
- remove(entry.key); // the previous entry is stale
+ remove(entry.key); // The previous entry is stale.
} else {
completeEdit(this, true);
}
+ committed = true;
}
/**
@@ -729,7 +820,16 @@ public final class DiskLruCache implements Closeable {
completeEdit(this, false);
}
- private final class FaultHidingOutputStream extends FilterOutputStream {
+ public void abortUnlessCommitted() {
+ if (!committed) {
+ try {
+ abort();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+
+ private class FaultHidingOutputStream extends FilterOutputStream {
private FaultHidingOutputStream(OutputStream out) {
super(out);
}
@@ -812,7 +912,7 @@ public final class DiskLruCache implements Closeable {
}
private IOException invalidLengths(String[] strings) throws IOException {
- throw new IOException("unexpected journal line: " + Arrays.toString(strings));
+ throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
}
public File getCleanFile(int i) {
diff --git a/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java b/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java
new file mode 100644
index 00000000..c32b27ae
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/FaultRecoveringOutputStream.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * 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;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
+
+/**
+ * An output stream wrapper that recovers from failures in the underlying stream
+ * by replacing it with another stream. This class buffers a fixed amount of
+ * data under the assumption that failures occur early in a stream's life.
+ * If a failure occurs after the buffer has been exhausted, no recovery is
+ * attempted.
+ *
+ * Subclasses must override {@link #replacementStream} which will request a
+ * replacement stream each time an {@link IOException} is encountered on the
+ * current stream.
+ */
+public abstract class FaultRecoveringOutputStream extends AbstractOutputStream {
+ private final int maxReplayBufferLength;
+
+ /** Bytes to transmit on the replacement stream, or null if no recovery is possible. */
+ private ByteArrayOutputStream replayBuffer;
+ private OutputStream out;
+
+ /**
+ * @param maxReplayBufferLength the maximum number of successfully written
+ * bytes to buffer so they can be replayed in the event of an error.
+ * Failure recoveries are not possible once this limit has been exceeded.
+ */
+ public FaultRecoveringOutputStream(int maxReplayBufferLength, OutputStream out) {
+ if (maxReplayBufferLength < 0) throw new IllegalArgumentException();
+ this.maxReplayBufferLength = maxReplayBufferLength;
+ this.replayBuffer = new ByteArrayOutputStream(maxReplayBufferLength);
+ this.out = out;
+ }
+
+ @Override public final void write(byte[] buffer, int offset, int count) throws IOException {
+ if (closed) throw new IOException("stream closed");
+ checkOffsetAndCount(buffer.length, offset, count);
+
+ while (true) {
+ try {
+ out.write(buffer, offset, count);
+
+ if (replayBuffer != null) {
+ if (count + replayBuffer.size() > maxReplayBufferLength) {
+ // Failure recovery is no longer possible once we overflow the replay buffer.
+ replayBuffer = null;
+ } else {
+ // Remember the written bytes to the replay buffer.
+ replayBuffer.write(buffer, offset, count);
+ }
+ }
+ return;
+ } catch (IOException e) {
+ if (!recover(e)) throw e;
+ }
+ }
+ }
+
+ @Override public final void flush() throws IOException {
+ if (closed) {
+ return; // don't throw; this stream might have been closed on the caller's behalf
+ }
+ while (true) {
+ try {
+ out.flush();
+ return;
+ } catch (IOException e) {
+ if (!recover(e)) throw e;
+ }
+ }
+ }
+
+ @Override public final void close() throws IOException {
+ if (closed) {
+ return;
+ }
+ while (true) {
+ try {
+ out.close();
+ closed = true;
+ return;
+ } catch (IOException e) {
+ if (!recover(e)) throw e;
+ }
+ }
+ }
+
+ /**
+ * Attempt to replace {@code out} with another equivalent stream. Returns true
+ * if a suitable replacement stream was found.
+ */
+ private boolean recover(IOException e) {
+ if (replayBuffer == null) {
+ return false; // Can't recover because we've dropped data that we would need to replay.
+ }
+
+ while (true) {
+ OutputStream replacementStream = null;
+ try {
+ replacementStream = replacementStream(e);
+ if (replacementStream == null) {
+ return false;
+ }
+ replaceStream(replacementStream);
+ return true;
+ } catch (IOException replacementStreamFailure) {
+ // The replacement was also broken. Loop to ask for another replacement.
+ Util.closeQuietly(replacementStream);
+ e = replacementStreamFailure;
+ }
+ }
+ }
+
+ /**
+ * Returns true if errors in the underlying stream can currently be recovered.
+ */
+ public boolean isRecoverable() {
+ return replayBuffer != null;
+ }
+
+ /**
+ * Replaces the current output stream with {@code replacementStream}, writing
+ * any replay bytes to it if they exist. The current output stream is closed.
+ */
+ public final void replaceStream(OutputStream replacementStream) throws IOException {
+ if (!isRecoverable()) {
+ throw new IllegalStateException();
+ }
+ if (this.out == replacementStream) {
+ return; // Don't replace a stream with itself.
+ }
+ replayBuffer.writeTo(replacementStream);
+ Util.closeQuietly(out);
+ out = replacementStream;
+ }
+
+ /**
+ * Returns a replacement output stream to recover from {@code e} thrown by the
+ * previous stream. Returns a new OutputStream if recovery was successful, in
+ * which case all previously-written data will be replayed. Returns null if
+ * the failure cannot be recovered.
+ */
+ protected abstract OutputStream replacementStream(IOException e) throws IOException;
+}
diff --git a/framework/src/com/squareup/okhttp/internal/Platform.java b/framework/src/com/squareup/okhttp/internal/Platform.java
index 75cd66f7..6b4ac343 100644
--- a/framework/src/com/squareup/okhttp/internal/Platform.java
+++ b/framework/src/com/squareup/okhttp/internal/Platform.java
@@ -17,6 +17,7 @@
package com.squareup.okhttp.internal;
import com.squareup.okhttp.OkHttpClient;
+import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
@@ -24,6 +25,7 @@ import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
+import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
@@ -123,8 +125,27 @@ public class Platform {
}
}
+ /**
+ * Returns the maximum transmission unit of the network interface used by
+ * {@code socket}, or a reasonable default if this platform doesn't expose the
+ * MTU to the application layer.
+ *
+ *
The returned value should only be used as an optimization; such as to
+ * size buffers efficiently.
+ */
+ public int getMtu(Socket socket) throws IOException {
+ return 1400; // Smaller than 1500 to leave room for headers on interfaces like PPPoE.
+ }
+
/** Attempt to match the host runtime to a capable Platform implementation. */
private static Platform findPlatform() {
+ Method getMtu;
+ try {
+ getMtu = NetworkInterface.class.getMethod("getMTU");
+ } catch (NoSuchMethodException e) {
+ return new Platform(); // No Java 1.6 APIs. It's either Java 1.5, Android 2.2 or earlier.
+ }
+
// Attempt to find Android 2.3+ APIs.
Class> openSslSocketClass;
Method setUseSessionTickets;
@@ -138,10 +159,10 @@ public class Platform {
try {
Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
- return new Android41(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols,
- getNpnSelectedProtocol);
+ return new Android41(getMtu, openSslSocketClass, setUseSessionTickets, setHostname,
+ setNpnProtocols, getNpnSelectedProtocol);
} catch (NoSuchMethodException ignored) {
- return new Android23(openSslSocketClass, setUseSessionTickets, setHostname);
+ return new Android23(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
}
} catch (ClassNotFoundException ignored) {
// This isn't an Android runtime.
@@ -158,12 +179,35 @@ public class Platform {
Class> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
- return new JdkWithJettyNpnPlatform(putMethod, getMethod, clientProviderClass,
+ return new JdkWithJettyNpnPlatform(getMtu, putMethod, getMethod, clientProviderClass,
serverProviderClass);
} catch (ClassNotFoundException ignored) {
- return new Platform(); // NPN isn't on the classpath.
+ // NPN isn't on the classpath.
} catch (NoSuchMethodException ignored) {
- return new Platform(); // The NPN version isn't what we expect.
+ // The NPN version isn't what we expect.
+ }
+
+ return getMtu != null ? new Java5(getMtu) : new Platform();
+ }
+
+ private static class Java5 extends Platform {
+ private final Method getMtu;
+
+ private Java5(Method getMtu) {
+ this.getMtu = getMtu;
+ }
+
+ @Override public int getMtu(Socket socket) throws IOException {
+ try {
+ NetworkInterface networkInterface = NetworkInterface.getByInetAddress(
+ socket.getLocalAddress());
+ return (Integer) getMtu.invoke(networkInterface);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError(e);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
+ throw new RuntimeException(e.getCause());
+ }
}
}
@@ -171,13 +215,14 @@ public class Platform {
* Android version 2.3 and newer support TLS session tickets and server name
* indication (SNI).
*/
- private static class Android23 extends Platform {
+ private static class Android23 extends Java5 {
protected final Class> openSslSocketClass;
private final Method setUseSessionTickets;
private final Method setHostname;
- private Android23(Class> openSslSocketClass, Method setUseSessionTickets,
+ private Android23(Method getMtu, Class> openSslSocketClass, Method setUseSessionTickets,
Method setHostname) {
+ super(getMtu);
this.openSslSocketClass = openSslSocketClass;
this.setUseSessionTickets = setUseSessionTickets;
this.setHostname = setHostname;
@@ -204,9 +249,9 @@ public class Platform {
private final Method setNpnProtocols;
private final Method getNpnSelectedProtocol;
- private Android41(Class> openSslSocketClass, Method setUseSessionTickets, Method setHostname,
- Method setNpnProtocols, Method getNpnSelectedProtocol) {
- super(openSslSocketClass, setUseSessionTickets, setHostname);
+ private Android41(Method getMtu, Class> openSslSocketClass, Method setUseSessionTickets,
+ Method setHostname, Method setNpnProtocols, Method getNpnSelectedProtocol) {
+ super(getMtu, openSslSocketClass, setUseSessionTickets, setHostname);
this.setNpnProtocols = setNpnProtocols;
this.getNpnSelectedProtocol = getNpnSelectedProtocol;
}
@@ -242,14 +287,15 @@ public class Platform {
* OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class
* path.
*/
- private static class JdkWithJettyNpnPlatform extends Platform {
+ private static class JdkWithJettyNpnPlatform extends Java5 {
private final Method getMethod;
private final Method putMethod;
private final Class> clientProviderClass;
private final Class> serverProviderClass;
- public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class> clientProviderClass,
- Class> serverProviderClass) {
+ public JdkWithJettyNpnPlatform(Method getMtu, Method putMethod, Method getMethod,
+ Class> clientProviderClass, Class> serverProviderClass) {
+ super(getMtu);
this.putMethod = putMethod;
this.getMethod = getMethod;
this.clientProviderClass = clientProviderClass;
diff --git a/framework/src/com/squareup/okhttp/internal/StrictLineReader.java b/framework/src/com/squareup/okhttp/internal/StrictLineReader.java
index 93f17540..3ddc693c 100644
--- a/framework/src/com/squareup/okhttp/internal/StrictLineReader.java
+++ b/framework/src/com/squareup/okhttp/internal/StrictLineReader.java
@@ -21,26 +21,23 @@ import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
-import static com.squareup.okhttp.internal.Util.ISO_8859_1;
-import static com.squareup.okhttp.internal.Util.US_ASCII;
-import static com.squareup.okhttp.internal.Util.UTF_8;
-
/**
* Buffers input from an {@link InputStream} for reading lines.
*
- * This class is used for buffered reading of lines. For purposes of this class, a line ends with
+ *
This class is used for buffered reading of lines. For purposes of this class, a line ends with
* "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated line at
* end of input is invalid and will be ignored, the caller may use {@code hasUnterminatedLine()}
* to detect it after catching the {@code EOFException}.
*
- * This class is intended for reading input that strictly consists of lines, such as line-based
- * cache entries or cache journal. Unlike the {@link BufferedReader} which in conjunction with
- * {@link InputStreamReader} provides similar functionality, this class uses different
+ *
This class is intended for reading input that strictly consists of lines, such as line-based
+ * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
+ * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
* end-of-input reporting and a more restrictive definition of a line.
*
- * This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
+ *
This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
* and 10, respectively, and the representation of no other character contains these values.
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
* The default charset is US_ASCII.
@@ -52,42 +49,22 @@ public class StrictLineReader implements Closeable {
private final InputStream in;
private final Charset charset;
- // Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
- // and the data in the range [pos, end) is buffered for reading. At end of input, if there is
- // an unterminated line, we set end == -1, otherwise end == pos. If the underlying
- // {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
+ /*
+ * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
+ * and the data in the range [pos, end) is buffered for reading. At end of input, if there is
+ * an unterminated line, we set end == -1, otherwise end == pos. If the underlying
+ * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
+ */
private byte[] buf;
private int pos;
private int end;
- /**
- * Constructs a new {@code StrictLineReader} with the default capacity and charset.
- *
- * @param in the {@code InputStream} to read data from.
- * @throws NullPointerException if {@code in} is null.
- */
- public StrictLineReader(InputStream in) {
- this(in, 8192);
- }
-
- /**
- * Constructs a new {@code LineReader} with the specified capacity and the default charset.
- *
- * @param in the {@code InputStream} to read data from.
- * @param capacity the capacity of the buffer.
- * @throws NullPointerException if {@code in} is null.
- * @throws IllegalArgumentException for negative or zero {@code capacity}.
- */
- public StrictLineReader(InputStream in, int capacity) {
- this(in, capacity, US_ASCII);
- }
-
/**
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
*
* @param in the {@code InputStream} to read data from.
- * @param charset the charset used to decode data.
- * Only US-ASCII, UTF-8 and ISO-8859-1 is supported.
+ * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
+ * supported.
* @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if the specified charset is not supported.
*/
@@ -100,11 +77,11 @@ public class StrictLineReader implements Closeable {
*
* @param in the {@code InputStream} to read data from.
* @param capacity the capacity of the buffer.
- * @param charset the charset used to decode data.
- * Only US-ASCII, UTF-8 and ISO-8859-1 is supported.
+ * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
+ * supported.
* @throws NullPointerException if {@code in} or {@code charset} is null.
* @throws IllegalArgumentException if {@code capacity} is negative or zero
- * or the specified charset is not supported.
+ * or the specified charset is not supported.
*/
public StrictLineReader(InputStream in, int capacity, Charset charset) {
if (in == null || charset == null) {
@@ -113,7 +90,7 @@ public class StrictLineReader implements Closeable {
if (capacity < 0) {
throw new IllegalArgumentException("capacity <= 0");
}
- if (!(charset.equals(US_ASCII) || charset.equals(UTF_8) || charset.equals(ISO_8859_1))) {
+ if (!(charset.equals(Util.US_ASCII))) {
throw new IllegalArgumentException("Unsupported encoding");
}
@@ -128,7 +105,6 @@ public class StrictLineReader implements Closeable {
*
* @throws IOException for errors when closing the underlying {@code InputStream}.
*/
- @Override
public void close() throws IOException {
synchronized (in) {
if (buf != null) {
@@ -162,7 +138,7 @@ public class StrictLineReader implements Closeable {
for (int i = pos; i != end; ++i) {
if (buf[i] == LF) {
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
- String res = new String(buf, pos, lineEnd - pos, charset);
+ String res = new String(buf, pos, lineEnd - pos, charset.name());
pos = i + 1;
return res;
}
@@ -173,7 +149,11 @@ public class StrictLineReader implements Closeable {
@Override
public String toString() {
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
- return new String(buf, 0, length, charset);
+ try {
+ return new String(buf, 0, length, charset.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e); // Since we control the charset this will never happen.
+ }
}
};
@@ -215,9 +195,6 @@ public class StrictLineReader implements Closeable {
/**
* Reads new input data into the buffer. Call only with pos == end or end == -1,
* depending on the desired outcome if the function throws.
- *
- * @throws IOException for underlying {@code InputStream} errors.
- * @throws EOFException for the end of source stream.
*/
private void fillBuf() throws IOException {
int result = in.read(buf, 0, buf.length);
diff --git a/framework/src/com/squareup/okhttp/internal/Util.java b/framework/src/com/squareup/okhttp/internal/Util.java
index dc914ccd..290e5ea9 100644
--- a/framework/src/com/squareup/okhttp/internal/Util.java
+++ b/framework/src/com/squareup/okhttp/internal/Util.java
@@ -149,12 +149,14 @@ public final class Util {
throw new AssertionError(thrown);
}
- /** Recursively delete everything in {@code dir}. */
- // TODO: this should specify paths as Strings rather than as Files
+ /**
+ * Deletes the contents of {@code dir}. Throws an IOException if any file
+ * could not be deleted, or if {@code dir} is not a readable directory.
+ */
public static void deleteContents(File dir) throws IOException {
File[] files = dir.listFiles();
if (files == null) {
- throw new IllegalArgumentException("not a directory: " + dir);
+ throw new IOException("not a readable directory: " + dir);
}
for (File file : files) {
if (file.isDirectory()) {
diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java b/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java
index 9caeb196..7a06dca5 100644
--- a/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -19,7 +19,6 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
-import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.ResponseSource;
import com.squareup.okhttp.TunnelRequest;
import com.squareup.okhttp.internal.Dns;
@@ -154,7 +153,7 @@ public class HttpEngine {
try {
uri = Platform.get().toUriLenient(policy.getURL());
} catch (URISyntaxException e) {
- throw new IOException(e);
+ throw new IOException(e.getMessage());
}
this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders));
@@ -176,8 +175,8 @@ public class HttpEngine {
prepareRawRequestHeaders();
initResponseSource();
- if (policy.responseCache instanceof OkResponseCache) {
- ((OkResponseCache) policy.responseCache).trackResponse(responseSource);
+ if (policy.responseCache != null) {
+ policy.responseCache.trackResponse(responseSource);
}
// The raw response source may require the network, but the request
@@ -198,6 +197,7 @@ public class HttpEngine {
sendSocketRequest();
} else if (connection != null) {
policy.connectionPool.recycle(connection);
+ policy.getFailedRoutes().remove(connection.getRoute());
connection = null;
}
}
@@ -279,16 +279,17 @@ public class HttpEngine {
}
Address address = new Address(uriHost, getEffectivePort(uri), sslSocketFactory,
hostnameVerifier, policy.requestedProxy);
- routeSelector =
- new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool, Dns.DEFAULT);
+ routeSelector = new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool,
+ Dns.DEFAULT, policy.getFailedRoutes());
}
connection = routeSelector.next();
if (!connection.isConnected()) {
connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig());
policy.connectionPool.maybeShare(connection);
+ policy.getFailedRoutes().remove(connection.getRoute());
}
connected(connection);
- if (connection.getProxy() != policy.requestedProxy) {
+ if (connection.getRoute().getProxy() != policy.requestedProxy) {
// Update the request line if the proxy changed; it may need a host name.
requestHeaders.getHeaders().setRequestLine(getRequestLine());
}
@@ -574,7 +575,7 @@ public class HttpEngine {
protected boolean includeAuthorityInRequestLine() {
return connection == null
? policy.usingProxy() // A proxy was requested.
- : connection.getProxy().type() == Proxy.Type.HTTP; // A proxy was selected.
+ : connection.getRoute().getProxy().type() == Proxy.Type.HTTP; // A proxy was selected.
}
public static String getDefaultUserAgent() {
@@ -635,11 +636,8 @@ public class HttpEngine {
release(false);
ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders);
setResponse(combinedHeaders, cachedResponseBody);
- if (policy.responseCache instanceof OkResponseCache) {
- OkResponseCache httpResponseCache = (OkResponseCache) policy.responseCache;
- httpResponseCache.trackConditionalCacheHit();
- httpResponseCache.update(cacheResponse, policy.getHttpConnectionToCache());
- }
+ policy.responseCache.trackConditionalCacheHit();
+ policy.responseCache.update(cacheResponse, policy.getHttpConnectionToCache());
return;
} else {
Util.closeQuietly(cachedResponseBody);
diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java b/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java
index dd7a38dc..f6d77b25 100644
--- a/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpTransport.java
@@ -17,8 +17,8 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.internal.AbstractOutputStream;
import com.squareup.okhttp.internal.Util;
-import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -30,14 +30,6 @@ 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
@@ -129,14 +121,8 @@ public final class HttpTransport implements Transport {
*/
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);
}
@@ -154,7 +140,7 @@ public final class HttpTransport implements Transport {
}
// We cannot reuse sockets that have incomplete output.
- if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) {
+ if (requestBodyOut != null && !((AbstractOutputStream) requestBodyOut).isClosed()) {
return false;
}
@@ -224,7 +210,7 @@ public final class HttpTransport implements Transport {
}
/** An HTTP body with a fixed length known in advance. */
- private static final class FixedLengthOutputStream extends AbstractHttpOutputStream {
+ private static final class FixedLengthOutputStream extends AbstractOutputStream {
private final OutputStream socketOut;
private int bytesRemaining;
@@ -266,7 +252,7 @@ public final class HttpTransport implements Transport {
* 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 class ChunkedOutputStream extends AbstractOutputStream {
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'
diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
index 25e8c3c8..eabe649d 100644
--- a/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
@@ -20,6 +20,9 @@ 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.Route;
+import com.squareup.okhttp.internal.AbstractOutputStream;
+import com.squareup.okhttp.internal.FaultRecoveringOutputStream;
import com.squareup.okhttp.internal.Util;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -32,13 +35,13 @@ 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 java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory;
@@ -70,6 +73,13 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
*/
private static final int MAX_REDIRECTS = 20;
+ /**
+ * The minimum number of request body bytes to transmit before we're willing
+ * to let a routine {@link IOException} bubble up to the user. This is used to
+ * size a buffer for data that will be replayed upon error.
+ */
+ private static final int MAX_REPLAY_BUFFER_LENGTH = 8192;
+
private final boolean followProtocolRedirects;
/** The proxy requested by the client, or null for a proxy to be selected automatically. */
@@ -77,29 +87,37 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
final ProxySelector proxySelector;
final CookieHandler cookieHandler;
- final ResponseCache responseCache;
+ final OkResponseCache responseCache;
final ConnectionPool connectionPool;
/* SSL configuration; necessary for HTTP requests that get redirected to HTTPS. */
SSLSocketFactory sslSocketFactory;
HostnameVerifier hostnameVerifier;
+ final Set failedRoutes;
private final RawHeaders rawRequestHeaders = new RawHeaders();
private int redirectionCount;
+ private FaultRecoveringOutputStream faultRecoveringRequestBody;
protected IOException httpEngineFailure;
protected HttpEngine httpEngine;
- public HttpURLConnectionImpl(URL url, OkHttpClient client) {
+ public HttpURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
+ Set failedRoutes) {
super(url);
this.followProtocolRedirects = client.getFollowProtocolRedirects();
+ this.failedRoutes = failedRoutes;
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();
+ this.responseCache = responseCache;
+ }
+
+ Set getFailedRoutes() {
+ return failedRoutes;
}
@Override public final void connect() throws IOException {
@@ -216,14 +234,29 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
@Override public final OutputStream getOutputStream() throws IOException {
connect();
- OutputStream result = httpEngine.getRequestBody();
- if (result == null) {
+ OutputStream out = httpEngine.getRequestBody();
+ if (out == 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;
+ if (faultRecoveringRequestBody == null) {
+ faultRecoveringRequestBody = new FaultRecoveringOutputStream(MAX_REPLAY_BUFFER_LENGTH, out) {
+ @Override protected OutputStream replacementStream(IOException e) throws IOException {
+ if (httpEngine.getRequestBody() instanceof AbstractOutputStream
+ && ((AbstractOutputStream) httpEngine.getRequestBody()).isClosed()) {
+ return null; // Don't recover once the underlying stream has been closed.
+ }
+ if (handleFailure(e)) {
+ return httpEngine.getRequestBody();
+ }
+ return null; // This is a permanent failure.
+ }
+ };
+ }
+
+ return faultRecoveringRequestBody;
}
@Override public final Permission getPermission() throws IOException {
@@ -353,29 +386,50 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
}
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.
+ if (handleFailure(e)) {
return false;
+ } else {
+ throw e;
}
- httpEngineFailure = e;
- throw e;
}
}
+ /**
+ * Report and attempt to recover from {@code e}. Returns true if the HTTP
+ * engine was replaced and the request should be retried. Otherwise the
+ * failure is permanent.
+ */
+ private boolean handleFailure(IOException e) throws IOException {
+ RouteSelector routeSelector = httpEngine.routeSelector;
+ if (routeSelector != null && httpEngine.connection != null) {
+ routeSelector.connectFailed(httpEngine.connection, e);
+ }
+
+ OutputStream requestBody = httpEngine.getRequestBody();
+ boolean canRetryRequestBody = requestBody == null
+ || requestBody instanceof RetryableOutputStream
+ || (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable());
+ if (routeSelector == null && httpEngine.connection == null // No connection.
+ || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
+ || !isRecoverable(e)
+ || !canRetryRequestBody) {
+ httpEngineFailure = e;
+ return false;
+ }
+
+ httpEngine.release(true);
+ RetryableOutputStream retryableOutputStream = requestBody instanceof RetryableOutputStream
+ ? (RetryableOutputStream) requestBody
+ : null;
+ httpEngine = newHttpEngine(method, rawRequestHeaders, null, retryableOutputStream);
+ httpEngine.routeSelector = routeSelector; // Keep the same routeSelector.
+ if (faultRecoveringRequestBody != null && faultRecoveringRequestBody.isRecoverable()) {
+ httpEngine.sendRequest();
+ faultRecoveringRequestBody.replaceStream(httpEngine.getRequestBody());
+ }
+ return true;
+ }
+
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.
@@ -385,7 +439,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
return !sslFailure && !protocolFailure;
}
- HttpEngine getHttpEngine() {
+ public HttpEngine getHttpEngine() {
return httpEngine;
}
@@ -402,7 +456,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
*/
private Retry processResponseHeaders() throws IOException {
Proxy selectedProxy = httpEngine.connection != null
- ? httpEngine.connection.getProxy()
+ ? httpEngine.connection.getRoute().getProxy()
: requestedProxy;
final int responseCode = getResponseCode();
switch (responseCode) {
diff --git a/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
index c224270b..235f8629 100644
--- a/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
+++ b/framework/src/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
@@ -18,6 +18,7 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Route;
import com.squareup.okhttp.TunnelRequest;
import java.io.IOException;
import java.io.InputStream;
@@ -32,6 +33,7 @@ import java.security.Principal;
import java.security.cert.Certificate;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException;
@@ -45,9 +47,10 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
/** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */
private final HttpUrlConnectionDelegate delegate;
- public HttpsURLConnectionImpl(URL url, OkHttpClient client) {
+ public HttpsURLConnectionImpl(URL url, OkHttpClient client, OkResponseCache responseCache,
+ Set failedRoutes) {
super(url);
- delegate = new HttpUrlConnectionDelegate(url, client);
+ delegate = new HttpUrlConnectionDelegate(url, client, responseCache, failedRoutes);
}
@Override public String getCipherSuite() {
@@ -112,7 +115,7 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
return null;
}
- HttpEngine getHttpEngine() {
+ public HttpEngine getHttpEngine() {
return delegate.getHttpEngine();
}
@@ -399,8 +402,9 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
}
private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl {
- private HttpUrlConnectionDelegate(URL url, OkHttpClient client) {
- super(url, client);
+ private HttpUrlConnectionDelegate(URL url, OkHttpClient client, OkResponseCache responseCache,
+ Set failedRoutes) {
+ super(url, client, responseCache, failedRoutes);
}
@Override protected HttpURLConnection getHttpConnectionToCache() {
@@ -425,8 +429,7 @@ public final class HttpsURLConnectionImpl extends HttpsURLConnection {
* @param policy the HttpURLConnectionImpl with connection configuration
*/
public HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders,
- Connection connection, RetryableOutputStream requestBody)
- throws IOException {
+ Connection connection, RetryableOutputStream requestBody) throws IOException {
super(policy, method, requestHeaders, connection, requestBody);
this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null;
}
diff --git a/framework/src/com/squareup/okhttp/internal/http/OkResponseCache.java b/framework/src/com/squareup/okhttp/internal/http/OkResponseCache.java
new file mode 100644
index 00000000..5829f024
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/OkResponseCache.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * 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.ResponseSource;
+import java.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An extended response cache API. Unlike {@link java.net.ResponseCache}, this
+ * interface supports conditional caching and statistics.
+ *
+ * Along with the rest of the {@code internal} package, this is not a public
+ * API. Applications wishing to supply their own caches must use the more
+ * limited {@link java.net.ResponseCache} interface.
+ */
+public interface OkResponseCache {
+ CacheResponse get(URI uri, String requestMethod, Map> requestHeaders)
+ throws IOException;
+
+ CacheRequest put(URI uri, URLConnection urlConnection) throws IOException;
+
+ /**
+ * 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.
+ */
+ void update(CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException;
+
+ /** Track an conditional GET that was satisfied by this cache. */
+ void trackConditionalCacheHit();
+
+ /** Track an HTTP response being satisfied by {@code source}. */
+ void trackResponse(ResponseSource source);
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java b/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java
new file mode 100644
index 00000000..2ac915a8
--- /dev/null
+++ b/framework/src/com/squareup/okhttp/internal/http/OkResponseCacheAdapter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * 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.ResponseSource;
+import java.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.HttpURLConnection;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+
+public final class OkResponseCacheAdapter implements OkResponseCache {
+ private final ResponseCache responseCache;
+ public OkResponseCacheAdapter(ResponseCache responseCache) {
+ this.responseCache = responseCache;
+ }
+
+ @Override public CacheResponse get(URI uri, String requestMethod,
+ Map> requestHeaders) throws IOException {
+ return responseCache.get(uri, requestMethod, requestHeaders);
+ }
+
+ @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+ return responseCache.put(uri, urlConnection);
+ }
+
+ @Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection connection)
+ throws IOException {
+ }
+
+ @Override public void trackConditionalCacheHit() {
+ }
+
+ @Override public void trackResponse(ResponseSource source) {
+ }
+}
diff --git a/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java b/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java
index c121abce..eba887ec 100644
--- a/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java
+++ b/framework/src/com/squareup/okhttp/internal/http/RawHeaders.java
@@ -17,7 +17,6 @@
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;
@@ -186,25 +185,27 @@ public final class RawHeaders {
public void addLine(String line) {
int index = line.indexOf(":");
if (index == -1) {
- add("", line);
+ addLenient("", line);
} else {
- add(line.substring(0, index), line.substring(index + 1));
+ addLenient(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;
+ if (fieldName == null) throw new IllegalArgumentException("fieldname == null");
+ if (value == null) throw new IllegalArgumentException("value == null");
+ if (fieldName.length() == 0 || fieldName.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
+ throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value);
}
+ addLenient(fieldName, value);
+ }
+
+ /**
+ * Add a field with the specified value without any validation. Only
+ * appropriate for headers from the remote peer.
+ */
+ private void addLenient(String fieldName, String value) {
namesAndValues.add(fieldName);
namesAndValues.add(value.trim());
}
@@ -351,7 +352,9 @@ public final class RawHeaders {
String fieldName = entry.getKey();
List values = entry.getValue();
if (fieldName != null) {
- result.addAll(fieldName, values);
+ for (String value : values) {
+ result.addLenient(fieldName, value);
+ }
} else if (!values.isEmpty()) {
result.setStatusLine(values.get(values.size() - 1));
}
@@ -371,14 +374,6 @@ public final class RawHeaders {
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")
diff --git a/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java b/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java
index 2544ceed..5ec4fcca 100644
--- a/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java
+++ b/framework/src/com/squareup/okhttp/internal/http/RequestHeaders.java
@@ -22,7 +22,7 @@ import java.util.List;
import java.util.Map;
/** Parsed HTTP request headers. */
-final class RequestHeaders {
+public final class RequestHeaders {
private final URI uri;
private final RawHeaders headers;
diff --git a/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java b/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java
index 22d8c5c3..2ab564dc 100644
--- a/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java
+++ b/framework/src/com/squareup/okhttp/internal/http/ResponseHeaders.java
@@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit;
import static com.squareup.okhttp.internal.Util.equal;
/** Parsed HTTP response headers. */
-final class ResponseHeaders {
+public final class ResponseHeaders {
/** HTTP header name for the local time when the request was sent. */
private static final String SENT_MILLIS = "X-Android-Sent-Millis";
@@ -410,7 +410,8 @@ final class ResponseHeaders {
if (ageMillis + minFreshMillis >= freshMillis) {
headers.add("Warning", "110 HttpURLConnection \"Response is stale\"");
}
- if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) {
+ long oneDayMillis = 24 * 60 * 60 * 1000L;
+ if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return ResponseSource.CACHE;
diff --git a/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java b/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java
index 325327db..5eb6b764 100644
--- a/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java
+++ b/framework/src/com/squareup/okhttp/internal/http/RetryableOutputStream.java
@@ -16,6 +16,7 @@
package com.squareup.okhttp.internal.http;
+import com.squareup.okhttp.internal.AbstractOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
@@ -28,7 +29,7 @@ import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
* the post body to be transparently re-sent if the HTTP request must be
* sent multiple times.
*/
-final class RetryableOutputStream extends AbstractHttpOutputStream {
+final class RetryableOutputStream extends AbstractOutputStream {
private final int limit;
private final ByteArrayOutputStream content;
diff --git a/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java b/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java
index 798cff3b..ce0a71d8 100644
--- a/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java
+++ b/framework/src/com/squareup/okhttp/internal/http/RouteSelector.java
@@ -18,6 +18,7 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.Route;
import com.squareup.okhttp.internal.Dns;
import java.io.IOException;
import java.net.InetAddress;
@@ -28,8 +29,11 @@ import java.net.SocketAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
+import java.util.Set;
+import javax.net.ssl.SSLHandshakeException;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
@@ -51,6 +55,7 @@ public final class RouteSelector {
private final ProxySelector proxySelector;
private final ConnectionPool pool;
private final Dns dns;
+ private final Set failedRoutes;
/* The most recently attempted route. */
private Proxy lastProxy;
@@ -64,19 +69,23 @@ public final class RouteSelector {
/* State for negotiating the next InetSocketAddress to use. */
private InetAddress[] socketAddresses;
private int nextSocketAddressIndex;
- private String socketHost;
private int socketPort;
/* State for negotiating the next TLS configuration */
private int nextTlsMode = TLS_MODE_NULL;
+ /* State for negotiating failed routes */
+ private final List postponedRoutes;
+
public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
- Dns dns) {
+ Dns dns, Set failedRoutes) {
this.address = address;
this.uri = uri;
this.proxySelector = proxySelector;
this.pool = pool;
this.dns = dns;
+ this.failedRoutes = failedRoutes;
+ this.postponedRoutes = new LinkedList();
resetNextProxy(uri, address.getProxy());
}
@@ -86,7 +95,7 @@ public final class RouteSelector {
* least one route.
*/
public boolean hasNext() {
- return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy();
+ return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy() || hasNextPostponed();
}
/**
@@ -105,7 +114,10 @@ public final class RouteSelector {
if (!hasNextTlsMode()) {
if (!hasNextInetSocketAddress()) {
if (!hasNextProxy()) {
- throw new NoSuchElementException();
+ if (!hasNextPostponed()) {
+ throw new NoSuchElementException();
+ }
+ return new Connection(nextPostponed());
}
lastProxy = nextProxy();
resetNextInetSocketAddress(lastProxy);
@@ -113,9 +125,17 @@ public final class RouteSelector {
lastInetSocketAddress = nextInetSocketAddress();
resetNextTlsMode();
}
- boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
- return new Connection(address, lastProxy, lastInetSocketAddress, modernTls);
+ boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
+ Route route = new Route(address, lastProxy, lastInetSocketAddress, modernTls);
+ if (failedRoutes.contains(route)) {
+ postponedRoutes.add(route);
+ // We will only recurse in order to skip previously failed routes. They will be
+ // tried last.
+ return next();
+ }
+
+ return new Connection(route);
}
/**
@@ -123,9 +143,17 @@ public final class RouteSelector {
* failure on a connection returned by this route selector.
*/
public void connectFailed(Connection connection, IOException failure) {
- if (connection.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) {
+ Route failedRoute = connection.getRoute();
+ if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) {
// Tell the proxy selector when we fail to connect on a fresh connection.
- proxySelector.connectFailed(uri, connection.getProxy().address(), failure);
+ proxySelector.connectFailed(uri, failedRoute.getProxy().address(), failure);
+ }
+
+ failedRoutes.add(failedRoute);
+ if (!(failure instanceof SSLHandshakeException)) {
+ // If the problem was not related to SSL then it will also fail with
+ // a different Tls mode therefore we can be proactive about it.
+ failedRoutes.add(failedRoute.flipTlsMode());
}
}
@@ -175,6 +203,7 @@ public final class RouteSelector {
private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException {
socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws!
+ String socketHost;
if (proxy.type() == Proxy.Type.DIRECT) {
socketHost = uri.getHost();
socketPort = getEffectivePort(uri);
@@ -233,4 +262,14 @@ public final class RouteSelector {
throw new AssertionError();
}
}
+
+ /** Returns true if there is another postponed route to try. */
+ private boolean hasNextPostponed() {
+ return !postponedRoutes.isEmpty();
+ }
+
+ /** Returns the next postponed route to try. */
+ private Route nextPostponed() {
+ return postponedRoutes.remove(0);
+ }
}
diff --git a/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java
index b3e248c3..fccd14f8 100644
--- a/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java
+++ b/framework/src/com/squareup/okhttp/internal/spdy/SpdyConnection.java
@@ -139,17 +139,17 @@ public final class SpdyConnection implements Closeable {
return stream;
}
- private void setIdle(boolean value) {
+ private synchronized void setIdle(boolean value) {
idleStartTimeNs = value ? System.nanoTime() : 0L;
}
/** Returns true if this connection is idle. */
- public boolean isIdle() {
+ public synchronized boolean isIdle() {
return idleStartTimeNs != 0L;
}
/** Returns the time in ns when this connection became idle or 0L if connection is not idle. */
- public long getIdleStartTimeNs() {
+ public synchronized long getIdleStartTimeNs() {
return idleStartTimeNs;
}
diff --git a/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java b/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java
index 7a7b1987..7d3f2bd5 100644
--- a/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java
+++ b/framework/src/com/squareup/okhttp/internal/spdy/SpdyReader.java
@@ -21,6 +21,7 @@ import java.io.Closeable;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
import java.net.ProtocolException;
import java.util.ArrayList;
import java.util.List;
@@ -31,39 +32,46 @@ import java.util.zip.InflaterInputStream;
/** Read spdy/3 frames. */
final class SpdyReader implements Closeable {
- static final byte[] DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
- + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
- + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
- + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
- + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
- + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
- + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000"
- + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000"
- + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000"
- + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type"
- + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe"
- + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000"
- + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since"
- + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000"
- + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati"
- + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000"
- + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000"
- + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after"
- + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai"
- + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000"
- + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via"
- + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000"
- + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000"
- + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1"
- + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo"
- + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
- + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
- + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
- + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
- + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th"
- + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml"
- + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate,"
- + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8);
+ static final byte[] DICTIONARY;
+ static {
+ try {
+ DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
+ + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
+ + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
+ + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
+ + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
+ + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
+ + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000"
+ + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000"
+ + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000"
+ + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type"
+ + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe"
+ + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000"
+ + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since"
+ + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000"
+ + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati"
+ + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000"
+ + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000"
+ + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after"
+ + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai"
+ + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000"
+ + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via"
+ + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000"
+ + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000"
+ + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1"
+ + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo"
+ + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
+ + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
+ + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
+ + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
+ + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th"
+ + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml"
+ + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate,"
+ + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError();
+ }
+ }
private final DataInputStream in;
private final DataInputStream nameValueBlockIn;
@@ -252,7 +260,7 @@ final class SpdyReader implements Closeable {
return entries;
} catch (DataFormatException e) {
- throw new IOException(e);
+ throw new IOException(e.getMessage());
}
}