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 
361         // Now notify of all of the changes ...
362         notify(new NetChanges(changes, netChanges));
363     }
364 
365     /**
366      * Method that is called for the set of net changes.
367      * 
368      * @param netChanges the net changes; never null
369      */
370     protected abstract void notify( NetChanges netChanges );
371 
372     /**
373      * A set of net changes that were made atomically. Each change is in the form of a frozen {@link ChangeRequest}, and the net
374      * change is in the form.
375      */
376     @Immutable
377     public static final class NetChanges extends Changes {
378         private static final long serialVersionUID = 1L;
379 
380         private final List<NetChange> netChanges;
381 
382         public NetChanges( Changes changes,
383                            List<NetChange> netChanges ) {
384             super(changes);
385             assert netChanges != null;
386             assert !netChanges.isEmpty();
387             this.netChanges = Collections.unmodifiableList(netChanges);
388         }
389 
390         /**
391          * Get the list of net changes.
392          * 
393          * @return the immutable list of net changes; never null and never empty
394          */
395         public List<NetChange> getNetChanges() {
396             return this.netChanges;
397         }
398 
399         /**
400          * {@inheritDoc}
401          * 
402          * @see java.lang.Object#toString()
403          */
404         @Override
405         public String toString() {
406             if (processId.length() != 0) {
407                 return getTimestamp() + " @" + getUserName() + " [" + getSourceName() + "] - " + netChanges.size() + " changes";
408             }
409             return getTimestamp() + " @" + getUserName() + " #" + getProcessId() + " [" + getSourceName() + "] - "
410                    + netChanges.size() + " changes";
411         }
412     }
413 
414     /**
415      * A notification of changes to a node.
416      */
417     @Immutable
418     public static final class NetChange {
419 
420         private final String workspaceName;
421         private final Location location;
422         private final EnumSet<ChangeType> eventTypes;
423         private final Set<Property> addedProperties;
424         private final Set<Property> modifiedProperties;
425         private final Set<Property> addedOrModifiedProperties;
426         private final Set<Name> removedProperties;
427         private final int hc;
428         private final Location movedFrom;
429         private final Location movedBefore;
430         private final boolean isReorder;
431 
432         public NetChange( String workspaceName,
433                           Location location,
434                           EnumSet<ChangeType> eventTypes,
435                           Set<Property> addedProperties,
436                           Set<Property> modifiedProperties,
437                           Set<Name> removedProperties,
438                           Location movedFrom,
439                           Location movedBefore,
440                           boolean isReorder ) {
441             assert workspaceName != null;
442             assert location != null;
443             this.workspaceName = workspaceName;
444             this.location = location;
445             this.hc = HashCode.compute(this.workspaceName, this.location);
446             this.eventTypes = eventTypes;
447             Set<Property> addedOrModified = null;
448             if (addedProperties == null) {
449                 addedProperties = Collections.emptySet();
450                 addedOrModified = modifiedProperties; // may be null
451             } else {
452                 addedOrModified = addedProperties;
453             }
454             if (modifiedProperties == null) {
455                 if (addedOrModified == null) addedOrModified = Collections.emptySet();
456                 modifiedProperties = Collections.emptySet();
457             } else {
458                 if (addedOrModified == null) {
459                     addedOrModified = modifiedProperties;
460                 } else {
461                     addedOrModified = new HashSet<Property>(modifiedProperties);
462                     addedOrModified.addAll(addedProperties);
463                 }
464             }
465             if (removedProperties == null) removedProperties = Collections.emptySet();
466             this.addedProperties = Collections.unmodifiableSet(addedProperties);
467             this.modifiedProperties = Collections.unmodifiableSet(modifiedProperties);
468             this.removedProperties = Collections.unmodifiableSet(removedProperties);
469             this.addedOrModifiedProperties = Collections.unmodifiableSet(addedOrModified);
470             this.movedFrom = movedFrom;
471             this.movedBefore = movedBefore;
472             this.isReorder = isReorder;
473         }
474 
475         /**
476          * @return the node location
477          */
478         public Location getLocation() {
479             return this.location;
480         }
481 
482         /**
483          * Get the original location for this node, or null if this node was not moved
484          * 
485          * @return the original location for this node before it was moved, or null if it was not moved
486          */
487         public Location getOriginalLocation() {
488             return this.movedFrom;
489         }
490 
491         /**
492          * Get the location before which the node was moved, or null if this node was not moved before a particular node
493          * 
494          * @return movedBefore the location of the existing child under the new parent, before which this node was moved; or null
495          *         if this node was not moved before another node
496          */
497         public Location getMovedBefore() {
498             return movedBefore;
499         }
500 
501         /**
502          * True if this is a simple re-ordering of sibling nodes.
503          * 
504          * @return true if this is a reordering operation, or false otherwise
505          */
506         public boolean isReorder() {
507             return isReorder;
508         }
509 
510         /**
511          * @return absolutePath
512          */
513         public Path getPath() {
514             return this.location.getPath();
515         }
516 
517         /**
518          * @return repositoryWorkspaceName
519          */
520         public String getRepositoryWorkspaceName() {
521             return this.workspaceName;
522         }
523 
524         /**
525          * @return the added properties
526          */
527         public Set<Property> getAddedProperties() {
528             return this.addedProperties;
529         }
530 
531         /**
532          * @return modifiedProperties
533          */
534         public Set<Property> getModifiedProperties() {
535             return this.modifiedProperties;
536         }
537 
538         /**
539          * Get the combination of {@link #getAddedProperties() added} and {@link #getModifiedProperties() modified} properties.
540          * 
541          * @return the immutable set of properties that were added or modified; never null but possibly empty
542          */
543         public Set<Property> getAddedOrModifiedProperties() {
544             return this.addedOrModifiedProperties;
545         }
546 
547         /**
548          * @return removedProperties
549          */
550         public Set<Name> getRemovedProperties() {
551             return this.removedProperties;
552         }
553 
554         /**
555          * {@inheritDoc}
556          */
557         @Override
558         public int hashCode() {
559             return this.hc;
560         }
561 
562         /**
563          * Determine whether this net change includes all of the supplied types.
564          * 
565          * @param jcrEventTypes the types to check for
566          * @return true if all of the supplied events are included in this net change, or false otherwise
567          */
568         public boolean includesAllOf( ChangeType... jcrEventTypes ) {
569             for (ChangeType jcrEventType : jcrEventTypes) {
570                 if (!this.eventTypes.contains(jcrEventType)) return false;
571             }
572             return true;
573         }
574 
575         /**
576          * Determine whether this net change includes any of the supplied types.
577          * 
578          * @param jcrEventTypes the types to check for
579          * @return true if any of the supplied events are included in this net change, or false otherwise
580          */
581         public boolean includes( ChangeType... jcrEventTypes ) {
582             for (ChangeType jcrEventType : jcrEventTypes) {
583                 if (this.eventTypes.contains(jcrEventType)) return true;
584             }
585             return false;
586         }
587 
588         public boolean isSameNode( NetChange that ) {
589             if (that == this) return true;
590             if (this.hc != that.hc) return false;
591             if (!this.workspaceName.equals(that.workspaceName)) return false;
592             if (!this.location.isSame(that.location)) return false;
593             return true;
594         }
595 
596         /**
597          * Determine whether this node change includes the setting of new value(s) for the supplied property.
598          * 
599          * @param property the name of the property
600          * @return true if the named property has a new value on this node, or false otherwise
601          */
602         public boolean isPropertyModified( String property ) {
603             return this.modifiedProperties.contains(property);
604         }
605 
606         /**
607          * Determine whether this node change includes the removal of the supplied property.
608          * 
609          * @param property the name of the property
610          * @return true if the named property was removed from this node, or false otherwise
611          */
612         public boolean isPropertyRemoved( String property ) {
613             return this.removedProperties.contains(property);
614         }
615 
616         /**
617          * {@inheritDoc}
618          */
619         @Override
620         public boolean equals( Object obj ) {
621             if (obj == this) return true;
622             if (obj instanceof NetChange) {
623                 NetChange that = (NetChange)obj;
624                 if (!this.isSameNode(that)) return false;
625                 if (this.eventTypes != that.eventTypes) return false;
626                 return true;
627             }
628             return false;
629         }
630 
631         /**
632          * {@inheritDoc}
633          */
634         @Override
635         public String toString() {
636             return this.workspaceName + "=>" + this.location;
637         }
638     }
639 
640     private enum LockAction {
641         LOCKED,
642         UNLOCKED;
643     }
644 
645     /**
646      * Internal utility class used in the computation of the net changes.
647      */
648     @NotThreadSafe
649     private static class NetChangeDetails {
650 
651         private final Set<Property> modifiedProperties = new HashSet<Property>();
652         private final Set<Property> addedProperties = new HashSet<Property>();
653         private final Set<Name> removedProperties = new HashSet<Name>();
654         private EnumSet<ChangeType> eventTypes = EnumSet.noneOf(ChangeType.class);
655         private Location movedFrom;
656         private Location movedBefore;
657         private boolean reorder;
658         private NetChangeDetails nextChanges;
659 
660         protected NetChangeDetails() {
661         }
662 
663         public void setLockAction( LockAction lockAction ) {
664             switch (lockAction) {
665                 case LOCKED:
666                     // always mark as locked and remove any unlocked state
667                     eventTypes.add(ChangeType.NODE_LOCKED);
668                     eventTypes.remove(ChangeType.NODE_UNLOCKED);
669                     break;
670                 case UNLOCKED:
671                     if (!eventTypes.remove(ChangeType.NODE_LOCKED)) {
672                         // It was not previously locked by this change set, so we should unlock it ...
673                         eventTypes.add(ChangeType.NODE_UNLOCKED);
674                     }
675                     break;
676             }
677         }
678 
679         public void setNextChanges( NetChangeDetails details ) {
680             this.nextChanges = details;
681         }
682 
683         public NetChangeDetails next() {
684             return nextChanges;
685         }
686 
687         public NetChangeDetails latest() {
688             return nextChanges != null ? nextChanges.latest() : this;
689         }
690 
691         public void movedFrom( Location originalLocation ) {
692             this.eventTypes.add(ChangeType.NODE_MOVED);
693             this.movedFrom = originalLocation;
694         }
695 
696         public void movedBefore( Location before,
697                                  boolean isReorder ) {
698             this.eventTypes.add(ChangeType.NODE_MOVED);
699             this.movedBefore = before;
700             this.reorder = isReorder;
701         }
702 
703         public void addEventType( ChangeType eventType ) {
704             this.eventTypes.add(eventType);
705         }
706 
707         public void addProperty( Property property ) {
708             this.addedProperties.add(property);
709             this.eventTypes.add(ChangeType.PROPERTY_ADDED);
710         }
711 
712         public void changeProperty( Property property ) {
713             // if property was previously added then changed just keep the added
714             if (!this.addedProperties.contains(property)) {
715                 this.modifiedProperties.add(property);
716                 this.eventTypes.add(ChangeType.PROPERTY_CHANGED);
717             }
718         }
719 
720         public void removeProperty( Name propertyName ) {
721             // if property was previously added a remove results in a net no change
722             boolean handled = false;
723 
724             for (Property property : this.addedProperties) {
725                 if (property.getName().equals(propertyName)) {
726                     handled = true;
727                     this.addedProperties.remove(property);
728 
729                     // get rid of event type if no longer applicable
730                     if (this.addedProperties.isEmpty()) {
731                         this.eventTypes.remove(ChangeType.PROPERTY_ADDED);
732                     }
733 
734                     break;
735                 }
736             }
737 
738             if (!handled) {
739                 // if property was previously changed and now is being removed the change is no longer needed
740                 for (Property property : this.modifiedProperties) {
741                     if (property.getName().equals(propertyName)) {
742                         this.modifiedProperties.remove(property);
743 
744                         // get rid of event type if no longer applicable
745                         if (this.modifiedProperties.isEmpty()) {
746                             this.eventTypes.remove(ChangeType.PROPERTY_CHANGED);
747                         }
748 
749                         break;
750                     }
751                 }
752 
753                 // now add to removed collection
754                 this.removedProperties.add(propertyName);
755                 this.eventTypes.add(ChangeType.PROPERTY_REMOVED);
756             }
757         }
758 
759         /**
760          * @return nodeAction
761          */
762         public EnumSet<ChangeType> getEventTypes() {
763             return this.eventTypes;
764         }
765 
766         /**
767          * @return the added properties
768          */
769         public Set<Property> getAddedProperties() {
770             return this.addedProperties;
771         }
772 
773         /**
774          * @return modified properties
775          */
776         public Set<Property> getModifiedProperties() {
777             return this.modifiedProperties;
778         }
779 
780         /**
781          * @return removedProperties
782          */
783         public Set<Name> getRemovedProperties() {
784             return this.removedProperties;
785         }
786 
787         /**
788          * @return movedFrom
789          */
790         public Location getMovedFrom() {
791             return movedFrom;
792         }
793 
794         /**
795          * @return movedBefore
796          */
797         public Location getMovedBefore() {
798             return movedBefore;
799         }
800 
801         /**
802          * @return reorder
803          */
804         public boolean isReorder() {
805             return reorder;
806         }
807     }
808 }