View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors.
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   * 
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.jcr;
25  
26  import static org.modeshape.graph.JcrLexicon.MIXIN_TYPES;
27  import static org.modeshape.graph.JcrLexicon.PRIMARY_TYPE;
28  import java.util.ArrayList;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.NoSuchElementException;
35  import java.util.UUID;
36  import java.util.concurrent.ConcurrentHashMap;
37  import javax.jcr.RangeIterator;
38  import javax.jcr.RepositoryException;
39  import javax.jcr.observation.Event;
40  import javax.jcr.observation.EventIterator;
41  import javax.jcr.observation.EventJournal;
42  import javax.jcr.observation.EventListener;
43  import javax.jcr.observation.EventListenerIterator;
44  import javax.jcr.observation.ObservationManager;
45  import net.jcip.annotations.Immutable;
46  import net.jcip.annotations.NotThreadSafe;
47  import org.modeshape.common.util.CheckArg;
48  import org.modeshape.common.util.Logger;
49  import org.modeshape.graph.ExecutionContext;
50  import org.modeshape.graph.Graph;
51  import org.modeshape.graph.Location;
52  import org.modeshape.graph.observe.Changes;
53  import org.modeshape.graph.observe.NetChangeObserver;
54  import org.modeshape.graph.observe.Observable;
55  import org.modeshape.graph.property.DateTime;
56  import org.modeshape.graph.property.Name;
57  import org.modeshape.graph.property.NamespaceRegistry;
58  import org.modeshape.graph.property.Path;
59  import org.modeshape.graph.property.PathFactory;
60  import org.modeshape.graph.property.Property;
61  import org.modeshape.graph.property.UuidFactory;
62  import org.modeshape.graph.property.ValueFactories;
63  import org.modeshape.graph.property.ValueFactory;
64  import org.modeshape.graph.property.ValueFormatException;
65  import org.modeshape.graph.request.ChangeRequest;
66  
67  /**
68   * The implementation of JCR {@link ObservationManager}.
69   */
70  final class JcrObservationManager implements ObservationManager {
71  
72      /**
73       * The key for storing the {@link JcrObservationManager#setUserData(String) observation user data} in the
74       * {@link ExecutionContext}'s {@link ExecutionContext#getData() data}.
75       */
76      static final String OBSERVATION_USER_DATA_KEY = "org.modeshape.jcr.observation.userdata";
77  
78      static final String MOVE_FROM_KEY = "srcAbsPath";
79      static final String MOVE_TO_KEY = "destAbsPath";
80      static final String ORDER_CHILD_KEY = "srcChildRelPath";
81      static final String ORDER_BEFORE_KEY = "destChildRelPath";
82  
83      /**
84       * The repository observable the JCR listeners will be registered with.
85       */
86      private final Observable repositoryObservable;
87  
88      /**
89       * The map of the JCR repository listeners and their associated wrapped class.
90       */
91      private final Map<EventListener, JcrListenerAdapter> listeners;
92  
93      /**
94       * The session's namespace registry used when handling events.
95       */
96      private final NamespaceRegistry namespaceRegistry;
97  
98      /**
99       * The associated session.
100      */
101     private final JcrSession session;
102 
103     /**
104      * The session's value factories.
105      */
106     private final ValueFactories valueFactories;
107 
108     private final ValueFactory<String> stringFactory;
109 
110     /**
111      * The name of the session's workspace; cached for performance reasons.
112      */
113     private final String workspaceName;
114 
115     /**
116      * @param session the owning session (never <code>null</code>)
117      * @param repositoryObservable the repository observable used to register JCR listeners (never <code>null</code>)
118      * @throws IllegalArgumentException if either parameter is <code>null</code>
119      */
120     public JcrObservationManager( JcrSession session,
121                                   Observable repositoryObservable ) {
122         CheckArg.isNotNull(session, "session");
123         CheckArg.isNotNull(repositoryObservable, "repositoryObservable");
124 
125         this.session = session;
126         this.repositoryObservable = repositoryObservable;
127         this.listeners = new ConcurrentHashMap<EventListener, JcrListenerAdapter>();
128         this.namespaceRegistry = this.session.getExecutionContext().getNamespaceRegistry();
129         this.valueFactories = this.session.getExecutionContext().getValueFactories();
130         this.stringFactory = this.valueFactories.getStringFactory();
131         this.workspaceName = this.session.getWorkspace().getName();
132     }
133 
134     /**
135      * {@inheritDoc}
136      * 
137      * @see javax.jcr.observation.ObservationManager#addEventListener(javax.jcr.observation.EventListener, int, java.lang.String,
138      *      boolean, java.lang.String[], java.lang.String[], boolean)
139      * @throws RepositoryException if the session is no longer live
140      * @throws IllegalArgumentException if <code>listener</code> is <code>null</code>
141      */
142     public synchronized void addEventListener( EventListener listener,
143                                                int eventTypes,
144                                                String absPath,
145                                                boolean isDeep,
146                                                String[] uuid,
147                                                String[] nodeTypeName,
148                                                boolean noLocal ) throws RepositoryException {
149         CheckArg.isNotNull(listener, "listener");
150         checkSession(); // make sure session is still active
151 
152         // create wrapper and register
153         JcrListenerAdapter adapter = new JcrListenerAdapter(listener, eventTypes, absPath, isDeep, uuid, nodeTypeName, noLocal);
154         // unregister if already registered
155         this.repositoryObservable.unregister(adapter);
156         this.repositoryObservable.register(adapter);
157         this.listeners.put(listener, adapter);
158     }
159 
160     /**
161      * @throws RepositoryException if session is not active
162      */
163     void checkSession() throws RepositoryException {
164         session.checkLive();
165     }
166 
167     /**
168      * @return the namespace registry used by listeners when handling events
169      */
170     NamespaceRegistry namespaceRegistry() {
171         return this.namespaceRegistry;
172     }
173 
174     final String stringFor( Path path ) {
175         return this.stringFactory.create(path);
176     }
177 
178     final String stringFor( Path.Segment segment ) {
179         return this.stringFactory.create(segment);
180     }
181 
182     final String stringFor( Name name ) {
183         return this.stringFactory.create(name);
184     }
185 
186     /**
187      * @return the node type manager
188      * @throws RepositoryException if there is a problem
189      */
190     JcrNodeTypeManager getNodeTypeManager() throws RepositoryException {
191         return (JcrNodeTypeManager)this.session.getWorkspace().getNodeTypeManager();
192     }
193 
194     /**
195      * {@inheritDoc}
196      * 
197      * @see javax.jcr.observation.ObservationManager#getRegisteredEventListeners()
198      */
199     public EventListenerIterator getRegisteredEventListeners() throws RepositoryException {
200         checkSession(); // make sure session is still active
201         return new JcrEventListenerIterator(this.listeners.keySet());
202     }
203 
204     /**
205      * @return the user ID used by the listeners when handling events
206      */
207     String getUserId() {
208         return this.session.getUserID();
209     }
210 
211     /**
212      * @return the value factories used by listeners when handling events
213      */
214     ValueFactories getValueFactories() {
215         return this.valueFactories;
216     }
217 
218     /**
219      * @return the workspace graph
220      */
221     Graph getGraph() {
222         return ((JcrWorkspace)this.session.getWorkspace()).graph();
223     }
224 
225     /**
226      * @return the session's unique identifier
227      */
228     String getSessionId() {
229         return this.session.sessionId();
230     }
231 
232     /**
233      * @return workspaceName
234      */
235     final String getWorkspaceName() {
236         return workspaceName;
237     }
238 
239     /**
240      * Remove all of the listeners. This is typically called when the {@link JcrSession#logout() session logs out}.
241      */
242     synchronized void removeAllEventListeners() {
243         for (JcrListenerAdapter listener : this.listeners.values()) {
244             assert (listener != null);
245             this.repositoryObservable.unregister(listener);
246         }
247 
248         this.listeners.clear();
249     }
250 
251     /**
252      * {@inheritDoc}
253      * 
254      * @see javax.jcr.observation.ObservationManager#removeEventListener(javax.jcr.observation.EventListener)
255      * @throws IllegalArgumentException if <code>listener</code> is <code>null</code>
256      */
257     public synchronized void removeEventListener( EventListener listener ) throws RepositoryException {
258         checkSession(); // make sure session is still active
259         CheckArg.isNotNull(listener, "listener");
260 
261         JcrListenerAdapter jcrListener = this.listeners.remove(listener);
262 
263         if (jcrListener != null) {
264             this.repositoryObservable.unregister(jcrListener);
265         }
266     }
267 
268     /**
269      * {@inheritDoc}
270      * <p>
271      * This method set's the user data on the {@link #session session's} {@link JcrSession#getExecutionContext() execution
272      * context}, under the {@link #OBSERVATION_USER_DATA_KEY} key.
273      * </p>
274      * 
275      * @see javax.jcr.observation.ObservationManager#setUserData(java.lang.String)
276      */
277     @Override
278     public void setUserData( String userData ) {
279         // User data value may be null
280         session.setSessionData(OBSERVATION_USER_DATA_KEY, userData);
281     }
282 
283     /**
284      * {@inheritDoc}
285      * <p>
286      * Since ModeShape does not support journaled observation, this method returns null.
287      * </p>
288      * 
289      * @see javax.jcr.observation.ObservationManager#getEventJournal()
290      */
291     @Override
292     public EventJournal getEventJournal() {
293         return null; // per the JavaDoc
294     }
295 
296     /**
297      * {@inheritDoc}
298      * <p>
299      * Since ModeShape does not support journaled observation, this method returns null.
300      * </p>
301      * 
302      * @see javax.jcr.observation.ObservationManager#getEventJournal(int, java.lang.String, boolean, java.lang.String[],
303      *      java.lang.String[])
304      */
305     @Override
306     public EventJournal getEventJournal( int eventTypes,
307                                          String absPath,
308                                          boolean isDeep,
309                                          String[] uuid,
310                                          String[] nodeTypeName ) {
311         return null;
312     }
313 
314     /**
315      * An implementation of JCR {@link RangeIterator} extended by the event and event listener iterators.
316      * 
317      * @param <E> the type being iterated over
318      */
319     class JcrRangeIterator<E> implements RangeIterator {
320 
321         /**
322          * The elements being iterated over.
323          */
324         private final List<? extends E> elements;
325 
326         /**
327          * The current position in the iterator.
328          */
329         private int position = 0;
330 
331         /**
332          * @param elements the elements to iterator over
333          * @throws IllegalArgumentException if <code>elements</code> is <code>null</code>
334          */
335         public JcrRangeIterator( Collection<? extends E> elements ) {
336             CheckArg.isNotNull(elements, "elements");
337             this.elements = new ArrayList<E>(elements);
338         }
339 
340         /**
341          * {@inheritDoc}
342          * 
343          * @see javax.jcr.RangeIterator#getPosition()
344          */
345         public long getPosition() {
346             return this.position;
347         }
348 
349         /**
350          * {@inheritDoc}
351          * 
352          * @see javax.jcr.RangeIterator#getSize()
353          */
354         public long getSize() {
355             return this.elements.size();
356         }
357 
358         /**
359          * {@inheritDoc}
360          * 
361          * @see java.util.Iterator#hasNext()
362          */
363         public boolean hasNext() {
364             return (getPosition() < getSize());
365         }
366 
367         /**
368          * {@inheritDoc}
369          * 
370          * @see java.util.Iterator#next()
371          */
372         public Object next() {
373             if (!hasNext()) {
374                 throw new NoSuchElementException();
375             }
376 
377             Object element = this.elements.get(this.position);
378             ++this.position;
379 
380             return element;
381         }
382 
383         /**
384          * {@inheritDoc}
385          * 
386          * @see java.util.Iterator#remove()
387          * @throws UnsupportedOperationException if called
388          */
389         public void remove() {
390             throw new UnsupportedOperationException();
391         }
392 
393         /**
394          * {@inheritDoc}
395          * 
396          * @see javax.jcr.RangeIterator#skip(long)
397          */
398         public void skip( long skipNum ) {
399             this.position += skipNum;
400 
401             if (!hasNext()) {
402                 throw new NoSuchElementException();
403             }
404         }
405     }
406 
407     /**
408      * An implementation of the JCR {@link EventListenerIterator}.
409      */
410     class JcrEventListenerIterator extends JcrRangeIterator<EventListener> implements EventListenerIterator {
411 
412         /**
413          * @param listeners the listeners being iterated over
414          * @throws IllegalArgumentException if <code>listeners</code> is <code>null</code>
415          */
416         public JcrEventListenerIterator( Collection<EventListener> listeners ) {
417             super(listeners);
418         }
419 
420         /**
421          * {@inheritDoc}
422          * 
423          * @see javax.jcr.observation.EventListenerIterator#nextEventListener()
424          */
425         public EventListener nextEventListener() {
426             return (EventListener)next();
427         }
428     }
429 
430     /**
431      * An implementation of JCR {@link EventIterator}.
432      */
433     class JcrEventIterator extends JcrRangeIterator<Event> implements EventIterator {
434 
435         /**
436          * @param events the events being iterated over
437          * @throws IllegalArgumentException if <code>events</code> is <code>null</code>
438          */
439         public JcrEventIterator( Collection<Event> events ) {
440             super(events);
441         }
442 
443         /**
444          * {@inheritDoc}
445          * 
446          * @see javax.jcr.observation.EventIterator#nextEvent()
447          */
448         public Event nextEvent() {
449             return (Event)next();
450         }
451     }
452 
453     /**
454      * The information related to and shared by a set of events that represent a single logical operation.
455      */
456     @Immutable
457     class JcrEventBundle {
458 
459         /**
460          * The date and time of the event bundle.
461          */
462         private final DateTime date;
463 
464         /**
465          * The user ID.
466          */
467         private final String userId;
468 
469         private final String userData;
470 
471         public JcrEventBundle( DateTime dateTime,
472                                String userId,
473                                String userData ) {
474             this.userId = userId;
475             this.userData = userData;
476             this.date = dateTime;
477         }
478 
479         public String getUserID() {
480             return this.userId;
481         }
482 
483         /**
484          * @return date
485          */
486         public DateTime getDate() {
487             return date;
488         }
489 
490         /**
491          * @return userData
492          */
493         public String getUserData() {
494             return userData;
495         }
496     }
497 
498     /**
499      * An implementation of JCR {@link Event}.
500      */
501     @Immutable
502     class JcrEvent implements Event {
503 
504         private final String id;
505 
506         /**
507          * The node path.
508          */
509         private final String path;
510 
511         /**
512          * The event type.
513          */
514         private final int type;
515 
516         /**
517          * The immutable bundle information, which may be shared amongst multiple events.
518          */
519         private final JcrEventBundle bundle;
520 
521         /**
522          * @param bundle the event bundle information
523          * @param type the event type
524          * @param path the node path
525          * @param id the node identifier
526          */
527         public JcrEvent( JcrEventBundle bundle,
528                          int type,
529                          String path,
530                          String id ) {
531             this.type = type;
532             this.path = path;
533             this.bundle = bundle;
534             this.id = id;
535         }
536 
537         /**
538          * {@inheritDoc}
539          * 
540          * @see javax.jcr.observation.Event#getPath()
541          */
542         public String getPath() {
543             return this.path;
544         }
545 
546         /**
547          * {@inheritDoc}
548          * 
549          * @see javax.jcr.observation.Event#getType()
550          */
551         public int getType() {
552             return this.type;
553         }
554 
555         /**
556          * {@inheritDoc}
557          * 
558          * @see javax.jcr.observation.Event#getUserID()
559          */
560         public String getUserID() {
561             return bundle.getUserID();
562         }
563 
564         /**
565          * {@inheritDoc}
566          * 
567          * @see javax.jcr.observation.Event#getDate()
568          */
569         @Override
570         public long getDate() {
571             return bundle.getDate().getMilliseconds();
572         }
573 
574         /**
575          * {@inheritDoc}
576          * 
577          * @see javax.jcr.observation.Event#getIdentifier()
578          */
579         @Override
580         public String getIdentifier() {
581             return id;
582         }
583 
584         /**
585          * {@inheritDoc}
586          * 
587          * @see javax.jcr.observation.Event#getUserData()
588          */
589         @Override
590         public String getUserData() {
591             return bundle.getUserData();
592         }
593 
594         /**
595          * {@inheritDoc}
596          * 
597          * @see javax.jcr.observation.Event#getInfo()
598          * @see JcrMoveEvent#getInfo()
599          */
600         @Override
601         public Map<String, String> getInfo() {
602             return Collections.emptyMap();
603         }
604 
605         /**
606          * {@inheritDoc}
607          * 
608          * @see java.lang.Object#toString()
609          */
610         @Override
611         public String toString() {
612             StringBuilder sb = new StringBuilder();
613             switch (this.type) {
614                 case Event.NODE_ADDED:
615                     sb.append("Node added");
616                     break;
617                 case Event.NODE_REMOVED:
618                     sb.append("Node removed");
619                     break;
620                 case Event.PROPERTY_ADDED:
621                     sb.append("Property added");
622                     break;
623                 case Event.PROPERTY_CHANGED:
624                     sb.append("Property changed");
625                     break;
626                 case Event.PROPERTY_REMOVED:
627                     sb.append("Property removed");
628                     break;
629                 case Event.NODE_MOVED:
630                     sb.append("Node moved");
631                     break;
632             }
633             sb.append(" at ").append(path).append(" by ").append(getUserID());
634             return sb.toString();
635         }
636     }
637 
638     /**
639      * An implementation of JCR {@link Event}.
640      */
641     @Immutable
642     class JcrMoveEvent extends JcrEvent {
643 
644         private final Map<String, String> info;
645 
646         /**
647          * @param bundle the event bundle information
648          * @param type the event type
649          * @param path the node path
650          * @param id the node identifier
651          * @param info the immutable map containing the source and destination absolute paths for the move
652          */
653         public JcrMoveEvent( JcrEventBundle bundle,
654                              int type,
655                              String path,
656                              String id,
657                              Map<String, String> info ) {
658             super(bundle, type, path, id);
659             this.info = info;
660         }
661 
662         /**
663          * {@inheritDoc}
664          * 
665          * @see org.modeshape.jcr.JcrObservationManager.JcrEvent#getInfo()
666          */
667         @Override
668         public Map<String, String> getInfo() {
669             return this.info;
670         }
671 
672         /**
673          * {@inheritDoc}
674          * 
675          * @see java.lang.Object#toString()
676          */
677         @Override
678         public String toString() {
679             StringBuilder sb = new StringBuilder();
680             sb.append("Node moved");
681             String from = info.containsKey(MOVE_FROM_KEY) ? info.get(MOVE_FROM_KEY) : info.get(ORDER_CHILD_KEY);
682             String to = info.containsKey(MOVE_TO_KEY) ? info.get(MOVE_TO_KEY) : info.get(ORDER_BEFORE_KEY);
683             sb.append(" from ").append(from).append(" to ").append(to).append(" by ").append(getUserID());
684             return sb.toString();
685         }
686     }
687 
688     /**
689      * The <code>JcrListener</code> class wraps JCR {@link EventListener} and is responsible for converting
690      * {@link NetChangeObserver.NetChange graph events} into JCR {@link Event events}.
691      */
692     @NotThreadSafe
693     class JcrListenerAdapter extends NetChangeObserver {
694 
695         /**
696          * The node path whose events should be handled (or <code>null</code>) if all node paths should be handled.
697          */
698         private final String absPath;
699 
700         /**
701          * The primary type and mixin types of the locations that have changes. Used only when the node type of the location needs
702          * to be checked.
703          */
704         private Map<Location, Map<Name, Property>> propertiesByLocation;
705 
706         /**
707          * The JCR event listener.
708          */
709         private final EventListener delegate;
710 
711         /**
712          * The event types this listener is interested in handling.
713          */
714         private final int eventTypes;
715 
716         /**
717          * A flag indicating if events of child nodes of the <code>absPath</code> should be processed.
718          */
719         private final boolean isDeep;
720 
721         /**
722          * 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
723          * listener wants to process that event. If <code>null</code> or empty than this listener wants to handle nodes of any
724          * type.
725          */
726         private final String[] nodeTypeNames;
727 
728         /**
729          * A flag indicating if events generated by the session that registered this listener should be ignored.
730          */
731         private final boolean noLocal;
732 
733         /**
734          * The node UUIDs or <code>null</code>. If a node with one of these UUIDs is the source node of an event than this
735          * listener wants to handle this event. If <code>null</code> or empty than this listener wants to handle nodes with any
736          * UUID.
737          */
738         private final String[] uuids;
739 
740         /**
741          * @param delegate the JCR listener
742          * @param eventTypes a combination of one or more JCR event types
743          * @param absPath the absolute path of a node or <code>null</code> if all node paths
744          * @param isDeep indicates if paths below <code>absPath</code> should be considered
745          * @param uuids UUIDs or <code>null</code>
746          * @param nodeTypeNames node type names or <code>null</code>
747          * @param noLocal indicates if events from this listener's session should be ignored
748          */
749         public JcrListenerAdapter( EventListener delegate,
750                                    int eventTypes,
751                                    String absPath,
752                                    boolean isDeep,
753                                    String[] uuids,
754                                    String[] nodeTypeNames,
755                                    boolean noLocal ) {
756             assert (delegate != null);
757 
758             this.delegate = delegate;
759             this.eventTypes = eventTypes;
760             this.absPath = absPath;
761             this.isDeep = isDeep;
762             this.uuids = uuids;
763             this.nodeTypeNames = nodeTypeNames;
764             this.noLocal = noLocal;
765         }
766 
767         /**
768          * @param changes the changes being processed
769          * @return <code>true</code> if event occurred in a different session or if events from same session should be processed
770          */
771         private boolean acceptBasedOnEventSource( Changes changes ) {
772             if (this.noLocal) {
773                 // don't accept unless IDs are different
774                 return !getSessionId().equals(changes.getContextId());
775             }
776             return true;
777         }
778 
779         /**
780          * @param change the change being processed
781          * @return <code>true</code> if all node types should be processed or if changed node type name matches a specified type
782          */
783         private boolean acceptBasedOnNodeTypeName( NetChange change ) {
784             boolean accept = true;
785 
786             if (shouldCheckNodeType()) {
787                 ValueFactory<String> stringFactory = getValueFactories().getStringFactory();
788                 Location parentLocation = Location.create(change.getLocation().getPath().getParent());
789                 Map<Name, Property> propMap = this.propertiesByLocation.get(parentLocation);
790                 assert (propMap != null);
791 
792                 try {
793                     String primaryTypeName = stringFactory.create(propMap.get(PRIMARY_TYPE).getFirstValue());
794                     String[] mixinNames = null;
795 
796                     if (propMap.get(MIXIN_TYPES) != null) {
797                         mixinNames = stringFactory.create(propMap.get(MIXIN_TYPES).getValuesAsArray());
798                     }
799 
800                     return getNodeTypeManager().isDerivedFrom(this.nodeTypeNames, primaryTypeName, mixinNames);
801                 } catch (RepositoryException e) {
802                     accept = false;
803                     Logger.getLogger(getClass()).error(e,
804                                                        JcrI18n.cannotPerformNodeTypeCheck,
805                                                        propMap.get(PRIMARY_TYPE),
806                                                        propMap.get(MIXIN_TYPES),
807                                                        this.nodeTypeNames);
808                 }
809             }
810 
811             return accept;
812         }
813 
814         /**
815          * @param change the change being processed
816          * @return <code>true</code> if there is no absolute path or if change path matches or optionally is a deep match
817          */
818         private boolean acceptBasedOnPath( NetChange change ) {
819             if ((this.absPath != null) && (this.absPath.length() != 0)) {
820                 Path matchPath = getValueFactories().getPathFactory().create(this.absPath);
821                 Path changePath = null;
822 
823                 if (change.includes(ChangeType.NODE_ADDED, ChangeType.NODE_REMOVED)) {
824                     changePath = change.getPath().getParent();
825                 } else {
826                     changePath = change.getPath();
827                 }
828 
829                 if (this.isDeep) {
830                     return matchPath.isAtOrAbove(changePath);
831                 }
832 
833                 return matchPath.equals(changePath);
834             }
835 
836             return true;
837         }
838 
839         /**
840          * @param change the change being processed
841          * @return <code>true</code> if there are no UUIDs to match or change UUID matches
842          */
843         private boolean acceptBasedOnUuid( NetChange change ) {
844             boolean accept = true;
845 
846             if ((this.uuids != null) && (this.uuids.length != 0)) {
847                 UUID matchUuid = change.getLocation().getUuid();
848 
849                 if (matchUuid != null) {
850                     accept = false;
851                     UuidFactory uuidFactory = getValueFactories().getUuidFactory();
852 
853                     for (String uuidText : this.uuids) {
854                         if ((uuidText != null) && (uuidText.length() != 0)) {
855                             try {
856                                 UUID testUuid = uuidFactory.create(uuidText);
857 
858                                 if (matchUuid.equals(testUuid)) {
859                                     accept = true;
860                                     break;
861                                 }
862                             } catch (ValueFormatException e) {
863                                 Logger.getLogger(getClass()).error(JcrI18n.cannotCreateUuid, uuidText);
864                             }
865                         }
866                     }
867                 }
868             }
869 
870             return accept;
871         }
872 
873         /**
874          * {@inheritDoc}
875          * 
876          * @see java.lang.Object#equals(java.lang.Object)
877          */
878         @Override
879         public boolean equals( Object obj ) {
880             if ((obj != null) && (obj instanceof JcrListenerAdapter)) {
881                 return (this.delegate == ((JcrListenerAdapter)obj).delegate);
882             }
883 
884             return false;
885         }
886 
887         /**
888          * {@inheritDoc}
889          * 
890          * @see java.lang.Object#hashCode()
891          */
892         @Override
893         public int hashCode() {
894             return this.delegate.hashCode();
895         }
896 
897         /**
898          * {@inheritDoc}
899          * 
900          * @see org.modeshape.graph.observe.NetChangeObserver#notify(org.modeshape.graph.observe.Changes)
901          */
902         @Override
903         public void notify( Changes changes ) {
904             // check source first
905             if (!acceptBasedOnEventSource(changes)) {
906                 return;
907             }
908 
909             try {
910                 if (shouldCheckNodeType()) {
911                     List<Location> changedLocations = new ArrayList<Location>();
912 
913                     // loop through changes saving the parent locations of the changed locations
914                     for (ChangeRequest request : changes.getChangeRequests()) {
915                         // ignore all events other than those on this workspace ...
916                         if (!getWorkspaceName().equals(request.changedWorkspace())) {
917                             continue;
918                         }
919                         Path changedPath = request.changedLocation().getPath();
920                         Path parentPath = changedPath.getParent();
921                         changedLocations.add(Location.create(parentPath));
922                     }
923 
924                     // more efficient to get all of the locations at once then it is one at a time using the NetChange
925                     Graph graph = getGraph();
926                     this.propertiesByLocation = graph.getProperties(PRIMARY_TYPE, MIXIN_TYPES).on(changedLocations);
927                 }
928 
929                 // handle events
930                 super.notify(changes);
931             } finally {
932                 this.propertiesByLocation = null;
933             }
934         }
935 
936         /**
937          * {@inheritDoc}
938          * 
939          * @see org.modeshape.graph.observe.NetChangeObserver#notify(org.modeshape.graph.observe.NetChangeObserver.NetChanges)
940          */
941         @Override
942         protected void notify( NetChanges netChanges ) {
943             Collection<Event> events = new ArrayList<Event>();
944 
945             String userData = netChanges.getData().get(OBSERVATION_USER_DATA_KEY);
946             JcrEventBundle bundle = new JcrEventBundle(netChanges.getTimestamp(), netChanges.getUserName(), userData);
947 
948             for (NetChange change : netChanges.getNetChanges()) {
949                 // ignore all events other than those on this workspace ...
950                 if (!getWorkspaceName().equals(change.getRepositoryWorkspaceName())) {
951                     continue;
952                 }
953 
954                 // ignore if lock/unlock
955                 if (change.includes(ChangeType.NODE_LOCKED) || change.includes(ChangeType.NODE_UNLOCKED)) {
956                     continue;
957                 }
958 
959                 // determine if need to process
960                 if (!acceptBasedOnNodeTypeName(change) || !acceptBasedOnPath(change) || !acceptBasedOnUuid(change)) {
961                     continue;
962                 }
963 
964                 // process event making sure we have the right event type
965                 Path path = change.getPath();
966                 PathFactory pathFactory = getValueFactories().getPathFactory();
967                 String id = change.getLocation().getUuid().toString();
968 
969                 if (change.includes(ChangeType.NODE_MOVED)) {
970                     Location original = change.getOriginalLocation();
971                     Path originalPath = original.getPath();
972                     if ((this.eventTypes & Event.NODE_MOVED) == Event.NODE_MOVED) {
973                         Location before = change.getMovedBefore();
974                         boolean sameParent = !originalPath.isRoot() && !path.isRoot()
975                                              && originalPath.getParent().equals(path.getParent());
976                         Map<String, String> info = new HashMap<String, String>();
977                         if (sameParent && change.isReorder()) {
978                             info.put(ORDER_CHILD_KEY, stringFor(originalPath.getLastSegment()));
979                             info.put(ORDER_BEFORE_KEY, before != null ? stringFor(before.getPath().getLastSegment()) : null);
980                         } else {
981                             info.put(MOVE_FROM_KEY, stringFor(originalPath));
982                             info.put(MOVE_TO_KEY, stringFor(path));
983                         }
984                         info = Collections.unmodifiableMap(info);
985                         events.add(new JcrMoveEvent(bundle, Event.NODE_MOVED, stringFor(path), id, info));
986                     }
987                     // For some bizarre reason, JCR 2.0 expects these methods <i>in addition to</i> the NODE_MOVED event
988                     if ((this.eventTypes & Event.NODE_ADDED) == Event.NODE_ADDED) {
989                         events.add(new JcrEvent(bundle, Event.NODE_ADDED, stringFor(path), id));
990                     }
991                     if ((this.eventTypes & Event.NODE_REMOVED) == Event.NODE_REMOVED) {
992                         events.add(new JcrEvent(bundle, Event.NODE_REMOVED, stringFor(originalPath), id));
993                     }
994                 }
995                 if (change.includes(ChangeType.NODE_ADDED) && ((this.eventTypes & Event.NODE_ADDED) == Event.NODE_ADDED)) {
996                     // create event for added node
997                     events.add(new JcrEvent(bundle, Event.NODE_ADDED, stringFor(path), id));
998                 } else if (change.includes(ChangeType.NODE_REMOVED)
999                            && ((this.eventTypes & Event.NODE_REMOVED) == Event.NODE_REMOVED)) {
1000                     // create event for removed node
1001                     events.add(new JcrEvent(bundle, Event.NODE_REMOVED, stringFor(path), id));
1002                 }
1003 
1004                 if (change.includes(ChangeType.PROPERTY_CHANGED)
1005                     && ((this.eventTypes & Event.PROPERTY_CHANGED) == Event.PROPERTY_CHANGED)) {
1006                     for (Property property : change.getModifiedProperties()) {
1007                         // create event for changed property
1008                         Path propertyPath = pathFactory.create(path, stringFor(property.getName()));
1009                         events.add(new JcrEvent(bundle, Event.PROPERTY_CHANGED, stringFor(propertyPath), id));
1010                     }
1011                 }
1012 
1013                 // properties have changed
1014                 if (change.includes(ChangeType.PROPERTY_ADDED)
1015                     && ((this.eventTypes & Event.PROPERTY_ADDED) == Event.PROPERTY_ADDED)) {
1016                     for (Property property : change.getAddedProperties()) {
1017                         // create event for added property
1018                         Path propertyPath = pathFactory.create(path, stringFor(property.getName()));
1019                         events.add(new JcrEvent(bundle, Event.PROPERTY_ADDED, stringFor(propertyPath), id));
1020                     }
1021                 }
1022 
1023                 if (change.includes(ChangeType.PROPERTY_REMOVED)
1024                     && ((this.eventTypes & Event.PROPERTY_REMOVED) == Event.PROPERTY_REMOVED)) {
1025                     for (Name name : change.getRemovedProperties()) {
1026                         // create event for removed property
1027                         Path propertyPath = pathFactory.create(path, name);
1028                         events.add(new JcrEvent(bundle, Event.PROPERTY_REMOVED, stringFor(propertyPath), id));
1029                     }
1030                 }
1031             }
1032 
1033             // notify delegate
1034             if (!events.isEmpty()) {
1035                 this.delegate.onEvent(new JcrEventIterator(events));
1036             }
1037         }
1038 
1039         /**
1040          * @return <code>true</code> if the node type of the event locations need to be checked
1041          */
1042         private boolean shouldCheckNodeType() {
1043             return ((this.nodeTypeNames != null) && (this.nodeTypeNames.length != 0));
1044         }
1045     }
1046 
1047 }