Index: src/main/java/org/jboss/dna/jcr/JcrWorkspace.java
===================================================================
--- src/main/java/org/jboss/dna/jcr/JcrWorkspace.java	(revision 1377)
+++ src/main/java/org/jboss/dna/jcr/JcrWorkspace.java	(working copy)
@@ -38,7 +38,6 @@
 import javax.jcr.PathNotFoundException;
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
-import javax.jcr.UnsupportedRepositoryOperationException;
 import javax.jcr.Workspace;
 import javax.jcr.lock.Lock;
 import javax.jcr.lock.LockException;
@@ -131,6 +130,11 @@
      * Reference to the JCR query manager for this workspace.
      */
     private final JcrQueryManager queryManager;
+    
+    /**
+     * Reference to the JCR observation manager for this workspace.
+     */
+    private final JcrObservationManager observationManager;
 
     private final WorkspaceLockManager lockManager;
 
@@ -166,6 +170,7 @@
         // This must be initialized after the session
         this.nodeTypeManager = new JcrNodeTypeManager(session, this.repository.getRepositoryTypeManager());
         this.queryManager = new JcrQueryManager(this.session);
+        this.observationManager = new JcrObservationManager(this.session, this.repository.getRepositoryObservable());
 
         // if (Boolean.valueOf(repository.getOptions().get(Option.PROJECT_NODE_TYPES))) {
         // Path parentOfTypeNodes = context.getValueFactories().getPathFactory().create(systemPath, JcrLexicon.NODE_TYPES);
@@ -197,6 +202,10 @@
         return this.lockManager;
     }
 
+    final JcrObservationManager observationManager() {
+        return this.observationManager;
+    }
+    
     /**
      * {@inheritDoc}
      */
@@ -252,9 +261,11 @@
 
     /**
      * {@inheritDoc}
+     *
+     * @see javax.jcr.Workspace#getObservationManager()
      */
