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 }