You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by md...@apache.org on 2013/09/09 13:35:14 UTC

svn commit: r1521055 - in /jackrabbit/oak/trunk: oak-core/src/main/java/org/apache/jackrabbit/oak/api/ oak-core/src/main/java/org/apache/jackrabbit/oak/core/ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/ oak-jcr/src/main/java/org/apache/...

Author: mduerig
Date: Mon Sep  9 11:35:14 2013
New Revision: 1521055

URL: http://svn.apache.org/r1521055
Log:
OAK-993  Improve backward compatibility for Item.save and Item.refresh
Item.save throws UnsupportedRepositoryException when its effect is not equivalent to calling Session.save

Added:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeExcludingValidator.java
    jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/ItemSaveTest.java
Modified:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/CommitFailedException.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Root.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/core/AbstractRoot.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ImmutableRoot.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeValidator.java
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/ItemImpl.java
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/ItemDelegate.java
    jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/CommitFailedException.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/CommitFailedException.java?rev=1521055&r1=1521054&r2=1521055&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/CommitFailedException.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/CommitFailedException.java Mon Sep  9 11:35:14 2013
@@ -16,12 +16,15 @@
  */
 package org.apache.jackrabbit.oak.api;
 
+import static java.lang.String.format;
+
 import javax.annotation.Nonnull;
 import javax.jcr.AccessDeniedException;
 import javax.jcr.InvalidItemStateException;
 import javax.jcr.NamespaceException;
 import javax.jcr.ReferentialIntegrityException;
 import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
 import javax.jcr.lock.LockException;
 import javax.jcr.nodetype.ConstraintViolationException;
 import javax.jcr.nodetype.NoSuchNodeTypeException;
@@ -29,8 +32,6 @@ import javax.jcr.security.AccessControlE
 import javax.jcr.version.LabelExistsVersionException;
 import javax.jcr.version.VersionException;
 
-import static java.lang.String.format;
-
 /**
  * Main exception thrown by methods defined on the {@code ContentSession}
  * interface indicating that committing a given set of changes failed.
@@ -58,7 +59,7 @@ public class CommitFailedException exten
     public static final String CONSTRAINT = "Constraint";
 
     /**
-     * Type name for referencial integrity violation errors.
+     * Type name for referential integrity violation errors.
      */
     public static final String INTEGRITY = "Integrity";
 
@@ -98,6 +99,11 @@ public class CommitFailedException exten
     public static final String LABEL_EXISTS = "LabelExists";
 
     /**
+     * Unsupported operation or feature
+     */
+    public static final String UNSUPPORTED = "Unsupported";
+
+    /**
      * Serial version UID
      */
     private static final long serialVersionUID = 2727602333350620918L;
@@ -230,6 +236,8 @@ public class CommitFailedException exten
             return new LabelExistsVersionException(message, this);
         } else if (isOfType(LOCK)) {
             return new LockException(message, this);
+        } else if (isOfType(UNSUPPORTED)) {
+            return new UnsupportedRepositoryOperationException(message, this);
         } else {
             return new RepositoryException(message, this);
         }

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Root.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Root.java?rev=1521055&r1=1521054&r2=1521055&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Root.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/api/Root.java Mon Sep  9 11:35:14 2013
@@ -20,6 +20,8 @@ package org.apache.jackrabbit.oak.api;
 
 import javax.annotation.Nonnull;
 
+import org.apache.jackrabbit.oak.spi.commit.CommitHook;
+
 /**
  * A {@code Root} instance serves as a container for a {@link Tree}. It is
  * obtained from a {@link ContentSession}, which governs accessibility and
@@ -30,8 +32,8 @@ import javax.annotation.Nonnull;
  * will throw an {@code InvalidStateException}.
  * <p>
  * {@link Tree} instances may become non existing after a call to
- * {@link #refresh()}, {@link #rebase()} or {@link #commit()}. Any write
- * access to non existing {@code Tree} instances will cause an
+ * {@link #refresh()}, {@link #rebase()} or {@link #commit(CommitHook... hooks)}.
+ * Any write access to non existing {@code Tree} instances will cause an
  * {@code InvalidStateException}.
  * @see Tree Existence and iterability of trees
  */
