/* * 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.Iterator; import java.util.Map; import javax.jcr.AccessDeniedException; import javax.jcr.InvalidItemStateException; import javax.jcr.ItemNotFoundException; import javax.jcr.NamespaceException; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.PropertyIterator; import javax.jcr.RepositoryException; import javax.jcr.nodetype.ConstraintViolationException; import org.apache.commons.collections.map.ReferenceMap; 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.nodetype.NodeTypeRegistry; import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException; import org.apache.jackrabbit.core.security.authorization.Permission; 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.ItemStateListener; 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.core.state.SessionItemStateManager; import org.apache.jackrabbit.core.version.VersionHistoryImpl; import org.apache.jackrabbit.core.version.VersionImpl; import org.apache.jackrabbit.core.security.AccessManager; import org.apache.jackrabbit.core.session.SessionContext; import org.apache.jackrabbit.spi.Name; import org.apache.jackrabbit.spi.Path; import org.apache.jackrabbit.spi.QPropertyDefinition; import org.apache.jackrabbit.spi.QNodeDefinition; import org.apache.jackrabbit.spi.commons.name.NameConstants; import org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl; import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * There's one <code>ItemManager</code> instance per <code>Session</code> * instance. It is the factory for <code>Node</code> and <code>Property</code> * instances. * <p> * The <code>ItemManager</code>'s responsibilities are: * <ul> * <li>providing access to <code>Item</code> instances by <code>ItemId</code> * whereas <code>Node</code> and <code>Item</code> are only providing relative access. * <li>returning the instance of an existing <code>Node</code> or <code>Property</code>, * given its absolute path. * <li>creating the per-session instance of a <code>Node</code> * or <code>Property</code> that doesn't exist yet and needs to be created first. * <li>guaranteeing that there aren't multiple instances representing the same * <code>Node</code> or <code>Property</code> associated with the same * <code>Session</code> instance. * <li>maintaining a cache of the item instances it created. * <li>respecting access rights of associated <code>Session</code> in all methods. * </ul> * <p> * If the parent <code>Session</code> is an <code>XASession</code>, there is * one <code>ItemManager</code> instance per started global transaction. */ public class ItemManager implements ItemStateListener { private static Logger log = LoggerFactory.getLogger(ItemManager.class); private final org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl rootNodeDef; /** * Component context of the associated session. */ protected final SessionContext sessionContext; protected final SessionImpl session; private final SessionItemStateManager sism; private final HierarchyManager hierMgr; /** * A cache for item instances created by this <code>ItemManager</code> */ private final Map<ItemId, ItemData> itemCache; /** * Shareable node cache. */ private final ShareableNodesCache shareableNodesCache; /** * Creates a new per-session instance <code>ItemManager</code> instance. * * @param sessionContext component context of the associated session */ @SuppressWarnings("unchecked") protected ItemManager(SessionContext sessionContext) { this.sism = sessionContext.getItemStateManager(); this.hierMgr = sessionContext.getHierarchyManager(); this.sessionContext = sessionContext; this.session = sessionContext.getSessionImpl(); this.rootNodeDef = sessionContext.getNodeTypeManager().getRootNodeDefinition(); // setup item cache with weak references to items itemCache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.WEAK); // setup shareable nodes cache shareableNodesCache = new ShareableNodesCache(); } /** * Checks that this session is alive. * * @throws RepositoryException if the session has been closed */ private void sanityCheck() throws RepositoryException { sessionContext.getSessionState().checkAlive(); } /** * Disposes this <code>ItemManager</code> and frees resources. */ void dispose() { synchronized (itemCache) { itemCache.clear(); } shareableNodesCache.clear(); } NodeDefinitionImpl getDefinition(NodeState state) throws RepositoryException { if (state.getId().equals(sessionContext.getRootNodeId())) { // special handling required for root node return rootNodeDef; } NodeId parentId = state.getParentId(); if (parentId == null) { // removed state has parentId set to null // get from overlayed state ItemState overlaid = state.getOverlayedState(); if (overlaid != null) { parentId = overlaid.getParentId(); } else { throw new InvalidItemStateException( "Could not find parent of node " + state.getNodeId()); } } NodeState parentState = null; try { // access the parent state circumventing permission check, since // read permission on the parent isn't required in order to retrieve // a node's definition. see also JCR-2418 ItemData parentData = getItemData(parentId, null, false); parentState = (NodeState) parentData.getState(); if (state.getParentId() == null) { // indicates state has been removed, must use // overlayed state of parent, otherwise child node entry // cannot be found. unless the parentState is new, which // means it was recreated in place of a removed node // that used to be the actual parent if (parentState.getStatus() == ItemState.STATUS_NEW) { // force getting parent from attic parentState = null; } else { parentState = (NodeState) parentState.getOverlayedState(); } } } catch (ItemNotFoundException e) { // parent probably removed, get it from attic. see below } if (parentState == null) { try { // use overlayed state if available parentState = (NodeState) sism.getAttic().getItemState( parentId).getOverlayedState(); } catch (ItemStateException ex) { throw new RepositoryException(ex); } } // get child node entry ChildNodeEntry cne = parentState.getChildNodeEntry(state.getNodeId()); if (cne == null) { throw new InvalidItemStateException( "Could not find child " + state.getNodeId() + " of node " + parentState.getNodeId()); } NodeTypeRegistry ntReg = sessionContext.getNodeTypeRegistry(); try { EffectiveNodeType ent = ntReg.getEffectiveNodeType( parentState.getNodeTypeName(), parentState.getMixinTypeNames()); QNodeDefinition def; try { def = ent.getApplicableChildNodeDef( cne.getName(), state.getNodeTypeName(), ntReg); } catch (ConstraintViolationException e) { // fallback to child node definition of a nt:unstructured ent = ntReg.getEffectiveNodeType(NameConstants.NT_UNSTRUCTURED); def = ent.getApplicableChildNodeDef( cne.getName(), state.getNodeTypeName(), ntReg); log.warn("Fallback to nt:unstructured due to unknown child " + "node definition for type '" + state.getNodeTypeName() + "'"); } return sessionContext.getNodeTypeManager().getNodeDefinition(def); } catch (NodeTypeConflictException e) { throw new RepositoryException(e); } } PropertyDefinitionImpl getDefinition(PropertyState state) throws RepositoryException { // this is a bit ugly // there might be cases where otherwise protected items turn into // non-protected items because a mixin has been removed from the parent // node state. // see also: JCR-2408 if (state.getStatus() == ItemState.STATUS_EXISTING_REMOVED && state.getName().equals(NameConstants.JCR_UUID)) { NodeTypeRegistry ntReg = sessionContext.getNodeTypeRegistry(); QPropertyDefinition def = ntReg.getEffectiveNodeType( NameConstants.MIX_REFERENCEABLE).getApplicablePropertyDef( state.getName(), state.getType()); return sessionContext.getNodeTypeManager().getPropertyDefinition(def); } try { // retrieve parent in 2 steps in order to avoid the check for // read permissions on the parent which isn't required in order // to read the property's definition. see also JCR-2418. ItemData parentData = getItemData(state.getParentId(), null, false); NodeImpl parent = (NodeImpl) createItemInstance(parentData); return parent.getApplicablePropertyDefinition( state.getName(), state.getType(), state.isMultiValued(), true); } catch (ItemNotFoundException e) { // parent probably removed, get it from attic } try { NodeState parent = (NodeState) sism.getAttic().getItemState( state.getParentId()).getOverlayedState(); NodeTypeRegistry ntReg = sessionContext.getNodeTypeRegistry(); EffectiveNodeType ent = ntReg.getEffectiveNodeType( parent.getNodeTypeName(), parent.getMixinTypeNames()); QPropertyDefinition def; try { def = ent.getApplicablePropertyDef( state.getName(), state.getType(), state.isMultiValued()); } catch (ConstraintViolationException e) { ent = ntReg.getEffectiveNodeType(NameConstants.NT_UNSTRUCTURED); def = ent.getApplicablePropertyDef(state.getName(), state.getType(), state.isMultiValued()); log.warn("Fallback to nt:unstructured due to unknown property " + "definition for '" + state.getName() + "'"); } return sessionContext.getNodeTypeManager().getPropertyDefinition(def); } catch (ItemStateException e) { throw new RepositoryException(e); } catch (NodeTypeConflictException e) { throw new RepositoryException(e); } } /** * Common implementation for all variants of item/node/propertyExists * with both itemId or path param. * * @param itemId The id of the item to test. * @param path Path of the item to check if known or <code>null</code>. In * the latter case the test for access permission is executed using the * itemId. * @return true if the item with the given <code>itemId</code> exists AND * can be read by this session. */ private boolean itemExists(ItemId itemId, Path path) { try { sanityCheck(); // shortcut: check if state exists for the given item if (!sism.hasItemState(itemId)) { return false; } getItemData(itemId, path, true); return true; } catch (RepositoryException re) { return false; } } /** * Common implementation for all variants of getItem/getNode/getProperty * with both itemId or path parameter. * * @param itemId * @param path Path of the item to retrieve or <code>null</code>. In * the latter case the test for access permission is executed using the * itemId. * @param permissionCheck * @return The item identified by the given <code>itemId</code>. * @throws ItemNotFoundException * @throws AccessDeniedException * @throws RepositoryException */ private ItemImpl getItem(ItemId itemId, Path path, boolean permissionCheck) throws ItemNotFoundException, AccessDeniedException, RepositoryException { sanityCheck(); ItemData data = getItemData(itemId, path, permissionCheck); return createItemInstance(data); } /** * Retrieves the data of the item with given <code>id</code>. If the * specified item doesn't exist an <code>ItemNotFoundException</code> will * be thrown. * If the item exists but the current session is not granted read access an * <code>AccessDeniedException</code> will be thrown. * * @param itemId id of item to be retrieved * @return state state of said item * @throws ItemNotFoundException if no item with given <code>id</code> exists * @throws AccessDeniedException if the current session is not allowed to * read the said item * @throws RepositoryException if another error occurs */ private ItemData getItemData(ItemId itemId) throws ItemNotFoundException, AccessDeniedException, RepositoryException { return getItemData(itemId, null, true); } /** * Retrieves the data of the item with given <code>id</code>. If the * specified item doesn't exist an <code>ItemNotFoundException</code> will * be thrown. * If <code>permissionCheck</code> is <code>true</code> and the item exists * but the current session is not granted read access an * <code>AccessDeniedException</code> will be thrown. * * @param itemId id of item to be retrieved * @param path The path of the item to retrieve the data for or * <code>null</code>. In the latter case the id (instead of the path) is * used to test if READ permission is granted. * @param permissionCheck * @return the ItemData for the item identified by the given itemId. * @throws ItemNotFoundException if no item with given <code>id</code> exists * @throws AccessDeniedException if the current session is not allowed to * read the said item * @throws RepositoryException if another error occurs */ ItemData getItemData(ItemId itemId, Path path, boolean permissionCheck) throws ItemNotFoundException, AccessDeniedException, RepositoryException { ItemData data = retrieveItem(itemId); if (data == null) { // not yet in cache, need to create instance: // - retrieve item state // - create instance of item data // NOTE: permission check & caching within createItemData ItemState state; try { state = sism.getItemState(itemId); } catch (NoSuchItemStateException nsise) { throw new ItemNotFoundException(itemId.toString(), nsise); } catch (ItemStateException ise) { String msg = "failed to retrieve item state of item " + itemId; log.error(msg, ise); throw new RepositoryException(msg, ise); } // create item data including: perm check and caching. data = createItemData(state, path, permissionCheck); } else { // already cached: if 'permissionCheck' is true, make sure read // permission is granted. if (permissionCheck && !canRead(data, path)) { // item exists but read-perm has been revoked in the mean time. // -> remove from cache evictItems(itemId); throw new AccessDeniedException("cannot read item " + data.getId()); } } return data; } /** * @param data * @param path Path to be used for the permission check or <code>null</code> * in which case the itemId present with the specified <code>data</code> is used. * @return true if the item with the given <code>data</code> can be read; * <code>false</code> otherwise. * @throws AccessDeniedException * @throws RepositoryException */ private boolean canRead(ItemData data, Path path) throws AccessDeniedException, RepositoryException { // JCR-1601: cached item may just have been invalidated ItemState state = data.getState(); if (state == null) { throw new InvalidItemStateException(data.getId() + ": the item does not exist anymore"); } if (state.getStatus() == ItemState.STATUS_NEW) { if (!data.getDefinition().isProtected()) { /* NEW items can always be read as long they have been added through the API and NOT by the system (i.e. protected items). */ return true; } else { /* NEW protected (system) item: need use the path to evaluate the effective permissions. */ return (path == null) ? sessionContext.getAccessManager().isGranted(data.getId(), AccessManager.READ) : sessionContext.getAccessManager().isGranted(path, Permission.READ); } } else { /* item is not NEW -> save to call acMgr.canRead(Path,ItemId) */ return sessionContext.getAccessManager().canRead(path, data.getId()); } } /** * @param parent The item data of the parent node. * @param childId * @return true if the item with the given <code>childId</code> can be read; * <code>false</code> otherwise. * @throws RepositoryException */ private boolean canRead(ItemData parent, ItemId childId) throws RepositoryException { if (parent.getStatus() == ItemState.STATUS_EXISTING) { // child item is for sure not NEW (because then the parent was modified). // safe to use AccessManager#canRead(Path, ItemId). return sessionContext.getAccessManager().canRead(null, childId); } else { // child could be NEW -> don't use AccessManager#canRead(Path, ItemId) return sessionContext.getAccessManager().isGranted(childId, AccessManager.READ); } } //--------------------------------------------------< item access methods > /** * Checks whether an item exists at the specified path. * * @deprecated As of JSR 283, a <code>Path</code> doesn't anymore uniquely * identify an <code>Item</code>, therefore {@link #nodeExists(Path)} and * {@link #propertyExists(Path)} should be used instead. * * @param path path to the item to be checked * @return true if the specified item exists */ public boolean itemExists(Path path) { try { sanityCheck(); ItemId id = hierMgr.resolvePath(path); return (id != null) && itemExists(id, path); } catch (RepositoryException re) { return false; } } /** * Checks whether a node exists at the specified path. * * @param path path to the node to be checked * @return true if a node exists at the specified path */ public boolean nodeExists(Path path) { try { sanityCheck(); NodeId id = hierMgr.resolveNodePath(path); return (id != null) && itemExists(id, path); } catch (RepositoryException re) { return false; } } /** * Checks whether a property exists at the specified path. * * @param path path to the property to be checked * @return true if a property exists at the specified path */ public boolean propertyExists(Path path) { try { sanityCheck(); PropertyId id = hierMgr.resolvePropertyPath(path); return (id != null) && itemExists(id, path); } catch (RepositoryException re) { return false; } } /** * Checks if the item with the given id exists. * * @param id id of the item to be checked * @return true if the specified item exists */ public boolean itemExists(ItemId id) { return itemExists(id, null); } /** * @return * @throws RepositoryException */ NodeImpl getRootNode() throws RepositoryException { return (NodeImpl) getItem(sessionContext.getRootNodeId()); } /** * Returns the node at the specified absolute path in the workspace. * If no such node exists, then it returns the property at the specified path. * If no such property exists a <code>PathNotFoundException</code> is thrown. * * @deprecated As of JSR 283, a <code>Path</code> doesn't anymore uniquely * identify an <code>Item</code>, therefore {@link #getNode(Path)} and * {@link #getProperty(Path)} should be used instead. * @param path * @return * @throws PathNotFoundException * @throws AccessDeniedException * @throws RepositoryException */ public ItemImpl getItem(Path path) throws PathNotFoundException, AccessDeniedException, RepositoryException { ItemId id = hierMgr.resolvePath(path); if (id == null) { throw new PathNotFoundException(safeGetJCRPath(path)); } try { ItemImpl item = getItem(id, path, true); // Test, if this item is a shareable node. if (item.isNode() && ((NodeImpl) item).isShareable()) { return getNode(path); } return item; } catch (ItemNotFoundException infe) { throw new PathNotFoundException(safeGetJCRPath(path)); } } /** * @param path * @return * @throws PathNotFoundException * @throws AccessDeniedException * @throws RepositoryException */ public NodeImpl getNode(Path path) throws PathNotFoundException, AccessDeniedException, RepositoryException { NodeId id = hierMgr.resolveNodePath(path); if (id == null) { throw new PathNotFoundException(safeGetJCRPath(path)); } NodeId parentId = null; if (!path.denotesRoot()) { parentId = hierMgr.resolveNodePath(path.getAncestor(1)); } try { if (parentId == null) { return (NodeImpl) getItem(id, path, true); } // if the node is shareable, it now returns the node with the right // parent return getNode(id, parentId); } catch (ItemNotFoundException infe) { throw new PathNotFoundException(safeGetJCRPath(path)); } } /** * @param path * @return * @throws PathNotFoundException * @throws AccessDeniedException * @throws RepositoryException */ public PropertyImpl getProperty(Path path) throws PathNotFoundException, AccessDeniedException, RepositoryException { PropertyId id = hierMgr.resolvePropertyPath(path); if (id == null) { throw new PathNotFoundException(safeGetJCRPath(path)); } try { return (PropertyImpl) getItem(id, path, true); } catch (ItemNotFoundException infe) { throw new PathNotFoundException(safeGetJCRPath(path)); } } /** * @param id * @return * @throws RepositoryException */ public synchronized ItemImpl getItem(ItemId id) throws ItemNotFoundException, AccessDeniedException, RepositoryException { return getItem(id, null, true); } /** * @param id * @return * @throws RepositoryException */ synchronized ItemImpl getItem(ItemId id, boolean permissionCheck) throws ItemNotFoundException, AccessDeniedException, RepositoryException { return getItem(id, null, permissionCheck); } /** * Returns a node with a given id and parent id. If the indicated node is * shareable, there might be multiple nodes associated with the same id, * but there'is only one node with the given parent id. * * @param id node id * @param parentId parent node id * @return node * @throws RepositoryException if an error occurs */ public synchronized NodeImpl getNode(NodeId id, NodeId parentId) throws ItemNotFoundException, AccessDeniedException, RepositoryException { return getNode(id, parentId, true); } /** * Returns a node with a given id and parent id. If the indicated node is * shareable, there might be multiple nodes associated with the same id, * but there'is only one node with the given parent id. * * @param id node id * @param parentId parent node id * @param permissionCheck Flag indicating if read permission must be check * upon retrieving the node. * @return node * @throws RepositoryException if an error occurs */ synchronized NodeImpl getNode(NodeId id, NodeId parentId, boolean permissionCheck) throws ItemNotFoundException, AccessDeniedException, RepositoryException { if (parentId == null) { return (NodeImpl) getItem(id); } AbstractNodeData data = retrieveItem(id, parentId); if (data == null) { data = (AbstractNodeData) getItemData(id, null, permissionCheck); } else if (permissionCheck && !canRead(data, id)) { // item exists but read-perm has been revoked in the mean time. // -> remove from cache evictItems(id); throw new AccessDeniedException("cannot read item " + data.getId()); } if (!data.getParentId().equals(parentId)) { // verify that parent actually appears in the shared set if (!data.getNodeState().containsShare(parentId)) { String msg = "Node with id '" + id + "' does not have shared parent with id: " + parentId; throw new ItemNotFoundException(msg); } // TODO: ev. need to check if read perm. is granted. data = new NodeDataRef(data, parentId); cacheItem(data); } return createNodeInstance(data); } /** * Create an item instance from an item state. This method creates a * new <code>ItemData</code> instance without looking at the cache nor * testing if the item can be read and returns a new item instance. * * @param state item state * @return item instance * @throws RepositoryException if an error occurs */ synchronized ItemImpl createItemInstance(ItemState state) throws RepositoryException { ItemData data = createItemData(state, null, false); return createItemInstance(data); } /** * @param parentId * @return * @throws ItemNotFoundException * @throws AccessDeniedException * @throws RepositoryException */ synchronized boolean hasChildNodes(NodeId parentId) throws ItemNotFoundException, AccessDeniedException, RepositoryException { sanityCheck(); ItemData data = getItemData(parentId); if (!data.isNode()) { String msg = "can't list child nodes of property " + parentId; log.debug(msg); throw new RepositoryException(msg); } NodeState state = (NodeState) data.getState(); for (ChildNodeEntry entry : state.getChildNodeEntries()) { // make sure any of the properties can be read. if (canRead(data, entry.getId())) { return true; } } return false; } /** * @param parentId * @return * @throws ItemNotFoundException * @throws AccessDeniedException * @throws RepositoryException */ synchronized NodeIterator getChildNodes(NodeId parentId) throws ItemNotFoundException, AccessDeniedException, RepositoryException { sanityCheck(); ItemData data = getItemData(parentId); if (!data.isNode()) { String msg = "can't list child nodes of property " + parentId; log.debug(msg); throw new RepositoryException(msg); } ArrayList<ItemId> childIds = new ArrayList<ItemId>(); Iterator<ChildNodeEntry> iter = ((NodeState) data.getState()).getChildNodeEntries().iterator(); while (iter.hasNext()) { ChildNodeEntry entry = iter.next(); // delay check for read-access until item is being built // thus avoid duplicate check childIds.add(entry.getId()); } return new LazyItemIterator(sessionContext, childIds, parentId); } /** * @param parentId * @return * @throws ItemNotFoundException * @throws AccessDeniedException * @throws RepositoryException */ synchronized boolean hasChildProperties(NodeId parentId) throws ItemNotFoundException, AccessDeniedException, RepositoryException { sanityCheck(); ItemData data = getItemData(parentId); if (!data.isNode()) { String msg = "can't list child properties of property " + parentId; log.debug(msg); throw new RepositoryException(msg); } Iterator<Name> iter = ((NodeState) data.getState()).getPropertyNames().iterator(); while (iter.hasNext()) { Name propName = iter.next(); // make sure any of the properties can be read. if (canRead(data, new PropertyId(parentId, propName))) { return true; } } return false; } /** * @param parentId * @return * @throws ItemNotFoundException * @throws AccessDeniedException * @throws RepositoryException */ synchronized PropertyIterator getChildProperties(NodeId parentId) throws ItemNotFoundException, AccessDeniedException, RepositoryException { sanityCheck(); ItemData data = getItemData(parentId); if (!data.isNode()) { String msg = "can't list child properties of property " + parentId; log.debug(msg); throw new RepositoryException(msg); } ArrayList<PropertyId> childIds = new ArrayList<PropertyId>(); Iterator<Name> iter = ((NodeState) data.getState()).getPropertyNames().iterator(); while (iter.hasNext()) { Name propName = iter.next(); PropertyId id = new PropertyId(parentId, propName); // delay check for read-access until item is being built // thus avoid duplicate check childIds.add(id); } return new LazyItemIterator(sessionContext, childIds); } //-------------------------------------------------< item factory methods > /** * Builds the <code>ItemData</code> for the specified <code>state</code>. * If <code>permissionCheck</code> is <code>true</code>, the access manager * is used to determine if reading that item would be granted. If this is * not the case an <code>AccessDeniedException</code> is thrown. * Before returning the created <code>ItemData</code> it is put into the * cache. In order to benefit from the cache * {@link #getItemData(ItemId, Path, boolean)} should be called. * * @param state * @return * @throws RepositoryException */ private ItemData createItemData(ItemState state, Path path, boolean permissionCheck) throws RepositoryException { ItemData data; if (state.isNode()) { NodeState nodeState = (NodeState) state; data = new NodeData(nodeState, this); } else { PropertyState propertyState = (PropertyState) state; data = new PropertyData(propertyState, this); } // make sure read-perm. is granted before returning the data. if (permissionCheck && !canRead(data, path)) { throw new AccessDeniedException("cannot read item " + state.getId()); } // before returning the data: put them into the cache. cacheItem(data); return data; } private ItemImpl createItemInstance(ItemData data) { if (data.isNode()) { return createNodeInstance((AbstractNodeData) data); } else { return createPropertyInstance((PropertyData) data); } } private NodeImpl createNodeInstance(AbstractNodeData data) { // check special nodes final NodeState state = data.getNodeState(); if (state.getNodeTypeName().equals(NameConstants.NT_VERSION)) { return new VersionImpl(this, sessionContext, data); } else if (state.getNodeTypeName().equals(NameConstants.NT_VERSIONHISTORY)) { return new VersionHistoryImpl(this, sessionContext, data); } else { // create node object return new NodeImpl(this, sessionContext, data); } } private PropertyImpl createPropertyInstance(PropertyData data) { // check special nodes return new PropertyImpl(this, sessionContext, data); } //---------------------------------------------------< item cache methods > /** * Returns an item reference from the cache. * * @param id id of the item that should be retrieved. * @return the item reference stored in the corresponding cache entry * or <code>null</code> if there's no corresponding cache entry. */ private ItemData retrieveItem(ItemId id) { synchronized (itemCache) { ItemData data = itemCache.get(id); if (data == null && id.denotesNode()) { data = shareableNodesCache.retrieveFirst((NodeId) id); } return data; } } /** * Return a node from the cache. * * @param id id of the node that should be retrieved. * @param parentId parent id of the node that should be retrieved * @return reference stored in the corresponding cache entry * or <code>null</code> if there's no corresponding cache entry. */ private AbstractNodeData retrieveItem(NodeId id, NodeId parentId) { synchronized (itemCache) { AbstractNodeData data = shareableNodesCache.retrieve(id, parentId); if (data == null) { data = (AbstractNodeData) itemCache.get(id); } return data; } } /** * Puts the reference of an item in the cache with * the item's path as the key. * * @param data the item data to cache */ private void cacheItem(ItemData data) { synchronized (itemCache) { if (data.isNode()) { AbstractNodeData nd = (AbstractNodeData) data; if (nd.getPrimaryParentId() != null) { shareableNodesCache.cache(nd); return; } } ItemId id = data.getId(); if (itemCache.containsKey(id)) { log.debug("overwriting cached item " + id); } if (log.isDebugEnabled()) { log.debug("caching item " + id); } itemCache.put(id, data); } } /** * Removes all cache entries with the given item id. If the item is * shareable, there might be more than one cache entry for this item. * * @param id id of the items to remove from the cache */ private void evictItems(ItemId id) { if (log.isDebugEnabled()) { log.debug("removing items " + id + " from cache"); } synchronized (itemCache) { itemCache.remove(id); if (id.denotesNode()) { shareableNodesCache.evictAll((NodeId) id); } } } /** * Removes a cache entry for a specific item. * * @param data The item data to remove from the cache */ private void evictItem(ItemData data) { if (log.isDebugEnabled()) { log.debug("removing item " + data.getId() + " from cache"); } synchronized (itemCache) { if (data.isNode()) { shareableNodesCache.evict((AbstractNodeData) data); } ItemData cached = itemCache.get(data.getId()); if (cached == data) { itemCache.remove(data.getId()); } } } //-------------------------------------------------< misc. helper methods > /** * Failsafe conversion of internal <code>Path</code> to JCR path for use in * error messages etc. * * @param path path to convert * @return JCR path */ String safeGetJCRPath(Path path) { try { return session.getJCRPath(path); } catch (NamespaceException e) { log.error("failed to convert " + path.toString() + " to JCR path."); // return string representation of internal path as a fallback return path.toString(); } } /** * Failsafe translation of internal <code>ItemId</code> to JCR path for use in * error messages etc. * * @param id path to convert * @return JCR path */ String safeGetJCRPath(ItemId id) { try { return safeGetJCRPath(hierMgr.getPath(id)); } catch (RepositoryException re) { log.error(id + ": failed to determine path to"); // return string representation if id as a fallback return id.toString(); } } //------------------------------------------------< ItemLifeCycleListener > /** * {@inheritDoc} */ public void itemInvalidated(ItemId id, ItemData data) { if (log.isDebugEnabled()) { log.debug("invalidated item " + id); } evictItem(data); } /** * {@inheritDoc} */ public void itemDestroyed(ItemId id, ItemData data) { if (log.isDebugEnabled()) { log.debug("destroyed item " + id); } synchronized (itemCache) { // remove instance from cache evictItems(id); } } //--------------------------------------------------------------< Object > /** * {@inheritDoc} */ public synchronized String toString() { StringBuilder builder = new StringBuilder(); builder.append("ItemManager (" + super.toString() + ")\n"); builder.append("Items in cache:\n"); synchronized (itemCache) { for (ItemId id : itemCache.keySet()) { ItemData item = itemCache.get(id); if (item.isNode()) { builder.append("Node: "); } else { builder.append("Property: "); } if (item.getState().isTransient()) { builder.append("transient "); } else { builder.append(" "); } builder.append(id + "\t" + safeGetJCRPath(id) + " (" + item + ")\n"); } } return builder.toString(); } //----------------------------------------------------< ItemStateListener > /** * {@inheritDoc} */ public void stateCreated(ItemState created) { ItemData data = retrieveItem(created.getId()); if (data != null) { data.setStatus(ItemImpl.STATUS_NORMAL); } } /** * {@inheritDoc} */ public void stateModified(ItemState modified) { ItemData data = retrieveItem(modified.getId()); if (data != null && data.getState() == modified) { data.setStatus(ItemImpl.STATUS_MODIFIED); /* if (modified.isNode()) { NodeState state = (NodeState) modified; if (state.isShareable()) { //evictItem(modified.getId()); NodeData nodeData = (NodeData) data; NodeData shareSibling = new NodeData(nodeData, state.getParentId()); shareableNodesCache.cache(shareSibling); } } */ } } /** * {@inheritDoc} */ public void stateDestroyed(ItemState destroyed) { ItemData data = retrieveItem(destroyed.getId()); if (data != null && data.getState() == destroyed) { itemDestroyed(destroyed.getId(), data); data.setStatus(ItemImpl.STATUS_DESTROYED); } } /** * {@inheritDoc} */ public void stateDiscarded(ItemState discarded) { ItemData data = retrieveItem(discarded.getId()); if (data != null && data.getState() == discarded) { if (discarded.isTransient()) { switch (discarded.getStatus()) { /** * persistent item that has been transiently removed */ case ItemState.STATUS_EXISTING_REMOVED: case ItemState.STATUS_EXISTING_MODIFIED: ItemState persistentState = discarded.getOverlayedState(); // the state is a transient wrapper for the underlying // persistent state, therefore restore the persistent state // and resurrect this item instance if necessary SessionItemStateManager stateMgr = sessionContext.getItemStateManager(); stateMgr.disconnectTransientItemState(discarded); data.setState(persistentState); return; /** * persistent item that has been transiently modified or * removed and the underlying persistent state has been * externally destroyed since the transient * modification/removal. */ case ItemState.STATUS_STALE_DESTROYED: /** * first notify the listeners that this instance has been * permanently invalidated */ itemDestroyed(discarded.getId(), data); // now set state of this instance to 'destroyed' data.setStatus(ItemImpl.STATUS_DESTROYED); data.setState(null); return; /** * new item that has been transiently added */ case ItemState.STATUS_NEW: /** * first notify the listeners that this instance has been * permanently invalidated */ itemDestroyed(discarded.getId(), data); // now set state of this instance to 'destroyed' // finally dispose state data.setStatus(ItemImpl.STATUS_DESTROYED); data.setState(null); return; } } /** * first notify the listeners that this instance has been * invalidated */ itemInvalidated(discarded.getId(), data); // now render this instance 'invalid' data.setStatus(ItemImpl.STATUS_INVALIDATED); } } /** * Cache of shareable nodes. For performance reasons, methods are not * synchronized and thread-safety must be guaranteed by caller. */ static class ShareableNodesCache { /** * This cache is based on a reference map, that maps an item id to a map, * which again maps a (hard-ref) parent id to a (weak-ref) shareable node. */ private final ReferenceMap cache; /** * Create a new instance of this class. */ public ShareableNodesCache() { cache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.HARD); } /** * Clear cache. * * @see ReferenceMap#clear() */ public void clear() { cache.clear(); } /** * Return the first available node that maps to the given id. * * @param id node id * @return node or <code>null</code> */ public AbstractNodeData retrieveFirst(NodeId id) { ReferenceMap map = (ReferenceMap) cache.get(id); if (map != null) { Iterator<AbstractNodeData> iter = map.values().iterator(); try { while (iter.hasNext()) { AbstractNodeData data = iter.next(); if (data != null) { return data; } } } finally { iter = null; } } return null; } /** * Return the node with the given id and parent id. * * @param id node id * @param parentId parent id * @return node or <code>null</code> */ public AbstractNodeData retrieve(NodeId id, NodeId parentId) { ReferenceMap map = (ReferenceMap) cache.get(id); if (map != null) { return (AbstractNodeData) map.get(parentId); } return null; } /** * Cache some node. * * @param data data to cache */ public void cache(AbstractNodeData data) { NodeId id = data.getNodeState().getNodeId(); ReferenceMap map = (ReferenceMap) cache.get(id); if (map == null) { map = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.WEAK); cache.put(id, map); } Object old = map.put(data.getPrimaryParentId(), data); if (old != null) { log.debug("overwriting cached item: " + old); } } /** * Evict some node from the cache. * * @param data data to evict */ public void evict(AbstractNodeData data) { ReferenceMap map = (ReferenceMap) cache.get(data.getId()); if (map != null) { map.remove(data.getPrimaryParentId()); } } /** * Evict all nodes with a given node id from the cache. * * @param id node id to evict */ public synchronized void evictAll(NodeId id) { cache.remove(id); } } }