package net.sf.openrocket.util;
import java.util.Iterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A list of listeners of a specific type. This class contains various utility,
* safety and debugging methods for handling listeners.
* <p>
* Note that unlike normal listener implementations, this list does NOT allow the
* exact same listener (equality using ==) twice. While adding a listener twice to
* a event source would in principle be valid, in practice it's most likely a bug.
* For example the Swing implementation Sun JRE contains such bugs.
*
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
* @param <T> the type of the listeners.
*/
public class ListenerList<T> implements Invalidatable, Iterable<T> {
private static final Logger log = LoggerFactory.getLogger(ListenerList.class);
private final ArrayList<ListenerData<T>> listeners = new ArrayList<ListenerData<T>>();
private final Throwable instantiationLocation;
private Throwable invalidated = null;
/**
* Sole contructor.
*/
public ListenerList() {
this.instantiationLocation = new Throwable();
}
/**
* Adds the specified listener to this list. The listener is not added if it
* already is in the list (checked by the equality operator ==). This method throws
* a BugException if {@link #invalidate()} has been called.
*
* @param listener the listener to add.
* @return whether the listeners was actually added to the list.
* @throws BugException if this listener list has been invalidated.
*/
public boolean addListener(T listener) {
checkState(true);
ListenerData<T> data = new ListenerData<T>(listener);
if (listeners.contains(data)) {
log.warn("Attempting to add duplicate listener " + listener);
return false;
}
listeners.add(data);
return true;
}
/**
* Remove the specified listener from the list. The listener is removed based on the
* quality operator ==, not by the equals() method.
*
* @param listener the listener to remove.
* @return whether the listener was actually removed.
*/
public boolean removeListener(T listener) {
checkState(false);
Iterator<ListenerData<T>> iterator = listeners.iterator();
while (iterator.hasNext()) {
if (iterator.next().listener == listener) {
iterator.remove();
log.trace("Removing listener " + listener);
return true;
}
}
log.info("Attempting to remove non-existant listener " + listener);
return false;
}
/**
* Return the number of listeners in this list.
*/
public int getListenerCount() {
return listeners.size();
}
/**
* Return an iterator that iterates of the listeners. This iterator is backed by
* a copy of the iterator list, so {@link #addListener(Object)} and {@link #removeListener(Object)}
* may be called while iterating the list without effect on the iteration. The returned
* iterator does not support the {@link Iterator#remove()} method.
*/
@Override
public Iterator<T> iterator() {
checkState(false);
return new ListenerDataIterator();
}
/**
* Return the instantiation location of this listener list.
* @return the location where this listener list was instantiated.
*/
public Throwable getInstantiationLocation() {
return instantiationLocation;
}
/**
* Invalidate this listener list. Invalidation removes all listeners from the list.
* After invalidation {@link #addListener(Object)} will throw an exception, the other
* methods produce a warning log message.
*/
@Override
public void invalidate() {
this.invalidated = new Throwable("Invalidation occurred at this point");
if (!listeners.isEmpty()) {
log.info("Invalidating " + this + " while still having listeners " + listeners);
}
listeners.clear();
}
public boolean isInvalidated() {
return this.invalidated != null;
}
private void checkState(boolean error) {
if (this.invalidated != null) {
if (error) {
throw new BugException(this + ": this ListenerList has been invalidated", invalidated);
} else {
log.warn(this + ": this ListenerList has been invalidated",
new Throwable("ListenerList was attempted to be used here", invalidated));
}
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("ListenerList[");
if (this.invalidated != null) {
sb.append("INVALIDATED]");
return sb.toString();
}
if (listeners.isEmpty()) {
sb.append("empty");
} else {
boolean first = true;
for (ListenerData<T> l : listeners) {
if (!first) {
sb.append("; ");
}
first = false;
sb.append(l);
}
}
sb.append("]");
return sb.toString();
}
/**
* A class containing data about a listener.
*
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
* @param <T> the listener type
*/
public static class ListenerData<T> {
private final T listener;
private final long addTimestamp;
private final Throwable addLocation;
private long accessTimestamp;
/**
* Sole constructor.
*/
private ListenerData(T listener) {
if (listener == null) {
throw new NullPointerException("listener is null");
}
this.listener = listener;
this.addTimestamp = System.currentTimeMillis();
this.accessTimestamp = this.addTimestamp;
this.addLocation = new Throwable("Listener " + listener + " add position");
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof ListenerData))
return false;
ListenerData<?> other = (ListenerData<?>) obj;
return this.listener == other.listener;
}
@Override
public int hashCode() {
return listener.hashCode();
}
/**
* Return the listener.
*/
public T getListener() {
return listener;
}
/**
* Return the millisecond timestamp when this listener was added to the
* listener list.
*/
public long getAddTimestamp() {
return addTimestamp;
}
/**
* Return the location where this listener was added to the listener list.
*/
public Throwable getAddLocation() {
return addLocation;
}
/**
* Return the millisecond timestamp when this listener was last accessed through
* the listener list iterator.
*/
public long getAccessTimestamp() {
return accessTimestamp;
}
}
private class ListenerDataIterator implements Iterator<T> {
private final Iterator<ListenerData<T>> iterator = listeners.clone().iterator();
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public T next() {
ListenerData<T> data = iterator.next();
data.accessTimestamp = System.currentTimeMillis();
return data.listener;
}
@Override
public void remove() {
throw new UnsupportedOperationException("Remove not supported");
}
}
}