@@ -50,7 +52,7 @@ public interface Root {
      * </ul>
      * If a tree at {@code destinationPath} exists but is not accessible to the
      * editing content session this method succeeds but a subsequent
-     * {@link #commit()} will detect the violation and fail.
+     * {@link #commit(CommitHook... hooks)} will detect the violation and fail.
      *
      * @param sourcePath The source path
      * @param destPath The destination path
@@ -70,7 +72,7 @@ public interface Root {
      * </ul>
      * If a tree at {@code destinationPath} exists but is not accessible to the
      * editing content session this method succeeds but a subsequent
-     * {@link #commit()} will detect the violation and fail.
+     * {@link #commit(CommitHook... hooks)} will detect the violation and fail.
      *
      * @param sourcePath source path
      * @param destPath destination path
@@ -102,13 +104,20 @@ public interface Root {
     void refresh();
 
     /**
-     * Atomically apply all changes made to the tree contained in this root to the
-     * underlying store and refreshes this root. After a call to this method,
-     * trees obtained through {@link #getTree(String)} may become non existing.
+     * Atomically persists all changes made to the tree contained in this root to the underlying
+     * store.
+     * <p>
+     * Before any changes are actually persisted the passed commit hooks are run and may fail the
+     * commit by throwing a {@code CommitFailedException}. The commit hooks are run in the order as
+     * passed and <em>before</em> any other commit hook that might be present in this root.
+     * <p>
+     * After a successful operation the root is automatically {@link #refresh() refreshed}, such
+     * that trees obtained through {@link #getTree(String)} may become non existing.
      *
+     * @param hooks  commit hooks to run before any changes are persisted.
      * @throws CommitFailedException
      */
-    void commit() throws CommitFailedException;
+    void commit(CommitHook... hooks) throws CommitFailedException;
 
     /**
      * Determine whether there are changes on this tree

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/core/AbstractRoot.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/core/AbstractRoot.java?rev=1521055&r1=1521054&r2=1521055&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/core/AbstractRoot.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/core/AbstractRoot.java Mon Sep  9 11:35:14 2013
@@ -18,15 +18,21 @@
  */
 package org.apache.jackrabbit.oak.core;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.jackrabbit.oak.commons.PathUtils.getName;
+import static org.apache.jackrabbit.oak.commons.PathUtils.getParentPath;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.PrivilegedAction;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+
 import javax.annotation.Nonnull;
 import javax.security.auth.Subject;
 
+import com.google.common.collect.Lists;
 import org.apache.jackrabbit.oak.api.Blob;
 import org.apache.jackrabbit.oak.api.BlobFactory;
 import org.apache.jackrabbit.oak.api.CommitFailedException;
@@ -56,10 +62,6 @@ import org.apache.jackrabbit.oak.spi.sta
 import org.apache.jackrabbit.oak.spi.state.NodeStoreBranch;
 import org.apache.jackrabbit.oak.util.LazyValue;
 
-import static com.google.common.base.Preconditions.checkNotNull;
-import static org.apache.jackrabbit.oak.commons.PathUtils.getName;
-import static org.apache.jackrabbit.oak.commons.PathUtils.getParentPath;
-
 public abstract class AbstractRoot implements Root {
 
     /**
@@ -234,7 +236,7 @@ public abstract class AbstractRoot imple
     }
 
     @Override
-    public void commit() throws CommitFailedException {
+    public void commit(final CommitHook... hooks) throws CommitFailedException {
         checkLive();
         purgePendingChanges();
         CommitFailedException exception = Subject.doAs(
@@ -242,7 +244,7 @@ public abstract class AbstractRoot imple
             @Override
             public CommitFailedException run() {
                 try {
-                    branch.merge(getCommitHook(), postHook);
+                    branch.merge(getCommitHook(hooks), postHook);
                     return null;
                 } catch (CommitFailedException e) {
                     return e;
@@ -256,14 +258,15 @@ public abstract class AbstractRoot imple
     }
 
     /**
-     * Combine the globally defined commit hook(s) with the hooks and
+     * Combine the passed {@code hooks}, the globally defined commit hook(s) and the hooks and
      * validators defined by the various security related configurations.
      *
-     * @return A commit hook combining repository global commit hook(s) with
-     *         the pluggable hooks defined with the security modules.
+     * @return A commit hook combining repository global commit hook(s) with the pluggable hooks
+     *         defined with the security modules and the padded {@code hooks}.
+     * @param hooks
      */
-    private CommitHook getCommitHook() {
-        List<CommitHook> commitHooks = new ArrayList<CommitHook>();
+    private CommitHook getCommitHook(CommitHook[] hooks) {
+        List<CommitHook> commitHooks = Lists.newArrayList(hooks);
         commitHooks.add(hook);
         List<CommitHook> postValidationHooks = new ArrayList<CommitHook>();
         for (SecurityConfiguration sc : securityProvider.getConfigurations()) {
@@ -285,7 +288,7 @@ public abstract class AbstractRoot imple
 
     /**
      * TODO: review again once the permission validation is completed.
-     * Build a read only subject for the {@link #commit()} call that makes the
+     * Build a read only subject for the {@link #commit(CommitHook...)} call that makes the
      * principals and the permission provider available to the commit hooks.
      *
      * @return a new read only subject.

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ImmutableRoot.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ImmutableRoot.java?rev=1521055&r1=1521054&r2=1521055&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ImmutableRoot.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/core/ImmutableRoot.java Mon Sep  9 11:35:14 2013
@@ -31,6 +31,7 @@ import org.apache.jackrabbit.oak.api.Tre
 import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexProvider;
 import org.apache.jackrabbit.oak.query.QueryEngineImpl;
+import org.apache.jackrabbit.oak.spi.commit.CommitHook;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 
 /**
@@ -91,7 +92,7 @@ public final class ImmutableRoot impleme
     }
 
     @Override
-    public void commit() {
+    public void commit(CommitHook... hooks) {
         throw new UnsupportedOperationException();
     }
 

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeExcludingValidator.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeExcludingValidator.java?rev=1521055&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeExcludingValidator.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeExcludingValidator.java Mon Sep  9 11:35:14 2013
@@ -0,0 +1,108 @@
+/*
+ * 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.oak.spi.commit;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+
+/**
+ * Validator that excludes a subtree from the validation process and delegates
+ * validation of other changes to another given validator.
+ *
+ * @see SubtreeValidator
+ * @since Oak 0.9
+ */
+public class SubtreeExcludingValidator extends DefaultValidator {
+
+    private final Validator validator;
+
+    private final String head;
+
+    private final List<String> tail;
+
+    public SubtreeExcludingValidator(Validator validator, String... path) {
+        this(validator, Arrays.asList(path));
+    }
+
+    protected SubtreeExcludingValidator(Validator validator, List<String> path) {
+        this.validator = checkNotNull(validator);
+        checkNotNull(path);
+        checkArgument(!path.isEmpty());
+        this.head = path.get(0);
+        this.tail = path.subList(1, path.size());
+    }
+
+    @Override
+    public void propertyAdded(PropertyState after) throws CommitFailedException {
+        validator.propertyAdded(after);
+    }
+
+    @Override
+    public void propertyChanged(PropertyState before, PropertyState after)
+            throws CommitFailedException {
+        validator.propertyChanged(before, after);
+    }
+
+    @Override
+    public void propertyDeleted(PropertyState before) throws CommitFailedException {
+        validator.propertyDeleted(before);
+    }
+
+    @Override
+    public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException {
+        validator.childNodeAdded(name, after);
+        return descend(name);
+    }
+
+    @Override
+    public Validator childNodeChanged(String name, NodeState before, NodeState after)
+            throws CommitFailedException {
+        validator.childNodeChanged(name, before, after);
+        return descend(name);
+    }
+
+    @Override
+    public Validator childNodeDeleted(String name, NodeState before)
+            throws CommitFailedException {
+        validator.childNodeDeleted(name, before);
+        return descend(name);
+    }
+
+    private Validator descend(String name) {
+        if (!head.equals(name)) {
+            return validator;
+        } else if (tail.isEmpty()) {
+            return null;
+        } else {
+            return createValidator(validator, tail);
+        }
+    }
+
+    protected SubtreeExcludingValidator createValidator(Validator validator, List<String> path) {
+        return new SubtreeExcludingValidator(validator, path);
+    }
+
+}

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeValidator.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeValidator.java?rev=1521055&r1=1521054&r2=1521055&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeValidator.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/commit/SubtreeValidator.java Mon Sep  9 11:35:14 2013
@@ -16,18 +16,19 @@
  */
 package org.apache.jackrabbit.oak.spi.commit;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import java.util.Arrays;
 import java.util.List;
 
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-
 /**
  * Validator that detects changes to a specified subtree and delegates the
  * validation of such changes to another given validator.
  *
+ * @see SubtreeExcludingValidator
  * @since Oak 0.3
  */
 public class SubtreeValidator extends DefaultValidator {

Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/ItemImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/ItemImpl.java?rev=1521055&r1=1521054&r2=1521055&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/ItemImpl.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/ItemImpl.java Mon Sep  9 11:35:14 2013
@@ -39,7 +39,6 @@ import javax.jcr.RepositoryException;
 import javax.jcr.Session;
 import javax.jcr.Value;
 import javax.jcr.ValueFactory;
-import javax.jcr.lock.LockException;
 import javax.jcr.nodetype.ConstraintViolationException;
 import javax.jcr.nodetype.ItemDefinition;
 import javax.jcr.version.VersionManager;
@@ -63,6 +62,14 @@ import org.slf4j.LoggerFactory;
 abstract class ItemImpl<T extends ItemDelegate> implements Item {
     private static final Logger log = LoggerFactory.getLogger(ItemImpl.class);
 
+    /**
+     * The value of this flag determines the behaviour of {@link #save()}. If {@code false},
+     * save will throw a {@link javax.jcr.UnsupportedRepositoryOperationException} if the
+     * sub tree rooted at this item does not contain <em>all</em> transient changes. If
+     * {@code true}, save will delegate to {@link Session#save()}.
+     */
+    public static final boolean SAVE_SESSION = Boolean.getBoolean("item-safe-does-session-safe");
+
     protected final SessionContext sessionContext;
     protected final T dlg;
     protected final SessionDelegate sessionDelegate;
@@ -121,7 +128,7 @@ abstract class ItemImpl<T extends ItemDe
     public String getName() throws RepositoryException {
         String oakName = perform(new ItemOperation<String>(dlg) {
             @Override
-            public String perform() throws RepositoryException {
+            public String perform() {
                 return item.getName();
             }
         });
@@ -137,7 +144,7 @@ abstract class ItemImpl<T extends ItemDe
     public String getPath() throws RepositoryException {
         return toJcrPath(perform(new ItemOperation<String>(dlg) {
             @Override
-            public String perform() throws RepositoryException {
+            public String perform() {
                 return item.getPath();
             }
         }));
@@ -228,17 +235,32 @@ abstract class ItemImpl<T extends ItemDe
     }
 
     /**
+     * This implementation delegates to {@link Session#save()} if {@link #SAVE_SESSION} is
+     * {@code true}. Otherwise it only performs the save if the subtree rooted at this item contains
+     * all transient changes. That is, if calling {@link Session#save()} would have the same effect
+     * as calling this method. In all other cases this method will throw an
+     * {@link javax.jcr.UnsupportedRepositoryOperationException}
+     *
      * @see javax.jcr.Item#save()
      */
     @Override
     public void save() throws RepositoryException {
-        log.warn("Item#save is no longer supported. Please use Session#save instead.");
-        
-        if (isNew()) {
-            throw new RepositoryException("Item.save() not allowed on new item");
+        if (SAVE_SESSION) {
+            getSession().save();
+        } else {
+            perform(new ItemWriteOperation<Void>() {
+                @Override
+                public Void perform() throws RepositoryException {
+                    dlg.save();
+                    return null;
+                }
+
+                @Override
+                public boolean isSave() {
+                    return true;
+                }
+            });
         }
-        
-        getSession().save();
     }
 
     /**

Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/ItemDelegate.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/ItemDelegate.java?rev=1521055&r1=1521054&r2=1521055&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/ItemDelegate.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/ItemDelegate.java Mon Sep  9 11:35:14 2013
@@ -110,4 +110,12 @@ public abstract class ItemDelegate {
      */
     public abstract boolean remove() throws InvalidItemStateException;
 
+    /**
+     * Save the subtree rooted at this item.
+     *
+     * @throws RepositoryException
+     */
+    public void save() throws RepositoryException {
+        sessionDelegate.save(getPath());
+    }
 }

Modified: jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java?rev=1521055&r1=1521054&r2=1521055&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java (original)
+++ jackrabbit/oak/trunk/oak-jcr/src/main/java/org/apache/jackrabbit/oak/jcr/delegate/SessionDelegate.java Mon Sep  9 11:35:14 2013
@@ -17,8 +17,12 @@
 package org.apache.jackrabbit.oak.jcr.delegate;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Lists.newArrayList;
+import static org.apache.jackrabbit.oak.commons.PathUtils.denotesRoot;
+import static org.apache.jackrabbit.oak.commons.PathUtils.elements;
 
 import java.io.IOException;
+import java.util.List;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nonnull;
@@ -30,6 +34,7 @@ import javax.jcr.nodetype.ConstraintViol
 import org.apache.jackrabbit.oak.api.AuthInfo;
 import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.QueryEngine;
 import org.apache.jackrabbit.oak.api.Root;
 import org.apache.jackrabbit.oak.api.Tree;
@@ -38,10 +43,18 @@ import org.apache.jackrabbit.oak.core.Id
 import org.apache.jackrabbit.oak.jcr.RefreshStrategy;
 import org.apache.jackrabbit.oak.jcr.operation.SessionOperation;
 import org.apache.jackrabbit.oak.jcr.security.AccessManager;
+import org.apache.jackrabbit.oak.spi.commit.Editor;
+import org.apache.jackrabbit.oak.spi.commit.EditorHook;
+import org.apache.jackrabbit.oak.spi.commit.EditorProvider;
+import org.apache.jackrabbit.oak.spi.commit.FailingValidator;
+import org.apache.jackrabbit.oak.spi.commit.SubtreeExcludingValidator;
+import org.apache.jackrabbit.oak.spi.commit.Validator;
 import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
 import org.apache.jackrabbit.oak.spi.security.authorization.AuthorizationConfiguration;
 import org.apache.jackrabbit.oak.spi.security.authorization.permission.PermissionProvider;
 import org.apache.jackrabbit.oak.spi.security.authorization.permission.Permissions;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -291,6 +304,34 @@ public class SessionDelegate {
         permissionProvider.refresh();
     }
 
+    /**
+     * Save the subtree rooted at the given {@code path}.
+     * <p>
+     * This implementation only performs the save if the subtree rooted at {@code path} contains
+     * all transient changes and will throw an
+     * {@link javax.jcr.UnsupportedRepositoryOperationException} otherwise.
+     *
+     * @param path
+     * @throws RepositoryException
+     */
+    public void save(final String path) throws RepositoryException {
+        if (denotesRoot(path)) {
+            save();
+        } else {
+            try {
+                root.commit(new EditorHook(new EditorProvider() {
+                    @Override
+                    public Editor getRootEditor(NodeState before, NodeState after, NodeBuilder builder) {
+                        return new ItemSaveValidator(path);
+                    }
+                }));
+            } catch (CommitFailedException e) {
+                throw newRepositoryException(e);
+            }
+        }
+        permissionProvider.refresh();
+    }
+
     public void refresh(boolean keepChanges) {
         if (keepChanges && hasPendingChanges()) {
             root.rebase();
@@ -434,4 +475,50 @@ public class SessionDelegate {
     private static RepositoryException newRepositoryException(CommitFailedException exception) {
         return exception.asRepositoryException();
     }
+
+    /**
+     * This validator checks that all changes are contained within the subtree
+     * rooted at a given path.
+     */
+    private static class ItemSaveValidator extends SubtreeExcludingValidator {
+
+        /**
+         * Name of the property whose {@link #propertyChanged(PropertyState, PropertyState)} to
+         * ignore or {@code null} if no property should be ignored.
+         */
+        private final String ignorePropertyChange;
+
+        /**
+         * Create a new validator that only throws a {@link CommitFailedException} whenever
+         * there are changes not contained in the subtree rooted at {@code path}.
+         * @param path
+         */
+        public ItemSaveValidator(String path) {
+            this(new FailingValidator(CommitFailedException.UNSUPPORTED, 0,
+                    "Failed to save subtree at " + path + ". There are " +
+                            "transient modifications outside that subtree."),
+                    newArrayList(elements(path)));
+        }
+
+        private ItemSaveValidator(Validator validator, List<String> path) {
+            super(validator, path);
+            // Ignore property changes if this is the head of the path.
+            // This allows for calling save on a changed property.
+            ignorePropertyChange = path.size() == 1 ? path.get(0) : null;
+        }
+
+        @Override
+        public void propertyChanged(PropertyState before, PropertyState after)
+                throws CommitFailedException {
+            if (!before.getName().equals(ignorePropertyChange)) {
+                super.propertyChanged(before, after);
+            }
+        }
+
+        @Override
+        protected SubtreeExcludingValidator createValidator(
+                Validator validator, final List<String> path) {
+            return new ItemSaveValidator(validator, path);
+        }
+    }
 }

Added: jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/ItemSaveTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/ItemSaveTest.java?rev=1521055&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/ItemSaveTest.java (added)
+++ jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/ItemSaveTest.java Mon Sep  9 11:35:14 2013
@@ -0,0 +1,139 @@
+/*
+ * 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.oak.jcr;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.UnsupportedRepositoryOperationException;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * See OAK-993
+ */
+public class ItemSaveTest extends AbstractRepositoryTest {
+
+    public ItemSaveTest(NodeStoreFixture fixture) {
+        super(fixture);
+    }
+
+    private Session session;
+    private Node root;
+    private Node foo;
+    private Property prop0;
+    private Property prop1;
+    private Property prop2;
+
+    @Before
+    public void setup() throws RepositoryException {
+        session = getAdminSession();
+        root = session.getRootNode();
+        foo = root.addNode("foo").addNode("child0");
+        prop0 = root.setProperty("p0", "v0");
+        prop1 = foo.setProperty("p1", "v1");
+        prop2 = foo.setProperty("p2", "v2");
+        session.save();
+    }
+
+    @Test
+    public void noChangesAtAll() throws RepositoryException {
+        foo.save();
+    }
+
+    @Test
+    public void saveContainsAllChanges() throws RepositoryException {
+        foo.addNode("child");
+        foo.save();
+    }
+
+    @Test
+    public void saveOnRoot() throws RepositoryException {
+        root.addNode("child");
+        root.save();
+    }
+
+    @Test
+    public void saveMissesNode() throws RepositoryException {
+        try {
+            root.addNode("child1");
+            foo.addNode("child2");
+            foo.save();
+            fail("Expected UnsupportedRepositoryOperationException");
+        } catch (UnsupportedRepositoryOperationException e) {
+            assertTrue(e.getCause() instanceof CommitFailedException);
+        }
+    }
+
+    @Test
+    public void saveOnNewNode() throws RepositoryException {
+        try {
+            foo.addNode("child").save();
+            fail("Expected UnsupportedRepositoryOperationException");
+        } catch (UnsupportedRepositoryOperationException e) {
+            assertTrue(e.getCause() instanceof CommitFailedException);
+        }
+    }
+
+    @Test
+    public void saveOnChangedProperty() throws RepositoryException {
+        // Property on root
+        prop0.setValue("changed");
+        prop0.save();
+
+        // Property on child node
+        prop1.setValue("changed");
+        prop1.save();
+    }
+
+    @Test
+    public void saveMissesProperty() throws RepositoryException {
+        try {
+            prop1.setValue("changed");
+            prop2.setValue("changed");
+            prop1.save();
+            fail("Expected UnsupportedRepositoryOperationException");
+        } catch (UnsupportedRepositoryOperationException e) {
+            assertTrue(e.getCause() instanceof CommitFailedException);
+        } finally {
+            session.refresh(false);
+        }
+    }
+
+    @Test
+    public void saveOnNewProperty() throws RepositoryException {
+        try {
+            foo.setProperty("p3", "v3").save();
+            fail("Expected UnsupportedRepositoryOperationException");
+        } catch (UnsupportedRepositoryOperationException e) {
+            assertTrue(e.getCause() instanceof CommitFailedException);
+        } finally {
+            session.refresh(false);
+        }
+
+    }
+
+}