You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by ju...@apache.org on 2010/06/23 12:14:27 UTC

svn commit: r957147 [2/2] - in /jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core: ItemImpl.java ItemSaveOperation.java SessionImpl.java session/SessionRefreshOperation.java session/SessionSaveOperation.java

Added: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemSaveOperation.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemSaveOperation.java?rev=957147&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemSaveOperation.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemSaveOperation.java Wed Jun 23 10:14:26 2010
@@ -0,0 +1,937 @@
+/*
+ * 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.Collection;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.InvalidItemStateException;
+import javax.jcr.Item;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.nodetype.ItemDefinition;
+import javax.jcr.nodetype.NodeDefinition;
+import javax.jcr.nodetype.NodeType;
+
+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.EffectiveNodeType;
+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.security.AccessManager;
+import org.apache.jackrabbit.core.security.authorization.Permission;
+import org.apache.jackrabbit.core.session.SessionContext;
+import org.apache.jackrabbit.core.session.SessionOperation;
+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.InternalVersionManager;
+import org.apache.jackrabbit.core.version.VersionHistoryInfo;
+import org.apache.jackrabbit.spi.Name;
+import org.apache.jackrabbit.spi.Path;
+import org.apache.jackrabbit.spi.QItemDefinition;
+import org.apache.jackrabbit.spi.QPropertyDefinition;
+import org.apache.jackrabbit.spi.commons.name.NameConstants;
+import org.apache.jackrabbit.util.Text;
+import org.slf4j.LoggerFactory;
+import org.slf4j.Logger;
+
+/**
+ * The session operation triggered by {@link Item#save()}.
+ */
+class ItemSaveOperation extends SessionOperation {
+
+    /**
+     * Logger instance.
+     */
+    private static final Logger log =
+        LoggerFactory.getLogger(ItemSaveOperation.class);
+
+    private final boolean isNode;
+
+    private final ItemState state;
+
+    public ItemSaveOperation(boolean isNode, ItemState state) {
+        super("item save");
+        this.isNode = isNode;
+        this.state = state;
+    }
+
+    @Override
+    public void perform(SessionContext context) throws RepositoryException {
+        SessionItemStateManager stateMgr = context.getItemStateManager();
+
+        /**
+         * build list of transient (i.e. new & modified) states that
+         * should be persisted
+         */
+        Collection<ItemState> dirty;
+        try {
+            dirty = getTransientStates(stateMgr);
+        } catch (ConcurrentModificationException e) {
+            String msg = "Concurrent modification; session is closed";
+            log.error(msg, e);
+            context.getSessionImpl().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<ItemState> removed = getRemovedStates(stateMgr);
+
+        // All affected item states. The keys are used to look up whether
+        // an item is affected, and the values are iterated through below
+        Map<ItemId, ItemState> affected =
+            new HashMap<ItemId, ItemState>(dirty.size() + removed.size());
+        for (ItemState state : dirty) {
+            affected.put(state.getId(), state);
+        }
+        for (ItemState state : removed) {
+            affected.put(state.getId(), state);
+        }
+
+        /**
+         * 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 (ItemState transientState : affected.values()) {
+            if (transientState.isNode()) {
+                NodeState nodeState = (NodeState) transientState;
+                Set<NodeId> dependentIDs = new HashSet<NodeId>();
+                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 (!affected.containsKey(newParentId)
+                                        && stateMgr.hasTransientItemState(newParentId)) {
+                                    try {
+                                        NodeState parent = (NodeState) stateMgr.getTransientItemState(newParentId);
+                                        // check parent's renamed child node entries
+                                        for (ChildNodeEntry cne : parent.getRenamedChildNodeEntries()) {
+                                            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 (ChildNodeEntry cne : nodeState.getRemovedChildNodeEntries()) {
+                    dependentIDs.add(cne.getId());
+                }
+                // added child node entries
+                for (ChildNodeEntry cne : nodeState.getAddedChildNodeEntries()) {
+                    dependentIDs.add(cne.getId());
+                }
+
+                // now walk through dependencies and check whether they
+                // are within the scope of this save operation
+                for (NodeId id : dependentIDs) {
+                    if (!affected.containsKey(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 =
+                                context.getItemManager().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(context, dirty, removed);
+
+        // 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(stateMgr, removed);
+
+            // process transient items that have change in mixins
+            processShareableNodes(context, dirty);
+
+            // initialize version histories for new nodes (might generate new transient state)
+            if (initVersionHistories(context, dirty)) {
+                // re-build the list of transient states because the previous call
+                // generated new transient state
+                dirty = getTransientStates(stateMgr);
+            }
+
+            // process 'new' or 'modified' transient states
+            persistTransientItems(context.getItemManager(), dirty);
+
+            // 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 (ItemState transientState : dirty) {
+                // 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(context, dirty);
+            }
+        }
+
+        // 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 (ItemState transientState : removed) {
+            // dispose the transient state, it is no longer used
+            stateMgr.disposeTransientItemStateInAttic(transientState);
+        }
+    }
+
+    /**
+     * 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<ItemState> getTransientStates(
+            SessionItemStateManager stateMgr)
+            throws InvalidItemStateException, RepositoryException {
+        // list of transient states that should be persisted
+        ArrayList<ItemState> dirty = new ArrayList<ItemState>();
+        ItemState transientState;
+
+        if (isNode) {
+            // build list of 'new' or 'modified' descendants
+            Iterator<ItemState> iter = stateMgr.getDescendantTransientItemStates((NodeId) state.getId());
+            while (iter.hasNext()) {
+                transientState = 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 (state.isTransient()) {
+            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<ItemState> getRemovedStates(
+            SessionItemStateManager stateMgr)
+            throws InvalidItemStateException, RepositoryException {
+        ArrayList<ItemState> removed = new ArrayList<ItemState>();
+        ItemState transientState;
+
+        if (isNode) {
+            Iterator<ItemState> iter = stateMgr.getDescendantTransientItemStatesInAttic((NodeId) state.getId());
+            while (iter.hasNext()) {
+                transientState = 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;
+    }
+
+    /**
+     * 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).
+     */
+    private void validateTransientItems(
+            SessionContext context,
+            Iterable<ItemState> dirty, Iterable<ItemState> removed)
+            throws RepositoryException {
+        SessionImpl session = context.getSessionImpl();
+        ItemManager itemMgr = context.getItemManager();
+        SessionItemStateManager stateMgr = context.getItemStateManager();
+
+        AccessManager accessMgr = session.getAccessManager();
+        NodeTypeManagerImpl ntMgr = session.getNodeTypeManager();
+        // walk through list of dirty transient items and validate each
+        for (ItemState itemState : dirty) {
+            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.
+                   note: reordering of child nodes has been covered upfront as
+                         this information isn't available here.
+                */
+                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(context, 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())) {
+                    for (NodeType ntReq : nodeDef.getRequiredPrimaryTypes()) {
+                        Name ntName = ((NodeTypeImpl) ntReq).getQName();
+                        if (!(pnt.getQName().equals(ntName)
+                                || pnt.isDerivedFrom(ntName))) {
+                            /**
+                             * 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
+                for (QPropertyDefinition pd : ent.getMandatoryPropDefs()) {
+                    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;
+                    }
+                    String msg = itemMgr.safeGetJCRPath(id)
+                                + ": mandatory property " + pd.getName()
+                                + " does not exist";
+                    if (!nodeState.hasPropertyName(pd.getName())) {
+                        log.debug(msg);
+                        throw new ConstraintViolationException(msg);
+                    } else {
+                        /*
+                        there exists a property with the mandatory-name.
+                        make sure the property really has the expected mandatory
+                        property definition (and not another non-mandatory def,
+                        such as e.g. multivalued residual instead of single-value
+                        mandatory, named def).
+                        */
+                        PropertyId pi = new PropertyId(nodeState.getNodeId(), pd.getName());
+                        ItemData childData = itemMgr.getItemData(pi, null, false);
+                        if (!childData.getDefinition().isMandatory()) {
+                            throw new ConstraintViolationException(msg);
+                        }
+                    }
+                }
+                // mandatory child nodes
+                for (QItemDefinition cnd : ent.getMandatoryNodeDefs()) {
+                    String msg = itemMgr.safeGetJCRPath(id)
+                                + ": mandatory child node " + cnd.getName()
+                                + " does not exist";
+                    if (!nodeState.hasChildNodeEntry(cnd.getName())) {
+                        log.debug(msg);
+                        throw new ConstraintViolationException(msg);
+                    } else {
+                        /*
+                        there exists a child node with the mandatory-name.
+                        make sure the node really has the expected mandatory
+                        node definition.
+                        */
+                        boolean hasMandatoryChild = false;
+                        for (ChildNodeEntry cne : nodeState.getChildNodeEntries(cnd.getName())) {
+                            ItemData childData = itemMgr.getItemData(cne.getId(), null, false);
+                            if (childData.getDefinition().isMandatory()) {
+                                hasMandatoryChild = true;
+                                break;
+                            }
+                        }
+                        if (!hasMandatoryChild) {
+                            throw new ConstraintViolationException(msg);
+                        }
+                    }
+                }
+            } else {
+                // the transient item is a property
+                PropertyState propState = (PropertyState) itemState;
+                ItemId propId = propState.getPropertyId();
+                org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl propDef = (org.apache.jackrabbit.spi.commons.nodetype.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
+                                    || propDef.getRequiredType() == PropertyType.WEAKREFERENCE)) {
+                            for (InternalValue internalV : values) {
+                                boolean satisfied = false;
+                                String constraintViolationMsg = null;
+                                try {
+                                    NodeId targetId = internalV.getNodeId();
+                                    if (propDef.getRequiredType() == PropertyType.WEAKREFERENCE
+                                        && !itemMgr.itemExists(targetId)) {
+                                        // target of weakref doesn;t exist, skip
+                                        continue;
+                                    }
+                                    Node targetNode = session.getNodeById(targetId);
+                                    /**
+                                     * constraints are OR-ed, i.e. at least one
+                                     * has to be satisfied
+                                     */
+                                    for (String constrNtName : constraints) {
+                                        /**
+                                         * a [WEAK]REFERENCE value constraint specifies
+                                         * the name of the required node type of
+                                         * the target node
+                                         */
+                                        if (targetNode.isNodeType(constrNtName)) {
+                                            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 "
+                                            + ((propDef.getRequiredType() == PropertyType.REFERENCE) ? "REFERENCE" : "WEAKREFERENCE")
+                                            + " 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
+        for (ItemState itemState : removed) {
+            QItemDefinition def;
+            try {
+                if (itemState.isNode()) {
+                    def = itemMgr.getDefinition((NodeState) itemState).unwrap();
+                } else {
+                    def = itemMgr.getDefinition((PropertyState) itemState).unwrap();
+                }
+            } 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);
+                }
+            }
+        }
+    }
+
+    /**
+     * walk through list of transient items marked 'removed' and
+     * definitively remove each one
+     */
+    private void removeTransientItems(
+            SessionItemStateManager stateMgr, Iterable<ItemState> states) {
+        for (ItemState transientState : states) {
+            ItemState persistentState = transientState.getOverlayedState();
+            /**
+             * remove persistent state
+             *
+             * this will indirectly (through stateDestroyed listener method)
+             * permanently invalidate all Item instances wrapping it
+             */
+            stateMgr.destroy(persistentState);
+        }
+    }
+
+    /**
+     * 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(
+            SessionContext context, Iterable<ItemState> states)
+            throws RepositoryException {
+        for (ItemState is : states) {
+            if (is.isNode()) {
+                NodeState ns = (NodeState) is;
+                boolean wasShareable = false;
+                if (ns.hasOverlayedState()) {
+                    NodeState old = (NodeState) ns.getOverlayedState();
+                    EffectiveNodeType ntOld = getEffectiveNodeType(context, old);
+                    wasShareable = ntOld.includesNodeType(NameConstants.MIX_SHAREABLE);
+                }
+                EffectiveNodeType ntNew = getEffectiveNodeType(context, 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 states
+     * @return true if this call generated new transient state; otherwise false
+     * @throws RepositoryException
+     */
+    private boolean initVersionHistories(
+            SessionContext context, Iterable<ItemState> states)
+            throws RepositoryException {
+        SessionImpl session = context.getSessionImpl();
+        ItemManager itemMgr = context.getItemManager();
+
+        // walk through list of transient items and search for new versionable nodes
+        boolean createdTransientState = false;
+        for (ItemState itemState : states) {
+            if (itemState.isNode()) {
+                NodeState nodeState = (NodeState) itemState;
+                EffectiveNodeType nt = getEffectiveNodeType(context, nodeState);
+                if (nt.includesNodeType(NameConstants.MIX_VERSIONABLE)) {
+                    if (!nodeState.hasPropertyName(NameConstants.JCR_VERSIONHISTORY)) {
+                        NodeImpl node = (NodeImpl) itemMgr.getItem(itemState.getId());
+                        InternalVersionManager vMgr = session.getInternalVersionManager();
+                        /**
+                         * 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, null);
+                        InternalValue historyId = InternalValue.create(
+                                history.getVersionHistoryId());
+                        InternalValue versionId = InternalValue.create(
+                                history.getRootVersionId());
+                        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
+                    InternalVersionManager vMgr = session.getInternalVersionManager();
+                    vMgr.getVersionHistory(session, nodeState, null);
+
+                    // 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;
+    }
+
+    /**
+     * walk through list of transient items and persist each one
+     */
+    private void persistTransientItems(
+            ItemManager itemMgr, Iterable<ItemState> states)
+            throws RepositoryException {
+        for (ItemState state : states) {
+            // persist state of transient item
+            itemMgr.getItem(state.getId()).makePersistent();
+        }
+    }
+
+    /**
+     * walk through list of transient states and re-apply transient changes
+     */
+    private void restoreTransientItems(
+            SessionContext context, Iterable<ItemState> items) {
+        ItemManager itemMgr = context.getItemManager();
+        SessionItemStateManager stateMgr = context.getItemStateManager();
+
+        for (ItemState itemState : items) {
+            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);
+            }
+        }
+    }
+
+    /**
+     * 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(
+            SessionContext context, NodeState state)
+            throws RepositoryException {
+        try {
+            NodeTypeRegistry registry =
+                context.getRepositoryContext().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);
+        }
+    }
+
+}
\ No newline at end of file

Propchange: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemSaveOperation.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionImpl.java?rev=957147&r1=957146&r2=957147&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionImpl.java (original)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionImpl.java Wed Jun 23 10:14:26 2010
@@ -22,8 +22,6 @@ import org.apache.jackrabbit.api.Jackrab
 import org.apache.jackrabbit.api.security.principal.PrincipalManager;
 import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.commons.AbstractSession;
-import org.apache.jackrabbit.core.cluster.ClusterException;
-import org.apache.jackrabbit.core.cluster.ClusterNode;
 import org.apache.jackrabbit.core.config.WorkspaceConfig;
 import org.apache.jackrabbit.core.data.GarbageCollector;
 import org.apache.jackrabbit.core.id.NodeId;
@@ -41,6 +39,8 @@ import org.apache.jackrabbit.core.sessio
 import org.apache.jackrabbit.core.session.ClosedSessionState;
 import org.apache.jackrabbit.core.session.SessionContext;
 import org.apache.jackrabbit.core.session.SessionOperation;
+import org.apache.jackrabbit.core.session.SessionRefreshOperation;
+import org.apache.jackrabbit.core.session.SessionSaveOperation;
 import org.apache.jackrabbit.core.state.NodeState;
 import org.apache.jackrabbit.core.state.SessionItemStateManager;
 import org.apache.jackrabbit.core.util.Dumpable;
@@ -870,49 +870,14 @@ public class SessionImpl extends Abstrac
      * {@inheritDoc}
      */
     public void save() throws RepositoryException {
-        perform(new SessionOperation("save") {
-            @Override
-            public void perform(SessionContext context)
-                    throws RepositoryException {
-                // JCR-2425: check whether session is allowed to read root node
-                if (hasPermission("/", ACTION_READ)) {
-                    context.getItemManager().getRootNode().save();
-                } else {
-                    NodeId id = context.getItemStateManager().getIdOfRootTransientNodeState();
-                    context.getItemManager().getItem(id).save();
-                }
-            }
-        });
+        perform(new SessionSaveOperation());
     }
 
     /**
      * {@inheritDoc}
      */
-    public void refresh(final boolean keepChanges) throws RepositoryException {
-        perform(new SessionOperation("refresh") {
-            @Override
-            public void perform(SessionContext context)
-                    throws RepositoryException {
-                // JCR-1753: Ensure that we are up to date with cluster changes
-                ClusterNode cluster = repositoryContext.getClusterNode();
-                if (cluster != null && clusterSyncOnRefresh()) {
-                    try {
-                        cluster.sync();
-                    } catch (ClusterException e) {
-                        throw new RepositoryException(
-                                "Unable to synchronize with the cluster", e);
-                    }
-                }
-
-                if (!keepChanges) {
-                    context.getItemStateManager().disposeAllTransientItemStates();
-                } else {
-                    // FIXME should reset Item#status field to STATUS_NORMAL
-                    // of all non-transient instances; maybe also
-                    // have to reset stale ItemState instances
-                }
-            }
-        });
+    public void refresh(boolean keepChanges) throws RepositoryException {
+        perform(new SessionRefreshOperation(keepChanges, clusterSyncOnRefresh()));
     }
 
     /**

Added: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionRefreshOperation.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionRefreshOperation.java?rev=957147&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionRefreshOperation.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionRefreshOperation.java Wed Jun 23 10:14:26 2010
@@ -0,0 +1,61 @@
+/*
+ * 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.session;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.core.cluster.ClusterException;
+import org.apache.jackrabbit.core.cluster.ClusterNode;
+
+/**
+ * Operation to refresh the state of a session.
+ */
+public class SessionRefreshOperation extends SessionOperation {
+
+    private final boolean keepChanges;
+
+    private final boolean clusterSync;
+
+    public SessionRefreshOperation(boolean keepChanges, boolean clusterSync) {
+        super("refresh");
+        this.keepChanges = keepChanges;
+        this.clusterSync = clusterSync;
+    }
+
+    @Override
+    public void perform(SessionContext context) throws RepositoryException {
+        // JCR-1753: Ensure that we are up to date with cluster changes
+        ClusterNode cluster = context.getRepositoryContext().getClusterNode();
+        if (cluster != null && clusterSync) {
+            try {
+                cluster.sync();
+            } catch (ClusterException e) {
+                throw new RepositoryException(
+                        "Unable to synchronize with the cluster", e);
+            }
+        }
+
+        if (!keepChanges) {
+            context.getItemStateManager().disposeAllTransientItemStates();
+        } else {
+            // FIXME should reset Item#status field to STATUS_NORMAL
+            // of all non-transient instances; maybe also
+            // have to reset stale ItemState instances
+        }
+    }
+
+}
\ No newline at end of file

Propchange: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionRefreshOperation.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionSaveOperation.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionSaveOperation.java?rev=957147&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionSaveOperation.java (added)
+++ jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionSaveOperation.java Wed Jun 23 10:14:26 2010
@@ -0,0 +1,43 @@
+/*
+ * 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.session;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.jackrabbit.core.id.NodeId;
+
+public class SessionSaveOperation extends SessionOperation {
+
+    public SessionSaveOperation() {
+        super("save");
+    }
+
+    @Override
+    public void perform(SessionContext context)
+            throws RepositoryException {
+        NodeId id;
+        // JCR-2425: check whether session is allowed to read root node
+        if (context.getSessionImpl().hasPermission("/", Session.ACTION_READ)) {
+            id = context.getRootNodeId();
+        } else {
+            id = context.getItemStateManager().getIdOfRootTransientNodeState();
+        }
+        context.getItemManager().getItem(id).save();
+    }
+
+}
\ No newline at end of file

Propchange: jackrabbit/trunk/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionSaveOperation.java
------------------------------------------------------------------------------
    svn:eol-style = native