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 }