diff --git a/filesystem/src/main/java/com/nytimes/android/external/fs3/FSEraser.java b/filesystem/src/main/java/com/nytimes/android/external/fs3/FSEraser.java index 4bc7e977..e5aa616e 100644 --- a/filesystem/src/main/java/com/nytimes/android/external/fs3/FSEraser.java +++ b/filesystem/src/main/java/com/nytimes/android/external/fs3/FSEraser.java @@ -8,10 +8,9 @@ import javax.annotation.Nonnull; -import io.reactivex.Observable; -import okio.BufferedSource; +import io.reactivex.Single; -public class FSEraser implements DiskErase { +public class FSEraser implements DiskErase { final FileSystem fileSystem; final PathResolver pathResolver; @@ -22,8 +21,8 @@ public FSEraser(FileSystem fileSystem, PathResolver pathResolver) { @Nonnull @Override - public Observable delete(final @Nonnull T key) { - return Observable.fromCallable(new Callable() { + public Single delete(final @Nonnull T key) { + return Single.fromCallable(new Callable() { @Nonnull @Override @SuppressWarnings("PMD.SignatureDeclareThrowsException") diff --git a/filesystem/src/main/java/com/nytimes/android/external/fs3/FileSystemPersister.java b/filesystem/src/main/java/com/nytimes/android/external/fs3/FileSystemPersister.java index 61aca875..234bbd76 100644 --- a/filesystem/src/main/java/com/nytimes/android/external/fs3/FileSystemPersister.java +++ b/filesystem/src/main/java/com/nytimes/android/external/fs3/FileSystemPersister.java @@ -1,10 +1,12 @@ package com.nytimes.android.external.fs3; import com.nytimes.android.external.fs3.filesystem.FileSystem; +import com.nytimes.android.external.store3.base.Clearable; import com.nytimes.android.external.store3.base.Persister; import javax.annotation.Nonnull; +import io.reactivex.Completable; import io.reactivex.Maybe; import io.reactivex.Single; import okio.BufferedSource; @@ -15,13 +17,15 @@ * Make sure to have keys containing same data resolve to same "path" * @param key type */ -public final class FileSystemPersister implements Persister { +public final class FileSystemPersister implements Persister, Clearable { private final FSReader fileReader; private final FSWriter fileWriter; + private final FSEraser fileEraser; private FileSystemPersister(FileSystem fileSystem, PathResolver pathResolver) { fileReader = new FSReader<>(fileSystem, pathResolver); fileWriter = new FSWriter<>(fileSystem, pathResolver); + fileEraser = new FSEraser<>(fileSystem, pathResolver); } @Nonnull @@ -44,4 +48,9 @@ public Maybe read(@Nonnull final T key) { public Single write(@Nonnull final T key, @Nonnull final BufferedSource data) { return fileWriter.write(key, data); } + + @Override + public Completable clear(@Nonnull T key) { + return fileEraser.delete(key).toCompletable(); + } } diff --git a/filesystem/src/main/java/com/nytimes/android/external/fs3/FileSystemRecordPersister.java b/filesystem/src/main/java/com/nytimes/android/external/fs3/FileSystemRecordPersister.java index 48d8cb7f..14c9420d 100644 --- a/filesystem/src/main/java/com/nytimes/android/external/fs3/FileSystemRecordPersister.java +++ b/filesystem/src/main/java/com/nytimes/android/external/fs3/FileSystemRecordPersister.java @@ -1,6 +1,7 @@ package com.nytimes.android.external.fs3; import com.nytimes.android.external.fs3.filesystem.FileSystem; +import com.nytimes.android.external.store3.base.Clearable; import com.nytimes.android.external.store3.base.Persister; import com.nytimes.android.external.store3.base.RecordProvider; import com.nytimes.android.external.store3.base.RecordState; @@ -9,6 +10,7 @@ import javax.annotation.Nonnull; +import io.reactivex.Completable; import io.reactivex.Maybe; import io.reactivex.Single; import okio.BufferedSource; @@ -20,9 +22,11 @@ * * @param key type */ -public final class FileSystemRecordPersister implements Persister, RecordProvider { +public final class FileSystemRecordPersister implements Persister, + Clearable, RecordProvider { private final FSReader fileReader; private final FSWriter fileWriter; + private final FSEraser fileEraser; private final FileSystem fileSystem; private final PathResolver pathResolver; private final long expirationDuration; @@ -38,6 +42,7 @@ private FileSystemRecordPersister(FileSystem fileSystem, PathResolver pathR this.expirationUnit = expirationUnit; fileReader = new FSReader<>(fileSystem, pathResolver); fileWriter = new FSWriter<>(fileSystem, pathResolver); + fileEraser = new FSEraser<>(fileSystem, pathResolver); } @Nonnull @@ -69,4 +74,9 @@ public Maybe read(@Nonnull Key key) { public Single write(@Nonnull Key key, @Nonnull BufferedSource bufferedSource) { return fileWriter.write(key, bufferedSource); } + + @Override + public Completable clear(@Nonnull Key key) { + return fileEraser.delete(key).toCompletable(); + } } diff --git a/filesystem/src/test/java/com/nytimes/android/external/fs3/StoreNetworkBeforeStaleFailTest.java b/filesystem/src/test/java/com/nytimes/android/external/fs3/StoreNetworkBeforeStaleFailTest.java index 749cffc8..06c1f796 100644 --- a/filesystem/src/test/java/com/nytimes/android/external/fs3/StoreNetworkBeforeStaleFailTest.java +++ b/filesystem/src/test/java/com/nytimes/android/external/fs3/StoreNetworkBeforeStaleFailTest.java @@ -27,7 +27,7 @@ @RunWith(MockitoJUnitRunner.class) public class StoreNetworkBeforeStaleFailTest { - static final Exception sorry = new Exception("sorry"); + static final Exception SORRY = new Exception("sorry"); private static final BarCode barCode = new BarCode("key", "value"); @Mock Fetcher fetcher; @@ -46,10 +46,10 @@ public void setUp() { @Test public void networkBeforeStaleNoNetworkResponse() { - Single exception = Single.error(sorry); + Single exception = Single.error(SORRY); when(fetcher.fetch(barCode)) .thenReturn(exception); - store.get(barCode).test().assertError(sorry); + store.get(barCode).test().assertError(SORRY); verify(fetcher, times(1)).fetch(barCode); } @@ -63,7 +63,7 @@ public RecordState getRecordState(@Nonnull BarCode barCode) { @Nonnull @Override public Maybe read(@Nonnull BarCode barCode) { - return Maybe.error(sorry); + return Maybe.error(SORRY); } @Nonnull diff --git a/filesystem/src/test/java/com/nytimes/android/external/fs3/StoreRefreshWhenStaleTest.java b/filesystem/src/test/java/com/nytimes/android/external/fs3/StoreRefreshWhenStaleTest.java index e29fb315..b8a1661b 100644 --- a/filesystem/src/test/java/com/nytimes/android/external/fs3/StoreRefreshWhenStaleTest.java +++ b/filesystem/src/test/java/com/nytimes/android/external/fs3/StoreRefreshWhenStaleTest.java @@ -91,7 +91,7 @@ public void diskWasNotRefreshedWhenFreshRecord() { verify(fetcher, times(0)).fetch(barCode); verify(persister, times(1)).getRecordState(barCode); - store.clear(barCode); + store.clear(barCode).test().awaitTerminalEvent(); testObserver = store .get(barCode) .test(); diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/Clearable.java b/store/src/main/java/com/nytimes/android/external/store3/base/Clearable.java index 6b2bb13f..7866e1f3 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/Clearable.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/Clearable.java @@ -3,10 +3,12 @@ import javax.annotation.Nonnull; +import io.reactivex.Completable; + /** * Persisters should implement Clearable if they want store.clear(key) to also clear the persister * @param Type of key/request param in store */ public interface Clearable { - void clear(@Nonnull T key); + Completable clear(@Nonnull T key); } diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/DiskErase.java b/store/src/main/java/com/nytimes/android/external/store3/base/DiskErase.java index a170280b..b5d39e61 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/DiskErase.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/DiskErase.java @@ -3,12 +3,12 @@ import javax.annotation.Nonnull; -import io.reactivex.Observable; +import io.reactivex.Single; -public interface DiskErase { +public interface DiskErase { /** * @param key to use to delete a particular file using persister */ @Nonnull - Observable delete(@Nonnull Key key); + Single delete(@Nonnull Key key); } diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RealInternalStore.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/RealInternalStore.java index b5332b7d..55ac61f8 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RealInternalStore.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/RealInternalStore.java @@ -7,11 +7,16 @@ import com.nytimes.android.external.store3.base.InternalStore; import com.nytimes.android.external.store3.base.Persister; import com.nytimes.android.external.store3.util.KeyParser; + import java.util.AbstractMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + import javax.annotation.Nonnull; import javax.annotation.Nullable; + +import io.reactivex.Completable; import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.Single; @@ -76,8 +81,8 @@ public Single get(@Nonnull final Key key) { @Override public Single> getWithResult(@Nonnull Key key) { return lazyCacheWithResult(key) - .switchIfEmpty(fetchWithResult(key).toMaybe()) - .toSingle(); + .switchIfEmpty(fetchWithResult(key).toMaybe()) + .toSingle(); } @Override @@ -112,8 +117,8 @@ Maybe cache(@Nonnull final Key key) { */ private Maybe> lazyCacheWithResult(@Nonnull final Key key) { return Maybe - .defer(() -> cacheWithResult(key)) - .onErrorResumeNext(Maybe.>empty()); + .defer(() -> cacheWithResult(key)) + .onErrorResumeNext(Maybe.>empty()); } Maybe> cacheWithResult(@Nonnull final Key key) { @@ -270,7 +275,7 @@ void updateMemory(@Nonnull final Key key, final Parsed data) { @Override @Deprecated public void clearMemory() { - clear(); + clear().blockingAwait(); } /** @@ -281,23 +286,30 @@ public void clearMemory() { @Override @Deprecated public void clearMemory(@Nonnull final Key key) { - clear(key); + clear(key).blockingAwait(); } @Override - public void clear() { - for (Key cachedKey : memCache.asMap().keySet()) { - clear(cachedKey); - } + public Completable clear() { + return Completable.concat(memCache + .asMap() + .keySet() + .stream() + .map(this::clear) + .collect(Collectors.toList()) + ); } @Override - public void clear(@Nonnull Key key) { - inFlightRequests.invalidate(key); - memCache.invalidate(key); - StoreUtil.clearPersister(persister(), key); - notifyRefresh(key); + public Completable clear(@Nonnull Key key) { + return Completable + .fromAction(() -> { + inFlightRequests.invalidate(key); + memCache.invalidate(key); + }) + .andThen(StoreUtil.clearPersister(persister(), key)) + .doOnComplete(() -> notifyRefresh(key)); } private void notifyRefresh(@Nonnull Key key) { diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RealStore.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/RealStore.java index 5fab322f..86f740cc 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RealStore.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/RealStore.java @@ -12,6 +12,7 @@ import javax.annotation.Nonnull; +import io.reactivex.Completable; import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.Single; @@ -132,13 +133,13 @@ public void clearMemory(@Nonnull final Key key) { } @Override - public void clear() { - internalStore.clear(); + public Completable clear() { + return internalStore.clear(); } @Override - public void clear(@Nonnull Key key) { - internalStore.clear(key); + public Completable clear(@Nonnull Key key) { + return internalStore.clear(key); } protected Maybe memory(@Nonnull Key key) { diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/Store.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/Store.java index 317dbd9b..0a146c80 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/Store.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/Store.java @@ -3,6 +3,8 @@ import com.nytimes.android.external.store.util.Result; import com.nytimes.android.external.store3.annotations.Experimental; import javax.annotation.Nonnull; + +import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; @@ -89,11 +91,11 @@ public interface Store { * purges all entries from memory and disk cache * Persister will only be cleared if they implements Clearable */ - void clear(); + Completable clear(); /** * Purge a particular entry from memory and disk cache. * Persister will only be cleared if they implements Clearable */ - void clear(@Nonnull V key); + Completable clear(@Nonnull V key); } diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/StoreUtil.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/StoreUtil.java index b03801d1..2762277d 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/StoreUtil.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/StoreUtil.java @@ -7,6 +7,7 @@ import javax.annotation.Nonnull; +import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.ObservableTransformer; import io.reactivex.subjects.PublishSubject; @@ -39,11 +40,13 @@ static boolean persisterIsStale(@Nonnull Key key, Persister return false; } - static void clearPersister(Persister persister, @Nonnull Key key) { + static Completable clearPersister(Persister persister, @Nonnull Key key) { boolean isPersisterClearable = persister instanceof Clearable; if (isPersisterClearable) { - ((Clearable) persister).clear(key); + return ((Clearable) persister).clear(key); + } else { + return Completable.complete(); } } } diff --git a/store/src/main/java/com/nytimes/android/external/store3/util/NoopPersister.java b/store/src/main/java/com/nytimes/android/external/store3/util/NoopPersister.java index b889430e..1d97c42f 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/util/NoopPersister.java +++ b/store/src/main/java/com/nytimes/android/external/store3/util/NoopPersister.java @@ -7,8 +7,10 @@ import com.nytimes.android.external.store3.base.impl.MemoryPolicy; import java.util.concurrent.TimeUnit; + import javax.annotation.Nonnull; +import io.reactivex.Completable; import io.reactivex.Maybe; import io.reactivex.Single; @@ -64,7 +66,7 @@ public Single write(@Nonnull Key key, @Nonnull Raw raw) { } @Override - public void clear(@Nonnull Key key) { - networkResponses.invalidate(key); + public Completable clear(@Nonnull Key key) { + return Completable.fromAction(() -> networkResponses.invalidate(key)); } } diff --git a/store/src/test/java/com/nytimes/android/external/store3/ClearStoreTest.java b/store/src/test/java/com/nytimes/android/external/store3/ClearStoreTest.java index 3a3a26fc..51791afb 100644 --- a/store/src/test/java/com/nytimes/android/external/store3/ClearStoreTest.java +++ b/store/src/test/java/com/nytimes/android/external/store3/ClearStoreTest.java @@ -12,6 +12,7 @@ import java.util.concurrent.atomic.AtomicInteger; +import io.reactivex.Completable; import io.reactivex.Maybe; import io.reactivex.Single; @@ -48,13 +49,13 @@ public void testClearSingleBarCode() { .thenReturn(Maybe.just(1)); //read from disk after making additional network call when(persister.write(barcode, 1)).thenReturn(Single.just(true)); when(persister.write(barcode, 2)).thenReturn(Single.just(true)); - + when(persister.clear(barcode)).thenReturn(Completable.complete()); store.get(barcode).test().awaitTerminalEvent(); assertThat(networkCalls.intValue()).isEqualTo(1); // after clearing the memory another call should be made - store.clear(barcode); + store.clear(barcode).test().awaitTerminalEvent(); store.get(barcode).test().awaitTerminalEvent(); verify(persister).clear(barcode); assertThat(networkCalls.intValue()).isEqualTo(2); @@ -82,13 +83,15 @@ public void testClearAllBarCodes() { when(persister.write(barcode2, 1)).thenReturn(Single.just(true)); when(persister.write(barcode2, 2)).thenReturn(Single.just(true)); + when(persister.clear(barcode1)).thenReturn(Completable.complete()); + when(persister.clear(barcode2)).thenReturn(Completable.complete()); // each request should produce one call store.get(barcode1).test().awaitTerminalEvent(); store.get(barcode2).test().awaitTerminalEvent(); assertThat(networkCalls.intValue()).isEqualTo(2); - store.clear(); + store.clear().test().awaitTerminalEvent(); // after everything is cleared each request should produce another 2 calls store.get(barcode1).test().awaitTerminalEvent(); diff --git a/store/src/test/java/com/nytimes/android/external/store3/GetRefreshingTest.java b/store/src/test/java/com/nytimes/android/external/store3/GetRefreshingTest.java index 1f9ea7a3..7a5c91b7 100644 --- a/store/src/test/java/com/nytimes/android/external/store3/GetRefreshingTest.java +++ b/store/src/test/java/com/nytimes/android/external/store3/GetRefreshingTest.java @@ -16,6 +16,7 @@ import javax.annotation.Nonnull; +import io.reactivex.Completable; import io.reactivex.Maybe; import io.reactivex.Single; import io.reactivex.observers.TestObserver; @@ -49,13 +50,14 @@ public void testRefreshOnClear() { .thenReturn(Maybe.just(1)); //read from disk after making additional network call when(persister.write(barcode, 1)).thenReturn(Single.just(true)); when(persister.write(barcode, 2)).thenReturn(Single.just(true)); + when(persister.clear(barcode)).thenReturn(Completable.complete()); TestObserver refreshingObservable = store.getRefreshing(barcode).test(); refreshingObservable.assertValueCount(1); assertThat(networkCalls.intValue()).isEqualTo(1); //clearing the store should produce another network call - store.clear(barcode); + store.clear(barcode).test().awaitTerminalEvent(); refreshingObservable.assertValueCount(2); assertThat(networkCalls.intValue()).isEqualTo(2); @@ -86,6 +88,9 @@ public void testRefreshOnClearAll() { when(persister.write(barcode2, 1)).thenReturn(Single.just(true)); when(persister.write(barcode2, 2)).thenReturn(Single.just(true)); + when(persister.clear(barcode1)).thenReturn(Completable.complete()); + when(persister.clear(barcode2)).thenReturn(Completable.complete()); + TestObserver testObservable1 = store.getRefreshing(barcode1).test(); TestObserver testObservable2 = store.getRefreshing(barcode2).test(); testObservable1.assertValueCount(1); @@ -93,7 +98,7 @@ public void testRefreshOnClearAll() { assertThat(networkCalls.intValue()).isEqualTo(2); - store.clear(); + store.clear().test().awaitTerminalEvent(); assertThat(networkCalls.intValue()).isEqualTo(4); @@ -102,7 +107,7 @@ public void testRefreshOnClearAll() { //everything will be mocked static class ClearingPersister implements Persister, Clearable { @Override - public void clear(@Nonnull BarCode key) { + public Completable clear(@Nonnull BarCode key) { throw new RuntimeException(); } diff --git a/store/src/test/java/com/nytimes/android/external/store3/StoreTest.java b/store/src/test/java/com/nytimes/android/external/store3/StoreTest.java index 4026088c..80c30694 100644 --- a/store/src/test/java/com/nytimes/android/external/store3/StoreTest.java +++ b/store/src/test/java/com/nytimes/android/external/store3/StoreTest.java @@ -179,7 +179,7 @@ public void testDoubleTapWithResult() { public void testSubclass() { RealStore simpleStore = new SampleStore(fetcher, persister); - simpleStore.clear(); + simpleStore.clear().test().awaitTerminalEvent(); when(fetcher.fetch(barCode)) .thenReturn(Single.just(NETWORK)); @@ -200,7 +200,7 @@ public void testSubclass() { public void testSubclassWithResult() { RealStore simpleStore = new SampleStore(fetcher, persister); - simpleStore.clear(); + simpleStore.clear().test().awaitTerminalEvent(); when(fetcher.fetch(barCode)) .thenReturn(Single.just(NETWORK)); diff --git a/store/src/test/java/com/nytimes/android/external/store3/StreamOneKeyTest.java b/store/src/test/java/com/nytimes/android/external/store3/StreamOneKeyTest.java index 51bee2d9..7654508d 100644 --- a/store/src/test/java/com/nytimes/android/external/store3/StreamOneKeyTest.java +++ b/store/src/test/java/com/nytimes/android/external/store3/StreamOneKeyTest.java @@ -66,7 +66,7 @@ public void testStream() { //fetch from network, write to disk and notifiy subscribers streamObservable.assertValueCount(1); - store.clear(); + store.clear().test().awaitTerminalEvent(); //fetch should notify subscribers again store.fetch(barCode).test().awaitCount(1); streamObservable.assertValues(TEST_ITEM, TEST_ITEM2);