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