MappedListDecorator.java
package org.thewonderlemming.c4plantuml.graphml.model;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* A decorator to the {@link List} interface, that relies on an internal {@link Map} to track existing data by the key
* {@code K}.
* <p>
* The "raison d'ĂȘtre" of that class is to simplify the JAXB data model by putting properties that appear to be lists
* and managing the add/replace feature internally and at the same location.
*
* @author thewonderlemming
*
* @param <M> the type of data to manage.
* @param <K> the type of the data key.
*/
public class MappedListDecorator<M, K> extends ArrayList<M> implements List<M> {
private static final long serialVersionUID = -7298751381485516154L;
/**
* The lookup map that maps the data to its key. It is protected for testing purposes.
*/
protected final transient Map<K, M> dataLookup = new HashMap<>();
private final transient List<M> decoratedList;
private final transient Function<M, K> getModelId;
private final Class<M> modelType;
/**
* Default constructor.
*
* @param modelType the type of the model to manage.
* @param model the decorated model list.
* @param getModelId a function that provides a model key of type {@code K} given a model of type {@code M}.
*/
public MappedListDecorator(final Class<M> modelType, final List<M> model,
final Function<M, K> getModelId) {
this.decoratedList = model;
this.modelType = modelType;
this.getModelId = getModelId;
this.dataLookup
.putAll(
model
.stream()
.map(d -> new SimpleEntry<K, M>(getModelId.apply(d), d))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue)));
}
/**
* {@inheritDoc}
*/
@Override
public void add(final int index, final M element) {
this.decoratedList.add(index, element);
this.dataLookup.put(this.getModelId.apply(element), element);
}
/**
* {@inheritDoc}
*/
@Override
public boolean add(final M e) {
final boolean result = this.decoratedList.add(e);
if (result) {
this.dataLookup.put(this.getModelId.apply(e), e);
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public boolean addAll(final Collection<? extends M> c) {
final boolean result = this.decoratedList.addAll(c);
if (result) {
c.forEach(item -> {
final K key = this.getModelId.apply(item);
this.dataLookup.put(key, item);
});
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public boolean addAll(final int index, final Collection<? extends M> c) {
final boolean result = this.decoratedList.addAll(index, c);
if (result) {
c.forEach(item -> {
final K key = this.getModelId.apply(item);
this.dataLookup.put(key, item);
});
}
return result;
}
/**
* Adds or replaces the given {@code model}, given that its key {@code K} is not yet registered in the current
* mapping or not.
*
* @param model the model to add or update.
*/
public void addOrReplaceData(final M model) {
final K key = this.getModelId.apply(model);
if (this.dataLookup.containsKey(key)) {
this.decoratedList.remove(this.dataLookup.get(key));
this.dataLookup.remove(key);
}
add(model);
}
/**
* {@inheritDoc}
*/
@Override
public void clear() {
this.decoratedList.clear();
this.dataLookup.clear();
}
/**
* {@inheritDoc}
*/
@Override
public boolean contains(final Object o) {
return this.decoratedList.contains(o);
}
/**
* {@inheritDoc}
*/
@Override
public boolean containsAll(final Collection<?> c) {
return this.decoratedList.containsAll(c);
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(final Object o) {
return this.decoratedList.equals(o);
}
/**
* {@inheritDoc}
*/
@Override
public M get(final int index) {
return this.decoratedList.get(index);
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return this.decoratedList.hashCode();
}
/**
* {@inheritDoc}
*/
@Override
public int indexOf(final Object o) {
return this.decoratedList.indexOf(o);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isEmpty() {
return this.decoratedList.isEmpty();
}
/**
* {@inheritDoc}
*/
@Override
public Iterator<M> iterator() {
return this.decoratedList.iterator();
}
/**
* {@inheritDoc}
*/
@Override
public int lastIndexOf(final Object o) {
return this.decoratedList.lastIndexOf(o);
}
/**
* {@inheritDoc}
*/
@Override
public ListIterator<M> listIterator() {
return this.decoratedList.listIterator();
}
/**
* {@inheritDoc}
*/
@Override
public ListIterator<M> listIterator(final int index) {
return this.decoratedList.listIterator(index);
}
/**
* {@inheritDoc}
*/
@Override
public M remove(final int index) {
final M results = this.decoratedList.remove(index);
final K key = this.getModelId.apply(results);
this.dataLookup.remove(key);
return results;
}
/**
* {@inheritDoc}
*/
@Override
public boolean remove(final Object o) {
final boolean results = this.decoratedList.remove(o);
if (results && this.modelType.isAssignableFrom(o.getClass())) {
@SuppressWarnings("unchecked")
final K key = this.getModelId.apply((M) o);
this.dataLookup.remove(key);
}
return results;
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public boolean removeAll(final Collection<?> c) {
final boolean results = this.decoratedList.removeAll(c);
if (results) {
c
.stream()
.filter(item -> this.modelType.isAssignableFrom(item.getClass()))
.map(item -> (M) item)
.forEach(model -> {
final K key = this.getModelId.apply(model);
this.dataLookup.remove(key);
});
}
return results;
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public boolean retainAll(final Collection<?> c) {
final boolean results = this.decoratedList.retainAll(c);
if (results) {
final Set<K> keys = c
.stream()
.filter(item -> this.modelType.isAssignableFrom(item.getClass()))
.map(item -> (M) item)
.map(this.getModelId::apply)
.collect(Collectors.toSet());
final Set<K> toBeRemoved = this.dataLookup
.keySet()
.stream()
.filter(key -> !keys.contains(key))
.collect(Collectors.toSet());
toBeRemoved.forEach(this.dataLookup::remove);
}
return results;
}
/**
* {@inheritDoc}
*/
@Override
public M set(final int index, final M element) {
final M results = this.decoratedList.set(index, element);
final K key = this.getModelId.apply(element);
this.dataLookup.put(key, element);
return results;
}
/**
* {@inheritDoc}
*/
@Override
public int size() {
return this.decoratedList.size();
}
/**
* {@inheritDoc}
*/
@Override
public List<M> subList(final int fromIndex, final int toIndex) {
return this.decoratedList.subList(fromIndex, toIndex);
}
/**
* {@inheritDoc}
*/
@Override
public Object[] toArray() {
return this.decoratedList.toArray();
}
/**
* {@inheritDoc}
*/
@Override
public <T> T[] toArray(final T[] a) {
return this.decoratedList.toArray(a);
}
}