/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.jackrabbit.core.state; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.apache.jackrabbit.core.id.ItemId; import org.apache.jackrabbit.core.id.NodeId; import org.apache.jackrabbit.spi.Name; /** * <code>NodeState</code> represents the state of a <code>Node</code>. */ public class NodeState extends ItemState { /** * the name of this node's primary type */ private Name nodeTypeName; /** * the names of this node's mixin types */ private NameSet mixinTypeNames = new NameSet(); /** * the id of this node. */ private NodeId id; /** * the id of the parent node or <code>null</code> if this instance * represents the root node */ private NodeId parentId; /** * insertion-ordered collection of ChildNodeEntry objects */ private ChildNodeEntries childNodeEntries = new ChildNodeEntries(); /** * set of property names (Name objects) */ private NameSet propertyNames = new NameSet(); /** * Shared set, consisting of the parent ids of this shareable node. This * entry is {@link Collections#EMPTY_SET} if this node is not shareable. */ private Set<NodeId> sharedSet = Collections.emptySet(); /** * Flag indicating whether we are using a read-write shared set. */ private boolean sharedSetRW; /** * Listener. */ private transient NodeStateListener listener; /** * Constructs a new node state that is initially connected to an overlayed * state. * * @param overlayedState the backing node state being overlayed * @param initialStatus the initial status of the node state object * @param isTransient flag indicating whether this state is transient or not */ public NodeState(NodeState overlayedState, int initialStatus, boolean isTransient) { super(overlayedState, initialStatus, isTransient); pull(); } /** * Constructs a new node state that is not connected. * * @param id id of this node * @param nodeTypeName node type of this node * @param parentId id of the parent node * @param initialStatus the initial status of the node state object * @param isTransient flag indicating whether this state is transient or not */ public NodeState(NodeId id, Name nodeTypeName, NodeId parentId, int initialStatus, boolean isTransient) { super(initialStatus, isTransient); this.id = id; this.parentId = parentId; this.nodeTypeName = nodeTypeName; } //-------------------------------------------------------< public methods > /** * {@inheritDoc} */ @Override public synchronized void copy(ItemState state, boolean syncModCount) { synchronized (state) { NodeState nodeState = (NodeState) state; id = nodeState.id; parentId = nodeState.parentId; nodeTypeName = nodeState.nodeTypeName; mixinTypeNames = (NameSet) nodeState.mixinTypeNames.clone(); propertyNames = (NameSet) nodeState.propertyNames.clone(); childNodeEntries = (ChildNodeEntries) nodeState.childNodeEntries.clone(); if (syncModCount) { setModCount(state.getModCount()); } sharedSet = nodeState.sharedSet; sharedSetRW = false; } } /** * {@inheritDoc} * * @return always true */ @Override public final boolean isNode() { return true; } /** * {@inheritDoc} */ @Override public NodeId getParentId() { return parentId; } /** * {@inheritDoc} */ @Override public ItemId getId() { return id; } /** * Returns the identifier of this node. * * @return the id of this node. */ public NodeId getNodeId() { return id; } /** * Sets the id of this node's parent. * * @param parentId the parent node's id or <code>null</code> * if either this node state should represent the root node or this node * state should be 'free floating', i.e. detached from the workspace's * hierarchy. */ public void setParentId(NodeId parentId) { this.parentId = parentId; } /** * Returns the name of this node's node type. * * @return the name of this node's node type. */ public Name getNodeTypeName() { return nodeTypeName; } /** * Returns the names of this node's mixin types. * * @return a set of the names of this node's mixin types. */ public synchronized Set<Name> getMixinTypeNames() { return mixinTypeNames; } /** * Sets the names of this node's mixin types. * * @param names set of names of mixin types */ public synchronized void setMixinTypeNames(Set<Name> names) { mixinTypeNames.replaceAll(names); } /** * Determines if there are any child node entries. * * @return <code>true</code> if there are child node entries, * <code>false</code> otherwise. */ public boolean hasChildNodeEntries() { return !childNodeEntries.isEmpty(); } /** * Determines if there is a <code>ChildNodeEntry</code> with the * specified <code>name</code>. * * @param name <code>Name</code> object specifying a node name * @return <code>true</code> if there is a <code>ChildNodeEntry</code> with * the specified <code>name</code>. */ public synchronized boolean hasChildNodeEntry(Name name) { return !childNodeEntries.get(name).isEmpty(); } /** * Determines if there is a <code>ChildNodeEntry</code> with the * specified <code>NodeId</code>. * * @param id the id of the child node * @return <code>true</code> if there is a <code>ChildNodeEntry</code> with * the specified <code>name</code>. */ public synchronized boolean hasChildNodeEntry(NodeId id) { return childNodeEntries.get(id) != null; } /** * Determines if there is a <code>ChildNodeEntry</code> with the * specified <code>name</code> and <code>index</code>. * * @param name <code>Name</code> object specifying a node name * @param index 1-based index if there are same-name child node entries * @return <code>true</code> if there is a <code>ChildNodeEntry</code> with * the specified <code>name</code> and <code>index</code>. */ public synchronized boolean hasChildNodeEntry(Name name, int index) { return childNodeEntries.get(name, index) != null; } /** * Determines if there is a property entry with the specified * <code>Name</code>. * * @param propName <code>Name</code> object specifying a property name * @return <code>true</code> if there is a property entry with the specified * <code>Name</code>. */ public synchronized boolean hasPropertyName(Name propName) { return propertyNames.contains(propName); } /** * Returns the <code>ChildNodeEntry</code> with the specified name and index * or <code>null</code> if there's no matching entry. * * @param nodeName <code>Name</code> object specifying a node name * @param index 1-based index if there are same-name child node entries * @return the <code>ChildNodeEntry</code> with the specified name and index * or <code>null</code> if there's no matching entry. */ public synchronized ChildNodeEntry getChildNodeEntry(Name nodeName, int index) { return childNodeEntries.get(nodeName, index); } /** * Returns the <code>ChildNodeEntry</code> with the specified <code>NodeId</code> or * <code>null</code> if there's no matching entry. * * @param id the id of the child node * @return the <code>ChildNodeEntry</code> with the specified <code>NodeId</code> or * <code>null</code> if there's no matching entry. * @see #addChildNodeEntry * @see #removeChildNodeEntry */ public synchronized ChildNodeEntry getChildNodeEntry(NodeId id) { return childNodeEntries.get(id); } /** * Returns a list of <code>ChildNodeEntry</code> objects denoting the * child nodes of this node. * * @return list of <code>ChildNodeEntry</code> objects * @see #addChildNodeEntry * @see #removeChildNodeEntry */ public synchronized List<ChildNodeEntry> getChildNodeEntries() { return childNodeEntries.list(); } /** * Returns a list of <code>ChildNodeEntry</code>s with the specified name. * * @param nodeName name of the child node entries that should be returned * @return list of <code>ChildNodeEntry</code> objects * @see #addChildNodeEntry * @see #removeChildNodeEntry */ public synchronized List<ChildNodeEntry> getChildNodeEntries(Name nodeName) { return childNodeEntries.get(nodeName); } /** * Adds a new <code>ChildNodeEntry</code>. * * @param nodeName <code>Name</code> object specifying the name of the new entry. * @param id the id the new entry is refering to. * @return the newly added <code>ChildNodeEntry</code> */ public ChildNodeEntry addChildNodeEntry(Name nodeName, NodeId id) { ChildNodeEntry entry = null; synchronized (this) { entry = childNodeEntries.add(nodeName, id); } notifyNodeAdded(entry); return entry; } /** * Renames a <code>ChildNodeEntry</code> by removing the old entry and * appending the new entry to the end of the list. * * @param oldName <code>Name</code> object specifying the entry's old name * @param index 1-based index if there are same-name child node entries * @param newName <code>Name</code> object specifying the entry's new name * @return <code>true</code> if the entry was successfully renamed; * otherwise <code>false</code> */ public boolean renameChildNodeEntry(Name oldName, int index, Name newName) { ChildNodeEntry oldEntry = childNodeEntries.get(oldName, index); if (oldEntry != null) { return renameChildNodeEntry(oldEntry.getId(), newName); } return false; } /** * Renames a <code>ChildNodeEntry</code> by removing the old entry and * appending the new entry to the end of the list. * * @param id id the entry to be renamed is refering to. * @param newName <code>Name</code> object specifying the entry's new name * @return <code>true</code> if the entry was successfully renamed; * otherwise <code>false</code> */ public boolean renameChildNodeEntry(NodeId id, Name newName) { ChildNodeEntry oldEntry = null; ChildNodeEntry newEntry = null; synchronized (this) { oldEntry = childNodeEntries.remove(id); if (oldEntry != null) { newEntry = childNodeEntries.add(newName, oldEntry.getId()); } } if (oldEntry != null) { if (oldEntry.getName().equals(newName)) { notifyNodesReplaced(); } else { notifyNodeAdded(newEntry); notifyNodeRemoved(oldEntry); } return true; } return false; } /** * Replaces the <code>ChildNodeEntry</code> identified by <code>oldId</code> * with a new entry. Note that the entry will <i>overwrite</i> the old * entry at the same relative position within the child node entries list. * * @param oldId id the entry to be replaced is referring to. * @param newName <code>Name</code> object specifying the entry's new name * @param newId the id the new entry is referring to. * @return <code>true</code> if the entry was successfully replaced; * otherwise <code>false</code> */ public boolean replaceChildNodeEntry(NodeId oldId, Name newName, NodeId newId) { synchronized (this) { ChildNodeEntry oldEntry = childNodeEntries.get(oldId); if (oldEntry == null) { return false; } ChildNodeEntries entries = new ChildNodeEntries(); for (ChildNodeEntry entry : childNodeEntries.list()) { if (entry.getId() == oldId) { entries.add(newName, newId); } else { entries.add(entry.getName(), entry.getId()); } } childNodeEntries = entries; } notifyNodesReplaced(); return true; } /** * Removes a <code>ChildNodeEntry</code>. * * @param nodeName <code>ChildNodeEntry</code> object specifying a node name * @param index 1-based index if there are same-name child node entries * @return <code>true</code> if the specified child node entry was found * in the list of child node entries and could be removed. */ public boolean removeChildNodeEntry(Name nodeName, int index) { ChildNodeEntry entry = null; synchronized (this) { entry = childNodeEntries.remove(nodeName, index); } if (entry != null) { notifyNodeRemoved(entry); } return entry != null; } /** * Removes a <code>ChildNodeEntry</code>. * * @param id the id of the entry to be removed * @return <code>true</code> if the specified child node entry was found * in the list of child node entries and could be removed. */ public boolean removeChildNodeEntry(NodeId id) { ChildNodeEntry entry = null; synchronized (this) { entry = childNodeEntries.remove(id); } if (entry != null) { notifyNodeRemoved(entry); } return entry != null; } /** * Removes all <code>ChildNodeEntry</code>s. */ public void removeAllChildNodeEntries() { synchronized (this) { childNodeEntries.removeAll(); } notifyNodesReplaced(); } /** * Sets the list of <code>ChildNodeEntry</code> objects denoting the * child nodes of this node. * @param nodeEntries list of {@link ChildNodeEntry}s */ public void setChildNodeEntries(List<ChildNodeEntry> nodeEntries) { synchronized (this) { childNodeEntries.removeAll(); childNodeEntries.addAll(nodeEntries); } notifyNodesReplaced(); } /** * Returns the names of this node's properties as a set of * <code>QNames</code> objects. * * @return set of <code>QNames</code> objects * @see #addPropertyName * @see #removePropertyName */ public synchronized Set<Name> getPropertyNames() { return propertyNames; } /** * Adds a property name entry. * * @param propName <code>Name</code> object specifying the property name */ public synchronized void addPropertyName(Name propName) { propertyNames.add(propName); } /** * Removes a property name entry. * * @param propName <code>Name</code> object specifying the property name * @return <code>true</code> if the specified property name was found * in the list of property name entries and could be removed. */ public synchronized boolean removePropertyName(Name propName) { return propertyNames.remove(propName); } /** * Removes all property name entries. */ public synchronized void removeAllPropertyNames() { propertyNames.removeAll(); } /** * Sets the set of <code>Name</code> objects denoting the * properties of this node. * @param propNames set of {@link Name}s. */ public synchronized void setPropertyNames(Set<Name> propNames) { propertyNames.replaceAll(propNames); } /** * Set the node type name. Needed for deserialization and should therefore * not change the internal status. * * @param nodeTypeName node type name */ public synchronized void setNodeTypeName(Name nodeTypeName) { this.nodeTypeName = nodeTypeName; } /** * Return a flag indicating whether this state is shareable, i.e. whether * there is at least one member inside its shared set. * @return <code>true</code> if this state is shareable. */ public synchronized boolean isShareable() { return sharedSet != Collections.EMPTY_SET; } /** * Add a parent to the shared set. * * @param parentId parent id to add to the shared set * @return <code>true</code> if the parent was successfully added; * <code>false</code> otherwise */ public synchronized boolean addShare(NodeId parentId) { // check first before making changes if (sharedSet.contains(parentId)) { return false; } if (!sharedSetRW) { sharedSet = new LinkedHashSet<NodeId>(sharedSet); sharedSetRW = true; } return sharedSet.add(parentId); } /** * Return a flag whether the given parent id appears in the shared set. * * @param parentId parent id * @return <code>true</code> if the parent id appears in the shared set; * <code>false</code> otherwise. */ public synchronized boolean containsShare(NodeId parentId) { return sharedSet.contains(parentId); } /** * Return the shared set as an unmodifiable collection. * * @return unmodifiable collection */ public Set<NodeId> getSharedSet() { if (sharedSet != Collections.EMPTY_SET) { return Collections.unmodifiableSet(sharedSet); } return Collections.emptySet(); } /** * Set the shared set of this state to the shared set of another state. * This state will get a deep copy of the shared set given. * * @param set shared set */ public synchronized void setSharedSet(Set<NodeId> set) { if (set != Collections.EMPTY_SET) { sharedSet = new LinkedHashSet<NodeId>(set); sharedSetRW = true; } else { sharedSet = Collections.emptySet(); sharedSetRW = false; } } /** * Remove a parent from the shared set. Returns the number of * elements in the shared set. If this number is <code>0</code>, * the shared set is empty, i.e. there are no more parent items * referencing this item and the state is free floating. * * @param parentId parent id to remove from the shared set * @return the number of elements left in the shared set */ public synchronized int removeShare(NodeId parentId) { // check first before making changes if (sharedSet.contains(parentId)) { if (!sharedSetRW) { sharedSet = new LinkedHashSet<NodeId>(sharedSet); sharedSetRW = true; } sharedSet.remove(parentId); if (parentId.equals(this.parentId)) { if (!sharedSet.isEmpty()) { this.parentId = sharedSet.iterator().next(); } else { this.parentId = null; } } } return sharedSet.size(); } //---------------------------------------------------------< diff methods > /** * Returns a set of <code>Name</code>s denoting those properties that * do not exist in the overlayed node state but have been added to * <i>this</i> node state. * * @return set of <code>Name</code>s denoting the properties that have * been added. */ public synchronized Set<Name> getAddedPropertyNames() { if (!hasOverlayedState()) { return propertyNames; } NodeState other = (NodeState) getOverlayedState(); HashSet<Name> set = new HashSet<Name>(propertyNames); set.removeAll(other.propertyNames); return set; } /** * Returns a list of child node entries that do not exist in the overlayed * node state but have been added to <i>this</i> node state. * * @return list of added child node entries */ public synchronized List<ChildNodeEntry> getAddedChildNodeEntries() { if (!hasOverlayedState()) { return childNodeEntries.list(); } NodeState other = (NodeState) getOverlayedState(); return childNodeEntries.removeAll(other.childNodeEntries); } /** * Returns a set of <code>Name</code>s denoting those properties that * exist in the overlayed node state but have been removed from * <i>this</i> node state. * * @return set of <code>Name</code>s denoting the properties that have * been removed. */ public synchronized Set<Name> getRemovedPropertyNames() { if (!hasOverlayedState()) { return Collections.emptySet(); } NodeState other = (NodeState) getOverlayedState(); HashSet<Name> set = new HashSet<Name>(other.propertyNames); set.removeAll(propertyNames); return set; } /** * Returns a list of child node entries, that exist in the overlayed node state * but have been removed from <i>this</i> node state. * * @return list of removed child node entries */ public synchronized List<ChildNodeEntry> getRemovedChildNodeEntries() { if (!hasOverlayedState()) { return Collections.emptyList(); } NodeState other = (NodeState) getOverlayedState(); return other.childNodeEntries.removeAll(childNodeEntries); } /** * Returns a list of child node entries that exist both in <i>this</i> node * state and in the overlayed node state but have been renamed. * * @return list of renamed child node entries */ public synchronized List<ChildNodeEntry> getRenamedChildNodeEntries() { if (!hasOverlayedState()) { return childNodeEntries.getRenamedEntries( ((NodeState) overlayedState).childNodeEntries); } else { return Collections.emptyList(); } } /** * Returns a list of child node entries that exist both in <i>this</i> node * state and in the overlayed node state but have been reordered. * <p> * The list may include only the minimal set of nodes that have been * reordered. That is, even though a certain number of nodes have changed * their absolute position the list may include less that this number of * nodes. * <p> * Example:<br> * Initial state: * <pre> * + node1 * + node2 * + node3 * </pre> * After reorder: * <pre> * + node2 * + node3 * + node1 * </pre> * All nodes have changed their absolute position. The returned list however * may only return that <code>node1</code> has been reordered (from the * first position to the end). * * @return list of reordered child node enties. */ public synchronized List<ChildNodeEntry> getReorderedChildNodeEntries() { if (!hasOverlayedState()) { return Collections.emptyList(); } ChildNodeEntries otherChildNodeEntries = ((NodeState) overlayedState).childNodeEntries; if (childNodeEntries.isEmpty() || otherChildNodeEntries.isEmpty()) { return Collections.emptyList(); } // build intersections of both collections, // each preserving their relative order List<ChildNodeEntry> ours = childNodeEntries.retainAll(otherChildNodeEntries); List<ChildNodeEntry> others = otherChildNodeEntries.retainAll(childNodeEntries); // do a lazy init List<ChildNodeEntry> reordered = null; // both entry lists now contain the set of nodes that have not // been removed or added, but they may have changed their position. for (int i = 0; i < ours.size();) { ChildNodeEntry entry = ours.get(i); ChildNodeEntry other = others.get(i); if (entry == other || entry.getId().equals(other.getId())) { // no reorder, move to next child entry i++; } else { // reordered entry detected if (reordered == null) { reordered = new ArrayList<ChildNodeEntry>(); } // Note that this check will not necessarily find the // minimal reorder operations required to convert the overlayed // child node entries into the current. // is there a next entry? if (i + 1 < ours.size()) { // if entry is the next in the other list then probably // the other entry at position <code>i</code> was reordered if (entry.getId().equals(others.get(i + 1).getId())) { // scan for the uuid of the other entry in our list for (int j = i; j < ours.size(); j++) { if (ours.get(j).getId().equals(other.getId())) { // found it entry = ours.get(j); break; } } } } reordered.add(entry); // remove the entry from both lists // entries > i are already cleaned for (int j = i; j < ours.size(); j++) { if (ours.get(j).getId().equals(entry.getId())) { ours.remove(j); } } for (int j = i; j < ours.size(); j++) { if (others.get(j).getId().equals(entry.getId())) { others.remove(j); } } // if a reorder has been detected index <code>i</code> is not // incremented because entries will be shifted when the // reordered entry is removed. } } if (reordered == null) { return Collections.emptyList(); } else { return reordered; } } /** * Returns a set of shares that were added. * * @return the set of shares that were added. Set of {@link NodeId}s. */ public synchronized Set<NodeId> getAddedShares() { if (!hasOverlayedState() || !isShareable()) { return Collections.emptySet(); } NodeState other = (NodeState) getOverlayedState(); HashSet<NodeId> set = new HashSet<NodeId>(sharedSet); set.removeAll(other.sharedSet); return set; } /** * Returns a set of shares that were removed. * * @return the set of shares that were removed. Set of {@link NodeId}s. */ public synchronized Set<NodeId> getRemovedShares() { if (!hasOverlayedState() || !isShareable()) { return Collections.emptySet(); } NodeState other = (NodeState) getOverlayedState(); HashSet<NodeId> set = new HashSet<NodeId>(other.sharedSet); set.removeAll(sharedSet); return set; } //--------------------------------------------------< ItemState overrides > /** * {@inheritDoc} * <p> * If the listener passed is at the same time a <code>NodeStateListener</code> * we remember it as well. */ @Override public void setContainer(ItemStateListener listener) { if (listener instanceof NodeStateListener) { if (this.listener != null) { throw new IllegalStateException("State already connected to a listener: " + this.listener); } this.listener = (NodeStateListener) listener; } super.setContainer(listener); } //-------------------------------------------------< misc. helper methods > /** * Returns an estimate of the memory size of this node state. The return * value actually highly overestimates the amount of required memory, but * changing the estimates would likely cause OOMs in many downstream * deployments that have set their cache sizes based on experience with * these erroneous size estimates. So we don't change the formula used * by this method. */ @Override public long calculateMemoryFootprint() { // Don't change this formula! See javadoc above. return 350 + mixinTypeNames.size() * 250 + childNodeEntries.size() * 300 + propertyNames.size() * 250; } /** * Notify the listeners that a child node entry has been added * @param added the entry that was added */ protected void notifyNodeAdded(ChildNodeEntry added) { if (listener != null) { listener.nodeAdded(this, added.getName(), added.getIndex(), added.getId()); } } /** * Notify the listeners that the child node entries have been replaced */ protected void notifyNodesReplaced() { if (listener != null) { listener.nodesReplaced(this); } } /** * Notify the listeners that a child node entry has been removed * @param removed the entry that was removed */ protected void notifyNodeRemoved(ChildNodeEntry removed) { if (listener != null) { listener.nodeRemoved(this, removed.getName(), removed.getIndex(), removed.getId()); } } }