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 }