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.CreateNodeRequest;
46  import org.modeshape.graph.request.DeleteChildrenRequest;
47  import org.modeshape.graph.request.MoveBranchRequest;
48  import org.modeshape.graph.request.RemovePropertyRequest;
49  import org.modeshape.graph.request.RenameNodeRequest;
50  import org.modeshape.graph.request.SetPropertyRequest;
51  import org.modeshape.graph.request.UpdatePropertiesRequest;
52  import org.modeshape.graph.request.UpdateValuesRequest;
53  
54  /**
55   * A specialized {@link Observer} that figures out the net changes made during a single {@link Changes set of changes}. For
56   * 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
57   * then deleted, no net change will be observed.
58   */
59  @ThreadSafe
60  public abstract class NetChangeObserver extends ChangeObserver {
61  
62      /**
63       * Representation of the context in which a set of changes are being made.
64       */
65      protected class ChangeContext {
66          private final Map<String, Map<Location, NetChangeDetails>> detailsByLocationByWorkspace = new HashMap<String, Map<Location, NetChangeDetails>>();
67  
68          protected ChangeContext() {
69          }
70  
71          /**
72           * Record the details of a deleted node at the supplied location in the named workspace.
73           * 
74           * @param workspace the workspace of the location; may not be null
75           * @param location the location whose details are being deleted; may not be null
76           */
77          public void delete( String workspace,
78                              Location location ) {
79              Map<Location, NetChangeDetails> detailsByLocation = detailsByLocationByWorkspace.get(workspace);
80              assert detailsByLocation != null;
81              detailsByLocation.remove(location);
82          }
83  
84          /**
85           * Find or create the details for the changes made to a node at the supplied location in the named workspace.
86           * 
87           * @param workspace the workspace of the location; may not be null
88           * @param location the location whose details are being deleted; may not be null
89           * @return the details of the changes made to the node; never null
90           */
91          public NetChangeDetails detailsFor( String workspace,
92                                              Location location ) {
93              Map<Location, NetChangeDetails> detailsByLocation = detailsByLocationByWorkspace.get(workspace);
94              NetChangeDetails details = null;
95  
96              if (detailsByLocation == null) {
97                  detailsByLocation = new TreeMap<Location, NetChangeDetails>();
98                  detailsByLocationByWorkspace.put(workspace, detailsByLocation);
99                  details = new NetChangeDetails();
100                 detailsByLocation.put(location, details);
101             } else {
102                 details = detailsByLocation.get(location);
103 
104                 if (details == null) {
105                     details = new NetChangeDetails();
106                     detailsByLocation.put(location, details);
107                 } else {
108                     details = details.latest();
109                 }
110             }
111             return details;
112         }
113 
114         /**
115          * Record that a node was moved from one location to another, and potentially into a new workspace.
116          * 
117          * @param workspace the name of the original workspace in which the node existed prior to this move; may not be null
118          * @param fromLocation the location of the node prior to this move; may not be null
119          * @param toLocation the location of the node after this move; may not be null
120          * @param before the location of the node before which this node is to be moved; may be null
121          * @param isReorder true if the operation is a simple re-ordering of siblings
122          */
123         public void move( String workspace,
124                           Location fromLocation,
125                           Location toLocation,
126                           Location before,
127                           boolean isReorder ) {
128             if (fromLocation.equals(toLocation) && before == null) {
129                 // This is just a same-location move without specifying a before, so it is a no-op
130                 return;
131             }
132 
133             // Find or create the workspace change details ...
134             Map<Location, NetChangeDetails> detailsByLocation = detailsByLocationByWorkspace.get(workspace);
135             if (detailsByLocation == null) {
136                 detailsByLocation = new TreeMap<Location, NetChangeDetails>();
137                 detailsByLocationByWorkspace.put(workspace, detailsByLocation);
138             }
139 
140             // Find any existing changes already made to the new location (meaning the node was inserted before
141             // other existing same-name-siblings) ...
142             NetChangeDetails existingBefore = detailsByLocation.get(toLocation);
143 
144             // Find any changes made to the node before it was moved ...
145             NetChangeDetails from = detailsByLocation.remove(fromLocation);
146 
147             // Create the changes for the node after it was moved ...
148             NetChangeDetails after = from != null ? from : new NetChangeDetails();
149             after.movedFrom(fromLocation);
150             if (isReorder) after.movedBefore(before, isReorder);
151 
152             if (existingBefore != null) {
153                 existingBefore.setNextChanges(after);
154             } else {
155                 detailsByLocation.put(toLocation, after);
156             }
157         }
158 
159         /**
160          * Compute the set of net changes that were made within this context.
161          * 
162          * @return the list of {@link NetChange} objects; never null but possibly empty
163          */
164         public List<NetChange> getNetChanges() {
165             // Walk through the net changes ...
166             List<NetChange> netChanges = new LinkedList<NetChange>();
167             for (Map.Entry<String, Map<Location, NetChangeDetails>> byWorkspaceEntry : detailsByLocationByWorkspace.entrySet()) {
168                 String workspaceName = byWorkspaceEntry.getKey();
169                 // Iterate over the entries. Since we've used a TreeSet, we'll get these with the lower paths first ...
170                 for (Map.Entry<Location, NetChangeDetails> entry : byWorkspaceEntry.getValue().entrySet()) {
171                     Location location = entry.getKey();
172                     NetChangeDetails details = entry.getValue();
173                     while (details != null) {
174                         netChanges.add(new NetChange(workspaceName, location, details.getEventTypes(),
175                                                      details.getAddedProperties(), details.getModifiedProperties(),
176                                                      details.getRemovedProperties(), details.getMovedFrom(),
177                                                      details.getMovedBefore(), details.isReorder()));
178                         details = details.next();
179                     }
180                 }
181             }
182             return netChanges;
183         }
184     }
185 
186     public enum ChangeType {
187         NODE_ADDED,
188         NODE_MOVED,
189         NODE_REMOVED,
190         PROPERTY_ADDED,
191         PROPERTY_REMOVED,
192         PROPERTY_CHANGED,
193         NODE_LOCKED,
194         NODE_UNLOCKED;
195     }
196 
197     protected NetChangeObserver() {
198     }
199 
200     /**
201      * {@inheritDoc}
202      * 
203      * @see org.modeshape.graph.observe.ChangeObserver#notify(org.modeshape.graph.observe.Changes)
204      */
205     @Override
206     public void notify( Changes changes ) {
207         ChangeContext changeContext = new ChangeContext();
208         // Process each of the events, extracting the node path and property details for each ...
209         for (ChangeRequest change : changes.getChangeRequests()) {
210             Location location = change.changedLocation();
211             assert (location.getPath() != null);
212 
213             // Find or create the NetChangeDetails for this node ...
214             String workspace = change.changedWorkspace();
215             NetChangeDetails details = null;
216             Location original;
217 
218             // Process the specific kind of change ...
219             switch (change.getType()) {
220                 case CREATE_NODE:
221                     CreateNodeRequest create = (CreateNodeRequest)change;
222                     details = changeContext.detailsFor(workspace, location);
223                     details.addEventType(ChangeType.NODE_ADDED);
224                     for (Property property : create) {
225                         details.addProperty(property);
226                     }
227                     break;
228                 case UPDATE_PROPERTIES:
229                     UpdatePropertiesRequest update = (UpdatePropertiesRequest)change;
230                     details = changeContext.detailsFor(workspace, location);
231                     for (Map.Entry<Name, Property> entry : update.properties().entrySet()) {
232                         Name propName = entry.getKey();
233                         Property property = entry.getValue();
234 
235                         if (property != null) {
236                             if (update.isNewProperty(propName)) {
237                                 details.addProperty(property);
238                             } else {
239                                 details.changeProperty(property);
240                             }
241                         } else {
242                             details.removeProperty(propName);
243                         }
244                     }
245                     break;
246                 case SET_PROPERTY:
247                     SetPropertyRequest set = (SetPropertyRequest)change;
248                     details = changeContext.detailsFor(workspace, location);
249                     if (set.isNewProperty()) {
250                         details.addProperty(set.property());
251                     } else {
252                         details.changeProperty(set.property());
253                     }
254                     break;
255                 case REMOVE_PROPERTY:
256                     RemovePropertyRequest remove = (RemovePropertyRequest)change;
257                     details = changeContext.detailsFor(workspace, location);
258                     details.removeProperty(remove.propertyName());
259                     break;
260                 case DELETE_BRANCH:
261                     // if the node was previously added than a remove results in a net no change
262                     details = changeContext.detailsFor(workspace, location);
263                     if (details.getEventTypes().contains(ChangeType.NODE_ADDED)) {
264                         changeContext.delete(workspace, location);
265                     } else {
266                         details.addEventType(ChangeType.NODE_REMOVED);
267                     }
268                     break;
269                 case DELETE_CHILDREN:
270                     DeleteChildrenRequest delete = (DeleteChildrenRequest)change;
271                     for (Location deletedChild : delete.getActualChildrenDeleted()) {
272                         NetChangeDetails childDetails = changeContext.detailsFor(workspace, deletedChild);
273                         // if a child node was previously added than a remove results in a net no change
274                         if (childDetails.getEventTypes().contains(ChangeType.NODE_ADDED)) {
275                             changeContext.delete(workspace, deletedChild);
276                         } else {
277                             childDetails.addEventType(ChangeType.NODE_REMOVED);
278                         }
279                     }
280                     break;
281                 case LOCK_BRANCH:
282                     details = changeContext.detailsFor(workspace, location);
283                     details.setLockAction(LockAction.LOCKED);
284                     break;
285                 case UNLOCK_BRANCH:
286                     details = changeContext.detailsFor(workspace, location);
287                     details.setLockAction(LockAction.UNLOCKED);
288                     break;
289                 case COPY_BRANCH:
290                     details = changeContext.detailsFor(workspace, location);
291                     details.addEventType(ChangeType.NODE_ADDED);
292                     break;
293                 case CLONE_BRANCH:
294                     CloneBranchRequest cloneRequest = (CloneBranchRequest)change;
295 
296                     // create event details for any nodes that were removed
297                     for (Location removed : cloneRequest.getRemovedNodes()) {
298                         NetChangeDetails removedDetails = changeContext.detailsFor(workspace, removed);
299                         removedDetails.addEventType(ChangeType.NODE_REMOVED);
300                     }
301 
302                     // create event details for new node
303                     details = changeContext.detailsFor(workspace, location);
304                     details.addEventType(ChangeType.NODE_ADDED);
305                     break;
306 
307                 case MOVE_BRANCH:
308                     // the old location is a removed node event and if it is the same location as the original location it is a
309                     // reorder
310                     MoveBranchRequest move = (MoveBranchRequest)change;
311                     original = move.getActualLocationBefore();
312                     Location before = move.before();
313                     boolean isReorder = move.desiredName() == null;
314                     changeContext.move(workspace, original, location, before, isReorder);
315                     break;
316                 case RENAME_NODE:
317                     // the old location is a removed node event
318                     original = ((RenameNodeRequest)change).getActualLocationBefore();
319                     changeContext.move(workspace, original, location, null, false);
320                     break;
321 
322                 case UPDATE_VALUES:
323                     UpdateValuesRequest updateValuesRequest = (UpdateValuesRequest)change;
324                     details = changeContext.detailsFor(workspace, location);
325                     if (!updateValuesRequest.addedValues().isEmpty() || !updateValuesRequest.removedValues().isEmpty()) {
326                         assert (updateValuesRequest.getActualProperty() != null);
327 
328                         if (updateValuesRequest.isNewProperty()) {
329                             details.addEventType(ChangeType.PROPERTY_ADDED);
330                             details.addProperty(updateValuesRequest.getActualProperty());
331                         } else {
332                             details.addEventType(ChangeType.PROPERTY_CHANGED);
333                             details.changeProperty(updateValuesRequest.getActualProperty());
334                         }
335                     } else if (details.getEventTypes().isEmpty()) {
336                         // details was just created for this request and now it is not needed
337                         changeContext.delete(workspace, location);
338                     }
339                     break;
340                 case CREATE_WORKSPACE:
341                     details = changeContext.detailsFor(workspace, location);
342                     details.addEventType(ChangeType.NODE_ADDED);
343                     break;
344                 case DESTROY_WORKSPACE:
345                     details = changeContext.detailsFor(workspace, location);
346                     details.addEventType(ChangeType.NODE_REMOVED);
347                     break;
348                 case CLONE_WORKSPACE:
349                     details = changeContext.detailsFor(workspace, location);
350                     details.addEventType(ChangeType.NODE_ADDED);
351                     break;
352                 default:
353                     // Do nothing, as it's not a change type
354                     break;
355             }
356         }
357 
358         // Compute the net changes ...
359         List<NetChange> netChanges = changeContext.getNetChanges();
360         if (!netChanges.isEmpty()) {
361             // Now notify of all of the changes ...
362             notify(new NetChanges(changes, netChanges));
363         }
364     }
365 
366     /**
367      * Method that is called for the set of net changes.
368      * 
369      * @param netChanges the net changes; never null
370      */
371     protected abstract void notify( NetChanges netChanges );
372 
373     /**
374      * A set of net changes that were made atomically. Each change is in the form of a frozen {@link ChangeRequest}, and the net
375      * change is in the form.
376      */
377     @Immutable
378     public static final class NetChanges extends Changes {
379         private static final long serialVersionUID = 1L;
380 
381         private final List<NetChange> netChanges;
382 
383         public NetChanges( Changes changes,
384                            List<NetChange> netChanges ) {
385             super(changes);
386             assert netChanges != null;
387             assert !netChanges.isEmpty();
388             this.netChanges = Collections.unmodifiableList(netChanges);
389         }
390 
391         /**
392          * Get the list of net changes.
393          * 
394          * @return the immutable list of net changes; never null and never empty
395          */
396         public List<NetChange> getNetChanges() {
397             return this.netChanges;
398         }
399 
400         /**
401          * {@inheritDoc}
402          * 
403          * @see java.lang.Object#toString()
404          */
405         @Override
406         public String toString() {
407             if (processId.length() != 0) {
408                 return getTimestamp() + " @" + getUserName() + " [" + getSourceName() + "] - " + netChanges.size() + " changes";
409             }
410             return getTimestamp() + " @" + getUserName() + " #" + getProcessId() + " [" + getSourceName() + "] - "
411                    + netChanges.size() + " changes";
412         }
413     }
414 
415     /**
416      * A notification of changes to a node.
417      */
418     @Immutable
419     public static final class NetChange {
420 
421         private final String workspaceName;
422         private final Location location;
423         private final EnumSet<ChangeType> eventTypes;
424         private final Set<Property> addedProperties;
425         private final Set<Property> modifiedProperties;
426         private final Set<Property> addedOrModifiedProperties;
427         private final Set<Name> removedProperties;
428         private final int hc;
429         private final Location movedFrom;
430         private final Location movedBefore;
431         private final boolean isReorder;
432 
433         public NetChange( String workspaceName,
434                           Location location,
435                           EnumSet<ChangeType> eventTypes,
436                           Set<Property> addedProperties,
437                           Set<Property> modifiedProperties,
438                           Set<Name> removedProperties,
439                           Location movedFrom,
440                           Location movedBefore,
441                           boolean isReorder ) {
442             assert workspaceName != null;
443             assert location != null;
444             this.workspaceName = workspaceName;
445             this.location = location;
446             this.hc = HashCode.compute(this.workspaceName, this.location);
447             this.eventTypes = eventTypes;
448             Set<Property> addedOrModified = null;
449             if (addedProperties == null) {
450                 addedProperties = Collections.emptySet();
451                 addedOrModified = modifiedProperties; // may be null
452             } else {
453                 addedOrModified = addedProperties;
454             }
455             if (modifiedProperties == null) {
456                 if (addedOrModified == null) addedOrModified = Collections.emptySet();
457                 modifiedProperties = Collections.emptySet();
458             } else {
459                 if (addedOrModified == null) {
460                     addedOrModified = modifiedProperties;
461                 } else {
462                     addedOrModified = new HashSet<Property>(modifiedProperties);
463                     addedOrModified.addAll(addedProperties);
464                 }
465             }
466             if (removedProperties == null) removedProperties = Collections.emptySet();
467             this.addedProperties = Collections.unmodifiableSet(addedProperties);
468             this.modifiedProperties = Collections.unmodifiableSet(modifiedProperties);
469             this.removedProperties = Collections.unmodifiableSet(removedProperties);
470             this.addedOrModifiedProperties = Collections.unmodifiableSet(addedOrModified);
471             this.movedFrom = movedFrom;
472             this.movedBefore = movedBefore;
473             this.isReorder = isReorder;
474         }
475 
476         /**
477          * @return the node location
478          */
479         public Location getLocation() {
480             return this.location;
481         }
482 
483         /**
484          * Get the original location for this node, or null if this node was not moved
485          * 
486          * @return the original location for this node before it was moved, or null if it was not moved
487          */
488         public Location getOriginalLocation() {
489             return this.movedFrom;
490         }
491 
492         /**
493          * Get the location before which the node was moved, or null if this node was not moved before a particular node
494          * 
495          * @return movedBefore the location of the existing child under the new parent, before which this node was moved; or null
496          *         if this node was not moved before another node
497          */
498         public Location getMovedBefore() {
499             return movedBefore;
500         }
501 
502         /**
503          * True if this is a simple re-ordering of sibling nodes.
504          * 
505          * @return true if this is a reordering operation, or false otherwise
506          */
507         public boolean isReorder() {
508             return isReorder;
509         }
510 
511         /**
512          * @return absolutePath
513          */
514         public Path getPath() {
515             return this.location.getPath();
516         }
517 
518         /**
519          * @return repositoryWorkspaceName
520          */
521         public String getRepositoryWorkspaceName() {
522             return this.workspaceName;
523         }
524 
525         /**
526          * @return the added properties
527          */
528         public Set<Property> getAddedProperties() {
529             return this.addedProperties;
530         }
531 
532         /**
533          * @return modifiedProperties
534          */
535         public Set<Property> getModifiedProperties() {
536             return this.modifiedProperties;
537         }
538 
539         /**
540          * Get the combination of {@link #getAddedProperties() added} and {@link #getModifiedProperties() modified} properties.
541          * 
542          * @return the immutable set of properties that were added or modified; never null but possibly empty
543          */
544         public Set<Property> getAddedOrModifiedProperties() {
545             return this.addedOrModifiedProperties;
546         }
547 
548         /**
549          * @return removedProperties
550          */
551         public Set<Name> getRemovedProperties() {
552             return this.removedProperties;
553         }
554 
555         /**
556          * {@inheritDoc}
557          */
558         @Override
559         public int hashCode() {
560             return this.hc;
561         }
562 
563         /**
564          * Determine whether this net change includes all of the supplied types.
565          * 
566          * @param jcrEventTypes the types to check for
567          * @return true if all of the supplied events are included in this net change, or false otherwise
568          */
569         public boolean includesAllOf( ChangeType... jcrEventTypes ) {
570             for (ChangeType jcrEventType : jcrEventTypes) {
571                 if (!this.eventTypes.contains(jcrEventType)) return false;
572             }
573             return true;
574         }
575 
576         /**
577          * Determine whether this net change includes any of the supplied types.
578          * 
579          * @param jcrEventTypes the types to check for
580          * @return true if any of the supplied events are included in this net change, or false otherwise
581          */
582         public boolean includes( ChangeType... jcrEventTypes ) {
583             for (ChangeType jcrEventType : jcrEventTypes) {
584                 if (this.eventTypes.contains(jcrEventType)) return true;
585             }
586             return false;
587         }
588 
589         public boolean isSameNode( NetChange that ) {
590             if (that == this) return true;
591             if (this.hc != that.hc) return false;
592             if (!this.workspaceName.equals(that.workspaceName)) return false;
593             if (!this.location.isSame(that.location)) return false;
594             return true;
595         }
596 
597         /**
598          * Determine whether this node change includes the setting of new value(s) for the supplied property.
599          * 
600          * @param property the name of the property
601          * @return true if the named property has a new value on this node, or false otherwise
602          */
603         public boolean isPropertyModified( String property ) {
604             return this.modifiedProperties.contains(property);
605         }
606 
607         /**
608          * Determine whether this node change includes the removal of the supplied property.
609          * 
610          * @param property the name of the property
611          * @return true if the named property was removed from this node, or false otherwise
612          */
613         public boolean isPropertyRemoved( String property ) {
614             return this.removedProperties.contains(property);
615         }
616 
617         /**
618          * {@inheritDoc}
619          */
620         @Override
621         public boolean equals( Object obj ) {
622             if (obj == this) return true;
623             if (obj instanceof NetChange) {
624                 NetChange that = (NetChange)obj;
625                 if (!this.isSameNode(that)) return false;
626                 if (this.eventTypes != that.eventTypes) return false;
627                 return true;
628             }
629             return false;
630         }
631 
632         /**
633          * {@inheritDoc}
634          */
635         @Override
636         public String toString() {
637             return this.workspaceName + "=>" + this.location;
638         }
639     }
640 
641     private enum LockAction {
642         LOCKED,
643         UNLOCKED;
644     }
645 
646     /**
647      * Internal utility class used in the computation of the net changes.
648      */
649     @NotThreadSafe
650     private static class NetChangeDetails {
651 
652         private final Set<Property> modifiedProperties = new HashSet<Property>();
653         private final Set<Property> addedProperties = new HashSet<Property>();
654         private final Set<Name> removedProperties = new HashSet<Name>();
655         private EnumSet<ChangeType> eventTypes = EnumSet.noneOf(ChangeType.class);
656         private Location movedFrom;
657         private Location movedBefore;
658         private boolean reorder;
659         private NetChangeDetails nextChanges;
660 
661         protected NetChangeDetails() {
662         }
663 
664         public void setLockAction( LockAction lockAction ) {
665             switch (lockAction) {
666                 case LOCKED:
667                     // always mark as locked and remove any unlocked state
668                     eventTypes.add(ChangeType.NODE_LOCKED);
669                     eventTypes.remove(ChangeType.NODE_UNLOCKED);
670                     break;
671                 case UNLOCKED:
672                     if (!eventTypes.remove(ChangeType.NODE_LOCKED)) {
673                         // It was not previously locked by this change set, so we should unlock it ...
674                         eventTypes.add(ChangeType.NODE_UNLOCKED);
675                     }
676                     break;
677             }
678         }
679 
680         public void setNextChanges( NetChangeDetails details ) {
681             this.nextChanges = details;
682         }
683 
684         public NetChangeDetails next() {
685             return nextChanges;
686         }
687 
688         public NetChangeDetails latest() {
689             return nextChanges != null ? nextChanges.latest() : this;
690         }
691 
692         public void movedFrom( Location originalLocation ) {
693             this.eventTypes.add(ChangeType.NODE_MOVED);
694             this.movedFrom = originalLocation;
695         }
696 
697         public void movedBefore( Location before,
698                                  boolean isReorder ) {
699             this.eventTypes.add(ChangeType.NODE_MOVED);
700             this.movedBefore = before;
701             this.reorder = isReorder;
702         }
703 
704         public void addEventType( ChangeType eventType ) {
705             this.eventTypes.add(eventType);
706         }
707 
708         public void addProperty( Property property ) {
709             this.addedProperties.add(property);
710             this.eventTypes.add(ChangeType.PROPERTY_ADDED);
711         }
712 
713         public void changeProperty( Property property ) {
714             // if property was previously added then changed just keep the added
715             if (!this.addedProperties.contains(property)) {
716                 this.modifiedProperties.add(property);
717                 this.eventTypes.add(ChangeType.PROPERTY_CHANGED);
718             }
719         }
720 
721         public void removeProperty( Name propertyName ) {
722             // if property was previously added a remove results in a net no change
723             boolean handled = false;
724 
725             for (Property property : this.addedProperties) {
726                 if (property.getName().equals(propertyName)) {
727                     handled = true;
728                     this.addedProperties.remove(property);
729 
730                     // get rid of event type if no longer applicable
731                     if (this.addedProperties.isEmpty()) {
732                         this.eventTypes.remove(ChangeType.PROPERTY_ADDED);
733                     }
734 
735                     break;
736                 }
737             }
738 
739             if (!handled) {
740                 // if property was previously changed and now is being removed the change is no longer needed
741                 for (Property property : this.modifiedProperties) {
742                     if (property.getName().equals(propertyName)) {
743                         this.modifiedProperties.remove(property);
744 
745                         // get rid of event type if no longer applicable
746                         if (this.modifiedProperties.isEmpty()) {
747                             this.eventTypes.remove(ChangeType.PROPERTY_CHANGED);
748                         }
749 
750                         break;
751                     }
752                 }
753 
754                 // now add to removed collection
755                 this.removedProperties.add(propertyName);
756                 this.eventTypes.add(ChangeType.PROPERTY_REMOVED);
757             }
758         }
759 
760         /**
761          * @return nodeAction
762          */
763         public EnumSet<ChangeType> getEventTypes() {
764             return this.eventTypes;
765         }
766 
767         /**
768          * @return the added properties
769          */
770         public Set<Property> getAddedProperties() {
771             return this.addedProperties;
772         }
773 
774         /**
775          * @return modified properties
776          */
777         public Set<Property> getModifiedProperties() {
778             return this.modifiedProperties;
779         }
780 
781         /**
782          * @return removedProperties
783          */
784         public Set<Name> getRemovedProperties() {
785             return this.removedProperties;
786         }
787 
788         /**
789          * @return movedFrom
790          */
791         public Location getMovedFrom() {
792             return movedFrom;
793         }
794 
795         /**
796          * @return movedBefore
797          */
798         public Location getMovedBefore() {
799             return movedBefore;
800         }
801 
802         /**
803          * @return reorder
804          */
805         public boolean isReorder() {
806             return reorder;
807         }
808     }
809 }