package mu.lab.common.rx.realm; import java.util.Collections; import java.util.HashSet; import java.util.Set; import io.realm.Realm; import io.realm.RealmConfiguration; import io.realm.RealmObject; import io.realm.RealmResults; import io.realm.exceptions.RealmException; import mu.lab.util.Log; import rx.Observable; import rx.functions.Func1; import rx.subjects.PublishSubject; /** * @author guangchen. */ public class RealmDatabase { private static final String LOG_TAG = RealmDatabase.class.toString(); private static final ThreadLocal<Realm> realmCache = new ThreadLocal<>(); private static final Set<String> INIT_VALUE = Collections.singleton("<init>"); private static RealmConfiguration configuration; private static final PublishSubject<Set<String>> triggers = PublishSubject.create(); public static void init(RealmConfiguration configuration) { RealmDatabase.configuration = configuration; } /** * Create a query which watches given tables and emit new query results every time watches get * updated. * Note: the realm used in the query are not closed. * But if you create query from the same thread, the rc of realm will not increase. * So it's wise that you never call {@link RealmDatabase#close()}. * @param query see {@link Query} * @param watches when watches table got insert/update/delete, the query will update * @param <T> a realm object type * @return {@link RealmResults} as observable */ @SafeVarargs public static <T extends RealmObject> RealmResultsObservable<T> createQuery(final Query<T> query, final Class<? extends RealmObject>... watches) { Observable<RealmResults<T>> resultsObservable = triggers .filter(new Func1<Set<String>, Boolean > () { @Override public Boolean call(Set<String> classes) { for (Class<? extends RealmObject> clazz : watches) { if (classes.contains(clazz.getCanonicalName())) { return true; } } return false; } }) .startWith(INIT_VALUE) .map(new Func1<Set<String>, RealmResults<T>>() { @Override public RealmResults<T> call(Set<String> classes) { Realm realm = realmCache.get(); if (realm == null) { realm = Realm.getInstance(configuration); realmCache.set(realm); } return query.call(realm); } }); /* sqlbrite add a custom back pressure operator by lift here aims at reducing pressures when it is rapidly triggered */ return new RealmResultsObservable<>(resultsObservable); } /** * Close realm for current thread. */ public static void close() throws IllegalStateException { Realm realm = realmCache.get(); if (realm == null) { throw new IllegalStateException("realm already closed"); } realm.close(); realmCache.set(null); } /** * Insert or update a {@link RealmObject}. * The object must have {@link io.realm.annotations.PrimaryKey}. * @param object object to insert or udpate * @return Observable for the convenience of control worker thread. */ public static Observable<Void> insertOrUpdate(final RealmObject object) { return Observable.just(object).map(new Func1<RealmObject, Void>() { @Override public Void call(final RealmObject t) { Realm realm = Realm.getInstance(configuration); executeTransaction(realm, new Realm.Transaction() { @Override public void execute(Realm realm) { realm.copyToRealmOrUpdate(t); } }); realm.close(); return null; } }).map(new Func1<Void, Void>() { @SuppressWarnings("unchecked") @Override public Void call(Void aVoid) { final Set<String> toNotify = Util.collectRelatedRealmClass(object.getClass()); sendTableTrigger(toNotify); return null; } }); } /** * Exec an arbitrary realm execution, typically useful for deletion. * @param exec see {@link Exec} * @param notifies notify queries that watched {@code notifies} to update * @return Observable for the convenience of control worker thread. */ @SafeVarargs public static Observable<Void> exec(final Exec exec, final Class<? extends RealmObject>... notifies) { return Observable.just(exec).map(new Func1<Exec, Void>() { @Override public Void call(final Exec tExec) { Realm realm = Realm.getInstance(configuration); executeTransaction(realm, new Realm.Transaction() { @Override public void execute(Realm realm) { tExec.run(realm); } }); realm.close(); final Set<String> toNotify = new HashSet<>(); for (Class clazz: notifies) { toNotify.add(clazz.getCanonicalName()); } sendTableTrigger(toNotify); return null; } }); } private static void sendTableTrigger(Set<String> tables) { synchronized (triggers) { triggers.onNext(tables); } } public static void deleteDatabase() { Realm.deleteRealm(configuration); } private static void executeTransaction(Realm realm, Realm.Transaction transaction) { if (transaction == null) return; realm.beginTransaction(); try { transaction.execute(realm); realm.commitTransaction(); } catch (RuntimeException e) { realm.cancelTransaction(); Log.e(LOG_TAG, e.getMessage(), e); throw new RealmException("Error during transaction.", e); } catch (Error e) { realm.cancelTransaction(); Log.e(LOG_TAG, e.getMessage(), e); throw e; } } }