package com.novoda.todoapp.tasks.service;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.jakewharton.rxrelay.BehaviorRelay;
import com.novoda.data.SyncState;
import com.novoda.data.SyncedData;
import com.novoda.data.SyncedDataCreator;
import com.novoda.event.Event;
import com.novoda.todoapp.rx.IfThenFlatMap;
import com.novoda.todoapp.task.data.model.Id;
import com.novoda.todoapp.task.data.model.Task;
import com.novoda.todoapp.tasks.data.TasksDataFreshnessChecker;
import com.novoda.todoapp.tasks.data.model.Tasks;
import com.novoda.todoapp.tasks.data.source.LocalTasksDataSource;
import com.novoda.todoapp.tasks.data.source.RemoteTasksDataSource;
import java.util.List;
import rx.Observable;
import rx.functions.Action0;
import rx.functions.Func0;
import rx.functions.Func1;
import static com.novoda.data.SyncFunctions.asSyncedAction;
import static com.novoda.event.EventFunctions.*;
import static com.novoda.todoapp.rx.RxFunctions.ifThenMap;
public class PersistedTasksService implements TasksService {
private final LocalTasksDataSource localDataSource;
private final RemoteTasksDataSource remoteDataSource;
private final TasksDataFreshnessChecker tasksDataFreshnessChecker;
private final Clock clock;
private final BehaviorRelay<Event<Tasks>> taskRelay = BehaviorRelay.create(Event.idle(noEmptyTasks()));
private static Predicate<Tasks> noEmptyTasks() {
return new NoEmptyTasksPredicate();
}
public PersistedTasksService(
LocalTasksDataSource localDataSource,
RemoteTasksDataSource remoteDataSource,
TasksDataFreshnessChecker tasksDataFreshnessChecker,
Clock clock) {
this.localDataSource = localDataSource;
this.remoteDataSource = remoteDataSource;
this.tasksDataFreshnessChecker = tasksDataFreshnessChecker;
this.clock = clock;
}
@Override
public Observable<Event<Tasks>> getTasksEvents() {
return taskRelay.asObservable()
.startWith(initialiseSubject())
.distinctUntilChanged();
}
@Override
public Observable<Tasks> getTasks() {
return getTasksEvents().compose(asData(Tasks.class));
}
@Override
public Observable<Event<Tasks>> getCompletedTasksEvents() {
return getTasksEvents()
.map(filterTasks(onlyCompleted()));
}
private static Func1<Event<Tasks>, Event<Tasks>> filterTasks(final Function<Tasks, Tasks> filter) {
return new Func1<Event<Tasks>, Event<Tasks>>() {
@Override
public Event<Tasks> call(Event<Tasks> event) {
return event.updateData(event.data().transform(filter));
}
};
}
private static Function<Tasks, Tasks> onlyCompleted() {
return new Function<Tasks, Tasks>() {
@Override
public Tasks apply(Tasks input) {
return input.onlyCompleted();
}
};
}
@Override
public Observable<Tasks> getCompletedTasks() {
return getCompletedTasksEvents().compose(asData(Tasks.class));
}
@Override
public Observable<Event<Tasks>> getActiveTasksEvents() {
return getTasksEvents()
.map(filterTasks(onlyActives()));
}
private static Function<Tasks, Tasks> onlyActives() {
return new Function<Tasks, Tasks>() {
@Override
public Tasks apply(Tasks input) {
return input.onlyActives();
}
};
}
@Override
public Observable<Tasks> getActiveTasks() {
return getActiveTasksEvents().compose(asData(Tasks.class));
}
private Observable<Event<Tasks>> initialiseSubject() {
return Observable.defer(new Func0<Observable<Event<Tasks>>>() {
@Override
public Observable<Event<Tasks>> call() {
if (isInitialised(taskRelay)) {
return Observable.empty();
}
return localDataSource.getTasks()
.flatMap(fetchFromRemoteIfOutOfDate())
.switchIfEmpty(fetchFromRemote())
.compose(asEvent(taskRelay.getValue()))
.doOnNext(taskRelay); //TODO fix issue with unsubscribe before completion. Either revert or persist request.
}
});
}
private Func1<Tasks, Observable<Tasks>> fetchFromRemoteIfOutOfDate() {
return new Func1<Tasks, Observable<Tasks>>() {
@Override
public Observable<Tasks> call(Tasks tasks) {
if (tasksDataFreshnessChecker.isFresh(tasks)) {
return Observable.just(tasks);
}
return fetchFromRemote().startWith(tasks);
}
};
}
private Observable<Tasks> fetchFromRemote() {
return remoteDataSource.getTasks()
.map(asSyncedTasksNow())
.flatMap(persistTasks());
}
private Func1<List<Task>, Tasks> asSyncedTasksNow() {
return new Func1<List<Task>, Tasks>() {
@Override
public Tasks call(List<Task> tasks) {
return Tasks.asSynced(tasks, clock.timeInMillis());
}
};
}
private Func1<Tasks, Observable<Tasks>> persistTasks() {
return new Func1<Tasks, Observable<Tasks>>() {
@Override
public Observable<Tasks> call(Tasks tasks) {
return localDataSource.saveTasks(tasks);
}
};
}
@Override
public Observable<SyncedData<Task>> getTask(final Id taskId) {
return getTasks().flatMap(new Func1<Tasks, Observable<SyncedData<Task>>>() {
@Override
public Observable<SyncedData<Task>> call(Tasks tasks) {
if (tasks.containsTask(taskId)) {
return Observable.just(tasks.syncedDataFor(taskId));
}
return Observable.empty(); //TODO handle remote fetching as fallback.
}
});
}
private Func1<Task, SyncedData<Task>> asSyncedNow() {
return new Func1<Task, SyncedData<Task>>() {
@Override
public SyncedData<Task> call(Task task) {
return SyncedData.from(task, SyncState.IN_SYNC, clock.timeInMillis());
}
};
}
@Override
public Action0 refreshTasks() {
return new Action0() {
@Override
public void call() {
fetchFromRemote()
.compose(asEvent(taskRelay.getValue()))
.subscribe(taskRelay);
}
};
}
@Override
public Action0 clearCompletedTasks() {
return new Action0() {
@Override
public void call() {
remoteDataSource.clearCompletedTasks()
.map(asSyncedTasksNow())
.flatMap(persistTasks())
.compose(asEvent(taskRelay.getValue()))
.subscribe(taskRelay);
}
};
}
@Override
public Action0 deleteAllTasks() {
return new Action0() {
@Override
public void call() {
remoteDataSource.deleteAllTasks()
.flatMap(deleteAllLocalTasks())
.compose(asEvent(taskRelay.getValue()))
.subscribe(taskRelay);
}
};
}
private Func1<Void, Observable<Tasks>> deleteAllLocalTasks() {
return new Func1<Void, Observable<Tasks>>() {
@Override
public Observable<Tasks> call(Void aVoid) {
return localDataSource.deleteAllTasks()
.map(new Func1<Void, Tasks>() {
@Override
public Tasks call(Void aVoid) {
return Tasks.empty();
}
});
}
};
}
@Override
public Action0 complete(final Task originalTask) {
return save(originalTask.complete());
}
@Override
public Action0 activate(final Task originalTask) {
return save(originalTask.activate());
}
@Override
public Action0 save(final Task updatedTask) {
return new Action0() {
@Override
public void call() {
final long actionTimestamp = clock.timeInMillis();
remoteDataSource.saveTask(updatedTask)
.compose(startAheadThenConfirmOrMarkAsError(updatedTask, actionTimestamp))
.compose(updateAndPersistIfMostRecentAction())
.subscribe();
}
};
}
private static Observable.Transformer<Task, SyncedData<Task>> startAheadThenConfirmOrMarkAsError(
final Task updatedTask,
final long actionTimestamp
) {
return asSyncedAction(new SyncedDataCreator<Task>() {
@Override
public SyncedData<Task> startWith() {
return SyncedData.from(updatedTask, SyncState.AHEAD, actionTimestamp);
}
@Override
public SyncedData<Task> onConfirmed(Task taskFromRemote) {
return SyncedData.from(taskFromRemote, SyncState.IN_SYNC, actionTimestamp);
}
@Override
public SyncedData<Task> onError() {
return SyncedData.from(updatedTask, SyncState.SYNC_ERROR, actionTimestamp); //TODO add a retry logic for all Sync_Error states
}
});
}
private Observable.Transformer<SyncedData<Task>, SyncedData<Task>> updateAndPersistIfMostRecentAction() {
return new Observable.Transformer<SyncedData<Task>, SyncedData<Task>>() {
@Override
public Observable<SyncedData<Task>> call(Observable<SyncedData<Task>> observable) {
return observable.flatMap(ifThenMap(new IfThenFlatMap<SyncedData<Task>, SyncedData<Task>>() {
@Override
public boolean ifMatches(SyncedData<Task> value) {
Tasks tasks = taskRelay.getValue().data().or(Tasks.empty());
return tasks.isMostRecentAction(value);
}
@Override
public Observable<SyncedData<Task>> thenMap(final SyncedData<Task> value) {
Event<Tasks> event = taskRelay.getValue();
Tasks tasks = event.data().or(Tasks.empty());
return Observable.just(value)
.map(asUpdated(event, tasks))
.doOnNext(taskRelay)
.flatMap(persistSyncedData(value));
}
@Override
public Observable<SyncedData<Task>> elseMap(SyncedData<Task> value) {
return Observable.empty();
}
}));
}
};
}
private static Func1<SyncedData<Task>, Event<Tasks>> asUpdated(final Event<Tasks> event, final Tasks tasks) {
return new Func1<SyncedData<Task>, Event<Tasks>>() {
@Override
public Event<Tasks> call(SyncedData<Task> syncedData) {
Tasks updatedTasks = tasks.save(syncedData);
return event.updateData(updatedTasks);
}
};
}
private Func1<Event<Tasks>, Observable<SyncedData<Task>>> persistSyncedData(final SyncedData<Task> value) {
return new Func1<Event<Tasks>, Observable<SyncedData<Task>>>() {
@Override
public Observable<SyncedData<Task>> call(Event<Tasks> event) {
return localDataSource.saveTask(value);
}
};
}
}