/* * 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.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import javax.jcr.InvalidItemStateException; import javax.jcr.ItemNotFoundException; import javax.jcr.RepositoryException; 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.NodeState; 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.conversion.MalformedPathException; import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; import org.apache.jackrabbit.spi.commons.name.PathBuilder; import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <code>HierarchyManagerImpl</code> ... */ public class HierarchyManagerImpl implements HierarchyManager { private static Logger log = LoggerFactory.getLogger(HierarchyManagerImpl.class); /** * The parent name returned for orphaned or root nodes. * TODO: Is it proper to use an invalid Name for this. */ private static final Name EMPTY_NAME = NameFactoryImpl.getInstance().create("", ""); protected final NodeId rootNodeId; protected final ItemStateManager provider; /** * Flags describing what items to return in {@link #resolvePath(Path, int)}. */ static final int RETURN_NODE = 1; static final int RETURN_PROPERTY = 2; static final int RETURN_ANY = (RETURN_NODE | RETURN_PROPERTY); public HierarchyManagerImpl(NodeId rootNodeId, ItemStateManager provider) { this.rootNodeId = rootNodeId; this.provider = provider; } public NodeId getRootNodeId() { return rootNodeId; } //-------------------------------------------------------< implementation > /** * Internal implementation that iteratively resolves a path into an item. * * @param elements path elements * @param next index of next item in <code>elements</code> to inspect * @param id id of item at path <code>elements[0]</code>..<code>elements[next - 1]</code> * @param typesAllowed one of <code>RETURN_ANY</code>, <code>RETURN_NODE</code> * or <code>RETURN_PROPERTY</code> * @return id or <code>null</code> * @throws ItemStateException if an intermediate item state is not found * @throws MalformedPathException if building an intermediate path fails */ protected ItemId resolvePath(Path.Element[] elements, int next, ItemId id, int typesAllowed) throws ItemStateException, MalformedPathException { PathBuilder builder = new PathBuilder(); for (int i = 0; i < next; i++) { builder.addLast(elements[i]); } for (int i = next; i < elements.length; i++) { Path.Element elem = elements[i]; NodeId parentId = (NodeId) id; id = null; Name name = elem.getName(); int index = elem.getIndex(); if (index == 0) { index = 1; } int typeExpected = typesAllowed; if (i < elements.length - 1) { // intermediate items must always be nodes typeExpected = RETURN_NODE; } NodeState parentState = (NodeState) getItemState(parentId); if ((typeExpected & RETURN_NODE) != 0) { ChildNodeEntry nodeEntry = getChildNodeEntry(parentState, name, index); if (nodeEntry != null) { id = nodeEntry.getId(); } } if (id == null && (typeExpected & RETURN_PROPERTY) != 0) { if (parentState.hasPropertyName(name) && (index <= 1)) { // property id = new PropertyId(parentState.getNodeId(), name); } } if (id == null) { break; } builder.addLast(elements[i]); pathResolved(id, builder); } return id; } //---------------------------------------------------------< overridables > /** * Return an item state, given its item id. * <p> * Low-level hook provided for specialized derived classes. * * @param id item id * @return item state * @throws NoSuchItemStateException if the item does not exist * @throws ItemStateException if an error occurs * @see ZombieHierarchyManager#getItemState(ItemId) */ protected ItemState getItemState(ItemId id) throws NoSuchItemStateException, ItemStateException { return provider.getItemState(id); } /** * Determines whether an item state for a given item id exists. * <p> * Low-level hook provided for specialized derived classes. * * @param id item id * @return <code>true</code> if an item state exists, otherwise * <code>false</code> * @see ZombieHierarchyManager#hasItemState(ItemId) */ protected boolean hasItemState(ItemId id) { return provider.hasItemState(id); } /** * Returns the <code>parentUUID</code> of the given item. * <p> * Low-level hook provided for specialized derived classes. * * @param state item state * @return <code>parentUUID</code> of the given item * @see ZombieHierarchyManager#getParentId(ItemState) */ protected NodeId getParentId(ItemState state) { return state.getParentId(); } /** * Return all parents of a node. A shareable node has possibly more than * one parent. * * @param state item state * @param useOverlayed whether to use overlayed state for shareable nodes * @return set of parent <code>NodeId</code>s. If state has no parent, * array has length <code>0</code>. */ protected Set<NodeId> getParentIds(ItemState state, boolean useOverlayed) { if (state.isNode()) { // if this is a node, quickly check whether it is shareable and // whether it contains more than one parent NodeState ns = (NodeState) state; if (ns.isShareable() && useOverlayed && ns.hasOverlayedState()) { ns = (NodeState) ns.getOverlayedState(); } Set<NodeId> s = ns.getSharedSet(); if (s.size() > 1) { return s; } } NodeId parentId = getParentId(state); if (parentId != null) { LinkedHashSet<NodeId> s = new LinkedHashSet<NodeId>(); s.add(parentId); return s; } return Collections.emptySet(); } /** * Returns the <code>ChildNodeEntry</code> of <code>parent</code> with the * specified <code>uuid</code> or <code>null</code> if there's no such entry. * <p> * Low-level hook provided for specialized derived classes. * * @param parent node state * @param id id of child node entry * @return the <code>ChildNodeEntry</code> of <code>parent</code> with * the specified <code>uuid</code> or <code>null</code> if there's * no such entry. * @see ZombieHierarchyManager#getChildNodeEntry(NodeState, NodeId) */ protected ChildNodeEntry getChildNodeEntry(NodeState parent, NodeId id) { return parent.getChildNodeEntry(id); } /** * Returns the <code>ChildNodeEntry</code> of <code>parent</code> with the * specified <code>name</code> and <code>index</code> or <code>null</code> * if there's no such entry. * <p> * Low-level hook provided for specialized derived classes. * * @param parent node state * @param name name of child node entry * @param index index of child node entry * @return the <code>ChildNodeEntry</code> of <code>parent</code> with * the specified <code>name</code> and <code>index</code> or * <code>null</code> if there's no such entry. * @see ZombieHierarchyManager#getChildNodeEntry(NodeState, Name, int) */ protected ChildNodeEntry getChildNodeEntry(NodeState parent, Name name, int index) { return parent.getChildNodeEntry(name, index); } /** * Adds the path element of an item id to the path currently being built. * Recursively invoked method that may be overridden by some subclass to * either return cached responses or add response to cache. On exit, * <code>builder</code> contains the path of <code>state</code>. * * @param builder builder currently being used * @param state item to find path of * @param detector path cycle detector */ protected void buildPath( PathBuilder builder, ItemState state, CycleDetector detector) throws ItemStateException, RepositoryException { // shortcut if (state.getId().equals(rootNodeId)) { builder.addRoot(); return; } NodeId parentId = getParentId(state); if (parentId == null) { String msg = "failed to build path of " + state.getId() + ": orphaned item"; log.debug(msg); throw new ItemNotFoundException(msg); } else if (detector.checkCycle(parentId)) { throw new InvalidItemStateException( "Path cycle detected: " + parentId); } NodeState parent = (NodeState) getItemState(parentId); // recursively build path of parent buildPath(builder, parent, detector); if (state.isNode()) { NodeState nodeState = (NodeState) state; NodeId id = nodeState.getNodeId(); ChildNodeEntry entry = getChildNodeEntry(parent, id); if (entry == null) { String msg = "failed to build path of " + state.getId() + ": " + parent.getNodeId() + " has no child entry for " + id; log.debug(msg); throw new ItemNotFoundException(msg); } // add to path if (entry.getIndex() == 1) { builder.addLast(entry.getName()); } else { builder.addLast(entry.getName(), entry.getIndex()); } } else { PropertyState propState = (PropertyState) state; Name name = propState.getName(); // add to path builder.addLast(name); } } /** * Internal implementation of {@link #resolvePath(Path)} that will either * resolve to a node or a property. Should be overridden by a subclass * that can resolve an intermediate path into an <code>ItemId</code>. This * subclass can then invoke {@link #resolvePath(org.apache.jackrabbit.spi.Path.Element[], int, ItemId, int)} * with a value of <code>next</code> greater than <code>1</code>. * * @param path path to resolve * @param typesAllowed one of <code>RETURN_ANY</code>, <code>RETURN_NODE</code> * or <code>RETURN_PROPERTY</code> * @return id or <code>null</code> * @throws RepositoryException if an error occurs */ protected ItemId resolvePath(Path path, int typesAllowed) throws RepositoryException { Path.Element[] elements = path.getElements(); ItemId id = rootNodeId; try { return resolvePath(elements, 1, id, typesAllowed); } catch (ItemStateException e) { String msg = "failed to retrieve state of intermediary node"; log.debug(msg); throw new RepositoryException(msg, e); } } /** * Called by {@link #resolvePath(org.apache.jackrabbit.spi.Path.Element[], int, ItemId, int)}. * May be overridden by some subclass to process/cache intermediate state. * * @param id id of resolved item * @param builder path builder containing path resolved * @throws MalformedPathException if the path contained in <code>builder</code> * is malformed */ protected void pathResolved(ItemId id, PathBuilder builder) throws MalformedPathException { // do nothing } //-----------------------------------------------------< HierarchyManager > /** * {@inheritDoc} */ public final ItemId resolvePath(Path path) throws RepositoryException { // shortcut if (path.denotesRoot()) { return rootNodeId; } if (!path.isCanonical()) { String msg = "path is not canonical"; log.debug(msg); throw new RepositoryException(msg); } return resolvePath(path, RETURN_ANY); } /** * {@inheritDoc} */ public NodeId resolveNodePath(Path path) throws RepositoryException { return (NodeId) resolvePath(path, RETURN_NODE); } /** * {@inheritDoc} */ public PropertyId resolvePropertyPath(Path path) throws RepositoryException { return (PropertyId) resolvePath(path, RETURN_PROPERTY); } /** * {@inheritDoc} */ public Path getPath(ItemId id) throws ItemNotFoundException, RepositoryException { // shortcut if (id.equals(rootNodeId)) { return PathFactoryImpl.getInstance().getRootPath(); } PathBuilder builder = new PathBuilder(); try { buildPath(builder, getItemState(id), new CycleDetector()); return builder.getPath(); } catch (NoSuchItemStateException nsise) { String msg = "failed to build path of " + id; log.debug(msg); throw new ItemNotFoundException(msg, nsise); } catch (ItemStateException ise) { String msg = "failed to build path of " + id; log.debug(msg); throw new RepositoryException(msg, ise); } catch (MalformedPathException mpe) { String msg = "failed to build path of " + id; log.debug(msg); throw new RepositoryException(msg, mpe); } } /** * {@inheritDoc} */ public Name getName(ItemId itemId) throws ItemNotFoundException, RepositoryException { if (itemId.denotesNode()) { NodeId nodeId = (NodeId) itemId; try { NodeState nodeState = (NodeState) getItemState(nodeId); NodeId parentId = getParentId(nodeState); if (parentId == null) { // this is the root or an orphaned node // FIXME return EMPTY_NAME; } return getName(nodeId, parentId); } catch (NoSuchItemStateException nsis) { String msg = "failed to resolve name of " + nodeId; log.debug(msg); throw new ItemNotFoundException(nodeId.toString()); } catch (ItemStateException ise) { String msg = "failed to resolve name of " + nodeId; log.debug(msg); throw new RepositoryException(msg, ise); } } else { return ((PropertyId) itemId).getName(); } } /** * {@inheritDoc} */ public Name getName(NodeId id, NodeId parentId) throws ItemNotFoundException, RepositoryException { NodeState parentState; try { parentState = (NodeState) getItemState(parentId); } catch (NoSuchItemStateException nsis) { String msg = "failed to resolve name of " + id; log.debug(msg); throw new ItemNotFoundException(id.toString()); } catch (ItemStateException ise) { String msg = "failed to resolve name of " + id; log.debug(msg); throw new RepositoryException(msg, ise); } ChildNodeEntry entry = getChildNodeEntry(parentState, id); if (entry == null) { String msg = "failed to resolve name of " + id; log.debug(msg); throw new ItemNotFoundException(msg); } return entry.getName(); } /** * {@inheritDoc} */ public int getDepth(ItemId id) throws ItemNotFoundException, RepositoryException { // shortcut if (id.equals(rootNodeId)) { return 0; } try { ItemState state = getItemState(id); NodeId parentId = getParentId(state); int depth = 0; while (parentId != null) { depth++; state = getItemState(parentId); parentId = getParentId(state); } return depth; } catch (NoSuchItemStateException nsise) { String msg = "failed to determine depth of " + id; log.debug(msg); throw new ItemNotFoundException(msg, nsise); } catch (ItemStateException ise) { String msg = "failed to determine depth of " + id; log.debug(msg); throw new RepositoryException(msg, ise); } } /** * {@inheritDoc} */ public int getRelativeDepth(NodeId ancestorId, ItemId descendantId) throws ItemNotFoundException, RepositoryException { if (ancestorId.equals(descendantId)) { return 0; } int depth = 1; try { ItemState state = getItemState(descendantId); NodeId parentId = getParentId(state); while (parentId != null) { if (parentId.equals(ancestorId)) { return depth; } depth++; state = getItemState(parentId); parentId = getParentId(state); } // not an ancestor return -1; } catch (NoSuchItemStateException nsise) { String msg = "failed to determine depth of " + descendantId + " relative to " + ancestorId; log.debug(msg); throw new ItemNotFoundException(msg, nsise); } catch (ItemStateException ise) { String msg = "failed to determine depth of " + descendantId + " relative to " + ancestorId; log.debug(msg); throw new RepositoryException(msg, ise); } } /** * {@inheritDoc} */ public boolean isAncestor(NodeId nodeId, ItemId itemId) throws ItemNotFoundException, RepositoryException { if (nodeId.equals(itemId)) { // can't be ancestor of self return false; } try { ItemState state = getItemState(itemId); NodeId parentId = getParentId(state); while (parentId != null) { if (parentId.equals(nodeId)) { return true; } state = getItemState(parentId); parentId = getParentId(state); } // not an ancestor return false; } catch (NoSuchItemStateException nsise) { String msg = "failed to determine degree of relationship of " + nodeId + " and " + itemId; log.debug(msg); throw new ItemNotFoundException(msg, nsise); } catch (ItemStateException ise) { String msg = "failed to determine degree of relationship of " + nodeId + " and " + itemId; log.debug(msg); throw new RepositoryException(msg, ise); } } /** * {@inheritDoc} */ public boolean isShareAncestor(NodeId ancestor, NodeId descendant) throws ItemNotFoundException, RepositoryException { if (ancestor.equals(descendant)) { // can't be ancestor of self return false; } try { ItemState state = getItemState(descendant); Set<NodeId> parentIds = getParentIds(state, false); while (parentIds.size() > 0) { if (parentIds.contains(ancestor)) { return true; } Set<NodeId> grandparentIds = new LinkedHashSet<NodeId>(); for (NodeId parentId : parentIds) { grandparentIds.addAll(getParentIds(getItemState(parentId), false)); } parentIds = grandparentIds; } // not an ancestor return false; } catch (NoSuchItemStateException nsise) { String msg = "failed to determine degree of relationship of " + ancestor + " and " + descendant; log.debug(msg); throw new ItemNotFoundException(msg, nsise); } catch (ItemStateException ise) { String msg = "failed to determine degree of relationship of " + ancestor + " and " + descendant; log.debug(msg); throw new RepositoryException(msg, ise); } } /** * {@inheritDoc} */ public int getShareRelativeDepth(NodeId ancestor, ItemId descendant) throws ItemNotFoundException, RepositoryException { if (ancestor.equals(descendant)) { return 0; } int depth = 1; try { ItemState state = getItemState(descendant); Set<NodeId> parentIds = getParentIds(state, true); while (parentIds.size() > 0) { if (parentIds.contains(ancestor)) { return depth; } depth++; Set<NodeId> grandparentIds = new LinkedHashSet<NodeId>(); for (NodeId parentId : parentIds) { state = getItemState(parentId); grandparentIds.addAll(getParentIds(state, true)); } parentIds = grandparentIds; } // not an ancestor return -1; } catch (NoSuchItemStateException nsise) { String msg = "failed to determine degree of relationship of " + ancestor + " and " + descendant; log.debug(msg); throw new ItemNotFoundException(msg, nsise); } catch (ItemStateException ise) { String msg = "failed to determine degree of relationship of " + ancestor + " and " + descendant; log.debug(msg); throw new RepositoryException(msg, ise); } } /** * Utility class used to detect path cycles with as little overhead * as possible. The {@link #checkCycle(ItemId)} method is called for * each path element as the * {@link HierarchyManagerImpl#buildPath(PathBuilder, ItemState, CycleDetector)} * method walks up the hierarchy. At first, during the first fifteen * path elements, the detector does nothing in order to avoid * introducing any unnecessary overhead to normal paths that seldom * are deeper than that. After that initial threshold all item * identifiers along the path are tracked, and a cycle is reported * if an identifier is encountered that already occurred along the * same path. */ protected static class CycleDetector { private int count = 0; private Set<ItemId> ids; boolean checkCycle(ItemId id) throws InvalidItemStateException { if (count++ >= 15) { if (ids == null) { ids = new HashSet<ItemId>(); } else { return !ids.add(id); } } return false; } } }