From 7c74b3409cec9d555e1212cec9d4cf9c80d0f4af Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Thu, 24 May 2018 17:18:03 -0400 Subject: [PATCH 01/11] roomStore --- app/build.gradle | 28 ++- .../com/nytimes/android/sample/SampleApp.java | 39 ++- .../nytimes/android/sample/SampleRoomStore.kt | 78 ++++++ build.gradle | 4 +- buildsystem/dependencies.gradle | 6 +- gradle/wrapper/gradle-wrapper.properties | 4 +- store/build.gradle | 16 ++ store/gradle.properties | 1 + .../external/store3/base/RoomDiskRead.java | 11 + .../external/store3/base/RoomDiskWrite.java | 15 ++ .../external/store3/base/RoomPersister.java | 33 +++ .../store3/base/impl/RoomCacheFactory.java | 62 +++++ .../store3/base/impl/RoomInternalStore.java | 232 ++++++++++++++++++ .../external/store3/base/impl/RoomStore.java | 47 ++++ 14 files changed, 562 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt create mode 100644 store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskRead.java create mode 100644 store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskWrite.java create mode 100644 store/src/main/java/com/nytimes/android/external/store3/base/RoomPersister.java create mode 100644 store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomCacheFactory.java create mode 100644 store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomInternalStore.java create mode 100644 store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomStore.java diff --git a/app/build.gradle b/app/build.gradle index eb3599aa..58144657 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' apply plugin: 'com.getkeepsafe.dexcount' - +apply plugin: 'kotlin-kapt' android { compileSdkVersion versions.compileSdk buildToolsVersion versions.buildTools @@ -31,6 +32,7 @@ android { exclude 'META-INF/rxjava.properties' } } +def room_version = "1.1.0" // or, for latest rc, use "1.1.1-rc1" dependencies { @@ -48,9 +50,25 @@ dependencies { annotationProcessor libraries.immutablesValue // <-- for annotation processor compileOnly libraries.immutablesValue // <-- for annotation API compileOnly libraries.immutablesGson // for annotations - implementation 'com.nytimes.android:store3:3.0.1' - implementation 'com.nytimes.android:cache3:3.0.1' - implementation 'com.nytimes.android:middleware3:3.0.1' - implementation 'com.nytimes.android:filesystem3:3.0.1' + implementation project(':store') + implementation project(':cache') + implementation project(':middleware') + implementation project(':filesystem') implementation libraries.rxAndroid2 + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation "android.arch.persistence.room:runtime:$room_version" + annotationProcessor "android.arch.persistence.room:compiler:$room_version" + kapt "android.arch.persistence.room:compiler:$room_version" + +// androidTestImplementation "android.arch.persistence.room:testing:$room_version" + // optional - RxJava support for Room + implementation "android.arch.persistence.room:rxjava2:$room_version" } +repositories { + mavenCentral() +} + +configurations.all { + resolutionStrategy.force 'com.android.support:support-v4:26.1.0' +} \ No newline at end of file diff --git a/app/src/main/java/com/nytimes/android/sample/SampleApp.java b/app/src/main/java/com/nytimes/android/sample/SampleApp.java index 08f0dafd..d5ae457f 100644 --- a/app/src/main/java/com/nytimes/android/sample/SampleApp.java +++ b/app/src/main/java/com/nytimes/android/sample/SampleApp.java @@ -1,6 +1,8 @@ package com.nytimes.android.sample; +import android.annotation.SuppressLint; import android.app.Application; +import android.content.Context; import android.support.annotation.NonNull; import com.google.gson.Gson; @@ -17,9 +19,15 @@ import com.nytimes.android.sample.data.remote.Api; import java.io.IOException; +import java.util.List; import java.util.concurrent.TimeUnit; +import io.reactivex.Observable; import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; import okhttp3.ResponseBody; import okio.BufferedSource; import retrofit2.Retrofit; @@ -32,13 +40,38 @@ public class SampleApp extends Application { private Store persistedStore; private Persister persister; + static Context appContext; + @Override public void onCreate() { super.onCreate(); + appContext = this; + + + Disposable foo = SampleRoomStoreKt.getStore().get("") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(strings1 -> { + boolean success = strings1 != null; + }, throwable -> { + throwable.getStackTrace(); + }); + + foo= Observable.timer(15, TimeUnit.SECONDS) + .subscribe(it -> again()); + + + } - initPersister(); - this.nonPersistedStore = provideRedditStore(); - this.persistedStore = providePersistedRedditStore(); + private void again() { + Disposable bar = SampleRoomStoreKt.getStore().fetch("") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(strings1 -> { + boolean success = strings1 != null; + }, throwable -> { + throwable.getStackTrace(); + }); } private void initPersister() { diff --git a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt new file mode 100644 index 00000000..814a9c1f --- /dev/null +++ b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt @@ -0,0 +1,78 @@ +package com.nytimes.android.sample + +import android.arch.persistence.room.Dao +import android.arch.persistence.room.Database +import android.arch.persistence.room.Delete +import android.arch.persistence.room.Entity +import android.arch.persistence.room.Insert +import android.arch.persistence.room.PrimaryKey +import android.arch.persistence.room.Query +import android.arch.persistence.room.Room +import android.arch.persistence.room.Room.databaseBuilder +import android.arch.persistence.room.RoomDatabase +import android.os.Parcel +import android.os.Parcelable +import com.nytimes.android.external.store3.base.Fetcher +import com.nytimes.android.external.store3.base.RoomPersister +import com.nytimes.android.external.store3.base.impl.RoomInternalStore +import com.nytimes.android.external.store3.base.impl.StalePolicy +import io.reactivex.Flowable +import io.reactivex.Observable +import io.reactivex.Single + + +// File: User.java +@Entity +data class User( + @PrimaryKey(autoGenerate = true) var uid: Int = 0, + val name: String, + val lastName: String) + + +// File: UserDao.java +@Dao +interface UserDao { + @Query("SELECT name FROM user") + fun loadAll(): Flowable> + + @Query("SELECT * FROM user WHERE uid IN (:userIds)") + fun loadAllByUserId(vararg userIds: Int): List + + @Query("SELECT * FROM user where name LIKE :first AND lastName LIKE :last LIMIT 1") + fun loadOneByNameAndLastName(first: String, last: String): User + + @Insert + fun insertAll(vararg users: User) + + @Delete + fun delete(user: User) +} + +// File: AppDatabase.java +@Database(entities = arrayOf(User::class), version = 1) +abstract class AppDatabase : RoomDatabase() { + abstract fun userDao(): UserDao +} + +val db = Room.databaseBuilder(SampleApp.appContext, AppDatabase::class.java, "db").build() + + +val persister = object : RoomPersister, String> { + override fun read(key: String): Observable> { + return db.userDao().loadAll().toObservable().map { if (it.isEmpty()) throw Exception() else it } + } + + override fun write(key: String, user: User) { + db.userDao().insertAll(user) + } + +} +val fetcher=Fetcher { Single.just(User(name = "Mike", lastName = "naki")) } + +val store = SampleRoomStore( fetcher, persister) + +class SampleRoomStore(fetcher: Fetcher, + persister: RoomPersister, String>, + stalePolicy: StalePolicy = StalePolicy.UNSPECIFIED) : + RoomInternalStore, String>(fetcher, persister, stalePolicy) + diff --git a/build.gradle b/build.gradle index 5fe5f11e..240524b8 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ apply from: 'buildsystem/dependencies.gradle' // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.kotlin_version = '1.2.41' repositories { mavenLocal() maven { @@ -22,12 +23,13 @@ buildscript { ] dependencies { - classpath 'com.android.tools.build:gradle:3.1.2' + classpath 'com.android.tools.build:gradle:3.2.0-alpha15' classpath 'com.google.gms:google-services:3.0.0' classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.5.6' classpath 'net.ltgt.gradle:gradle-errorprone-plugin:0.0.11' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$rootProject.ext.versions.kotlin" classpath 'org.jetbrains.dokka:dokka-gradle-plugin:0.9.14' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index bc4fa42a..9f310258 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -15,13 +15,13 @@ allprojects { ext.versions = [ minSdk : 16, - targetSdk : 25, - compileSdk : 25, + targetSdk : 27, + compileSdk : 27, buildTools : '27.0.3', kotlin : '1.1.2-5', // UI libs. - supportLibs : '25.1.1', + supportLibs : '26.1.0', picasso : '2.5.2', butterKnife : '7.0.1', diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0fcbb13d..b5e6f608 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Mar 29 16:46:38 EEST 2018 +#Thu May 24 12:01:07 EDT 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip diff --git a/store/build.gradle b/store/build.gradle index e40f9636..a846f366 100644 --- a/store/build.gradle +++ b/store/build.gradle @@ -3,6 +3,11 @@ apply plugin: 'java' group = GROUP version = VERSION_NAME + +//Room +def room_version = "1.1.0" // or, for latest rc, use "1.1.1-rc1" + + dependencies { implementation project(path: ':cache') implementation libraries.rxJava2 @@ -13,6 +18,17 @@ dependencies { testImplementation libraries.assertJ testImplementation libraries.junit testCompileOnly libraries.jsr305 + + implementation "android.arch.persistence.room:runtime:$room_version" + annotationProcessor "android.arch.persistence.room:compiler:$room_version" +// kapt "android.arch.persistence.room:compiler:$room_version" + +// androidTestImplementation "android.arch.persistence.room:testing:$room_version" + // optional - RxJava support for Room + implementation "android.arch.persistence.room:rxjava2:$room_version" + + + } buildscript { diff --git a/store/gradle.properties b/store/gradle.properties index 6e7128dd..edef291d 100644 --- a/store/gradle.properties +++ b/store/gradle.properties @@ -1,3 +1,4 @@ POM_NAME=com.nytimes.android POM_ARTIFACT_ID=store3 POM_PACKAGING=aar +android.enableAapt2=false \ No newline at end of file diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskRead.java b/store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskRead.java new file mode 100644 index 00000000..0ef1ad4a --- /dev/null +++ b/store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskRead.java @@ -0,0 +1,11 @@ +package com.nytimes.android.external.store3.base; + +import javax.annotation.Nonnull; + +import io.reactivex.Maybe; +import io.reactivex.Observable; + +public interface RoomDiskRead { + @Nonnull + Observable read(@Nonnull Key key); +} diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskWrite.java b/store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskWrite.java new file mode 100644 index 00000000..57a032fd --- /dev/null +++ b/store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskWrite.java @@ -0,0 +1,15 @@ +package com.nytimes.android.external.store3.base; + +import javax.annotation.Nonnull; + +import io.reactivex.Single; + +public interface RoomDiskWrite { + /** + * @param key to use to get data from persister + * If data is not available implementer needs to + * either return Observable.empty or throw an exception + */ + @Nonnull + void write(@Nonnull Key key, @Nonnull Raw raw); +} diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/RoomPersister.java b/store/src/main/java/com/nytimes/android/external/store3/base/RoomPersister.java new file mode 100644 index 00000000..063a5870 --- /dev/null +++ b/store/src/main/java/com/nytimes/android/external/store3/base/RoomPersister.java @@ -0,0 +1,33 @@ +package com.nytimes.android.external.store3.base; + +import javax.annotation.Nonnull; + +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; + +/** + * Interface for fetching data from persister + * when implementing also think about implementing PathResolver to ease in creating primary keys + * + * @param data type before parsing + */ +public interface RoomPersister< Raw, Parsed,Key> extends RoomDiskRead, RoomDiskWrite { + + /** + * @param key to use to get data from persister + * If data is not available implementer needs to + * either return Observable.empty or throw an exception + */ + @Override + @Nonnull + Observable read(@Nonnull final Key key); + + /** + * @param key to use to store data to persister + * @param raw raw string to be stored + */ + @Override + @Nonnull + void write(@Nonnull final Key key, @Nonnull final Raw raw); +} diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomCacheFactory.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomCacheFactory.java new file mode 100644 index 00000000..a888d586 --- /dev/null +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomCacheFactory.java @@ -0,0 +1,62 @@ +package com.nytimes.android.external.store3.base.impl; + +import com.nytimes.android.external.cache3.Cache; +import com.nytimes.android.external.cache3.CacheBuilder; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; + +final class RoomCacheFactory { + private RoomCacheFactory() { + + } + + static Cache> createCache(MemoryPolicy memoryPolicy) { + if (memoryPolicy == null) { + return CacheBuilder + .newBuilder() + .maximumSize(StoreDefaults.getCacheSize()) + .expireAfterWrite(StoreDefaults.getCacheTTL(), StoreDefaults.getCacheTTLTimeUnit()) + .build(); + } else { + if (memoryPolicy.getExpireAfterAccess() == memoryPolicy.DEFAULT_POLICY) { + return CacheBuilder + .newBuilder() + .maximumSize(memoryPolicy.getMaxSize()) + .expireAfterWrite(memoryPolicy.getExpireAfterWrite(), memoryPolicy.getExpireAfterTimeUnit()) + .build(); + } else { + return CacheBuilder + .newBuilder() + .maximumSize(memoryPolicy.getMaxSize()) + .expireAfterAccess(memoryPolicy.getExpireAfterAccess(), memoryPolicy.getExpireAfterTimeUnit()) + .build(); + } + } + } + + static Cache> createInflighter(MemoryPolicy memoryPolicy) { + long expireAfterToSeconds = memoryPolicy == null ? StoreDefaults.getCacheTTLTimeUnit() + .toSeconds(StoreDefaults.getCacheTTL()) + : memoryPolicy.getExpireAfterTimeUnit().toSeconds(memoryPolicy.getExpireAfterWrite()); + long maximumInFlightRequestsDuration = TimeUnit.MINUTES.toSeconds(1); + + if (expireAfterToSeconds > maximumInFlightRequestsDuration) { + return CacheBuilder + .newBuilder() + .expireAfterWrite(maximumInFlightRequestsDuration, TimeUnit.SECONDS) + .build(); + } else { + long expireAfter = memoryPolicy == null ? StoreDefaults.getCacheTTL() : + memoryPolicy.getExpireAfterWrite(); + TimeUnit expireAfterUnit = memoryPolicy == null ? StoreDefaults.getCacheTTLTimeUnit() : + memoryPolicy.getExpireAfterTimeUnit(); + return CacheBuilder.newBuilder() + .expireAfterWrite(expireAfter, expireAfterUnit) + .build(); + } + } +} diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomInternalStore.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomInternalStore.java new file mode 100644 index 00000000..43a9e015 --- /dev/null +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomInternalStore.java @@ -0,0 +1,232 @@ +package com.nytimes.android.external.store3.base.impl; + +import com.nytimes.android.external.cache3.Cache; +import com.nytimes.android.external.store.util.Result; +import com.nytimes.android.external.store3.annotations.Experimental; +import com.nytimes.android.external.store3.base.Fetcher; +import com.nytimes.android.external.store3.base.InternalStore; +import com.nytimes.android.external.store3.base.RoomPersister; +import com.nytimes.android.external.store3.util.KeyParser; + +import java.util.AbstractMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import io.reactivex.Maybe; +import io.reactivex.Observable; +import io.reactivex.Single; +import io.reactivex.subjects.PublishSubject; + +/** + * Store to be used for loading an object from different data sources + * + * @param data type before parsing, usually a String, Reader or BufferedSource + * @param data type after parsing + *

