package org.tessell.model.properties;
import static org.tessell.model.properties.NewProperty.booleanProperty;
import static org.tessell.model.properties.NewProperty.derivedProperty;
import static org.tessell.model.properties.NewProperty.integerProperty;
import static org.tessell.model.properties.NewProperty.listProperty;
import java.util.*;
import org.tessell.model.Model;
import org.tessell.model.events.*;
import org.tessell.model.values.DerivedValue;
import org.tessell.model.values.Value;
import org.tessell.util.ListDiff;
import org.tessell.util.ListDiff.Location;
import org.tessell.util.ObjectUtils;
import com.google.common.base.Function;
import com.google.common.collect.Ordering;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.shared.HandlerRegistration;
public class ListProperty<E> extends AbstractProperty<List<E>, ListProperty<E>> implements HasMemberChangedHandlers {
private IntegerProperty size;
private BasicProperty<E> first;
private BasicProperty<E> last;
private List<E> readOnly;
private List<E> readOnlySource;
private Comparator<E> lastComparator;
private Comparator<E> persistentComparator;
private PropertyGroup allValid;
/** Used to convert a list from one type of element to another. */
public interface ElementConverter<E, F> {
F to(E element);
E from(F element);
}
/** Used to map a list from one type of element to another. */
public interface ElementMapper<E, F> {
F map(E element);
}
/** Used to filter a list to a matching condition. */
public interface ElementFilter<E> {
boolean matches(E element);
}
@SuppressWarnings("unchecked")
public ListProperty(final Value<? extends List<E>> value) {
// the "? extends List<E>" is so we can be called with Value<ArrayList<E>>
// types, which dtonator currently generates in the value inner classes
super((Value<List<E>>) value);
}
@Override
public List<E> get() {
List<E> current = super.get();
// change the wrapped list only when the source identity changes
if (readOnly == null || current != readOnlySource) {
readOnly = current == null ? null : Collections.unmodifiableList(current);
readOnlySource = current;
}
return readOnly;
}
public <E1> ListProperty<E1> asList(final PropertyConverter<List<E>, List<E1>> converter) {
return listProperty(new DerivedValue<List<E1>>(getValueName() + ".asList") {
public List<E1> get() {
return converter.to(ListProperty.this.get());
}
});
}
@Override
public Property<Boolean> is(final List<E> value) {
return is(value, new ArrayList<E>());
}
@Override
public Property<Boolean> is(final Property<List<E>> other) {
return is(other, new ArrayList<E>());
}
/** @return a copy of our list as an {@link ArrayList}, e.g. for GWT-RPC calls. */
public ArrayList<E> toArrayList() {
return new ArrayList<E>(getDirect());
}
@Override
public String toString() {
List<E> e = getDirect();
if (e == null) {
return getValueName() + " null";
}
// Janky, but keep ListProperty.toString from being huge and accidentally ruining perf
String s = getValueName() + " [";
for (int i = 0; i < e.size() && i < 20; i++) {
s += ObjectUtils.toStr(e.get(i), "null");
if (i != e.size() - 1) {
s += ", ";
}
}
if (e.size() > 20) {
s += "...";
}
s += "]";
return s;
}
/** Adds {@code item}, firing a {@link ValueAddedEvent}. */
public void add(final E item) {
getDirect().add(item);
sortIfNeeded();
setTouched(true);
// will fire add+change if needed
reassess();
}
/** Adds {@code item}, firing a {@link ValueAddedEvent}. */
public void add(final int index, final E item) {
getDirect().add(index, item);
sortIfNeeded();
setTouched(true);
// will fire add+change if needed
reassess();
}
/** Adds each item in {@code items}, firing a {@link ValueAddedEvent} for each. */
public void addAll(Collection<? extends E> items) {
if (items.size() == 0) {
return;
}
getDirect().addAll(items);
sortIfNeeded();
setTouched(true);
// will fire adds+change if needed
reassess();
}
/** Removes {@code item}, firing a {@link ValueRemovedEvent}. */
public void remove(final E item) {
getDirect().remove(item);
sortIfNeeded();
setTouched(true);
// will fire remove+change if needed
reassess();
}
/** Removes each item in {@code items}, firing a {@link ValueRemovedEvent} for each. */
public void removeAll(Collection<? extends E> items) {
if (items.size() == 0) {
return;
}
getDirect().removeAll(items);
setTouched(true);
// will fire adds+change if needed
reassess();
}
/** Removes all entries, firing a {@link ValueRemovedEvent} for each. */
public void clear() {
getDirect().clear();
// will fire removes+change if needed
reassess();
}
/** @return a derived property of whether at least one element satisfies {@code condition}. */
public Property<Boolean> exists(final Condition<E> condition) {
return derivedProperty(() -> {
List<E> list = get();
if (list != null) {
for (E element : list) {
if (condition.evaluate(element)) {
return true;
}
}
}
return false;
});
}
/** @return a derived property of whether {@code item} is in this list. */
public BooleanProperty contains(final E item) {
return addDerived(booleanProperty(new Value<Boolean>() {
@Override
public Boolean get() {
final List<E> current = ListProperty.this.get();
return (current == null) ? false : current.contains(item);
}
@Override
public void set(Boolean value) {
final List<E> current = ListProperty.this.get();
if (current != null) {
if (Boolean.TRUE.equals(value) && !current.contains(item)) {
add(item);
} else if (!Boolean.TRUE.equals(value) && current.contains(item)) {
remove(item);
}
}
}
@Override
public String getName() {
return "contains " + item;
}
@Override
public boolean isReadOnly() {
return false;
}
}));
}
/** @return a two-way property of whether {@code items} are a subset of this list. */
public BooleanProperty containsAll(final List<E> items) {
final BooleanProperty b = booleanProperty(getName() + "ContainsAll", false);
final boolean[] firing = { false };
// conditionally update us when b goes true->false/false->true
b.addPropertyChangedHandler(new PropertyChangedHandler<Boolean>() {
public void onPropertyChanged(final PropertyChangedEvent<Boolean> event) {
if (!firing[0]) {
firing[0] = true;
if (event.getNewValue()) {
for (final E item : items) {
if (!get().contains(item)) {
add(item);
}
}
} else {
removeAll(items);
}
firing[0] = false;
}
}
});
// conditionally update b when we change
addPropertyChangedHandler(new PropertyChangedHandler<List<E>>() {
public void onPropertyChanged(final PropertyChangedEvent<List<E>> event) {
if (!firing[0]) {
firing[0] = true;
b.set(get().containsAll(items));
firing[0] = false;
}
}
});
return b;
}
/** @return a derived property that reflects this list's size. */
public IntegerProperty size() {
if (size == null) {
size = addDerived(integerProperty(new DerivedValue<Integer>(getValueName() + ".size") {
public Integer get() {
final List<E> current = ListProperty.this.get();
return (current == null) ? null : current.size();
}
}));
}
return size;
}
/** @return a derived property that reflects this list's first element (or null) */
public Property<E> first() {
if (first == null) {
first = derivedProperty(new DerivedValue<E>("first") {
public E get() {
List<E> list = ListProperty.this.get();
return list == null || list.isEmpty() ? null : list.get(0);
}
});
}
return first;
}
/** @return a derived property that reflects this list's last element (or null) */
public Property<E> last() {
if (last == null) {
last = derivedProperty(new DerivedValue<E>("last") {
public E get() {
List<E> list = ListProperty.this.get();
return list == null || list.isEmpty() ? null : list.get(list.size() - 1);
}
});
}
return last;
}
/** @return a property that will reflect whether {@code element} is the first element. */
public BooleanProperty isFirst(final E element) {
return booleanProperty(new DerivedValue<Boolean>(element + "IsFirst") {
public Boolean get() {
List<E> l = ListProperty.this.get();
return l != null && !l.isEmpty() && element.equals(l.get(0));
}
});
}
/** Moves {@code element} up in the list; noop if it's already first. */
public void moveUp(E element) {
int at = getDirect().indexOf(element);
if (at > 0) {
getDirect().remove(at);
getDirect().add(at - 1, element);
setTouched(true);
reassess();
}
}
/** Moves {@code element} down in the list; noop if it's already last. */
public void moveDown(E element) {
int at = getDirect().indexOf(element);
if (at > -1 && at < getDirect().size() - 1) {
getDirect().remove(at);
getDirect().add(at + 1, element);
setTouched(true);
reassess();
}
}
/** @return a property that will reflect whether {@code element} is the last element. */
public BooleanProperty isLast(final E element) {
return booleanProperty(new DerivedValue<Boolean>(element + "IsLast") {
public Boolean get() {
List<E> l = ListProperty.this.get();
return l != null && !l.isEmpty() && element.equals(l.get(l.size() - 1));
}
});
}
/** @return a property that will reflect the index of {@code element}. */
public IntegerProperty indexOf(final E element) {
return integerProperty(new DerivedValue<Integer>(element + "Index") {
public Integer get() {
List<E> l = ListProperty.this.get();
return l == null ? -1 : l.indexOf(element);
}
});
}
/** @return a new {@link ListCursor} that can be used to move through the list. */
public ListCursor<E> newCursor() {
return new ListCursor<E>(this);
}
/**
* Sorts our values by {@code comparator} (only once, to keep the sort
* continually updated, see {@link #setComparator}).
*
* If we've already been sorted by comparator, it will reverse the order.
*/
public void sort(Comparator<E> comparator) {
if (lastComparator == comparator) {
// Creating this new reverse comparator means we'll also "reset"
// lastComparator to some other value, such that if we get called
// with the same comparator again, we'll toggle back/forth the order
//
// Eventually we could get fancier and keep a stack of comparators.
comparator = Collections.reverseOrder(comparator);
}
Collections.sort(getDirect(), comparator);
lastComparator = comparator;
reassess();
}
/**
* Sorts our list by {@code f} when {@code clickable} is clicked.
*/
public <C extends Comparable<?>> void sortOn(HasClickHandlers clickable, final Function<E, C> f) {
Ordering<E> order = Ordering.natural().nullsFirst().onResultOf(f);
clickable.addClickHandler(e -> sort(order));
// window.scrollTo(0, view.table().getAbsoluteTop());
}
/**
* Invoke a sort in case any of our contents have changed. (Eventually this should be done
* automatically on MemberChangedEvent.)
*/
public void resort() {
Collections.sort(getDirect(), lastComparator);
reassess();
}
/**
* Sorts our values by {@code comparator} (and continually applies
* the comparator as new values are added/removed/set).
*/
public void setComparator(Comparator<E> comparator) {
this.persistentComparator = comparator;
List<E> copy = copyLastValue(getDirect());
if (copy != null && !copy.equals(getDirect())) {
set(copy);
}
}
/** Registers {@code handler} to be called when new values are added. */
public HandlerRegistration addValueAddedHandler(final ValueAddedHandler<E> handler) {
return addHandler(ValueAddedEvent.getType(), handler);
}
/** Registers {@code handler} to be called when values are removed. */
public HandlerRegistration addValueRemovedHandler(final ValueRemovedHandler<E> handler) {
return addHandler(ValueRemovedEvent.getType(), handler);
}
/** Registers {@code handler} to be called when the list changes. */
public HandlerRegistration addListChangedHandler(final ListChangedHandler<E> handler) {
return addHandler(ListChangedEvent.getType(), handler);
}
/** Registers {@code handler} to be called when values changed. */
public HandlerRegistration addMemberChangedHandler(final MemberChangedHandler handler) {
return addHandler(MemberChangedEvent.getType(), handler);
}
/**
* Creates a new {@link ListProperty>} of type {@code F}.
*
* Any changes made to either list will be reflected in the other,
* using {@code converter} to go between {@code E} and {@code F}.
*/
public <F> ListProperty<F> as(final ElementConverter<E, F> converter) {
// make an intial copy of all the elements currently in our list
List<F> initial = null;
if (get() != null) {
initial = new ArrayList<F>();
for (E e : get()) {
F f = converter.to(e);
initial.add(f);
}
}
final ListProperty<F> as = listProperty(getName(), initial);
final boolean[] active = { false };
// keep converting E -> F into as
addListChangedHandler(new ListChangedHandler<E>() {
public void onListChanged(ListChangedEvent<E> event) {
if (!active[0]) {
active[0] = true;
if (get() != null && as.get() == null) {
as.setInitialValue(new ArrayList<F>());
}
event.getDiff().apply(as.getDirect(), new ListDiff.Mapper<E, F>() {
public F map(E e) {
return converter.to(e);
}
});
if (isTouched() && !as.isTouched()) {
as.setTouched(true);
} else {
as.reassess();
}
active[0] = false;
}
}
});
// also convert new Fs back into Es
as.addListChangedHandler(new ListChangedHandler<F>() {
public void onListChanged(ListChangedEvent<F> event) {
if (!active[0]) {
active[0] = true;
if (get() == null && as.get() != null) {
setInitialValue(new ArrayList<E>());
}
event.getDiff().apply(getDirect(), new ListDiff.Mapper<F, E>() {
public E map(F f) {
return converter.from(f);
}
});
if (as.isTouched() && !isTouched()) {
setTouched(true);
} else {
reassess();
}
active[0] = false;
}
}
});
return as;
}
/**
* Creates a new {@link ListProperty>} of type {@code F}.
*
* Only changes in this list will be reflected in the returned
* list, e.g. it's a one way conversion.
*/
public <F> ListProperty<F> map(final ElementMapper<E, F> mapper) {
// make an intial copy of all the elements currently in our list
List<F> initial = null;
if (get() != null) {
initial = new ArrayList<F>();
for (E e : get()) {
initial.add(mapper.map(e));
}
}
final ListProperty<F> mapped = listProperty(getName(), initial);
// keep converting E -> F into as
addListChangedHandler(new ListChangedHandler<E>() {
public void onListChanged(ListChangedEvent<E> event) {
if (get() != null && mapped.get() == null) {
mapped.setInitialValue(new ArrayList<F>());
}
event.getDiff().apply(mapped.getDirect(), new ListDiff.Mapper<E, F>() {
public F map(E e) {
return mapper.map(e);
}
});
if (isTouched() && !mapped.isTouched()) {
mapped.setTouched(true);
} else {
mapped.reassess();
}
}
});
return mapped;
}
public ListProperty<E> filter(final ElementFilter<E> filter) {
return listProperty(new DerivedValue<List<E>>(getValueName() + ".filtered") {
public List<E> get() {
List<E> filtered = new ArrayList<E>();
if (ListProperty.this.get() != null) {
for (E item : ListProperty.this.get()) {
if (filter.matches(item)) {
filtered.add(item);
}
}
}
return Collections.unmodifiableList(filtered);
}
});
}
public ListProperty<E> prependNull() {
return listProperty(new DerivedValue<List<E>>(getValueName() + ".prependNull") {
public List<E> get() {
final List<E> listWithNull = new ArrayList<E>();
listWithNull.add(null);
listWithNull.addAll(ListProperty.this.get());
return listWithNull;
}
});
}
public ListProperty<E> appendNull() {
return listProperty(new DerivedValue<List<E>>(getValueName() + ".appendNull") {
public List<E> get() {
final List<E> listWithNull = new ArrayList<E>();
listWithNull.addAll(ListProperty.this.get());
listWithNull.add(null);
return listWithNull;
}
});
}
/**
* @return a property that, if we contain properties or models, will be true if all
* contains properties/models (as well as ourself) are valid.
*/
public Property<Boolean> allValid() {
if (allValid == null) {
allValid = new PropertyGroup(getValueName() + ".allValid");
allValid.add(this);
for (E element : getDirect()) {
addToAllValidIfNeeded(element);
}
}
return allValid;
}
@Override
protected ListProperty<E> getThis() {
return this;
}
@Override
protected List<E> copyLastValue(List<E> newValue) {
if (newValue == null) {
return null;
}
List<E> copy = new ArrayList<E>(newValue);
if (persistentComparator != null) {
Collections.sort(copy, persistentComparator);
}
return copy;
}
@Override
protected void fireChanged(List<E> oldValue, List<E> newValue) {
ListDiff<E> diff = ListDiff.of(oldValue, newValue);
for (Location<E> added : diff.added) {
fireEvent(new ValueAddedEvent<E>(added.element));
listenForMemberChanged(added.element);
addToAllValidIfNeeded(added.element);
}
for (Location<E> removed : diff.removed) {
fireEvent(new ValueRemovedEvent<E>(removed.element));
removeFromAllValidIfNeeded(removed.element);
}
fireEvent(new ListChangedEvent<E>(this, oldValue, newValue, diff));
// if someone is listening for "did one of your models change", they
// probably also care about a new model showing up/old model going away
fireEvent(new MemberChangedEvent());
super.fireChanged(oldValue, newValue);
}
private void addToAllValidIfNeeded(E element) {
if (allValid != null) {
if (element instanceof Property<?>) {
allValid.add((Property<?>) element);
} else if (element instanceof Model) {
allValid.add(((Model) element).allValid());
}
}
}
private void removeFromAllValidIfNeeded(E element) {
if (allValid != null) {
if (element instanceof Property<?>) {
allValid.remove((Property<?>) element);
} else if (element instanceof Model) {
allValid.remove(((Model) element).allValid());
}
}
}
private void sortIfNeeded() {
if (persistentComparator != null) {
Collections.sort(getDirect(), persistentComparator);
}
}
private List<E> getDirect() {
return super.get();
}
// Forwards member changed events on our models to our own model
@SuppressWarnings("unchecked")
private void listenForMemberChanged(final E item) {
if (item instanceof HasMemberChangedHandlers) {
((HasMemberChangedHandlers) item).addMemberChangedHandler(new MemberChangedHandler() {
public void onMemberChanged(MemberChangedEvent event) {
// in case the item was removed, we don't currently unsubscribe
if (getDirect().contains(item)) {
fireEvent(event);
}
}
});
} else if (item instanceof Property<?>) {
((Property<Object>) item).addPropertyChangedHandler(new PropertyChangedHandler<Object>() {
public void onPropertyChanged(PropertyChangedEvent<Object> event) {
if (getDirect().contains(item)) {
fireEvent(new MemberChangedEvent());
}
}
});
}
}
/**
* Checks equality between a and b by ignoring list order.
*
* This is because a frequent use case of "listA.is(listB)" is for "Select All"
* functionality, and if a user selects items in a different order, we still want
* to consider listA equal to listB.
*/
@Override
protected boolean isEqual(List<E> a, List<E> b) {
if (ObjectUtils.eq(a, b)) {
return true;
} else if (a != null && b != null && a.size() == b.size()) {
List<E> b2 = new ArrayList<E>(b);
for (E e : a) {
if (!b2.remove(e)) {
return false;
}
}
return true;
} else {
return false;
}
}
}