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 * Unless otherwise indicated, all code in JBoss DNA is licensed
010 * 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.jcr.cache;
025
026 import java.util.Collection;
027 import java.util.Collections;
028 import java.util.HashMap;
029 import java.util.HashSet;
030 import java.util.LinkedList;
031 import java.util.List;
032 import java.util.Map;
033 import java.util.Set;
034 import java.util.UUID;
035 import net.jcip.annotations.NotThreadSafe;
036 import org.jboss.dna.graph.JcrLexicon;
037 import org.jboss.dna.graph.Location;
038 import org.jboss.dna.graph.property.Name;
039 import org.jboss.dna.graph.property.NameFactory;
040 import org.jboss.dna.graph.property.Path;
041 import org.jboss.dna.graph.property.PathFactory;
042 import org.jboss.dna.graph.property.ValueFactories;
043 import org.jboss.dna.graph.property.ValueFactory;
044 import org.jboss.dna.graph.property.Path.Segment;
045 import org.jboss.dna.jcr.DnaIntLexicon;
046 import org.jboss.dna.jcr.NodeDefinitionId;
047
048 /**
049 * The information that describes a node. This is the information that is kept in the cache.
050 * <p>
051 * Each instance maintains a reference to the original (usually immutable) NodeInfo representation that was probably read from the
052 * repository.
053 */
054 @NotThreadSafe
055 public class ChangedNodeInfo implements NodeInfo {
056
057 protected static final PropertyInfo DELETED_PROPERTY = null;
058
059 /**
060 * A reference to the original representation of the node.
061 */
062 private final NodeInfo original;
063
064 /**
065 * The new parent for this node if it has been changed, or null if the parent has not be changed.
066 */
067 private UUID newParent;
068
069 /**
070 * The updated children, or null if the children have not been changed from the original's.
071 */
072 private ChangedChildren changedChildren;
073
074 /**
075 * This map, if it is non-null, contains the changed properties, overriding whatever is in the original. If a property is
076 * removed from the original, an entry is added to this map with the name of the removed property and a null PropertyInfo.
077 */
078 private Map<Name, PropertyInfo> changedProperties;
079
080 /**
081 * The updated list of mixin node type names. This is merely a cached version of what's already in the
082 * {@link JcrLexicon#MIXIN_TYPES "jcr:mixinTypes"} property.
083 */
084 private List<Name> changedMixinTypeNames;
085
086 /**
087 * The updated node definition, which may be changed when this node is moved to a different parent (with a different node
088 * type)
089 */
090 private NodeDefinitionId changedDefinitionId;
091
092 private List<UUID> peers;
093
094 private Set<Name> singleMultiPropertyNames;
095
096 /**
097 * Create an immutable NodeInfo instance.
098 *
099 * @param original the original node information, may not be null
100 */
101 public ChangedNodeInfo( NodeInfo original ) {
102 assert original != null;
103 this.original = original;
104 }
105
106 /**
107 * Returns the peer nodes for this changed node.
108 * <p>
109 * Peer nodes are nodes that must be saved with this node (e.g., the other changed node in a
110 * {@link javax.jcr.Session#move(String, String)} operation.
111 * </p>
112 *
113 * @return a collection of the UUIDs for any other nodes that must be saved with this node; may be null
114 */
115 public final Collection<UUID> getPeers() {
116 return peers;
117 }
118
119 /**
120 * Adds a peer node to this change.
121 * <p>
122 * Peer nodes are nodes that must be saved with this node (e.g., the other changed node in a
123 * {@link javax.jcr.Session#move(String, String)} operation.
124 * </p>
125 *
126 * @param peerUuid the UUID of the peer node
127 */
128 public void addPeer( UUID peerUuid ) {
129 if (peers == null) {
130 peers = new LinkedList<UUID>();
131 }
132 peers.add(peerUuid);
133 }
134
135 public boolean setSingleMultiProperty( Name name ) {
136 if (singleMultiPropertyNames == null) singleMultiPropertyNames = new HashSet<Name>();
137 return singleMultiPropertyNames.add(name);
138 }
139
140 public boolean removeSingleMultiProperty( Name name ) {
141 return singleMultiPropertyNames == null ? false : singleMultiPropertyNames.remove(name);
142 }
143
144 public Set<Name> getSingleMultiPropertyNames() {
145 return singleMultiPropertyNames;
146 }
147
148 /**
149 * Return the original node information. May be null if this is a new node.
150 *
151 * @return the original node information
152 */
153 public NodeInfo getOriginal() {
154 return original;
155 }
156
157 /**
158 * {@inheritDoc}
159 *
160 * @see org.jboss.dna.jcr.cache.NodeInfo#getOriginalLocation()
161 */
162 public Location getOriginalLocation() {
163 return original.getOriginalLocation();
164 }
165
166 /**
167 * {@inheritDoc}
168 *
169 * @see org.jboss.dna.jcr.cache.NodeInfo#getUuid()
170 */
171 public UUID getUuid() {
172 return original.getUuid();
173 }
174
175 /**
176 * {@inheritDoc}
177 *
178 * @see org.jboss.dna.jcr.cache.NodeInfo#getParent()
179 */
180 public UUID getParent() {
181 // Even if this is used for recording changes to the root node (which has no parent),
182 // the root node cannot be moved to a different node (and no other node can be moved to
183 // the root). Therefore, if this represents the root node, the original's parent UUID will
184 // be the correct parent (null), and this representation will not need to have a different
185 // (non-null) value.
186 if (newParent != null) return newParent;
187 return original.getParent();
188 }
189
190 /**
191 * Record that this node has been moved under a new parent. This method does <i>not</i> change the ChildNode references in the
192 * old or new parent.
193 *
194 * @param parent the new parent, or null if the original's parent should be used
195 * @return the previous parent (either the original's or the last new parent); may be null
196 */
197 public UUID setParent( UUID parent ) {
198 UUID result = newParent != null ? newParent : original.getParent(); // may still be null
199 newParent = parent;
200 return result;
201 }
202
203 /**
204 * {@inheritDoc}
205 *
206 * @see org.jboss.dna.jcr.cache.NodeInfo#getPrimaryTypeName()
207 */
208 public Name getPrimaryTypeName() {
209 return original.getPrimaryTypeName();
210 }
211
212 /**
213 * {@inheritDoc}
214 *
215 * @see org.jboss.dna.jcr.cache.NodeInfo#getMixinTypeNames()
216 */
217 public List<Name> getMixinTypeNames() {
218 if (changedMixinTypeNames != null) return changedMixinTypeNames;
219 return original.getMixinTypeNames();
220 }
221
222 /**
223 * {@inheritDoc}
224 *
225 * @see org.jboss.dna.jcr.cache.NodeInfo#getDefinitionId()
226 */
227 public NodeDefinitionId getDefinitionId() {
228 if (changedDefinitionId != null) return changedDefinitionId;
229 return original.getDefinitionId();
230 }
231
232 /**
233 * Set the identifier of the node definition for this node. This should normally be changed by
234 * {@link #setProperty(PropertyInfo, ValueFactories) setting} the {@link DnaIntLexicon#NODE_DEFINITON} property. However,
235 * since that property is not always allowed, this method provides a way to set it locally (without requiring a property).
236 *
237 * @param definitionId the new property definition identifier; may not be null
238 * @see #setProperty(PropertyInfo, ValueFactories)
239 */
240 public void setDefinitionId( NodeDefinitionId definitionId ) {
241 if (!getDefinitionId().equals(definitionId)) {
242 assert definitionId != null;
243 changedDefinitionId = definitionId;
244 }
245 }
246
247 /**
248 * {@inheritDoc}
249 *
250 * @see org.jboss.dna.jcr.cache.NodeInfo#getChildren()
251 */
252 public Children getChildren() {
253 if (changedChildren != null) return changedChildren;
254 return original.getChildren();
255 }
256
257 /**
258 * Get the UUIDs for the children for this node that have been removed since the node was last persisted.
259 *
260 * @return a collection of the UUIDs of the removed children; never null but possibly empty
261 */
262 public Collection<UUID> getUuidsForRemovedChildren() {
263 if (original == null) return Collections.emptySet();
264
265 Set<UUID> removedChildren = new HashSet<UUID>();
266 for (ChildNode originalChildNode : original.getChildren()) {
267 if (!this.changedChildren.childrenByUuid.containsKey(originalChildNode.getUuid())) {
268 removedChildren.add(originalChildNode.getUuid());
269 }
270 }
271 return removedChildren;
272 }
273
274 /**
275 * Add a child to the children. This method does nothing if the child is already in the children.
276 *
277 * @param childName the name of the child that is to be added; may not be null
278 * @param childUuid the UUID of the child that is to be added; may not be null
279 * @param factory the path factory that should be used to create a {@link Segment} for the new {@link ChildNode} object
280 * @return the child node that was just added; never null
281 */
282 public ChildNode addChild( Name childName,
283 UUID childUuid,
284 PathFactory factory ) {
285 if (changedChildren == null) {
286 // We need to capture the original children as a changed contained ...
287 changedChildren = new ChangedChildren(original.getChildren());
288 }
289 return changedChildren.add(childName, childUuid, factory);
290 }
291
292 /**
293 * Add a child to the children. This method does nothing if the child is already in the children.
294 *
295 * @param childName the name of the child that is to be added; may not be null
296 * @param childUuid the UUID of the child that is to be added; may not be null
297 * @param beforeChild the segment for the child that the new child should be added before; may not be null
298 * @param factory the path factory that should be used to create a {@link Segment} for the new {@link ChildNode} object
299 */
300 public void addChild( Name childName,
301 Path.Segment beforeChild,
302 UUID childUuid,
303 PathFactory factory ) {
304 if (changedChildren == null) {
305 // We need to capture the original children as a changed contained ...
306 changedChildren = new ChangedChildren(original.getChildren());
307 }
308 changedChildren = changedChildren.with(childName, beforeChild, childUuid, factory);
309 }
310
311 /**
312 * Remove a child from the children. This method only uses the child's UUID to identify the contained ChildNode instance that
313 * should be removed.
314 *
315 * @param childUUID the UUID of the child that is to be removed; may not be null
316 * @param factory the path factory that should be used to create a {@link Segment} for replacement {@link ChildNode} objects
317 * for nodes with the same name that and higher same-name-sibiling indexes.
318 * @return the child node that was removed, or null if no such child could be removed
319 */
320 public ChildNode removeChild( UUID childUUID,
321 PathFactory factory ) {
322 ChildNode deleted = null;
323 if (changedChildren == null) {
324 // Create the changed children. First check whether there are 0 or 1 child ...
325 Children existing = original.getChildren();
326 int numExisting = existing.size();
327 if (numExisting == 0) {
328 // nothing to do, so return the original's children
329 return null;
330 }
331 deleted = existing.getChild(childUUID);
332 if (deleted == null) {
333 // The requested child doesn't exist in the children, so return ...
334 return null;
335 }
336 if (numExisting == 1) {
337 // We're removing the only child in the original ...
338 changedChildren = new ChangedChildren(existing.getParentUuid());
339 return existing.getChild(childUUID);
340 }
341 // There is at least one child, so create the new children container ...
342 assert existing instanceof InternalChildren;
343 InternalChildren internal = (InternalChildren)existing;
344 changedChildren = internal.without(childUUID, factory);
345 } else {
346 deleted = changedChildren.getChild(childUUID);
347 changedChildren = changedChildren.without(childUUID, factory);
348 }
349 return deleted;
350 }
351
352 /**
353 * {@inheritDoc}
354 *
355 * @see org.jboss.dna.jcr.cache.NodeInfo#hasProperties()
356 */
357 public boolean hasProperties() {
358 if (changedProperties == null) return original.hasProperties();
359 int numUnchanged = original.getPropertyCount();
360 int numChangedOrDeleted = changedProperties.size();
361 if (numUnchanged > numChangedOrDeleted) return true; // more unchanged than could be deleted
362 // They could all be changed or deleted, so we need to find one changed property ...
363 for (Map.Entry<Name, PropertyInfo> entry : changedProperties.entrySet()) {
364 if (entry.getValue() != DELETED_PROPERTY) return true;
365 }
366 return false; // all properties must have been deleted ...
367 }
368
369 /**
370 * {@inheritDoc}
371 *
372 * @see org.jboss.dna.jcr.cache.NodeInfo#getPropertyCount()
373 */
374 public int getPropertyCount() {
375 int numUnchanged = original.getPropertyCount();
376 if (changedProperties == null) return numUnchanged;
377 return getPropertyNames().size();
378 }
379
380 /**
381 * {@inheritDoc}
382 *
383 * @see org.jboss.dna.jcr.cache.NodeInfo#getPropertyNames()
384 */
385 public Set<Name> getPropertyNames() {
386 if (changedProperties != null) {
387 Set<Name> result = new HashSet<Name>(original.getPropertyNames());
388 for (Map.Entry<Name, PropertyInfo> entry : changedProperties.entrySet()) {
389 if (entry.getValue() != DELETED_PROPERTY) {
390 result.add(entry.getKey());
391 } else {
392 result.remove(entry.getKey());
393 }
394 }
395 return result; // don't make unmod wrapper, since we've already made a copy ...
396 }
397 return original.getPropertyNames();
398 }
399
400 /**
401 * {@inheritDoc}
402 *
403 * @see org.jboss.dna.jcr.cache.NodeInfo#getProperty(org.jboss.dna.graph.property.Name)
404 */
405 public PropertyInfo getProperty( Name name ) {
406 if (changedProperties != null && changedProperties.containsKey(name)) {
407 return changedProperties.get(name); // either the changed PropertyInfo, or null if property was deleted
408 }
409 return original.getProperty(name);
410 }
411
412 public PropertyInfo setProperty( PropertyInfo newProperty,
413 ValueFactories factories ) {
414 Name name = newProperty.getPropertyName();
415 PropertyInfo previous = null;
416 if (changedProperties == null) {
417 // There were no changes made yet ...
418
419 // Create the map of changed properties ...
420 changedProperties = new HashMap<Name, PropertyInfo>();
421 changedProperties.put(name, newProperty);
422
423 // And return the original property (or null if there was none) ...
424 previous = original.getProperty(name);
425 } else if (changedProperties.containsKey(name)) {
426 // The property was already changed, in which case we need to return the changed one ...
427 previous = changedProperties.put(name, newProperty);
428 } else {
429 // Otherwise, the property was not yet changed or deleted ...
430 previous = original.getProperty(name);
431 changedProperties.put(name, newProperty);
432 }
433 // If this property was the "jcr:mixinTypes" property, update the cached values ...
434 if (name.equals(JcrLexicon.MIXIN_TYPES)) {
435 if (changedMixinTypeNames == null) {
436 changedMixinTypeNames = new LinkedList<Name>();
437 } else {
438 changedMixinTypeNames.clear();
439 }
440 NameFactory nameFactory = factories.getNameFactory();
441 for (Object value : newProperty.getProperty()) {
442 changedMixinTypeNames.add(nameFactory.create(value));
443 }
444 } else if (name.equals(DnaIntLexicon.NODE_DEFINITON)) {
445 ValueFactory<String> stringFactory = factories.getStringFactory();
446 String value = stringFactory.create(newProperty.getProperty().getFirstValue());
447 changedDefinitionId = NodeDefinitionId.fromString(value, factories.getNameFactory());
448 }
449
450 return previous;
451 }
452
453 public PropertyInfo removeProperty( Name name ) {
454 if (changedProperties == null) {
455 // Make sure the property was in the original ...
456 PropertyInfo existing = original.getProperty(name);
457 if (existing == null) {
458 // The named property didn't exist in the original, nor was it added and deleted in this object ...
459 return null;
460 }
461
462 // Create the map of changed properties ...
463 changedProperties = new HashMap<Name, PropertyInfo>();
464 changedProperties.put(name, DELETED_PROPERTY);
465 return existing;
466 }
467 // The property may already have been changed, in which case we need to return the changed one ...
468 if (changedProperties.containsKey(name)) {
469 PropertyInfo changed = changedProperties.put(name, null);
470 // The named property was indeed deleted ...
471 return changed;
472 }
473 // Otherwise, the property was not yet changed or deleted ...
474 PropertyInfo changed = original.getProperty(name);
475 changedProperties.put(name, null);
476 return changed;
477 }
478
479 /**
480 * {@inheritDoc}
481 *
482 * @see org.jboss.dna.jcr.cache.NodeInfo#isNew()
483 */
484 public boolean isNew() {
485 return false;
486 }
487
488 /**
489 * {@inheritDoc}
490 *
491 * @see org.jboss.dna.jcr.cache.NodeInfo#isModified()
492 */
493 public boolean isModified() {
494 return true;
495 }
496 }