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 }