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    }