+ * Example usage: @link + */ +public class RoomInternalStore implements RoomStore { + Cache> inFlightRequests; + Cache> memCache; + StalePolicy stalePolicy; + RoomPersister persister; + + private final PublishSubject refreshSubject = PublishSubject.create(); + private Fetcher fetcher; + private PublishSubject> subject; + + public RoomInternalStore(Fetcher fetcher, + RoomPersister persister, + StalePolicy stalePolicy) { + this(fetcher, persister, null, stalePolicy); + } + + RoomInternalStore(Fetcher fetcher, + RoomPersister persister, + MemoryPolicy memoryPolicy, + StalePolicy stalePolicy) { + + this.fetcher = fetcher; + this.persister = persister; + this.stalePolicy = stalePolicy; + + this.memCache = RoomCacheFactory.createCache(memoryPolicy); + this.inFlightRequests = RoomCacheFactory.createInflighter(memoryPolicy); + + subject = PublishSubject.create(); + } + + /** + * @param key + * @return an observable from the first data source that is available + */ + @Nonnull + @Override + public Observable get(@Nonnull final Key key) { + return lazyCache(key) + .switchIfEmpty(fetch(key)); + } + + /** + * @return data from memory + */ + private Observable lazyCache(@Nonnull final Key key) { + return Observable + .defer(() -> cache(key)) + .onErrorResumeNext(Observable.empty()); + } + + Observable cache(@Nonnull final Key key) { + try { + return memCache.get(key, () -> disk(key)); + } catch (ExecutionException e) { + return Observable.empty(); + } + } + + @Nonnull + public Observable memory(@Nonnull Key key) { + Observable cachedValue = memCache.getIfPresent(key); + return cachedValue == null ? Observable.empty() : cachedValue; + } + + /** + * Fetch data from persister and update memory after. If an error occurs, emit an empty observable + * so that the concat call in {@link #get(Key)} moves on to {@link #fetch(Key)} + * + * @param key + * @return + */ + @Nonnull + public Observable disk(@Nonnull final Key key) { +// if (StoreUtil.shouldReturnNetworkBeforeStale(persister, stalePolicy, key)) { +// return Maybe.empty(); +// } + return readDisk(key); + } + + Observable readDisk(@Nonnull final Key key) { + return persister().read(key) + .onErrorResumeNext( + Observable.empty()) + .doOnNext(parsed -> { + updateMemory(key, parsed); +// if (stalePolicy == StalePolicy.REFRESH_ON_STALE +// && StoreUtil.persisterIsStale(key, persister)) { +// backfillCache(key); +// } + }).cache(); + } + + @SuppressWarnings("CheckReturnValue") + void backfillCache(@Nonnull Key key) { + fetch(key).subscribe(parsed -> { + // do Nothing we are just backfilling cache + }, throwable -> { + // do nothing as we are just backfilling cache + }); + } + + + /** + * Will check to see if there exists an in flight observable and return it before + * going to network + * + * @return data from fetch and store it in memory and persister + */ + @Nonnull + @Override + public Observable fetch(@Nonnull final Key key) { + return Observable.defer(() -> fetchAndPersist(key)); + } + + + /** + * There should only be one fetch request in flight at any give time. + *

+ * Return cached request in the form of a Behavior Subject which will emit to its subscribers + * the last value it gets. Subject/Observable is cached in a {@link ConcurrentMap} to maintain + * thread safety. + * + * @param key resource identifier + * @return observable that emits a {@link Parsed} value + */ + @Nullable + Observable fetchAndPersist(@Nonnull final Key key) { + try { + return inFlightRequests.get(key, () -> response(key)); + } catch (ExecutionException e) { + return Observable.error(e); + } + } + + @Nonnull + Observable response(@Nonnull final Key key) { + return fetcher() + .fetch(key) + .doOnSuccess(it -> persister().write(key, it)) + .flatMapObservable(it -> readDisk(key)) + .onErrorResumeNext(throwable -> { + if (stalePolicy == StalePolicy.NETWORK_BEFORE_STALE) { + return readDisk(key).switchIfEmpty(Observable.error(throwable)); + } + return Observable.error(throwable); + }) + .doOnNext(data -> notifySubscribers(data, key)) + .doAfterTerminate(() -> inFlightRequests.invalidate(key)) + .cache(); + } + + void notifySubscribers(Parsed data, Key key) { + subject.onNext(new AbstractMap.SimpleEntry<>(key, data)); + } + + + /** + * Only update memory after persister has been successfully updated + * + * @param key + * @param data + */ + void updateMemory(@Nonnull final Key key, final Parsed data) { + memCache.put(key, Observable.just(data)); + } + + + @Override + public void clear() { + for (Key cachedKey : memCache.asMap().keySet()) { + clear(cachedKey); + } + } + + @Override + public void clear(@Nonnull Key key) { + inFlightRequests.invalidate(key); + memCache.invalidate(key); +// StoreUtil.clearPersister(persister(), key); + notifyRefresh(key); + } + + private void notifyRefresh(@Nonnull Key key) { + refreshSubject.onNext(key); + } + + /** + * @return DiskDAO that stores and stores data + */ + RoomPersister persister() { + return persister; + } + + /** + * + */ + Fetcher fetcher() { + return fetcher; + } +} + diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomStore.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomStore.java new file mode 100644 index 00000000..a6cbe537 --- /dev/null +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomStore.java @@ -0,0 +1,47 @@ +package com.nytimes.android.external.store3.base.impl; + +import com.nytimes.android.external.store.util.Result; +import com.nytimes.android.external.store3.annotations.Experimental; + +import javax.annotation.Nonnull; + +import io.reactivex.Observable; +import io.reactivex.Single; + +/** + * a {@link StoreBuilder StoreBuilder} + * will return an instance of a store + *

+ * A {@link RoomStore Store} can + * {@link RoomStore#get(V) Store.get() } cached data or + * force a call to {@link RoomStore#fetch(V) Store.fetch() } + * (skipping cache) + */ +public interface RoomStore { + + /** + * Return an Observable of T for request Barcode + * Data will be returned from oldest non expired source + * Sources are Memory Cache, Disk Cache, Inflight, Network Response + */ + @Nonnull + Observable get(@Nonnull V key); + + /** + * Return an Observable of T for requested Barcode skipping Memory & Disk Cache + */ + @Nonnull + Observable fetch(@Nonnull V key); + + /** + * purges all entries from memory and disk cache + * Persister will only be cleared if they implements Clearable + */ + void clear(); + + /** + * Purge a particular entry from memory and disk cache. + * Persister will only be cleared if they implements Clearable + */ + void clear(@Nonnull V key); +} From 67a353a5a62b159d28cf0bdc47edcd597b66eba2 Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Thu, 24 May 2018 17:26:46 -0400 Subject: [PATCH 02/11] kt the app class --- .../com/nytimes/android/sample/SampleApp.java | 152 ------------------ .../com/nytimes/android/sample/SampleApp.kt | 138 ++++++++++++++++ .../nytimes/android/sample/SampleRoomStore.kt | 14 +- 3 files changed, 144 insertions(+), 160 deletions(-) delete mode 100644 app/src/main/java/com/nytimes/android/sample/SampleApp.java create mode 100644 app/src/main/java/com/nytimes/android/sample/SampleApp.kt diff --git a/app/src/main/java/com/nytimes/android/sample/SampleApp.java b/app/src/main/java/com/nytimes/android/sample/SampleApp.java deleted file mode 100644 index d5ae457f..00000000 --- a/app/src/main/java/com/nytimes/android/sample/SampleApp.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.nytimes.android.sample; - -import android.annotation.SuppressLint; -import android.app.Application; -import android.content.Context; -import android.support.annotation.NonNull; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.nytimes.android.external.fs3.SourcePersisterFactory; -import com.nytimes.android.external.store3.base.Persister; -import com.nytimes.android.external.store3.base.impl.BarCode; -import com.nytimes.android.external.store3.base.impl.MemoryPolicy; -import com.nytimes.android.external.store3.base.impl.Store; -import com.nytimes.android.external.store3.base.impl.StoreBuilder; -import com.nytimes.android.external.store3.middleware.GsonParserFactory; -import com.nytimes.android.sample.data.model.GsonAdaptersModel; -import com.nytimes.android.sample.data.model.RedditData; -import com.nytimes.android.sample.data.remote.Api; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import io.reactivex.schedulers.Schedulers; -import okhttp3.ResponseBody; -import okio.BufferedSource; -import retrofit2.Retrofit; -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; -import retrofit2.converter.gson.GsonConverterFactory; - -public class SampleApp extends Application { - - private Store nonPersistedStore; - private Store persistedStore; - private Persister persister; - - static Context appContext; - - @Override - public void onCreate() { - super.onCreate(); - appContext = this; - - - Disposable foo = SampleRoomStoreKt.getStore().get("") - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(strings1 -> { - boolean success = strings1 != null; - }, throwable -> { - throwable.getStackTrace(); - }); - - foo= Observable.timer(15, TimeUnit.SECONDS) - .subscribe(it -> again()); - - - } - - private void again() { - Disposable bar = SampleRoomStoreKt.getStore().fetch("") - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(strings1 -> { - boolean success = strings1 != null; - }, throwable -> { - throwable.getStackTrace(); - }); - } - - private void initPersister() { - try { - persister = newPersister(); - } catch (IOException exception) { - throw new RuntimeException(exception); - } - } - - public Store getNonPersistedStore() { - return this.nonPersistedStore; - } - - public Store getPersistedStore() { - return this.persistedStore; - } - - /** - * Provides a Store which only retains RedditData for 10 seconds in memory. - */ - private Store provideRedditStore() { - return StoreBuilder.barcode() - .fetcher(barCode -> provideRetrofit().fetchSubreddit(barCode.getKey(), "10")) - .memoryPolicy( - MemoryPolicy - .builder() - .setExpireAfterWrite(10) - .setExpireAfterTimeUnit(TimeUnit.SECONDS) - .build() - ) - .open(); - } - - /** - * Provides a Store which will persist RedditData to the cache, and use Gson to parse the JSON - * that comes back from the network into RedditData. - */ - private Store providePersistedRedditStore() { - return StoreBuilder.parsedWithKey() - .fetcher(this::fetcher) - .persister(persister) - .parser(GsonParserFactory.createSourceParser(provideGson(), RedditData.class)) - .open(); - } - - /** - * Returns a new Persister with the cache as the root. - */ - private Persister newPersister() throws IOException { - return SourcePersisterFactory.create(getApplicationContext().getCacheDir()); - } - - /** - * Returns a "fetcher" which will retrieve new data from the network. - */ - @NonNull - private Single fetcher(BarCode barCode) { - return provideRetrofit().fetchSubredditForPersister(barCode.getKey(), "10") - .map(ResponseBody::source); - } - - private Api provideRetrofit() { - return new Retrofit.Builder() - .baseUrl("http://reddit.com/") - .addConverterFactory(GsonConverterFactory.create(provideGson())) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .validateEagerly(BuildConfig.DEBUG) // Fail early: check Retrofit configuration at creation time in Debug build. - .build() - .create(Api.class); - } - - Gson provideGson() { - return new GsonBuilder() - .registerTypeAdapterFactory(new GsonAdaptersModel()) - .create(); - } -} diff --git a/app/src/main/java/com/nytimes/android/sample/SampleApp.kt b/app/src/main/java/com/nytimes/android/sample/SampleApp.kt new file mode 100644 index 00000000..7f76047f --- /dev/null +++ b/app/src/main/java/com/nytimes/android/sample/SampleApp.kt @@ -0,0 +1,138 @@ +package com.nytimes.android.sample + +import android.annotation.SuppressLint +import android.app.Application +import android.content.Context + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.nytimes.android.external.fs3.SourcePersisterFactory +import com.nytimes.android.external.store3.base.Fetcher +import com.nytimes.android.external.store3.base.Persister +import com.nytimes.android.external.store3.base.impl.BarCode +import com.nytimes.android.external.store3.base.impl.MemoryPolicy +import com.nytimes.android.external.store3.base.impl.Store +import com.nytimes.android.external.store3.base.impl.StoreBuilder +import com.nytimes.android.external.store3.middleware.GsonParserFactory +import com.nytimes.android.sample.data.model.GsonAdaptersModel +import com.nytimes.android.sample.data.model.RedditData +import com.nytimes.android.sample.data.remote.Api + +import java.io.IOException +import java.util.concurrent.TimeUnit + +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.functions.Consumer +import io.reactivex.schedulers.Schedulers +import okhttp3.ResponseBody +import okio.BufferedSource +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory + +class SampleApp : Application() { + + val nonPersistedStore: Store? = null + val persistedStore: Store? = null + private var persister: Persister? = null + + override fun onCreate() { + super.onCreate() + appContext = this + + + var foo = store.get("") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ strings1 -> val success = strings1 != null }) { throwable -> throwable.stackTrace } + + foo = Observable.timer(15, TimeUnit.SECONDS) + .subscribe { it -> again() } + + + } + + private fun again() { + val bar = store.fetch("") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ strings1 -> val success = strings1 != null }) { throwable -> throwable.stackTrace } + } + + private fun initPersister() { + try { + persister = newPersister() + } catch (exception: IOException) { + throw RuntimeException(exception) + } + + } + + /** + * Provides a Store which only retains RedditData for 10 seconds in memory. + */ + private fun provideRedditStore(): Store { + return StoreBuilder.barcode() + .fetcher { barCode -> provideRetrofit().fetchSubreddit(barCode.key, "10") } + .memoryPolicy( + MemoryPolicy + .builder() + .setExpireAfterWrite(10) + .setExpireAfterTimeUnit(TimeUnit.SECONDS) + .build() + ) + .open() + } + + /** + * Provides a Store which will persist RedditData to the cache, and use Gson to parse the JSON + * that comes back from the network into RedditData. + */ + private fun providePersistedRedditStore(): Store { + return StoreBuilder.parsedWithKey() + .fetcher(Fetcher { this.fetcher(it) }) + .persister(persister!!) + .parser(GsonParserFactory.createSourceParser(provideGson(), RedditData::class.java)) + .open() + } + + /** + * Returns a new Persister with the cache as the root. + */ + @Throws(IOException::class) + private fun newPersister(): Persister { + return SourcePersisterFactory.create(applicationContext.cacheDir) + } + + /** + * Returns a "fetcher" which will retrieve new data from the network. + */ + private fun fetcher(barCode: BarCode): Single { + return provideRetrofit().fetchSubredditForPersister(barCode.key, "10") + .map( { it.source() }) + } + + private fun provideRetrofit(): Api { + return Retrofit.Builder() + .baseUrl("http://reddit.com/") + .addConverterFactory(GsonConverterFactory.create(provideGson())) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .validateEagerly(BuildConfig.DEBUG) // Fail early: check Retrofit configuration at creation time in Debug build. + .build() + .create(Api::class.java) + } + + internal fun provideGson(): Gson { + return GsonBuilder() + .registerTypeAdapterFactory(GsonAdaptersModel()) + .create() + } + + companion object { + + var appContext: Context? = null + } +} diff --git a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt index 814a9c1f..861d99bf 100644 --- a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt +++ b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt @@ -35,11 +35,6 @@ interface UserDao { @Query("SELECT name FROM user") fun loadAll(): Flowable> - @Query("SELECT * FROM user WHERE uid IN (:userIds)") - fun loadAllByUserId(vararg userIds: Int): List - - @Query("SELECT * FROM user where name LIKE :first AND lastName LIKE :last LIMIT 1") - fun loadOneByNameAndLastName(first: String, last: String): User @Insert fun insertAll(vararg users: User) @@ -67,12 +62,15 @@ val persister = object : RoomPersister, String> { } } -val fetcher=Fetcher { Single.just(User(name = "Mike", lastName = "naki")) } - -val store = SampleRoomStore( fetcher, persister) +//store class SampleRoomStore(fetcher: Fetcher, persister: RoomPersister, String>, stalePolicy: StalePolicy = StalePolicy.UNSPECIFIED) : RoomInternalStore, String>(fetcher, persister, stalePolicy) +val fetcher=Fetcher { Single.just(User(name = "Mike", lastName = "naki")) } + +val store = SampleRoomStore( fetcher, persister) + + From 907d2309d64d537b4ce0edf02849916d33d04983 Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Thu, 24 May 2018 22:29:15 -0400 Subject: [PATCH 03/11] refactor --- .../nytimes/android/sample/SampleRoomStore.kt | 22 ++--- store/build.gradle | 3 +- .../external/store3/base/BasePersister.java | 8 ++ .../external/store3/base/Persister.java | 2 +- .../external/store3/base/RoomPersister.java | 33 ------- .../store3/base/impl/CacheFactory.java | 49 +++++++++- .../store3/base/impl/RoomCacheFactory.java | 62 ------------ .../external/store3/base/impl/StoreUtil.java | 14 +-- .../impl/{ => room}/RoomInternalStore.java | 94 ++++++++----------- .../base/impl/{ => room}/RoomStore.java | 6 +- .../store3/base/{ => room}/RoomDiskRead.java | 3 +- .../store3/base/{ => room}/RoomDiskWrite.java | 2 +- .../store3/base/room/RoomPersister.java | 33 +++++++ 13 files changed, 152 insertions(+), 179 deletions(-) create mode 100644 store/src/main/java/com/nytimes/android/external/store3/base/BasePersister.java delete mode 100644 store/src/main/java/com/nytimes/android/external/store3/base/RoomPersister.java delete mode 100644 store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomCacheFactory.java rename store/src/main/java/com/nytimes/android/external/store3/base/impl/{ => room}/RoomInternalStore.java (67%) rename store/src/main/java/com/nytimes/android/external/store3/base/impl/{ => room}/RoomStore.java (84%) rename store/src/main/java/com/nytimes/android/external/store3/base/{ => room}/RoomDiskRead.java (68%) rename store/src/main/java/com/nytimes/android/external/store3/base/{ => room}/RoomDiskWrite.java (87%) create mode 100644 store/src/main/java/com/nytimes/android/external/store3/base/room/RoomPersister.java diff --git a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt index 861d99bf..587b3398 100644 --- a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt +++ b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt @@ -2,20 +2,16 @@ package com.nytimes.android.sample import android.arch.persistence.room.Dao import android.arch.persistence.room.Database -import android.arch.persistence.room.Delete import android.arch.persistence.room.Entity import android.arch.persistence.room.Insert import android.arch.persistence.room.PrimaryKey import android.arch.persistence.room.Query import android.arch.persistence.room.Room -import android.arch.persistence.room.Room.databaseBuilder import android.arch.persistence.room.RoomDatabase -import android.os.Parcel -import android.os.Parcelable import com.nytimes.android.external.store3.base.Fetcher -import com.nytimes.android.external.store3.base.RoomPersister -import com.nytimes.android.external.store3.base.impl.RoomInternalStore import com.nytimes.android.external.store3.base.impl.StalePolicy +import com.nytimes.android.external.store3.base.impl.room.RoomInternalStore +import com.nytimes.android.external.store3.base.room.RoomPersister import io.reactivex.Flowable import io.reactivex.Observable import io.reactivex.Single @@ -39,8 +35,8 @@ interface UserDao { @Insert fun insertAll(vararg users: User) - @Delete - fun delete(user: User) +// @Delete +// fun delete(user: User) } // File: AppDatabase.java @@ -49,12 +45,12 @@ abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao } -val db = Room.databaseBuilder(SampleApp.appContext, AppDatabase::class.java, "db").build() +val db = Room.databaseBuilder(SampleApp.appContext!!, AppDatabase::class.java, "db").build() +val persister = object : RoomPersister, String>() { -val persister = object : RoomPersister, String> { override fun read(key: String): Observable> { - return db.userDao().loadAll().toObservable().map { if (it.isEmpty()) throw Exception() else it } + return db.userDao().loadAll().toObservable() } override fun write(key: String, user: User) { @@ -62,8 +58,10 @@ val persister = object : RoomPersister, String> { } } -//store + + +//store class SampleRoomStore(fetcher: Fetcher, persister: RoomPersister, String>, stalePolicy: StalePolicy = StalePolicy.UNSPECIFIED) : diff --git a/store/build.gradle b/store/build.gradle index a846f366..dfa0e648 100644 --- a/store/build.gradle +++ b/store/build.gradle @@ -19,9 +19,10 @@ dependencies { testImplementation libraries.junit testCompileOnly libraries.jsr305 + apiElements "android.arch.persistence.room:runtime:$room_version" implementation "android.arch.persistence.room:runtime:$room_version" + compile 'android.arch.persistence.room:runtime:1.1.0'; annotationProcessor "android.arch.persistence.room:compiler:$room_version" -// kapt "android.arch.persistence.room:compiler:$room_version" // androidTestImplementation "android.arch.persistence.room:testing:$room_version" // optional - RxJava support for Room diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/BasePersister.java b/store/src/main/java/com/nytimes/android/external/store3/base/BasePersister.java new file mode 100644 index 00000000..89bfd866 --- /dev/null +++ b/store/src/main/java/com/nytimes/android/external/store3/base/BasePersister.java @@ -0,0 +1,8 @@ +package com.nytimes.android.external.store3.base; + + + + +public interface BasePersister { + +} diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/Persister.java b/store/src/main/java/com/nytimes/android/external/store3/base/Persister.java index d8274943..72f086c8 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/Persister.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/Persister.java @@ -11,7 +11,7 @@ * * @param data type before parsing */ -public interface Persister extends DiskRead, DiskWrite { +public interface Persister extends DiskRead, DiskWrite,BasePersister { /** * @param key to use to get data from persister diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/RoomPersister.java b/store/src/main/java/com/nytimes/android/external/store3/base/RoomPersister.java deleted file mode 100644 index 063a5870..00000000 --- a/store/src/main/java/com/nytimes/android/external/store3/base/RoomPersister.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.nytimes.android.external.store3.base; - -import javax.annotation.Nonnull; - -import io.reactivex.Maybe; -import io.reactivex.Observable; -import io.reactivex.Single; - -/** - * Interface for fetching data from persister - * when implementing also think about implementing PathResolver to ease in creating primary keys - * - * @param data type before parsing - */ -public interface RoomPersister< Raw, Parsed,Key> extends RoomDiskRead, RoomDiskWrite { - - /** - * @param key to use to get data from persister - * If data is not available implementer needs to - * either return Observable.empty or throw an exception - */ - @Override - @Nonnull - Observable read(@Nonnull final Key key); - - /** - * @param key to use to store data to persister - * @param raw raw string to be stored - */ - @Override - @Nonnull - void write(@Nonnull final Key key, @Nonnull final Raw raw); -} diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/CacheFactory.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/CacheFactory.java index 5cb32c19..849a6f16 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/CacheFactory.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/CacheFactory.java @@ -6,9 +6,10 @@ import java.util.concurrent.TimeUnit; import io.reactivex.Maybe; +import io.reactivex.Observable; import io.reactivex.Single; -final class CacheFactory { +public final class CacheFactory { private CacheFactory() { } @@ -58,4 +59,50 @@ static Cache> createInflighter(MemoryPolicy me .build(); } } + + public static Cache> createRoomCache(MemoryPolicy memoryPolicy) { + if (memoryPolicy == null) { + return CacheBuilder + .newBuilder() + .maximumSize(StoreDefaults.getCacheSize()) + .expireAfterWrite(StoreDefaults.getCacheTTL(), StoreDefaults.getCacheTTLTimeUnit()) + .build(); + } else { + if (memoryPolicy.getExpireAfterAccess() == memoryPolicy.DEFAULT_POLICY) { + return CacheBuilder + .newBuilder() + .maximumSize(memoryPolicy.getMaxSize()) + .expireAfterWrite(memoryPolicy.getExpireAfterWrite(), memoryPolicy.getExpireAfterTimeUnit()) + .build(); + } else { + return CacheBuilder + .newBuilder() + .maximumSize(memoryPolicy.getMaxSize()) + .expireAfterAccess(memoryPolicy.getExpireAfterAccess(), memoryPolicy.getExpireAfterTimeUnit()) + .build(); + } + } + } + + public static Cache> createRoomInflighter(MemoryPolicy memoryPolicy) { + long expireAfterToSeconds = memoryPolicy == null ? StoreDefaults.getCacheTTLTimeUnit() + .toSeconds(StoreDefaults.getCacheTTL()) + : memoryPolicy.getExpireAfterTimeUnit().toSeconds(memoryPolicy.getExpireAfterWrite()); + long maximumInFlightRequestsDuration = TimeUnit.MINUTES.toSeconds(1); + + if (expireAfterToSeconds > maximumInFlightRequestsDuration) { + return CacheBuilder + .newBuilder() + .expireAfterWrite(maximumInFlightRequestsDuration, TimeUnit.SECONDS) + .build(); + } else { + long expireAfter = memoryPolicy == null ? StoreDefaults.getCacheTTL() : + memoryPolicy.getExpireAfterWrite(); + TimeUnit expireAfterUnit = memoryPolicy == null ? StoreDefaults.getCacheTTLTimeUnit() : + memoryPolicy.getExpireAfterTimeUnit(); + return CacheBuilder.newBuilder() + .expireAfterWrite(expireAfter, expireAfterUnit) + .build(); + } + } } diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomCacheFactory.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomCacheFactory.java deleted file mode 100644 index a888d586..00000000 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomCacheFactory.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.nytimes.android.external.store3.base.impl; - -import com.nytimes.android.external.cache3.Cache; -import com.nytimes.android.external.cache3.CacheBuilder; - -import java.util.concurrent.TimeUnit; - -import io.reactivex.Maybe; -import io.reactivex.Observable; -import io.reactivex.Single; - -final class RoomCacheFactory { - private RoomCacheFactory() { - - } - - static Cache> createCache(MemoryPolicy memoryPolicy) { - if (memoryPolicy == null) { - return CacheBuilder - .newBuilder() - .maximumSize(StoreDefaults.getCacheSize()) - .expireAfterWrite(StoreDefaults.getCacheTTL(), StoreDefaults.getCacheTTLTimeUnit()) - .build(); - } else { - if (memoryPolicy.getExpireAfterAccess() == memoryPolicy.DEFAULT_POLICY) { - return CacheBuilder - .newBuilder() - .maximumSize(memoryPolicy.getMaxSize()) - .expireAfterWrite(memoryPolicy.getExpireAfterWrite(), memoryPolicy.getExpireAfterTimeUnit()) - .build(); - } else { - return CacheBuilder - .newBuilder() - .maximumSize(memoryPolicy.getMaxSize()) - .expireAfterAccess(memoryPolicy.getExpireAfterAccess(), memoryPolicy.getExpireAfterTimeUnit()) - .build(); - } - } - } - - static Cache> createInflighter(MemoryPolicy memoryPolicy) { - long expireAfterToSeconds = memoryPolicy == null ? StoreDefaults.getCacheTTLTimeUnit() - .toSeconds(StoreDefaults.getCacheTTL()) - : memoryPolicy.getExpireAfterTimeUnit().toSeconds(memoryPolicy.getExpireAfterWrite()); - long maximumInFlightRequestsDuration = TimeUnit.MINUTES.toSeconds(1); - - if (expireAfterToSeconds > maximumInFlightRequestsDuration) { - return CacheBuilder - .newBuilder() - .expireAfterWrite(maximumInFlightRequestsDuration, TimeUnit.SECONDS) - .build(); - } else { - long expireAfter = memoryPolicy == null ? StoreDefaults.getCacheTTL() : - memoryPolicy.getExpireAfterWrite(); - TimeUnit expireAfterUnit = memoryPolicy == null ? StoreDefaults.getCacheTTLTimeUnit() : - memoryPolicy.getExpireAfterTimeUnit(); - return CacheBuilder.newBuilder() - .expireAfterWrite(expireAfter, expireAfterUnit) - .build(); - } - } -} 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..639827d0 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 @@ -1,7 +1,7 @@ package com.nytimes.android.external.store3.base.impl; +import com.nytimes.android.external.store3.base.BasePersister; 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; @@ -13,24 +13,24 @@ import static com.nytimes.android.external.store3.base.RecordState.STALE; -final class StoreUtil { +public final class StoreUtil { private StoreUtil() { } @Nonnull - static ObservableTransformer + public static ObservableTransformer repeatWhenSubjectEmits(PublishSubject refreshSubject, @Nonnull final Key keyForRepeat) { Observable filter = refreshSubject.filter(key -> key.equals(keyForRepeat)); return RepeatWhenEmits.from(filter); } - static boolean shouldReturnNetworkBeforeStale( - Persister persister, StalePolicy stalePolicy, Key key) { + public static boolean shouldReturnNetworkBeforeStale( + BasePersister persister, StalePolicy stalePolicy, Key key) { return stalePolicy == StalePolicy.NETWORK_BEFORE_STALE && persisterIsStale(key, persister); } - static boolean persisterIsStale(@Nonnull Key key, Persister persister) { + public static boolean persisterIsStale(@Nonnull Key key, BasePersister persister) { if (persister instanceof RecordProvider) { RecordProvider provider = (RecordProvider) persister; RecordState recordState = provider.getRecordState(key); @@ -39,7 +39,7 @@ static boolean persisterIsStale(@Nonnull Key key, Persister return false; } - static void clearPersister(Persister persister, @Nonnull Key key) { + public static void clearPersister(BasePersister persister, @Nonnull Key key) { boolean isPersisterClearable = persister instanceof Clearable; if (isPersisterClearable) { diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomInternalStore.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomInternalStore.java similarity index 67% rename from store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomInternalStore.java rename to store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomInternalStore.java index 43a9e015..20fab7a6 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomInternalStore.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomInternalStore.java @@ -1,24 +1,22 @@ -package com.nytimes.android.external.store3.base.impl; +package com.nytimes.android.external.store3.base.impl.room; import com.nytimes.android.external.cache3.Cache; -import com.nytimes.android.external.store.util.Result; -import com.nytimes.android.external.store3.annotations.Experimental; import com.nytimes.android.external.store3.base.Fetcher; -import com.nytimes.android.external.store3.base.InternalStore; -import com.nytimes.android.external.store3.base.RoomPersister; -import com.nytimes.android.external.store3.util.KeyParser; +import com.nytimes.android.external.store3.base.impl.CacheFactory; +import com.nytimes.android.external.store3.base.impl.MemoryPolicy; +import com.nytimes.android.external.store3.base.impl.StalePolicy; +import com.nytimes.android.external.store3.base.impl.StoreUtil; +import com.nytimes.android.external.store3.base.room.RoomPersister; -import java.util.AbstractMap; +import java.util.Collection; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import io.reactivex.Maybe; import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.subjects.PublishSubject; +import io.reactivex.disposables.Disposable; /** * Store to be used for loading an object from different data sources @@ -26,21 +24,18 @@ * @param data type before parsing, usually a String, Reader or BufferedSource * @param data type after parsing *

- * Example usage: @link */ -public class RoomInternalStore implements RoomStore { - Cache> inFlightRequests; - Cache> memCache; - StalePolicy stalePolicy; - RoomPersister persister; +public class RoomInternalStore implements RoomStore { + private final Fetcher fetcher; + private final RoomPersister persister; + private final Cache> memCache; + private final StalePolicy stalePolicy; + private final Cache> inFlightRequests; - private final PublishSubject refreshSubject = PublishSubject.create(); - private Fetcher fetcher; - private PublishSubject> subject; public RoomInternalStore(Fetcher fetcher, - RoomPersister persister, - StalePolicy stalePolicy) { + RoomPersister persister, + StalePolicy stalePolicy) { this(fetcher, persister, null, stalePolicy); } @@ -48,15 +43,11 @@ public RoomInternalStore(Fetcher fetcher, RoomPersister persister, MemoryPolicy memoryPolicy, StalePolicy stalePolicy) { - this.fetcher = fetcher; this.persister = persister; this.stalePolicy = stalePolicy; - - this.memCache = RoomCacheFactory.createCache(memoryPolicy); - this.inFlightRequests = RoomCacheFactory.createInflighter(memoryPolicy); - - subject = PublishSubject.create(); + this.memCache = CacheFactory.createRoomCache(memoryPolicy); + this.inFlightRequests = CacheFactory.createRoomInflighter(memoryPolicy); } /** @@ -66,17 +57,14 @@ public RoomInternalStore(Fetcher fetcher, @Nonnull @Override public Observable get(@Nonnull final Key key) { - return lazyCache(key) - .switchIfEmpty(fetch(key)); + return lazyCache(key).switchIfEmpty(fetch(key)); } /** * @return data from memory */ private Observable lazyCache(@Nonnull final Key key) { - return Observable - .defer(() -> cache(key)) - .onErrorResumeNext(Observable.empty()); + return Observable.defer(() -> cache(key)).onErrorResumeNext(Observable.empty()); } Observable cache(@Nonnull final Key key) { @@ -102,32 +90,30 @@ public Observable memory(@Nonnull Key key) { */ @Nonnull public Observable disk(@Nonnull final Key key) { -// if (StoreUtil.shouldReturnNetworkBeforeStale(persister, stalePolicy, key)) { -// return Maybe.empty(); -// } + if (StoreUtil.shouldReturnNetworkBeforeStale(persister, stalePolicy, key)) { + return Observable.empty(); + } return readDisk(key); } Observable readDisk(@Nonnull final Key key) { - return persister().read(key) + return persister() + .read(key) + .doOnNext(this::guardAgainstEmptyCollection) .onErrorResumeNext( Observable.empty()) .doOnNext(parsed -> { updateMemory(key, parsed); -// if (stalePolicy == StalePolicy.REFRESH_ON_STALE -// && StoreUtil.persisterIsStale(key, persister)) { -// backfillCache(key); -// } + if (stalePolicy == StalePolicy.REFRESH_ON_STALE + && StoreUtil.persisterIsStale(key, persister)) { + backfillCache(key); + } }).cache(); } @SuppressWarnings("CheckReturnValue") void backfillCache(@Nonnull Key key) { - fetch(key).subscribe(parsed -> { - // do Nothing we are just backfilling cache - }, throwable -> { - // do nothing as we are just backfilling cache - }); + Disposable noop = fetch(key).subscribe(it -> { }, it -> { }); } @@ -175,15 +161,10 @@ Observable response(@Nonnull final Key key) { } return Observable.error(throwable); }) - .doOnNext(data -> notifySubscribers(data, key)) .doAfterTerminate(() -> inFlightRequests.invalidate(key)) .cache(); } - void notifySubscribers(Parsed data, Key key) { - subject.onNext(new AbstractMap.SimpleEntry<>(key, data)); - } - /** * Only update memory after persister has been successfully updated @@ -197,6 +178,7 @@ void updateMemory(@Nonnull final Key key, final Parsed data) { @Override + //TODO: create a clearable override to clear all since room knows what table associated with data public void clear() { for (Key cachedKey : memCache.asMap().keySet()) { clear(cachedKey); @@ -207,13 +189,9 @@ public void clear() { public void clear(@Nonnull Key key) { inFlightRequests.invalidate(key); memCache.invalidate(key); -// StoreUtil.clearPersister(persister(), key); - notifyRefresh(key); + StoreUtil.clearPersister(persister(), key); } - private void notifyRefresh(@Nonnull Key key) { - refreshSubject.onNext(key); - } /** * @return DiskDAO that stores and stores data @@ -228,5 +206,11 @@ RoomPersister persister() { Fetcher fetcher() { return fetcher; } + + private void guardAgainstEmptyCollection(Parsed v) { + if (v instanceof Collection && ((Collection) v).isEmpty()) { + throw new IllegalStateException("empty result set"); + } + } } diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomStore.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomStore.java similarity index 84% rename from store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomStore.java rename to store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomStore.java index a6cbe537..0f582479 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/RoomStore.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomStore.java @@ -1,12 +1,10 @@ -package com.nytimes.android.external.store3.base.impl; +package com.nytimes.android.external.store3.base.impl.room; -import com.nytimes.android.external.store.util.Result; -import com.nytimes.android.external.store3.annotations.Experimental; +import com.nytimes.android.external.store3.base.impl.StoreBuilder; import javax.annotation.Nonnull; import io.reactivex.Observable; -import io.reactivex.Single; /** * a {@link StoreBuilder StoreBuilder} diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskRead.java b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskRead.java similarity index 68% rename from store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskRead.java rename to store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskRead.java index 0ef1ad4a..ac3fda8c 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskRead.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskRead.java @@ -1,8 +1,7 @@ -package com.nytimes.android.external.store3.base; +package com.nytimes.android.external.store3.base.room; import javax.annotation.Nonnull; -import io.reactivex.Maybe; import io.reactivex.Observable; public interface RoomDiskRead { diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskWrite.java b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskWrite.java similarity index 87% rename from store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskWrite.java rename to store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskWrite.java index 57a032fd..53e0d95b 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/RoomDiskWrite.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskWrite.java @@ -1,4 +1,4 @@ -package com.nytimes.android.external.store3.base; +package com.nytimes.android.external.store3.base.room; import javax.annotation.Nonnull; diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomPersister.java b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomPersister.java new file mode 100644 index 00000000..af8eba0d --- /dev/null +++ b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomPersister.java @@ -0,0 +1,33 @@ +package com.nytimes.android.external.store3.base.room; + +import com.nytimes.android.external.store3.base.BasePersister; + +import javax.annotation.Nonnull; + +import io.reactivex.Observable; + +/** + * Interface for fetching data from persister + * when implementing also think about implementing PathResolver to ease in creating primary keys + * + * @param data type before parsing + */ +public abstract class RoomPersister implements RoomDiskRead, RoomDiskWrite, BasePersister { + + /** + * @param key to use to get data from persister + * If data is not available implementer needs to + * either return Observable.empty or throw an exception + */ + @Override + @Nonnull + public abstract Observable read(@Nonnull final Key key); + + /** + * @param key to use to store data to persister + * @param raw raw string to be stored + */ + @Override + @Nonnull + public abstract void write(@Nonnull final Key key, @Nonnull final Raw raw); +} From 395fd4274d9287fb2abe2b99259e8ff20a7a8cf6 Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Thu, 24 May 2018 22:45:26 -0400 Subject: [PATCH 04/11] fix compilation --- .../com/nytimes/android/sample/SampleApp.kt | 37 +++++++++---------- store/build.gradle | 17 --------- 2 files changed, 18 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/nytimes/android/sample/SampleApp.kt b/app/src/main/java/com/nytimes/android/sample/SampleApp.kt index 7f76047f..7c3dcc7c 100644 --- a/app/src/main/java/com/nytimes/android/sample/SampleApp.kt +++ b/app/src/main/java/com/nytimes/android/sample/SampleApp.kt @@ -1,9 +1,7 @@ package com.nytimes.android.sample -import android.annotation.SuppressLint import android.app.Application import android.content.Context - import com.google.gson.Gson import com.google.gson.GsonBuilder import com.nytimes.android.external.fs3.SourcePersisterFactory @@ -17,42 +15,43 @@ import com.nytimes.android.external.store3.middleware.GsonParserFactory import com.nytimes.android.sample.data.model.GsonAdaptersModel import com.nytimes.android.sample.data.model.RedditData import com.nytimes.android.sample.data.remote.Api - -import java.io.IOException -import java.util.concurrent.TimeUnit - import io.reactivex.Observable import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.functions.Consumer import io.reactivex.schedulers.Schedulers -import okhttp3.ResponseBody import okio.BufferedSource import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory +import java.io.IOException +import java.util.concurrent.TimeUnit class SampleApp : Application() { - val nonPersistedStore: Store? = null - val persistedStore: Store? = null - private var persister: Persister? = null + var nonPersistedStore: Store? = null + var persistedStore: Store? =null + private var persister: Persister? =null override fun onCreate() { super.onCreate() appContext = this + initPersister(); + nonPersistedStore = provideRedditStore(); + persistedStore=providePersistedRedditStore(); + RoomSample() + + + } + private fun RoomSample() { var foo = store.get("") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ strings1 -> val success = strings1 != null }) { throwable -> throwable.stackTrace } foo = Observable.timer(15, TimeUnit.SECONDS) - .subscribe { it -> again() } - - + .subscribe { again() } } private fun again() { @@ -94,7 +93,7 @@ class SampleApp : Application() { private fun providePersistedRedditStore(): Store { return StoreBuilder.parsedWithKey() .fetcher(Fetcher { this.fetcher(it) }) - .persister(persister!!) + .persister(newPersister()) .parser(GsonParserFactory.createSourceParser(provideGson(), RedditData::class.java)) .open() } @@ -104,7 +103,7 @@ class SampleApp : Application() { */ @Throws(IOException::class) private fun newPersister(): Persister { - return SourcePersisterFactory.create(applicationContext.cacheDir) + return SourcePersisterFactory.create(this.cacheDir) } /** @@ -112,7 +111,7 @@ class SampleApp : Application() { */ private fun fetcher(barCode: BarCode): Single { return provideRetrofit().fetchSubredditForPersister(barCode.key, "10") - .map( { it.source() }) + .map({ it.source() }) } private fun provideRetrofit(): Api { @@ -133,6 +132,6 @@ class SampleApp : Application() { companion object { - var appContext: Context? = null + var appContext: Context? = null } } diff --git a/store/build.gradle b/store/build.gradle index dfa0e648..e40f9636 100644 --- a/store/build.gradle +++ b/store/build.gradle @@ -3,11 +3,6 @@ apply plugin: 'java' group = GROUP version = VERSION_NAME - -//Room -def room_version = "1.1.0" // or, for latest rc, use "1.1.1-rc1" - - dependencies { implementation project(path: ':cache') implementation libraries.rxJava2 @@ -18,18 +13,6 @@ dependencies { testImplementation libraries.assertJ testImplementation libraries.junit testCompileOnly libraries.jsr305 - - apiElements "android.arch.persistence.room:runtime:$room_version" - implementation "android.arch.persistence.room:runtime:$room_version" - compile 'android.arch.persistence.room:runtime:1.1.0'; - annotationProcessor "android.arch.persistence.room:compiler:$room_version" - -// androidTestImplementation "android.arch.persistence.room:testing:$room_version" - // optional - RxJava support for Room - implementation "android.arch.persistence.room:rxjava2:$room_version" - - - } buildscript { From 7c669f94016f145aefd6ab04a335143b8facb9ee Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Fri, 25 May 2018 09:13:39 -0400 Subject: [PATCH 05/11] Update .travis.yml --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cb0990f0..2d0ff2cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ android: - tools - platform-tools - build-tools-27.0.3 - - android-25 + - android-27 - extra-android-m2repository licenses: - 'android-sdk-license-.+' @@ -14,6 +14,8 @@ script: - ./gradlew check --stacktrace after_success: - gradle/deploy_snapshot.sh +before_install: +- yes | sdkmanager "platforms;android-27" branches: except: - gh-pages From 1a77719a4e60308019e8e1e08700b5a07347ae4d Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Fri, 25 May 2018 22:32:37 -0400 Subject: [PATCH 06/11] migrate relevant tests to RoomStore, mark new Room API with @Experimental --- .../nytimes/android/sample/SampleRoomStore.kt | 2 +- .../external/store3/base/Persister.java | 8 +- .../base/impl/room/RoomInternalStore.java | 10 +- .../store3/base/impl/room/RoomStore.java | 2 + .../store3/base/room/RoomDiskRead.java | 3 + .../store3/base/room/RoomDiskWrite.java | 7 +- .../store3/base/room/RoomPersister.java | 5 +- .../store3/room/ClearRoomStoreTest.java | 113 ++++++++++++++++++ .../external/store3/room/RoomStoreTest.java | 102 ++++++++++++++++ 9 files changed, 240 insertions(+), 12 deletions(-) create mode 100644 store/src/test/java/com/nytimes/android/external/store3/room/ClearRoomStoreTest.java create mode 100644 store/src/test/java/com/nytimes/android/external/store3/room/RoomStoreTest.java diff --git a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt index 587b3398..2cf755e0 100644 --- a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt +++ b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt @@ -47,7 +47,7 @@ abstract class AppDatabase : RoomDatabase() { val db = Room.databaseBuilder(SampleApp.appContext!!, AppDatabase::class.java, "db").build() -val persister = object : RoomPersister, String>() { +val persister = object : RoomPersister, String> { override fun read(key: String): Observable> { return db.userDao().loadAll().toObservable() diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/Persister.java b/store/src/main/java/com/nytimes/android/external/store3/base/Persister.java index 72f086c8..e404b991 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/Persister.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/Persister.java @@ -11,12 +11,12 @@ * * @param data type before parsing */ -public interface Persister extends DiskRead, DiskWrite,BasePersister { +public interface Persister extends DiskRead, DiskWrite, BasePersister { /** * @param key to use to get data from persister - * If data is not available implementer needs to - * either return Observable.empty or throw an exception + * If data is not available implementer needs to + * either return Observable.empty or throw an exception */ @Override @Nonnull @@ -24,7 +24,7 @@ public interface Persister extends DiskRead, DiskWrite data type after parsing *

*/ +@Experimental public class RoomInternalStore implements RoomStore { private final Fetcher fetcher; private final RoomPersister persister; @@ -113,7 +114,9 @@ Observable readDisk(@Nonnull final Key key) { @SuppressWarnings("CheckReturnValue") void backfillCache(@Nonnull Key key) { - Disposable noop = fetch(key).subscribe(it -> { }, it -> { }); + fetch(key).subscribe(it -> { + }, it -> { + }); } @@ -178,7 +181,8 @@ void updateMemory(@Nonnull final Key key, final Parsed data) { @Override - //TODO: create a clearable override to clear all since room knows what table associated with data + //need to create a clearable override to clear all + // since room knows what table associated with data public void clear() { for (Key cachedKey : memCache.asMap().keySet()) { clear(cachedKey); diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomStore.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomStore.java index 0f582479..581b52e9 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomStore.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomStore.java @@ -1,5 +1,6 @@ package com.nytimes.android.external.store3.base.impl.room; +import com.nytimes.android.external.store3.annotations.Experimental; import com.nytimes.android.external.store3.base.impl.StoreBuilder; import javax.annotation.Nonnull; @@ -15,6 +16,7 @@ * force a call to {@link RoomStore#fetch(V) Store.fetch() } * (skipping cache) */ +@Experimental public interface RoomStore { /** diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskRead.java b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskRead.java index ac3fda8c..59d53909 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskRead.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskRead.java @@ -1,9 +1,12 @@ package com.nytimes.android.external.store3.base.room; +import com.nytimes.android.external.store3.annotations.Experimental; + import javax.annotation.Nonnull; import io.reactivex.Observable; +@Experimental public interface RoomDiskRead { @Nonnull Observable read(@Nonnull Key key); diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskWrite.java b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskWrite.java index 53e0d95b..aa8bb304 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskWrite.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskWrite.java @@ -1,9 +1,10 @@ package com.nytimes.android.external.store3.base.room; -import javax.annotation.Nonnull; +import com.nytimes.android.external.store3.annotations.Experimental; -import io.reactivex.Single; +import javax.annotation.Nonnull; +@Experimental public interface RoomDiskWrite { /** * @param key to use to get data from persister @@ -11,5 +12,5 @@ public interface RoomDiskWrite { * either return Observable.empty or throw an exception */ @Nonnull - void write(@Nonnull Key key, @Nonnull Raw raw); + void write(@Nonnull Key key, @Nonnull Raw raw); } diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomPersister.java b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomPersister.java index af8eba0d..7fb89f01 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomPersister.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomPersister.java @@ -1,5 +1,6 @@ package com.nytimes.android.external.store3.base.room; +import com.nytimes.android.external.store3.annotations.Experimental; import com.nytimes.android.external.store3.base.BasePersister; import javax.annotation.Nonnull; @@ -12,7 +13,9 @@ * * @param data type before parsing */ -public abstract class RoomPersister implements RoomDiskRead, RoomDiskWrite, BasePersister { +@Experimental +public interface RoomPersister extends + RoomDiskRead, RoomDiskWrite, BasePersister { /** * @param key to use to get data from persister diff --git a/store/src/test/java/com/nytimes/android/external/store3/room/ClearRoomStoreTest.java b/store/src/test/java/com/nytimes/android/external/store3/room/ClearRoomStoreTest.java new file mode 100644 index 00000000..1b9485b1 --- /dev/null +++ b/store/src/test/java/com/nytimes/android/external/store3/room/ClearRoomStoreTest.java @@ -0,0 +1,113 @@ +package com.nytimes.android.external.store3.room; + +import com.nytimes.android.external.store3.base.Clearable; +import com.nytimes.android.external.store3.base.impl.BarCode; +import com.nytimes.android.external.store3.base.impl.StalePolicy; +import com.nytimes.android.external.store3.base.impl.room.RoomInternalStore; +import com.nytimes.android.external.store3.base.impl.room.RoomStore; +import com.nytimes.android.external.store3.base.room.RoomPersister; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.concurrent.atomic.AtomicInteger; + +import javax.annotation.Nonnull; + +import io.reactivex.Observable; +import io.reactivex.Single; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ClearRoomStoreTest { + @Mock + RoomClearingPersister persister; + private AtomicInteger networkCalls; + private RoomStore store; + + @Before + public void setUp() { + networkCalls = new AtomicInteger(0); + store = new RoomInternalStore<>( + barCode -> Single.fromCallable(() -> networkCalls.incrementAndGet()), + persister, + StalePolicy.UNSPECIFIED); + } + + @Test + public void testClearSingleBarCode() { + // one request should produce one call + BarCode barcode = new BarCode("type", "key"); + + when(persister.read(barcode)) + .thenReturn(Observable.empty()) //read from disk on get + .thenReturn(Observable.just(1)) //read from disk after fetching from network + .thenReturn(Observable.empty()) //read from disk after clearing + .thenReturn(Observable.just(1)); //read from disk after making additional network call + + store.get(barcode).test().awaitTerminalEvent(); + assertThat(networkCalls.intValue()).isEqualTo(1); + + // after clearing the memory another call should be made + store.clear(barcode); + store.get(barcode).test().awaitTerminalEvent(); + verify(persister).clear(barcode); + assertThat(networkCalls.intValue()).isEqualTo(2); + } + + @Test + public void testClearAllBarCodes() { + BarCode barcode1 = new BarCode("type1", "key1"); + BarCode barcode2 = new BarCode("type2", "key2"); + + when(persister.read(barcode1)) + .thenReturn(Observable.empty()) //read from disk + .thenReturn(Observable.just(1)) //read from disk after fetching from network + .thenReturn(Observable.empty()) //read from disk after clearing disk cache + .thenReturn(Observable.just(1)); //read from disk after making additional network call + + when(persister.read(barcode2)) + .thenReturn(Observable.empty()) //read from disk + .thenReturn(Observable.just(1)) //read from disk after fetching from network + .thenReturn(Observable.empty()) //read from disk after clearing disk cache + .thenReturn(Observable.just(1)); //read from disk after making additional network call + + + // each request should produce one call + store.get(barcode1).test().awaitTerminalEvent(); + store.get(barcode2).test().awaitTerminalEvent(); + assertThat(networkCalls.intValue()).isEqualTo(2); + + store.clear(); + + // after everything is cleared each request should produce another 2 calls + store.get(barcode1).test().awaitTerminalEvent(); + store.get(barcode2).test().awaitTerminalEvent(); + assertThat(networkCalls.intValue()).isEqualTo(4); + } + + //everything will be mocked + static class RoomClearingPersister implements RoomPersister, Clearable { + @Override + public void clear(@Nonnull BarCode key) { + throw new RuntimeException(); + } + + @Nonnull + @Override + public Observable read(@Nonnull BarCode barCode) { + throw new RuntimeException(); + } + + @Override + public void write(@Nonnull BarCode barCode, @Nonnull Integer integer) { + //noop + } + } +} diff --git a/store/src/test/java/com/nytimes/android/external/store3/room/RoomStoreTest.java b/store/src/test/java/com/nytimes/android/external/store3/room/RoomStoreTest.java new file mode 100644 index 00000000..c2bdacd3 --- /dev/null +++ b/store/src/test/java/com/nytimes/android/external/store3/room/RoomStoreTest.java @@ -0,0 +1,102 @@ +package com.nytimes.android.external.store3.room; + +import com.nytimes.android.external.store3.base.Fetcher; +import com.nytimes.android.external.store3.base.impl.BarCode; +import com.nytimes.android.external.store3.base.impl.StalePolicy; +import com.nytimes.android.external.store3.base.impl.room.RoomInternalStore; +import com.nytimes.android.external.store3.base.impl.room.RoomStore; +import com.nytimes.android.external.store3.base.room.RoomPersister; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.Observable; +import io.reactivex.Single; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class RoomStoreTest { + + private static final String DISK = "disk"; + private static final String NETWORK = "fetch"; + final AtomicInteger counter = new AtomicInteger(0); + @Mock + Fetcher fetcher; + @Mock + RoomPersister persister; + private final BarCode barCode = new BarCode("key", "value"); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testSimple() { + + RoomStore simpleStore = new RoomInternalStore<>( + fetcher, + persister, + StalePolicy.UNSPECIFIED + ); + + + when(fetcher.fetch(barCode)) + .thenReturn(Single.just(NETWORK)); + + when(persister.read(barCode)) + .thenReturn(Observable.empty()) + .thenReturn(Observable.just(DISK)); + + + String value = simpleStore.get(barCode).blockingFirst(); + + assertThat(value).isEqualTo(DISK); + value = simpleStore.get(barCode).blockingFirst(); + assertThat(value).isEqualTo(DISK); + verify(fetcher, times(1)).fetch(barCode); + } + + + @Test + public void testDoubleTap() { + + + RoomStore simpleStore = new RoomInternalStore<>( + fetcher, + persister, + StalePolicy.UNSPECIFIED + ); + + Single networkSingle = + Single.create(emitter -> { + if (counter.incrementAndGet() == 1) { + emitter.onSuccess(NETWORK); + } else { + emitter.onError(new RuntimeException("Yo Dawg your inflight is broken")); + } + }); + + + when(fetcher.fetch(barCode)) + .thenReturn(networkSingle); + + when(persister.read(barCode)) + .thenReturn(Observable.empty()) + .thenReturn(Observable.just(DISK)); + + + String response = simpleStore.get(barCode) + .zipWith(simpleStore.get(barCode), (s, s2) -> "hello") + .blockingFirst(); + assertThat(response).isEqualTo("hello"); + verify(fetcher, times(1)).fetch(barCode); + } +} From 5804e19fd36c07a68258745285856dfa8ad354ba Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Fri, 25 May 2018 22:37:07 -0400 Subject: [PATCH 07/11] pr feedback --- app/src/main/java/com/nytimes/android/sample/SampleApp.kt | 4 ---- buildsystem/dependencies.gradle | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/com/nytimes/android/sample/SampleApp.kt b/app/src/main/java/com/nytimes/android/sample/SampleApp.kt index 7c3dcc7c..805f2393 100644 --- a/app/src/main/java/com/nytimes/android/sample/SampleApp.kt +++ b/app/src/main/java/com/nytimes/android/sample/SampleApp.kt @@ -35,13 +35,10 @@ class SampleApp : Application() { override fun onCreate() { super.onCreate() appContext = this - initPersister(); nonPersistedStore = provideRedditStore(); persistedStore=providePersistedRedditStore(); RoomSample() - - } private fun RoomSample() { @@ -131,7 +128,6 @@ class SampleApp : Application() { } companion object { - var appContext: Context? = null } } diff --git a/buildsystem/dependencies.gradle b/buildsystem/dependencies.gradle index 9f310258..f10c12f5 100644 --- a/buildsystem/dependencies.gradle +++ b/buildsystem/dependencies.gradle @@ -21,7 +21,7 @@ ext.versions = [ kotlin : '1.1.2-5', // UI libs. - supportLibs : '26.1.0', + supportLibs : '27.1.0', picasso : '2.5.2', butterKnife : '7.0.1', From b1a7b947ebfedb21b5c84a2eddbc92ea7cd3720a Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Fri, 25 May 2018 22:40:52 -0400 Subject: [PATCH 08/11] bump minor rev up --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 240524b8..9fc42603 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ allprojects { ext { // POM file GROUP = "com.nytimes.android" - VERSION_NAME = "3.0.2-SNAPSHOT" + VERSION_NAME = "3.1.0-SNAPSHOT" POM_PACKAGING = "pom" POM_DESCRIPTION = "Store3 is built with RxJava2" From 39eab81863bba88bb9ae2512c7f5525834f9a048 Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Fri, 25 May 2018 22:48:44 -0400 Subject: [PATCH 09/11] cleanup store --- .../nytimes/android/sample/SampleRoomStore.kt | 44 ++++++------------- .../base/impl/room/RoomInternalStore.java | 5 +++ 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt index 2cf755e0..f230756a 100644 --- a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt +++ b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt @@ -9,7 +9,6 @@ import android.arch.persistence.room.Query import android.arch.persistence.room.Room import android.arch.persistence.room.RoomDatabase import com.nytimes.android.external.store3.base.Fetcher -import com.nytimes.android.external.store3.base.impl.StalePolicy import com.nytimes.android.external.store3.base.impl.room.RoomInternalStore import com.nytimes.android.external.store3.base.room.RoomPersister import io.reactivex.Flowable @@ -17,29 +16,22 @@ import io.reactivex.Observable import io.reactivex.Single -// File: User.java @Entity data class User( - @PrimaryKey(autoGenerate = true) var uid: Int = 0, - val name: String, - val lastName: String) + @PrimaryKey(autoGenerate = true) + var uid: Int = 0, + val name: String) - -// File: UserDao.java @Dao interface UserDao { @Query("SELECT name FROM user") fun loadAll(): Flowable> - @Insert fun insertAll(vararg users: User) -// @Delete -// fun delete(user: User) } -// File: AppDatabase.java @Database(entities = arrayOf(User::class), version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao @@ -47,28 +39,18 @@ abstract class AppDatabase : RoomDatabase() { val db = Room.databaseBuilder(SampleApp.appContext!!, AppDatabase::class.java, "db").build() -val persister = object : RoomPersister, String> { - - override fun read(key: String): Observable> { - return db.userDao().loadAll().toObservable() - } - - override fun write(key: String, user: User) { - db.userDao().insertAll(user) - } - -} - - -//store -class SampleRoomStore(fetcher: Fetcher, - persister: RoomPersister, String>, - stalePolicy: StalePolicy = StalePolicy.UNSPECIFIED) : - RoomInternalStore, String>(fetcher, persister, stalePolicy) +val store = RoomInternalStore( + Fetcher { Single.just(User(name = "Mike")) }, + object : RoomPersister, String> { -val fetcher=Fetcher { Single.just(User(name = "Mike", lastName = "naki")) } + override fun read(key: String): Observable> { + return db.userDao().loadAll().toObservable() + } -val store = SampleRoomStore( fetcher, persister) + override fun write(key: String, user: User) { + db.userDao().insertAll(user) + } + }) diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomInternalStore.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomInternalStore.java index 552962cb..cc9c4f5e 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomInternalStore.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomInternalStore.java @@ -34,6 +34,11 @@ public class RoomInternalStore implements RoomStore> inFlightRequests; + public RoomInternalStore(Fetcher fetcher, + RoomPersister persister) { + this(fetcher, persister, null, StalePolicy.UNSPECIFIED); + } + public RoomInternalStore(Fetcher fetcher, RoomPersister persister, StalePolicy stalePolicy) { From c1b7ead6dfb23f8c1406200dddf9e051daf3d71c Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Sat, 26 May 2018 21:43:29 -0400 Subject: [PATCH 10/11] create factory, clean up example --- .../nytimes/android/sample/SampleRoomStore.kt | 26 +++---- ...mInternalStore.java => RealStoreRoom.java} | 20 +++--- .../store3/base/impl/room/RoomStore.java | 47 ------------- .../store3/base/impl/room/StoreRoom.java | 70 +++++++++++++++++++ .../store3/base/room/RoomDiskRead.java | 2 +- .../store3/base/room/RoomPersister.java | 4 +- ...StoreTest.java => ClearStoreRoomTest.java} | 9 ++- ...{RoomStoreTest.java => StoreRoomTest.java} | 12 ++-- 8 files changed, 104 insertions(+), 86 deletions(-) rename store/src/main/java/com/nytimes/android/external/store3/base/impl/room/{RoomInternalStore.java => RealStoreRoom.java} (91%) delete mode 100644 store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomStore.java create mode 100644 store/src/main/java/com/nytimes/android/external/store3/base/impl/room/StoreRoom.java rename store/src/test/java/com/nytimes/android/external/store3/room/{ClearRoomStoreTest.java => ClearStoreRoomTest.java} (93%) rename store/src/test/java/com/nytimes/android/external/store3/room/{RoomStoreTest.java => StoreRoomTest.java} (89%) diff --git a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt index f230756a..a5a7daf1 100644 --- a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt +++ b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt @@ -9,13 +9,12 @@ import android.arch.persistence.room.Query import android.arch.persistence.room.Room import android.arch.persistence.room.RoomDatabase import com.nytimes.android.external.store3.base.Fetcher -import com.nytimes.android.external.store3.base.impl.room.RoomInternalStore +import com.nytimes.android.external.store3.base.impl.room.StoreRoom import com.nytimes.android.external.store3.base.room.RoomPersister import io.reactivex.Flowable import io.reactivex.Observable import io.reactivex.Single - @Entity data class User( @PrimaryKey(autoGenerate = true) @@ -28,7 +27,7 @@ interface UserDao { fun loadAll(): Flowable> @Insert - fun insertAll(vararg users: User) + fun insertAll(user: User) } @@ -40,17 +39,18 @@ abstract class AppDatabase : RoomDatabase() { val db = Room.databaseBuilder(SampleApp.appContext!!, AppDatabase::class.java, "db").build() -val store = RoomInternalStore( - Fetcher { Single.just(User(name = "Mike")) }, - object : RoomPersister, String> { +val fetcher = Fetcher { Single.just(User(name = "Mike")) } +val persister = object : RoomPersister, String> { + + override fun read(key: String): Observable> { + return db.userDao().loadAll().toObservable() + } - override fun read(key: String): Observable> { - return db.userDao().loadAll().toObservable() - } + override fun write(key: String, user: User) { + db.userDao().insertAll(user) + } +} - override fun write(key: String, user: User) { - db.userDao().insertAll(user) - } - }) +val store = StoreRoom.from(fetcher, persister) diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomInternalStore.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RealStoreRoom.java similarity index 91% rename from store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomInternalStore.java rename to store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RealStoreRoom.java index cc9c4f5e..8c93cd2b 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomInternalStore.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RealStoreRoom.java @@ -26,7 +26,7 @@ *

*/ @Experimental -public class RoomInternalStore implements RoomStore { +class RealStoreRoom extends StoreRoom { private final Fetcher fetcher; private final RoomPersister persister; private final Cache> memCache; @@ -34,21 +34,21 @@ public class RoomInternalStore implements RoomStore> inFlightRequests; - public RoomInternalStore(Fetcher fetcher, - RoomPersister persister) { + RealStoreRoom(Fetcher fetcher, + RoomPersister persister) { this(fetcher, persister, null, StalePolicy.UNSPECIFIED); } - public RoomInternalStore(Fetcher fetcher, - RoomPersister persister, - StalePolicy stalePolicy) { + RealStoreRoom(Fetcher fetcher, + RoomPersister persister, + StalePolicy stalePolicy) { this(fetcher, persister, null, stalePolicy); } - RoomInternalStore(Fetcher fetcher, - RoomPersister persister, - MemoryPolicy memoryPolicy, - StalePolicy stalePolicy) { + RealStoreRoom(Fetcher fetcher, + RoomPersister persister, + MemoryPolicy memoryPolicy, + StalePolicy stalePolicy) { this.fetcher = fetcher; this.persister = persister; this.stalePolicy = stalePolicy; diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomStore.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomStore.java deleted file mode 100644 index 581b52e9..00000000 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/RoomStore.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.nytimes.android.external.store3.base.impl.room; - -import com.nytimes.android.external.store3.annotations.Experimental; -import com.nytimes.android.external.store3.base.impl.StoreBuilder; - -import javax.annotation.Nonnull; - -import io.reactivex.Observable; - -/** - * a {@link StoreBuilder StoreBuilder} - * will return an instance of a store - *

- * A {@link RoomStore Store} can - * {@link RoomStore#get(V) Store.get() } cached data or - * force a call to {@link RoomStore#fetch(V) Store.fetch() } - * (skipping cache) - */ -@Experimental -public interface RoomStore { - - /** - * Return an Observable of T for request Barcode - * Data will be returned from oldest non expired source - * Sources are Memory Cache, Disk Cache, Inflight, Network Response - */ - @Nonnull - Observable get(@Nonnull V key); - - /** - * Return an Observable of T for requested Barcode skipping Memory & Disk Cache - */ - @Nonnull - Observable fetch(@Nonnull V key); - - /** - * purges all entries from memory and disk cache - * Persister will only be cleared if they implements Clearable - */ - void clear(); - - /** - * Purge a particular entry from memory and disk cache. - * Persister will only be cleared if they implements Clearable - */ - void clear(@Nonnull V key); -} diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/StoreRoom.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/StoreRoom.java new file mode 100644 index 00000000..4c28017a --- /dev/null +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/room/StoreRoom.java @@ -0,0 +1,70 @@ +package com.nytimes.android.external.store3.base.impl.room; + +import com.nytimes.android.external.store3.annotations.Experimental; +import com.nytimes.android.external.store3.base.Fetcher; +import com.nytimes.android.external.store3.base.impl.MemoryPolicy; +import com.nytimes.android.external.store3.base.impl.StalePolicy; +import com.nytimes.android.external.store3.base.impl.StoreBuilder; +import com.nytimes.android.external.store3.base.room.RoomPersister; + +import javax.annotation.Nonnull; + +import io.reactivex.Observable; + +/** + * a {@link StoreBuilder StoreBuilder} + * will return an instance of a store + *

+ * A {@link StoreRoom Store} can + * {@link StoreRoom#get(V) Store.get() } cached data or + * force a call to {@link StoreRoom#fetch(V) Store.fetch() } + * (skipping cache) + */ +@Experimental +public abstract class StoreRoom { + + /** + * Return an Observable of T for request Barcode + * Data will be returned from oldest non expired source + * Sources are Memory Cache, Disk Cache, Inflight, Network Response + */ + @Nonnull + public abstract Observable get(@Nonnull V key); + + /** + * Return an Observable of T for requested Barcode skipping Memory & Disk Cache + */ + @Nonnull + public abstract Observable fetch(@Nonnull V key); + + /** + * purges all entries from memory and disk cache + * Persister will only be cleared if they implements Clearable + */ + public abstract void clear(); + + /** + * Purge a particular entry from memory and disk cache. + * Persister will only be cleared if they implements Clearable + */ + public abstract void clear(@Nonnull V key); + + + public static StoreRoom from + (Fetcher fetcher, RoomPersister persister) { + return new RealStoreRoom<>(fetcher, persister); + } + + public static StoreRoom from( + Fetcher fetcher, + RoomPersister persister, + StalePolicy policy) { + return new RealStoreRoom<>(fetcher, persister, policy); + } + + public static StoreRoom from + (Fetcher fetcher, RoomPersister persister, + StalePolicy stalePolicy, MemoryPolicy memoryPolicy) { + return new RealStoreRoom<>(fetcher, persister, memoryPolicy, stalePolicy); + } +} diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskRead.java b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskRead.java index 59d53909..92b5d570 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskRead.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomDiskRead.java @@ -6,7 +6,7 @@ import io.reactivex.Observable; -@Experimental + @Experimental public interface RoomDiskRead { @Nonnull Observable read(@Nonnull Key key); diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomPersister.java b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomPersister.java index 7fb89f01..fc6bfbdb 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomPersister.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/room/RoomPersister.java @@ -24,7 +24,7 @@ public interface RoomPersister extends */ @Override @Nonnull - public abstract Observable read(@Nonnull final Key key); + Observable read(@Nonnull final Key key); /** * @param key to use to store data to persister @@ -32,5 +32,5 @@ public interface RoomPersister extends */ @Override @Nonnull - public abstract void write(@Nonnull final Key key, @Nonnull final Raw raw); + void write(@Nonnull final Key key, @Nonnull final Raw raw); } diff --git a/store/src/test/java/com/nytimes/android/external/store3/room/ClearRoomStoreTest.java b/store/src/test/java/com/nytimes/android/external/store3/room/ClearStoreRoomTest.java similarity index 93% rename from store/src/test/java/com/nytimes/android/external/store3/room/ClearRoomStoreTest.java rename to store/src/test/java/com/nytimes/android/external/store3/room/ClearStoreRoomTest.java index 1b9485b1..e398845e 100644 --- a/store/src/test/java/com/nytimes/android/external/store3/room/ClearRoomStoreTest.java +++ b/store/src/test/java/com/nytimes/android/external/store3/room/ClearStoreRoomTest.java @@ -3,8 +3,7 @@ import com.nytimes.android.external.store3.base.Clearable; import com.nytimes.android.external.store3.base.impl.BarCode; import com.nytimes.android.external.store3.base.impl.StalePolicy; -import com.nytimes.android.external.store3.base.impl.room.RoomInternalStore; -import com.nytimes.android.external.store3.base.impl.room.RoomStore; +import com.nytimes.android.external.store3.base.impl.room.StoreRoom; import com.nytimes.android.external.store3.base.room.RoomPersister; import org.junit.Before; @@ -25,16 +24,16 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) -public class ClearRoomStoreTest { +public class ClearStoreRoomTest { @Mock RoomClearingPersister persister; private AtomicInteger networkCalls; - private RoomStore store; + private StoreRoom store; @Before public void setUp() { networkCalls = new AtomicInteger(0); - store = new RoomInternalStore<>( + store = StoreRoom.from( barCode -> Single.fromCallable(() -> networkCalls.incrementAndGet()), persister, StalePolicy.UNSPECIFIED); diff --git a/store/src/test/java/com/nytimes/android/external/store3/room/RoomStoreTest.java b/store/src/test/java/com/nytimes/android/external/store3/room/StoreRoomTest.java similarity index 89% rename from store/src/test/java/com/nytimes/android/external/store3/room/RoomStoreTest.java rename to store/src/test/java/com/nytimes/android/external/store3/room/StoreRoomTest.java index c2bdacd3..3f2e6e70 100644 --- a/store/src/test/java/com/nytimes/android/external/store3/room/RoomStoreTest.java +++ b/store/src/test/java/com/nytimes/android/external/store3/room/StoreRoomTest.java @@ -3,8 +3,7 @@ import com.nytimes.android.external.store3.base.Fetcher; import com.nytimes.android.external.store3.base.impl.BarCode; import com.nytimes.android.external.store3.base.impl.StalePolicy; -import com.nytimes.android.external.store3.base.impl.room.RoomInternalStore; -import com.nytimes.android.external.store3.base.impl.room.RoomStore; +import com.nytimes.android.external.store3.base.impl.room.StoreRoom; import com.nytimes.android.external.store3.base.room.RoomPersister; import org.junit.Before; @@ -22,7 +21,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class RoomStoreTest { +public class StoreRoomTest { private static final String DISK = "disk"; private static final String NETWORK = "fetch"; @@ -41,7 +40,7 @@ public void setUp() { @Test public void testSimple() { - RoomStore simpleStore = new RoomInternalStore<>( + StoreRoom simpleStore = StoreRoom.from( fetcher, persister, StalePolicy.UNSPECIFIED @@ -67,9 +66,7 @@ public void testSimple() { @Test public void testDoubleTap() { - - - RoomStore simpleStore = new RoomInternalStore<>( + StoreRoom simpleStore = StoreRoom.from( fetcher, persister, StalePolicy.UNSPECIFIED @@ -84,7 +81,6 @@ public void testDoubleTap() { } }); - when(fetcher.fetch(barCode)) .thenReturn(networkSingle); From 8ee3d09437fefe4643f5680efcabbc6d06bf8056 Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Wed, 30 May 2018 12:42:58 -0400 Subject: [PATCH 11/11] pr comments --- .../com/nytimes/android/sample/SampleApp.kt | 12 ++-- .../nytimes/android/sample/SampleRoomStore.kt | 22 ++++--- .../store3/base/impl/CacheFactory.java | 61 ++++++------------- 3 files changed, 37 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/com/nytimes/android/sample/SampleApp.kt b/app/src/main/java/com/nytimes/android/sample/SampleApp.kt index 805f2393..8005d984 100644 --- a/app/src/main/java/com/nytimes/android/sample/SampleApp.kt +++ b/app/src/main/java/com/nytimes/android/sample/SampleApp.kt @@ -5,7 +5,6 @@ import android.content.Context import com.google.gson.Gson import com.google.gson.GsonBuilder import com.nytimes.android.external.fs3.SourcePersisterFactory -import com.nytimes.android.external.store3.base.Fetcher import com.nytimes.android.external.store3.base.Persister import com.nytimes.android.external.store3.base.impl.BarCode import com.nytimes.android.external.store3.base.impl.MemoryPolicy @@ -31,6 +30,7 @@ class SampleApp : Application() { var nonPersistedStore: Store? = null var persistedStore: Store? =null private var persister: Persister? =null + private val sampleRoomStore=SampleRoomStore(this) override fun onCreate() { super.onCreate() @@ -42,17 +42,17 @@ class SampleApp : Application() { } private fun RoomSample() { - var foo = store.get("") + var foo = sampleRoomStore.store.get("") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ strings1 -> val success = strings1 != null }) { throwable -> throwable.stackTrace } foo = Observable.timer(15, TimeUnit.SECONDS) - .subscribe { again() } + .subscribe { makeFetchRequest() } } - private fun again() { - val bar = store.fetch("") + private fun makeFetchRequest() { + val bar = sampleRoomStore.store.fetch("") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ strings1 -> val success = strings1 != null }) { throwable -> throwable.stackTrace } @@ -89,7 +89,7 @@ class SampleApp : Application() { */ private fun providePersistedRedditStore(): Store { return StoreBuilder.parsedWithKey() - .fetcher(Fetcher { this.fetcher(it) }) + .fetcher({ this.fetcher(it) }) .persister(newPersister()) .parser(GsonParserFactory.createSourceParser(provideGson(), RedditData::class.java)) .open() diff --git a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt index a5a7daf1..f3bf00f9 100644 --- a/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt +++ b/app/src/main/java/com/nytimes/android/sample/SampleRoomStore.kt @@ -8,6 +8,7 @@ import android.arch.persistence.room.PrimaryKey import android.arch.persistence.room.Query import android.arch.persistence.room.Room import android.arch.persistence.room.RoomDatabase +import android.content.Context import com.nytimes.android.external.store3.base.Fetcher import com.nytimes.android.external.store3.base.impl.room.StoreRoom import com.nytimes.android.external.store3.base.room.RoomPersister @@ -36,21 +37,24 @@ abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao } -val db = Room.databaseBuilder(SampleApp.appContext!!, AppDatabase::class.java, "db").build() +class SampleRoomStore(context: Context){ + val db = Room.databaseBuilder(context, AppDatabase::class.java, "db").build() + val fetcher = Fetcher { Single.just(User(name = "Mike")) } + val persister = object : RoomPersister, String> { -val fetcher = Fetcher { Single.just(User(name = "Mike")) } -val persister = object : RoomPersister, String> { + override fun read(key: String): Observable> { + return db.userDao().loadAll().toObservable() + } - override fun read(key: String): Observable> { - return db.userDao().loadAll().toObservable() + override fun write(key: String, user: User) { + db.userDao().insertAll(user) + } } - override fun write(key: String, user: User) { - db.userDao().insertAll(user) - } + val store = StoreRoom.from(fetcher, persister) } -val store = StoreRoom.from(fetcher, persister) + diff --git a/store/src/main/java/com/nytimes/android/external/store3/base/impl/CacheFactory.java b/store/src/main/java/com/nytimes/android/external/store3/base/impl/CacheFactory.java index 849a6f16..0abbc980 100644 --- a/store/src/main/java/com/nytimes/android/external/store3/base/impl/CacheFactory.java +++ b/store/src/main/java/com/nytimes/android/external/store3/base/impl/CacheFactory.java @@ -15,30 +15,25 @@ private CacheFactory() { } static Cache> createCache(MemoryPolicy memoryPolicy) { - if (memoryPolicy == null) { - return CacheBuilder - .newBuilder() - .maximumSize(StoreDefaults.getCacheSize()) - .expireAfterWrite(StoreDefaults.getCacheTTL(), StoreDefaults.getCacheTTLTimeUnit()) - .build(); - } else { - if (memoryPolicy.getExpireAfterAccess() == memoryPolicy.DEFAULT_POLICY) { - return CacheBuilder - .newBuilder() - .maximumSize(memoryPolicy.getMaxSize()) - .expireAfterWrite(memoryPolicy.getExpireAfterWrite(), memoryPolicy.getExpireAfterTimeUnit()) - .build(); - } else { - return CacheBuilder - .newBuilder() - .maximumSize(memoryPolicy.getMaxSize()) - .expireAfterAccess(memoryPolicy.getExpireAfterAccess(), memoryPolicy.getExpireAfterTimeUnit()) - .build(); - } - } + return createBaseCache(memoryPolicy); } static Cache> createInflighter(MemoryPolicy memoryPolicy) { + return createBaseInFlighter(memoryPolicy); + } + + public static Cache> createRoomCache(MemoryPolicy memoryPolicy) { + return createBaseCache(memoryPolicy); + } + + + + public static Cache> createRoomInflighter(MemoryPolicy memoryPolicy) { + return createBaseInFlighter(memoryPolicy); + } + + + private static Cache createBaseInFlighter(MemoryPolicy memoryPolicy) { long expireAfterToSeconds = memoryPolicy == null ? StoreDefaults.getCacheTTLTimeUnit() .toSeconds(StoreDefaults.getCacheTTL()) : memoryPolicy.getExpireAfterTimeUnit().toSeconds(memoryPolicy.getExpireAfterWrite()); @@ -60,7 +55,8 @@ static Cache> createInflighter(MemoryPolicy me } } - public static Cache> createRoomCache(MemoryPolicy memoryPolicy) { + + private static Cache createBaseCache(MemoryPolicy memoryPolicy){ if (memoryPolicy == null) { return CacheBuilder .newBuilder() @@ -84,25 +80,4 @@ public static Cache> createRoomCache(Memor } } - public static Cache> createRoomInflighter(MemoryPolicy memoryPolicy) { - long expireAfterToSeconds = memoryPolicy == null ? StoreDefaults.getCacheTTLTimeUnit() - .toSeconds(StoreDefaults.getCacheTTL()) - : memoryPolicy.getExpireAfterTimeUnit().toSeconds(memoryPolicy.getExpireAfterWrite()); - long maximumInFlightRequestsDuration = TimeUnit.MINUTES.toSeconds(1); - - if (expireAfterToSeconds > maximumInFlightRequestsDuration) { - return CacheBuilder - .newBuilder() - .expireAfterWrite(maximumInFlightRequestsDuration, TimeUnit.SECONDS) - .build(); - } else { - long expireAfter = memoryPolicy == null ? StoreDefaults.getCacheTTL() : - memoryPolicy.getExpireAfterWrite(); - TimeUnit expireAfterUnit = memoryPolicy == null ? StoreDefaults.getCacheTTLTimeUnit() : - memoryPolicy.getExpireAfterTimeUnit(); - return CacheBuilder.newBuilder() - .expireAfterWrite(expireAfter, expireAfterUnit) - .build(); - } - } }