package co.smartreceipts.android.persistence.database.controllers.impl; import android.support.annotation.NonNull; import com.google.common.base.Preconditions; import com.hadisatrio.optional.Optional; import java.util.Collections; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; import co.smartreceipts.android.analytics.Analytics; import co.smartreceipts.android.analytics.events.ErrorEvent; import co.smartreceipts.android.persistence.database.controllers.TableController; import co.smartreceipts.android.persistence.database.controllers.TableEventsListener; import co.smartreceipts.android.persistence.database.controllers.alterations.StubTableActionAlterations; import co.smartreceipts.android.persistence.database.controllers.alterations.TableActionAlterations; import co.smartreceipts.android.persistence.database.controllers.results.DeleteResult; import co.smartreceipts.android.persistence.database.controllers.results.GetResult; import co.smartreceipts.android.persistence.database.controllers.results.InsertResult; import co.smartreceipts.android.persistence.database.controllers.results.UpdateResult; import co.smartreceipts.android.persistence.database.operations.DatabaseOperationMetadata; import co.smartreceipts.android.persistence.database.tables.Table; import co.smartreceipts.android.utils.PreFixedThreadFactory; import co.smartreceipts.android.utils.log.Logger; import io.reactivex.Observable; import io.reactivex.Scheduler; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.Subject; /** * Provides a top-level implementation of the {@link TableController} contract * * @param <ModelType> the model object type that this will be used to create */ abstract class AbstractTableController<ModelType> implements TableController<ModelType> { protected final String TAG = getClass().getSimpleName(); private final Table<ModelType, ?> mTable; private final ConcurrentHashMap<TableEventsListener<ModelType>, BridgingTableEventsListener<ModelType>> mBridgingTableEventsListeners = new ConcurrentHashMap<>(); protected final CopyOnWriteArrayList<TableEventsListener<ModelType>> mTableEventsListeners = new CopyOnWriteArrayList<>(); protected final TableActionAlterations<ModelType> mTableActionAlterations; protected final Analytics mAnalytics; protected final Scheduler mSubscribeOnScheduler; protected final Scheduler mObserveOnScheduler; private final Subject<GetResult<ModelType>> getStreamSubject = PublishSubject.<GetResult<ModelType>>create().toSerialized(); private final Subject<InsertResult<ModelType>> insertStreamSubject = PublishSubject.<InsertResult<ModelType>>create().toSerialized(); private final Subject<UpdateResult<ModelType>> updateStreamSubject = PublishSubject.<UpdateResult<ModelType>>create().toSerialized(); private final Subject<DeleteResult<ModelType>> deleteStreamSubject = PublishSubject.<DeleteResult<ModelType>>create().toSerialized(); protected CompositeDisposable compositeDisposable = new CompositeDisposable(); public AbstractTableController(@NonNull Table<ModelType, ?> table, @NonNull Analytics analytics) { this(table, new StubTableActionAlterations<ModelType>(), analytics); } public AbstractTableController(@NonNull Table<ModelType, ?> table, @NonNull TableActionAlterations<ModelType> tableActionAlterations, @NonNull Analytics analytics) { mTable = Preconditions.checkNotNull(table); mTableActionAlterations = Preconditions.checkNotNull(tableActionAlterations); mAnalytics = Preconditions.checkNotNull(analytics); mSubscribeOnScheduler = Schedulers.from(Executors.newSingleThreadExecutor(new PreFixedThreadFactory(getClass().getSimpleName()))); mObserveOnScheduler = AndroidSchedulers.mainThread(); } AbstractTableController(@NonNull Table<ModelType, ?> table, @NonNull TableActionAlterations<ModelType> tableActionAlterations, @NonNull Analytics analytics, @NonNull Scheduler subscribeOnScheduler, @NonNull Scheduler observeOnScheduler) { mTable = Preconditions.checkNotNull(table); mTableActionAlterations = Preconditions.checkNotNull(tableActionAlterations); mAnalytics = Preconditions.checkNotNull(analytics); mSubscribeOnScheduler = Preconditions.checkNotNull(subscribeOnScheduler); mObserveOnScheduler = Preconditions.checkNotNull(observeOnScheduler); } @Override public synchronized void subscribe(@NonNull TableEventsListener<ModelType> tableEventsListener) { final BridgingTableEventsListener<ModelType> bridge = new BridgingTableEventsListener<ModelType>(this, tableEventsListener, mObserveOnScheduler); mBridgingTableEventsListeners.put(tableEventsListener, bridge); mTableEventsListeners.add(tableEventsListener); bridge.subscribe(); } @Override public synchronized void unsubscribe(@NonNull TableEventsListener<ModelType> tableEventsListener) { mTableEventsListeners.remove(tableEventsListener); final BridgingTableEventsListener<ModelType> bridge = mBridgingTableEventsListeners.remove(tableEventsListener); if (bridge != null) { bridge.unsubscribe(); } } @Override public void get() { Logger.info(this, "#get"); mTableActionAlterations.preGet() .subscribeOn(mSubscribeOnScheduler) .andThen(mTable.get()) .flatMap(mTableActionAlterations::postGet) .doOnSuccess(modelTypes -> { Logger.debug(AbstractTableController.this, "#onGetSuccess - onNext"); getStreamSubject.onNext(new GetResult<>(modelTypes)); }) .doOnError(throwable -> { Logger.error(AbstractTableController.this, "#onGetFailure - onError", throwable); mAnalytics.record(new ErrorEvent(AbstractTableController.this, throwable)); getStreamSubject.onNext(new GetResult<>(throwable)); }) .onErrorReturnItem(Collections.emptyList()) .subscribe(); } @NonNull @Override public Observable<GetResult<ModelType>> getStream() { return getStreamSubject; } @Override public void insert(@NonNull final ModelType modelType, @NonNull final DatabaseOperationMetadata databaseOperationMetadata) { Logger.info(this, "#insert: {}", modelType); mTableActionAlterations.preInsert(modelType) .subscribeOn(mSubscribeOnScheduler) .flatMap(insertedItem -> mTable.insert(insertedItem, databaseOperationMetadata)) .flatMap(mTableActionAlterations::postInsert) .doOnSuccess(insertedItem -> { Logger.debug(AbstractTableController.this, "#onInsertSuccess - onNext"); insertStreamSubject.onNext(new InsertResult<>(insertedItem, databaseOperationMetadata)); }) .doOnError(throwable -> { Logger.error(AbstractTableController.this, "#onInsertFailure - onError", throwable); mAnalytics.record(new ErrorEvent(AbstractTableController.this, throwable)); insertStreamSubject.onNext(new InsertResult<>(modelType, throwable, databaseOperationMetadata)); }) .map(Optional::of) .onErrorReturnItem(Optional.absent()) .subscribe(); } @NonNull @Override public Observable<InsertResult<ModelType>> insertStream() { return insertStreamSubject; } @NonNull @Override public Observable<Optional<ModelType>> update(@NonNull final ModelType oldModelType, @NonNull ModelType newModelType, @NonNull final DatabaseOperationMetadata databaseOperationMetadata) { Logger.info(this, "#update: {}; {}", oldModelType, newModelType); final Subject<Optional<ModelType>> updateSubject = PublishSubject.create(); mTableActionAlterations.preUpdate(oldModelType, newModelType) .flatMap(updatedItem -> mTable.update(oldModelType, updatedItem, databaseOperationMetadata)) .flatMap(modelType -> mTableActionAlterations.postUpdate(oldModelType, modelType)) .doOnSuccess(updatedItem -> { Logger.debug(AbstractTableController.this, "#onUpdateSuccess - onNext"); updateStreamSubject.onNext(new UpdateResult<>(oldModelType, updatedItem, databaseOperationMetadata)); }) .doOnError(throwable -> { Logger.error(AbstractTableController.this, "#onUpdateFailure - onError", throwable); mAnalytics.record(new ErrorEvent(AbstractTableController.this, throwable)); updateStreamSubject.onNext(new UpdateResult<>(oldModelType, null, throwable, databaseOperationMetadata)); }) .map(Optional::of) .onErrorReturnItem(Optional.absent()) .toObservable() .subscribe(updateSubject); return updateSubject; } @NonNull @Override public Observable<UpdateResult<ModelType>> updateStream() { return updateStreamSubject; } @Override public synchronized void delete(@NonNull final ModelType modelType, @NonNull final DatabaseOperationMetadata databaseOperationMetadata) { Logger.info(this, "#delete: {}", modelType); mTableActionAlterations.preDelete(modelType) .subscribeOn(mSubscribeOnScheduler) .flatMap(deletedItem -> mTable.delete(deletedItem, databaseOperationMetadata)) .flatMap(mTableActionAlterations::postDelete) .doOnSuccess(deletedItem -> { Logger.debug(AbstractTableController.this, "#onDeleteSuccess - onNext"); deleteStreamSubject.onNext(new DeleteResult<>(deletedItem, databaseOperationMetadata)); }) .doOnError(throwable -> { Logger.error(AbstractTableController.this, "#onDeleteFailure - onError", throwable); mAnalytics.record(new ErrorEvent(AbstractTableController.this, throwable)); deleteStreamSubject.onNext(new DeleteResult<>(modelType, throwable, databaseOperationMetadata)); }) .map(Optional::of) .onErrorReturnItem(Optional.absent()) .subscribe(); } @NonNull @Override public Observable<DeleteResult<ModelType>> deleteStream() { return deleteStreamSubject; } protected void unsubscribeReference(@NonNull AtomicReference<Disposable> disposableReference) { final Disposable disposable = disposableReference.get(); if (disposable != null && !disposable.isDisposed()) { disposable.dispose(); } } }