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    }