From ddde1340a3b433fdcdd89c78654e751f69459587 Mon Sep 17 00:00:00 2001 From: Marc Arndt Date: Wed, 12 Jun 2019 22:34:48 +0200 Subject: [PATCH] - add a MappedList implementation --- .../javafx/collections/ObservableList.java | 13 + .../transformation/MappedList.java | 233 ++++++++++++++++++ .../javafx/collections/MappedListTest.java | 157 ++++++++++++ 3 files changed, 403 insertions(+) create mode 100644 modules/javafx.base/src/main/java/javafx/collections/transformation/MappedList.java create mode 100644 modules/javafx.base/src/test/java/test/javafx/collections/MappedListTest.java diff --git a/modules/javafx.base/src/main/java/javafx/collections/ObservableList.java b/modules/javafx.base/src/main/java/javafx/collections/ObservableList.java index 5695a5db53..251eeeed2d 100644 --- a/modules/javafx.base/src/main/java/javafx/collections/ObservableList.java +++ b/modules/javafx.base/src/main/java/javafx/collections/ObservableList.java @@ -29,10 +29,12 @@ import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.function.Function; import java.util.function.Predicate; import javafx.beans.Observable; import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.MappedList; import javafx.collections.transformation.SortedList; /** @@ -103,6 +105,17 @@ public interface ObservableList extends List, Observable { */ public void remove(int from, int to); + /** + * Creates a {@link MappedList} wrapper of this list using + * the specified mapper. + * @param mapper the mapper to use + * @return new {@code MappedList} + * @since JavaFX 13.0 + */ + public default MappedList mapped(Function mapper) { + return new MappedList(this, mapper); + } + /** * Creates a {@link FilteredList} wrapper of this list using * the specified predicate. diff --git a/modules/javafx.base/src/main/java/javafx/collections/transformation/MappedList.java b/modules/javafx.base/src/main/java/javafx/collections/transformation/MappedList.java new file mode 100644 index 0000000000..0b5ff262e0 --- /dev/null +++ b/modules/javafx.base/src/main/java/javafx/collections/transformation/MappedList.java @@ -0,0 +1,233 @@ +package javafx.collections.transformation; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.ListChangeListener.Change; +import javafx.collections.ObservableList; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.IntStream; + +/** + * An implementation of a mapped {@link ObservableList}, which maps the values of the source {@link ObservableList} into + * values of the target type {@link E}. + * + * @param The instance type of the target elements + * @param The instance type of the source elements + */ +public class MappedList extends TransformationList { + /** + * The mapper function used to map the source values to the target type {@link E}. + * If the mapper function is set to null the list acts as if it were empty + */ + private final ObjectProperty> mapper; + + /** + * A list of all mapped values + */ + private final List mappedValues; + + /** + * Constructor + * + * @param source The source list + * @param mapper The mapper function + */ + public MappedList(ObservableList source, ObjectProperty> mapper) { + super(source); + + this.mapper = mapper; + this.mappedValues = new ArrayList<>(); + + // create a cache of all mapped source elements + Optional.ofNullable(getMapper()) + .ifPresent(mapperFunction -> source.stream().map(mapperFunction).forEach(mappedValues::add)); + + // add a listener to detect changes of the mapper function + mapper.addListener((observable, oldMapper, newMapper) -> { + beginChange(); + // the previous mapper function was not null -> remove all values + if (oldMapper != null) { + final List removed = new ArrayList<>(mappedValues); + + mappedValues.clear(); + + nextRemove(0, removed); + } + + // the current mapper function is not null -> calculate new values + if (newMapper != null) { + for (F element : getSource()) { + mappedValues.add(newMapper.apply(element)); + } + + nextAdd(0, size()); + } + endChange(); + }); + + // fire an initialisation event containing all mapped elements + fireInitialisationChange(); + } + + /** + * Constructor + * + * @param source The source list + * @param mapper The mapper function + */ + public MappedList(ObservableList source, Function mapper) { + this(source, new SimpleObjectProperty<>(mapper)); + } + + /** + * Constructor + * + * @param source The source list + */ + public MappedList(ObservableList source) { + this(source, new SimpleObjectProperty<>()); + } + + /** + * Fires a {@link Change} event containing all elements inside this {@link TransformationList}. + * This method should be used directly after the {@link TransformationList} has been initialized + */ + private void fireInitialisationChange() { + beginChange(); + nextAdd(0, size()); + endChange(); + } + + /** + * {@inheritDoc} + */ + @Override + public void sourceChanged(ListChangeListener.Change change) { + beginChange(); + while (change.next()) { + if (change.wasPermutated()) { + permute(change); + } else if (change.wasUpdated()) { + update(change); + } else { + addRemove(change); + } + } + endChange(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getSourceIndex(int index) { + if (index >= size()) { + throw new IndexOutOfBoundsException(); + } + + return index; + } + + /** + * {@inheritDoc} + */ + @Override + public int getViewIndex(int index) { + if (index >= size()) { + throw new IndexOutOfBoundsException(); + } + + return index; + } + + /** + * {@inheritDoc} + */ + @Override + public E get(int index) { + if (index >= size()) { + throw new IndexOutOfBoundsException(); + } + + return mappedValues.get(index); + } + + /** + * {@inheritDoc} + *

+ * If no mapper function is set, the size of this list is always 0, otherwise it equals + * the size of the source list + */ + @Override + public int size() { + return Optional.ofNullable(getMapper()) + .map(mapper -> getSource().size()).orElse(0); + } + + private void permute(Change change) { + final int from = change.getFrom(); + final int to = change.getTo(); + + if (to > from) { + final List clone = new ArrayList<>(mappedValues); + final int[] perm = IntStream.range(0, size()).toArray(); + + for (int i = from; i < to; ++i) { + perm[i] = change.getPermutation(i); + mappedValues.set(i, clone.get(change.getPermutation(i))); + } + + nextPermutation(from, to, perm); + } + } + + private void update(Change change) { + final int from = change.getFrom(); + final int to = change.getTo(); + + final Function mapper = getMapper(); + + if (mapper != null) { + for (int i = from; i < to; ++i) { + mappedValues.set(i, mapper.apply(getSource().get(i))); + + nextUpdate(i); + } + } + } + + private void addRemove(Change change) { + final int from = change.getFrom(); + + final Function mapper = getMapper(); + + if (mapper != null) { + for (int index = from + change.getRemovedSize() - 1; index >= from; index--) { + nextRemove(index, mappedValues.remove(index)); + } + + for (int index = from; index < from + change.getAddedSize(); index++) { + mappedValues.add(index, mapper.apply(getSource().get(index))); + + nextAdd(index, index + 1); + } + } + } + + public Function getMapper() { + return mapper.get(); + } + + public void setMapper(Function mapper) { + this.mapper.set(mapper); + } + + public ObjectProperty> mapperProperty() { + return mapper; + } +} diff --git a/modules/javafx.base/src/test/java/test/javafx/collections/MappedListTest.java b/modules/javafx.base/src/test/java/test/javafx/collections/MappedListTest.java new file mode 100644 index 0000000000..6ebd4c3900 --- /dev/null +++ b/modules/javafx.base/src/test/java/test/javafx/collections/MappedListTest.java @@ -0,0 +1,157 @@ +package test.javafx.collections; + +import javafx.beans.binding.Bindings; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.MappedList; +import javafx.collections.transformation.SortedList; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class MappedListTest { + @Test + public void testListCreation() { + final ObservableList observableList = FXCollections.observableArrayList(List.of(3, 7, 1, 5)); + final MappedList mappedList = new MappedList<>(observableList, String::valueOf); + + final List actual = new ArrayList<>(); + + Bindings.bindContent(actual, mappedList); + + assertEquals(List.of("3", "7", "1", "5"), mappedList); + assertEquals(List.of("3", "7", "1", "5"), actual); + } + + @Test + public void testListAdd() { + final ObservableList observableList = FXCollections.observableArrayList(List.of(3, 7, 1, 5)); + final MappedList mappedList = new MappedList<>(observableList, String::valueOf); + + final List actual = new ArrayList<>(); + + Bindings.bindContent(actual, mappedList); + + assertEquals(List.of("3", "7", "1", "5"), mappedList); + assertEquals(List.of("3", "7", "1", "5"), actual); + + observableList.add(0); + + assertEquals(List.of("3", "7", "1", "5", "0"), mappedList); + assertEquals(List.of("3", "7", "1", "5", "0"), actual); + } + + @Test + public void testListRemove() { + final ObservableList observableList = FXCollections.observableArrayList(List.of(3, 7, 1, 5)); + final MappedList mappedList = new MappedList<>(observableList, String::valueOf); + + final List actual = new ArrayList<>(); + + Bindings.bindContent(actual, mappedList); + + assertEquals(List.of("3", "7", "1", "5"), mappedList); + assertEquals(List.of("3", "7", "1", "5"), actual); + + observableList.remove(2); + + assertEquals(List.of("3", "7", "5"), mappedList); + assertEquals(List.of("3", "7", "5"), actual); + } + + @Test + public void testListUpdate() { + final ObservableList observableList = FXCollections.observableArrayList(List.of(3, 7, 1, 5)); + final MappedList mappedList = new MappedList<>(observableList, String::valueOf); + + final List actual = new ArrayList<>(); + + Bindings.bindContent(actual, mappedList); + + assertEquals(List.of("3", "7", "1", "5"), mappedList); + assertEquals(List.of("3", "7", "1", "5"), actual); + + observableList.set(2, 4); + + assertEquals(List.of("3", "7", "4", "5"), mappedList); + assertEquals(List.of("3", "7", "4", "5"), actual); + } + + @Test + public void testListPermutation() { + final SortedList sortedList = FXCollections.observableArrayList(List.of(3, 7, 1, 5)) + .sorted(Comparator.naturalOrder()); + final MappedList mappedList = new MappedList<>(sortedList, String::valueOf); + + final List actual = new ArrayList<>(); + + Bindings.bindContent(actual, mappedList); + + assertEquals(List.of("1", "3", "5", "7"), mappedList); + assertEquals(List.of("1", "3", "5", "7"), actual); + + sortedList.comparatorProperty().set(Comparator.comparing(String::valueOf).reversed()); + + assertEquals(List.of("7", "5", "3", "1"), mappedList); + assertEquals(List.of("7", "5", "3", "1"), actual); + } + + @Test + public void testMapperChange() { + final ObservableList observableList = FXCollections.observableArrayList(List.of(3, 7, 1, 5)); + final MappedList mappedList = new MappedList<>(observableList, String::valueOf); + + final List actual = new ArrayList<>(); + + Bindings.bindContent(actual, mappedList); + + assertEquals(List.of("3", "7", "1", "5"), mappedList); + assertEquals(List.of("3", "7", "1", "5"), actual); + + mappedList.setMapper(i -> i + "!"); + + assertEquals(List.of("3!", "7!", "1!", "5!"), mappedList); + assertEquals(List.of("3!", "7!", "1!", "5!"), actual); + } + + @Test + public void testMapperChangeToNull() { + final ObservableList observableList = FXCollections.observableArrayList(List.of(3, 7, 1, 5)); + final MappedList mappedList = new MappedList<>(observableList, String::valueOf); + + final List actual = new ArrayList<>(); + + Bindings.bindContent(actual, mappedList); + + assertEquals(List.of("3", "7", "1", "5"), mappedList); + assertEquals(List.of("3", "7", "1", "5"), actual); + + mappedList.setMapper(null); + + assertEquals(Collections.emptyList(), mappedList); + assertEquals(Collections.emptyList(), actual); + } + + @Test + public void testMapperChangeFromNull() { + final ObservableList observableList = FXCollections.observableArrayList(List.of(3, 7, 1, 5)); + final MappedList mappedList = new MappedList<>(observableList); + + final List actual = new ArrayList<>(); + + Bindings.bindContent(actual, mappedList); + + assertEquals(Collections.emptyList(), mappedList); + assertEquals(Collections.emptyList(), actual); + + mappedList.setMapper(String::valueOf); + + assertEquals(List.of("3", "7", "1", "5"), mappedList); + assertEquals(List.of("3", "7", "1", "5"), actual); + } +}