001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006     * See the AUTHORS.txt file in the distribution for a full listing of 
007     * individual contributors.
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     * 
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.dna.graph.observe;
025    
026    import java.util.Collections;
027    import java.util.EnumSet;
028    import java.util.HashMap;
029    import java.util.HashSet;
030    import java.util.Map;
031    import java.util.Set;
032    import java.util.TreeMap;
033    import net.jcip.annotations.Immutable;
034    import net.jcip.annotations.NotThreadSafe;
035    import org.jboss.dna.common.util.HashCode;
036    import org.jboss.dna.graph.Location;
037    import org.jboss.dna.graph.property.Name;
038    import org.jboss.dna.graph.property.Path;
039    import org.jboss.dna.graph.property.Property;
040    import org.jboss.dna.graph.request.ChangeRequest;
041    import org.jboss.dna.graph.request.CreateNodeRequest;
042    import org.jboss.dna.graph.request.DeleteBranchRequest;
043    import org.jboss.dna.graph.request.DeleteChildrenRequest;
044    import org.jboss.dna.graph.request.RemovePropertyRequest;
045    import org.jboss.dna.graph.request.SetPropertyRequest;
046    import org.jboss.dna.graph.request.UpdatePropertiesRequest;
047    
048    /**
049     * A specialized {@link Observer} that figures out the net changes made during a single {@link Changes set of changes}. For
050     * 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
051     * then deleted, no net change will be observed.
052     */
053    public abstract class NetChangeObserver extends ChangeObserver {
054    
055        public enum ChangeType {
056            NODE_ADDED,
057            NODE_REMOVED,
058            PROPERTY_ADDED,
059            PROPERTY_REMOVED,
060            PROPERTY_CHANGED;
061        }
062    
063        protected NetChangeObserver() {
064        }
065    
066        /**
067         * {@inheritDoc}
068         * 
069         * @see org.jboss.dna.graph.observe.ChangeObserver#notify(org.jboss.dna.graph.observe.Changes)
070         */
071        @Override
072        public void notify( Changes changes ) {
073            Map<String, Map<Location, NetChangeDetails>> detailsByLocationByWorkspace = new HashMap<String, Map<Location, NetChangeDetails>>();
074            // Process each of the events, extracting the node path and property details for each ...
075            for (ChangeRequest change : changes) {
076                Location location = change.changedLocation();
077                String workspace = change.changedWorkspace();
078    
079                // Find the NetChangeDetails for this node ...
080                Map<Location, NetChangeDetails> detailsByLocation = detailsByLocationByWorkspace.get(workspace);
081                NetChangeDetails details = null;
082                if (detailsByLocation == null) {
083                    detailsByLocation = new TreeMap<Location, NetChangeDetails>();
084                    detailsByLocationByWorkspace.put(workspace, detailsByLocation);
085                    details = new NetChangeDetails();
086                    detailsByLocation.put(location, details);
087                } else {
088                    details = detailsByLocation.get(location);
089                    if (details == null) {
090                        details = new NetChangeDetails();
091                        detailsByLocation.put(location, details);
092                    }
093                }
094    
095                // Process the specific kind of change ...
096                if (change instanceof CreateNodeRequest) {
097                    CreateNodeRequest create = (CreateNodeRequest)change;
098                    details.addEventType(ChangeType.NODE_ADDED);
099                    for (Property property : create) {
100                        details.addProperty(property);
101                    }
102                } else if (change instanceof UpdatePropertiesRequest) {
103                    UpdatePropertiesRequest update = (UpdatePropertiesRequest)change;
104                    for (Map.Entry<Name, Property> entry : update.properties().entrySet()) {
105                        Property property = entry.getValue();
106                        if (property != null) {
107                            details.changeProperty(property);
108                        } else {
109                            details.removeProperty(entry.getKey());
110                        }
111                    }
112                } else if (change instanceof SetPropertyRequest) {
113                    SetPropertyRequest set = (SetPropertyRequest)change;
114                    details.changeProperty(set.property());
115                } else if (change instanceof RemovePropertyRequest) {
116                    RemovePropertyRequest remove = (RemovePropertyRequest)change;
117                    details.removeProperty(remove.propertyName());
118                } else if (change instanceof DeleteBranchRequest) {
119                    details.addEventType(ChangeType.NODE_REMOVED);
120                } else if (change instanceof DeleteChildrenRequest) {
121                    DeleteChildrenRequest delete = (DeleteChildrenRequest)change;
122                    for (Location deletedChild : delete.getActualChildrenDeleted()) {
123                        NetChangeDetails childDetails = detailsByLocation.get(location);
124                        if (childDetails == null) {
125                            childDetails = new NetChangeDetails();
126                            detailsByLocation.put(deletedChild, childDetails);
127                        }
128                        childDetails.addEventType(ChangeType.NODE_REMOVED);
129                    }
130                }
131            }
132    
133            // Walk through the net changes ...
134            String sourceName = changes.getSourceName();
135            for (Map.Entry<String, Map<Location, NetChangeDetails>> byWorkspaceEntry : detailsByLocationByWorkspace.entrySet()) {
136                String workspaceName = byWorkspaceEntry.getKey();
137                // Iterate over the entries. Since we've used a TreeSet, we'll get these with the lower paths first ...
138                for (Map.Entry<Location, NetChangeDetails> entry : byWorkspaceEntry.getValue().entrySet()) {
139                    Location location = entry.getKey();
140                    NetChangeDetails details = entry.getValue();
141                    notify(new NetChange(sourceName, workspaceName, location, details.getEventTypes(),
142                                         details.getModifiedProperties(), details.getRemovedProperties()));
143                }
144            }
145        }
146    
147        /**
148         * Method that is called for each net change.
149         * 
150         * @param change
151         */
152        protected abstract void notify( NetChange change );
153    
154        /**
155         * A notification of changes to a node.
156         */
157        @Immutable
158        public static class NetChange {
159    
160            private final String sourceName;
161            private final String workspaceName;
162            private final Location location;
163            private final EnumSet<ChangeType> eventTypes;
164            private final Set<Property> modifiedProperties;
165            private final Set<Name> removedProperties;
166            private final int hc;
167    
168            public NetChange( String sourceName,
169                              String workspaceName,
170                              Location location,
171                              EnumSet<ChangeType> eventTypes,
172                              Set<Property> modifiedProperties,
173                              Set<Name> removedProperties ) {
174                assert sourceName != null;
175                assert workspaceName != null;
176                assert location != null;
177                this.sourceName = sourceName;
178                this.workspaceName = workspaceName;
179                this.location = location;
180                this.hc = HashCode.compute(this.workspaceName, this.location);
181                this.eventTypes = eventTypes;
182                if (modifiedProperties == null) modifiedProperties = Collections.emptySet();
183                if (removedProperties == null) removedProperties = Collections.emptySet();
184                this.modifiedProperties = Collections.unmodifiableSet(modifiedProperties);
185                this.removedProperties = Collections.unmodifiableSet(removedProperties);
186            }
187    
188            /**
189             * @return absolutePath
190             */
191            public Path getPath() {
192                return this.location.getPath();
193            }
194    
195            /**
196             * @return repositorySourceName
197             */
198            public String getRepositorySourceName() {
199                return this.sourceName;
200            }
201    
202            /**
203             * @return repositoryWorkspaceName
204             */
205            public String getRepositoryWorkspaceName() {
206                return this.workspaceName;
207            }
208    
209            /**
210             * @return modifiedProperties
211             */
212            public Set<Property> getModifiedProperties() {
213                return this.modifiedProperties;
214            }
215    
216            /**
217             * @return removedProperties
218             */
219            public Set<Name> getRemovedProperties() {
220                return this.removedProperties;
221            }
222    
223            /**
224             * {@inheritDoc}
225             */
226            @Override
227            public int hashCode() {
228                return this.hc;
229            }
230    
231            public boolean includesAll( ChangeType... jcrEventTypes ) {
232                for (ChangeType jcrEventType : jcrEventTypes) {
233                    if (!this.eventTypes.contains(jcrEventType)) return false;
234                }
235                return true;
236            }
237    
238            public boolean includes( ChangeType... jcrEventTypes ) {
239                for (ChangeType jcrEventType : jcrEventTypes) {
240                    if (this.eventTypes.contains(jcrEventType)) return true;
241                }
242                return false;
243            }
244    
245            public boolean is( ChangeType jcrEventTypes ) {
246                return this.eventTypes.contains(jcrEventTypes);
247            }
248    
249            public boolean isSameNode( NetChange that ) {
250                if (that == this) return true;
251                if (this.hc != that.hc) return false;
252                if (!this.workspaceName.equals(that.workspaceName)) return false;
253                if (!this.location.equals(that.location)) return false;
254                return true;
255            }
256    
257            /**
258             * Determine whether this node change includes the setting of new value(s) for the supplied property.
259             * 
260             * @param property the name of the property
261             * @return true if the named property has a new value on this node, or false otherwise
262             */
263            public boolean isPropertyModified( String property ) {
264                return this.modifiedProperties.contains(property);
265            }
266    
267            /**
268             * Determine whether this node change includes the removal of the supplied property.
269             * 
270             * @param property the name of the property
271             * @return true if the named property was removed from this node, or false otherwise
272             */
273            public boolean isPropertyRemoved( String property ) {
274                return this.removedProperties.contains(property);
275            }
276    
277            /**
278             * {@inheritDoc}
279             */
280            @Override
281            public boolean equals( Object obj ) {
282                if (obj == this) return true;
283                if (obj instanceof NetChange) {
284                    NetChange that = (NetChange)obj;
285                    if (!this.isSameNode(that)) return false;
286                    if (this.eventTypes != that.eventTypes) return false;
287                    return true;
288                }
289                return false;
290            }
291    
292            /**
293             * {@inheritDoc}
294             */
295            @Override
296            public String toString() {
297                return this.workspaceName + "=>" + this.location;
298            }
299        }
300    
301        /**
302         * Internal utility class used in the computation of the net changes.
303         */
304        @NotThreadSafe
305        private static class NetChangeDetails {
306    
307            private final Set<Property> modifiedProperties = new HashSet<Property>();
308            private final Set<Name> removedProperties = new HashSet<Name>();
309            private EnumSet<ChangeType> eventTypes = EnumSet.noneOf(ChangeType.class);
310    
311            protected NetChangeDetails() {
312            }
313    
314            public void addEventType( ChangeType eventType ) {
315                this.eventTypes.add(eventType);
316            }
317    
318            public void addProperty( Property property ) {
319                this.modifiedProperties.add(property);
320                this.eventTypes.add(ChangeType.PROPERTY_ADDED);
321            }
322    
323            public void changeProperty( Property property ) {
324                this.modifiedProperties.add(property);
325                this.eventTypes.add(ChangeType.PROPERTY_CHANGED);
326            }
327    
328            public void removeProperty( Name propertyName ) {
329                this.removedProperties.add(propertyName);
330                this.eventTypes.add(ChangeType.PROPERTY_REMOVED);
331            }
332    
333            /**
334             * @return nodeAction
335             */
336            public EnumSet<ChangeType> getEventTypes() {
337                return this.eventTypes;
338            }
339    
340            /**
341             * @return addedProperties
342             */
343            public Set<Property> getModifiedProperties() {
344                return this.modifiedProperties;
345            }
346    
347            /**
348             * @return removedProperties
349             */
350            public Set<Name> getRemovedProperties() {
351                return this.removedProperties;
352            }
353        }
354    }