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.graph.observe;
25  
26  import java.util.Collections;
27  import java.util.EnumSet;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.LinkedList;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.TreeMap;
35  import net.jcip.annotations.Immutable;
36  import net.jcip.annotations.NotThreadSafe;
37  import net.jcip.annotations.ThreadSafe;
38  import org.modeshape.common.util.HashCode;
39  import org.modeshape.graph.Location;
40  import org.modeshape.graph.property.Name;
41  import org.modeshape.graph.property.Path;
42  import org.modeshape.graph.property.Property;
43  import org.modeshape.graph.request.ChangeRequest;
44  import org.modeshape.graph.request.CloneBranchRequest;
45  import org.modeshape.graph.request.CloneWorkspaceRequest;
46  import org.modeshape.graph.request.CopyBranchRequest;
47  import org.modeshape.graph.request.CreateNodeRequest;
48  import org.modeshape.graph.request.CreateWorkspaceRequest;
49  import org.modeshape.graph.request.DeleteBranchRequest;
50  import org.modeshape.graph.request.DeleteChildrenRequest;
51  import org.modeshape.graph.request.DestroyWorkspaceRequest;
52  import org.modeshape.graph.request.LockBranchRequest;
53  import org.modeshape.graph.request.MoveBranchRequest;
54  import org.modeshape.graph.request.RemovePropertyRequest;
55  import org.modeshape.graph.request.RenameNodeRequest;
56  import org.modeshape.graph.request.SetPropertyRequest;
57  import org.modeshape.graph.request.UnlockBranchRequest;
58  import org.modeshape.graph.request.UpdatePropertiesRequest;
59  import org.modeshape.graph.request.UpdateValuesRequest;
60  
61  /**
62   * A specialized {@link Observer} that figures out the net changes made during a single {@link Changes set of changes}. For
63   * example, if a property is updated and then updated again, the net change will be a single change. Or, if a node is created and
64   * then deleted, no net change will be observed.
65   */
66  @ThreadSafe
67  public abstract class NetChangeObserver extends ChangeObserver {
68  
69      public enum ChangeType {
70          NODE_ADDED,
71          NODE_REMOVED,
72          PROPERTY_ADDED,
73          PROPERTY_REMOVED,
74          PROPERTY_CHANGED,
75          NODE_LOCKED,
76          NODE_UNLOCKED;
77      }
78  
79      protected NetChangeObserver() {
80      }
81  
82      /**
83       * @param workspace the workspace of the location (never <code>null</code>)
84       * @param location the location whose details are being deleted (never <code>null</code>)
85       * @param workspaceLocationMap the map where the details are stored (never <code>null</code>)
86       */
87      private void deleteLocationDetails( String workspace,
88                                          Location location,
89                                          Map<String, Map<Location, NetChangeDetails>> workspaceLocationMap ) {
90          Map<Location, NetChangeDetails> detailsByLocation = workspaceLocationMap.get(workspace);
91          assert (detailsByLocation != null);
92          detailsByLocation.remove(location);
93      }
94  
95      /**
96       * @param workspace the workspace of the location (never <code>null</code>)
97       * @param location the location whose details are being requested (never <code>null</code>)
98       * @param workspaceLocationMap the map where the details are stored (never <code>null</code>)
99       * @return the found or created details (never <code>null</code>)
100      */
101     private NetChangeDetails findDetailsByLocation( String workspace,
102                                                     Location location,
103                                                     Map<String, Map<Location, NetChangeDetails>> workspaceLocationMap ) {
104         Map<Location, NetChangeDetails> detailsByLocation = workspaceLocationMap.get(workspace);
105         NetChangeDetails details = null;
106 
107         if (detailsByLocation == null) {
108             detailsByLocation = new TreeMap<Location, NetChangeDetails>();
109             workspaceLocationMap.put(workspace, detailsByLocation);
110             details = new NetChangeDetails();
111             detailsByLocation.put(location, details);
112         } else {
113             details = detailsByLocation.get(location);
114 
115             if (details == null) {
116                 details = new NetChangeDetails();
117                 detailsByLocation.put(location, details);
118             }
119         }
120 
121         return details;
122     }
123 
124     /**
125      * {@inheritDoc}
126      * 
127      * @see org.modeshape.graph.observe.ChangeObserver#notify(org.modeshape.graph.observe.Changes)
128      */
129     @Override
130     public void notify( Changes changes ) {
131         Map<String, Map<Location, NetChangeDetails>> detailsByLocationByWorkspace = new HashMap<String, Map<Location, NetChangeDetails>>();
132         // Process each of the events, extracting the node path and property details for each ...
133         for (ChangeRequest change : changes.getChangeRequests()) {
134             Location location = change.changedLocation();
135             assert (location.getPath() != null);
136 
137             // Find or create the NetChangeDetails for this node ...
138             String workspace = change.changedWorkspace();
139             NetChangeDetails details = findDetailsByLocation(workspace, location, detailsByLocationByWorkspace);
140 
141             // Process the specific kind of change ...
142             if (change instanceof CreateNodeRequest) {
143                 CreateNodeRequest create = (CreateNodeRequest)change;
144                 details.addEventType(ChangeType.NODE_ADDED);
145                 for (Property property : create) {
146                     details.addProperty(property);
147                 }
148             } else if (change instanceof UpdatePropertiesRequest) {
149                 UpdatePropertiesRequest update = (UpdatePropertiesRequest)change;
150                 for (Map.Entry<Name, Property> entry : update.properties().entrySet()) {
151                     Name propName = entry.getKey();
152                     Property property = entry.getValue();
153 
154                     if (property != null) {
155                         if (update.isNewProperty(propName)) {
156                             details.addProperty(property);
157                         } else {
158                             details.changeProperty(property);
159                         }
160                     } else {
161                         details.removeProperty(propName);
162                     }
163                 }
164             } else if (change instanceof SetPropertyRequest) {
165                 SetPropertyRequest set = (SetPropertyRequest)change;
166 
167                 if (set.isNewProperty()) {
168                     details.addProperty(set.property());
169                 } else {
170                     details.changeProperty(set.property());
171                 }
172             } else if (change instanceof RemovePropertyRequest) {
173                 RemovePropertyRequest remove = (RemovePropertyRequest)change;
174                 details.removeProperty(remove.propertyName());
175             } else if (change instanceof DeleteBranchRequest) {
176                 // if the node was previously added than a remove results in a net no change
177                 if (details.getEventTypes().contains(ChangeType.NODE_ADDED)) {
178                     deleteLocationDetails(workspace, location, detailsByLocationByWorkspace);
179                 } else {
180                     details.addEventType(ChangeType.NODE_REMOVED);
181                 }
182             } else if (change instanceof DeleteChildrenRequest) {
183                 DeleteChildrenRequest delete = (DeleteChildrenRequest)change;
184                 for (Location deletedChild : delete.getActualChildrenDeleted()) {
185                     NetChangeDetails childDetails = findDetailsByLocation(workspace, deletedChild, detailsByLocationByWorkspace);
186                     // if a child node was previously added than a remove results in a net no change
187                     if (childDetails.getEventTypes().contains(ChangeType.NODE_ADDED)) {
188                         deleteLocationDetails(workspace, deletedChild, detailsByLocationByWorkspace);
189                     } else {
190                         childDetails.addEventType(ChangeType.NODE_REMOVED);
191                     }
192                 }
193             } else if (change instanceof LockBranchRequest) {
194                 details.setLockAction(LockAction.LOCKED);
195             } else if (change instanceof UnlockBranchRequest) {
196                 details.setLockAction(LockAction.UNLOCKED);
197             } else if (change instanceof CopyBranchRequest) {
198                 details.addEventType(ChangeType.NODE_ADDED);
199             } else if (change instanceof MoveBranchRequest) {
200                 // the old location is a removed node event and if it is the same location as the original location it is a
201                 // reorder
202                 Location original = ((MoveBranchRequest)change).getActualLocationBefore();
203                 NetChangeDetails originalDetails = findDetailsByLocation(workspace, original, detailsByLocationByWorkspace);
204                 originalDetails.addEventType(ChangeType.NODE_REMOVED);
205 
206                 // the new location is a new node event
207                 details.addEventType(ChangeType.NODE_ADDED);
208             } else if (change instanceof CloneBranchRequest) {
209                 CloneBranchRequest cloneRequest = (CloneBranchRequest)change;
210 
211                 // create event details for any nodes that were removed
212                 for (Location removed : cloneRequest.getRemovedNodes()) {
213                     NetChangeDetails removedDetails = findDetailsByLocation(workspace, removed, detailsByLocationByWorkspace);
214                     removedDetails.addEventType(ChangeType.NODE_REMOVED);
215                 }
216 
217                 // create event details for new node
218                 details.addEventType(ChangeType.NODE_ADDED);
219             } else if (change instanceof RenameNodeRequest) {
220                 // the old location is a removed node event
221                 Location original = ((RenameNodeRequest)change).getActualLocationBefore();
222                 NetChangeDetails originalDetails = findDetailsByLocation(workspace, original, detailsByLocationByWorkspace);
223                 originalDetails.addEventType(ChangeType.NODE_REMOVED);
224 
225                 // the new location is a new node event
226                 details.addEventType(ChangeType.NODE_ADDED);
227             } else if (change instanceof UpdateValuesRequest) {
228                 UpdateValuesRequest updateValuesRequest = (UpdateValuesRequest)change;
229 
230                 if (!updateValuesRequest.addedValues().isEmpty() || !updateValuesRequest.removedValues().isEmpty()) {
231                     assert (updateValuesRequest.getActualProperty() != null);
232 
233                     if (updateValuesRequest.isNewProperty()) {
234                         details.addEventType(ChangeType.PROPERTY_ADDED);
235                         details.addProperty(updateValuesRequest.getActualProperty());
236                     } else {
237                         details.addEventType(ChangeType.PROPERTY_CHANGED);
238                         details.changeProperty(updateValuesRequest.getActualProperty());
239                     }
240                 } else if (details.getEventTypes().isEmpty()) {
241                     // details was just created for this request and now it is not needed
242                     deleteLocationDetails(workspace, location, detailsByLocationByWorkspace);
243                 }
244             } else if (change instanceof CreateWorkspaceRequest) {
245                 details.addEventType(ChangeType.NODE_ADDED);
246             } else if (change instanceof DestroyWorkspaceRequest) {
247                 details.addEventType(ChangeType.NODE_REMOVED);
248             } else if (change instanceof CloneWorkspaceRequest) {
249                 details.addEventType(ChangeType.NODE_ADDED);
250             }
251         }
252 
253         // Walk through the net changes ...
254         List<NetChange> netChanges = new LinkedList<NetChange>();
255         for (Map.Entry<String, Map<Location, NetChangeDetails>> byWorkspaceEntry : detailsByLocationByWorkspace.entrySet()) {
256             String workspaceName = byWorkspaceEntry.getKey();
257             // Iterate over the entries. Since we've used a TreeSet, we'll get these with the lower paths first ...
258             for (Map.Entry<Location, NetChangeDetails> entry : byWorkspaceEntry.getValue().entrySet()) {
259                 Location location = entry.getKey();
260                 NetChangeDetails details = entry.getValue();
261                 netChanges.add(new NetChange(workspaceName, location, details.getEventTypes(), details.getAddedProperties(),
262                                              details.getModifiedProperties(), details.getRemovedProperties()));
263             }
264         }
265         // Now notify of all of the changes ...
266         notify(new NetChanges(changes, netChanges));
267     }
268 
269     /**
270      * Method that is called for the set of net changes.
271      * 
272      * @param netChanges the net changes; never null
273      */
274     protected abstract void notify( NetChanges netChanges );
275 
276     /**
277      * A set of net changes that were made atomically. Each change is in the form of a frozen {@link ChangeRequest}, and the net
278      * change is in the form.
279      */
280     @Immutable
281     public static final class NetChanges extends Changes {
282         private static final long serialVersionUID = 1L;
283 
284         private final List<NetChange> netChanges;
285 
286         public NetChanges( Changes changes,
287                            List<NetChange> netChanges ) {
288             super(changes);
289             assert netChanges != null;
290             assert !netChanges.isEmpty();
291             this.netChanges = Collections.unmodifiableList(netChanges);
292         }
293 
294         /**
295          * Get the list of net changes.
296          * 
297          * @return the immutable list of net changes; never null and never empty
298          */
299         public List<NetChange> getNetChanges() {
300             return this.netChanges;
301         }
302 
303         /**
304          * {@inheritDoc}
305          * 
306          * @see java.lang.Object#toString()
307          */
308         @Override
309         public String toString() {
310             if (processId.length() != 0) {
311                 return getTimestamp() + " @" + getUserName() + " [" + getSourceName() + "] - " + netChanges.size() + " changes";
312             }
313             return getTimestamp() + " @" + getUserName() + " #" + getProcessId() + " [" + getSourceName() + "] - "
314                    + netChanges.size() + " changes";
315         }
316     }
317 
318     /**
319      * A notification of changes to a node.
320      */
321     @Immutable
322     public static final class NetChange {
323 
324         private final String workspaceName;
325         private final Location location;
326         private final EnumSet<ChangeType> eventTypes;
327         private final Set<Property> addedProperties;
328         private final Set<Property> modifiedProperties;
329         private final Set<Property> addedOrModifiedProperties;
330         private final Set<Name> removedProperties;
331         private final int hc;
332 
333         public NetChange( String workspaceName,
334                           Location location,
335                           EnumSet<ChangeType> eventTypes,
336                           Set<Property> addedProperties,
337                           Set<Property> modifiedProperties,
338                           Set<Name> removedProperties ) {
339             assert workspaceName != null;
340             assert location != null;
341             this.workspaceName = workspaceName;
342             this.location = location;
343             this.hc = HashCode.compute(this.workspaceName, this.location);
344             this.eventTypes = eventTypes;
345             Set<Property> addedOrModified = null;
346             if (addedProperties == null) {
347                 addedProperties = Collections.emptySet();
348                 addedOrModified = modifiedProperties; // may be null
349             } else {
350                 addedOrModified = addedProperties;
351             }
352             if (modifiedProperties == null) {
353                 if (addedOrModified == null) addedOrModified = Collections.emptySet();
354                 modifiedProperties = Collections.emptySet();
355             } else {
356                 if (addedOrModified == null) {
357                     addedOrModified = modifiedProperties;
358                 } else {
359                     addedOrModified = new HashSet<Property>(modifiedProperties);
360                     addedOrModified.addAll(addedProperties);
361                 }
362             }
363             if (removedProperties == null) removedProperties = Collections.emptySet();
364             this.addedProperties = Collections.unmodifiableSet(addedProperties);
365             this.modifiedProperties = Collections.unmodifiableSet(modifiedProperties);
366             this.removedProperties = Collections.unmodifiableSet(removedProperties);
367             this.addedOrModifiedProperties = Collections.unmodifiableSet(addedOrModified);
368         }
369 
370         /**
371          * @return the node location
372          */
373         public Location getLocation() {
374             return this.location;
375         }
376 
377         /**
378          * @return absolutePath
379          */
380         public Path getPath() {
381             return this.location.getPath();
382         }
383 
384         /**
385          * @return repositoryWorkspaceName
386          */
387         public String getRepositoryWorkspaceName() {
388             return this.workspaceName;
389         }
390 
391         /**
392          * @return the added properties
393          */
394         public Set<Property> getAddedProperties() {
395             return this.addedProperties;
396         }
397 
398         /**
399          * @return modifiedProperties
400          */
401         public Set<Property> getModifiedProperties() {
402             return this.modifiedProperties;
403         }
404 
405         /**
406          * Get the combination of {@link #getAddedProperties() added} and {@link #getModifiedProperties() modified} properties.
407          * 
408          * @return the immutable set of properties that were added or modified; never null but possibly empty
409          */
410         public Set<Property> getAddedOrModifiedProperties() {
411             return this.addedOrModifiedProperties;
412         }
413 
414         /**
415          * @return removedProperties
416          */
417         public Set<Name> getRemovedProperties() {
418             return this.removedProperties;
419         }
420 
421         /**
422          * {@inheritDoc}
423          */
424         @Override
425         public int hashCode() {
426             return this.hc;
427         }
428 
429         /**
430          * Determine whether this net change includes all of the supplied types.
431          * 
432          * @param jcrEventTypes the types to check for
433          * @return true if all of the supplied events are included in this net change, or false otherwise
434          */
435         public boolean includesAllOf( ChangeType... jcrEventTypes ) {
436             for (ChangeType jcrEventType : jcrEventTypes) {
437                 if (!this.eventTypes.contains(jcrEventType)) return false;
438             }
439             return true;
440         }
441 
442         /**
443          * Determine whether this net change includes any of the supplied types.
444          * 
445          * @param jcrEventTypes the types to check for
446          * @return true if any of the supplied events are included in this net change, or false otherwise
447          */
448         public boolean includes( ChangeType... jcrEventTypes ) {
449             for (ChangeType jcrEventType : jcrEventTypes) {
450                 if (this.eventTypes.contains(jcrEventType)) return true;
451             }
452             return false;
453         }
454 
455         public boolean isSameNode( NetChange that ) {
456             if (that == this) return true;
457             if (this.hc != that.hc) return false;
458             if (!this.workspaceName.equals(that.workspaceName)) return false;
459             if (!this.location.isSame(that.location)) return false;
460             return true;
461         }
462 
463         /**
464          * Determine whether this node change includes the setting of new value(s) for the supplied property.
465          * 
466          * @param property the name of the property
467          * @return true if the named property has a new value on this node, or false otherwise
468          */
469         public boolean isPropertyModified( String property ) {
470             return this.modifiedProperties.contains(property);
471         }
472 
473         /**
474          * Determine whether this node change includes the removal of the supplied property.
475          * 
476          * @param property the name of the property
477          * @return true if the named property was removed from this node, or false otherwise
478          */
479         public boolean isPropertyRemoved( String property ) {
480             return this.removedProperties.contains(property);
481         }
482 
483         /**
484          * {@inheritDoc}
485          */
486         @Override
487         public boolean equals( Object obj ) {
488             if (obj == this) return true;
489             if (obj instanceof NetChange) {
490                 NetChange that = (NetChange)obj;
491                 if (!this.isSameNode(that)) return false;
492                 if (this.eventTypes != that.eventTypes) return false;
493                 return true;
494             }
495             return false;
496         }
497 
498         /**
499          * {@inheritDoc}
500          */
501         @Override
502         public String toString() {
503             return this.workspaceName + "=>" + this.location;
504         }
505     }
506 
507     private enum LockAction {
508         LOCKED,
509         UNLOCKED;
510     }
511 
512     /**
513      * Internal utility class used in the computation of the net changes.
514      */
515     @NotThreadSafe
516     private static class NetChangeDetails {
517 
518         private final Set<Property> modifiedProperties = new HashSet<Property>();
519         private final Set<Property> addedProperties = new HashSet<Property>();
520         private final Set<Name> removedProperties = new HashSet<Name>();
521         private EnumSet<ChangeType> eventTypes = EnumSet.noneOf(ChangeType.class);
522 
523         protected NetChangeDetails() {
524         }
525 
526         public void setLockAction( LockAction lockAction ) {
527             switch (lockAction) {
528                 case LOCKED:
529                     // always mark as locked and remove any unlocked state
530                     eventTypes.add(ChangeType.NODE_LOCKED);
531                     eventTypes.remove(ChangeType.NODE_UNLOCKED);
532                     break;
533                 case UNLOCKED:
534                     if (!eventTypes.remove(ChangeType.NODE_LOCKED)) {
535                         // It was not previously locked by this change set, so we should unlock it ...
536                         eventTypes.add(ChangeType.NODE_UNLOCKED);
537                     }
538                     break;
539             }
540         }
541 
542         public void addEventType( ChangeType eventType ) {
543             this.eventTypes.add(eventType);
544         }
545 
546         public void addProperty( Property property ) {
547             this.addedProperties.add(property);
548             this.eventTypes.add(ChangeType.PROPERTY_ADDED);
549         }
550 
551         public void changeProperty( Property property ) {
552             // if property was previously added then changed just keep the added
553             if (!this.addedProperties.contains(property)) {
554                 this.modifiedProperties.add(property);
555                 this.eventTypes.add(ChangeType.PROPERTY_CHANGED);
556             }
557         }
558 
559         public void removeProperty( Name propertyName ) {
560             // if property was previously added a remove results in a net no change
561             boolean handled = false;
562 
563             for (Property property : this.addedProperties) {
564                 if (property.getName().equals(propertyName)) {
565                     handled = true;
566                     this.addedProperties.remove(property);
567 
568                     // get rid of event type if no longer applicable
569                     if (this.addedProperties.isEmpty()) {
570                         this.eventTypes.remove(ChangeType.PROPERTY_ADDED);
571                     }
572 
573                     break;
574                 }
575             }
576 
577             if (!handled) {
578                 // if property was previously changed and now is being removed the change is no longer needed
579                 for (Property property : this.modifiedProperties) {
580                     if (property.getName().equals(propertyName)) {
581                         this.modifiedProperties.remove(property);
582 
583                         // get rid of event type if no longer applicable
584                         if (this.modifiedProperties.isEmpty()) {
585                             this.eventTypes.remove(ChangeType.PROPERTY_CHANGED);
586                         }
587 
588                         break;
589                     }
590                 }
591 
592                 // now add to removed collection
593                 this.removedProperties.add(propertyName);
594                 this.eventTypes.add(ChangeType.PROPERTY_REMOVED);
595             }
596         }
597 
598         /**
599          * @return nodeAction
600          */
601         public EnumSet<ChangeType> getEventTypes() {
602             return this.eventTypes;
603         }
604 
605         /**
606          * @return the added properties
607          */
608         public Set<Property> getAddedProperties() {
609             return this.addedProperties;
610         }
611 
612         /**
613          * @return modified properties
614          */
615         public Set<Property> getModifiedProperties() {
616             return this.modifiedProperties;
617         }
618 
619         /**
620          * @return removedProperties
621          */
622         public Set<Name> getRemovedProperties() {
623             return this.removedProperties;
624         }
625     }
626 }