/* * 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; import java.util.ArrayList; import java.util.HashMap; import junit.framework.TestCase; import org.apache.jackrabbit.core.id.ItemId; import org.apache.jackrabbit.core.id.NodeId; import org.apache.jackrabbit.core.id.PropertyId; import org.apache.jackrabbit.core.state.ChildNodeEntry; import org.apache.jackrabbit.core.state.ItemState; import org.apache.jackrabbit.core.state.ItemStateException; import org.apache.jackrabbit.core.state.ItemStateManager; import org.apache.jackrabbit.core.state.NoSuchItemStateException; import org.apache.jackrabbit.core.state.NodeReferences; import org.apache.jackrabbit.core.state.NodeState; import org.apache.jackrabbit.core.state.NodeStateListener; import org.apache.jackrabbit.core.state.PropertyState; import org.apache.jackrabbit.spi.Name; import org.apache.jackrabbit.spi.Path; import org.apache.jackrabbit.spi.commons.name.NameConstants; import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; public class CachingHierarchyManagerTest extends TestCase { volatile Exception exception; volatile boolean stop; CachingHierarchyManager cache; /** * Test multi-threaded read and write access to the cache. */ public void testResolveNodePath() throws Exception { StaticItemStateManager ism = new StaticItemStateManager(); cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); ism.setContainer(cache); ism.addNode(ism.getRoot(), "a"); ism.addNode(ism.getRoot(), "b"); final Path aPath = toPath("/a"); final Path bPath = toPath("/b"); for (int i = 0; i < 3; i++) { new Thread(new Runnable() { public void run() { while (!stop) { try { cache.resolveNodePath(aPath); cache.resolveNodePath(bPath); } catch (Exception e) { exception = e; } } } }).start(); } Thread.sleep(1000); stop = true; if (exception != null) { throw exception; } } //-------------------------------------------------------------- basic tests /** * Verify that resolving node and property paths will only return valid hits. */ public void testResolveNodePropertyPath() throws Exception { StaticItemStateManager ism = new StaticItemStateManager(); cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); ism.setContainer(cache); NodeState a = ism.addNode(ism.getRoot(), "a"); NodeState b = ism.addNode(a, "b"); Path path = toPath("/a/b"); // /a/b points to node only assertIsNodeId(cache.resolvePath(path)); assertIsNodeId(cache.resolveNodePath(path)); assertNull(cache.resolvePropertyPath(path)); ism.addProperty(a, "b"); // /a/b points to node and property assertNotNull(cache.resolvePath(path)); assertIsNodeId(cache.resolveNodePath(path)); assertIsPropertyId(cache.resolvePropertyPath(path)); ism.removeNode(b); // /a/b points to property only assertIsPropertyId(cache.resolvePath(path)); assertNull(cache.resolveNodePath(path)); assertIsPropertyId(cache.resolvePropertyPath(path)); } /** * Assert that an item id is a property id. * @param id item id */ private static void assertIsPropertyId(ItemId id) { assertTrue(id instanceof PropertyId); } /** * Assert that an item id is a node id. * @param id item id */ private static void assertIsNodeId(ItemId id) { assertTrue(id instanceof NodeId); } //------------------------------------------------------------ caching tests /** * Add a SNS (same name sibling) and verify that cached paths are * adapted accordingly. */ public void testAddSNS() throws Exception { StaticItemStateManager ism = new StaticItemStateManager(); cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); ism.setContainer(cache); NodeState a = ism.addNode(ism.getRoot(), "a"); NodeState b1 = ism.addNode(a, "b"); Path path = cache.getPath(b1.getNodeId()); assertEquals(toPath("/a/b"), path); NodeState b2 = ism.addNode(a, "b"); ism.orderBefore(b2, b1); assertTrue(cache.isCached(b1.getNodeId(), null)); path = cache.getPath(b1.getNodeId()); assertEquals(toPath("/a/b[2]"), path); } /** * Clone a node, cache its path and remove it afterwards. Should remove * the cached path as well. */ public void testCloneAndRemove() throws Exception { StaticItemStateManager ism = new StaticItemStateManager(); cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); ism.setContainer(cache); NodeState a1 = ism.addNode(ism.getRoot(), "a1"); NodeState a2 = ism.addNode(ism.getRoot(), "a2"); NodeState b1 = ism.addNode(a1, "b1"); b1.addShare(b1.getParentId()); ism.cloneNode(b1, a2, "b2"); Path path1 = toPath("/a1/b1"); Path path2 = toPath("/a2/b2"); assertNotNull(cache.resolvePath(path1)); assertTrue(cache.isCached(b1.getNodeId(), path1)); ism.removeNode(b1); assertNull(cache.resolvePath(path1)); assertNotNull(cache.resolvePath(path2)); } /** * Clone a node, create a child and resolve its path in all valid * combinations. Then, move the child away. Should remove the cached * paths as well. */ public void testCloneAndAddChildAndMove() throws Exception { StaticItemStateManager ism = new StaticItemStateManager(); cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); ism.setContainer(cache); NodeState a1 = ism.addNode(ism.getRoot(), "a1"); NodeState a2 = ism.addNode(ism.getRoot(), "a2"); NodeState b1 = ism.addNode(a1, "b1"); b1.addShare(b1.getParentId()); ism.cloneNode(b1, a2, "b2"); NodeState c = ism.addNode(b1, "c"); Path path1 = toPath("/a1/b1/c"); Path path2 = toPath("/a2/b2/c"); assertNotNull(cache.resolvePath(path1)); assertTrue(cache.isCached(c.getNodeId(), path1)); assertNotNull(cache.resolvePath(path2)); assertTrue(cache.isCached(c.getNodeId(), path2)); ism.moveNode(c, a1, "c"); assertNull(cache.resolvePath(path1)); assertNull(cache.resolvePath(path2)); } /** * Move a node and verify that cached path is adapted. */ public void testMove() throws Exception { StaticItemStateManager ism = new StaticItemStateManager(); cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); ism.setContainer(cache); NodeState a1 = ism.addNode(ism.getRoot(), "a1"); NodeState a2 = ism.addNode(ism.getRoot(), "a2"); NodeState b1 = ism.addNode(a1, "b1"); Path path = cache.getPath(b1.getNodeId()); assertEquals(toPath("/a1/b1"), path); ism.moveNode(b1, a2, "b2"); path = cache.getPath(b1.getNodeId()); assertEquals(toPath("/a2/b2"), path); } /** * Reorder child nodes and verify that cached paths are still adequate. */ public void testOrderBefore() throws Exception { StaticItemStateManager ism = new StaticItemStateManager(); cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); ism.setContainer(cache); NodeState a = ism.addNode(ism.getRoot(), "a"); NodeState b1 = ism.addNode(a, "b"); NodeState b2 = ism.addNode(a, "b"); NodeState b3 = ism.addNode(a, "b"); Path path = cache.getPath(b1.getNodeId()); assertEquals(toPath("/a/b"), path); ism.orderBefore(b2, b1); ism.orderBefore(b1, b3); path = cache.getPath(b1.getNodeId()); assertEquals(toPath("/a/b[2]"), path); } /** * Remove a node and verify that cached path is gone. */ public void testRemove() throws Exception { StaticItemStateManager ism = new StaticItemStateManager(); cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); ism.setContainer(cache); NodeState a = ism.addNode(ism.getRoot(), "a"); NodeState b = ism.addNode(a, "b"); NodeState c = ism.addNode(b, "c"); cache.getPath(c.getNodeId()); assertTrue(cache.isCached((NodeId) c.getId(), null)); ism.removeNode(b); assertFalse(cache.isCached((NodeId) c.getId(), null)); } /** * Remove a SNS (same name sibling) and verify that cached paths are * adapted accordingly. The removed SNS's path is not cached. */ public void testRemoveSNS() throws Exception { StaticItemStateManager ism = new StaticItemStateManager(); cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); ism.setContainer(cache); NodeState a = ism.addNode(ism.getRoot(), "a"); NodeState b1 = ism.addNode(a, "b"); NodeState b2 = ism.addNode(a, "b"); Path path = cache.getPath(b2.getNodeId()); assertEquals(toPath("/a/b[2]"), path); ism.removeNode(b1); path = cache.getPath(b2.getNodeId()); assertEquals(toPath("/a/b"), path); } /** * Remove a SNS (same name sibling) and verify that cached paths are * adapted accordingly. The removed SNS's path is cached. */ public void testRemoveSNSWithCachedPath() throws Exception { StaticItemStateManager ism = new StaticItemStateManager(); cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); ism.setContainer(cache); NodeState a = ism.addNode(ism.getRoot(), "a"); NodeState b1 = ism.addNode(a, "b"); NodeState b2 = ism.addNode(a, "b"); cache.getPath(b1.getNodeId()); Path path = cache.getPath(b2.getNodeId()); assertEquals(toPath("/a/b[2]"), path); ism.removeNode(b1); path = cache.getPath(b2.getNodeId()); assertEquals(toPath("/a/b"), path); } /** * Rename a node and verify that cached path is adapted. */ public void testRename() throws Exception { StaticItemStateManager ism = new StaticItemStateManager(); cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); ism.setContainer(cache); NodeState a1 = ism.addNode(ism.getRoot(), "a1"); NodeState b1 = ism.addNode(a1, "b"); NodeState b2 = ism.addNode(a1, "b"); Path path = cache.getPath(b1.getNodeId()); assertEquals(toPath("/a1/b"), path); path = cache.getPath(b2.getNodeId()); assertEquals(toPath("/a1/b[2]"), path); ism.renameNode(b1, "b1"); assertTrue(cache.isCached(b1.getNodeId(), null)); assertTrue(cache.isCached(b2.getNodeId(), null)); path = cache.getPath(b1.getNodeId()); assertEquals(toPath("/a1/b1"), path); } /** * Static item state manager, that can be filled programmatically and that * keeps a hash map of item states. <code>ItemId</code>s generated by * this state manager start with <code>0</code>. */ static class StaticItemStateManager implements ItemStateManager { /** Root node id */ private final NodeId rootNodeId; /** Map of item states */ private final HashMap states = new HashMap(); /** UUID generator base */ private long lsbGenerator; /** Root node state */ private NodeState root; /** Node state listener to register in item states */ private NodeStateListener listener; /** * Create a new instance of this class. */ public StaticItemStateManager() { rootNodeId = nextNodeId(); } /** * Return the root node id. * * @return root node id */ public NodeId getRootNodeId() { return rootNodeId; } /** * Return the root node. * * @return root node */ public NodeState getRoot() { if (root == null) { root = new NodeState(rootNodeId, NameConstants.JCR_ROOT, null, NodeState.STATUS_EXISTING, false); if (listener != null) { root.setContainer(listener); } } return root; } /** * Set the listener that should be registered in new item states. * * @param listener listener */ public void setContainer(NodeStateListener listener) { this.listener = listener; } /** * Add a node. * * @param parent parent node * @param name node name * @return new node */ public NodeState addNode(NodeState parent, String name) { NodeId id = nextNodeId(); NodeState child = new NodeState(id, NameConstants.NT_UNSTRUCTURED, parent.getNodeId(), NodeState.STATUS_EXISTING, false); if (listener != null) { child.setContainer(listener); } states.put(id, child); parent.addChildNodeEntry(toName(name), child.getNodeId()); return child; } /** * Add a property. * * @param parent parent node * @param name property name * @return new property */ public PropertyState addProperty(NodeState parent, String name) { PropertyId id = new PropertyId(parent.getNodeId(), toName(name)); PropertyState child = new PropertyState(id, PropertyState.STATUS_EXISTING, false); if (listener != null) { child.setContainer(listener); } states.put(id, child); parent.addPropertyName(toName(name)); return child; } /** * Clone a node. * * @param src node to clone * @param parent destination parent node * @param name node name */ public void cloneNode(NodeState src, NodeState parent, String name) { src.addShare(parent.getNodeId()); parent.addChildNodeEntry(toName(name), src.getNodeId()); } /** * Move a node. * * @param child node to move * @param newParent destination parent node * @param name node name * @throws ItemStateException if getting the old parent node fails */ public void moveNode(NodeState child, NodeState newParent, String name) throws ItemStateException { NodeState oldParent = (NodeState) getItemState(child.getParentId()); ChildNodeEntry cne = oldParent.getChildNodeEntry(child.getNodeId()); if (cne == null) { throw new ItemStateException(child.getNodeId().toString()); } oldParent.removeChildNodeEntry(cne.getName(), cne.getIndex()); child.setParentId(newParent.getNodeId()); newParent.addChildNodeEntry(toName(name), child.getNodeId()); } /** * Order a child node before another node. * * @param src src node * @param dest destination node, may be <code>null</code> * @throws ItemStateException if getting the parent node fails */ public void orderBefore(NodeState src, NodeState dest) throws ItemStateException { NodeState parent = (NodeState) getItemState(src.getParentId()); ArrayList list = new ArrayList(parent.getChildNodeEntries()); int srcIndex = -1, destIndex = -1; for (int i = 0; i < list.size(); i++) { ChildNodeEntry cne = (ChildNodeEntry) list.get(i); if (cne.getId().equals(src.getId())) { srcIndex = i; } else if (dest != null && cne.getId().equals(dest.getId())) { destIndex = i; } } if (destIndex == -1) { list.add(list.remove(srcIndex)); } else { if (srcIndex < destIndex) { list.add(destIndex, list.get(srcIndex)); list.remove(srcIndex); } else { list.add(destIndex, list.remove(srcIndex)); } } parent.setChildNodeEntries(list); } /** * Remove a node. * * @param child node to remove * @throws ItemStateException if getting the parent node fails */ public void removeNode(NodeState child) throws ItemStateException { NodeState parent = (NodeState) getItemState(child.getParentId()); if (child.isShareable()) { if (child.removeShare(parent.getNodeId()) == 0) { child.setParentId(null); } } parent.removeChildNodeEntry(child.getNodeId()); } /** * Rename a node. * * @param child node to rename * @param newName new name * @throws ItemStateException if getting the parent node fails */ public void renameNode(NodeState child, String newName) throws ItemStateException { NodeState parent = (NodeState) getItemState(child.getParentId()); ChildNodeEntry cne = parent.getChildNodeEntry(child.getNodeId()); if (cne == null) { throw new ItemStateException(child.getNodeId().toString()); } parent.renameChildNodeEntry(cne.getName(), cne.getIndex(), toName(newName)); } /** * Return the next available node id. Simply increments the last UUID * returned by <code>1</code>. * * @return next UUID */ private NodeId nextNodeId() { return new NodeId(0, lsbGenerator++); } //----------------------------------------------------- ItemStateManager /** * {@inheritDoc} */ public ItemState getItemState(ItemId id) throws NoSuchItemStateException, ItemStateException { if (id.equals(root.getId())) { return root; } ItemState item = (ItemState) states.get(id); if (item == null) { throw new NoSuchItemStateException(id.toString()); } return item; } /** * {@inheritDoc} */ public boolean hasItemState(ItemId id) { if (id.equals(root.getId())) { return true; } return states.containsKey(id); } /** * {@inheritDoc} */ public NodeReferences getNodeReferences(NodeId id) throws NoSuchItemStateException, ItemStateException { return null; } /** * {@inheritDoc} */ public boolean hasNodeReferences(NodeId id) { return false; } } /** * Utility method, converting a string into a path. * * @param s string * @return path */ private static Path toPath(String s) { StringBuffer buf = new StringBuffer("{}"); int start = 1, length = s.length(); while (start < length) { int end = s.indexOf('/', start); if (end == -1) { end = length; } String name = s.substring(start, end); if (name.length() > 0) { buf.append("\t{}"); buf.append(name); } start = end + 1; } return PathFactoryImpl.getInstance().create(buf.toString()); } /** * Utility method, converting a string into a name. * * @param s string * @return name */ private static Name toName(String s) { return NameFactoryImpl.getInstance().create("", s); } }