Java tutorial
/* * 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 org.apache.commons.collections.iterators.IteratorChain; import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; import org.apache.jackrabbit.core.nodetype.NodeDef; import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException; import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl; import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; import org.apache.jackrabbit.core.nodetype.PropDef; import org.apache.jackrabbit.core.nodetype.PropertyDefinitionImpl; import org.apache.jackrabbit.core.security.AccessManager; 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.NodeState; import org.apache.jackrabbit.core.state.PropertyState; import org.apache.jackrabbit.core.state.SessionItemStateManager; import org.apache.jackrabbit.core.state.StaleItemStateException; import org.apache.jackrabbit.core.value.InternalValue; import org.apache.jackrabbit.core.version.VersionHistoryInfo; import org.apache.jackrabbit.core.version.VersionManager; import org.apache.jackrabbit.spi.Name; import org.apache.jackrabbit.spi.Path; import org.apache.jackrabbit.spi.commons.name.NameConstants; import org.apache.jackrabbit.util.Text; import org.apache.jackrabbit.uuid.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.AccessDeniedException; import javax.jcr.InvalidItemStateException; import javax.jcr.Item; import javax.jcr.ItemExistsException; import javax.jcr.ItemNotFoundException; import javax.jcr.ItemVisitor; import javax.jcr.Node; import javax.jcr.PathNotFoundException; import javax.jcr.PropertyType; import javax.jcr.ReferentialIntegrityException; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.UnsupportedRepositoryOperationException; import javax.jcr.lock.LockException; import javax.jcr.nodetype.ConstraintViolationException; import javax.jcr.nodetype.ItemDefinition; import javax.jcr.nodetype.NoSuchNodeTypeException; import javax.jcr.nodetype.NodeDefinition; import javax.jcr.nodetype.NodeType; import javax.jcr.version.VersionException; import java.util.ArrayList; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.HashSet; import java.util.Iterator; import java.util.Set; /** * <code>ItemImpl</code> implements the <code>Item</code> interface. */ public abstract class ItemImpl implements Item { private static Logger log = LoggerFactory.getLogger(ItemImpl.class); protected static final int STATUS_NORMAL = 0; protected static final int STATUS_MODIFIED = 1; protected static final int STATUS_DESTROYED = 2; protected static final int STATUS_INVALIDATED = 3; protected final ItemId id; /** * <code>Session</code> through which this <code>Item</code> was acquired */ protected final SessionImpl session; /** * the <code>Repository</code> object */ protected final RepositoryImpl rep; /** * Item data associated with this item. */ protected final ItemData data; /** * <code>ItemManager</code> that created this <code>Item</code> */ protected final ItemManager itemMgr; /** * <code>SessionItemStateManager</code> associated with this <code>Item</code> */ protected final SessionItemStateManager stateMgr; /** * Package private constructor. * * @param itemMgr the <code>ItemManager</code> that created this <code>Item</code> * @param session the <code>Session</code> through which this <code>Item</code> is acquired * @param data ItemData of this <code>Item</code> */ ItemImpl(ItemManager itemMgr, SessionImpl session, ItemData data) { this.session = session; rep = (RepositoryImpl) session.getRepository(); stateMgr = session.getItemStateManager(); this.id = data.getId(); this.itemMgr = itemMgr; this.data = data; } /** * Performs a sanity check on this item and the associated session. * * @throws RepositoryException if this item has been rendered invalid for some reason */ protected void sanityCheck() throws RepositoryException { // check session status session.sanityCheck(); // check status of this item for read operation final int status = data.getStatus(); if (status == STATUS_DESTROYED || status == STATUS_INVALIDATED) { throw new InvalidItemStateException(id + ": the item does not exist anymore"); } } protected boolean isTransient() { return getItemState().isTransient(); } protected abstract ItemState getOrCreateTransientItemState() throws RepositoryException; protected abstract void makePersistent() throws InvalidItemStateException; /** * Marks this instance as 'removed' and notifies its listeners. * The resulting state is either 'temporarily invalidated' or * 'permanently invalidated', depending on the initial state. * * @throws RepositoryException if an error occurs */ protected void setRemoved() throws RepositoryException { final int status = data.getStatus(); if (status == STATUS_INVALIDATED || status == STATUS_DESTROYED) { // this instance is already 'invalid', get outta here return; } ItemState transientState = getOrCreateTransientItemState(); if (transientState.getStatus() == ItemState.STATUS_NEW) { // this is a 'new' item, simply dispose the transient state // (it is no longer used); this will indirectly (through // stateDiscarded listener method) invalidate this instance permanently stateMgr.disposeTransientItemState(transientState); } else { // this is an 'existing' item (i.e. it is backed by persistent // state), mark it as 'removed' transientState.setStatus(ItemState.STATUS_EXISTING_REMOVED); // transfer the transient state to the attic stateMgr.moveTransientItemStateToAttic(transientState); // set state of this instance to 'invalid' data.setStatus(STATUS_INVALIDATED); // notify the manager that this instance has been // temporarily invalidated itemMgr.itemInvalidated(id, data); } } /** * Returns the item-state associated with this <code>Item</code>. * * @return state associated with this <code>Item</code> */ ItemState getItemState() { return data.getState(); } /** * Return the id of this <code>Item</code>. * * @return the id of this <code>Item</code> */ public ItemId getId() { return id; } /** * Returns the primary path to this <code>Item</code>. * * @return the primary path to this <code>Item</code> */ public Path getPrimaryPath() throws RepositoryException { return session.getHierarchyManager().getPath(id); } /** * Builds a list of transient (i.e. new or modified) item states that are * within the scope of <code>this.{@link #save()}</code>. The collection * returned is ordered depth-first, i.e. the item itself (if transient) * comes last. * * @return list of transient item states * @throws InvalidItemStateException * @throws RepositoryException */ private Collection getTransientStates() throws InvalidItemStateException, RepositoryException { // list of transient states that should be persisted ArrayList dirty = new ArrayList(); ItemState transientState; if (isNode()) { // build list of 'new' or 'modified' descendants Iterator iter = stateMgr.getDescendantTransientItemStates((NodeId) id); while (iter.hasNext()) { transientState = (ItemState) iter.next(); // fail-fast test: check status of transient state switch (transientState.getStatus()) { case ItemState.STATUS_NEW: case ItemState.STATUS_EXISTING_MODIFIED: // add modified state to the list dirty.add(transientState); break; case ItemState.STATUS_STALE_MODIFIED: throw new InvalidItemStateException( "Item cannot be saved because it has been " + "modified externally: " + this); case ItemState.STATUS_STALE_DESTROYED: throw new InvalidItemStateException( "Item cannot be saved because it has been " + "deleted externally: " + this); case ItemState.STATUS_UNDEFINED: throw new InvalidItemStateException( "Item cannot be saved; it seems to have been " + "removed externally: " + this); default: log.warn("Unexpected item state status: " + transientState.getStatus() + " of " + this); // ignore break; } } } // fail-fast test: check status of this item's state if (isTransient()) { final ItemState state = getItemState(); switch (state.getStatus()) { case ItemState.STATUS_EXISTING_MODIFIED: // add this item's state to the list dirty.add(state); break; case ItemState.STATUS_NEW: throw new RepositoryException("Cannot save a new item: " + this); case ItemState.STATUS_STALE_MODIFIED: throw new InvalidItemStateException( "Item cannot be saved because it has been" + " modified externally: " + this); case ItemState.STATUS_STALE_DESTROYED: throw new InvalidItemStateException( "Item cannot be saved because it has been" + " deleted externally:" + this); case ItemState.STATUS_UNDEFINED: throw new InvalidItemStateException( "Item cannot be saved; it seems to have been" + " removed externally: " + this); default: log.warn("Unexpected item state status:" + state.getStatus() + " of " + this); // ignore break; } } return dirty; } /** * Builds a list of transient descendant item states in the attic * (i.e. those marked as 'removed') that are within the scope of * <code>this.{@link #save()}</code>. * * @return list of transient item states * @throws InvalidItemStateException * @throws RepositoryException */ private Collection getRemovedStates() throws InvalidItemStateException, RepositoryException { ArrayList removed = new ArrayList(); ItemState transientState; if (isNode()) { Iterator iter = stateMgr.getDescendantTransientItemStatesInAttic((NodeId) id); while (iter.hasNext()) { transientState = (ItemState) iter.next(); // check if stale if (transientState.getStatus() == ItemState.STATUS_STALE_MODIFIED) { String msg = transientState.getId() + ": the item cannot be removed because it has been modified externally."; log.debug(msg); throw new InvalidItemStateException(msg); } if (transientState.getStatus() == ItemState.STATUS_STALE_DESTROYED) { String msg = transientState.getId() + ": the item cannot be removed because it has already been deleted externally."; log.debug(msg); throw new InvalidItemStateException(msg); } removed.add(transientState); } } return removed; } private void validateTransientItems(Iterator dirtyIter, Iterator removedIter) throws AccessDeniedException, ConstraintViolationException, RepositoryException { /** * the following validations/checks are performed on transient items: * * for every transient item: * - if it is 'modified' or 'new' check the corresponding write permission. * - if it is 'removed' check the REMOVE permission * * for every transient node: * - if it is 'new' check that its node type satisfies the * 'required node type' constraint specified in its definition * - check if 'mandatory' child items exist * * for every transient property: * - check if the property value satisfies the value constraints * specified in the property's definition * * note that the protected flag is checked in Node.addNode/Node.remove * (for adding/removing child entries of a node), in * Node.addMixin/removeMixin/setPrimaryType (for type changes on nodes) * and in Property.setValue (for properties to be modified). */ AccessManager accessMgr = session.getAccessManager(); NodeTypeManagerImpl ntMgr = session.getNodeTypeManager(); // walk through list of dirty transient items and validate each while (dirtyIter.hasNext()) { ItemState itemState = (ItemState) dirtyIter.next(); ItemDefinition def; if (itemState.isNode()) { def = itemMgr.getDefinition((NodeState) itemState); } else { def = itemMgr.getDefinition((PropertyState) itemState); } /* check permissions for non-protected items. protected items are only added through API methods which need to assert that permissions are not violated. */ if (!def.isProtected()) { /* detect the effective set of modification: - new added node -> add_node perm on the child - new property added -> set_property permission - property modified -> set_property permission - modified nodes can be ignored for changes only included child-item addition or removal or changes of protected properties such as mixin-types which are covered separately note: removed items are checked later on. */ Path path = stateMgr.getHierarchyMgr().getPath(itemState.getId()); boolean isGranted = true; if (itemState.isNode()) { if (itemState.getStatus() == ItemState.STATUS_NEW) { isGranted = accessMgr.isGranted(path, Permission.ADD_NODE); } // else: modified node (see comment above) } else { // modified or new property: set_property permission isGranted = accessMgr.isGranted(path, Permission.SET_PROPERTY); } if (!isGranted) { String msg = itemMgr.safeGetJCRPath(path) + ": not allowed to add or modify item"; log.debug(msg); throw new AccessDeniedException(msg); } } if (itemState.isNode()) { // the transient item is a node NodeState nodeState = (NodeState) itemState; ItemId id = nodeState.getNodeId(); NodeDefinition nodeDef = (NodeDefinition) def; // primary type NodeTypeImpl pnt = ntMgr.getNodeType(nodeState.getNodeTypeName()); // effective node type (primary type incl. mixins) EffectiveNodeType ent = getEffectiveNodeType(nodeState); /** * if the transient node was added (i.e. if it is 'new') or if * its primary type has changed, check its node type against the * required node type in its definition */ if (nodeState.getStatus() == ItemState.STATUS_NEW || !nodeState.getNodeTypeName() .equals(((NodeState) nodeState.getOverlayedState()).getNodeTypeName())) { NodeType[] nta = nodeDef.getRequiredPrimaryTypes(); for (int i = 0; i < nta.length; i++) { NodeTypeImpl ntReq = (NodeTypeImpl) nta[i]; if (!(pnt.getQName().equals(ntReq.getQName()) || pnt.isDerivedFrom(ntReq.getQName()))) { /** * the transient node's primary node type does not * satisfy the 'required primary types' constraint */ String msg = itemMgr.safeGetJCRPath(id) + " must be of node type " + ntReq.getName(); log.debug(msg); throw new ConstraintViolationException(msg); } } } // mandatory child properties PropDef[] pda = ent.getMandatoryPropDefs(); for (int i = 0; i < pda.length; i++) { PropDef pd = pda[i]; if (pd.getDeclaringNodeType().equals(NameConstants.MIX_VERSIONABLE) || pd.getDeclaringNodeType().equals(NameConstants.MIX_SIMPLE_VERSIONABLE)) { /** * todo FIXME workaround for mix:versionable: * the mandatory properties are initialized at a * later stage and might not exist yet */ continue; } if (!nodeState.hasPropertyName(pd.getName())) { String msg = itemMgr.safeGetJCRPath(id) + ": mandatory property " + pd.getName() + " does not exist"; log.debug(msg); throw new ConstraintViolationException(msg); } } // mandatory child nodes NodeDef[] cnda = ent.getMandatoryNodeDefs(); for (int i = 0; i < cnda.length; i++) { NodeDef cnd = cnda[i]; if (!nodeState.hasChildNodeEntry(cnd.getName())) { String msg = itemMgr.safeGetJCRPath(id) + ": mandatory child node " + cnd.getName() + " does not exist"; log.debug(msg); throw new ConstraintViolationException(msg); } } } else { // the transient item is a property PropertyState propState = (PropertyState) itemState; ItemId propId = propState.getPropertyId(); PropertyDefinitionImpl propDef = (PropertyDefinitionImpl) def; /** * check value constraints * (no need to check value constraints of protected properties * as those are set by the implementation only, i.e. they * cannot be set by the user through the api) */ if (!def.isProtected()) { String[] constraints = propDef.getValueConstraints(); if (constraints != null) { InternalValue[] values = propState.getValues(); try { EffectiveNodeType.checkSetPropertyValueConstraints(propDef.unwrap(), values); } catch (RepositoryException e) { // repack exception for providing more verbose error message String msg = itemMgr.safeGetJCRPath(propId) + ": " + e.getMessage(); log.debug(msg); throw new ConstraintViolationException(msg); } /** * need to manually check REFERENCE value constraints * as this requires a session (target node needs to * be checked) */ if (constraints.length > 0 && propDef.getRequiredType() == PropertyType.REFERENCE) { for (int i = 0; i < values.length; i++) { boolean satisfied = false; String constraintViolationMsg = null; try { UUID targetUUID = values[i].getUUID(); Node targetNode = session.getNodeByUUID(targetUUID); /** * constraints are OR-ed, i.e. at least one * has to be satisfied */ for (int j = 0; j < constraints.length; j++) { /** * a REFERENCE value constraint specifies * the name of the required node type of * the target node */ String ntName = constraints[j]; if (targetNode.isNodeType(ntName)) { satisfied = true; break; } } if (!satisfied) { NodeType[] mixinNodeTypes = targetNode.getMixinNodeTypes(); String[] targetMixins = new String[mixinNodeTypes.length]; for (int j = 0; j < mixinNodeTypes.length; j++) { targetMixins[j] = mixinNodeTypes[j].getName(); } String targetMixinsString = Text.implode(targetMixins, ", "); String constraintsString = Text.implode(constraints, ", "); constraintViolationMsg = itemMgr.safeGetJCRPath(propId) + ": is constraint to [" + constraintsString + "] but references [primaryType=" + targetNode.getPrimaryNodeType().getName() + ", mixins=" + targetMixinsString + "]"; } } catch (RepositoryException re) { String msg = itemMgr.safeGetJCRPath(propId) + ": failed to check REFERENCE value constraint"; log.debug(msg); throw new ConstraintViolationException(msg, re); } if (!satisfied) { log.debug(constraintViolationMsg); throw new ConstraintViolationException(constraintViolationMsg); } } } } } /** * no need to check the protected flag as this is checked * in PropertyImpl.setValue(Value) */ } } // walk through list of removed transient items and check REMOVE permission while (removedIter.hasNext()) { ItemState itemState = (ItemState) removedIter.next(); ItemDefinition def; try { if (itemState.isNode()) { def = itemMgr.getDefinition((NodeState) itemState); } else { def = itemMgr.getDefinition((PropertyState) itemState); } } catch (ConstraintViolationException e) { // since identifier of assigned definition is not stored anymore // with item state (see JCR-2170), correct definition cannot be // determined for items which have been removed due to removal // of a mixin (see also JCR-2130 & JCR-2408) continue; } if (!def.isProtected()) { Path path = stateMgr.getAtticAwareHierarchyMgr().getPath(itemState.getId()); // check REMOVE permission int permission = (itemState.isNode()) ? Permission.REMOVE_NODE : Permission.REMOVE_PROPERTY; if (!accessMgr.isGranted(path, permission)) { String msg = itemMgr.safeGetJCRPath(path) + ": not allowed to remove item"; log.debug(msg); throw new AccessDeniedException(msg); } } } } private void removeTransientItems(Iterator iter) { /** * walk through list of transient items marked 'removed' and * definitively remove each one */ while (iter.hasNext()) { ItemState transientState = (ItemState) iter.next(); ItemState persistentState = transientState.getOverlayedState(); /** * remove persistent state * * this will indirectly (through stateDestroyed listener method) * permanently invalidate all Item instances wrapping it */ stateMgr.destroy(persistentState); } } private void persistTransientItems(Iterator iter) throws RepositoryException { // walk through list of transient items and persist each one while (iter.hasNext()) { ItemState state = (ItemState) iter.next(); ItemImpl item = itemMgr.getItem(state.getId()); // persist state of transient item item.makePersistent(); } } private void restoreTransientItems(Iterator iter) { // walk through list of transient states and re-apply transient changes while (iter.hasNext()) { ItemState itemState = (ItemState) iter.next(); ItemId id = itemState.getId(); ItemImpl item; try { if (stateMgr.isItemStateInAttic(id)) { // If an item has been removed and then again created, the // item is lost after persistTransientItems() and the // TransientItemStateManager will bark because of a deleted // state in its attic. We therefore have to forge a new item // instance ourself. item = itemMgr.createItemInstance(itemState); itemState.setStatus(ItemState.STATUS_NEW); } else { try { item = itemMgr.getItem(id); } catch (ItemNotFoundException infe) { // itemState probably represents a 'new' item and the // ItemImpl instance wrapping it has already been gc'ed; // we have to re-create the ItemImpl instance item = itemMgr.createItemInstance(itemState); itemState.setStatus(ItemState.STATUS_NEW); } } // re-apply transient changes // for persistent nodes undo effect of item.makePersistent() if (item.isNode()) { NodeImpl node = (NodeImpl) item; node.restoreTransient((NodeState) itemState); } else { PropertyImpl prop = (PropertyImpl) item; prop.restoreTransient((PropertyState) itemState); } } catch (RepositoryException re) { // something went wrong, log exception and carry on String msg = itemMgr.safeGetJCRPath(id) + ": failed to restore transient state"; log.warn(msg, re); } } } /** * Process all items given in iterator and check whether <code>mix:shareable</code> * or (some derived node type) has been added or removed: * <ul> * <li>If the mixin <code>mix:shareable</code> (or some derived node type), * then initialize the shared set inside the state.</li> * <li>If the mixin <code>mix:shareable</code> (or some derived node type) * has been removed, throw.</li> * </ul> */ private void processShareableNodes(Iterator iter) throws RepositoryException { while (iter.hasNext()) { ItemState is = (ItemState) iter.next(); if (is.isNode()) { NodeState ns = (NodeState) is; boolean wasShareable = false; if (ns.hasOverlayedState()) { NodeState old = (NodeState) ns.getOverlayedState(); EffectiveNodeType ntOld = getEffectiveNodeType(old); wasShareable = ntOld.includesNodeType(NameConstants.MIX_SHAREABLE); } EffectiveNodeType ntNew = getEffectiveNodeType(ns); boolean isShareable = ntNew.includesNodeType(NameConstants.MIX_SHAREABLE); if (!wasShareable && isShareable) { // mix:shareable has been added ns.addShare(ns.getParentId()); } else if (wasShareable && !isShareable) { // mix:shareable has been removed: not supported String msg = "Removing mix:shareable is not supported."; log.debug(msg); throw new UnsupportedRepositoryOperationException(msg); } } } } /** * Initializes the version history of all new nodes of node type * <code>mix:versionable</code>. * <p/> * Called by {@link #save()}. * * @param iter * @return true if this call generated new transient state; otherwise false * @throws RepositoryException */ private boolean initVersionHistories(Iterator iter) throws RepositoryException { // walk through list of transient items and search for new versionable nodes boolean createdTransientState = false; while (iter.hasNext()) { ItemState itemState = (ItemState) iter.next(); if (itemState.isNode()) { NodeState nodeState = (NodeState) itemState; EffectiveNodeType nt = getEffectiveNodeType(nodeState); if (nt.includesNodeType(NameConstants.MIX_VERSIONABLE)) { if (!nodeState.hasPropertyName(NameConstants.JCR_VERSIONHISTORY)) { NodeImpl node = (NodeImpl) itemMgr.getItem(itemState.getId()); VersionManager vMgr = session.getVersionManager(); /** * check if there's already a version history for that * node; this would e.g. be the case if a versionable * node had been exported, removed and re-imported with * either IMPORT_UUID_COLLISION_REMOVE_EXISTING or * IMPORT_UUID_COLLISION_REPLACE_EXISTING; * otherwise create a new version history */ VersionHistoryInfo history = vMgr.getVersionHistory(session, nodeState); InternalValue historyId = InternalValue.create(history.getVersionHistoryId().getUUID()); InternalValue versionId = InternalValue.create(history.getRootVersionId().getUUID()); node.internalSetProperty(NameConstants.JCR_VERSIONHISTORY, historyId); node.internalSetProperty(NameConstants.JCR_BASEVERSION, versionId); node.internalSetProperty(NameConstants.JCR_ISCHECKEDOUT, InternalValue.create(true)); node.internalSetProperty(NameConstants.JCR_PREDECESSORS, new InternalValue[] { versionId }); createdTransientState = true; } } else if (nt.includesNodeType(NameConstants.MIX_SIMPLE_VERSIONABLE)) { // we need to check the version manager for an existing // version history, since simple versioning does not // expose it's reference in a property VersionManager vMgr = session.getVersionManager(); vMgr.getVersionHistory(session, nodeState); // create isCheckedOutProperty if not already exists NodeImpl node = (NodeImpl) itemMgr.getItem(itemState.getId()); if (!nodeState.hasPropertyName(NameConstants.JCR_ISCHECKEDOUT)) { node.internalSetProperty(NameConstants.JCR_ISCHECKEDOUT, InternalValue.create(true)); createdTransientState = true; } } } } return createdTransientState; } /** * Helper method that builds the effective (i.e. merged and resolved) * node type representation of the specified node's primary and mixin * node types. * * @param state * @return the effective node type * @throws RepositoryException */ private EffectiveNodeType getEffectiveNodeType(NodeState state) throws RepositoryException { try { NodeTypeRegistry registry = session.getNodeTypeManager().getNodeTypeRegistry(); return registry.getEffectiveNodeType(state.getNodeTypeName(), state.getMixinTypeNames()); } catch (NodeTypeConflictException e) { throw new RepositoryException("Failed to build effective node type of node state " + state.getId(), e); } } /** * Failsafe mapping of internal <code>id</code> to JCR path for use in * diagnostic output, error messages etc. * * @return JCR path or some fallback value */ public String safeGetJCRPath() { return itemMgr.safeGetJCRPath(id); } /** * Same as <code>{@link Item#remove()}</code> except for the * <code>noChecks</code> parameter. * * @param noChecks * @throws VersionException * @throws LockException * @throws RepositoryException */ protected void internalRemove(boolean noChecks) throws VersionException, LockException, ConstraintViolationException, RepositoryException { // check state of this instance sanityCheck(); // check if this is the root node if (getDepth() == 0) { throw new RepositoryException("Cannot remove the root node"); } NodeImpl parentNode = (NodeImpl) getParent(); if (!noChecks) { // check if protected and not under retention/hold int options = ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD | ItemValidator.CHECK_RETENTION; session.getValidator().checkRemove(this, options, Permission.NONE); // parent node: make sure it is checked-out and not protected nor locked. options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_VERSIONING | ItemValidator.CHECK_CONSTRAINTS; session.getValidator().checkModify(parentNode, options, Permission.NONE); } // delegate the removal of the child item to the parent node if (isNode()) { parentNode.removeChildNode((NodeId) getId()); } else { Path.Element thisName = getPrimaryPath().getNameElement(); parentNode.removeChildProperty(thisName.getName()); } } /** * Same as <code>{@link Item#getName()}</code> except that * this method returns a <code>Name</code> instead of a * <code>String</code>. * * @return the name of this item as <code>Name</code> * @throws RepositoryException if an error occurs. */ public abstract Name getQName() throws RepositoryException; //-----------------------------------------------------------------< Item > /** * {@inheritDoc} */ public abstract void accept(ItemVisitor visitor) throws RepositoryException; /** * {@inheritDoc} */ public abstract boolean isNode(); /** * {@inheritDoc} */ public abstract String getName() throws RepositoryException; /** * {@inheritDoc} */ public abstract Node getParent() throws ItemNotFoundException, AccessDeniedException, RepositoryException; /** * {@inheritDoc} */ public boolean isNew() { final ItemState state = getItemState(); return state.isTransient() && state.getOverlayedState() == null; } /** * checks if this item is new. running outside of transactions, this * is the same as {@link #isNew()} but within a transaction an item can * be saved but not yet persisted. */ protected boolean isTransactionalNew() { final ItemState state = getItemState(); return state.getStatus() == ItemState.STATUS_NEW; } /** * {@inheritDoc} */ public boolean isModified() { final ItemState state = getItemState(); return state.isTransient() && state.getOverlayedState() != null; } /** * {@inheritDoc} */ public void remove() throws VersionException, LockException, ConstraintViolationException, RepositoryException { internalRemove(false); } /** * {@inheritDoc} */ public void save() throws AccessDeniedException, ItemExistsException, ConstraintViolationException, InvalidItemStateException, ReferentialIntegrityException, VersionException, LockException, NoSuchNodeTypeException, RepositoryException { // check state of this instance sanityCheck(); // synchronize on this session synchronized (session) { /** * build list of transient (i.e. new & modified) states that * should be persisted */ Collection dirty; try { dirty = getTransientStates(); } catch (ConcurrentModificationException e) { String msg = "Concurrent modification; session is closed"; log.error(msg, e); session.logout(); throw e; } if (dirty.size() == 0) { // no transient items, nothing to do here return; } /** * build list of transient descendants in the attic * (i.e. those marked as 'removed') */ Collection removed = getRemovedStates(); /** * build set of item id's which are within the scope of * (i.e. affected by) this save operation */ Set affectedIds = new HashSet(dirty.size() + removed.size()); for (Iterator it = new IteratorChain(dirty.iterator(), removed.iterator()); it.hasNext();) { affectedIds.add(((ItemState) it.next()).getId()); } /** * make sure that this save operation is totally 'self-contained' * and independent; items within the scope of this save operation * must not have 'external' dependencies; * (e.g. moving a node requires that the target node including both * old and new parents are saved) */ for (Iterator it = new IteratorChain(dirty.iterator(), removed.iterator()); it.hasNext();) { ItemState transientState = (ItemState) it.next(); if (transientState.isNode()) { NodeState nodeState = (NodeState) transientState; Set dependentIDs = new HashSet(); if (nodeState.hasOverlayedState()) { NodeState overlayedState = (NodeState) nodeState.getOverlayedState(); NodeId oldParentId = overlayedState.getParentId(); NodeId newParentId = nodeState.getParentId(); if (oldParentId != null) { if (newParentId == null) { // node has been removed, add old parents // to dependencies if (overlayedState.isShareable()) { dependentIDs.addAll(overlayedState.getSharedSet()); } else { dependentIDs.add(oldParentId); } } else { if (!oldParentId.equals(newParentId)) { // node has been moved to a new location, // add old and new parent to dependencies dependentIDs.add(oldParentId); dependentIDs.add(newParentId); } else { // parent id hasn't changed, check whether // the node has been renamed (JCR-1034) if (!affectedIds.contains(newParentId) && stateMgr.hasTransientItemState(newParentId)) { try { NodeState parent = (NodeState) stateMgr .getTransientItemState(newParentId); // check parent's renamed child node entries for (Iterator cneIt = parent.getRenamedChildNodeEntries() .iterator(); cneIt.hasNext();) { ChildNodeEntry cne = (ChildNodeEntry) cneIt.next(); if (cne.getId().equals(nodeState.getId())) { // node has been renamed, // add parent to dependencies dependentIDs.add(newParentId); } } } catch (ItemStateException ise) { // should never get here log.warn("failed to retrieve transient state: " + newParentId, ise); } } } } } } // removed child node entries for (Iterator cneIt = nodeState.getRemovedChildNodeEntries().iterator(); cneIt.hasNext();) { ChildNodeEntry cne = (ChildNodeEntry) cneIt.next(); dependentIDs.add(cne.getId()); } // added child node entries for (Iterator cneIt = nodeState.getAddedChildNodeEntries().iterator(); cneIt.hasNext();) { ChildNodeEntry cne = (ChildNodeEntry) cneIt.next(); dependentIDs.add(cne.getId()); } // now walk through dependencies and check whether they // are within the scope of this save operation Iterator depIt = dependentIDs.iterator(); while (depIt.hasNext()) { NodeId id = (NodeId) depIt.next(); if (!affectedIds.contains(id)) { // JCR-1359 workaround: check whether unresolved // dependencies originate from 'this' session; // otherwise ignore them if (stateMgr.hasTransientItemState(id) || stateMgr.hasTransientItemStateInAttic(id)) { // need to save dependency as well String msg = itemMgr.safeGetJCRPath(id) + " needs to be saved as well."; log.debug(msg); throw new ConstraintViolationException(msg); } } } } } /** * validate access and node type constraints * (this will also validate child removals) */ validateTransientItems(dirty.iterator(), removed.iterator()); // start the update operation try { stateMgr.edit(); } catch (IllegalStateException e) { String msg = "Unable to start edit operation"; log.debug(msg); throw new RepositoryException(msg, e); } boolean succeeded = false; try { // process transient items marked as 'removed' removeTransientItems(removed.iterator()); // process transient items that have change in mixins processShareableNodes(dirty.iterator()); // initialize version histories for new nodes (might generate new transient state) if (initVersionHistories(dirty.iterator())) { // re-build the list of transient states because the previous call // generated new transient state dirty = getTransientStates(); } // process 'new' or 'modified' transient states persistTransientItems(dirty.iterator()); // dispose the transient states marked 'new' or 'modified' // at this point item state data is pushed down one level, // node instances are disconnected from the transient // item state and connected to the 'overlayed' item state. // transient item states must be removed now. otherwise // the session item state provider will return an orphaned // item state which is not referenced by any node instance. for (Iterator it = dirty.iterator(); it.hasNext();) { ItemState transientState = (ItemState) it.next(); // dispose the transient state, it is no longer used stateMgr.disposeTransientItemState(transientState); } // end update operation stateMgr.update(); // update operation succeeded succeeded = true; } catch (StaleItemStateException e) { throw new InvalidItemStateException(e.getMessage()); } catch (ItemStateException e) { throw new RepositoryException("Unable to update item: " + this, e); } finally { if (!succeeded) { // update operation failed, cancel all modifications stateMgr.cancel(); // JCR-288: if an exception has been thrown during // update() the transient changes have already been // applied by persistTransientItems() and we need to // restore transient state, i.e. undo the effect of // persistTransientItems() restoreTransientItems(dirty.iterator()); } } // now it is safe to dispose the transient states: // dispose the transient states marked 'removed'. // item states in attic are removed after store, because // the observation mechanism needs to build paths of removed // items in store(). for (Iterator it = removed.iterator(); it.hasNext();) { ItemState transientState = (ItemState) it.next(); // dispose the transient state, it is no longer used stateMgr.disposeTransientItemStateInAttic(transientState); } } } /** * {@inheritDoc} */ public synchronized void refresh(boolean keepChanges) throws InvalidItemStateException, RepositoryException { // check state of this instance sanityCheck(); if (keepChanges) { /** todo FIXME should reset Item#status field to STATUS_NORMAL * of all descendent non-transient instances; maybe also * have to reset stale ItemState instances */ return; } if (isNode()) { // check if this is the root node if (getDepth() == 0) { // optimization stateMgr.disposeAllTransientItemStates(); return; } } // list of transient items that should be discarded ArrayList list = new ArrayList(); ItemState transientState; // check status of this item's state if (isTransient()) { transientState = getItemState(); switch (transientState.getStatus()) { case ItemState.STATUS_STALE_MODIFIED: case ItemState.STATUS_STALE_DESTROYED: // add this item's state to the list list.add(transientState); break; case ItemState.STATUS_EXISTING_MODIFIED: if (!transientState.getParentId().equals(transientState.getOverlayedState().getParentId())) { throw new RepositoryException( "Cannot refresh a moved item: " + this + " - possible solution: refresh the parent"); } list.add(transientState); break; case ItemState.STATUS_NEW: throw new RepositoryException("Cannot refresh a new item: " + this); default: log.warn("Unexpected item state status:" + transientState.getStatus() + " of " + this); // ignore break; } } if (isNode()) { // build list of 'new', 'modified' or 'stale' descendants Iterator iter = stateMgr.getDescendantTransientItemStates((NodeId) id); while (iter.hasNext()) { transientState = (ItemState) iter.next(); switch (transientState.getStatus()) { case ItemState.STATUS_STALE_MODIFIED: case ItemState.STATUS_STALE_DESTROYED: case ItemState.STATUS_NEW: case ItemState.STATUS_EXISTING_MODIFIED: // add new or modified state to the list list.add(transientState); break; default: log.debug("unexpected state status (" + transientState.getStatus() + ")"); // ignore break; } } } // process list of 'new', 'modified' or 'stale' transient states Iterator iter = list.iterator(); while (iter.hasNext()) { transientState = (ItemState) iter.next(); // dispose the transient state, it is no longer used; // this will indirectly (through stateDiscarded listener method) // either restore or permanently invalidate the wrapping Item instances stateMgr.disposeTransientItemState(transientState); } if (isNode()) { // discard all transient descendants in the attic (i.e. those marked // as 'removed'); this will resurrect the removed items iter = stateMgr.getDescendantTransientItemStatesInAttic((NodeId) id); while (iter.hasNext()) { transientState = (ItemState) iter.next(); // dispose the transient state; this will indirectly (through // stateDiscarded listener method) resurrect the wrapping Item instances stateMgr.disposeTransientItemStateInAttic(transientState); } } } /** * {@inheritDoc} */ public Item getAncestor(int degree) throws ItemNotFoundException, AccessDeniedException, RepositoryException { // check state of this instance sanityCheck(); if (degree == 0) { return itemMgr.getRootNode(); } try { // Path.getAncestor requires relative degree, i.e. we need // to convert absolute to relative ancestor degree Path path = getPrimaryPath(); int relDegree = path.getAncestorCount() - degree; if (relDegree < 0) { throw new ItemNotFoundException(); } // shortcut if (relDegree == 0) { return this; } Path ancestorPath = path.getAncestor(relDegree); return itemMgr.getNode(ancestorPath); } catch (PathNotFoundException pnfe) { throw new ItemNotFoundException(); } } /** * {@inheritDoc} */ public String getPath() throws RepositoryException { // check state of this instance sanityCheck(); return session.getJCRPath(getPrimaryPath()); } /** * {@inheritDoc} */ public int getDepth() throws RepositoryException { // check state of this instance sanityCheck(); final ItemState state = getItemState(); if (state.getParentId() == null) { // shortcut return 0; } return session.getHierarchyManager().getDepth(id); } /** * Returns the session associated with this item. * <p> * Since Jackrabbit 1.4 it is safe to use this method regardless * of item state. * * @see <a href="http://issues.apache.org/jira/browse/JCR-911">Issue JCR-911</a> * @return current session */ public Session getSession() { return session; } /** * {@inheritDoc} */ public boolean isSame(Item otherItem) throws RepositoryException { // check state of this instance sanityCheck(); if (this == otherItem) { return true; } if (otherItem instanceof ItemImpl) { ItemImpl other = (ItemImpl) otherItem; return id.equals(other.id) && session.getWorkspace().getName().equals(other.getSession().getWorkspace().getName()); } return false; } //--------------------------------------------------------------< Object > /** * Returns the({@link #safeGetJCRPath() safe}) path of this item for use * in diagnostic output. * * @return "/path/to/item" */ public String toString() { return safeGetJCRPath(); } }