blob: 8b4061704da6eb3506a2c2e82205924616a28937 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.baku.toolkit.bind;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ListView;
import com.google.common.collect.Ordering;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.v.rx.syncbase.RangeWatchBatch;
import io.v.rx.syncbase.RangeWatchEvent;
import io.v.rx.syncbase.RxTable;
import io.v.v23.syncbase.nosql.ChangeType;
import io.v.v23.syncbase.nosql.PrefixRange;
import io.v.v23.syncbase.nosql.RowRange;
import java8.util.function.Function;
import lombok.Getter;
import lombok.experimental.Accessors;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
import rx.functions.Func1;
@Accessors(prefix = "m")
public class SyncbaseRangeAdapter<T> implements RangeAdapter {
private static final String ERR_INCONSISTENT = "Sorted data are inconsistent with map data";
/**
* If {@code T} is {@link Comparable}, the default row ordering is natural ordering on row
* values. Otherwise, the default is natural ordering on row names.
*/
@Accessors(prefix = "m")
public static class Builder<T, A extends RangeAdapter>
extends BaseBuilder<Builder<T, ? extends RangeAdapter>> {
private PrefixRange mPrefix = RowRange.prefix("");
private Class<T> mType;
private Ordering<? super RxTable.Row<T>> mOrdering;
private Func1<String, Boolean> mKeyFilter;
private ViewAdapter<? super RxTable.Row<T>, ?> mViewAdapter;
private Context mViewAdapterContext;
@Getter
private A mAdapter;
public Builder<T, A> prefix(final PrefixRange prefix) {
mPrefix = prefix;
return this;
}
public Builder<T, A> prefix(final String prefix) {
return prefix(RowRange.prefix(prefix));
}
/**
* This setter is minimally typesafe; after setting the {@code type}, clients should
* probably also update {@code ordering} and {@code viewAdapter}.
*/
public <U> Builder<U, SyncbaseRangeAdapter<U>> type(final Class<U> type) {
@SuppressWarnings("unchecked")
final Builder<U, SyncbaseRangeAdapter<U>> casted =
(Builder<U, SyncbaseRangeAdapter<U>>) this;
casted.mType = type;
return casted;
}
public Builder<T, A> ordering(final Ordering<? super RxTable.Row<? extends T>> ordering) {
mOrdering = ordering;
return this;
}
public Builder<T, A> valueOrdering(final Ordering<? super T> ordering) {
return ordering(ordering.onResultOf(RxTable.Row::getValue));
}
/**
* For comparable {@code T}, default to natural ordering on values. Otherwise, default to
* natural ordering on row names.
*/
private Ordering<? super RxTable.Row<? extends T>> getDefaultOrdering() {
if (mOrdering == null && Comparable.class.isAssignableFrom(mType)) {
return Ordering.natural().onResultOf(r -> (Comparable) r.getValue());
} else {
return Ordering.natural().onResultOf(RxTable.Row::getRowName);
}
}
public Builder<T, A> keyFilter(final Func1<String, Boolean> keyFilter) {
mKeyFilter = keyFilter;
return this;
}
public Builder<T, A> viewAdapter(final ViewAdapter<? super RxTable.Row<T>, ?> viewAdapter) {
mViewAdapter = viewAdapter;
return this;
}
public <U> Builder<T, A> textViewAdapter(final Function<RxTable.Row<T>, U> fn) {
return viewAdapter(getDefaultViewAdapter(fn));
}
public Builder<T, A> valueAdapter(final ViewAdapter<T, ?> viewAdapter) {
return viewAdapter(new TransformingViewAdapter<>(viewAdapter, RxTable.Row::getValue));
}
private <U> ViewAdapter<? super RxTable.Row<T>, ?> getDefaultViewAdapter(
final Function<RxTable.Row<T>, U> fn) {
return new TextViewAdapter<U>(getDefaultViewAdapterContext()).map(fn);
}
/**
* The default view adapter stringizes values.
*/
private ViewAdapter<? super RxTable.Row<T>, ?> getDefaultViewAdapter() {
return getDefaultViewAdapter(RxTable.Row::getValue);
}
public Builder<T, A> viewAdapterContext(final Context context) {
mViewAdapterContext = context;
return this;
}
public Context getDefaultViewAdapterContext() {
return mViewAdapterContext == null ? mActivity : mViewAdapterContext;
}
public Observable<RangeWatchBatch<T>> buildWatch() {
if (mType == null) {
throw new IllegalStateException("Missing required type property");
}
return mRxTable.watch(mPrefix, mKeyFilter, mType);
}
private Ordering<? super RxTable.Row<T>> getOrdering() {
return mOrdering == null ? getDefaultOrdering() : mOrdering;
}
private ViewAdapter<? super RxTable.Row<T>, ?> getViewAdapter() {
return mViewAdapter == null ? getDefaultViewAdapter() : mViewAdapter;
}
private <U extends RangeAdapter> U subscribeAdapter(final U adapter) {
subscribe(adapter.getSubscription());
mAdapter = null;
return adapter;
}
public SyncbaseListAdapter<T> buildListAdapter() {
return subscribeAdapter(new SyncbaseListAdapter<>(
buildWatch(), getOrdering(), getViewAdapter(), mOnError));
}
public SyncbaseRecyclerAdapter<T, ?> buildRecyclerAdapter() {
return subscribeAdapter(new SyncbaseRecyclerAdapter<>(
buildWatch(), getOrdering(), getViewAdapter(), mOnError));
}
public Builder<T, SyncbaseListAdapter<T>> bindTo(final ListView listView) {
@SuppressWarnings("unchecked")
final Builder<T, SyncbaseListAdapter<T>> casted =
(Builder<T, SyncbaseListAdapter<T>>) this;
casted.mAdapter = buildListAdapter();
listView.setAdapter(casted.mAdapter);
return casted;
}
public Builder<T, SyncbaseRecyclerAdapter<T, ?>> bindTo(final RecyclerView recyclerView) {
@SuppressWarnings("unchecked")
final Builder<T, SyncbaseRecyclerAdapter<T, ?>> casted =
(Builder<T, SyncbaseRecyclerAdapter<T, ?>>) this;
casted.mAdapter = buildRecyclerAdapter();
recyclerView.setAdapter(casted.mAdapter);
return casted;
}
@Override
public Builder<T, ?> bindTo(final View view) {
if (view instanceof ListView) {
return bindTo((ListView) view);
} else if (view instanceof RecyclerView) {
return bindTo((RecyclerView) view);
} else {
throw new IllegalArgumentException("No default binding for view " + view);
}
}
}
public static <T, A extends RangeAdapter> Builder<T, A> builder() {
return new Builder<>();
}
private final Map<String, T> mRows = new HashMap<>();
private List<RxTable.Row<T>> mSorted = new ArrayList<>();
private final Ordering<? super RxTable.Row<T>> mOrdering;
private final Action1<Throwable> mOnError;
@Getter
private final Subscription mSubscription;
public SyncbaseRangeAdapter(final Observable<RangeWatchBatch<T>> watch,
final Ordering<? super RxTable.Row<T>> ordering,
final Action1<Throwable> onError) {
// ensure deterministic ordering by always applying secondary order on row name
mOrdering = ordering.compound(Ordering.natural().onResultOf(RxTable.Row::getRowName));
mOnError = onError;
mSubscription = subscribeTo(watch);
}
@Override
public void close() {
mSubscription.unsubscribe();
}
private Subscription subscribeTo(final Observable<RangeWatchBatch<T>> watch) {
return watch
.concatMap(RangeWatchBatch::collectChanges)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::processEvents, mOnError);
}
private int findRowForEdit(final String rowName, final T oldValue) {
final int oldIndex = Collections.binarySearch(mSorted,
new RxTable.Row<>(rowName, oldValue), mOrdering);
if (oldIndex < 0) {
throw new ConcurrentModificationException(ERR_INCONSISTENT);
} else {
return oldIndex;
}
}
protected void processEvents(final Collection<RangeWatchEvent<T>> events) {
// TODO(rosswang): more efficient updates for larger batches
for (final RangeWatchEvent<T> e : events) {
if (e.getChangeType() == ChangeType.DELETE_CHANGE) {
removeOne(e.getRow());
} else {
updateOne(e.getRow());
}
}
}
protected void removeOne(final RxTable.Row<T> entry) {
final T old = mRows.remove(entry.getRowName());
if (old != null) {
mSorted.remove(findRowForEdit(entry.getRowName(), old));
}
}
private int insertionIndex(final RxTable.Row<T> entry) {
final int bs = Collections.binarySearch(mSorted, entry, mOrdering);
return bs < 0 ? ~bs : bs;
}
protected void updateOne(final RxTable.Row<T> entry) {
final T old = mRows.put(entry.getRowName(), entry.getValue());
if (old == null) {
mSorted.add(insertionIndex(entry), entry);
} else {
final int oldIndex = findRowForEdit(entry.getRowName(), old);
int newIndex = insertionIndex(entry);
if (newIndex >= oldIndex) {
newIndex--;
for (int i = oldIndex; i < newIndex; i++) {
mSorted.set(i, mSorted.get(i + 1));
}
} else {
for (int i = oldIndex; i > newIndex; i--) {
mSorted.set(i, mSorted.get(i - 1));
}
}
mSorted.set(newIndex, entry);
}
}
public int getCount() {
return mRows.size();
}
public RxTable.Row<T> getRowAt(final int position) {
return mSorted.get(position);
}
public T getValue(final String rowName) {
return mRows.get(rowName);
}
public int getRowIndex(final String rowName) {
return Collections.binarySearch(mSorted, new RxTable.Row<>(rowName, mRows.get(rowName)),
mOrdering);
}
public boolean containsRow(final String rowName) {
return mRows.containsKey(rowName);
}
}