ObservableListAggregation.java

// Copyright (C) 2017 Benoît Moreau (ben.12)
// 
// This software may be modified and distributed under the terms
// of the MIT license.  See the LICENSE file for details.
package com.ben12.infxnity.collections;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableListBase;

/**
 * <p>
 * An {@link ObservableList} aggregating other {@link ObservableList}(s).
 * </p>
 * <p>
 * All events of aggregated {@link ObservableList}(s) are forwarded.
 * </p>
 * <p>
 * The list of aggregated {@link ObservableList}(s) can be modified to add, remove or replace one or more of the aggregated {@link ObservableList}(s).
 * An appropriate event will be fired.
 * </p>
 * <p>
 * An instance of {@link ObservableListAggregation} is a read only list. Any attempt to modify the list will throw an {@link UnsupportedOperationException}.
 * </p>
 * 
 * @author Benoît Moreau (ben.12)
 */
public class ObservableListAggregation<E> extends ObservableListBase<E>
{
    private final ObservableList<ObservableList<? extends E>> lists = FXCollections.observableArrayList();

    private ListChangeListener<E>                             listener;

    /**
     * Construct an empty list.
     */
    public ObservableListAggregation()
    {
        this(Collections.emptyList());
    }

    /**
     * Construct an aggregate list.
     * 
     * @param pLists
     *            list of {@link ObservableList}s to aggregate
     */
    @SuppressWarnings("unchecked")
    public ObservableListAggregation(final List<ObservableList<? extends E>> pLists)
    {
        this(pLists.toArray(new ObservableList[pLists.size()]));
    }

    /**
     * Construct an aggregate list.
     * 
     * @param pLists
     *            {@link ObservableList}s to aggregate
     */
    @SafeVarargs
    public ObservableListAggregation(final ObservableList<? extends E>... pLists)
    {
        lists.setAll(pLists);

        listener = c -> {
            final ObservableList<? extends E> source = c.getList();
            final int listIndex = lists.indexOf(source);

            beginChange();
            while (c.next())
            {
                final int offset = lists.stream().limit(listIndex).mapToInt(List::size).sum();
                final int from = offset + c.getFrom();
                final int to = offset + c.getTo();

                if (c.wasPermutated())
                {
                    final int[] perm = new int[to - from];
                    for (int i = 0; i < perm.length; i++)
                    {
                        perm[i] = offset + c.getPermutation(i);
                    }
                    nextPermutation(from, to, perm);
                }
                else
                {
                    if (c.wasUpdated())
                    {
                        for (int i = from; i < to; i++)
                        {
                            nextUpdate(i);
                        }
                    }
                    if (c.wasRemoved())
                    {
                        nextRemove(from, c.getRemoved());
                    }
                    if (c.wasAdded())
                    {
                        nextAdd(from, to);
                    }
                }
            }
            endChange();
        };
        lists.forEach(l -> l.addListener(listener));

        lists.addListener((ListChangeListener<ObservableList<? extends E>>) c -> {
            beginChange();
            while (c.next())
            {
                final int from = c.getList().stream().limit(c.getFrom()).mapToInt(List::size).sum();

                if (c.wasRemoved())
                {
                    c.getRemoved().forEach(l -> l.removeListener(listener));
                    nextRemove(from, c.getRemoved().stream().flatMap(List::stream).collect(Collectors.toList()));
                }
                if (c.wasAdded())
                {
                    c.getAddedSubList().forEach(l -> l.addListener(listener));
                    nextAdd(from, from + c.getAddedSubList().stream().mapToInt(List::size).sum());
                }
            }
            endChange();
        });
    }

    /**
     * <p>
     * Returns the {@link ObservableList} of aggregated {@link ObservableList}s.
     * </p>
     * <p>
     * The returned {@link ObservableList} can be modified.
     * </p>
     * 
     * @return the {@link ObservableList} of aggregated {@link ObservableList}s.
     */
    public ObservableList<ObservableList<? extends E>> getLists()
    {
        return lists;
    }

    @Override
    public E get(final int index)
    {
        if (index < 0)
        {
            throw new IndexOutOfBoundsException();
        }

        return lists.stream() //
                    .flatMap(List::stream)
                    .skip(index)
                    .findFirst()
                    .orElseThrow(IndexOutOfBoundsException::new);
    }

    @Override
    public int size()
    {
        return lists.stream() //
                    .mapToInt(List::size)
                    .sum();
    }
}