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