package org.rr.commons.collection; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; /** * Wrapps an {@link ArrayList} and provides a cursor which allows to navigate on the {@link ArrayList}. The * {@link CursableCollection} is synchronized and can be used by multiple threads. */ public class CursableCollection<E,A> implements Collection<E> { private final ListenerList<CursableCollectionListener<E, A>> listeners = new ListenerList<CursableCollectionListener<E, A>>(); private ArrayList<E> list; private ArrayList<A> attachment; private int cursor; private int maxCapacity; public CursableCollection() { this(10, -1); } public CursableCollection(int initialCapacity, int maxCapacity) { this.list = new ArrayList<>(initialCapacity); this.attachment = new ArrayList<>(initialCapacity); this.maxCapacity = maxCapacity; } /** * Adds a cursor listener to this {@link CursableCollection} instance. * @param listener The listener to be added. */ public void addCursorListener(CursableCollectionListener<E, A> listener) { listeners.removeListener(listener); //no duplicate listeners listeners.addListener(listener); } /** * Removes a previously added listener from this {@link CursableCollection} instance. * @param listener The listener to be removed. */ public void removeCursorListener(CursableCollectionListener<E, A> listener) { listeners.removeListener(listener); } /** * Should be always invoked after the cursor points to a new element. */ private void fireAfterCursorChanged() { final Iterator<CursableCollectionListener<E, A>> listenersIterator = listeners.iterator(); while (listenersIterator.hasNext()) { final CursableCollectionListener<E, A> eventListener = listenersIterator.next(); eventListener.afterCursorChanged(this); } } /** * Should be always invoked before the cursor points to a new element. */ private void fireBeforeCursorChanged() { final Iterator<CursableCollectionListener<E, A>> listenersIterator = listeners.iterator(); while (listenersIterator.hasNext()) { final CursableCollectionListener<E, A> eventListener = listenersIterator.next(); eventListener.beforeCursorChanged(this); } } /** * Prepends an element to the collection. The cursor is incremented by one. The cursor * points to the same collection element as before. * @param e The element to be prepended. */ public synchronized void prepend(E e, A attachment) { list.add(0, e); this.attachment.add(0, attachment); cursor ++; } public synchronized void prepend(E e) { this.prepend(e, null); } /** * Appends an element at the cursor location and removes all * elements behind the cursor location. The cursor is placed * to the new appended element. * * @param element The element to be added. * @param attachment The object to be bind to the element. */ public synchronized boolean append(E element, A attachment) { for (int i = cursor+1; i < list.size(); i++) { list.remove(i); this.attachment.remove(i); } this.fireBeforeCursorChanged(); final boolean succses = this.add(element, attachment); cursor = list.size()-1; this.fireAfterCursorChanged(); return succses; } public synchronized boolean append(E element) { return this.append(element, null); } /** * Gets the attached value for the given element. * @param element The element having an attached value * @return The attached value or <code>null</code> if there is no attached value present. */ public synchronized A getAttachment(E element) { for (int i = 0; i < this.list.size(); i++) { if(this.list.get(i)==element) { return this.attachment.get(i); } } return null; } /** * Gets the cursor location on the list. * @return The cursor location. */ public int getCursorLocation() { return cursor; } /** * Moves the cursor to the given index. * @param index The index where the cursor should be moved to. */ public void moveCursorTo(int index) { if(list.size() <= index+1) { throw new IndexOutOfBoundsException("could not set index to " + index + ". Element count is " + list.size()); } else if (index < 0) { throw new IndexOutOfBoundsException("could not set index to " + index); } this.fireBeforeCursorChanged(); this.cursor = index; this.fireAfterCursorChanged(); } /** * Moves the cursor to the next element and returns the element. * @return The next element. */ public synchronized E next() { if(!hasNext()) { return null; } this.fireBeforeCursorChanged(); cursor++; this.fireAfterCursorChanged(); return list.get(cursor); } /** * Tells if there is an element behind the cursor. * @return <code>true</code> if there is an element or <code>false</code> otherwise. */ public synchronized boolean hasNext() { if(list.size() <= cursor+1) { return false; } return true; } /** * Moves the cursor to the previous element and returns the element. * @return The next element. */ public synchronized E previous() { if(!hasPrevious()) { return null; } this.fireBeforeCursorChanged(); cursor--; this.fireAfterCursorChanged(); return list.get(cursor); } /** * Tells if there is an element before the cursor. * @return <code>true</code> if there is an element or <code>false</code> otherwise. */ public synchronized boolean hasPrevious() { if(cursor == 0) { return false; } return true; } /** * Gets the element where the cursor is currently placed at. * @return The current element. If there is no element, <code>null</code> is returned. */ public synchronized E current() { if(this.isEmpty()) { this.cursor = 0; return null; } return list.get(this.cursor); } /** * Removes the given element. If the element to be removed is before or at the * cursor, the cursor will moves down by one. */ @Override public synchronized boolean remove(Object o) { for (int i = 0; i < list.size(); i++) { E e = list.get(i); if(e==o) { boolean fireBefore = false; if(i<=cursor) { if(cursor-1 < 0) { fireBeforeCursorChanged(); fireBefore = true; } else { cursor--; } } final boolean succsess = this.list.remove(i)!=null; if(succsess) { this.attachment.remove(i); } if(succsess && fireBefore) { fireAfterCursorChanged(); } return succsess; } } return false; } @Override public synchronized boolean add(E e) { return this.add(e, null); } /** * Adds an element at the end of the collection but did not moves the cursor. */ public synchronized boolean add(E e, A attachment) { final boolean succsess = this.list.add(e); if(succsess) { this.attachment.add(attachment); } if(maxCapacity!=-1 && this.list.size() > maxCapacity) { this.remove(this.list.get(0)); } return succsess; } /** * Adds all given elements at the end of the collection but did not moves the cursor. */ @Override public synchronized boolean addAll(Collection<? extends E> c) { boolean succsess = this.list.addAll(c); for (int i = 0; i < c.size(); i++) { this.attachment.add(null); } return succsess; } /** * Clears the collection and resets the cursor. */ @Override public synchronized void clear() { this.fireBeforeCursorChanged(); this.list.clear(); this.attachment.clear(); this.cursor = 0; this.fireAfterCursorChanged(); } @Override public boolean contains(Object o) { return this.list.contains(o); } @Override public boolean containsAll(Collection<?> c) { return this.list.containsAll(c); } @Override public synchronized boolean isEmpty() { return this.list.isEmpty(); } @Override public Iterator<E> iterator() { return this.list.iterator(); } @Override public boolean removeAll(Collection<?> c) { throw new UnsupportedOperationException("not implemented yet"); } @Override public boolean retainAll(Collection<?> c) { throw new UnsupportedOperationException("not implemented yet"); } @Override public synchronized int size() { return this.list.size(); } @Override public Object[] toArray() { return this.list.toArray(); } @Override public <T> T[] toArray(T[] a) { return this.list.toArray(a); } }