I was having the same need to concat/aggregate several ObservableLists into one list for a JavaFX LineChart. The examples I found here or on the github posted in another answer always copied all the entries from the sublists into the aggregated List on each change. For a list with many entries this seemed not very elegant.
I decided to implement my on version, which keeps track of the position of the sublists in the aggregated list and when elements in the sublists change, apply the same changes in the aggregated list. There is still room for improvement (not using a delegate List but extending a ObservableList directly, or firing the events from sublists up to the aggregated list and overriding the getters and iterators - help with that would be appreciated), but I thought I post my version here as it is, maybe it helps someone.
Code:
/**
* This class aggregates several other Observed Lists (sublists), observes changes on those sublists and applies those same changes to the
* aggregated list.
* Inspired by:
* - https://mcmap.net/q/1479617/-listchangelistener-waspermutated-block
* - https://mcmap.net/q/1446695/-how-to-concatenate-observable-lists-in-javafx
* - https://github.com/lestard/advanced-bindings/blob/master/src/main/java/eu/lestard/advanced_bindings/api/CollectionBindings.java
* Posted result on: https://mcmap.net/q/1446695/-how-to-concatenate-observable-lists-in-javafx
*/
public class AggregatedObservableArrayList<T> {
protected final List<ObservableList<T>> lists = new ArrayList<>();
final private List<Integer> sizes = new ArrayList<>();
final private List<InternalListModificationListener> listeners = new ArrayList<>();
final protected ObservableList<T> aggregatedList = FXCollections.observableArrayList();
public AggregatedObservableArrayList() {
}
/**
* The Aggregated Observable List. This list is unmodifiable, because sorting this list would mess up the entire bookkeeping we do here.
*
* @return an unmodifiable view of the aggregatedList
*/
public ObservableList<T> getAggregatedList() {
return FXCollections.unmodifiableObservableList(aggregatedList);
}
public void appendList(@NotNull ObservableList<T> list) {
assert !lists.contains(list) : "List is already contained: " + list;
lists.add(list);
final InternalListModificationListener listener = new InternalListModificationListener(list);
list.addListener(listener);
//System.out.println("list = " + list + " puttingInMap=" + list.hashCode());
sizes.add(list.size());
aggregatedList.addAll(list);
listeners.add(listener);
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
}
public void prependList(@NotNull ObservableList<T> list) {
assert !lists.contains(list) : "List is already contained: " + list;
lists.add(0, list);
final InternalListModificationListener listener = new InternalListModificationListener(list);
list.addListener(listener);
//System.out.println("list = " + list + " puttingInMap=" + list.hashCode());
sizes.add(0, list.size());
aggregatedList.addAll(0, list);
listeners.add(0, listener);
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
}
public void removeList(@NotNull ObservableList<T> list) {
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
final int index = lists.indexOf(list);
if (index < 0) {
throw new IllegalArgumentException("Cannot remove a list that is not contained: " + list + " lists=" + lists);
}
final int startIndex = getStartIndex(list);
final int endIndex = getEndIndex(list, startIndex);
// we want to find the start index of this list inside the aggregated List. End index will be start + size - 1.
lists.remove(list);
sizes.remove(index);
final InternalListModificationListener listener = listeners.remove(index);
list.removeListener(listener);
aggregatedList.remove(startIndex, endIndex + 1); // end + 1 because end is exclusive
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
}
/**
* Get the start index of this list inside the aggregated List.
* This is a private function. we can safely asume, that the list is in the map.
*
* @param list the list in question
* @return the start index of this list in the aggregated List
*/
private int getStartIndex(@NotNull ObservableList<T> list) {
int startIndex = 0;
//System.out.println("=== searching startIndex of " + list);
assert lists.size() == sizes.size() : "lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size();
final int listIndex = lists.indexOf(list);
for (int i = 0; i < listIndex; i++) {
final Integer size = sizes.get(i);
startIndex += size;
//System.out.println(" startIndex = " + startIndex + " added=" + size);
}
//System.out.println("startIndex = " + startIndex);
return startIndex;
}
/**
* Get the end index of this list inside the aggregated List.
* This is a private function. we can safely asume, that the list is in the map.
*
* @param list the list in question
* @param startIndex the start of the list (retrieve with {@link #getStartIndex(ObservableList)}
* @return the end index of this list in the aggregated List
*/
private int getEndIndex(@NotNull ObservableList<T> list, int startIndex) {
assert lists.size() == sizes.size() : "lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size();
final int index = lists.indexOf(list);
return startIndex + sizes.get(index) - 1;
}
private class InternalListModificationListener implements ListChangeListener<T> {
@NotNull
private final ObservableList<T> list;
public InternalListModificationListener(@NotNull ObservableList<T> list) {
this.list = list;
}
/**
* Called after a change has been made to an ObservableList.
*
* @param change an object representing the change that was done
* @see Change
*/
@Override
public void onChanged(Change<? extends T> change) {
final ObservableList<? extends T> changedList = change.getList();
final int startIndex = getStartIndex(list);
final int index = lists.indexOf(list);
final int newSize = changedList.size();
//System.out.println("onChanged for list=" + list + " aggregate=" + aggregatedList);
while (change.next()) {
final int from = change.getFrom();
final int to = change.getTo();
//System.out.println(" startIndex=" + startIndex + " from=" + from + " to=" + to);
if (change.wasPermutated()) {
final ArrayList<T> copy = new ArrayList<>(aggregatedList.subList(startIndex + from, startIndex + to));
//System.out.println(" permutating sublist=" + copy);
for (int oldIndex = from; oldIndex < to; oldIndex++) {
int newIndex = change.getPermutation(oldIndex);
copy.set(newIndex - from, aggregatedList.get(startIndex + oldIndex));
}
//System.out.println(" permutating done sublist=" + copy);
aggregatedList.subList(startIndex + from, startIndex + to).clear();
aggregatedList.addAll(startIndex + from, copy);
} else if (change.wasUpdated()) {
// do nothing
} else {
if (change.wasRemoved()) {
List<? extends T> removed = change.getRemoved();
//System.out.println(" removed= " + removed);
// IMPORTANT! FROM == TO when removing items.
aggregatedList.remove(startIndex + from, startIndex + from + removed.size());
}
if (change.wasAdded()) {
List<? extends T> added = change.getAddedSubList();
//System.out.println(" added= " + added);
//add those elements to your data
aggregatedList.addAll(startIndex + from, added);
}
}
}
// update the size of the list in the map
//System.out.println("list = " + list + " puttingInMap=" + list.hashCode());
sizes.set(index, newSize);
//System.out.println("listSizesMap = " + sizes);
}
}
public String dump(Function<T, Object> function) {
StringBuilder sb = new StringBuilder();
sb.append("[");
aggregatedList.forEach(el -> sb.append(function.apply(el)).append(","));
final int length = sb.length();
sb.replace(length - 1, length, "");
sb.append("]");
return sb.toString();
}
}
jUnit Test:
/**
* Testing the AggregatedObservableArrayList
*/
public class AggregatedObservableArrayListTest {
@Test
public void testObservableValue() {
final AggregatedObservableArrayList<IntegerProperty> aggregatedWrapper = new AggregatedObservableArrayList<>();
final ObservableList<IntegerProperty> aggregatedList = aggregatedWrapper.getAggregatedList();
aggregatedList.addListener((Observable observable) -> {
System.out.println("observable = " + observable);
});
final ObservableList<IntegerProperty> list1 = FXCollections.observableArrayList();
final ObservableList<IntegerProperty> list2 = FXCollections.observableArrayList();
final ObservableList<IntegerProperty> list3 = FXCollections.observableArrayList();
list1.addAll(new SimpleIntegerProperty(1), new SimpleIntegerProperty(2), new SimpleIntegerProperty(3), new SimpleIntegerProperty(4),
new SimpleIntegerProperty(5));
list2.addAll(new SimpleIntegerProperty(10), new SimpleIntegerProperty(11), new SimpleIntegerProperty(12), new SimpleIntegerProperty(13),
new SimpleIntegerProperty(14), new SimpleIntegerProperty(15));
list3.addAll(new SimpleIntegerProperty(100), new SimpleIntegerProperty(110), new SimpleIntegerProperty(120), new SimpleIntegerProperty(130),
new SimpleIntegerProperty(140), new SimpleIntegerProperty(150));
// adding list 1 to aggregate
aggregatedWrapper.appendList(list1);
assertEquals("wrong content", "[1,2,3,4,5]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// removing elems from list1
list1.remove(2, 4);
assertEquals("wrong content", "[1,2,5]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// adding second List
aggregatedWrapper.appendList(list2);
assertEquals("wrong content", "[1,2,5,10,11,12,13,14,15]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// removing elems from second List
list2.remove(1, 3);
assertEquals("wrong content", "[1,2,5,10,13,14,15]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// replacing element in first list
list1.set(1, new SimpleIntegerProperty(3));
assertEquals("wrong content", "[1,3,5,10,13,14,15]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// adding third List
aggregatedWrapper.appendList(list3);
assertEquals("wrong content", "[1,3,5,10,13,14,15,100,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// emptying second list
list2.clear();
assertEquals("wrong content", "[1,3,5,100,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// adding new elements to second list
list2.addAll(new SimpleIntegerProperty(203), new SimpleIntegerProperty(202), new SimpleIntegerProperty(201));
assertEquals("wrong content", "[1,3,5,203,202,201,100,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// sorting list2. this results in permutation
list2.sort((o1, o2) -> o1.getValue().compareTo(o2.getValue()));
assertEquals("wrong content", "[1,3,5,201,202,203,100,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// removing list2 completely
aggregatedWrapper.removeList(list2);
assertEquals("wrong content", "[1,3,5,100,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// updating one integer value in list 3
SimpleIntegerProperty integer = (SimpleIntegerProperty) list3.get(0);
integer.set(1);
assertEquals("wrong content", "[1,3,5,1,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
// prepending list 2 again
aggregatedWrapper.prependList(list2);
assertEquals("wrong content", "[201,202,203,1,3,5,1,110,120,130,140,150]", aggregatedWrapper.dump(ObservableIntegerValue::get));
}
}