/* * The MIT License (MIT) * Copyright (c) 2017 pakoito & 2015 César Ferreira * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the Software * is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.pacoworks.rxpaper2; import android.content.Context; import android.util.Pair; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; import io.paperdb.Book; import io.paperdb.Paper; import io.reactivex.BackpressureStrategy; import io.reactivex.Completable; import io.reactivex.Flowable; import io.reactivex.Scheduler; import io.reactivex.Single; import io.reactivex.functions.Action; import io.reactivex.functions.Function; import io.reactivex.functions.Predicate; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.Subject; /** * Adapter class with a new interface to perform PaperDB operations. * * @author pakoito */ @SuppressWarnings("unused") public class RxPaperBook { private static final AtomicBoolean INITIALIZED = new AtomicBoolean(); final Book book; final Scheduler scheduler; final Subject<Pair<String, ?>> updates = PublishSubject.<Pair<String, ?>>create().toSerialized(); private RxPaperBook(Scheduler scheduler) { this.scheduler = scheduler; book = Paper.book(); } private RxPaperBook(String customBook, Scheduler scheduler) { this.scheduler = scheduler; book = Paper.book(customBook); } private RxPaperBook(Scheduler scheduler, String path) { this.scheduler = scheduler; book = Paper.bookOn(path); } private RxPaperBook(String path, String customBook, Scheduler scheduler) { this.scheduler = scheduler; book = Paper.bookOn(path, customBook); } /** * Initializes the underlying {@link Paper} database. * <p/> * This operation is required only once, but can be called multiple times safely. * * @param context application context */ public static void init(Context context) { if (INITIALIZED.compareAndSet(false, true)) { Paper.init(context.getApplicationContext()); } } private static void assertInitialized() { if (!INITIALIZED.get()) { throw new IllegalStateException( "RxPaper not initialized. Call RxPaper#init(Context) once"); } } /** * Open the main {@link Book} running its operations on {@link Schedulers#io()}. * <p/> * Requires calling {@link RxPaperBook#init(Context)} at least once beforehand. * * @return new RxPaperBook */ public static RxPaperBook with() { assertInitialized(); return new RxPaperBook(Schedulers.io()); } /** * Open a custom {@link Book} running its operations on {@link Schedulers#io()}. * <p/> * Requires calling {@link RxPaperBook#init(Context)} at least once beforehand. * * @param customBook book name * @return new RxPaperBook */ public static RxPaperBook with(String customBook) { assertInitialized(); return new RxPaperBook(customBook, Schedulers.io()); } /** * Open the main {@link Book} running its operations on a provided scheduler. * <p/> * Requires calling {@link RxPaperBook#init(Context)} at least once beforehand. * * @param scheduler scheduler where operations will be run * @return new RxPaperBook */ public static RxPaperBook with(Scheduler scheduler) { assertInitialized(); return new RxPaperBook(scheduler); } /** * Open a custom {@link Book} running its operations on a provided scheduler. * <p/> * Requires calling {@link RxPaperBook#init(Context)} at least once beforehand. * * @param customBook book name * @param scheduler scheduler where operations will be run * @return new RxPaperBook */ public static RxPaperBook with(String customBook, Scheduler scheduler) { assertInitialized(); return new RxPaperBook(customBook, scheduler); } /** * Open a custom {@link Book} with custom storage location path running its operations on * {@link Schedulers#io()}. * <p/> * Requires calling {@link RxPaperBook#init(Context)} at least once beforehand. * * @param path storage location name * @return new RxPaperBook */ public static RxPaperBook withPath(String path) { assertInitialized(); return new RxPaperBook(Schedulers.io(), path); } /** * Open a custom {@link Book} with custom storage location path running its operations on a * provided scheduler. * <p/> * Requires calling {@link RxPaperBook#init(Context)} at least once beforehand. * * @param path storage location * @param scheduler scheduler where operations will be run * @return new RxPaperBook */ public static RxPaperBook withPath(String path, Scheduler scheduler) { assertInitialized(); return new RxPaperBook(scheduler, path); } /** * Open a custom {@link Book} with custom storage location path running its operations on * {@link Schedulers#io()}. * <p/> * Requires calling {@link RxPaperBook#init(Context)} at least once beforehand. * * @param path storage location * @param customBook book name * @return new RxPaperBook */ public static RxPaperBook withPath(String path, String customBook) { assertInitialized(); return new RxPaperBook(path, customBook, Schedulers.io()); } /** * Open a custom {@link Book} with custom storage location path running its operations on a * provided scheduler. * <p/> * Requires calling {@link RxPaperBook#init(Context)} at least once beforehand. * * @param path storage location * @param scheduler scheduler where operations will be run * @param customBook book name * @return new RxPaperBook */ public static RxPaperBook withPath(String path, String customBook, Scheduler scheduler) { assertInitialized(); return new RxPaperBook(path, customBook, scheduler); } /** * Saves most types of POJOs or collections in {@link Book} storage. * <p/> * To deserialize correctly it is recommended to have an all-args constructor, but other types * may be available. * * @param key object key is used as part of object's file name * @param value object to save, must have no-arg constructor, can't be null. * @return this Book instance */ public <T> Completable write(final String key, final T value) { return Completable.fromAction(new Action() { @Override public void run() { book.write(key, value); } }) // FIXME in RxJava1 the error would be propagated to updates. // In RxJava2 the error happens on the Completable this method returns. // This andThen block reproduces the behavior in RxJava1. .andThen(Completable.fromAction(new Action() { @Override public void run() { try { updates.onNext(Pair.create(key, value)); } catch (Throwable t) { updates.onError(t); } } })).subscribeOn(scheduler); } /** * Instantiates saved object using original object class (e.g. LinkedList). Support limited * backward and forward compatibility: removed fields are ignored, new fields have their default * values. * * @param key object key to read * @param defaultValue value to be returned if key doesn't exist * @return the saved object instance or defaultValue */ public <T> Single<T> read(final String key, final T defaultValue) { return Single.fromCallable(new Callable<T>() { @Override public T call() { return book.read(key, defaultValue); } }).subscribeOn(scheduler); } /** * Instantiates saved object using original object class (e.g. LinkedList). Support limited * backward and forward compatibility: removed fields are ignored, new fields have their default * values. * * @param key object key to read * @return the saved object instance */ public <T> Single<T> read(final String key) { return Single.fromCallable(new Callable<T>() { @Override public T call() { final T read = book.read(key); if (null == read) { throw new IllegalArgumentException("Key " + key + " not found"); } return read; } }).subscribeOn(scheduler); } /** * Delete saved object for given key if it is exist. */ public Completable delete(final String key) { return Completable.fromAction(new Action() { @Override public void run() { book.delete(key); } }).subscribeOn(scheduler); } /** * Check if an object with the given key is saved in Book storage. * * @param key object key * @return true if object with given key exists in Book storage, false otherwise * @deprecated As of PaperDB release 2.6, replaced by {@link #contains(String)}} */ public Single<Boolean> exists(final String key) { return Single.fromCallable(new Callable<Boolean>() { @Override public Boolean call() { //noinspection deprecation return book.exist(key); } }).subscribeOn(scheduler); } /** * Returns all keys for objects in {@link Book}. * * @return all keys */ public Single<List<String>> keys() { return Single.fromCallable(new Callable<List<String>>() { @Override public List<String> call() { return book.getAllKeys(); } }).subscribeOn(scheduler); } /** * Destroys all data saved in {@link Book}. */ public Completable destroy() { return Completable.fromAction(new Action() { @Override public void run() { book.destroy(); } }).subscribeOn(scheduler); } /** * Naive update subscription for saved objects. Subscription is filtered by key and type. * * @param key object key * @param backPressureStrategy how the backpressure is handled downstream * @return hot observable */ public <T> Flowable<T> observe(final String key, final Class<T> clazz, BackpressureStrategy backPressureStrategy) { return updates.toFlowable(backPressureStrategy) .filter(new Predicate<Pair<String, ?>>() { @Override public boolean test(Pair<String, ?> stringPair) { return stringPair.first.equals(key); } }).map(new Function<Pair<String, ?>, Object>() { @Override public Object apply(Pair<String, ?> stringPair) { return stringPair.second; } }).ofType(clazz); } /** * Naive update subscription for saved objects. * <p/> * This method will return all objects for a key casted unsafely, and throw * {@link ClassCastException} if types do not match. For a safely checked and filtered version * use {@link this#observe(String, Class, BackpressureStrategy)}. * * @param key object key * @param backPressureStrategy how the backpressure is handled downstream * @return hot observable */ @SuppressWarnings("unchecked") public <T> Flowable<T> observeUnsafe(final String key, BackpressureStrategy backPressureStrategy) { return updates.toFlowable(backPressureStrategy) .filter(new Predicate<Pair<String, ?>>() { @Override public boolean test(Pair<String, ?> stringPair) { return stringPair.first.equals(key); } }).map(new Function<Pair<String, ?>, T>() { @Override public T apply(Pair<String, ?> stringPair) { return (T) stringPair.second; } }); } /** * Naive update subscription for saved objects. Subscription is filtered by type. * * @param backPressureStrategy how the backpressure is handled downstream * @return hot observable */ public <T> Flowable<T> observeAll(final Class<T> clazz, BackpressureStrategy backPressureStrategy) { return updates.toFlowable(backPressureStrategy) .map(new Function<Pair<String, ?>, Object>() { @Override public Object apply(Pair<String, ?> stringPair) { return stringPair.second; } }).ofType(clazz); } /** * Naive update subscription for saved objects. * <p/> * This method will return all objects casted unsafely, and throw * {@link ClassCastException} if types do not match. For a safely checked and filtered version * use {@link this#observeAll(Class, BackpressureStrategy)}. * * @param backPressureStrategy how the backpressure is handled downstream * @return hot observable */ @SuppressWarnings("unchecked") public <T> Flowable<T> observeAllUnsafe(BackpressureStrategy backPressureStrategy) { return updates.toFlowable(backPressureStrategy) .map(new Function<Pair<String, ?>, T>() { @Override public T apply(Pair<String, ?> stringPair) { return (T) stringPair.second; } }); } /** * Checks whether the current book contains the key given * * @param key the key to look up * @return true is the book contains a value for the given key */ public Single<Boolean> contains(final String key) { return Single.fromCallable(new Callable<Boolean>() { @Override public Boolean call() { return book.contains(key); } }).subscribeOn(scheduler); } /** * Returns the path of the current book * * @return the path to the book */ public Single<String> getPath() { return Single.fromCallable(new Callable<String>() { @Override public String call() { return book.getPath(); } }).subscribeOn(scheduler); } /** * Returns the path of the data stored at the key passed as a parameter. * The returned path does not exist if the method has been called prior * saving data for the given key. * * @param key the key to look up * @return the path to the value stored at the key */ public Single<String> getPath(final String key) { return Single.fromCallable(new Callable<String>() { @Override public String call() { return book.getPath(key); } }).subscribeOn(scheduler); } }