package org.oddjob.structural; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.apache.log4j.Logger; import org.oddjob.FailedToStopException; import org.oddjob.Resetable; import org.oddjob.Stoppable; import org.oddjob.Structural; /** * Helper for managing child Objects. This class will track structural * changes and notify listeners. * * @author Rob Gordon */ public class ChildHelper<E> implements Structural, Iterable<E>, ChildList<E> { private static final Logger logger = Logger.getLogger(ChildHelper.class); /** Contains the child jobs. */ private final List<E> jobList = new ArrayList<E>(); /** Structural listeners. */ private final List<StructuralListener> listeners = new ArrayList<StructuralListener>(); /** Missed child actions. Allows a newly added listener to receive * outstanding event while not missing a new event that arrives * asynchronously. */ private final Set<List<ChildAction>> missed = new HashSet<List<ChildAction>>(); /** The source. */ private final Structural source; /** * Constructor. * * @param source The source used as the source of the event. */ public ChildHelper(Structural source) { this.source = source; } /** * Insert a child. * * @param index The index. * @param child The child. */ @Override public void insertChild(int index, E child) { if (child == null) { throw new NullPointerException("Attempt to add a null child."); } StructuralEvent event = null; synchronized (missed) { jobList.add(index, child); event = new StructuralEvent(source, child, index); for (List<ChildAction> missing : missed) { missing.add(new ChildAdded(event)); } } notifyChildAdded(event); } /** * Add a child to the end of the list. * * @param child The child. Must not be null. * * @return The index the child was added at. */ @Override public int addChild(E child) { if (child == null) { throw new NullPointerException("Attempt to add a null child."); } int index = -1; StructuralEvent event = null; synchronized (missed) { index = jobList.size(); jobList.add(index, child); event = new StructuralEvent(source, child, index); for (List<ChildAction> missing : missed) { missing.add(new ChildAdded(event)); } } notifyChildAdded(event); return index; } /** * Remove a child by index. This method * fires the appropriate event in accordance with the Strucutral interface. * * @param index The index of the child to remove. * @return The child removed. * * @throws IndexOutOfBoundsException If there is no child at the index. */ @Override public E removeChildAt(int index) throws IndexOutOfBoundsException { E child = null; StructuralEvent event; synchronized (missed) { child = jobList.remove(index); event = new StructuralEvent(source, child, index); for (List<ChildAction> missing : missed) { missing.add(new ChildRemoved(event)); } } notifyChildRemoved(event); return child; } /** * Remove a child. * * @param child The child to be removed. * @return The index the child was removed from. * * @throws IllegalStateException If the child is not our child. */ @Override public int removeChild(Object child) throws IllegalStateException { int index = -1; StructuralEvent event; synchronized (missed) { index = jobList.indexOf(child); if (index < 0) { throw new IllegalStateException("Failed removing child, [" + child + "] is not a child"); } jobList.remove(child); event = new StructuralEvent(source, child, index); for (List<ChildAction> missing : missed) { missing.add(new ChildRemoved(event)); } } notifyChildRemoved(event); return index; } /** * Allows a sub class to remove all children from itself. This method * fires the appropriate events in accordance with the structural interface. * <p> * This method isn't synchronized. Simultaneous * removal of children by a different thread could result in an * IndexOutOfBoundsException. * <p> */ public void removeAllChildren() { while (true) { int size = jobList.size(); if (size == 0) { break; } removeChildAt(size - 1); } } /** * Stops all the child jobs. Jobs are stopped in reverse order. */ public void stopChildren() throws FailedToStopException { Object [] children = getChildren(); FailedToStopException failed = null; for (int i = children.length - 1; i > -1; --i) { Object child = children[i]; if (child instanceof Stoppable) { try { ((Stoppable) child).stop(); } catch (FailedToStopException e) { failed = e; logger.debug("Child job [" + child + "] has failed to stop." + " Continuing to stop any other children before throwing Exception."); } catch (RuntimeException e) { failed = new FailedToStopException(child, "[" + child + "] failed to stop.", e); logger.debug("Child job [" + child + "] has thrown an Exception " + e.getMessage() + "." + " Continuing to stop any other children before throwing Exception."); } } } if (failed != null) { throw failed; } } /** * Perform a soft reset. This method propagates the soft reset message down to * all child jobs. This is a convenience method that a sub class can choose to use. */ public void softResetChildren() { Object [] children = getChildren(); for (int i = 0; i < children.length; ++i) { if (children[i] instanceof Resetable) { ((Resetable)children[i]).softReset(); } } } /** * Perform a hard reset. This method propergates the hard reset message down * to all child jobs. This is a convenience method a sub class can choose to use. */ public void hardResetChildren() { Object [] children = getChildren(); for (int i = 0; i < children.length; ++i) { if (children[i] instanceof Resetable) { ((Resetable)children[i]).hardReset(); } } } /** * Return an array of children. * * @return An array of child objects. */ public Object[] getChildren() { synchronized (missed) { return jobList.toArray(new Object[jobList.size()]); } } /** * Return an array of children. * * @return An array of child objects. */ public E[] getChildren(E[] array) { synchronized (missed) { return jobList.toArray(array); } } /** * Return a child. * * @return A child. */ public E getChildAt(int index) { synchronized (missed) { return jobList.get(index); } } /** * Return an only child. * * @return A child. */ public E getChild() { synchronized (missed) { if (jobList.size() == 0) { return null; } if (jobList.size() > 1) { throw new IllegalStateException("Can't use getChild with more than one child!"); } return jobList.get(0); } } /** * Is this child ours? * * @param child * * @return true if it is, false if it isn't. */ public boolean contains(E child) { synchronized (missed) { return jobList.contains(child); } } @Override public Iterator<E> iterator() { return new Iterator<E>() { int index; E next; @Override public boolean hasNext() { synchronized (missed) { // Work out the next index by adding one to the // position of the last child in case a child has been removed. if (next != null) { int last = jobList.indexOf(next); if (last >= 0) { index = last + 1; } } if (index < jobList.size()) { next = jobList.get(index); } else { next = null; } } return next != null; } @Override public E next() { synchronized (missed) { return next; } } @Override public void remove() { throw new UnsupportedOperationException(); } }; } /* * (non-Javadoc) * @see org.oddjob.structural.Structural#addStructuralListener(org.oddjob.structural.StructuralListener) */ public void addStructuralListener(StructuralListener listener) { List<ChildAction> ours = new ArrayList<ChildAction>(); synchronized (missed) { for (int i = 0; i < jobList.size(); ++i) { StructuralEvent event = new StructuralEvent(source, jobList.get(i), i); ours.add(new ChildAdded(event)); } missed.add(ours); } while (true) { ChildAction action = null; synchronized (missed) { if (ours.isEmpty()) { missed.remove(ours); listeners.add(listener); break; } else { action = ours.remove(0); } } if (action != null) { action.dispatch(listener); } } } /* * (non-Javadoc) * @see org.oddjob.structural.Structural#removeStructuralListener(org.oddjob.structural.StructuralListener) */ public void removeStructuralListener(StructuralListener listener) { synchronized (missed) { listeners.remove(listener); } } /** * Returns true if there are no listeners listening for * {@link StructuralEvent}s. * * @return true/false. */ public boolean isNoListeners() { synchronized (missed) { return listeners.isEmpty(); } } /** * The number of children. * * @return The number of children. */ public int size() { synchronized (missed) { return jobList.size(); } } /** * Used to record child added/removed events. */ abstract class ChildAction { protected final StructuralEvent event; ChildAction(StructuralEvent event) { this.event = event; } final void dispatch(StructuralListener listener) { try { doDispatch(listener); } catch (RuntimeException e) { logger.error("Failed dispatching " + getClass().getSimpleName() + " event to listener " + listener, e); } } abstract void doDispatch(StructuralListener listener) throws RuntimeException; } /** * Used to record a child added event. */ class ChildAdded extends ChildAction { public ChildAdded(StructuralEvent event) { super(event); } @Override void doDispatch(StructuralListener listener) { listener.childAdded(event); } } /** * Used record a child removed event. */ class ChildRemoved extends ChildAction { public ChildRemoved(StructuralEvent event) { super(event); } @Override public void doDispatch(StructuralListener listener) { listener.childRemoved(event); } } /** * Notify the listeners. * * @param event The event. */ private void notifyChildAdded(StructuralEvent event) { List<StructuralListener> copy = null; synchronized (missed) { copy = new ArrayList<StructuralListener>(listeners); } for (StructuralListener l : copy) { new ChildAdded(event).dispatch(l); } } /* * Notify the listeners. * * @param event The event. */ private void notifyChildRemoved(StructuralEvent event) { List<StructuralListener> copy = null; synchronized (missed) { copy = new ArrayList<StructuralListener>(listeners); } for (StructuralListener l : copy) { new ChildRemoved(event).dispatch(l); } } public static Object[] getChildren(Structural structural) { class ChildCatcher implements StructuralListener { List<Object> results = new ArrayList<Object>(); public void childAdded(StructuralEvent event) { synchronized (results) { results.add(event.getIndex(), event.getChild()); } } public void childRemoved(StructuralEvent event) { synchronized (results) { results.remove(event.getIndex()); } } } ChildCatcher cc = new ChildCatcher(); structural.addStructuralListener(cc); structural.removeStructuralListener(cc); return cc.results.toArray(); } @Override public String toString() { return getClass().getSimpleName() + " for " + source; } }