-    public final ObservationManager getObservationManager() throws UnsupportedRepositoryOperationException {
-        throw new UnsupportedRepositoryOperationException();
+    public final ObservationManager getObservationManager() {
+        return this.observationManager;
     }
 
     /**
Index: src/main/java/org/jboss/dna/jcr/JcrEngine.java
===================================================================
--- src/main/java/org/jboss/dna/jcr/JcrEngine.java	(revision 1377)
+++ src/main/java/org/jboss/dna/jcr/JcrEngine.java	(working copy)
@@ -163,7 +163,8 @@
         String sourceName = context.getValueFactories().getStringFactory().create(property.getFirstValue());
 
         // Create the repository ...
-        JcrRepository repository = new JcrRepository(context, connectionFactory, sourceName, descriptors, options);
+        JcrRepository repository = new JcrRepository(context, connectionFactory, sourceName,
+                                                     getRepositoryService().getRepositoryLibrary(), descriptors, options);
 
         // Register all the the node types ...
         Node nodeTypesNode = subgraph.getNode(JcrLexicon.NODE_TYPES);
Index: src/main/java/org/jboss/dna/jcr/JcrNodeTypeManager.java
===================================================================
--- src/main/java/org/jboss/dna/jcr/JcrNodeTypeManager.java	(revision 1377)
+++ src/main/java/org/jboss/dna/jcr/JcrNodeTypeManager.java	(working copy)
@@ -39,6 +39,7 @@
 import javax.jcr.nodetype.NodeTypeManager;
 import javax.jcr.nodetype.PropertyDefinition;
 import net.jcip.annotations.Immutable;
+import org.jboss.dna.common.util.CheckArg;
 import org.jboss.dna.graph.ExecutionContext;
 import org.jboss.dna.graph.property.Name;
 import org.jboss.dna.graph.property.NameFactory;
@@ -538,4 +539,49 @@
     public PropertyDefinitionTemplate createPropertyDefinitionTemplate() throws RepositoryException {
         return new JcrPropertyDefinitionTemplate(context());
     }
+
+    /**
+     * Determine whether the primary type or mixins are directly or indirectly derived from a node type with one of the supplied
+     * names.
+     * 
+     * @param primaryTypeName the primary type being checked (never <code>null</code>)
+     * @param mixinNames the mixins being checked (may be <code>null</code>)
+     * @param superTypeNames the names of the node types the primary type and mixins are tested against (never <code>null</code>)
+     * @return <code>true</code> if the primary type or one of the mixins are derived from one of the type names
+     * @throws RepositoryException if there is an exception obtaining node types
+     * @throws IllegalArgumentException if <code>primaryTypeProperty</code> is <code>null</code>
+     */
+    public boolean isDerivedFrom( String primaryTypeName,
+                                  String[] mixinNames,
+                                  String[] superTypeNames ) throws RepositoryException {
+        CheckArg.isNotNull(primaryTypeName, "primaryTypeName");
+        CheckArg.isNotNull(superTypeNames, "superTypeNames");
+
+        NameFactory nameFactory = context().getValueFactories().getNameFactory();
+        Name[] typeNames = nameFactory.create(superTypeNames);
+
+        // first check primary type
+        JcrNodeType primaryType = getNodeType(primaryTypeName);
+
+        for (Name typeName : typeNames) {
+            if (primaryType.isNodeType(typeName)) {
+                return true;
+            }
+        }
+
+        // now check mixins
+        if (mixinNames != null) {
+            for (String mixin : mixinNames) {
+                JcrNodeType mixinType = getNodeType(mixin);
+
+                for (Name typeName : typeNames) {
+                    if (mixinType.isNodeType(typeName)) {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
 }
Index: src/main/java/org/jboss/dna/jcr/JcrObservationManager.java
===================================================================
--- src/main/java/org/jboss/dna/jcr/JcrObservationManager.java	(revision 0)
+++ src/main/java/org/jboss/dna/jcr/JcrObservationManager.java	(revision 0)
@@ -0,0 +1,804 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you 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.
+ * 
+ * JBoss DNA 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.jcr;
+
+import static org.jboss.dna.graph.JcrLexicon.MIXIN_TYPES;
+import static org.jboss.dna.graph.JcrLexicon.PRIMARY_TYPE;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import javax.jcr.RangeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.EventListenerIterator;
+import javax.jcr.observation.ObservationManager;
+import net.jcip.annotations.GuardedBy;
+import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.Logger;
+import org.jboss.dna.graph.Graph;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.observe.Changes;
+import org.jboss.dna.graph.observe.NetChangeObserver;
+import org.jboss.dna.graph.observe.Observable;
+import org.jboss.dna.graph.property.Name;
+import org.jboss.dna.graph.property.NamespaceRegistry;
+import org.jboss.dna.graph.property.Path;
+import org.jboss.dna.graph.property.PathFactory;
+import org.jboss.dna.graph.property.Property;
+import org.jboss.dna.graph.property.UuidFactory;
+import org.jboss.dna.graph.property.ValueFactories;
+import org.jboss.dna.graph.property.ValueFactory;
+import org.jboss.dna.graph.property.ValueFormatException;
+import org.jboss.dna.graph.request.ChangeRequest;
+import org.jboss.dna.graph.session.InvalidStateException;
+
+/**
+ * The implementation of JCR {@link ObservationManager}.
+ */
+final class JcrObservationManager implements ObservationManager {
+
+    /**
+     * The repository observable the JCR listeners will be registered with.
+     */
+    private final Observable repositoryObservable;
+
+    /**
+     * The map of the JCR repository listeners and their associated wrapped class.
+     */
+    private final Map<EventListener, JcrListener> listeners;
+
+    /**
+     * The session's namespace registry used when handling events.
+     */
+    private final NamespaceRegistry namespaceRegistry;
+
+    /**
+     * The associated session.
+     */
+    private final JcrSession session;
+
+    /**
+     * The session's value factories.
+     */
+    private final ValueFactories valueFactories;
+
+    /**
+     * @param session the owning session (never <code>null</code>)
+     * @param repositoryObservable the repository observable used to register JCR listeners (never <code>null</code>)
+     * @throws IllegalArgumentException if either parameter is <code>null</code>
+     */
+    public JcrObservationManager( JcrSession session,
+                                  Observable repositoryObservable ) {
+        CheckArg.isNotNull(session, "session");
+        CheckArg.isNotNull(repositoryObservable, "repositoryObservable");
+
+        this.session = session;
+        this.repositoryObservable = repositoryObservable;
+        this.listeners = new ConcurrentHashMap<EventListener, JcrListener>();
+        this.namespaceRegistry = this.session.getExecutionContext().getNamespaceRegistry();
+        this.valueFactories = this.session.getExecutionContext().getValueFactories();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see javax.jcr.observation.ObservationManager#addEventListener(javax.jcr.observation.EventListener, int, java.lang.String,
+     *      boolean, java.lang.String[], java.lang.String[], boolean)
+     * @throws IllegalArgumentException if <code>listener</code> is <code>null</code>
+     */
+    public synchronized void addEventListener( EventListener listener,
+                                               int eventTypes,
+                                               String absPath,
+                                               boolean isDeep,
+                                               String[] uuid,
+                                               String[] nodeTypeName,
+                                               boolean noLocal ) {
+        CheckArg.isNotNull(listener, "listener");
+        
+        // make sure session is still active
+        if (!isValidSession()) {
+            // TODO is this appropriate? Need message.
+            throw new InvalidStateException();
+        }
+
+        // unregister if already registered
+        removeEventListener(listener);
+
+        // create wrapper and register
+        JcrListener jcrListener = new JcrListener(listener, eventTypes, absPath, isDeep, uuid, nodeTypeName, noLocal);
+        this.listeners.put(listener, jcrListener);
+
+        // set the time registered timestamp
+        Lock timestampLock = jcrListener.getTimestampLock().writeLock();
+
+        try {
+            timestampLock.lock();
+            this.repositoryObservable.register(jcrListener);
+            jcrListener.setTimeRegistered(System.currentTimeMillis());
+        } finally {
+            timestampLock.unlock();
+        }
+    }
+
+    /**
+     * @return the namespace registry used by listeners when handling events
+     */
+    NamespaceRegistry geNamespaceRegistry() {
+        return this.namespaceRegistry;
+    }
+
+    /**
+     * @return the node type manager
+     * @throws RepositoryException if there is a problem
+     */
+    JcrNodeTypeManager getNodeTypeManager() throws RepositoryException {
+        return (JcrNodeTypeManager)this.session.getWorkspace().getNodeTypeManager();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see javax.jcr.observation.ObservationManager#getRegisteredEventListeners()
+     */
+    public EventListenerIterator getRegisteredEventListeners() {
+        // make sure session is still active
+        if (!isValidSession()) {
+            // TODO is this appropriate? Need message.
+            throw new InvalidStateException();
+        }
+
+        return new JcrEventListenerIterator(this.listeners.keySet());
+    }
+
+    /**
+     * @return the user ID used by the listeners when handling events
+     */
+    String getUserId() {
+        return this.session.getUserID();
+    }
+
+    /**
+     * @return the value factories used by listeners when handling events
+     */
+    ValueFactories getValueFactories() {
+        return this.valueFactories;
+    }
+
+    /**
+     * @return the workspace graph
+     */
+    Graph getGraph() {
+        return ((JcrWorkspace)this.session.getWorkspace()).graph();
+    }
+
+    /**
+     * @return the session's unique identifier
+     */
+    String getSessionId() {
+        return this.session.sessionId();
+    }
+    
+    /**
+     * @return <code>true</code> if session is still logged in
+     */
+    boolean isValidSession() {
+        return this.session.isLive();
+    }
+
+    /**
+     * Remove all of the listeners. This is typically called when the {@link JcrSession#logout() session logs out}.
+     */
+    synchronized void removeAllEventListeners() {
+        for (JcrListener listener : listeners.values()) {
+            assert (listener != null);
+            this.repositoryObservable.unregister(listener);
+        }
+
+        this.listeners.clear();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @see javax.jcr.observation.ObservationManager#removeEventListener(javax.jcr.observation.EventListener)
+     * @throws IllegalArgumentException if <code>listener</code> is <code>null</code>
+     */
+    public synchronized void removeEventListener( EventListener listener ) {
+        CheckArg.isNotNull(listener, "listener");
+        
+        // make sure session is still active
+        if (!isValidSession()) {
+            // TODO is this appropriate? Need message.
+            throw new InvalidStateException();
+        }
+
+        JcrListener jcrListener = this.listeners.remove(listener);
+
+        if (jcrListener != null) {
+            this.repositoryObservable.unregister(jcrListener);
+        }
+    }
+
+    /**
+     * An implementation of JCR {@link RangeIterator} extended by the event and event listener iterators.
+     * 
+     * @param <E> the type being iterated over
+     */
+    class JcrRangeIterator<E> implements RangeIterator {
+
+        /**
+         * The elements being iterated over.
+         */
+        private final List<? extends E> elements;
+
+        /**
+         * The current position in the iterator.
+         */
+        private int position = 0;
+
+        /**
+         * @param elements the elements to iterator over
+         * @throws IllegalArgumentException if <code>elements</code> is <code>null</code>
+         */
+        public JcrRangeIterator( Collection<? extends E> elements ) {
+            CheckArg.isNotNull(elements, "elements");
+            this.elements = new ArrayList<E>(elements);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see javax.jcr.RangeIterator#getPosition()
+         */
+        public long getPosition() {
+            return this.position;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see javax.jcr.RangeIterator#getSize()
+         */
+        public long getSize() {
+            return this.elements.size();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.util.Iterator#hasNext()
+         */
+        public boolean hasNext() {
+            return (getPosition() < getSize());
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.util.Iterator#next()
+         */
+        public Object next() {
+            if (!hasNext()) {
+                throw new NoSuchElementException();
+            }
+
+            Object element = this.elements.get(this.position);
+            ++this.position;
+
+            return element;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.util.Iterator#remove()
+         * @throws UnsupportedOperationException if called
+         */
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see javax.jcr.RangeIterator#skip(long)
+         */
+        public void skip( long skipNum ) {
+            this.position += skipNum;
+
+            if (!hasNext()) {
+                throw new NoSuchElementException();
+            }
+        }
+    }
+
+    /**
+     * An implementation of the JCR {@link EventListenerIterator}.
+     */
+    class JcrEventListenerIterator extends JcrRangeIterator<EventListener> implements EventListenerIterator {
+
+        /**
+         * @param listeners the listeners being iterated over
+         * @throws IllegalArgumentException if <code>listeners</code> is <code>null</code>
+         */
+        public JcrEventListenerIterator( Collection<EventListener> listeners ) {
+            super(listeners);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see javax.jcr.observation.EventListenerIterator#nextEventListener()
+         */
+        public EventListener nextEventListener() {
+            return (EventListener)next();
+        }
+    }
+
+    /**
+     * An implementation of JCR {@link EventIterator}.
+     */
+    class JcrEventIterator extends JcrRangeIterator<Event> implements EventIterator {
+
+        /**
+         * @param events the events being iterated over
+         * @throws IllegalArgumentException if <code>events</code> is <code>null</code>
+         */
+        public JcrEventIterator( Collection<Event> events ) {
+            super(events);
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see javax.jcr.observation.EventIterator#nextEvent()
+         */
+        public Event nextEvent() {
+            return (Event)next();
+        }
+    }
+
+    /**
+     * An implementation of JCR {@link Event}.
+     */
+    class JcrEvent implements Event {
+
+        /**
+         * The node path.
+         */
+        private final String path;
+
+        /**
+         * The event type.
+         */
+        private final int type;
+
+        /**
+         * The user ID.
+         */
+        private final String userId;
+
+        /**
+         * @param type the event type
+         * @param path the node path
+         * @param userId the user ID
+         */
+        public JcrEvent( int type,
+                         String path,
+                         String userId ) {
+            this.type = type;
+            this.path = path;
+            this.userId = userId;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see javax.jcr.observation.Event#getPath()
+         */
+        public String getPath() {
+            return this.path;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see javax.jcr.observation.Event#getType()
+         */
+        public int getType() {
+            return this.type;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see javax.jcr.observation.Event#getUserID()
+         */
+        public String getUserID() {
+            return this.userId;
+        }
+    }
+
+    /**
+     * The <code>JcrListener</code> class wraps JCR {@link EventListener} and is responsible for converting
+     * {@link NetChangeObserver.NetChange graph events} into JCR {@link Event events}.
+     */
+    class JcrListener extends NetChangeObserver {
+
+        /**
+         * The node path whose events should be handled (or <code>null</code>) if all node paths should be handled.
+         */
+        private final String absPath;
+
+        /**
+         * The primary type and mixin types of the locations that have changes. Used only when the node type of the location needs
+         * to be checked.
+         */
+        private Map<Location, Map<Name, Property>> propertiesByLocation;
+
+        /**
+         * The JCR event listener.
+         */
+        private final EventListener delegate;
+
+        /**
+         * The event types this listener is interested in handling.
+         */
+        private final int eventTypes;
+
+        /**
+         * A flag indicating if events of child nodes of the <code>absPath</code> should be processed.
+         */
+        private final boolean isDeep;
+
+        /**
+         * The node type names or <code>null</code>. If a node with one of these types is the source node of an event than this
+         * listener wants to process that event. If <code>null</code> or empty than this listener wants to handle nodes of any
+         * type.
+         */
+        private final String[] nodeTypeNames;
+
+        /**
+         * A flag indicating if events generated by the session that registered this listener should be ignored.
+         */
+        private final boolean noLocal;
+
+        /**
+         * Only events after this time should be processed.
+         */
+        @GuardedBy( "timestampLock" )
+        private long timeRegistered;
+
+        /**
+         * Lock used for when accessing the time registered timestamp.
+         */
+        private final ReadWriteLock timestampLock = new ReentrantReadWriteLock();
+
+        /**
+         * The node UUIDs or <code>null</code>. If a node with one of these UUIDs is the source node of an event than this
+         * listener wants to handle this event. If <code>null</code> or empty than this listener wants to handle nodes with any
+         * UUID.
+         */
+        private final String[] uuids;
+
+        /**
+         * @param delegate the JCR listener
+         * @param eventTypes a combination of one or more JCR event types
+         * @param absPath the absolute path of a node or <code>null</code> if all node paths
+         * @param isDeep indicates if paths below <code>absPath</code> should be considered
+         * @param uuids UUIDs or <code>null</code>
+         * @param nodeTypeNames node type names or <code>null</code>
+         * @param noLocal indicates if events from this listener's session should be ignored
+         */
+        public JcrListener( EventListener delegate,
+                            int eventTypes,
+                            String absPath,
+                            boolean isDeep,
+                            String[] uuids,
+                            String[] nodeTypeNames,
+                            boolean noLocal ) {
+            assert (delegate != null);
+
+            this.delegate = delegate;
+            this.eventTypes = eventTypes;
+            this.absPath = absPath;
+            this.isDeep = isDeep;
+            this.uuids = uuids;
+            this.nodeTypeNames = nodeTypeNames;
+            this.noLocal = noLocal;
+        }
+
+        /**
+         * @param changes the changes being processed
+         * @return <code>true</code> if event occurred in a different session or if events from same session should be processed
+         */
+        private boolean acceptBasedOnEventSource( Changes changes ) {
+            if (this.noLocal) {
+                // don't accept unless IDs are different
+                return !getSessionId().equals(changes.getContextId());
+            }
+
+            return true;
+        }
+
+        /**
+         * @param change the change being processed
+         * @return <code>true</code> if all node types should be processed or if changed node type name matches a specified type
+         */
+        private boolean acceptBasedOnNodeTypeName( NetChange change ) {
+            boolean accept = true;
+
+            if (shouldCheckNodeType()) {
+                ValueFactory<String> stringFactory = getValueFactories().getStringFactory();
+                Location parentLocation = Location.create(change.getLocation().getPath().getParent());
+                Map<Name, Property> propMap = this.propertiesByLocation.get(parentLocation);
+                assert (propMap != null);
+
+                try {
+                    String primaryTypeName = stringFactory.create(propMap.get(PRIMARY_TYPE).getFirstValue());
+                    String[] mixinNames = null;
+
+                    if (propMap.get(MIXIN_TYPES) != null) {
+                        mixinNames = stringFactory.create(propMap.get(MIXIN_TYPES).getValuesAsArray());
+                    }
+
+                    return getNodeTypeManager().isDerivedFrom(primaryTypeName, mixinNames, this.nodeTypeNames);
+                } catch (RepositoryException e) {
+                    accept = false;
+                    Logger.getLogger(getClass()).error(e,
+                                                       JcrI18n.cannotPerformNodeTypeCheck,
+                                                       propMap.get(PRIMARY_TYPE),
+                                                       propMap.get(MIXIN_TYPES),
+                                                       this.nodeTypeNames);
+                }
+            }
+
+            return accept;
+        }
+
+        /**
+         * @param change the change being processed
+         * @return <code>true</code> if there is no absolute path or if change path matches or optionally is a deep match
+         */
+        private boolean acceptBasedOnPath( NetChange change ) {
+            if ((this.absPath != null) && (this.absPath.length() != 0)) {
+                Path matchPath = getValueFactories().getPathFactory().create(this.absPath);
+
+                if (this.isDeep) {
+                    return matchPath.isAtOrAbove(change.getPath().getParent());
+                }
+
+                return matchPath.equals(change.getPath().getParent());
+            }
+
+            return true;
+        }
+
+        /**
+         * @param change the change being processed
+         * @return <code>true</code> if there are no UUIDs to match or change UUID matches
+         */
+        private boolean acceptBasedOnUuid( NetChange change ) {
+            boolean accept = true;
+
+            if ((this.uuids != null) && (this.uuids.length != 0)) {
+                UUID matchUuid = change.getLocation().getUuid();
+
+                if (matchUuid != null) {
+                    accept = false;
+                    UuidFactory uuidFactory = getValueFactories().getUuidFactory();
+
+                    for (String uuidText : this.uuids) {
+                        if ((uuidText != null) && (uuidText.length() != 0)) {
+                            try {
+                                UUID testUuid = uuidFactory.create(uuidText);
+
+                                if (matchUuid.equals(testUuid)) {
+                                    accept = true;
+                                    break;
+                                }
+                            } catch (ValueFormatException e) {
+                                Logger.getLogger(getClass()).error(JcrI18n.cannotCreateUuid, uuidText);
+                            }
+                        }
+                    }
+                }
+            }
+
+            return accept;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.lang.Object#equals(java.lang.Object)
+         */
+        @Override
+        public boolean equals( Object obj ) {
+            if ((obj != null) && (obj instanceof JcrListener)) {
+                return (this.delegate == ((JcrListener)obj).delegate);
+            }
+
+            return false;
+        }
+
+        public ReadWriteLock getTimestampLock() {
+            return this.timestampLock;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see java.lang.Object#hashCode()
+         */
+        @Override
+        public int hashCode() {
+            return this.delegate.hashCode();
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.observe.NetChangeObserver#notify(org.jboss.dna.graph.observe.Changes)
+         */
+        @Override
+        public void notify( Changes changes ) {
+            long timestamp = 0;
+
+            try {
+                this.timestampLock.readLock().lock();
+                timestamp = this.timeRegistered;
+            } finally {
+                this.timestampLock.readLock().unlock();
+            }
+
+            // don't process if changes occurred before this listener was registered
+            if ((timestamp == 0) || changes.getTimestamp().getMilliseconds() <= timestamp) {
+                return;
+            }
+
+            // check source first
+            if (!acceptBasedOnEventSource(changes)) {
+                return;
+            }
+
+            try {
+                if (shouldCheckNodeType()) {
+                    List<Location> changedLocations = new ArrayList<Location>();
+
+                    // loop through changes saving the parent locations of the changed locations
+                    for (ChangeRequest request : changes.getChangeRequests()) {
+                        Path changedPath = request.changedLocation().getPath();
+                        Path parentPath = changedPath.getParent();
+                        changedLocations.add(Location.create(parentPath));
+                    }
+
+                    // more efficient to get all of the locations at once then it is one at a time using the NetChange
+                    Graph graph = getGraph();
+                    this.propertiesByLocation = graph.getProperties(PRIMARY_TYPE, MIXIN_TYPES).on(changedLocations);
+                }
+
+                // handle events
+                super.notify(changes);
+            } finally {
+                this.propertiesByLocation = null;
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see org.jboss.dna.graph.observe.NetChangeObserver#notify(org.jboss.dna.graph.observe.NetChangeObserver.NetChanges)
+         */
+        @Override
+        protected void notify( NetChanges netChanges ) {
+            Collection<Event> events = new ArrayList<Event>();
+
+            for (NetChange change : netChanges.getNetChanges()) {
+                // ignore if lock/unlock
+                if (change.includes(ChangeType.NODE_LOCKED) || change.includes(ChangeType.NODE_UNLOCKED)) {
+                    continue;
+                }
+
+                // determine if need to process
+                if (!acceptBasedOnNodeTypeName(change) || !acceptBasedOnPath(change) || !acceptBasedOnUuid(change)) {
+                    continue;
+                }
+
+                // process event making sure we have the right event type
+                Path path = change.getPath();
+                PathFactory pathFactory = getValueFactories().getPathFactory();
+                String userId = getUserId();
+
+                if (change.includes(ChangeType.NODE_ADDED) && ((this.eventTypes & Event.NODE_ADDED) == Event.NODE_ADDED)) {
+                    // create event for added node
+                    events.add(new JcrEvent(Event.NODE_ADDED, path.getString(geNamespaceRegistry()), userId));
+                } else if (change.includes(ChangeType.NODE_REMOVED)
+                           && ((this.eventTypes & Event.NODE_REMOVED) == Event.NODE_REMOVED)) {
+                    // create event for removed node
+                    events.add(new JcrEvent(Event.NODE_REMOVED, path.getString(geNamespaceRegistry()), userId));
+                }
+
+                if (change.includes(ChangeType.PROPERTY_CHANGED)
+                    && ((this.eventTypes & Event.PROPERTY_CHANGED) == Event.PROPERTY_CHANGED)) {
+                    for (Property property : change.getModifiedProperties()) {
+                        // create event for changed property
+                        Path propertyPath = pathFactory.create(path, property.getName().getString(geNamespaceRegistry()));
+                        events.add(new JcrEvent(Event.PROPERTY_CHANGED, propertyPath.getString(geNamespaceRegistry()), userId));
+                    }
+                }
+
+                // properties have changed
+                if (change.includes(ChangeType.PROPERTY_ADDED)
+                    && ((this.eventTypes & Event.PROPERTY_ADDED) == Event.PROPERTY_ADDED)) {
+                    for (Property property : change.getAddedProperties()) {
+                        // create event for added property
+                        Path propertyPath = pathFactory.create(path, property.getName().getString(geNamespaceRegistry()));
+                        events.add(new JcrEvent(Event.PROPERTY_ADDED, propertyPath.getString(geNamespaceRegistry()), userId));
+                    }
+                }
+
+                if (change.includes(ChangeType.PROPERTY_REMOVED)
+                    && ((this.eventTypes & Event.PROPERTY_REMOVED) == Event.PROPERTY_REMOVED)) {
+                    for (Name name : change.getRemovedProperties()) {
+                        // create event for removed property
+                        Path propertyPath = pathFactory.create(path, name);
+                        events.add(new JcrEvent(Event.PROPERTY_REMOVED, propertyPath.getString(geNamespaceRegistry()), userId));
+                    }
+                }
+            }
+
+            // notify delegate
+            if (!events.isEmpty()) {
+                this.delegate.onEvent(new JcrEventIterator(events));
+            }
+        }
+
+        /**
+         * Call this only after obtaining the write lock.
+         * 
+         * @param timestamp the time this listener was registered to receive events
+         */
+        public void setTimeRegistered( long timestamp ) {
+            this.timeRegistered = timestamp;
+        }
+
+        /**
+         * @return <code>true</code> if the node type of the event locations need to be checked
+         */
+        private boolean shouldCheckNodeType() {
+            return ((this.nodeTypeNames != null) && (this.nodeTypeNames.length != 0));
+        }
+    }
+
+}
Index: src/main/java/org/jboss/dna/jcr/JcrSession.java
===================================================================
--- src/main/java/org/jboss/dna/jcr/JcrSession.java	(revision 1377)
+++ src/main/java/org/jboss/dna/jcr/JcrSession.java	(working copy)
@@ -775,9 +775,10 @@
             return;
         }
 
+        isLive = false;
+        this.workspace().observationManager().removeAllEventListeners();
         this.workspace().lockManager().cleanLocks(this);
         this.executionContext.getSecurityContext().logout();
-        isLive = false;
     }
 
     /**
Index: src/main/java/org/jboss/dna/jcr/JcrI18n.java
===================================================================
--- src/main/java/org/jboss/dna/jcr/JcrI18n.java	(revision 1377)
+++ src/main/java/org/jboss/dna/jcr/JcrI18n.java	(working copy)
@@ -175,6 +175,10 @@
     public static I18n lockTokenNotHeld;
     public static I18n lockTokenAlreadyHeld;
     public static I18n uuidRequiredForLock;
+    
+    // JcrObservationManager messages
+    public static I18n cannotCreateUuid;
+    public static I18n cannotPerformNodeTypeCheck;
 
     static {
         try {
Index: src/main/java/org/jboss/dna/jcr/JcrRepository.java
===================================================================
--- src/main/java/org/jboss/dna/jcr/JcrRepository.java	(revision 1377)
+++ src/main/java/org/jboss/dna/jcr/JcrRepository.java	(working copy)
@@ -37,6 +37,9 @@
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import javax.jcr.Credentials;
 import javax.jcr.NoSuchWorkspaceException;
 import javax.jcr.Repository;
@@ -68,6 +71,7 @@
 import org.jboss.dna.graph.connector.federation.ProjectionParser;
 import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource;
 import org.jboss.dna.graph.observe.Changes;
+import org.jboss.dna.graph.observe.Observable;
 import org.jboss.dna.graph.observe.Observer;
 import org.jboss.dna.graph.property.Name;
 import org.jboss.dna.graph.property.NamespaceRegistry;
@@ -215,24 +219,8 @@
     private final String systemWorkspaceName;
     private final Projection systemSourceProjection;
     private final FederatedRepositorySource federatedSource;
-    private final Observer observer;
     private final NamespaceRegistry persistentRegistry;
-
-    /**
-     * Creates a JCR repository that uses the supplied {@link RepositoryConnectionFactory repository connection factory} to
-     * establish {@link Session sessions} to the underlying repository source upon {@link #login() login}.
-     * 
-     * @param executionContext An execution context.
-     * @param connectionFactory A repository connection factory.
-     * @param repositorySourceName the name of the repository source (in the connection factory) that should be used
-     * @throws IllegalArgumentException If <code>executionContextFactory</code> or <code>connectionFactory</code> is
-     *         <code>null</code>.
-     */
-    public JcrRepository( ExecutionContext executionContext,
-                          RepositoryConnectionFactory connectionFactory,
-                          String repositorySourceName ) {
-        this(executionContext, connectionFactory, repositorySourceName, null, null);
-    }
+    private final RepositoryObservationManager repositoryObservationManager;
 
     /**
      * Creates a JCR repository that uses the supplied {@link RepositoryConnectionFactory repository connection factory} to
@@ -241,19 +229,23 @@
      * @param executionContext the execution context in which this repository is to operate
      * @param connectionFactory the factory for repository connections
      * @param repositorySourceName the name of the repository source (in the connection factory) that should be used
+     * @param repositoryObservable the repository library observable associated with this repository (never <code>null</code>)
      * @param descriptors the {@link #getDescriptorKeys() descriptors} for this repository; may be <code>null</code>.
      * @param options the optional {@link Option settings} for this repository; may be null
-     * @throws IllegalArgumentException If <code>executionContextFactory</code> or <code>connectionFactory</code> is
-     *         <code>null</code>.
+     * @throws IllegalArgumentException If <code>executionContext</code>, <code>connectionFactory</code>, 
+     *         <code>repositorySourceName</code>, or <code>repositoryObservable</code> is <code>null</code>.
      */
     public JcrRepository( ExecutionContext executionContext,
                           RepositoryConnectionFactory connectionFactory,
                           String repositorySourceName,
+                          Observable repositoryObservable,
                           Map<String, String> descriptors,
                           Map<Option, String> options ) {
         CheckArg.isNotNull(executionContext, "executionContext");
         CheckArg.isNotNull(connectionFactory, "connectionFactory");
         CheckArg.isNotNull(repositorySourceName, "repositorySourceName");
+        CheckArg.isNotNull(repositoryObservable, "repositoryObservable");
+
         Map<String, String> modifiableDescriptors;
         if (descriptors == null) {
             modifiableDescriptors = new HashMap<String, String>();
@@ -264,7 +256,7 @@
         modifiableDescriptors.put(Repository.LEVEL_1_SUPPORTED, "true");
         modifiableDescriptors.put(Repository.LEVEL_2_SUPPORTED, "true");
         modifiableDescriptors.put(Repository.OPTION_LOCKING_SUPPORTED, "true");
-        modifiableDescriptors.put(Repository.OPTION_OBSERVATION_SUPPORTED, "false");
+        modifiableDescriptors.put(Repository.OPTION_OBSERVATION_SUPPORTED, "true");
         modifiableDescriptors.put(Repository.OPTION_QUERY_SQL_SUPPORTED, "false");
         modifiableDescriptors.put(Repository.OPTION_TRANSACTIONS_SUPPORTED, "false");
         modifiableDescriptors.put(Repository.OPTION_VERSIONING_SUPPORTED, "false");
@@ -297,8 +289,9 @@
             this.options = Collections.unmodifiableMap(localOptions);
         }
 
-        // Initialize the observer, which receives events from all repository sources ...
-        this.observer = new RepositoryObserver();
+        // Initialize the observer, which receives events from all repository sources
+        this.repositoryObservationManager = new RepositoryObservationManager();
+        repositoryObservable.register(this.repositoryObservationManager);
 
         // Set up the system source ...
         String systemSourceNameValue = this.options.get(Option.SYSTEM_SOURCE_NAME);
@@ -490,7 +483,16 @@
      * @return the current observer, or null if there is no observer
      */
     Observer getObserver() {
-        return observer;
+        return this.repositoryObservationManager;
+    }
+
+    /**
+     * The repository observable that listeners can be registered with.
+     * 
+     * @return the repository observable (never <code>null</code>)
+     */
+    Observable getRepositoryObservable() {
+        return this.repositoryObservationManager;
     }
 
     /**
@@ -833,14 +835,48 @@
         }
     }
 
-    protected class RepositoryObserver implements Observer {
+    protected class RepositoryObservationManager implements Observable, Observer {
+        
+        private final ExecutorService observerService = Executors.newSingleThreadExecutor();
+        private final CopyOnWriteArrayList<Observer> observers = new CopyOnWriteArrayList<Observer>();
+
         /**
          * {@inheritDoc}
          * 
          * @see org.jboss.dna.graph.observe.Observer#notify(org.jboss.dna.graph.observe.Changes)
          */
-        public void notify( Changes changes ) {
-            // does nothing at the moment, but eventually will fire to all of the listeners on the appropriate sessions
+        public void notify( final Changes changes ) {
+            final List<Observer> listeners = observers;
+            
+            Runnable command = new Runnable() {
+                public void run() {
+                    for (Observer observer : listeners) {
+                        observer.notify(changes);
+                    }
+                }                
+            };
+
+            this.observerService.execute(command);
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @see org.jboss.dna.graph.observe.Observable#register(org.jboss.dna.graph.observe.Observer)
+         */
+        public boolean register( Observer observer ) {
+            CheckArg.isNotNull(observer, "observer");
+            return this.observers.addIfAbsent(observer);
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @see org.jboss.dna.graph.observe.Observable#unregister(org.jboss.dna.graph.observe.Observer)
+         */
+        public boolean unregister( Observer observer ) {
+            CheckArg.isNotNull(observer, "observer");
+            return this.observers.remove(observer);
         }
     }
 
Index: src/main/java/org/jboss/dna/jcr/WorkspaceLockManager.java
===================================================================
--- src/main/java/org/jboss/dna/jcr/WorkspaceLockManager.java	(revision 1377)
+++ src/main/java/org/jboss/dna/jcr/WorkspaceLockManager.java	(working copy)
@@ -189,12 +189,12 @@
             ExecutionContext context = session.getExecutionContext();
             PathFactory pathFactory = context.getValueFactories().getPathFactory();
 
+            // Remove the lock node under the /jcr:system branch ...
             Graph.Batch batch = repository.createSystemGraph(context).batch();
-
             batch.delete(pathFactory.create(locksPath, pathFactory.createSegment(lock.getUuid().toString())));
-            batch.remove(JcrLexicon.LOCK_OWNER, JcrLexicon.LOCK_IS_DEEP).on(lock.nodeUuid);
             batch.execute();
 
+            // Unlock the node in the repository graph ...
             unlockNodeInRepository(session, lock);
 
             workspaceLocksByNodeUuid.remove(lock.nodeUuid);
@@ -208,7 +208,6 @@
                 throw new IllegalStateException(pnfe);
             }
             workspaceLocksByNodeUuid.remove(lock.nodeUuid);
-
         }
     }
 
Index: src/main/resources/org/jboss/dna/jcr/JcrI18n.properties
===================================================================
--- src/main/resources/org/jboss/dna/jcr/JcrI18n.properties	(revision 1377)
+++ src/main/resources/org/jboss/dna/jcr/JcrI18n.properties	(working copy)
@@ -159,3 +159,7 @@
 lockTokenNotHeld = The node at location '{0}' is locked and this session does not hold its lock token
 lockTokenAlreadyHeld = The lock token '{0}' is already held by another session.  It must be removed from that session before it can be added to another session.
 uuidRequiredForLock = Only referenceable nodes can be locked.  The node at location '(0}' is not referenceable.
+
+# JcrObservationManager messages
+cannotCreateUuid = Factory was unable to create UUID from text '{0}'
+cannotPerformNodeTypeCheck = Error checking primary type '{0}' with mixins of '{1}' against type names of '{2}'
Index: src/test/java/org/jboss/dna/jcr/JcrRepositoryTest.java
===================================================================
--- src/test/java/org/jboss/dna/jcr/JcrRepositoryTest.java	(revision 1377)
+++ src/test/java/org/jboss/dna/jcr/JcrRepositoryTest.java	(working copy)
@@ -49,6 +49,7 @@
 import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
 import org.jboss.dna.graph.connector.RepositorySourceException;
 import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource;
+import org.jboss.dna.graph.observe.MockObservable;
 import org.jboss.security.config.IDTrustConfiguration;
 import org.junit.After;
 import org.junit.Before;
@@ -113,7 +114,7 @@
 
         // Set up the repository ...
         descriptors = new HashMap<String, String>();
-        repository = new JcrRepository(context, connectionFactory, sourceName, descriptors, null);
+        repository = new JcrRepository(context, connectionFactory, sourceName, new MockObservable(), descriptors, null);
 
         // Set up the graph that goes directly to the source ...
         sourceGraph = Graph.create(source, context);
@@ -135,22 +136,27 @@
 
     @Test
     public void shouldAllowNullDescriptors() {
-        new JcrRepository(context, connectionFactory, sourceName, null, null);
+        new JcrRepository(context, connectionFactory, sourceName, new MockObservable(), null, null);
     }
 
     @Test( expected = IllegalArgumentException.class )
     public void shouldNotAllowNullExecutionContext() throws Exception {
-        new JcrRepository(null, connectionFactory, sourceName, descriptors, null);
+        new JcrRepository(null, connectionFactory, sourceName, new MockObservable(), descriptors, null);
     }
 
     @Test( expected = IllegalArgumentException.class )
     public void shouldNotAllowNullConnectionFactories() throws Exception {
-        new JcrRepository(context, null, sourceName, descriptors, null);
+        new JcrRepository(context, null, sourceName, new MockObservable(), descriptors, null);
+    }
+
+    @Test( expected = IllegalArgumentException.class )
+    public void shouldNotAllowNullObservable() throws Exception {
+        new JcrRepository(context, connectionFactory, sourceName, null, null, null);
     }
 
     @Test( expected = IllegalArgumentException.class )
     public void shouldNotAllowNullSourceName() throws Exception {
-        new JcrRepository(context, connectionFactory, null, descriptors, null);
+        new JcrRepository(context, connectionFactory, null, new MockObservable(), descriptors, null);
     }
 
     @Test( expected = IllegalArgumentException.class )
@@ -175,14 +181,24 @@
 
     @Test
     public void shouldProvideBuiltInDescriptorsWhenNotSuppliedDescriptors() {
-        Repository repository = new JcrRepository(context, connectionFactory, sourceName, descriptors, null);
+        Repository repository = new JcrRepository(context, connectionFactory, sourceName, new MockObservable(), descriptors, null);
         testDescriptorKeys(repository);
         testDescriptorValues(repository);
     }
+    
+    @Test
+    public void shouldProvideObserver() {
+        assertThat(this.repository.getObserver(), is(notNullValue()));
+    }
+    
+    @Test
+    public void shouldProvideRepositoryObservable() {
+        assertThat(this.repository.getRepositoryObservable(), is(notNullValue()));
+    }
 
     @Test
     public void shouldHaveDefaultOptionsWhenNotOverridden() {
-        JcrRepository repository = new JcrRepository(context, connectionFactory, sourceName, descriptors, null);
+        JcrRepository repository = new JcrRepository(context, connectionFactory, sourceName, new MockObservable(), descriptors, null);
         assertThat(repository.getOptions().get(JcrRepository.Option.PROJECT_NODE_TYPES),
                    is(JcrRepository.DefaultOption.PROJECT_NODE_TYPES));
     }
@@ -191,7 +207,7 @@
     public void shouldProvideUserSuppliedDescriptors() {
         Map<String, String> descriptors = new HashMap<String, String>();
         descriptors.put("property", "value");
-        Repository repository = new JcrRepository(context, connectionFactory, sourceName, descriptors, null);
+        Repository repository = new JcrRepository(context, connectionFactory, sourceName, new MockObservable(), descriptors, null);
         testDescriptorKeys(repository);
         testDescriptorValues(repository);
         assertThat(repository.getDescriptor("property"), is("value"));
@@ -433,7 +449,7 @@
         assertThat(repository.getDescriptor(Repository.LEVEL_1_SUPPORTED), is("true"));
         assertThat(repository.getDescriptor(Repository.LEVEL_2_SUPPORTED), is("true"));
         assertThat(repository.getDescriptor(Repository.OPTION_LOCKING_SUPPORTED), is("true"));
-        assertThat(repository.getDescriptor(Repository.OPTION_OBSERVATION_SUPPORTED), is("false"));
+        assertThat(repository.getDescriptor(Repository.OPTION_OBSERVATION_SUPPORTED), is("true"));
         assertThat(repository.getDescriptor(Repository.OPTION_QUERY_SQL_SUPPORTED), is("false"));
         assertThat(repository.getDescriptor(Repository.OPTION_TRANSACTIONS_SUPPORTED), is("false"));
         assertThat(repository.getDescriptor(Repository.OPTION_VERSIONING_SUPPORTED), is("false"));
Index: src/test/java/org/jboss/dna/jcr/JcrObservationManagerTest.java
===================================================================
--- src/test/java/org/jboss/dna/jcr/JcrObservationManagerTest.java	(revision 0)
+++ src/test/java/org/jboss/dna/jcr/JcrObservationManagerTest.java	(revision 0)
@@ -0,0 +1,1702 @@
+/*
+ * JBoss DNA (http://www.jboss.org/dna)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership.  Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of 
+ * individual contributors.
+ *
+ * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
+ * is licensed to you 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.
+ * 
+ * JBoss DNA 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.jcr;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import javax.jcr.Credentials;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+import javax.jcr.Workspace;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.ObservationManager;
+import junit.framework.TestSuite;
+import org.apache.jackrabbit.test.api.observation.AddEventListenerTest;
+import org.apache.jackrabbit.test.api.observation.EventIteratorTest;
+import org.apache.jackrabbit.test.api.observation.EventTest;
+import org.apache.jackrabbit.test.api.observation.GetRegisteredEventListenersTest;
+import org.apache.jackrabbit.test.api.observation.LockingTest;
+import org.apache.jackrabbit.test.api.observation.NodeAddedTest;
+import org.apache.jackrabbit.test.api.observation.NodeMovedTest;
+import org.apache.jackrabbit.test.api.observation.NodeRemovedTest;
+import org.apache.jackrabbit.test.api.observation.NodeReorderTest;
+import org.apache.jackrabbit.test.api.observation.PropertyAddedTest;
+import org.apache.jackrabbit.test.api.observation.PropertyChangedTest;
+import org.apache.jackrabbit.test.api.observation.PropertyRemovedTest;
+import org.apache.jackrabbit.test.api.observation.WorkspaceOperationTest;
+import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource;
+import org.jboss.dna.jcr.JcrRepository.Option;
+import org.jboss.security.config.IDTrustConfiguration;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * 
+ */
+public class JcrObservationManagerTest extends TestSuite {
+
+    // ===========================================================================================================================
+    // Constants
+    // ===========================================================================================================================
+
+    private static final int ALL_EVENTS = Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED
+                                          | Event.PROPERTY_REMOVED;
+
+    // ===========================================================================================================================
+    // Class Fields
+    // ===========================================================================================================================
+
+    private static final String LOCK_MIXIN = "mix:lockable"; // extends referenceable
+    private static final String LOCK_OWNER = "jcr:lockOwner"; // property
+    private static final String LOCK_IS_DEEP = "jcr:lockIsDeep"; // property
+    private static final String NT_BASE = "nt:base";
+    private static final String REF_MIXIN = "mix:referenceable";
+    private static final String UNSTRUCTURED = "nt:unstructured";
+    private static final String USER_ID = "superuser";
+
+    // ===========================================================================================================================
+    // Class Methods
+    // ===========================================================================================================================
+
+    @BeforeClass
+    public static void beforeClass() {
+        // Initialize IDTrust
+        String configFile = "security/jaas.conf.xml";
+        IDTrustConfiguration idtrustConfig = new IDTrustConfiguration();
+
+        try {
+            idtrustConfig.config(configFile);
+        } catch (Exception ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    // ===========================================================================================================================
+    // Fields
+    // ===========================================================================================================================
+
+    private JcrConfiguration config;
+    private JcrEngine engine;
+    private Repository repository;
+    private Session session;
+    private Node testRootNode;
+
+    // ===========================================================================================================================
+    // Methods
+    // ===========================================================================================================================
+
+    TestListener addListener( int eventsExpected,
+                              int eventTypes,
+                              String absPath,
+                              boolean isDeep,
+                              String[] uuids,
+                              String[] nodeTypeNames,
+                              boolean noLocal ) throws Exception {
+        TestListener listener = new TestListener(eventsExpected, eventTypes);
+        this.session.getWorkspace().getObservationManager().addEventListener(listener,
+                                                                             eventTypes,
+                                                                             absPath,
+                                                                             isDeep,
+                                                                             uuids,
+                                                                             nodeTypeNames,
+                                                                             noLocal);
+        return listener;
+    }
+
+    @After
+    public void afterEach() {
+        try {
+            if (this.session != null) {
+                this.session.logout();
+            }
+        } finally {
+            this.session = null;
+
+            try {
+                this.repository = null;
+                this.engine.shutdown();
+            } finally {
+                this.engine = null;
+            }
+        }
+    }
+
+    @Before
+    public void beforeEach() throws RepositoryException {
+        final String WORKSPACE = "ws1";
+        final String REPOSITORY = "r1";
+        final String SOURCE = "store";
+
+        this.config = new JcrConfiguration();
+        this.config.repositorySource("store").usingClass(InMemoryRepositorySource.class).setRetryLimit(100).setProperty("defaultWorkspaceName",
+                                                                                                                        WORKSPACE);
+        this.config.repository(REPOSITORY).setSource(SOURCE).setOption(Option.JAAS_LOGIN_CONFIG_NAME, "dna-jcr");
+        this.config.save();
+
+        // Create and start the engine ...
+        this.engine = this.config.build();
+        this.engine.start();
+
+        // Create repository and session
+        this.repository = this.engine.getRepository(REPOSITORY);
+        Credentials credentials = new SimpleCredentials(USER_ID, USER_ID.toCharArray());
+        this.session = this.repository.login(credentials, WORKSPACE);
+
+        this.testRootNode = this.session.getRootNode().addNode("testroot", UNSTRUCTURED);
+        save();
+    }
+
+    void checkResults( TestListener listener ) {
+        assertThat("Received incorrect number of events", listener.getActualEventCount(), is(listener.getExpectedEventCount()));
+        assertThat(listener.getErrorMessage(), listener.getErrorMessage(), is(nullValue()));
+    }
+
+    boolean containsPath( TestListener listener,
+                          String path ) throws Exception {
+        for (Event event : listener.getEvents()) {
+            if (event.getPath().equals(path)) return true;
+        }
+
+        return false;
+    }
+
+    ObservationManager getObservationManager() throws RepositoryException {
+        return this.session.getWorkspace().getObservationManager();
+    }
+
+    Node getRoot() {
+        return this.testRootNode;
+    }
+
+    Workspace getWorkspace() {
+        return this.session.getWorkspace();
+    }
+
+    void removeListener( TestListener listener ) throws Exception {
+        this.session.getWorkspace().getObservationManager().removeEventListener(listener);
+    }
+
+    void save() throws RepositoryException {
+        this.session.save();
+    }
+
+    // ===========================================================================================================================
+    // Tests
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see AddEventListenerTest#testUUID()
+     */
+    @Test
+    public void shouldNotReceiveEventIfUuidDoesNotMatch() throws Exception {
+        // setup
+        Node n1 = getRoot().addNode("node1", UNSTRUCTURED);
+        n1.addMixin(REF_MIXIN);
+        save();
+
+        // register listener
+        TestListener listener = addListener(0,
+                                            Event.PROPERTY_ADDED,
+                                            getRoot().getPath(),
+                                            true,
+                                            new String[] {UUID.randomUUID().toString()},
+                                            null,
+                                            false);
+
+        // create properties
+        n1.setProperty("prop1", "foo");
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+    }
+
+    @Test
+    public void shouldNotReceiveEventIfNodeTypeDoesNotMatch() throws Exception {
+        // setup
+        Node node1 = getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // register listener
+        TestListener listener = addListener(0, ALL_EVENTS, null, false, null, new String[] {REF_MIXIN}, false);
+
+        // create event triggers
+        node1.setProperty("newProperty", "newValue"); // node1 is NOT referenceable
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+    }
+
+    @Test
+    public void shouldReceiveNodeAddedEventWhenRegisteredToReceiveAllEvents() throws Exception {
+        // register listener (add + 3 property events)
+        TestListener listener = addListener(4, ALL_EVENTS, null, false, null, null, false);
+
+        // add node
+        Node addedNode = getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for added node is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + addedNode.getPath(), containsPath(listener, addedNode.getPath()));
+    }
+
+    @Test
+    public void shouldReceiveNodeRemovedEventWhenRegisteredToReceiveAllEvents() throws Exception {
+        // add the node that will be removed
+        Node addedNode = getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // register listener (add + 3 property events)
+        TestListener listener = addListener(1, ALL_EVENTS, null, false, null, null, false);
+
+        // remove node
+        String path = addedNode.getPath();
+        addedNode.remove();
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for removed node is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected=" + path,
+                   containsPath(listener, path));
+    }
+
+    @Test
+    public void shouldReceivePropertyAddedEventWhenRegisteredToReceiveAllEvents() throws Exception {
+        // setup
+        Node node = getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // register listener
+        TestListener listener = addListener(1, ALL_EVENTS, null, false, null, null, false);
+
+        // add the property
+        Property prop1 = node.setProperty("prop1", "prop1 content");
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for added property is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + prop1.getPath(), containsPath(listener, prop1.getPath()));
+    }
+
+    @Test
+    public void shouldReceivePropertyChangedEventWhenRegisteredToReceiveAllEvents() throws Exception {
+        // setup
+        Node node = getRoot().addNode("node1", UNSTRUCTURED);
+        Property prop1 = node.setProperty("prop1", "prop1 content");
+        save();
+
+        // register listener
+        TestListener listener = addListener(1, ALL_EVENTS, null, false, null, null, false);
+
+        // change the property
+        prop1.setValue("prop1 modified content");
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for changed property is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + prop1.getPath(), containsPath(listener, prop1.getPath()));
+    }
+
+    @Test
+    public void shouldReceivePropertyRemovedEventWhenRegisteredToReceiveAllEvents() throws Exception {
+        // setup
+        Node node = getRoot().addNode("node1", UNSTRUCTURED);
+        Property prop = node.setProperty("prop1", "prop1 content");
+        String propPath = prop.getPath();
+        save();
+
+        // register listener
+        TestListener listener = addListener(1, ALL_EVENTS, null, false, null, null, false);
+
+        // remove the property
+        prop.remove();
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for removed property is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + propPath, containsPath(listener, propPath));
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.EventIteratorTest
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see EventIteratorTest#testGetPosition()
+     */
+    @Test
+    public void shouldTestEventIteratorTest_testGetPosition() throws Exception {
+        // register listener
+        TestListener listener = addListener(3, Event.NODE_ADDED, null, false, null, null, false);
+
+        // add nodes to generate events
+        getRoot().addNode("node1", UNSTRUCTURED);
+        getRoot().addNode("node2", UNSTRUCTURED);
+        getRoot().addNode("node3", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+    }
+
+    /**
+     * @throws Exception
+     * @see EventIteratorTest#testGetSize()
+     */
+    @Test
+    public void shouldTestEventIteratorTest_testGetSize() throws Exception {
+        // register listener
+        TestListener listener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+
+        // add node to generate event
+        getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+    }
+
+    /**
+     * @throws Exception
+     * @see EventIteratorTest#testSkip()
+     */
+    @Test
+    public void shouldTestEventIteratorTest_testSkip() throws Exception {
+        // create events
+        List<Event> events = new ArrayList<Event>();
+        events.add(((JcrObservationManager)getObservationManager()).new JcrEvent(Event.NODE_ADDED, "/testroot/node1", "userId"));
+        events.add(((JcrObservationManager)getObservationManager()).new JcrEvent(Event.NODE_ADDED, "/testroot/node2", "userId"));
+        events.add(((JcrObservationManager)getObservationManager()).new JcrEvent(Event.NODE_ADDED, "/testroot/node3", "userId"));
+
+        // create iterator
+        EventIterator itr = ((JcrObservationManager)getObservationManager()).new JcrEventIterator(events);
+
+        // tests
+        itr.skip(0); // skip zero elements
+        assertThat("getPosition() for first element should return 0.", itr.getPosition(), is(0L));
+
+        itr.skip(2); // skip one element
+        assertThat("Wrong value when skipping ", itr.getPosition(), is(2L));
+
+        try {
+            itr.skip(2); // skip past end
+            fail("EventIterator must throw NoSuchElementException when skipping past the end");
+        } catch (NoSuchElementException e) {
+            // success
+        }
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.EventTest
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see EventTest#testGetNodePath()
+     */
+    @Test
+    public void shouldTestEventTest_testGetNodePath() throws Exception {
+        // register listener
+        TestListener listener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+
+        // add node to generate event
+        Node addedNode = getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path added node is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + addedNode.getPath(), containsPath(listener, addedNode.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see EventTest#testGetType()
+     */
+    @Test
+    public void shouldTestEventTest_testGetType() throws Exception {
+        // register listener
+        TestListener listener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+
+        // add node to generate event
+        getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertThat("Event did not return correct event type", listener.getEvents().get(0).getType(), is(Event.NODE_ADDED));
+    }
+
+    /**
+     * @throws Exception
+     * @see EventTest#testGetUserId()
+     */
+    @Test
+    public void shouldTestEventTest_testGetUserId() throws Exception {
+        // register listener
+        TestListener listener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+
+        // add a node to generate event
+        getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertThat("UserId of event is not equal to userId of session", listener.getEvents().get(0).getUserID(), is(USER_ID));
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.GetRegisteredEventListenersTest
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see GetRegisteredEventListenersTest#testGetSize()
+     */
+    @Test
+    public void shouldTestGetRegisteredEventListenersTest_testGetSize() throws Exception {
+        assertThat("A new session must not have any event listeners registered.",
+                   getObservationManager().getRegisteredEventListeners().getSize(),
+                   is(0L));
+
+        // register listener
+        TestListener listener = addListener(0, ALL_EVENTS, null, false, null, null, false);
+        addListener(0, ALL_EVENTS, null, false, null, null, false);
+        assertThat("Wrong number of event listeners.", getObservationManager().getRegisteredEventListeners().getSize(), is(2L));
+
+        // make sure same listener isn't added again
+        getObservationManager().addEventListener(listener, ALL_EVENTS, null, false, null, null, false);
+        assertThat("The same listener should not be added more than once.",
+                   getObservationManager().getRegisteredEventListeners().getSize(),
+                   is(2L));
+    }
+
+    /**
+     * @throws Exception
+     * @see GetRegisteredEventListenersTest#testRemoveEventListener()
+     */
+    @Test
+    public void shouldTestGetRegisteredEventListenersTest_testRemoveEventListener() throws Exception {
+        TestListener listener1 = addListener(0, ALL_EVENTS, null, false, null, null, false);
+        EventListener listener2 = addListener(0, ALL_EVENTS, null, false, null, null, false);
+        assertThat("Wrong number of event listeners.", getObservationManager().getRegisteredEventListeners().getSize(), is(2L));
+
+        // now remove
+        removeListener(listener1);
+        assertThat("Wrong number of event listeners after removing a listener.",
+                   getObservationManager().getRegisteredEventListeners().getSize(),
+                   is(1L));
+        assertThat("Wrong number of event listeners after removing a listener.",
+                   getObservationManager().getRegisteredEventListeners().nextEventListener(),
+                   is(listener2));
+
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.LockingTest
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see LockingTest#testAddLockToNode()
+     */
+    @Test
+    public void shouldTestLockingTest_testAddLockToNode() throws Exception {
+        // setup
+        String node1 = "node1";
+        Node lockable = getRoot().addNode(node1, UNSTRUCTURED);
+        lockable.addMixin(LOCK_MIXIN);
+        save();
+
+        // register listener
+        TestListener listener = addListener(2, Event.PROPERTY_ADDED, null, false, null, null, false);
+
+        // lock node (no save needed)
+        lockable.lock(false, true);
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("No event created for " + LOCK_OWNER, containsPath(listener, lockable.getPath() + '/' + LOCK_OWNER));
+        assertTrue("No event created for " + LOCK_IS_DEEP, containsPath(listener, lockable.getPath() + '/' + LOCK_IS_DEEP));
+    }
+
+    /**
+     * @throws Exception
+     * @see LockingTest#testRemoveLockFromNode()
+     */
+    @Test
+    public void shouldTestLockingTest_testRemoveLockFromNode() throws Exception {
+        // setup
+        String node1 = "node1";
+        Node lockable = getRoot().addNode(node1, UNSTRUCTURED);
+        lockable.addMixin(LOCK_MIXIN);
+        save();
+        lockable.lock(false, true);
+
+        // register listener
+        TestListener listener = addListener(2, Event.PROPERTY_REMOVED, null, false, null, null, false);
+
+        // lock node (no save needed)
+        lockable.unlock();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("No event created for " + LOCK_OWNER, containsPath(listener, lockable.getPath() + '/' + LOCK_OWNER));
+        assertTrue("No event created for " + LOCK_IS_DEEP, containsPath(listener, lockable.getPath() + '/' + LOCK_IS_DEEP));
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.NodeAddedTest
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see NodeAddedTest#testMultipleNodeAdded1()
+     */
+    @Test
+    public void shouldTestNodeAddedTest_testMultipleNodeAdded1() throws Exception {
+        // register listener
+        TestListener listener = addListener(2, Event.NODE_ADDED, null, false, null, null, false);
+
+        // add a couple sibling nodes
+        Node addedNode1 = getRoot().addNode("node1", UNSTRUCTURED);
+        Node addedNode2 = getRoot().addNode("node2", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for first added node is wrong", containsPath(listener, addedNode1.getPath()));
+        assertTrue("Path for second added node is wrong", containsPath(listener, addedNode2.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see NodeAddedTest#testMultipleNodeAdded2()
+     */
+    @Test
+    public void shouldTestNodeAddedTest_testMultipleNodeAdded2() throws Exception {
+        // register listener
+        TestListener listener = addListener(2, Event.NODE_ADDED, null, false, null, null, false);
+
+        // add node and child node
+        Node addedNode = getRoot().addNode("node1", UNSTRUCTURED);
+        Node addedChildNode = addedNode.addNode("node2", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for added node is wrong", containsPath(listener, addedNode.getPath()));
+        assertTrue("Path for added child node is wrong", containsPath(listener, addedChildNode.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see NodeAddedTest#testSingleNodeAdded()
+     */
+    @Test
+    public void shouldTestNodeAddedTest_testSingleNodeAdded() throws Exception {
+        // register listener
+        TestListener listener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+
+        // add node
+        Node addedNode = getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for added node is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + addedNode.getPath(), containsPath(listener, addedNode.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see NodeAddedTest#testTransientNodeAddedRemoved()
+     */
+    @Test
+    public void shouldTestNodeAddedTest_testTransientNodeAddedRemoved() throws Exception {
+        // register listener
+        TestListener listener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+
+        // add a child node and immediately remove it
+        Node addedNode = getRoot().addNode("node1", UNSTRUCTURED);
+        Node transientNode = addedNode.addNode("node2", UNSTRUCTURED); // should not get this event because of the following
+        // remove
+        transientNode.remove();
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for added node is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + addedNode.getPath(), containsPath(listener, addedNode.getPath()));
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.NodeRemovedTest
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see NodeRemovedTest#testMultiNodesRemoved()
+     */
+    @Test
+    @Ignore
+    public void shouldTestNodeRemovedTest_testMultiNodesRemoved() throws Exception {
+        // register listener
+        TestListener listener = addListener(2, Event.NODE_REMOVED, null, false, null, null, false);
+
+        // add nodes to be removed
+        Node addedNode = getRoot().addNode("node1", UNSTRUCTURED);
+        Node childNode = addedNode.addNode("node2", UNSTRUCTURED);
+        save();
+
+        // remove parent node which removes child node
+        String parentPath = addedNode.getPath();
+        String childPath = childNode.getPath();
+        addedNode.remove();
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for removed node is wrong", containsPath(listener, parentPath));
+        assertTrue("Path for removed child node is wrong", containsPath(listener, childPath));
+    }
+
+    /**
+     * @throws Exception
+     * @see NodeRemovedTest#testSingleNodeRemoved()
+     */
+    @Test
+    public void shouldTestNodeRemovedTest_testSingleNodeRemoved() throws Exception {
+        // register listener
+        TestListener listener = addListener(1, Event.NODE_REMOVED, null, false, null, null, false);
+
+        // add the node that will be removed
+        Node addedNode = getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // remove node
+        String path = addedNode.getPath();
+        addedNode.remove();
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for removed node is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected=" + path,
+                   containsPath(listener, path));
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.NodeMovedTest
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see NodeMovedTest#testMoveNode()
+     */
+    @Test
+    public void shouldTestNodeMovedTest_testMoveNode() throws Exception {
+        // setup
+        String node1 = "node1";
+        String node2 = "node2";
+        Node n1 = getRoot().addNode(node1, UNSTRUCTURED);
+        Node n2 = n1.addNode(node2, UNSTRUCTURED);
+        String oldPath = n2.getPath();
+        save();
+
+        // register listeners
+        TestListener addNodeListener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+        TestListener removeNodeListener = addListener(1, Event.NODE_REMOVED, null, false, null, null, false);
+
+        // move node
+        String newPath = getRoot().getPath() + '/' + node2;
+        getWorkspace().move(n2.getPath(), newPath);
+        save();
+
+        // event handling
+        addNodeListener.waitForEvents();
+        removeListener(addNodeListener);
+        removeNodeListener.waitForEvents();
+        removeListener(removeNodeListener);
+
+        // tests
+        checkResults(addNodeListener);
+        checkResults(removeNodeListener);
+        assertTrue("Path for new location of moved node is wrong: actual=" + addNodeListener.getEvents().get(0).getPath()
+                   + ", expected=" + newPath, containsPath(addNodeListener, newPath));
+        assertTrue("Path for old location of moved node is wrong: actual=" + removeNodeListener.getEvents().get(0).getPath()
+                   + ", expected=" + oldPath, containsPath(removeNodeListener, oldPath));
+    }
+
+    /**
+     * @throws Exception
+     * @see NodeMovedTest#testMoveTree()
+     */
+    @Test
+    public void shouldTestNodeMovedTest_testMoveTree() throws Exception {
+        // setup
+        Node n1 = getRoot().addNode("node1", UNSTRUCTURED);
+        String oldPath = n1.getPath();
+        n1.addNode("node2", UNSTRUCTURED);
+        save();
+
+        // register listeners
+        TestListener addNodeListener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+        TestListener removeNodeListener = addListener(1, Event.NODE_REMOVED, null, false, null, null, false);
+
+        // move node
+        String newPath = getRoot().getPath() + "/node3";
+        getWorkspace().move(n1.getPath(), newPath);
+        save();
+
+        // event handling
+        addNodeListener.waitForEvents();
+        removeListener(addNodeListener);
+        removeNodeListener.waitForEvents();
+        removeListener(removeNodeListener);
+
+        // tests
+        checkResults(addNodeListener);
+        checkResults(removeNodeListener);
+        assertTrue("Path for new location of moved node is wrong: actual=" + addNodeListener.getEvents().get(0).getPath()
+                   + ", expected=" + newPath, containsPath(addNodeListener, newPath));
+        assertTrue("Path for old location of moved node is wrong: actual=" + removeNodeListener.getEvents().get(0).getPath()
+                   + ", expected=" + oldPath, containsPath(removeNodeListener, oldPath));
+    }
+
+    /**
+     * @throws Exception
+     * @see NodeMovedTest#testMoveWithRemove()
+     */
+    @Test
+    public void shouldTestNodeMovedTest_testMoveWithRemove() throws Exception {
+        // setup
+        String node2 = "node2";
+        Node n1 = getRoot().addNode("node1", UNSTRUCTURED);
+        Node n2 = n1.addNode(node2, UNSTRUCTURED);
+        Node n3 = getRoot().addNode("node3", UNSTRUCTURED);
+        save();
+
+        // register listeners
+        TestListener addNodeListener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+        TestListener removeNodeListener = addListener(2, Event.NODE_REMOVED, null, false, null, null, false);
+
+        // move node
+        String oldPath = n2.getPath();
+        String newPath = n3.getPath() + '/' + node2;
+        getWorkspace().move(oldPath, newPath);
+
+        // remove node
+        String removedNodePath = n1.getPath();
+        n1.remove();
+        save();
+
+        // event handling
+        addNodeListener.waitForEvents();
+        removeListener(addNodeListener);
+        removeNodeListener.waitForEvents();
+        removeListener(removeNodeListener);
+
+        // tests
+        checkResults(addNodeListener);
+        checkResults(removeNodeListener);
+        assertTrue("Path for new location of moved node is wrong: actual=" + addNodeListener.getEvents().get(0).getPath()
+                   + ", expected=" + newPath, containsPath(addNodeListener, newPath));
+        assertTrue("Path for removed node is wrong", containsPath(removeNodeListener, removedNodePath));
+        assertTrue("Path for old path of moved node is wrong", containsPath(removeNodeListener, oldPath));
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.NodeReorderTest
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see NodeReorderTest#testNodeReorder()
+     */
+    @Test
+    public void shouldTestNodeReorderTest_testNodeReorder() throws Exception {
+        // setup
+        getRoot().addNode("node1", UNSTRUCTURED);
+        Node n2 = getRoot().addNode("node2", UNSTRUCTURED);
+        Node n3 = getRoot().addNode("node3", UNSTRUCTURED);
+        save();
+
+        // register listeners
+        TestListener addNodeListener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+        TestListener removeNodeListener = addListener(1, Event.NODE_REMOVED, null, false, null, null, false);
+
+        // reorder to trigger events
+        getRoot().orderBefore(n3.getName(), n2.getName());
+        save();
+
+        // handle events
+        addNodeListener.waitForEvents();
+        removeListener(addNodeListener);
+        removeNodeListener.waitForEvents();
+        removeListener(removeNodeListener);
+
+        // tests
+        checkResults(addNodeListener);
+        checkResults(removeNodeListener);
+        assertTrue("Added reordered node has wrong path: actual=" + addNodeListener.getEvents().get(0).getPath() + ", expected="
+                   + n3.getPath(), containsPath(addNodeListener, n3.getPath()));
+        assertTrue("Removed reordered node has wrong path: actual=" + removeNodeListener.getEvents().get(0).getPath()
+                   + ", expected=" + n3.getPath(), containsPath(addNodeListener, n3.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see NodeReorderTest#testNodeReorderSameName()
+     */
+    @Test
+    public void shouldTestNodeReorderTest_testNodeReorderSameName() throws Exception {
+        // setup
+        String node1 = "node1";
+        Node n1 = getRoot().addNode(node1, UNSTRUCTURED);
+        getRoot().addNode(node1, UNSTRUCTURED);
+        getRoot().addNode(node1, UNSTRUCTURED);
+        save();
+
+        // register listeners + "[2]"
+        TestListener addNodeListener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+        TestListener removeNodeListener = addListener(1, Event.NODE_REMOVED, null, false, null, null, false);
+
+        // reorder to trigger events
+        getRoot().orderBefore(node1 + "[3]", node1 + "[2]");
+        save();
+
+        // handle events
+        addNodeListener.waitForEvents();
+        removeListener(addNodeListener);
+        removeNodeListener.waitForEvents();
+        removeListener(removeNodeListener);
+
+        // tests
+        checkResults(addNodeListener);
+        checkResults(removeNodeListener);
+        assertTrue("Added reordered node has wrong path: actual=" + addNodeListener.getEvents().get(0).getPath() + ", expected="
+                   + n1.getPath() + "[2]", containsPath(addNodeListener, n1.getPath() + "[2]"));
+        assertTrue("Removed reordered node has wrong path: actual=" + removeNodeListener.getEvents().get(0).getPath()
+                   + ", expected=" + n1.getPath() + "[3]", containsPath(removeNodeListener, n1.getPath() + "[3]"));
+    }
+
+    /**
+     * @throws Exception
+     * @see NodeReorderTest#testNodeReorderSameNameWithRemove()
+     */
+    @Test
+    public void shouldTestNodeReorderTest_testNodeReorderSameNameWithRemove() throws Exception {
+        // setup
+        String node1 = "node1";
+        Node n1 = getRoot().addNode(node1, UNSTRUCTURED);
+        getRoot().addNode("node2", UNSTRUCTURED);
+        getRoot().addNode(node1, UNSTRUCTURED);
+        getRoot().addNode(node1, UNSTRUCTURED);
+        Node n3 = getRoot().addNode("node3", UNSTRUCTURED);
+        save();
+
+        // register listeners + "[2]"
+        TestListener addNodeListener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+        TestListener removeNodeListener = addListener(2, Event.NODE_REMOVED, null, false, null, null, false);
+
+        // trigger events
+        getRoot().orderBefore(node1 + "[2]", null);
+        String removedPath = n3.getPath();
+        n3.remove();
+        save();
+
+        // handle events
+        addNodeListener.waitForEvents();
+        removeListener(addNodeListener);
+        removeNodeListener.waitForEvents();
+        removeListener(removeNodeListener);
+
+        // tests
+        checkResults(addNodeListener);
+        checkResults(removeNodeListener);
+        assertTrue("Added reordered node has wrong path: actual=" + addNodeListener.getEvents().get(0).getPath() + ", expected="
+                   + n1.getPath() + "[3]", containsPath(addNodeListener, n1.getPath() + "[3]"));
+        assertTrue("Removed reordered node path not found: " + n1.getPath() + "[2]", containsPath(removeNodeListener,
+                                                                                                  n1.getPath() + "[2]"));
+        assertTrue("Removed node path not found: " + removedPath, containsPath(removeNodeListener, removedPath));
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.PropertyAddedTest
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see PropertyAddedTest#testMultiPropertyAdded()
+     */
+    @Test
+    public void shouldTestPropertyAddedTest_testMultiPropertyAdded() throws Exception {
+        // setup
+        Node node = getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // register listener
+        TestListener listener = addListener(2, Event.PROPERTY_ADDED, null, false, null, null, false);
+
+        // add multiple properties
+        Property prop1 = node.setProperty("prop1", "prop1 content");
+        Property prop2 = node.setProperty("prop2", "prop2 content");
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for first added property not found: " + prop1.getPath(), containsPath(listener, prop1.getPath()));
+        assertTrue("Path for second added property not found: " + prop2.getPath(), containsPath(listener, prop2.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see PropertyAddedTest#testSinglePropertyAdded()
+     */
+    @Test
+    public void shouldTestPropertyAddedTest_testSinglePropertyAdded() throws Exception {
+        // setup
+        Node node = getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // register listener
+        TestListener listener = addListener(1, Event.PROPERTY_ADDED, null, false, null, null, false);
+
+        // add the property
+        Property prop1 = node.setProperty("prop1", "prop1 content");
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for added property is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + prop1.getPath(), containsPath(listener, prop1.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see PropertyAddedTest#testSystemGenerated()
+     */
+    @Test
+    public void shouldTestPropertyAddedTest_testSystemGenerated() throws Exception {
+        // register listener
+        TestListener listener = addListener(3, Event.PROPERTY_ADDED, null, false, null, null, false);
+
+        // create node (which adds 3 properties)
+        Node node = getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for jrc:primaryType property was not found.",
+                   containsPath(listener, node.getProperty("jcr:primaryType").getPath()));
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.PropertyChangedTests
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see PropertyChangedTest#testMultiPropertyChanged()
+     */
+    @Test
+    public void shouldTestPropertyChangedTests_testMultiPropertyChanged() throws Exception {
+        // setup
+        Node node = getRoot().addNode("node1", UNSTRUCTURED);
+        Property prop1 = node.setProperty("prop1", "prop1 content");
+        Property prop2 = node.setProperty("prop2", "prop2 content");
+        save();
+
+        // register listener
+        TestListener listener = addListener(2, Event.PROPERTY_CHANGED, null, false, null, null, false);
+
+        // add multiple properties
+        prop1.setValue("prop1 modified content");
+        prop2.setValue("prop2 modified content");
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for first changed property not found: " + prop1.getPath(), containsPath(listener, prop1.getPath()));
+        assertTrue("Path for second changed property not found: " + prop2.getPath(), containsPath(listener, prop2.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see PropertyChangedTest#testPropertyRemoveCreate()
+     */
+    @Test
+    public void shouldTestPropertyChangedTests_testPropertyRemoveCreate() throws Exception {
+        // setup
+        Node node = getRoot().addNode("node1", UNSTRUCTURED);
+        String propName = "prop1";
+        Property prop = node.setProperty(propName, propName + " content");
+        String propPath = prop.getPath();
+        save();
+
+        // register listeners
+        TestListener listener1 = addListener(1, Event.PROPERTY_CHANGED, null, false, null, null, false);
+        TestListener listener2 = addListener(2, Event.PROPERTY_ADDED | Event.PROPERTY_REMOVED, null, false, null, null, false);
+
+        // trigger events
+        prop.remove();
+        node.setProperty(propName, true);
+        save();
+
+        // event handling
+        listener1.waitForEvents();
+        removeListener(listener1);
+        listener2.waitForEvents();
+        removeListener(listener2);
+
+        // tests
+        if (listener1.getEvents().size() == 1) {
+            checkResults(listener1);
+            assertTrue("Path for removed then added property is wrong: actual=" + listener1.getEvents().get(0).getPath()
+                       + ", expected=" + propPath, containsPath(listener1, propPath));
+        } else {
+            checkResults(listener2);
+            assertTrue("Path for removed then added property is wrong: actual=" + listener2.getEvents().get(0).getPath()
+                       + ", expected=" + propPath, containsPath(listener2, propPath));
+            assertTrue("Path for removed then added property is wrong: actual=" + listener2.getEvents().get(1).getPath()
+                       + ", expected=" + propPath, containsPath(listener2, propPath));
+        }
+    }
+
+    /**
+     * @throws Exception
+     * @see PropertyChangedTest#testSinglePropertyChanged()
+     */
+    @Test
+    public void shouldTestPropertyChangedTests_testSinglePropertyChanged() throws Exception {
+        // setup
+        Node node = getRoot().addNode("node1", UNSTRUCTURED);
+        Property prop1 = node.setProperty("prop1", "prop1 content");
+        save();
+
+        // register listener
+        TestListener listener = addListener(1, Event.PROPERTY_CHANGED, null, false, null, null, false);
+
+        // change the property
+        prop1.setValue("prop1 modified content");
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for changed property is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + prop1.getPath(), containsPath(listener, prop1.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see PropertyChangedTest#testSinglePropertyChangedWithAdded()
+     */
+    @Test
+    public void shouldTestPropertyChangedTests_testSinglePropertyChangedWithAdded() throws Exception {
+        // setup
+        Node node = getRoot().addNode("node1", UNSTRUCTURED);
+        Property prop1 = node.setProperty("prop1", "prop1 content");
+        save();
+
+        // register listener
+        TestListener listener = addListener(1, Event.PROPERTY_CHANGED, null, false, null, null, false);
+
+        // change the property
+        prop1.setValue("prop1 modified content");
+        node.setProperty("prop2", "prop2 content"); // property added event should not be received
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for changed property is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + prop1.getPath(), containsPath(listener, prop1.getPath()));
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.PropertyRemovedTest
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see PropertyRemovedTest#testMultiPropertyRemoved()
+     */
+    @Test
+    public void shouldTestPropertyRemovedTest_testMultiPropertyRemoved() throws Exception {
+        // setup
+        Node node = getRoot().addNode("node1", UNSTRUCTURED);
+        Property prop1 = node.setProperty("prop1", "prop1 content");
+
+        Property prop2 = node.setProperty("prop2", "prop2 content");
+        save();
+
+        // register listener
+        TestListener listener = addListener(2, Event.PROPERTY_REMOVED, null, false, null, null, false);
+
+        // remove the property
+        String prop1Path = prop1.getPath();
+        prop1.remove();
+        String prop2Path = prop2.getPath();
+        prop2.remove();
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for first removed property not found: " + prop1Path, containsPath(listener, prop1Path));
+        assertTrue("Path for second removed property not found: " + prop2Path, containsPath(listener, prop2Path));
+    }
+
+    /**
+     * @throws Exception
+     * @see PropertyRemovedTest#testSinglePropertyRemoved()
+     */
+    @Test
+    public void shouldTestPropertyRemovedTest_testSinglePropertyRemoved() throws Exception {
+        // setup
+        Node node = getRoot().addNode("node1", UNSTRUCTURED);
+        Property prop = node.setProperty("prop1", "prop1 content");
+        String propPath = prop.getPath();
+        save();
+
+        // register listener
+        TestListener listener = addListener(1, Event.PROPERTY_REMOVED, null, false, null, null, false);
+
+        // remove the property
+        prop.remove();
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for removed property is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + propPath, containsPath(listener, propPath));
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.AddEventListenerTest
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see AddEventListenerTest#testIsDeepFalseNodeAdded()
+     */
+    @Test
+    public void shouldTestAddEventListenerTest_testIsDeepFalseNodeAdded() throws Exception {
+        // setup
+        String node1 = "node1";
+        String path = getRoot().getPath() + '/' + node1;
+
+        // register listener
+        TestListener listener = addListener(1, Event.NODE_ADDED, path, false, null, null, false);
+
+        // add child node under the path we care about
+        Node n1 = getRoot().addNode(node1, UNSTRUCTURED);
+        Node childNode = n1.addNode("node2", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        checkResults(listener);
+        assertTrue("Child node path is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + childNode.getPath(), containsPath(listener, childNode.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see AddEventListenerTest#testIsDeepFalsePropertyAdded()
+     */
+    @Test
+    @Ignore
+    public void shouldTestAddEventListenerTest_testIsDeepFalsePropertyAdded() throws Exception {
+        // setup
+        Node n1 = getRoot().addNode("node1", UNSTRUCTURED);
+        Node n2 = getRoot().addNode("node2", UNSTRUCTURED);
+        save();
+
+        // register listener
+        TestListener listener = addListener(1, Event.PROPERTY_ADDED, n1.getPath(), false, null, null, false);
+
+        // add property
+        String prop = "prop";
+        Property n1Prop = n1.setProperty(prop, "foo");
+        n2.setProperty(prop, "foo"); // should not receive event for this
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for added property is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + n1Prop.getPath(), containsPath(listener, n1Prop.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see AddEventListenerTest#testNodeType()
+     */
+    @Test
+    public void shouldTestAddEventListenerTest_testNodeType() throws Exception {
+        // setup
+        Node n1 = getRoot().addNode("node1", UNSTRUCTURED);
+        n1.addMixin(LOCK_MIXIN);
+        Node n2 = getRoot().addNode("node2", UNSTRUCTURED);
+        save();
+
+        // register listener
+        TestListener listener = addListener(1, Event.NODE_ADDED, getRoot().getPath(), true, null, new String[] {REF_MIXIN}, false);
+
+        // trigger events
+        String node3 = "node3";
+        Node n3 = n1.addNode(node3, NT_BASE);
+        n2.addNode(node3, UNSTRUCTURED);
+        save();
+
+        // handle events
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Wrong path: actual=" + listener.getEvents().get(0).getPath() + ", expected=" + n3.getPath(),
+                   containsPath(listener, n3.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see AddEventListenerTest#testNoLocalTrue()
+     */
+    @Test
+    public void shouldTestAddEventListenerTest_testNoLocalTrue() throws Exception {
+        // register listener
+        TestListener listener = addListener(0, Event.NODE_ADDED, getRoot().getPath(), true, null, null, true);
+
+        // trigger events
+        getRoot().addNode("node1", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+    }
+
+    /**
+     * @throws Exception
+     * @see AddEventListenerTest#testPath()
+     */
+    @Test
+    public void shouldTestAddEventListenerTest_testPath() throws Exception {
+        // setup
+        String node1 = "node1";
+        String path = getRoot().getPath() + '/' + node1;
+
+        // register listener
+        TestListener listener = addListener(1, Event.NODE_ADDED, path, true, null, null, false);
+
+        // add child node under the path we care about
+        Node n1 = getRoot().addNode(node1, UNSTRUCTURED);
+        Node childNode = n1.addNode("node2", UNSTRUCTURED);
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Child node path is wrong: actual=" + listener.getEvents().get(0).getPath() + ", expected="
+                   + childNode.getPath(), containsPath(listener, childNode.getPath()));
+    }
+
+    /**
+     * @throws Exception
+     * @see AddEventListenerTest#testUUID()
+     */
+    @Test
+    public void shouldTestAddEventListenerTest_testUUID() throws Exception {
+        // setup
+        Node n1 = getRoot().addNode("node1", UNSTRUCTURED);
+        n1.addMixin(REF_MIXIN);
+        Node n2 = getRoot().addNode("node2", UNSTRUCTURED);
+        n2.addMixin(REF_MIXIN);
+        save();
+
+        // register listener
+        TestListener listener = addListener(1,
+                                            Event.PROPERTY_ADDED,
+                                            getRoot().getPath(),
+                                            true,
+                                            new String[] {n1.getUUID()},
+                                            null,
+                                            false);
+
+        // create properties
+        String prop1 = "prop1";
+        Property n1Prop = n1.setProperty(prop1, "foo");
+        n2.setProperty(prop1, "foo"); // should not get an event for this
+        save();
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Wrong path: actual=" + listener.getEvents().get(0).getPath() + ", expected=" + n1Prop.getPath(),
+                   containsPath(listener, n1Prop.getPath()));
+    }
+
+    // ===========================================================================================================================
+    // @see org.apache.jackrabbit.test.api.observation.WorkspaceOperationTest
+    // ===========================================================================================================================
+
+    /**
+     * @throws Exception
+     * @see WorkspaceOperationTest#testCopy()
+     */
+    @Test
+    @Ignore
+    public void shouldTestWorkspaceOperationTest_testCopy() throws Exception {
+        // setup
+        Node addedNode = getRoot().addNode("node1", UNSTRUCTURED);
+        String node2 = "node2";
+        addedNode.addNode(node2, UNSTRUCTURED);
+        save();
+
+        // register listener
+        TestListener listener = addListener(2, Event.NODE_ADDED, null, false, null, null, false);
+
+        // perform copy
+        String targetPath = getRoot().getPath() + "/node3";
+        getWorkspace().copy(addedNode.getPath(), targetPath);
+
+        // event handling
+        listener.waitForEvents();
+        removeListener(listener);
+
+        // tests
+        checkResults(listener);
+        assertTrue("Path for copied node not found: " + targetPath, containsPath(listener, targetPath));
+        assertTrue("Path for copied child node not found: " + (targetPath + '/' + node2),
+                   containsPath(listener, (targetPath + '/' + node2)));
+    }
+
+    /**
+     * @throws Exception
+     * @see WorkspaceOperationTest#testMove()
+     */
+    @Test
+    public void shouldTestWorkspaceOperationTest_testMove() throws Exception {
+        // setup
+        String node2 = "node2";
+        Node n1 = getRoot().addNode("node1", UNSTRUCTURED);
+        n1.addNode(node2, UNSTRUCTURED);
+        Node n3 = getRoot().addNode("node3", UNSTRUCTURED);
+        save();
+
+        // register listeners
+        TestListener addNodeListener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+        TestListener removeNodeListener = addListener(1, Event.NODE_REMOVED, null, false, null, null, false);
+
+        // perform move
+        String oldPath = n1.getPath();
+        String targetPath = n3.getPath() + "/node4";
+        getWorkspace().move(oldPath, targetPath);
+        save();
+
+        // event handling
+        addNodeListener.waitForEvents();
+        removeListener(addNodeListener);
+        removeNodeListener.waitForEvents();
+        removeListener(removeNodeListener);
+
+        // tests
+        checkResults(addNodeListener);
+        checkResults(removeNodeListener);
+        assertTrue("Path for new location of moved node is wrong: actual=" + addNodeListener.getEvents().get(0).getPath()
+                   + ", expected=" + targetPath, containsPath(addNodeListener, targetPath));
+        assertTrue("Path for old location of moved node is wrong: actual=" + removeNodeListener.getEvents().get(0).getPath()
+                   + ", expected=" + oldPath, containsPath(removeNodeListener, oldPath));
+    }
+
+    /**
+     * @throws Exception
+     * @see WorkspaceOperationTest#testRename()
+     */
+    @Test
+    public void shouldTestWorkspaceOperationTest_testRename() throws Exception {
+        // setup
+        Node n1 = getRoot().addNode("node1", UNSTRUCTURED);
+        n1.addNode("node2", UNSTRUCTURED);
+        save();
+
+        // register listeners
+        TestListener addNodeListener = addListener(1, Event.NODE_ADDED, null, false, null, null, false);
+        TestListener removeNodeListener = addListener(1, Event.NODE_REMOVED, null, false, null, null, false);
+
+        // rename node
+        String oldPath = n1.getPath();
+        String renamedPath = getRoot().getPath() + "/node3";
+        getWorkspace().move(oldPath, renamedPath);
+        save();
+
+        // event handling
+        addNodeListener.waitForEvents();
+        removeListener(addNodeListener);
+
+        removeNodeListener.waitForEvents();
+        removeListener(removeNodeListener);
+
+        // tests
+        checkResults(addNodeListener);
+        checkResults(removeNodeListener);
+        assertTrue("Path for renamed node is wrong: actual=" + addNodeListener.getEvents().get(0).getPath() + ", expected="
+                   + renamedPath, containsPath(addNodeListener, renamedPath));
+        assertTrue("Path for old name of renamed node is wrong: actual=" + removeNodeListener.getEvents().get(0).getPath()
+                   + ", expected=" + oldPath, containsPath(removeNodeListener, oldPath));
+    }
+
+    // ===========================================================================================================================
+    // Inner Class
+    // ===========================================================================================================================
+
+    private class TestListener implements EventListener {
+
+        private String errorMessage;
+        private final List<Event> events;
+        private int eventsProcessed = 0;
+        private final int eventTypes;
+        private final int expectedEvents;
+        private final CountDownLatch latch;
+
+        public TestListener( int expectedEvents,
+                             int eventTypes ) {
+            this.eventTypes = eventTypes;
+            this.expectedEvents = expectedEvents;
+            this.events = new ArrayList<Event>();
+
+            // if no events are expected set it to 1 and let the timeout stop the test
+            this.latch = new CountDownLatch((this.expectedEvents == 0) ? 1 : this.expectedEvents);
+        }
+
+        public int getActualEventCount() {
+            return this.eventsProcessed;
+        }
+
+        public String getErrorMessage() {
+            return this.errorMessage;
+        }
+
+        public List<Event> getEvents() {
+            return this.events;
+        }
+
+        public int getExpectedEventCount() {
+            return this.expectedEvents;
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @see javax.jcr.observation.EventListener#onEvent(javax.jcr.observation.EventIterator)
+         */
+        public void onEvent( EventIterator itr ) {
+            long position = itr.getPosition();
+
+            // iterator position must be set initially zero
+            if (position == 0) {
+                while (itr.hasNext()) {
+                    this.latch.countDown();
+                    Event event = itr.nextEvent();
+
+                    // check iterator position
+                    if (++position != itr.getPosition()) {
+                        this.errorMessage = "EventIterator position was " + itr.getPosition() + " and should be " + position;
+                        break;
+                    }
+
+                    this.events.add(event);
+                    ++this.eventsProcessed;
+
+                    // check event type
+                    int eventType = event.getType();
+
+                    if ((this.eventTypes & eventType) == 0) {
+                        this.errorMessage = "Received a wrong event type of " + eventType;
+                        break;
+                    }
+                }
+            } else {
+                this.errorMessage = "EventIterator position was not initially set to zero";
+            }
+        }
+
+        public void waitForEvents() throws Exception {
+            this.latch.await(5, TimeUnit.SECONDS);
+        }
+    }
+
+}
Index: src/test/java/org/jboss/dna/jcr/AbstractSessionTest.java
===================================================================
--- src/test/java/org/jboss/dna/jcr/AbstractSessionTest.java	(revision 1377)
+++ src/test/java/org/jboss/dna/jcr/AbstractSessionTest.java	(working copy)
@@ -41,6 +41,7 @@
 import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
 import org.jboss.dna.graph.connector.RepositorySourceException;
 import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource;
+import org.jboss.dna.graph.observe.MockObservable;
 import org.jboss.dna.graph.property.NamespaceRegistry;
 import org.jboss.dna.graph.property.Path;
 import org.jboss.dna.graph.property.PathFactory;
@@ -148,6 +149,7 @@
                 return graph;
             }
         });
+        stub(this.repository.getRepositoryObservable()).toReturn(new MockObservable());
 
         Path locksPath = pathFactory.createAbsolutePath(JcrLexicon.SYSTEM, DnaLexicon.LOCKS);
         workspaceLockManager = new WorkspaceLockManager(context, repository, workspaceName, locksPath);
Index: src/test/java/org/jboss/dna/jcr/ImportExportTest.java
===================================================================
--- src/test/java/org/jboss/dna/jcr/ImportExportTest.java	(revision 1377)
+++ src/test/java/org/jboss/dna/jcr/ImportExportTest.java	(working copy)
@@ -37,6 +37,7 @@
 import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
 import org.jboss.dna.graph.connector.RepositorySourceException;
 import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource;
+import org.jboss.dna.graph.observe.MockObservable;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
@@ -83,7 +84,7 @@
             }
         };
 
-        repository = new JcrRepository(context, connectionFactory, "unused");
+        repository = new JcrRepository(context, connectionFactory, "unused", new MockObservable(), null, null);
 
         SecurityContext mockSecurityContext = new MockSecurityContext("testuser",
                                                                       Collections.singleton(JcrSession.DNA_WRITE_PERMISSION));
Index: src/test/java/org/jboss/dna/jcr/JcrTckTest.java
===================================================================
--- src/test/java/org/jboss/dna/jcr/JcrTckTest.java	(revision 1377)
+++ src/test/java/org/jboss/dna/jcr/JcrTckTest.java	(working copy)
@@ -81,6 +81,19 @@
 import org.apache.jackrabbit.test.api.WorkspaceMoveSameNameSibsTest;
 import org.apache.jackrabbit.test.api.WorkspaceMoveTest;
 import org.apache.jackrabbit.test.api.WorkspaceMoveVersionableTest;
+import org.apache.jackrabbit.test.api.observation.AddEventListenerTest;
+import org.apache.jackrabbit.test.api.observation.EventIteratorTest;
+import org.apache.jackrabbit.test.api.observation.EventTest;
+import org.apache.jackrabbit.test.api.observation.GetRegisteredEventListenersTest;
+import org.apache.jackrabbit.test.api.observation.LockingTest;
+import org.apache.jackrabbit.test.api.observation.NodeAddedTest;
+import org.apache.jackrabbit.test.api.observation.NodeMovedTest;
+import org.apache.jackrabbit.test.api.observation.NodeRemovedTest;
+import org.apache.jackrabbit.test.api.observation.NodeReorderTest;
+import org.apache.jackrabbit.test.api.observation.PropertyAddedTest;
+import org.apache.jackrabbit.test.api.observation.PropertyChangedTest;
+import org.apache.jackrabbit.test.api.observation.PropertyRemovedTest;
+import org.apache.jackrabbit.test.api.observation.WorkspaceOperationTest;
 
 /**
  * Test suite to wrap Apache Jackrabbit JCR technology compatibility kit (TCK) unit tests. Note that technically these are not the
@@ -272,10 +285,32 @@
             // We currently don't pass the tests in those suites that are commented out
             // See https://jira.jboss.org/jira/browse/DNA-285
 
-            // addTest(org.apache.jackrabbit.test.api.observation.TestAll.suite());
+            addTest(new ObservationTests()); // remove this and the ObservationTests inner class when all tests pass and uncomment observation.TestAll
+//            addTest(org.apache.jackrabbit.test.api.observation.TestAll.suite());
             // addTest(org.apache.jackrabbit.test.api.version.TestAll.suite());
             addTest(org.apache.jackrabbit.test.api.lock.TestAll.suite());
-            addTest(org.apache.jackrabbit.test.api.util.TestAll.suite());
+//            addTest(org.apache.jackrabbit.test.api.util.TestAll.suite());
+        }
+    }
+    
+    private static class ObservationTests extends TestSuite {
+        protected ObservationTests() {
+            super("JCR Observation Tests");
+            
+            // these are the tests included in observation.TestAll.suite()
+            addTestSuite(EventIteratorTest.class);
+            addTestSuite(EventTest.class);
+            addTestSuite(GetRegisteredEventListenersTest.class);
+            addTestSuite(LockingTest.class);
+            addTestSuite(NodeAddedTest.class);
+//            addTestSuite(NodeRemovedTest.class);
+            addTestSuite(NodeMovedTest.class);
+            addTestSuite(NodeReorderTest.class);
+            addTestSuite(PropertyAddedTest.class);
+            addTestSuite(PropertyChangedTest.class);
+            addTestSuite(PropertyRemovedTest.class);
+//            addTestSuite(AddEventListenerTest.class);
+//            addTestSuite(WorkspaceOperationTest.class);
         }
     }
 }
Index: src/test/java/org/jboss/dna/jcr/JcrWorkspaceTest.java
===================================================================
--- src/test/java/org/jboss/dna/jcr/JcrWorkspaceTest.java	(revision 1377)
+++ src/test/java/org/jboss/dna/jcr/JcrWorkspaceTest.java	(working copy)
@@ -29,7 +29,6 @@
 import java.io.ByteArrayInputStream;
 import javax.jcr.NamespaceRegistry;
 import javax.jcr.Node;
-import javax.jcr.UnsupportedRepositoryOperationException;
 import javax.jcr.query.Query;
 import javax.jcr.query.QueryManager;
 import org.jboss.dna.graph.JcrLexicon;
@@ -133,9 +132,9 @@
         assertThat(workspace.getNodeTypeManager(), is(notNullValue()));
     }
 
-    @Test( expected = UnsupportedRepositoryOperationException.class )
-    public void shouldNotAllowGetObservationManager() throws Exception {
-        workspace.getObservationManager();
+    @Test
+    public void shouldGetObservationManager() throws Exception {
+        assertThat(workspace.getObservationManager(), is(notNullValue()));
     }
 
     @Test
Index: src/test/java/org/jboss/dna/jcr/AbstractJcrAccessTest.java
===================================================================
--- src/test/java/org/jboss/dna/jcr/AbstractJcrAccessTest.java	(revision 1377)
+++ src/test/java/org/jboss/dna/jcr/AbstractJcrAccessTest.java	(working copy)
@@ -38,6 +38,7 @@
 import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
 import org.jboss.dna.graph.connector.RepositorySourceException;
 import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource;
+import org.jboss.dna.graph.observe.MockObservable;
 import org.jboss.dna.graph.property.PathFactory;
 import org.junit.After;
 import org.junit.Before;
@@ -82,7 +83,7 @@
             }
         };
 
-        repository = new JcrRepository(context, connectionFactory, "unused");
+        repository = new JcrRepository(context, connectionFactory, "unused", new MockObservable(), null, null);
 
         SecurityContext mockSecurityContext = new MockSecurityContext("testuser",
                                                                       Collections.singleton(JcrSession.DNA_WRITE_PERMISSION));
