Index: src/main/resources/org/jboss/dna/graph/GraphI18n.properties
===================================================================
--- src/main/resources/org/jboss/dna/graph/GraphI18n.properties	(revision 612)
+++ src/main/resources/org/jboss/dna/graph/GraphI18n.properties	(working copy)
@@ -65,3 +65,10 @@
 errorImportingContent = Error importing {0} content from {1}
 unableToFindRepositorySourceWithName = Unable to find a repository source named "{0}"
 
+unableToReportErrorUsingFinishedTask = Task '{1}' is finished; unable to report error '{0}'
+unableToReportWarningUsingFinishedTask = Task '{1}' is finished; unable to report warning '{0}'
+unableToReportMessageUsingFinishedTask = Task '{1}' is finished; unable to report message '{0}'
+unableToStartTaskFromFinishedTask = Task '{1}' is finished; unable to start task '{0}'
+unableToStartNextTaskFromFinishedTasks = Task '{1} is finished; unable to start next task
+
+
Index: src/main/java/org/jboss/dna/graph/tasks/SerialTask.java
===================================================================
--- src/main/java/org/jboss/dna/graph/tasks/SerialTask.java	(revision 0)
+++ src/main/java/org/jboss/dna/graph/tasks/SerialTask.java	(revision 0)
@@ -0,0 +1,88 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.graph.tasks;
+
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.i18n.I18n;
+
+/**
+ * A series of {@link Task}s that can be used to signal work on each of the tasks in the series.
+ * <p>
+ * Notice that the <code>SerialTask</code> interface extends the {@link TaskMonitor} interface, meaning it can be used to report
+ * {@link TaskMonitor#reportMessage(org.jboss.dna.common.i18n.I18n, Object...) messages},
+ * {@link TaskMonitor#reportWarning(org.jboss.dna.common.i18n.I18n, Object...) warnings}, and
+ * {@link TaskMonitor#reportError(Throwable, org.jboss.dna.common.i18n.I18n, Object...) errors}, check {@link #isCancelled()
+ * cancellation}, and even {@link TaskMonitor#startTask(org.jboss.dna.common.i18n.I18n, Object...) start subtasks}.
+ * </p>
+ * <p>
+ * One way in which this interface can be used is when iterating, where a separate task is to be represented by each iteration.
+ * Here's an example:
+ * 
+ * <pre>
+ * SerialTask series = monitor.startTasks(10, message, params);
+ * for (int i = 0; i != 10; ++i) {
+ *     series.next();
+ *     // do work
+ * }
+ * series.finish();
+ * </pre>
+ * 
+ * </p>
+ * <p>
+ * Another way to use this method is when there are multiple known tasks that are different. For example, consider a case when
+ * there are three different tasks. This method can be used to create those three tasks and cycle through them:
+ * 
+ * <pre>
+ * SerialTask series = monitor.startTasks(3, nameOfTask1, params);
+ * // do work for first task
+ * series.next(nameOfTask2, params)
+ * // do work for second task
+ * series.next(nameOfTask3, params)
+ * // do work for third task
+ * series.finish();
+ * </pre>
+ * 
+ * </p>
+ * 
+ * @see TaskMonitor#startTasks(int, I18n, Object...)
+ * @author Randall Hauch
+ */
+@ThreadSafe
+public interface SerialTask extends Task {
+
+    /**
+     * Start the next task in this series, using the supplied name for the task. Any prior task in this series is finished
+     * automatically.
+     * 
+     * @param name the internationalized name of the next task
+     * @param params the parameters that will be substituted into the name during localization
+     */
+    void next( I18n name,
+               Object... params );
+
+    /**
+     * Start the next task in this series, using the same name as the previous task. Any prior task in this series is finished
+     * automatically.
+     */
+    void next();
+
+}
Index: src/main/java/org/jboss/dna/graph/tasks/Task.java
===================================================================
--- src/main/java/org/jboss/dna/graph/tasks/Task.java	(revision 0)
+++ src/main/java/org/jboss/dna/graph/tasks/Task.java	(revision 0)
@@ -0,0 +1,50 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.graph.tasks;
+
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.i18n.I18n;
+
+/**
+ * A logical and distinguishable unit of work. Tasks are started via the
+ * {@link TaskMonitor#startTask(org.jboss.dna.common.i18n.I18n, Object...) TaskMonitor}, and must be {@link #finish() finished}.
+ * <p>
+ * Notice that the <code>Task</code> interface extends the {@link TaskMonitor} interface, meaning it can be used to report
+ * {@link TaskMonitor#reportMessage(org.jboss.dna.common.i18n.I18n, Object...) messages},
+ * {@link TaskMonitor#reportWarning(org.jboss.dna.common.i18n.I18n, Object...) warnings}, and
+ * {@link TaskMonitor#reportError(Throwable, org.jboss.dna.common.i18n.I18n, Object...) errors}, check {@link #isCancelled()
+ * cancellation}, and even {@link TaskMonitor#startTask(org.jboss.dna.common.i18n.I18n, Object...) start subtasks}.
+ * </p>
+ * 
+ * @see TaskMonitor#startTask(I18n, Object...)
+ * @author Randall Hauch
+ */
+@ThreadSafe
+public interface Task extends TaskMonitor {
+
+    /**
+     * Signal that this task has been completed. This method should be called once, but calling more than that does nothing. Also,
+     * finishing a task implicitly finishes any and all subtasks.
+     */
+    void finish();
+
+}
Index: src/main/java/org/jboss/dna/graph/tasks/TaskMonitor.java
===================================================================
--- src/main/java/org/jboss/dna/graph/tasks/TaskMonitor.java	(revision 0)
+++ src/main/java/org/jboss/dna/graph/tasks/TaskMonitor.java	(revision 0)
@@ -0,0 +1,162 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.graph.tasks;
+
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.i18n.I18n;
+
+/**
+ * A task monitor is a component that is interested in monitoring the ongoing status of the important tasks that are performed
+ * within an operation. The monitor can be used to report {@link #reportMessage(I18n, Object...) messages},
+ * {@link #reportWarning(I18n, Object...) warnings}, and {@link #reportError(Throwable, I18n, Object...) errors}. The monitor can
+ * also be consulted to determine whether the operation's tasks should be {@link #isCancelled() canceled}. And, the monitor may be
+ * used to create {@link #startTask(I18n, Object...) start tasks}, which represent distinguishable units of work that the monitor
+ * may be interested in.
+ * 
+ * @author Randall Hauch
+ */
+@ThreadSafe
+public interface TaskMonitor {
+
+    /**
+     * Return whether a request has been made to cancel any current activities. In general, this should be checked periodically
+     * and the activity changed based upon the answer. However, just because a request was made to cancel does not mean the work
+     * must be immediately cancelled and not completed.
+     * 
+     * @return true if cancel has been requested, or false if no such request has been made.
+     */
+    boolean isCancelled();
+
+    /**
+     * Report a message to this monitor. The message may represent any significant milestone during the ongoing tasks or
+     * activities.
+     * 
+     * @param message the internationalized message
+     * @param params the parameters that will be substituted into the message during localization
+     */
+    void reportMessage( I18n message,
+                        Object... params );
+
+    /**
+     * Report a warning to this monitor.
+     * 
+     * @param message the internationalized message
+     * @param params the parameters that will be substituted into the message during localization
+     */
+    void reportWarning( I18n message,
+                        Object... params );
+
+    /**
+     * Report to this monitor the occurrence of an error or other problem. Use this method in cases where a problem has occurred
+     * but where throwing an exception is not appropriate or desired.
+     * <p>
+     * For example, an method may attempt to perform an operation, but based upon a condition that operation may not be
+     * appropriate. The problem may be recorded or reported using this method.
+     * </p>
+     * 
+     * @param message the internationalized message
+     * @param params the parameters that will be substituted into the message during localization
+     */
+    void reportError( I18n message,
+                      Object... params );
+
+    /**
+     * Report to this monitor the occurrence of an error or other problem. Use this method in cases where a problem has occurred
+     * but where throwing an exception is not appropriate or desired.
+     * <p>
+     * For example, an method may attempt to perform an operation, but based upon a condition that operation may not be
+     * appropriate. The problem may be recorded or reported using this method.
+     * </p>
+     * 
+     * @param t the exception; may be null
+     * @param message the internationalized message
+     * @param params the parameters that will be substituted into the message during localization
+     */
+    void reportError( Throwable t,
+                      I18n message,
+                      Object... params );
+
+    /**
+     * Start a new task with the supplied name. The resulting task should be {@link Task#finish() finished} when completed.
+     * 
+     * @param name the internationalized name of the task
+     * @param params the parameters that will be substituted into the name during localization
+     * @return the new task
+     * @see #startTasks(int,I18n, Object...)
+     * @see Task
+     */
+    Task startTask( I18n name,
+                    Object... params );
+
+    /**
+     * Start a series of new tasks, including the first one with the supplied name. The resulting Tasks object can be used to
+     * cycle through the series and should be {@link SerialTask#finish() finished} when completed. Note that it is possible to
+     * keep moving to the {@link SerialTask#next() next task} more times than supplied in the <code>numberOfTasks</code>.
+     * <p>
+     * One way in which this method can be used is when iterating, where a separate task is to be represented by each iteration.
+     * Here's an example:
+     * 
+     * <pre>
+     * Tasks series = monitor.startTasks(10, message, params);
+     * for (int i = 0; i != 10; ++i) {
+     *     series.next();
+     *     // do work
+     * }
+     * series.finish();
+     * </pre>
+     * 
+     * </p>
+     * <p>
+     * Another way to use this method is when there are multiple known tasks that are different. For example, consider a case when
+     * there are three different tasks. This method can be used to create those three tasks and cycle through them:
+     * 
+     * <pre>
+     * Tasks series = monitor.startTasks(3, nameOfTask1, params);
+     * // do work for first task
+     * series.next(nameOfTask2, params)
+     * // do work for second task
+     * series.next(nameOfTask3, params)
+     * // do work for third task
+     * series.finish();
+     * </pre>
+     * 
+     * </p>
+     * 
+     * @param numberOfTasks the estimated number of tasks in the series
+     * @param name the internationalized name of the task
+     * @param params the parameters that will be substituted into the name during localization
+     * @return the new serial task, which can be used to step through the task
+     * @see #startTask(I18n, Object...)
+     * @see SerialTask
+     */
+    SerialTask startTasks( int numberOfTasks,
+                           I18n name,
+                           Object... params );
+
+    /**
+     * Get a task monitor that is able to be forked from the current task, ensuring that finishing the current task has no bearing
+     * on the result. Use this method to pass this monitor to an asynchronous method.
+     * 
+     * @return the monitor that may be passed to an asynchronous method
+     */
+    TaskMonitor fork();
+}
Index: src/main/java/org/jboss/dna/graph/tasks/Tasks.java
===================================================================
--- src/main/java/org/jboss/dna/graph/tasks/Tasks.java	(revision 0)
+++ src/main/java/org/jboss/dna/graph/tasks/Tasks.java	(revision 0)
@@ -0,0 +1,678 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.graph.tasks;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import net.jcip.annotations.Immutable;
+import net.jcip.annotations.ThreadSafe;
+import org.jboss.dna.common.i18n.I18n;
+import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.graph.GraphI18n;
+import org.jboss.dna.graph.properties.DateTime;
+import org.jboss.dna.graph.properties.DateTimeFactory;
+
+/**
+ * Provides a single hub that acts as a {@link TaskMonitor} as well as something to which {@link Listener listeners} can be added
+ * to receive {@link Event events} about {@link Task tasks}.
+ * <p>
+ * This class is safe to be used by multiple threads.
+ * </p>
+ * 
+ * @author Randall Hauch
+ */
+@ThreadSafe
+public class Tasks implements TaskMonitor {
+
+    public enum EventType {
+        MESSAGE,
+        WARNING,
+        ERROR,
+        STARTED,
+        PROGRESS,
+        FINISHED;
+    }
+
+    @Immutable
+    public static class TaskId {
+        private final TaskId parent;
+        private final I18n name;
+        private final Object[] parameters;
+
+        /*package*/TaskId( I18n name,
+                            Object[] parameters ) {
+            assert name != null;
+            this.parent = null;
+            this.name = name;
+            this.parameters = parameters;
+        }
+
+        /*package*/TaskId( TaskId parent,
+                            I18n name,
+                            Object[] parameters ) {
+            assert name != null;
+            assert parent != null;
+            this.parent = parent;
+            this.name = name;
+            this.parameters = parameters;
+        }
+
+        public TaskId getParent() {
+            return parent;
+        }
+
+        /**
+         * Get the depth of the task.
+         * 
+         * @return the depth
+         */
+        public int getDepth() {
+            return parent != null ? parent.getDepth() + 1 : 1;
+        }
+
+        /**
+         * Get the internationalized name of the event.
+         * 
+         * @return the event's name
+         * @see #getNameParameters()
+         */
+        public I18n getName() {
+            return name;
+        }
+
+        /**
+         * Get the parameters for the {@link #getName() name}, used when {@link I18n#text(Object...) localizing} the name.
+         * 
+         * @return the name's parameters
+         * @see #getName()
+         */
+        public Object[] getNameParameters() {
+            return parameters;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.lang.Object#toString()
+         */
+        @Override
+        public String toString() {
+            return parent != null ? parent.toString() + "." + name.text(parameters) : name.text(parameters);
+        }
+    }
+
+    @Immutable
+    public static interface Event {
+        /**
+         * Get the task identifier.
+         * 
+         * @return the task's identifier
+         */
+        public TaskId getId();
+
+        /**
+         * Get the timestamp of the event
+         * 
+         * @return the timestamp; never null
+         */
+        public DateTime getTimestamp();
+
+        /**
+         * Get the type of event.
+         * 
+         * @return the event's type
+         */
+        public EventType getType();
+
+        public int getCurrentCount();
+
+        public int getTotalCount();
+
+        /**
+         * Get the exception contained by this event.
+         * 
+         * @return the exception, or null if there is no exception or if the {@link #getType() event type} is not
+         *         {@link EventType#ERROR}
+         */
+        public Throwable getException();
+    }
+
+    @Immutable
+    /*package*/class TaskEvent implements Event {
+        private final TaskId id;
+        private final DateTime timestamp;
+        private final EventType type;
+        private final Throwable error;
+
+        /*package*/TaskEvent( TaskId id,
+                               EventType type,
+                               DateTime timestamp,
+                               Throwable error ) {
+            assert id != null;
+            assert type != null;
+            assert timestamp != null;
+            this.id = id;
+            this.timestamp = timestamp;
+            this.type = type;
+            this.error = error;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see Event#getId()
+         */
+        public TaskId getId() {
+            return id;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see Event#getTimestamp()
+         */
+        public DateTime getTimestamp() {
+            return timestamp;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see Event#getType()
+         */
+        public EventType getType() {
+            return type;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see Event#getException()
+         */
+        public Throwable getException() {
+            return error;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.Tasks.Event#getCurrentCount()
+         */
+        public int getCurrentCount() {
+            return 1;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.Tasks.Event#getTotalCount()
+         */
+        public int getTotalCount() {
+            return 1;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.lang.Object#toString()
+         */
+        @Override
+        public String toString() {
+            return timestamp.getString() + " " + type + " " + id.getName().text(id.getNameParameters());
+        }
+    }
+
+    @Immutable
+    /*package*/class FractionalTaskEvent extends TaskEvent {
+        private final int current;
+        private final int total;
+
+        /*package*/FractionalTaskEvent( TaskId id,
+                                         EventType type,
+                                         DateTime timestamp,
+                                         int current,
+                                         int total,
+                                         Throwable error ) {
+            super(id, type, timestamp, error);
+            this.current = current;
+            this.total = total;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.Tasks.TaskEvent#getCurrentCount()
+         */
+        @Override
+        public int getCurrentCount() {
+            return current;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.Tasks.TaskEvent#getTotalCount()
+         */
+        @Override
+        public int getTotalCount() {
+            return total;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.Tasks.TaskEvent#toString()
+         */
+        @Override
+        public String toString() {
+            return getTimestamp() + " " + getType() + " (" + current + "/" + total + ")"
+                   + getId().getName().text(getId().getNameParameters());
+        }
+    }
+
+    /**
+     * An interface that is implemented by any component wishing to receive events regarding the tasks performed within a
+     * {@link Tasks}.
+     * 
+     * @author Randall Hauch
+     */
+    public static interface Listener {
+        void notify( Event event );
+    }
+
+    private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<Listener>();
+    private final AtomicBoolean cancelled = new AtomicBoolean(false);
+    private final DateTimeFactory timestampFactory;
+    private final TaskId rootId;
+
+    /**
+     * Create a new instance, specifying the factory for {@link DateTime} instances and the name for the root of all tasks.
+     * 
+     * @param timestampFactory the factory for {@link DateTime} instances
+     * @param name the name for the root of all tasks.
+     * @param params the parameters for the name
+     * @throws IllegalArgumentException if <code>timestampFactory</code> or <code>message</code> is null
+     */
+    public Tasks( DateTimeFactory timestampFactory,
+                  I18n name,
+                  Object... params ) {
+        CheckArg.isNotNull(timestampFactory, "timestampFactory");
+        CheckArg.isNotNull(name, "name");
+        this.timestampFactory = timestampFactory;
+        this.rootId = new TaskId(name, params);
+    }
+
+    /**
+     * Add a listener to this context.
+     * 
+     * @param listener the listener to add
+     * @return this context object, to facilitate method chaining
+     * @throws IllegalArgumentException if <code>listener</code> is null
+     * @see #removeListener(Listener)
+     */
+    public Tasks addListener( Listener listener ) {
+        CheckArg.isNotNull(listener, "listener");
+        listeners.addIfAbsent(listener);
+        return this;
+    }
+
+    /**
+     * Add a listener to this context.
+     * 
+     * @param listener the listener to add
+     * @return this context object, to facilitate method chaining
+     * @throws IllegalArgumentException if <code>listener</code> is null
+     * @see #addListener(Listener)
+     */
+    public Tasks removeListener( Listener listener ) {
+        CheckArg.isNotNull(listener, "listener");
+        listeners.remove(listener);
+        return this;
+    }
+
+    protected void fire( TaskId id,
+                         EventType type,
+                         Throwable error ) {
+        if (listeners.isEmpty()) return;
+        Event event = new TaskEvent(id, type, timestampFactory.createUtc(), error);
+        for (Listener listener : listeners) {
+            listener.notify(event);
+        }
+    }
+
+    protected void fire( TaskId id,
+                         EventType type,
+                         int current,
+                         int total,
+                         Throwable error ) {
+        if (listeners.isEmpty()) return;
+        Event event = new FractionalTaskEvent(id, type, timestampFactory.createUtc(), current, total, error);
+        for (Listener listener : listeners) {
+            listener.notify(event);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.tasks.TaskMonitor#isCancelled()
+     */
+    public boolean isCancelled() {
+        return cancelled.get();
+    }
+
+    /**
+     * Set the cancelled request state.
+     * 
+     * @param cancelled true if the current task(s) are requested to be cancelled, or false if they are to resume.
+     */
+    public void setCancelled( boolean cancelled ) {
+        this.cancelled.set(cancelled);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.tasks.TaskMonitor#reportMessage(org.jboss.dna.common.i18n.I18n, java.lang.Object[])
+     */
+    public void reportMessage( I18n message,
+                               Object... params ) {
+        fire(new TaskId(rootId, message, params), EventType.MESSAGE, null);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.tasks.TaskMonitor#reportWarning(org.jboss.dna.common.i18n.I18n, java.lang.Object[])
+     */
+    public void reportWarning( I18n message,
+                               Object... params ) {
+        fire(new TaskId(rootId, message, params), EventType.WARNING, null);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.tasks.TaskMonitor#reportError(org.jboss.dna.common.i18n.I18n, java.lang.Object[])
+     */
+    public void reportError( I18n message,
+                             Object... params ) {
+        fire(new TaskId(rootId, message, params), EventType.ERROR, null);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.tasks.TaskMonitor#reportError(java.lang.Throwable, org.jboss.dna.common.i18n.I18n,
+     *      java.lang.Object[])
+     */
+    public void reportError( Throwable error,
+                             I18n message,
+                             Object... params ) {
+        fire(new TaskId(rootId, message, params), EventType.ERROR, error);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.tasks.TaskMonitor#startTask(org.jboss.dna.common.i18n.I18n, java.lang.Object[])
+     */
+    public Task startTask( I18n name,
+                           Object... params ) {
+        return new SingleTask(new TaskId(rootId, name, params)).start();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.tasks.TaskMonitor#startTasks(int, org.jboss.dna.common.i18n.I18n, java.lang.Object[])
+     */
+    public SerialTask startTasks( int numberOfTasks,
+                                  I18n name,
+                                  Object... params ) {
+        return (MultipleTasks)new MultipleTasks(new TaskId(rootId, name, params), numberOfTasks).start();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see org.jboss.dna.graph.tasks.TaskMonitor#fork()
+     */
+    public TaskMonitor fork() {
+        return this;
+    }
+
+    @Immutable
+    /*package*/class SingleTask implements Task {
+        /*package*/TaskId id;
+        /*package*/final AtomicBoolean finished = new AtomicBoolean(false);
+        /*package*/final AtomicReference<Task> currentSubtask = new AtomicReference<Task>(null);
+
+        /*package*/SingleTask( TaskId id ) {
+            assert id != null;
+            this.id = id;
+        }
+
+        /*package*/Task start() {
+            fire(id, EventType.STARTED, null);
+            return this;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.Task#finish()
+         */
+        public void finish() {
+            if (finished.compareAndSet(false, true)) {
+                Task subtask = currentSubtask.getAndSet(null);
+                if (subtask != null) subtask.finish();
+                fireFinish();
+            }
+        }
+
+        protected void fireFinish() {
+            fire(id, EventType.FINISHED, null);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.TaskMonitor#isCancelled()
+         */
+        public boolean isCancelled() {
+            return Tasks.this.isCancelled();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.TaskMonitor#reportError(org.jboss.dna.common.i18n.I18n, java.lang.Object[])
+         */
+        public void reportError( I18n message,
+                                 Object... params ) {
+            if (finished.get()) {
+                String msg = message.text(params);
+                throw new IllegalStateException(GraphI18n.unableToReportErrorUsingFinishedTask.text(msg, id));
+            }
+            fire(new TaskId(id, message, params), EventType.ERROR, null);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.TaskMonitor#reportError(java.lang.Throwable, org.jboss.dna.common.i18n.I18n,
+         *      java.lang.Object[])
+         */
+        public void reportError( Throwable error,
+                                 I18n message,
+                                 Object... params ) {
+            if (finished.get()) {
+                String msg = message.text(params);
+                throw new IllegalStateException(GraphI18n.unableToReportErrorUsingFinishedTask.text(msg, id));
+            }
+            fire(new TaskId(id, message, params), EventType.ERROR, error);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.TaskMonitor#reportMessage(org.jboss.dna.common.i18n.I18n, java.lang.Object[])
+         */
+        public void reportMessage( I18n message,
+                                   Object... params ) {
+            if (finished.get()) {
+                String msg = message.text(params);
+                throw new IllegalStateException(GraphI18n.unableToReportMessageUsingFinishedTask.text(msg, id));
+            }
+            fire(new TaskId(id, message, params), EventType.MESSAGE, null);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.TaskMonitor#reportWarning(org.jboss.dna.common.i18n.I18n, java.lang.Object[])
+         */
+        public void reportWarning( I18n message,
+                                   Object... params ) {
+            if (finished.get()) {
+                String msg = message.text(params);
+                throw new IllegalStateException(GraphI18n.unableToReportWarningUsingFinishedTask.text(msg, id));
+            }
+            fire(new TaskId(id, message, params), EventType.WARNING, null);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.TaskMonitor#startTask(org.jboss.dna.common.i18n.I18n, java.lang.Object[])
+         */
+        public Task startTask( I18n name,
+                               Object... params ) {
+            TaskId newTask = new TaskId(id, name, params);
+            if (finished.get()) throw new IllegalStateException(GraphI18n.unableToStartTaskFromFinishedTask.text(newTask, id));
+            SingleTask newSubtask = new SingleTask(newTask);
+            Task priorSubtask = currentSubtask.getAndSet(newSubtask);
+            if (priorSubtask != null) {
+                priorSubtask.finish();
+            }
+            return newSubtask.start();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.TaskMonitor#startTasks(int, org.jboss.dna.common.i18n.I18n, java.lang.Object[])
+         */
+        public SerialTask startTasks( int numberOfTasks,
+                                      I18n name,
+                                      Object... params ) {
+            TaskId newTask = new TaskId(id, name, params);
+            if (finished.get()) throw new IllegalStateException(GraphI18n.unableToStartTaskFromFinishedTask.text(newTask, id));
+            MultipleTasks newSubtask = new MultipleTasks(newTask, numberOfTasks);
+            Task priorSubtask = currentSubtask.getAndSet(newSubtask);
+            if (priorSubtask != null) {
+                priorSubtask.finish();
+            }
+            newSubtask.start();
+            return newSubtask;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.TaskMonitor#fork()
+         */
+        public TaskMonitor fork() {
+            return Tasks.this.fork();
+        }
+
+    }
+
+    @ThreadSafe
+    /*package*/class MultipleTasks extends SingleTask implements SerialTask {
+        private int totalNumber;
+        private int currentNumber;
+
+        /*package*/MultipleTasks( TaskId id,
+                                   int totalNumber ) {
+            super(id);
+            this.totalNumber = totalNumber < 1 ? 1 : totalNumber;
+        }
+
+        /*package*/@Override
+        Task start() {
+            currentNumber = 0;
+            fire(id, EventType.STARTED, 1, totalNumber, null);
+            return this;
+        }
+
+        @Override
+        protected void fireFinish() {
+            fire(id, EventType.FINISHED, totalNumber, totalNumber, null);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.SerialTask#next()
+         */
+        public synchronized void next() {
+            if (finished.get()) throw new IllegalStateException(GraphI18n.unableToStartNextTaskFromFinishedTasks.text(id));
+            ++currentNumber;
+            if (currentNumber > totalNumber) totalNumber = currentNumber;
+            // We should not fire a PROGRESS event if this was the first time 'next' was called
+            // (since 'start' essentially started the task) ...
+            if (currentNumber > 1) {
+                fire(id, EventType.PROGRESS, currentNumber, totalNumber, null);
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.tasks.SerialTask#next(org.jboss.dna.common.i18n.I18n, java.lang.Object[])
+         */
+        public synchronized void next( I18n name,
+                                       Object... params ) {
+            if (finished.get()) throw new IllegalStateException(GraphI18n.unableToStartNextTaskFromFinishedTasks.text(id));
+            if (currentNumber == 0) {
+                // This is the first time that 'next' is called with a new name, so we need to finish the
+                // task (with the old name) that was already started ...
+                fire(id, EventType.FINISHED, 1, totalNumber, null);
+                currentNumber = 2;
+            } else {
+                // This is not the first time that 'next' is called with a new name, but we still need to
+                // finish the previous task ...
+                fire(id, EventType.FINISHED, currentNumber, totalNumber, null);
+                ++currentNumber;
+            }
+            // Make sure the current is not more than the total ...
+            if (currentNumber > totalNumber) totalNumber = currentNumber;
+            // Now start the task with the new name ...
+            id = new TaskId(id.getParent(), name, params);
+            fire(id, EventType.STARTED, currentNumber, totalNumber, null);
+        }
+
+    }
+}
Index: src/main/java/org/jboss/dna/graph/GraphI18n.java
===================================================================
--- src/main/java/org/jboss/dna/graph/GraphI18n.java	(revision 612)
+++ src/main/java/org/jboss/dna/graph/GraphI18n.java	(working copy)
@@ -78,6 +78,12 @@
     public static I18n errorImportingContent;
     public static I18n unableToFindRepositorySourceWithName;
 
+    public static I18n unableToReportErrorUsingFinishedTask;
+    public static I18n unableToReportWarningUsingFinishedTask;
+    public static I18n unableToReportMessageUsingFinishedTask;
+    public static I18n unableToStartTaskFromFinishedTask;
+    public static I18n unableToStartNextTaskFromFinishedTasks;
+
     static {
         try {
             I18n.initialize(GraphI18n.class);
Index: src/test/resources/org/jboss/dna/graph/tasks/TasksTest.properties
===================================================================
--- src/test/resources/org/jboss/dna/graph/tasks/TasksTest.properties	(revision 0)
+++ src/test/resources/org/jboss/dna/graph/tasks/TasksTest.properties	(revision 0)
@@ -0,0 +1,28 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2008, Red Hat Middleware LLC, and individual contributors
+# as indicated by the @author tags. See the copyright.txt file in the
+# distribution for a full listing of individual contributors.
+#
+# This is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of
+# the License, or (at your option) any later version.
+#
+# This software is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this software; if not, write to the Free
+# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+# 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+#
+rootMessage = Root
+message1 = message1
+message2 = message2
+message3 = message3
+messageWithOneParam = message {0}
+
+
Index: src/test/java/org/jboss/dna/graph/tasks/TaskMonitorTest.java
===================================================================
--- src/test/java/org/jboss/dna/graph/tasks/TaskMonitorTest.java	(revision 0)
+++ src/test/java/org/jboss/dna/graph/tasks/TaskMonitorTest.java	(revision 0)
@@ -0,0 +1,95 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.graph.tasks;
+
+import static org.mockito.Mockito.stub;
+import org.jboss.dna.common.i18n.I18n;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoAnnotations.Mock;
+
+/**
+ * This test really just verifies that the interfaces "work" in different situations. The unit tests don't really verify any
+ * behavior, since the interfaces don't have any behavior.
+ * 
+ * @author Randall Hauch
+ */
+public class TaskMonitorTest {
+
+    // We know these are null and are never initialized, but we really don't use them
+    private I18n message1;
+    private I18n part1;
+    private I18n part2;
+    private I18n part3;
+
+    // The remainder of the members are all mocks
+    @Mock
+    private TaskMonitor monitor;
+    @Mock
+    private Task task;
+    @Mock
+    private SerialTask serialTask;
+
+    @Before
+    public void beforeEach() {
+        MockitoAnnotations.initMocks(this);
+        stub(monitor.startTask(message1)).toReturn(task);
+        stub(monitor.startTasks(3, part1)).toReturn(serialTask);
+        stub(monitor.startTasks(10, part1)).toReturn(serialTask);
+        stub(monitor.isCancelled()).toReturn(false);
+    }
+
+    @Test
+    public void shouldCreateTask() {
+        Task task = monitor.startTask(message1);
+        task.finish();
+    }
+
+    @Test
+    public void shouldCheckIsCancelled() {
+        Task task = monitor.startTask(message1);
+        task.finish();
+    }
+
+    @Test
+    public void shouldCreateSeriesOfTasks() {
+        SerialTask tasks = monitor.startTasks(3, part1);
+        // do work 1 of 3
+        tasks.next(part2);
+        // do work 2 of 3
+        tasks.next(part3);
+        // do work 3 of 3
+        tasks.finish();
+    }
+
+    @Test
+    public void shouldCreateIterableTasks() {
+        SerialTask tasks = monitor.startTasks(10, message1);
+        for (int i = 0; i != 10; ++i) {
+            tasks.next();
+            // do work
+        }
+        tasks.finish(); // optional, since the last 'next()' technically finished the tasks
+    }
+
+}
Index: src/test/java/org/jboss/dna/graph/tasks/TasksTest.java
===================================================================
--- src/test/java/org/jboss/dna/graph/tasks/TasksTest.java	(revision 0)
+++ src/test/java/org/jboss/dna/graph/tasks/TasksTest.java	(revision 0)
@@ -0,0 +1,694 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2008, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors. 
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.dna.graph.tasks;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNot.not;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.hamcrest.core.IsSame.sameInstance;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import java.util.LinkedList;
+import org.jboss.dna.common.i18n.I18n;
+import org.jboss.dna.graph.BasicExecutionContext;
+import org.jboss.dna.graph.ExecutionContext;
+import org.jboss.dna.graph.properties.DateTimeFactory;
+import org.jboss.dna.graph.tasks.Tasks.Event;
+import org.jboss.dna.graph.tasks.Tasks.EventType;
+import org.jboss.dna.graph.tasks.Tasks.TaskId;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Randall Hauch
+ */
+public class TasksTest {
+
+    public static I18n rootMessage;
+    public static I18n message1;
+    public static I18n message2;
+    public static I18n message3;
+    public static I18n messageWithOneParam;
+
+    private Tasks tasks;
+    private ExecutionContext context;
+    private DateTimeFactory timestampFactory;
+    private Tasks.Listener listener;
+    private LinkedList<Event> events;
+
+    @Before
+    public void beforeEach() {
+        I18n.initialize(TasksTest.class);
+        context = new BasicExecutionContext();
+        timestampFactory = context.getValueFactories().getDateFactory();
+        tasks = new Tasks(timestampFactory, rootMessage);
+        events = new LinkedList<Event>();
+        listener = new Tasks.Listener() {
+            @SuppressWarnings( "synthetic-access" )
+            public void notify( Event event ) {
+                events.add(event);
+            }
+        };
+        tasks.addListener(listener);
+    }
+
+    public void assertEvent( EventType type,
+                             I18n message,
+                             Object... params ) {
+        assertEvent(type, null, message, params);
+    }
+
+    public void assertEvent( EventType type,
+                             Throwable error,
+                             I18n message,
+                             Object... params ) {
+        Event next = events.removeFirst();
+        assertThat(next, is(notNullValue()));
+        assertThat(next.getType(), is(type));
+        assertThat(next.getId().getName(), is(sameInstance(message)));
+        assertThat(next.getId().getNameParameters(), is(params));
+        assertThat(next.getTimestamp(), is(notNullValue()));
+        assertThat(next.getException(), is(error));
+        assertThat(next.getTotalCount(), is(1));
+        switch (type) {
+            case STARTED:
+                assertThat(next.getCurrentCount(), is(1));
+                break;
+            case PROGRESS:
+                fail("progress event not expected");
+                break;
+            case FINISHED:
+                assertThat(next.getCurrentCount(), is(1));
+                break;
+            case WARNING:
+                assertThat(next.getCurrentCount(), is(1));
+                break;
+            case ERROR:
+                assertThat(next.getCurrentCount(), is(1));
+                break;
+            case MESSAGE:
+                assertThat(next.getCurrentCount(), is(1));
+                break;
+        }
+    }
+
+    public void assertEvent( EventType type,
+                             int current,
+                             int total,
+                             I18n message,
+                             Object... params ) {
+        Event next = events.removeFirst();
+        assertThat(next, is(notNullValue()));
+        assertThat(next.getType(), is(type));
+        assertThat(next.getId().getName(), is(sameInstance(message)));
+        assertThat(next.getId().getNameParameters(), is(params));
+        assertThat(next.getTimestamp(), is(notNullValue()));
+        assertThat(next.getException(), is(nullValue()));
+        switch (type) {
+            case STARTED:
+                assertThat(next.getCurrentCount(), is(current));
+                assertThat(next.getTotalCount(), is(total));
+                break;
+            case PROGRESS:
+                assertThat(next.getCurrentCount(), is(current));
+                assertThat(next.getTotalCount(), is(total));
+                break;
+            case FINISHED:
+                assertThat(next.getCurrentCount(), is(current));
+                assertThat(next.getTotalCount(), is(total));
+                break;
+            case WARNING:
+                assertThat(next.getCurrentCount(), is(1));
+                assertThat(next.getTotalCount(), is(1));
+                break;
+            case ERROR:
+                assertThat(next.getCurrentCount(), is(1));
+                assertThat(next.getTotalCount(), is(1));
+                break;
+            case MESSAGE:
+                assertThat(next.getCurrentCount(), is(1));
+                assertThat(next.getTotalCount(), is(1));
+                break;
+        }
+    }
+
+    public void assertNoEvents() {
+        assertThat(events.isEmpty(), is(true));
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowNullTimestampFactoryInConstructor() {
+        timestampFactory = null;
+        tasks = new Tasks(timestampFactory, rootMessage);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowNullMessageInConstructor() {
+        rootMessage = null;
+        tasks = new Tasks(timestampFactory, rootMessage);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowAddingNullListener() {
+        tasks.addListener(null);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowRemovingNullListener() {
+        tasks.removeListener(null);
+    }
+
+    @Test
+    public void shouldNotAddListenerIfAlreadyRegistered() {
+        // Add the listener 10x
+        for (int i = 0; i != 10; ++i) {
+            tasks.addListener(listener);
+        }
+        // Still should only get one event ...
+        tasks.reportMessage(message1);
+        assertEvent(EventType.MESSAGE, message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldAllowRemovingListenerThatIsNotRegistered() {
+        // Should get the event ...
+        tasks.reportMessage(message1);
+        assertEvent(EventType.MESSAGE, message1);
+        assertNoEvents();
+
+        for (int i = 0; i != 10; ++i) {
+            tasks.removeListener(listener);
+        }
+
+        // Should no longer get the events ...
+        tasks.reportMessage(message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldRemoveListenerIfRegistered() {
+        // Should get the event ...
+        tasks.reportMessage(message1);
+        assertEvent(EventType.MESSAGE, message1);
+        assertNoEvents();
+
+        // Remove the listener ...
+        tasks.removeListener(listener);
+
+        // Should no longer get the events ...
+        tasks.reportMessage(message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldReportMessages() {
+        tasks.reportMessage(message1);
+        assertEvent(EventType.MESSAGE, message1);
+        assertNoEvents();
+
+        for (int i = 0; i != 10; ++i) {
+            tasks.reportMessage(message1);
+        }
+        for (int i = 0; i != 10; ++i) {
+            assertEvent(EventType.MESSAGE, message1);
+        }
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldReportWarnings() {
+        tasks.reportWarning(message1);
+        assertEvent(EventType.WARNING, message1);
+        assertNoEvents();
+
+        for (int i = 0; i != 10; ++i) {
+            tasks.reportWarning(message1);
+        }
+        for (int i = 0; i != 10; ++i) {
+            assertEvent(EventType.WARNING, message1);
+        }
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldReportErrors() {
+        tasks.reportError(message1);
+        assertEvent(EventType.ERROR, message1);
+        assertNoEvents();
+
+        for (int i = 0; i != 10; ++i) {
+            tasks.reportError(message1);
+        }
+        for (int i = 0; i != 10; ++i) {
+            assertEvent(EventType.ERROR, message1);
+        }
+        assertNoEvents();
+
+        Throwable error = new UnsupportedOperationException();
+        tasks.reportError(error, message1);
+        assertEvent(EventType.ERROR, error, message1);
+        assertNoEvents();
+
+        for (int i = 0; i != 10; ++i) {
+            tasks.reportError(error, message1);
+        }
+        for (int i = 0; i != 10; ++i) {
+            assertEvent(EventType.ERROR, error, message1);
+        }
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldStartAndFinishTask() {
+        Task task = tasks.startTask(message1);
+        assertThat(task, is(notNullValue()));
+        task.finish();
+        assertEvent(EventType.STARTED, message1);
+        assertEvent(EventType.FINISHED, message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldAllowStartingMultipleLevelsOfSubtask() {
+        Task task = tasks.startTask(messageWithOneParam, 1);
+        LinkedList<Task> stack = new LinkedList<Task>();
+        stack.addFirst(task);
+        for (int i = 1; i != 10; ++i) {
+            task = task.startTask(messageWithOneParam, i + 1);
+            assertThat(task, is(notNullValue()));
+            stack.addFirst(task);
+        }
+        assertThat(stack.size(), is(10));
+        // Verify the 'start' events ...
+        for (int i = 0; i != 10; ++i) {
+            assertEvent(EventType.STARTED, messageWithOneParam, i + 1);
+        }
+        assertNoEvents();
+
+        // Close them, started with the last one created (stack has last one created in the first slot) ...
+        for (Task aTask : stack) {
+            aTask.finish();
+        }
+
+        // Verify the 'finish' events ...
+        assertThat(events.size(), is(10));
+        for (int i = 10; i != 0; --i) {
+            assertEvent(EventType.FINISHED, messageWithOneParam, i);
+        }
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldFinishSubtaskWhenFinishingParentTask() {
+        Task topTask = tasks.startTask(messageWithOneParam, 1);
+        assertEvent(EventType.STARTED, messageWithOneParam, 1);
+        Task subTask = topTask.startTask(messageWithOneParam, 2);
+        assertThat(subTask, is(notNullValue()));
+        assertEvent(EventType.STARTED, messageWithOneParam, 2);
+
+        topTask.finish();
+        assertEvent(EventType.FINISHED, messageWithOneParam, 2);
+        assertEvent(EventType.FINISHED, messageWithOneParam, 1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldFinishStackOfSubtasksWhenFinishingParentTask() {
+        Task topTask = tasks.startTask(messageWithOneParam, 1);
+        Task task = topTask;
+        for (int i = 1; i != 10; ++i) {
+            task = task.startTask(messageWithOneParam, i + 1);
+            assertThat(task, is(notNullValue()));
+        }
+        // Verify the 'start' events ...
+        for (int i = 0; i != 10; ++i) {
+            assertEvent(EventType.STARTED, messageWithOneParam, i + 1);
+        }
+        assertNoEvents();
+
+        // Close the top task ...
+        topTask.finish();
+
+        // Verify all the 'finish' events ...
+        assertThat(events.size(), is(10));
+        for (int i = 10; i != 0; --i) {
+            assertEvent(EventType.FINISHED, messageWithOneParam, i);
+        }
+        assertNoEvents();
+
+    }
+
+    @Test( expected = IllegalStateException.class )
+    public void shouldNotAllowReportingMessageIfTaskIsAlreadyFinished() {
+        Task topTask = tasks.startTask(message1);
+        topTask.finish();
+        topTask.reportMessage(message1);
+    }
+
+    @Test( expected = IllegalStateException.class )
+    public void shouldNotAllowReportingWarningIfTaskIsAlreadyFinished() {
+        Task topTask = tasks.startTask(message1);
+        topTask.finish();
+        topTask.reportWarning(message1);
+    }
+
+    @Test( expected = IllegalStateException.class )
+    public void shouldNotAllowReportingErrorIfTaskIsAlreadyFinished() {
+        Task topTask = tasks.startTask(message1);
+        topTask.finish();
+        topTask.reportError(message1);
+    }
+
+    @Test( expected = IllegalStateException.class )
+    public void shouldNotAllowReportingErrorWithExceptionIfTaskIsAlreadyFinished() {
+        Task topTask = tasks.startTask(message1);
+        topTask.finish();
+        topTask.reportError(new UnsupportedOperationException(), message1);
+    }
+
+    @Test( expected = IllegalStateException.class )
+    public void shouldNotAllowStartingSubtaskIfTaskIsAlreadyFinished() {
+        Task topTask = tasks.startTask(message1);
+        topTask.finish();
+        topTask.startTask(message2);
+    }
+
+    @Test( expected = IllegalStateException.class )
+    public void shouldNotAllowStartingSubtasksIfTaskIsAlreadyFinished() {
+        SerialTask topTask = tasks.startTasks(10, message1);
+        topTask.finish();
+        topTask.startTask(message2);
+    }
+
+    @Test
+    public void shouldAllowReportingMessageIfCancelled() {
+        tasks.setCancelled(true);
+        tasks.reportMessage(message1);
+        assertEvent(EventType.MESSAGE, message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldAllowReportingWarningIfCancelled() {
+        tasks.setCancelled(true);
+        tasks.reportWarning(message1);
+        assertEvent(EventType.WARNING, message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldAllowReportingErrorIfCancelled() {
+        tasks.setCancelled(true);
+        tasks.reportError(message1);
+        assertEvent(EventType.ERROR, message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldAllowReportingErrorWithExceptionIfCancelled() {
+        Throwable t = new UnsupportedOperationException();
+        tasks.setCancelled(true);
+        tasks.reportError(t, message1);
+        assertEvent(EventType.ERROR, t, message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldNotAllowStartingSubtaskIfCancelled() {
+        tasks.setCancelled(true);
+        tasks.startTask(message1);
+        assertEvent(EventType.STARTED, message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldAllowStartingSubtasksIfCancelled() {
+        Task topTask = tasks.startTask(message1);
+        tasks.setCancelled(true);
+        topTask.startTask(message2);
+        assertEvent(EventType.STARTED, message1);
+        assertEvent(EventType.STARTED, message2);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldStartAndFinishTasksWithOneStep() {
+        SerialTask topTask = tasks.startTasks(1, message1);
+        topTask.finish();
+        assertEvent(EventType.STARTED, message1);
+        assertEvent(EventType.FINISHED, message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldStartAndFinishTasksWithMultipleSteps() {
+        // Use 5 steps
+        SerialTask topTask = tasks.startTasks(5, message1);
+        for (int i = 0; i != 5; ++i) {
+            topTask.next();
+        }
+        topTask.finish();
+
+        // Check the events ...
+        assertEvent(EventType.STARTED, 1, 5, message1);
+        assertEvent(EventType.PROGRESS, 2, 5, message1);
+        assertEvent(EventType.PROGRESS, 3, 5, message1);
+        assertEvent(EventType.PROGRESS, 4, 5, message1);
+        assertEvent(EventType.PROGRESS, 5, 5, message1);
+        assertEvent(EventType.FINISHED, 5, 5, message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldStartTasksAndAllowFinishingBeforeCyclingThroughAllSteps() {
+        // Use 5 steps
+        SerialTask topTask = tasks.startTasks(5, message1);
+        for (int i = 0; i != 3; ++i) {
+            topTask.next();
+        }
+        topTask.finish();
+
+        // Check the events ...
+        assertEvent(EventType.STARTED, 1, 5, message1);
+        assertEvent(EventType.PROGRESS, 2, 5, message1);
+        assertEvent(EventType.PROGRESS, 3, 5, message1);
+        assertEvent(EventType.FINISHED, 5, 5, message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldStartTasksAndAllowCyclingThroughExtraStepsBeforeFinishing() {
+        // Use 5 steps
+        SerialTask topTask = tasks.startTasks(5, message1);
+        for (int i = 0; i != 10; ++i) {
+            topTask.next();
+        }
+        topTask.finish();
+
+        // Check the events ...
+        assertEvent(EventType.STARTED, 1, 5, message1);
+        assertEvent(EventType.PROGRESS, 2, 5, message1);
+        assertEvent(EventType.PROGRESS, 3, 5, message1);
+        assertEvent(EventType.PROGRESS, 4, 5, message1);
+        assertEvent(EventType.PROGRESS, 5, 5, message1);
+        assertEvent(EventType.PROGRESS, 6, 6, message1);
+        assertEvent(EventType.PROGRESS, 7, 7, message1);
+        assertEvent(EventType.PROGRESS, 8, 8, message1);
+        assertEvent(EventType.PROGRESS, 9, 9, message1);
+        assertEvent(EventType.PROGRESS, 10, 10, message1);
+        assertEvent(EventType.FINISHED, 10, 10, message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldStartAndFinishTasksWithMultipleStepsUsingDifferentMessageForEachStep() {
+        // Use 5 steps
+        SerialTask topTask = tasks.startTasks(5, message1);
+        topTask.next(messageWithOneParam, 2);
+        topTask.next(messageWithOneParam, 3);
+        topTask.next(messageWithOneParam, 4);
+        topTask.next(messageWithOneParam, 5);
+        topTask.finish();
+
+        // Check the events ...
+        assertEvent(EventType.STARTED, 1, 5, message1);
+        assertEvent(EventType.FINISHED, 1, 5, message1);
+        assertEvent(EventType.STARTED, 2, 5, messageWithOneParam, 2);
+        assertEvent(EventType.FINISHED, 2, 5, messageWithOneParam, 2);
+        assertEvent(EventType.STARTED, 3, 5, messageWithOneParam, 3);
+        assertEvent(EventType.FINISHED, 3, 5, messageWithOneParam, 3);
+        assertEvent(EventType.STARTED, 4, 5, messageWithOneParam, 4);
+        assertEvent(EventType.FINISHED, 4, 5, messageWithOneParam, 4);
+        assertEvent(EventType.STARTED, 5, 5, messageWithOneParam, 5);
+        assertEvent(EventType.FINISHED, 5, 5, messageWithOneParam, 5);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldStartTasksAndAllowFinishingBeforeCyclingThroughAllStepsUsingDifferentMessageForEachStep() {
+        // Use 5 steps
+        SerialTask topTask = tasks.startTasks(5, message1);
+        topTask.next(messageWithOneParam, 2);
+        topTask.next(messageWithOneParam, 3);
+        topTask.finish();
+
+        // Check the events ...
+        assertEvent(EventType.STARTED, 1, 5, message1);
+        assertEvent(EventType.FINISHED, 1, 5, message1);
+        assertEvent(EventType.STARTED, 2, 5, messageWithOneParam, 2);
+        assertEvent(EventType.FINISHED, 2, 5, messageWithOneParam, 2);
+        assertEvent(EventType.STARTED, 3, 5, messageWithOneParam, 3);
+        assertEvent(EventType.FINISHED, 5, 5, messageWithOneParam, 3);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldStartTasksAndAllowCyclingThroughExtraStepsBeforeFinishingUsingDifferentMessageForEachStep() {
+        // Use 5 steps
+        SerialTask topTask = tasks.startTasks(5, message1);
+        topTask.next(messageWithOneParam, 2);
+        topTask.next(messageWithOneParam, 3);
+        topTask.next(messageWithOneParam, 4);
+        topTask.next(messageWithOneParam, 5);
+        // Keep calling !
+        topTask.next(messageWithOneParam, 6);
+        topTask.next(messageWithOneParam, 7);
+        topTask.next(messageWithOneParam, 8);
+        topTask.next(messageWithOneParam, 9);
+        topTask.finish();
+
+        // Check the events ...
+        assertEvent(EventType.STARTED, 1, 5, message1);
+        assertEvent(EventType.FINISHED, 1, 5, message1);
+        assertEvent(EventType.STARTED, 2, 5, messageWithOneParam, 2);
+        assertEvent(EventType.FINISHED, 2, 5, messageWithOneParam, 2);
+        assertEvent(EventType.STARTED, 3, 5, messageWithOneParam, 3);
+        assertEvent(EventType.FINISHED, 3, 5, messageWithOneParam, 3);
+        assertEvent(EventType.STARTED, 4, 5, messageWithOneParam, 4);
+        assertEvent(EventType.FINISHED, 4, 5, messageWithOneParam, 4);
+        assertEvent(EventType.STARTED, 5, 5, messageWithOneParam, 5);
+        assertEvent(EventType.FINISHED, 5, 5, messageWithOneParam, 5);
+        // The rest are extras ...
+        assertEvent(EventType.STARTED, 6, 6, messageWithOneParam, 6);
+        assertEvent(EventType.FINISHED, 6, 6, messageWithOneParam, 6);
+        assertEvent(EventType.STARTED, 7, 7, messageWithOneParam, 7);
+        assertEvent(EventType.FINISHED, 7, 7, messageWithOneParam, 7);
+        assertEvent(EventType.STARTED, 8, 8, messageWithOneParam, 8);
+        assertEvent(EventType.FINISHED, 8, 8, messageWithOneParam, 8);
+        assertEvent(EventType.STARTED, 9, 9, messageWithOneParam, 9);
+        assertEvent(EventType.FINISHED, 9, 9, messageWithOneParam, 9);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldStartTasksAndAllowMixtureOfCyclingWithAndWithoutDifferentMessagesForEachStep() {
+        // Use 5 steps
+        SerialTask topTask = tasks.startTasks(6, message1); // #1
+        topTask.next(); // #2
+        topTask.next(); // #3
+        topTask.next(messageWithOneParam, 4); // #4
+        topTask.next(); // #5
+        topTask.next(messageWithOneParam, 6); // #6
+        topTask.next(messageWithOneParam, 7); // #7
+        topTask.finish(); // #8
+
+        // Check the events ...
+        assertEvent(EventType.STARTED, 1, 6, message1); // #1
+        assertEvent(EventType.PROGRESS, 2, 6, message1); // #3
+        assertEvent(EventType.FINISHED, 2, 6, message1); // #4
+        assertEvent(EventType.STARTED, 3, 6, messageWithOneParam, 4); // #4
+        assertEvent(EventType.PROGRESS, 4, 6, messageWithOneParam, 4); // #5
+        assertEvent(EventType.FINISHED, 4, 6, messageWithOneParam, 4); // #6
+        assertEvent(EventType.STARTED, 5, 6, messageWithOneParam, 6); // #6
+        assertEvent(EventType.FINISHED, 5, 6, messageWithOneParam, 6); // #7
+        assertEvent(EventType.STARTED, 6, 6, messageWithOneParam, 7); // #7
+        assertEvent(EventType.FINISHED, 6, 6, messageWithOneParam, 7); // #8
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldUseSameIdsForSameTasks() {
+        // Use 5 steps
+        SerialTask topTask = tasks.startTasks(5, message1);
+        for (int i = 0; i != 5; ++i) {
+            topTask.next();
+        }
+        topTask.finish();
+
+        // Check the ID of each event (so that all events refer to the same task ) ...
+        TaskId id = null;
+        for (Event event : events) {
+            if (id == null) id = event.getId();
+            assertThat(event.getId(), is(sameInstance(id)));
+        }
+
+        assertEvent(EventType.STARTED, 1, 5, message1);
+        assertEvent(EventType.PROGRESS, 2, 5, message1);
+        assertEvent(EventType.PROGRESS, 3, 5, message1);
+        assertEvent(EventType.PROGRESS, 4, 5, message1);
+        assertEvent(EventType.PROGRESS, 5, 5, message1);
+        assertEvent(EventType.FINISHED, 5, 5, message1);
+        assertNoEvents();
+    }
+
+    @Test
+    public void shouldReferenceParentTasksByIds() {
+        // Create a task ...
+        Task topTask = tasks.startTask(messageWithOneParam, "top");
+        // Create a subtask ...
+        Task subtask1 = topTask.startTask(message1);
+        subtask1.finish();
+        // Create another subtask ...
+        Task subtask2 = topTask.startTask(message2);
+        subtask2.finish();
+        topTask.finish();
+
+        Event startTop = events.get(0);
+        Event startSub1 = events.get(1);
+        Event finishSub1 = events.get(2);
+        Event startSub2 = events.get(3);
+        Event finishSub2 = events.get(4);
+        Event finishTop = events.get(5);
+        assertThat(startTop.getId(), is(sameInstance(finishTop.getId())));
+        assertThat(startSub1.getId(), is(sameInstance(finishSub1.getId())));
+        assertThat(startSub2.getId(), is(sameInstance(finishSub2.getId())));
+        assertThat(startSub1.getId().getParent(), is(sameInstance(startTop.getId())));
+        assertThat(startSub2.getId().getParent(), is(sameInstance(startTop.getId())));
+        assertThat(startSub1.getId(), is(not(sameInstance(startTop.getId()))));
+        assertThat(startSub1.getId(), is(not(sameInstance(startSub2.getId()))));
+        assertThat(startTop.getId().getDepth(), is(2)); // the top task is below the 'tasks' root
+        assertThat(startSub1.getId().getDepth(), is(3));
+        assertThat(startSub2.getId().getDepth(), is(3));
+
+        // Check the events ...
+        assertEvent(EventType.STARTED, messageWithOneParam, "top");
+        assertEvent(EventType.STARTED, message1);
+        assertEvent(EventType.FINISHED, message1);
+        assertEvent(EventType.STARTED, message2);
+        assertEvent(EventType.FINISHED, message2);
+        assertEvent(EventType.FINISHED, messageWithOneParam, "top");
+        assertNoEvents();
+    }
+
+}
