storable bug fix + static fixes

This commit is contained in:
Gavriil Sitnikov 2017-03-09 01:55:03 +03:00
parent 2927215c57
commit 631cf157dd
8 changed files with 361 additions and 102 deletions

View File

@ -2,7 +2,7 @@ apply plugin: 'com.android.library'
apply plugin: 'me.tatarka.retrolambda'
android {
compileSdkVersion 24
compileSdkVersion 25
buildToolsVersion '25.0.2'
compileOptions {
@ -12,11 +12,11 @@ android {
defaultConfig {
minSdkVersion 9
targetSdkVersion 24
targetSdkVersion 25
}
}
dependencies {
provided 'com.android.support:support-annotations:24.2.1'
provided 'com.android.support:support-annotations:25.2.0'
provided 'io.reactivex:rxandroid:1.2.1'
}

View File

@ -41,6 +41,8 @@ public class ConsoleLogProcessor extends LogProcessor {
}
@Override
@SuppressWarnings("WrongConstant")
//WrongConstant: level.getPriority() is not wrong constant!
public void processLogMessage(@NonNull final LcGroup group, @NonNull final LcLevel level,
@NonNull final String tag, @NonNull final String message, @Nullable final Throwable throwable) {
final String messageToLog = normalize(message + (throwable != null ? '\n' + Log.getStackTraceString(throwable) : ""));
@ -50,8 +52,9 @@ public class ConsoleLogProcessor extends LogProcessor {
newline = newline != -1 ? newline : length;
do {
final int end = Math.min(newline, i + MAX_LOG_LENGTH);
//noinspection WrongConstant
Log.println(level.getPriority(), tag, messageToLog.substring(i, end));
if (Log.isLoggable(tag, level.getPriority())) {
Log.println(level.getPriority(), tag, messageToLog.substring(i, end));
}
i = end;
}
while (i < newline);

View File

@ -264,7 +264,9 @@ public final class Lc {
public static void printStackTrace(@NonNull final String tag) {
final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
Log.d(tag, TextUtils.join("\n", Arrays.copyOfRange(stackTrace, STACK_TRACE_CODE_DEPTH, stackTrace.length)));
if (Log.isLoggable(tag, Log.DEBUG)) {
Log.d(tag, TextUtils.join("\n", Arrays.copyOfRange(stackTrace, STACK_TRACE_CODE_DEPTH, stackTrace.length)));
}
}
private Lc() {

View File

@ -0,0 +1,212 @@
/**
* Copyright 2014 Netflix, Inc.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package ru.touchin.roboswag.core.observables;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import rx.Observable.OnSubscribe;
import rx.Scheduler;
import rx.Subscriber;
import rx.Subscription;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.observables.ConnectableObservable;
import rx.schedulers.Schedulers;
import rx.subscriptions.CompositeSubscription;
import rx.subscriptions.Subscriptions;
/**
* Returns an observable sequence that stays connected to the source as long as
* there is at least one subscription to the observable sequence and also it stays connected
* for cache time after everyone unsubscribe.
*
* @param <T> the value type
*/
@SuppressWarnings({"PMD.AvoidUsingVolatile", "PMD.CompareObjectsWithEquals"})
//AvoidUsingVolatile,CompareObjectsWithEquals: from OnSubscribeRefCount code
public final class OnSubscribeRefCountWithCacheTime<T> implements OnSubscribe<T> {
@NonNull
private final ConnectableObservable<? extends T> source;
@NonNull
private volatile CompositeSubscription baseSubscription = new CompositeSubscription();
@NonNull
private final AtomicInteger subscriptionCount = new AtomicInteger(0);
@NonNull
private final Scheduler scheduler = Schedulers.computation();
private final long cacheTime;
@NonNull
private final TimeUnit cacheTimeUnit;
@Nullable
private Scheduler.Worker worker;
/**
* Use this lock for every subscription and disconnect action.
*/
@NonNull
private final ReentrantLock lock = new ReentrantLock();
public OnSubscribeRefCountWithCacheTime(@NonNull final ConnectableObservable<? extends T> source,
final long cacheTime, @NonNull final TimeUnit cacheTimeUnit) {
this.source = source;
this.cacheTime = cacheTime;
this.cacheTimeUnit = cacheTimeUnit;
}
@Override
public void call(@NonNull final Subscriber<? super T> subscriber) {
lock.lock();
if (subscriptionCount.incrementAndGet() == 1) {
if (worker != null) {
worker.unsubscribe();
worker = null;
}
final AtomicBoolean writeLocked = new AtomicBoolean(true);
try {
// need to use this overload of connect to ensure that
// baseSubscription is set in the case that source is a
// synchronous Observable
source.connect(onSubscribe(subscriber, writeLocked));
} finally {
// need to cover the case where the source is subscribed to
// outside of this class thus preventing the Action1 passed
// to source.connect above being called
if (writeLocked.get()) {
// Action1 passed to source.connect was not called
lock.unlock();
}
}
} else {
try {
// ready to subscribe to source so do it
doSubscribe(subscriber, baseSubscription);
} finally {
// release the read lock
lock.unlock();
}
}
}
@NonNull
private Action1<Subscription> onSubscribe(@NonNull final Subscriber<? super T> subscriber,
@NonNull final AtomicBoolean writeLocked) {
return new Action1<Subscription>() {
@Override
public void call(@NonNull final Subscription subscription) {
try {
baseSubscription.add(subscription);
// ready to subscribe to source so do it
doSubscribe(subscriber, baseSubscription);
} finally {
// release the write lock
lock.unlock();
writeLocked.set(false);
}
}
};
}
private void doSubscribe(@NonNull final Subscriber<? super T> subscriber, @NonNull final CompositeSubscription currentBase) {
// handle unsubscribing from the base subscription
subscriber.add(disconnect(currentBase));
source.unsafeSubscribe(new Subscriber<T>(subscriber) {
@Override
public void onError(@NonNull final Throwable throwable) {
cleanup();
subscriber.onError(throwable);
}
@Override
public void onNext(@Nullable final T item) {
subscriber.onNext(item);
}
@Override
public void onCompleted() {
cleanup();
subscriber.onCompleted();
}
private void cleanup() {
// on error or completion we need to unsubscribe the base subscription
// and set the subscriptionCount to 0
lock.lock();
try {
if (baseSubscription == currentBase) {
baseSubscription.unsubscribe();
baseSubscription = new CompositeSubscription();
subscriptionCount.set(0);
}
} finally {
lock.unlock();
}
}
});
}
@NonNull
private Subscription disconnect(@NonNull final CompositeSubscription current) {
return Subscriptions.create(new Action0() {
@Override
public void call() {
lock.lock();
try {
if (baseSubscription == current && subscriptionCount.decrementAndGet() == 0) {
if (worker != null) {
worker.unsubscribe();
} else {
worker = scheduler.createWorker();
}
worker.schedule(new Action0() {
@Override
public void call() {
lock.lock();
try {
if (subscriptionCount.get() == 0) {
baseSubscription.unsubscribe();
// need a new baseSubscription because once
// unsubscribed stays that way
worker.unsubscribe();
worker = null;
baseSubscription = new CompositeSubscription();
}
} finally {
lock.unlock();
}
}
}, cacheTime, cacheTimeUnit);
}
} finally {
lock.unlock();
}
}
});
}
}

View File

@ -1,41 +0,0 @@
package ru.touchin.roboswag.core.observables;
import android.support.annotation.NonNull;
import java.util.concurrent.CountDownLatch;
import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
import rx.Observable;
/**
* Created by Gavriil Sitnikov on 21/05/2016.
* Some helper methods to work with JavaRx.
*/
public final class RxUtils {
/**
* Subscribes to specific {@link Observable} and waits for it's onCompleted event
* and then returns {@link ObservableResult} with all collected items and errors during subscription.
* You should NOT use such method normally. It is safer than {@link Observable#toBlocking()} but it is also like a hack.
*
* @param observable {@link Observable} to be executed;
* @param <T> Type of {@link Observable}'s items;
* @return {@link ObservableResult} which contains all items and errors collected during execution.
*/
@NonNull
public static <T> ObservableResult<T> executeSync(@NonNull final Observable<T> observable) {
final ObservableResult<T> result = new ObservableResult<>();
final CountDownLatch waiter = new CountDownLatch(1);
observable.subscribe(result::onNext, result::onError, waiter::countDown);
try {
waiter.await();
} catch (final InterruptedException exception) {
throw new ShouldNotHappenException(exception);
}
return result;
}
private RxUtils() {
}
}

View File

@ -24,13 +24,12 @@ import android.support.annotation.Nullable;
import java.lang.reflect.Type;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import ru.touchin.roboswag.core.log.LcGroup;
import ru.touchin.roboswag.core.observables.ObservableResult;
import ru.touchin.roboswag.core.observables.RxUtils;
import ru.touchin.roboswag.core.observables.OnSubscribeRefCountWithCacheTime;
import ru.touchin.roboswag.core.observables.storable.builders.NonNullStorableBuilder;
import ru.touchin.roboswag.core.utils.ObjectUtils;
import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
import rx.Completable;
import rx.Observable;
import rx.Scheduler;
@ -55,6 +54,8 @@ public class Storable<TKey, TObject, TStoreObject> {
public static final LcGroup STORABLE_LC_GROUP = new LcGroup("STORABLE");
private static final long DEFAULT_CACHE_TIME_MILLIS = TimeUnit.SECONDS.toMillis(5);
@NonNull
private static ObserveStrategy getDefaultObserveStrategyFor(@NonNull final Type objectType, @NonNull final Type storeObjectType) {
if (objectType instanceof Class && ObjectUtils.isSimpleClass((Class) objectType)) {
@ -88,18 +89,21 @@ public class Storable<TKey, TObject, TStoreObject> {
public Storable(@NonNull final BuilderCore<TKey, TObject, TStoreObject> builderCore) {
this(builderCore.key, builderCore.objectType, builderCore.storeObjectType,
builderCore.store, builderCore.converter, builderCore.observeStrategy,
builderCore.migration, builderCore.defaultValue, builderCore.storeScheduler);
builderCore.migration, builderCore.defaultValue, builderCore.storeScheduler, builderCore.cacheTimeMillis);
}
public Storable(@NonNull final TKey key,
@NonNull final Type objectType,
@NonNull final Type storeObjectType,
@NonNull final Store<TKey, TStoreObject> store,
@NonNull final Converter<TObject, TStoreObject> converter,
@Nullable final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> migration,
@Nullable final TObject defaultValue,
@Nullable final Scheduler storeScheduler) {
@SuppressWarnings("PMD.ExcessiveParameterList")
//ExcessiveParameterList: that's why we are using builder to create it
private Storable(@NonNull final TKey key,
@NonNull final Type objectType,
@NonNull final Type storeObjectType,
@NonNull final Store<TKey, TStoreObject> store,
@NonNull final Converter<TObject, TStoreObject> converter,
@Nullable final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> migration,
@Nullable final TObject defaultValue,
@Nullable final Scheduler storeScheduler,
final long cacheTimeMillis) {
this.key = key;
this.objectType = objectType;
this.storeObjectType = storeObjectType;
@ -109,8 +113,8 @@ public class Storable<TKey, TObject, TStoreObject> {
= observeStrategy != null ? observeStrategy : getDefaultObserveStrategyFor(objectType, storeObjectType);
scheduler = storeScheduler != null ? storeScheduler : Schedulers.from(Executors.newSingleThreadExecutor());
storeValueObservable
= createStoreValueObservable(nonNullObserveStrategy, migration, defaultValue);
valueObservable = createValueObservable(storeValueObservable, nonNullObserveStrategy);
= createStoreValueObservable(nonNullObserveStrategy, migration, defaultValue, cacheTimeMillis);
valueObservable = createValueObservable(storeValueObservable, nonNullObserveStrategy, cacheTimeMillis);
}
@Nullable
@ -134,7 +138,8 @@ public class Storable<TKey, TObject, TStoreObject> {
@NonNull
private Observable<TStoreObject> createStoreValueObservable(@NonNull final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> migration,
@Nullable final TObject defaultValue) {
@Nullable final TObject defaultValue,
final long cacheTimeMillis) {
final Observable<TStoreObject> result = (migration != null
? migration.migrateToLatestVersion(key).subscribeOn(scheduler)
: Completable.complete())
@ -149,13 +154,14 @@ public class Storable<TKey, TObject, TStoreObject> {
.concatWith(newStoreValueEvent)
.map(storeObject -> returnDefaultValueIfNull(storeObject, defaultValue));
return observeStrategy == ObserveStrategy.CACHE_STORE_VALUE || observeStrategy == ObserveStrategy.CACHE_STORE_AND_ACTUAL_VALUE
? result.replay(1).refCount()
? Observable.create(new OnSubscribeRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS))
: result;
}
@NonNull
private Observable<TObject> createValueObservable(@NonNull final Observable<TStoreObject> storeValueObservable,
@NonNull final ObserveStrategy observeStrategy) {
@NonNull final ObserveStrategy observeStrategy,
final long cacheTimeMillis) {
final Observable<TObject> result = storeValueObservable
.switchMap(storeObject -> Observable
.fromCallable(() -> converter.toObject(objectType, storeObjectType, storeObject))
@ -169,7 +175,7 @@ public class Storable<TKey, TObject, TStoreObject> {
}
}));
return observeStrategy == ObserveStrategy.CACHE_ACTUAL_VALUE || observeStrategy == ObserveStrategy.CACHE_STORE_AND_ACTUAL_VALUE
? result.replay(1).refCount()
? Observable.create(new OnSubscribeRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS))
: result;
}
@ -223,28 +229,24 @@ public class Storable<TKey, TObject, TStoreObject> {
return converter;
}
/**
* Creates observable which is async setting value to store.
* NOTE: It could emit ONLY completed and errors events. It is not providing onNext event! //TODO: it's Completable :(
*
* @param newValue Value to set;
* @return Observable of setting process.
*/
@NonNull
public Observable<?> set(@Nullable final TObject newValue) {
return storeValueObservable
.first()
private Completable internalSet(@Nullable final TObject newValue, final boolean checkForEqualityBeforeSet) {
return (checkForEqualityBeforeSet ? storeValueObservable.first() : Observable.just(null))
.switchMap(oldStoreValue -> Observable
.fromCallable(() -> converter.toStoreObject(objectType, storeObjectType, newValue))
.subscribeOn(scheduler)
.switchMap(newStoreValue -> {
if (ObjectUtils.equals(newStoreValue, oldStoreValue)) {
if (checkForEqualityBeforeSet && ObjectUtils.equals(newStoreValue, oldStoreValue)) {
return Observable.empty();
}
return store.storeObject(storeObjectType, key, newStoreValue)
.doOnCompleted(() -> {
newStoreValueEvent.onNext(newStoreValue);
STORABLE_LC_GROUP.i("Value of '%s' changed from '%s' to '%s'", key, oldStoreValue, newStoreValue);
if (checkForEqualityBeforeSet) {
STORABLE_LC_GROUP.i("Value of '%s' changed from '%s' to '%s'", key, oldStoreValue, newStoreValue);
} else {
STORABLE_LC_GROUP.i("Value of '%s' force changed to '%s'", key, newStoreValue);
}
})
.toObservable();
}))
@ -255,23 +257,48 @@ public class Storable<TKey, TObject, TStoreObject> {
STORABLE_LC_GROUP.w(throwable, "Exception while trying to store value of '%s' from store %s by %s",
key, newValue, store, converter);
}
});
})
.toCompletable();
}
/**
* Creates observable which is async setting value to store.
* It is not checking if stored value equals new value.
* In result it will be faster to not get value from store and compare but it will emit item to {@link #observe()} subscribers.
* NOTE: It could emit ONLY completed and errors events. It is not providing onNext event! //TODO: it's Completable :(
*
* @param newValue Value to set;
* @return Observable of setting process.
*/
@NonNull
public Observable<?> forceSet(@Nullable final TObject newValue) {
return internalSet(newValue, false).toObservable();
}
/**
* Creates observable which is async setting value to store.
* It is checking if stored value equals new value.
* In result it will take time to get value from store and compare
* but it won't emit item to {@link #observe()} subscribers if stored value equals new value.
* NOTE: It could emit ONLY completed and errors events. It is not providing onNext event! //TODO: it's Completable :(
*
* @param newValue Value to set;
* @return Observable of setting process.
*/
@NonNull
public Observable<?> set(@Nullable final TObject newValue) {
return internalSet(newValue, true).toObservable();
}
/**
* Sets value synchronously. You should NOT use this method normally. Use {@link #set(Object)} asynchronously instead.
*
* @param newValue Value to set;
* @throws Converter.ConversionException Throws if {@link Converter} threw exception during conversion;
* @throws Migration.MigrationException Throws if {@link Migration} threw exception during migration.
*/
@Deprecated
//deprecation: it should be used for debug only and in very rare cases.
public void setSync(@Nullable final TObject newValue) throws Throwable {
final ObservableResult<?> setResult = RxUtils.executeSync(set(newValue));
if (setResult.getError() != null) {
throw setResult.getError();
}
public void setSync(@Nullable final TObject newValue) {
set(newValue).toBlocking().subscribe();
}
/**
@ -300,21 +327,12 @@ public class Storable<TKey, TObject, TStoreObject> {
* Gets value synchronously. You should NOT use this method normally. Use {@link #get()} or {@link #observe()} asynchronously instead.
*
* @return Returns value;
* @throws Converter.ConversionException Throws if {@link Converter} threw exception during conversion;
* @throws Migration.MigrationException Throws if {@link Migration} threw exception during migration.
*/
@Deprecated
//deprecation: it should be used for debug only and in very rare cases.
@Nullable
public TObject getSync() throws Throwable {
final ObservableResult<TObject> getResult = RxUtils.executeSync(get());
if (getResult.getError() != null) {
throw getResult.getError();
}
if (getResult.getItems().size() != 1) {
throw new ShouldNotHappenException();
}
return getResult.getItems().get(0);
public TObject getSync() {
return get().toBlocking().first();
}
/**
@ -376,23 +394,25 @@ public class Storable<TKey, TObject, TStoreObject> {
private TObject defaultValue;
@Nullable
private Scheduler storeScheduler;
private long cacheTimeMillis;
protected BuilderCore(@NonNull final TKey key,
@NonNull final Type objectType,
@NonNull final Type storeObjectType,
@NonNull final Store<TKey, TStoreObject> store,
@NonNull final Converter<TObject, TStoreObject> converter) {
this(key, objectType, storeObjectType, store, converter, null, null, null, null);
this(key, objectType, storeObjectType, store, converter, null, null, null, null, DEFAULT_CACHE_TIME_MILLIS);
}
protected BuilderCore(@NonNull final BuilderCore<TKey, TObject, TStoreObject> sourceBuilder) {
this(sourceBuilder.key, sourceBuilder.objectType, sourceBuilder.storeObjectType,
sourceBuilder.store, sourceBuilder.converter, sourceBuilder.observeStrategy,
sourceBuilder.migration, sourceBuilder.defaultValue, sourceBuilder.storeScheduler);
sourceBuilder.migration, sourceBuilder.defaultValue, sourceBuilder.storeScheduler, sourceBuilder.cacheTimeMillis);
}
@SuppressWarnings("CPD-START")
@SuppressWarnings({"PMD.ExcessiveParameterList", "CPD-START"})
//CPD: it is same code as constructor of Storable
//ExcessiveParameterList: that's why we are using builder to create it
private BuilderCore(@NonNull final TKey key,
@NonNull final Type objectType,
@NonNull final Type storeObjectType,
@ -401,7 +421,8 @@ public class Storable<TKey, TObject, TStoreObject> {
@Nullable final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> migration,
@Nullable final TObject defaultValue,
@Nullable final Scheduler storeScheduler) {
@Nullable final Scheduler storeScheduler,
final long cacheTimeMillis) {
this.key = key;
this.objectType = objectType;
this.storeObjectType = storeObjectType;
@ -411,6 +432,7 @@ public class Storable<TKey, TObject, TStoreObject> {
this.migration = migration;
this.defaultValue = defaultValue;
this.storeScheduler = storeScheduler;
this.cacheTimeMillis = cacheTimeMillis;
}
@SuppressWarnings("CPD-END")
@ -426,6 +448,10 @@ public class Storable<TKey, TObject, TStoreObject> {
this.migration = migration;
}
protected void setCacheTimeInternal(final long cacheTime, @NonNull final TimeUnit timeUnit) {
this.cacheTimeMillis = timeUnit.toMillis(cacheTime);
}
@Nullable
protected TObject getDefaultValue() {
return defaultValue;
@ -478,6 +504,20 @@ public class Storable<TKey, TObject, TStoreObject> {
return this;
}
/**
* Sets cache time for while value that cached by {@link #setObserveStrategy(ObserveStrategy)} will be in memory after everyone unsubscribe.
* It is important for example for cases when user switches between screens and hide/open app very fast.
*
* @param cacheTime Cache time value;
* @param timeUnit Cache time units.
* @return Builder that allows to specify other fields.
*/
@NonNull
public Builder<TKey, TObject, TStoreObject> setCacheTime(final long cacheTime, @NonNull final TimeUnit timeUnit) {
setCacheTimeInternal(cacheTime, timeUnit);
return this;
}
/**
* Sets specific {@link Migration} to migrate values from specific version to latest version.
*

View File

@ -20,11 +20,15 @@
package ru.touchin.roboswag.core.observables.storable.builders;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import ru.touchin.roboswag.core.observables.storable.Migration;
import ru.touchin.roboswag.core.observables.storable.Storable;
import ru.touchin.roboswag.core.observables.storable.concrete.NonNullStorable;
import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
import rx.Scheduler;
/**
* Created by Gavriil Sitnikov on 15/05/2016.
@ -42,6 +46,45 @@ public class NonNullStorableBuilder<TKey, TObject, TStoreObject> extends Storabl
setDefaultValueInternal(defaultValue);
}
/**
* Sets specific {@link Scheduler} to store/load/convert values on it.
*
* @param storeScheduler Scheduler;
* @return Builder that allows to specify other fields.
*/
@NonNull
public NonNullStorableBuilder<TKey, TObject, TStoreObject> setStoreScheduler(@Nullable final Scheduler storeScheduler) {
setStoreSchedulerInternal(storeScheduler);
return this;
}
/**
* Sets specific {@link Storable.ObserveStrategy} to cache value in memory in specific way.
*
* @param observeStrategy ObserveStrategy;
* @return Builder that allows to specify other fields.
*/
@NonNull
public NonNullStorableBuilder<TKey, TObject, TStoreObject> setObserveStrategy(@Nullable final Storable.ObserveStrategy observeStrategy) {
setObserveStrategyInternal(observeStrategy);
return this;
}
/**
* Sets cache time for while value that cached by {@link #setObserveStrategy(Storable.ObserveStrategy)}
* will be in memory after everyone unsubscribe.
* It is important for example for cases when user switches between screens and hide/open app very fast.
*
* @param cacheTime Cache time value;
* @param timeUnit Cache time units.
* @return Builder that allows to specify other fields.
*/
@NonNull
public NonNullStorableBuilder<TKey, TObject, TStoreObject> setCacheTime(final long cacheTime, @NonNull final TimeUnit timeUnit) {
setCacheTimeInternal(cacheTime, timeUnit);
return this;
}
/**
* Sets specific {@link Migration} to migrate values from specific version to latest version.
*

View File

@ -42,7 +42,7 @@ public class NonNullStorable<TKey, TObject, TStoreObject> extends Storable<TKey,
@NonNull
@Override
public TObject getSync() throws Throwable {
public TObject getSync() {
final TObject result = super.getSync();
if (result == null) {
throw new ShouldNotHappenException();