jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/340906 )
Change subject: Update: add second OkHttp cache for saved content
......................................................................
Update: add second OkHttp cache for saved content
Bug: T156917
Change-Id: I45825777d18cbbc686d757a715002464cf94ae3f
---
M app/build.gradle
A app/src/main/java/okhttp3/CacheDelegate.java
A app/src/main/java/okhttp3/internal/cache/CacheDelegateInterceptor.java
M app/src/main/java/org/wikipedia/dataclient/okhttp/CacheIfErrorInterceptor.java
M app/src/main/java/org/wikipedia/dataclient/okhttp/OkHttpConnectionFactory.java
A
app/src/main/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategy.java
A app/src/main/java/org/wikipedia/dataclient/okhttp/cache/SaveHeader.java
A app/src/test/java/okhttp3/internal/cache/CacheDelegateInterceptorTest.java
M app/src/test/java/org/wikipedia/dataclient/mwapi/page/MwPageClientTest.java
A
app/src/test/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategyTest.java
A app/src/test/java/org/wikipedia/dataclient/okhttp/cache/SaveHeaderTest.java
M app/src/test/java/org/wikipedia/dataclient/restbase/page/RbPageClientTest.java
M app/src/test/java/org/wikipedia/test/MockWebServerTest.java
M gradle/src/checkstyle.gradle
14 files changed, 733 insertions(+), 31 deletions(-)
Approvals:
Dbrant: Looks good to me, approved
jenkins-bot: Verified
Mholloway: Looks good to me, but someone else must approve
diff --git a/app/build.gradle b/app/build.gradle
index f81c165..99bb773 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -157,12 +157,14 @@
// use http://gradleplease.appspot.com/ or http://search.maven.org/.
// Debug with ./gradlew -q app:dependencies --configuration compile
- String okHttpVersion = '3.6.0'
+ String okHttpVersion = '3.6.0' // When updating this version, resync file
copies under
+ // app/src/main/java/okhttp3
String retrofitVersion = '2.2.0'
String supportVersion = '25.3.1'
String espressoVersion = '2.2.2'
String butterKnifeVersion = '8.4.0'
- String frescoVersion = '1.1.0'
+ String frescoVersion = '1.1.0' // When updating this version, resync file
copies under
+ // app/src/main/java/com/facebook
String testingSupportVersion = '0.5'
String mockitoCore = 'org.mockito:mockito-core:1.9.5'
String leakCanaryVersion = '1.5'
@@ -192,6 +194,7 @@
compile 'com.mobsandgeeks:android-saripaar:2.0.3'
compile 'com.github.ryanjohn1:onboarding:1.0.3'
compile "com.jakewharton:butterknife:$butterKnifeVersion"
+ // When upgrading the Mapbox version, remove the superfluous ProGuard rule
and delete this comment
compile('com.mapbox.mapboxsdk:mapbox-android-sdk:5.0.0@aar') {
transitive = true
}
diff --git a/app/src/main/java/okhttp3/CacheDelegate.java
b/app/src/main/java/okhttp3/CacheDelegate.java
new file mode 100644
index 0000000..f696b21
--- /dev/null
+++ b/app/src/main/java/okhttp3/CacheDelegate.java
@@ -0,0 +1,53 @@
+package okhttp3;
+
+import android.support.annotation.NonNull;
+
+import java.io.IOException;
+
+import okhttp3.internal.Util;
+import okhttp3.internal.cache.DiskLruCache;
+import okhttp3.internal.cache.InternalCache;
+import okio.ByteString;
+
+public class CacheDelegate {
+ @NonNull public static InternalCache internalCache(@NonNull Cache cache) {
+ return cache.internalCache;
+ }
+
+ @NonNull private final Cache cache;
+
+ public CacheDelegate(@NonNull Cache cache) {
+ this.cache = cache;
+ }
+
+ // Copy of Cache.get(). Calling this method modifies the Cache. If the URL
is present, it's
+ // cache entry is moved to the head of the LRU queue. This method performs
file I/O
+ public boolean isCached(@NonNull String url) {
+ String key = key(url);
+ DiskLruCache.Snapshot snapshot;
+ try {
+ snapshot = cache.cache.get(key);
+ if (snapshot == null) {
+ return false;
+ }
+ } catch (IOException e) {
+ // Give up because the cache cannot be read.
+ return false;
+ }
+
+ Util.closeQuietly(snapshot);
+ return true;
+ }
+
+ // Copy of Cache.remove(). This method performs file I/O
+ public void remove(@NonNull Request req) {
+ try {
+ cache.remove(req);
+ } catch (IOException ignore) { }
+ }
+
+ // Copy of Cache.key()
+ @NonNull private String key(@NonNull String url) {
+ return ByteString.encodeUtf8(url).md5().hex();
+ }
+}
diff --git
a/app/src/main/java/okhttp3/internal/cache/CacheDelegateInterceptor.java
b/app/src/main/java/okhttp3/internal/cache/CacheDelegateInterceptor.java
new file mode 100644
index 0000000..2e70b70
--- /dev/null
+++ b/app/src/main/java/okhttp3/internal/cache/CacheDelegateInterceptor.java
@@ -0,0 +1,301 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okhttp3.internal.cache;
+
+import org.wikipedia.dataclient.okhttp.cache.CacheDelegateStrategy;
+
+import java.io.IOException;
+
+import okhttp3.Headers;
+import okhttp3.Interceptor;
+import okhttp3.Protocol;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.internal.Internal;
+import okhttp3.internal.Util;
+import okhttp3.internal.http.HttpCodec;
+import okhttp3.internal.http.HttpHeaders;
+import okhttp3.internal.http.HttpMethod;
+import okhttp3.internal.http.RealResponseBody;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
+import okio.Timeout;
+
+import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static okhttp3.internal.Util.closeQuietly;
+import static okhttp3.internal.Util.discard;
+
+/** Serves requests from the cache and writes responses to the cache. Copied
from
+
https://github.com/square/okhttp/blob/a27afaf/okhttp/src/main/java/okhttp3/internal/cache/CacheInterceptor.java
+ to allow a custom cache strategy. Deviations are marked with "Change:". */
+public final class CacheDelegateInterceptor implements Interceptor {
+ final InternalCache cache;
+ // Change: add secondaryCache member variable
+ final InternalCache secondaryCache;
+
+ // Change: add secondaryCache param
+ public CacheDelegateInterceptor(InternalCache cache, InternalCache
secondaryCache) {
+ this.cache = cache;
+
+ // Change: initialize secondaryCache param
+ this.secondaryCache = secondaryCache;
+ }
+
+ @Override public Response intercept(Chain chain) throws IOException {
+ Response cacheCandidate = cache != null
+ ? cache.get(chain.request())
+ : null;
+
+ long now = System.currentTimeMillis();
+
+ CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(),
cacheCandidate).get();
+ Request networkRequest = strategy.networkRequest;
+ Response cacheResponse = strategy.cacheResponse;
+
+ if (cache != null) {
+ cache.trackResponse(strategy);
+ }
+
+ // Change: the true cache further down the chain must be used directly to
retrieve non-network
+ // responses
+ if (networkRequest == null && cacheResponse == null) {
+ cacheResponse = secondaryCache.get(chain.request());
+ }
+
+ if (cacheCandidate != null && cacheResponse == null) {
+ closeQuietly(cacheCandidate.body()); // The cache candidate wasn't
applicable. Close it.
+ }
+
+ // If we're forbidden from using the network and the cache is
insufficient, fail.
+ if (networkRequest == null && cacheResponse == null) {
+ return new Response.Builder()
+ .request(chain.request())
+ .protocol(Protocol.HTTP_1_1)
+ .code(504)
+ .message("Unsatisfiable Request (only-if-cached)")
+ .body(Util.EMPTY_RESPONSE)
+ .sentRequestAtMillis(-1L)
+ .receivedResponseAtMillis(System.currentTimeMillis())
+ .build();
+ }
+
+ // If we don't need the network, we're done.
+ if (networkRequest == null) {
+ return cacheResponse.newBuilder()
+ .cacheResponse(stripBody(cacheResponse))
+ .build();
+ }
+
+ Response networkResponse = null;
+ try {
+ networkResponse = chain.proceed(networkRequest);
+ } finally {
+ // If we're crashing on I/O or otherwise, don't leak the cache body.
+ if (networkResponse == null && cacheCandidate != null) {
+ closeQuietly(cacheCandidate.body());
+ }
+ }
+
+ // Change: the original CacheInterceptor sets Response.Builder.*Response()
but Response.Builder,
+ // invoked further down for the final Response, forbids a Response's cache
/ network / prior
+ // Response to itself contain a cache / network / prior Response or a
body. The body is already
+ // stripped by stripBody()
+ networkResponse = networkResponse.newBuilder()
+ .cacheResponse(null)
+ .networkResponse(null)
+ .priorResponse(null)
+ .build();
+
+ // If we have a cache response too, then we're doing a conditional get.
+ if (cacheResponse != null) {
+ if (networkResponse.code() == HTTP_NOT_MODIFIED) {
+ Response response = cacheResponse.newBuilder()
+ .headers(combine(cacheResponse.headers(),
networkResponse.headers()))
+ .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
+
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
+ .cacheResponse(stripBody(cacheResponse))
+ .networkResponse(stripBody(networkResponse))
+ .build();
+ networkResponse.body().close();
+
+ // Update the cache after combining headers but before stripping the
+ // Content-Encoding header (as performed by initContentStream()).
+ cache.trackConditionalCacheHit();
+ cache.update(cacheResponse, response);
+ return response;
+ } else {
+ closeQuietly(cacheResponse.body());
+ }
+ }
+
+ Response response = networkResponse.newBuilder()
+ .cacheResponse(stripBody(cacheResponse))
+ .networkResponse(stripBody(networkResponse))
+ .build();
+
+ if (HttpHeaders.hasBody(response)) {
+ // Change: add cacheCandidate parameter
+ CacheRequest cacheRequest = maybeCache(cacheCandidate, response,
networkResponse.request(), cache);
+ response = cacheWritingResponse(cacheRequest, response);
+ }
+
+ return response;
+ }
+
+ private static Response stripBody(Response response) {
+ return response != null && response.body() != null
+ ? response.newBuilder().body(null).build()
+ : response;
+ }
+
+ // Change: add cacheCandidate parameter
+ private CacheRequest maybeCache(Response cacheCandidate, Response
userResponse, Request networkRequest,
+ InternalCache responseCache) throws IOException {
+ if (responseCache == null) return null;
+
+ // Should we cache this response for this request?
+ if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
+ if (HttpMethod.invalidatesCache(networkRequest.method())) {
+ try {
+ responseCache.remove(networkRequest);
+ } catch (IOException ignored) {
+ // The cache cannot be written.
+ }
+ }
+ return null;
+ }
+
+ // Change: do not permit cache writes unless the page has been cached
previously or the request
+ // is cacheable
+ if (cacheCandidate == null &&
!CacheDelegateStrategy.isCacheable(networkRequest)) {
+ return null;
+ }
+
+ // Offer this request to the cache.
+ return responseCache.put(userResponse);
+ }
+
+ /**
+ * Returns a new source that writes bytes to {@code cacheRequest} as they
are read by the source
+ * consumer. This is careful to discard bytes left over when the stream is
closed; otherwise we
+ * may never exhaust the source stream and therefore not complete the cached
response.
+ */
+ private Response cacheWritingResponse(final CacheRequest cacheRequest,
Response response)
+ throws IOException {
+ // Some apps return a null body; for compatibility we treat that like a
null cache request.
+ if (cacheRequest == null) return response;
+ Sink cacheBodyUnbuffered = cacheRequest.body();
+ if (cacheBodyUnbuffered == null) return response;
+
+ final BufferedSource source = response.body().source();
+ final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
+
+ Source cacheWritingSource = new Source() {
+ boolean cacheRequestClosed;
+
+ @Override public long read(Buffer sink, long byteCount) throws
IOException {
+ long bytesRead;
+ try {
+ bytesRead = source.read(sink, byteCount);
+ } catch (IOException e) {
+ if (!cacheRequestClosed) {
+ cacheRequestClosed = true;
+ cacheRequest.abort(); // Failed to write a complete cache response.
+ }
+ throw e;
+ }
+
+ if (bytesRead == -1) {
+ if (!cacheRequestClosed) {
+ cacheRequestClosed = true;
+ cacheBody.close(); // The cache response is complete!
+ }
+ return -1;
+ }
+
+ sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
+ cacheBody.emitCompleteSegments();
+ return bytesRead;
+ }
+
+ @Override public Timeout timeout() {
+ return source.timeout();
+ }
+
+ @Override public void close() throws IOException {
+ if (!cacheRequestClosed
+ && !discard(this, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS,
MILLISECONDS)) {
+ cacheRequestClosed = true;
+ cacheRequest.abort();
+ }
+ source.close();
+ }
+ };
+
+ return response.newBuilder()
+ .body(new RealResponseBody(response.headers(),
Okio.buffer(cacheWritingSource)))
+ .build();
+ }
+
+ /** Combines cached headers with a network headers as defined by RFC 2616,
13.5.3. */
+ private static Headers combine(Headers cachedHeaders, Headers
networkHeaders) {
+ Headers.Builder result = new Headers.Builder();
+
+ for (int i = 0, size = cachedHeaders.size(); i < size; i++) {
+ String fieldName = cachedHeaders.name(i);
+ String value = cachedHeaders.value(i);
+ if ("Warning".equalsIgnoreCase(fieldName) && value.startsWith("1")) {
+ continue; // Drop 100-level freshness warnings.
+ }
+ if (!isEndToEnd(fieldName) || networkHeaders.get(fieldName) == null) {
+ Internal.instance.addLenient(result, fieldName, value);
+ }
+ }
+
+ for (int i = 0, size = networkHeaders.size(); i < size; i++) {
+ String fieldName = networkHeaders.name(i);
+ if ("Content-Length".equalsIgnoreCase(fieldName)) {
+ continue; // Ignore content-length headers of validating responses.
+ }
+ if (isEndToEnd(fieldName)) {
+ Internal.instance.addLenient(result, fieldName,
networkHeaders.value(i));
+ }
+ }
+
+ return result.build();
+ }
+
+ /**
+ * Returns true if {@code fieldName} is an end-to-end HTTP header, as
defined by RFC 2616,
+ * 13.5.1.
+ */
+ static boolean isEndToEnd(String fieldName) {
+ return !"Connection".equalsIgnoreCase(fieldName)
+ && !"Keep-Alive".equalsIgnoreCase(fieldName)
+ && !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
+ && !"Proxy-Authorization".equalsIgnoreCase(fieldName)
+ && !"TE".equalsIgnoreCase(fieldName)
+ && !"Trailers".equalsIgnoreCase(fieldName)
+ && !"Transfer-Encoding".equalsIgnoreCase(fieldName)
+ && !"Upgrade".equalsIgnoreCase(fieldName);
+ }
+}
diff --git
a/app/src/main/java/org/wikipedia/dataclient/okhttp/CacheIfErrorInterceptor.java
b/app/src/main/java/org/wikipedia/dataclient/okhttp/CacheIfErrorInterceptor.java
index 317e9f1..dee97fa 100644
---
a/app/src/main/java/org/wikipedia/dataclient/okhttp/CacheIfErrorInterceptor.java
+++
b/app/src/main/java/org/wikipedia/dataclient/okhttp/CacheIfErrorInterceptor.java
@@ -4,25 +4,32 @@
import java.io.IOException;
+import okhttp3.CacheControl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
class CacheIfErrorInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
- Request req = chain.request();
- Response rsp = chain.proceed(req);
+ Response rsp = null;
+ try {
+ rsp =
chain.proceed(chain.request().newBuilder().cacheControl(CacheControl.FORCE_CACHE).build());
+ } catch (IOException ignore) { }
// when max-stale is set, CacheStrategy forbids a conditional network
request in
// CacheInterceptor
- if (rsp == null || stale(rsp)) {
- Request netReq = forceNetRequest(req);
+ if (!chain.request().cacheControl().onlyIfCached()) {
+ Request netReq = forceNetRequest(chain.request());
Response netRsp = null;
try {
netRsp = chain.proceed(netReq);
- } catch (IOException ignore) { }
+ } catch (IOException ignore) {
+ if (rsp == null) {
+ throw ignore;
+ }
+ }
- if (netRsp != null) {
+ if (netRsp != null && (netRsp.isSuccessful() || rsp == null ||
!rsp.isSuccessful())) {
return netRsp;
}
}
@@ -30,18 +37,8 @@
return rsp;
}
- @NonNull static Request forceNetRequest(@NonNull Request req) {
+ @NonNull private Request forceNetRequest(@NonNull Request req) {
String cacheControl =
CacheControlUtil.forceNetRequest(req.cacheControl().toString());
return req.newBuilder().header("Cache-Control", cacheControl).build();
- }
-
- private boolean stale(@NonNull Response response) {
- final String staleRspCode = "110";
- for (String header : response.headers("Warning")) {
- if (header.startsWith(staleRspCode)) {
- return true;
- }
- }
- return false;
}
}
diff --git
a/app/src/main/java/org/wikipedia/dataclient/okhttp/OkHttpConnectionFactory.java
b/app/src/main/java/org/wikipedia/dataclient/okhttp/OkHttpConnectionFactory.java
index 3063dc2..b5ac3cd 100644
---
a/app/src/main/java/org/wikipedia/dataclient/okhttp/OkHttpConnectionFactory.java
+++
b/app/src/main/java/org/wikipedia/dataclient/okhttp/OkHttpConnectionFactory.java
@@ -1,6 +1,7 @@
package org.wikipedia.dataclient.okhttp;
import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
import com.github.kevinsawicki.http.HttpRequest;
@@ -9,22 +10,30 @@
import org.wikipedia.settings.Prefs;
import org.wikipedia.settings.RbSwitch;
+import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import okhttp3.Cache;
+import okhttp3.CacheDelegate;
import okhttp3.CookieJar;
import okhttp3.JavaNetCookieJar;
import okhttp3.OkHttpClient;
import okhttp3.OkUrlFactory;
+import okhttp3.internal.cache.CacheDelegateInterceptor;
import okhttp3.logging.HttpLoggingInterceptor;
public class OkHttpConnectionFactory implements HttpRequest.ConnectionFactory {
- private static final long HTTP_CACHE_SIZE = 64 * 1024 * 1024;
- @NonNull private static final Cache HTTP_CACHE = new
Cache(WikipediaApp.getInstance().getCacheDir(),
- HTTP_CACHE_SIZE);
+ private static final String CACHE_DIR_NAME = "okhttp-cache";
+ private static final long NET_CACHE_SIZE = 64 * 1024 * 1024;
+ @VisibleForTesting @NonNull public static final Cache NET_CACHE = new
Cache(new File(WikipediaApp.getInstance().getCacheDir(),
+ CACHE_DIR_NAME), NET_CACHE_SIZE);
+ private static final long SAVED_PAGE_CACHE_SIZE = NET_CACHE_SIZE * 1024;
+ @NonNull public static final Cache SAVE_CACHE = new Cache(new
File(WikipediaApp.getInstance().getFilesDir(),
+ CACHE_DIR_NAME), SAVED_PAGE_CACHE_SIZE);
+
@NonNull private static final OkHttpClient CLIENT = createClient();
@NonNull public static OkHttpClient getClient() {
@@ -50,7 +59,7 @@
return new OkHttpClient.Builder()
.cookieJar(cookieJar)
- .cache(HTTP_CACHE)
+ .cache(NET_CACHE)
.addInterceptor(new
HttpLoggingInterceptor().setLevel(Prefs.getRetrofitLogLevel()))
.addInterceptor(new UnsuccessfulResponseInterceptor())
.addInterceptor(new
StatusResponseInterceptor(RbSwitch.INSTANCE))
@@ -58,6 +67,7 @@
.addInterceptor(new CommonHeaderRequestInterceptor())
.addInterceptor(new DefaultMaxStaleRequestInterceptor())
.addInterceptor(new CacheIfErrorInterceptor())
+ .addInterceptor(new
CacheDelegateInterceptor(CacheDelegate.internalCache(SAVE_CACHE),
CacheDelegate.internalCache(NET_CACHE)))
.addInterceptor(new
WikipediaZeroResponseInterceptor(WikipediaApp.getInstance().getWikipediaZeroHandler()))
// this interceptor should appear last since it examines the
final cache and network responses
.addInterceptor(new
ResponseLoggingInterceptor().setLevel(Prefs.getRetrofitLogLevel()))
diff --git
a/app/src/main/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategy.java
b/app/src/main/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategy.java
new file mode 100644
index 0000000..33fbab9
--- /dev/null
+++
b/app/src/main/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategy.java
@@ -0,0 +1,13 @@
+package org.wikipedia.dataclient.okhttp.cache;
+
+import android.support.annotation.NonNull;
+
+import okhttp3.Request;
+
+public final class CacheDelegateStrategy {
+ public static boolean isCacheable(@NonNull Request request) {
+ return SaveHeader.isSaveEnabled(request.header(SaveHeader.FIELD));
+ }
+
+ private CacheDelegateStrategy() { }
+}
diff --git
a/app/src/main/java/org/wikipedia/dataclient/okhttp/cache/SaveHeader.java
b/app/src/main/java/org/wikipedia/dataclient/okhttp/cache/SaveHeader.java
new file mode 100644
index 0000000..0ccc004
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/dataclient/okhttp/cache/SaveHeader.java
@@ -0,0 +1,15 @@
+package org.wikipedia.dataclient.okhttp.cache;
+
+import android.support.annotation.Nullable;
+
+public final class SaveHeader {
+ public static final String FIELD = "X-Save";
+ public static final String VAL_ENABLED = "true";
+ public static final String VAL_DISABLED = "false";
+
+ public static boolean isSaveEnabled(@Nullable String val) {
+ return VAL_ENABLED.equalsIgnoreCase(val);
+ }
+
+ private SaveHeader() { }
+}
diff --git
a/app/src/test/java/okhttp3/internal/cache/CacheDelegateInterceptorTest.java
b/app/src/test/java/okhttp3/internal/cache/CacheDelegateInterceptorTest.java
new file mode 100644
index 0000000..ae835e0
--- /dev/null
+++ b/app/src/test/java/okhttp3/internal/cache/CacheDelegateInterceptorTest.java
@@ -0,0 +1,252 @@
+package okhttp3.internal.cache;
+
+import android.support.annotation.NonNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.wikipedia.dataclient.okhttp.HttpStatusException;
+import org.wikipedia.dataclient.okhttp.OkHttpConnectionFactory;
+import org.wikipedia.dataclient.okhttp.cache.SaveHeader;
+import org.wikipedia.test.MockWebServerTest;
+
+import okhttp3.CacheControl;
+import okhttp3.CacheDelegate;
+import okhttp3.Request;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class CacheDelegateInterceptorTest extends MockWebServerTest {
+ private static final String URL = "url";
+
+ @NonNull private final CacheDelegate netCache = new
CacheDelegate(OkHttpConnectionFactory.NET_CACHE);
+ @NonNull private final CacheDelegate saveCache = new
CacheDelegate(OkHttpConnectionFactory.SAVE_CACHE);
+
+ @Before public void setUp() throws Throwable {
+ super.setUp();
+
+ Request req = newRequest();
+ netCache.remove(req);
+ saveCache.remove(req);
+ }
+
+ // Sanity check that both caches are truly empty
+ @Test(expected = HttpStatusException.class) public void testAssumptions()
throws Throwable {
+ Request req = newOnlyIfCachedRequest();
+
+ assertCached(netCache, req, false);
+ assertCached(saveCache, req, false);
+
+ executeRequest(req);
+ }
+
+ @Test public void testInterceptWriteNetCacheNoHeader() throws Throwable {
+ Request req = newRequest();
+ requestResponse("0", req);
+ assertCached(netCache, req, true);
+ }
+
+ @Test public void testInterceptWriteSaveCacheNoHeader() throws Throwable {
+ Request req = newRequest();
+ requestResponse("0", req);
+ assertCached(saveCache, req, false);
+ }
+
+ @Test public void testInterceptWriteNetCacheSaveHeaderEnabled() throws
Throwable {
+ Request req = newSaveEnabledRequest();
+ requestResponse("0", req);
+ assertCached(netCache, req, true);
+ }
+
+ @Test public void testInterceptWriteSaveCacheSaveHeaderEnabled() throws
Throwable {
+ Request req = newSaveEnabledRequest();
+ requestResponse("0", req);
+ assertCached(saveCache, req, true);
+ }
+
+ @Test public void testInterceptWriteNetCacheSaveHeaderDisabled() throws
Throwable {
+ Request req = newSaveDisabledRequest();
+ requestResponse("0", req);
+ assertCached(netCache, req, true);
+ }
+
+ @Test public void testInterceptWriteSaveCacheSaveHeaderDisabled() throws
Throwable {
+ Request req = newSaveDisabledRequest();
+ requestResponse("0", req);
+ assertCached(saveCache, req, false);
+ }
+
+ @Test public void testInterceptReadNetCacheNoHeader() throws Throwable {
+ Request req = newRequest();
+ requestResponse("0", req);
+
+ saveCache.remove(req);
+ requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
+ }
+
+ @Test public void testInterceptReadSaveCacheNoHeader() throws Throwable {
+ Request req = newRequest();
+ requestResponse("0", newSaveEnabledRequest());
+
+ netCache.remove(req);
+ requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
+ }
+
+ @Test public void testInterceptReadNetCacheSaveHeaderEnabled() throws
Throwable {
+ Request req = newSaveEnabledRequest();
+ requestResponse("0", req);
+
+ saveCache.remove(req);
+ requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
+ }
+
+ @Test public void testInterceptReadSaveCacheSaveHeaderEnabled() throws
Throwable {
+ Request req = newSaveEnabledRequest();
+ requestResponse("0", req);
+
+ netCache.remove(req);
+ requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
+ }
+
+ @Test public void testInterceptReadNetCacheSaveHeaderDisabled() throws
Throwable {
+ Request req = newSaveDisabledRequest();
+ requestResponse("0", req);
+
+ saveCache.remove(req);
+ requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
+ }
+
+ @Test public void testInterceptReadSaveCacheSaveHeaderDisabled() throws
Throwable {
+ Request req = newSaveDisabledRequest();
+ requestResponse("0", newSaveEnabledRequest());
+
+ netCache.remove(req);
+ requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
+ }
+
+ @Test public void testInterceptUpdateNetCacheNoHeader() throws Throwable {
+ Request req = newRequest();
+ requestResponse("0", req);
+ requestResponse("1", req);
+
+ saveCache.remove(req);
+ requestOnlyIfCachedResponse("1", newOnlyIfCachedRequest());
+ }
+
+ @Test public void testInterceptUpdateSaveCacheNoHeader() throws Throwable {
+ Request req = newRequest();
+ requestResponse("0", newSaveEnabledRequest());
+ requestResponse("1", req);
+
+ netCache.remove(req);
+ requestOnlyIfCachedResponse("1", newOnlyIfCachedRequest());
+ }
+
+ @Test public void testInterceptUpdateNetCacheSaveHeaderEnabled() throws
Throwable {
+ Request req = newRequest();
+ requestResponse("0", req);
+ requestResponse("1", newSaveEnabledRequest());
+
+ saveCache.remove(req);
+ requestOnlyIfCachedResponse("1", newOnlyIfCachedRequest());
+ }
+
+ @Test public void testInterceptUpdateSaveCacheSaveHeaderEnabled() throws
Throwable {
+ Request req = newRequest();
+ requestResponse("0", req);
+ requestResponse("1", newSaveEnabledRequest());
+
+ netCache.remove(req);
+ requestOnlyIfCachedResponse("1", newOnlyIfCachedRequest());
+ }
+
+ @Test public void
testInterceptUpdateNetCacheSaveHeaderEnabledThenNoHeader() throws Throwable {
+ Request req = newRequest();
+ requestResponse("0", req);
+ requestResponse("1", newSaveEnabledRequest());
+
+ saveCache.remove(req);
+
+ requestResponse("2", req);
+
+ saveCache.remove(req);
+ requestOnlyIfCachedResponse("2", newOnlyIfCachedRequest());
+ }
+
+ @Test public void
testInterceptUpdateSaveCacheSaveHeaderEnabledThenNoHeader() throws Throwable {
+ Request req = newRequest();
+ requestResponse("0", req);
+ requestResponse("1", newSaveEnabledRequest());
+
+ netCache.remove(req);
+
+ requestResponse("2", req);
+
+ netCache.remove(req);
+ requestOnlyIfCachedResponse("2", newOnlyIfCachedRequest());
+ }
+
+ @Test public void testInterceptErrorNetCache() throws Throwable {
+ Request req = newRequest();
+ requestResponse("0", req);
+
+ saveCache.remove(req);
+
+ requestResponseError("0", req);
+ }
+
+ @Test public void testInterceptErrorSaveCache() throws Throwable {
+ Request req = newSaveEnabledRequest();
+ requestResponse("0", req);
+
+ netCache.remove(req);
+
+ requestResponseError("0", req);
+ }
+
+ private void requestResponseError(@NonNull String body, @NonNull Request
req) throws Throwable {
+ enqueue404();
+ String rsp = executeRequest(req);
+ server().takeRequest();
+ assertThat(rsp, is(body));
+ }
+
+ private void requestResponse(@NonNull String body, @NonNull Request req)
throws Throwable {
+ server().enqueue(body);
+ String rsp = executeRequest(req);
+ server().takeRequest();
+ assertThat(rsp, is(body));
+ }
+
+ private void requestOnlyIfCachedResponse(@NonNull String body, @NonNull
Request req) throws Throwable {
+ String rsp = executeRequest(req);
+ assertThat(rsp, is(body));
+ }
+
+ private String executeRequest(@NonNull Request req) throws Throwable {
+ // Note: raw non-Retrofit usage of OkHttp Requests requires that the
Response body is read
+ // for the cache to be written.
+ return okHttpClient().newCall(req).execute().body().string();
+ }
+
+ @NonNull private Request newOnlyIfCachedRequest() {
+ return
newRequest().newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
+ }
+
+ @NonNull private Request newSaveEnabledRequest() {
+ return newRequest().newBuilder().header(SaveHeader.FIELD,
SaveHeader.VAL_ENABLED).build();
+ }
+
+ @NonNull private Request newSaveDisabledRequest() {
+ return newRequest().newBuilder().header(SaveHeader.FIELD,
SaveHeader.VAL_DISABLED).build();
+ }
+
+ @NonNull private Request newRequest() {
+ return new Request.Builder().url(server().getUrl(URL)).build();
+ }
+
+ private void assertCached(@NonNull CacheDelegate cacheDelegate, @NonNull
Request req,
+ boolean cached) {
+ assertThat(cacheDelegate.isCached(req.url().toString()), is(cached));
+ }
+}
diff --git
a/app/src/test/java/org/wikipedia/dataclient/mwapi/page/MwPageClientTest.java
b/app/src/test/java/org/wikipedia/dataclient/mwapi/page/MwPageClientTest.java
index 1993765..1ef1445 100644
---
a/app/src/test/java/org/wikipedia/dataclient/mwapi/page/MwPageClientTest.java
+++
b/app/src/test/java/org/wikipedia/dataclient/mwapi/page/MwPageClientTest.java
@@ -15,7 +15,8 @@
public class MwPageClientTest extends MockWebServerTest {
private PageClient subject;
- @Before public void setUp() {
+ @Before public void setUp() throws Throwable {
+ super.setUp();
subject = new MwPageClient(service(MwPageService.class));
}
diff --git
a/app/src/test/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategyTest.java
b/app/src/test/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategyTest.java
new file mode 100644
index 0000000..76a0713
--- /dev/null
+++
b/app/src/test/java/org/wikipedia/dataclient/okhttp/cache/CacheDelegateStrategyTest.java
@@ -0,0 +1,26 @@
+package org.wikipedia.dataclient.okhttp.cache;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.wikipedia.Constants;
+import org.wikipedia.test.TestRunner;
+
+import okhttp3.Request;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+@RunWith(TestRunner.class) public class CacheDelegateStrategyTest {
+ @Test public void testIsCacheableTrue() {
+ Request req = new Request.Builder()
+ .url(Constants.WIKIPEDIA_URL)
+ .addHeader(SaveHeader.FIELD, SaveHeader.VAL_ENABLED)
+ .build();
+ assertThat(CacheDelegateStrategy.isCacheable(req), is(true));
+ }
+
+ @Test public void testIsCacheableFalse() {
+ Request req = new
Request.Builder().url(Constants.WIKIPEDIA_URL).build();
+ assertThat(CacheDelegateStrategy.isCacheable(req), is(false));
+ }
+}
diff --git
a/app/src/test/java/org/wikipedia/dataclient/okhttp/cache/SaveHeaderTest.java
b/app/src/test/java/org/wikipedia/dataclient/okhttp/cache/SaveHeaderTest.java
new file mode 100644
index 0000000..d36867c
--- /dev/null
+++
b/app/src/test/java/org/wikipedia/dataclient/okhttp/cache/SaveHeaderTest.java
@@ -0,0 +1,22 @@
+package org.wikipedia.dataclient.okhttp.cache;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.wikipedia.test.TestRunner;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+@RunWith(TestRunner.class) public class SaveHeaderTest {
+ @Test public void testIsSaveEnabledTrue() {
+ assertThat(SaveHeader.isSaveEnabled("true"), is(true));
+ }
+
+ @Test public void testIsSaveEnabledFalse() {
+ assertThat(SaveHeader.isSaveEnabled("false"), is(false));
+ }
+
+ @Test public void testIsSaveEnabledCaseInsensitive() {
+ assertThat(SaveHeader.isSaveEnabled("TRUE"), is(true));
+ }
+}
diff --git
a/app/src/test/java/org/wikipedia/dataclient/restbase/page/RbPageClientTest.java
b/app/src/test/java/org/wikipedia/dataclient/restbase/page/RbPageClientTest.java
index b326379..0eee3e6 100644
---
a/app/src/test/java/org/wikipedia/dataclient/restbase/page/RbPageClientTest.java
+++
b/app/src/test/java/org/wikipedia/dataclient/restbase/page/RbPageClientTest.java
@@ -15,7 +15,8 @@
public class RbPageClientTest extends MockWebServerTest {
private PageClient subject;
- @Before public void setUp() {
+ @Before public void setUp() throws Throwable {
+ super.setUp();
subject = new RbPageClient(service(RbPageService.class));
}
diff --git a/app/src/test/java/org/wikipedia/test/MockWebServerTest.java
b/app/src/test/java/org/wikipedia/test/MockWebServerTest.java
index 0cbf765..0df67d4 100644
--- a/app/src/test/java/org/wikipedia/test/MockWebServerTest.java
+++ b/app/src/test/java/org/wikipedia/test/MockWebServerTest.java
@@ -16,9 +16,15 @@
@RunWith(TestRunner.class)
public abstract class MockWebServerTest {
+ private OkHttpClient okHttpClient;
private final TestWebServer server = new TestWebServer();
@Before public void setUp() throws Throwable {
+ okHttpClient = OkHttpConnectionFactory
+ .getClient()
+ .newBuilder()
+ .dispatcher(new Dispatcher(new ImmediateExecutorService()))
+ .build();
server.setUp();
}
@@ -44,18 +50,19 @@
server.enqueue(new MockResponse().setBody("{}"));
}
- @NonNull public <T> T service(Class<T> clazz) {
+ @NonNull protected OkHttpClient okHttpClient() {
+ return okHttpClient;
+ }
+
+ @NonNull protected <T> T service(Class<T> clazz) {
return service(clazz, server().getUrl());
}
- @NonNull public <T> T service(Class<T> clazz, @NonNull String url) {
- OkHttpClient okHttp = OkHttpConnectionFactory.getClient().newBuilder()
- .dispatcher(new Dispatcher(new ImmediateExecutorService()))
- .build();
+ @NonNull protected <T> T service(Class<T> clazz, @NonNull String url) {
return new Retrofit.Builder()
.baseUrl(url)
.callbackExecutor(new ImmediateExecutor())
- .client(okHttp)
+ .client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson()))
.build()
.create(clazz);
diff --git a/gradle/src/checkstyle.gradle b/gradle/src/checkstyle.gradle
index 325e80b..80f4b07 100644
--- a/gradle/src/checkstyle.gradle
+++ b/gradle/src/checkstyle.gradle
@@ -16,6 +16,7 @@
include '**/*.java'
exclude '**/gen/**'
exclude '**/com/facebook/samples/**/*.java'
+ exclude '**/okhttp3/**/*.java'
classpath = configurations.compile
}
\ No newline at end of file
--
To view, visit https://gerrit.wikimedia.org/r/340906
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I45825777d18cbbc686d757a715002464cf94ae3f
Gerrit-PatchSet: 7
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Niedzielski <[email protected]>
Gerrit-Reviewer: Brion VIBBER <[email protected]>
Gerrit-Reviewer: Dbrant <[email protected]>
Gerrit-Reviewer: Mholloway <[email protected]>
Gerrit-Reviewer: Niedzielski <[email protected]>
Gerrit-Reviewer: Sniedzielski <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits