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 ba...@apache.org on 2015/08/05 21:15:57 UTC

svn commit: r1694289 [1/2] - in /jackrabbit/oak/trunk/oak-upgrade/src: main/java/org/apache/jackrabbit/oak/upgrade/ main/java/org/apache/jackrabbit/oak/upgrade/nodestate/ test/java/org/apache/jackrabbit/oak/upgrade/ test/java/org/apache/jackrabbit/oak/...

Author: baedke
Date: Wed Aug  5 19:15:56 2015
New Revision: 1694289

URL: http://svn.apache.org/r1694289
Log:
OAK-2586: Support including and excluding paths during upgrade

Initial implementation. Full credit goes to Julian Sedding (jsedding@gmail.com) for the patch and to Tomek Rekawek (trekawek@gmail.com) for aligning it with the current trunk.

Added:
    jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeState.java
    jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/IncludeExcludeUpgradeTest.java
    jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/UpgradeFromTwoSourcesTest.java
    jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeStateTest.java
Modified:
    jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java
    jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java
    jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java
    jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java
    jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java

Modified: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java?rev=1694289&r1=1694288&r2=1694289&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java (original)
+++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java Wed Aug  5 19:15:56 2015
@@ -18,8 +18,8 @@ package org.apache.jackrabbit.oak.upgrad
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.Iterables.addAll;
-import static com.google.common.collect.Iterables.skip;
 import static com.google.common.collect.Lists.newArrayList;
 import static com.google.common.collect.Lists.newArrayListWithCapacity;
 import static com.google.common.collect.Maps.newHashMap;
@@ -39,6 +39,9 @@ import static org.apache.jackrabbit.JcrC
 import static org.apache.jackrabbit.JcrConstants.NT_FROZENNODE;
 import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED;
 import static org.apache.jackrabbit.JcrConstants.NT_VERSIONHISTORY;
+import static org.apache.jackrabbit.core.RepositoryImpl.ACTIVITIES_NODE_ID;
+import static org.apache.jackrabbit.core.RepositoryImpl.ROOT_NODE_ID;
+import static org.apache.jackrabbit.core.RepositoryImpl.VERSION_STORAGE_NODE_ID;
 import static org.apache.jackrabbit.oak.api.Type.NAME;
 import static org.apache.jackrabbit.oak.api.Type.NAMES;
 import static org.apache.jackrabbit.oak.api.Type.STRING;
@@ -49,6 +52,7 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.math.BigDecimal;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -58,7 +62,10 @@ import javax.jcr.Binary;
 import javax.jcr.PropertyType;
 import javax.jcr.RepositoryException;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import org.apache.jackrabbit.api.ReferenceBinary;
+import org.apache.jackrabbit.core.RepositoryContext;
 import org.apache.jackrabbit.core.id.NodeId;
 import org.apache.jackrabbit.core.persistence.PersistenceManager;
 import org.apache.jackrabbit.core.persistence.util.NodePropBundle;
@@ -98,7 +105,7 @@ class JackrabbitNodeState extends Abstra
         }
     }
 
-    private final JackrabbitNodeState parent;
+    private JackrabbitNodeState parent;
 
     private final String name;
 
@@ -109,6 +116,10 @@ class JackrabbitNodeState extends Abstra
      */
     private final BundleLoader loader;
 
+    /**
+     * Workspace name used for versionable paths. This is null
+     * for the jcr:versionStorage and jcr:activities nodes.
+     */
     private final String workspaceName;
 
     private final TypePredicate isReferenceable;
@@ -136,9 +147,57 @@ class JackrabbitNodeState extends Abstra
 
     private final Map<String, PropertyState> properties;
 
+    private final Map<NodeId, JackrabbitNodeState> mountPoints;
+
+    private final Map<NodeId, JackrabbitNodeState> nodeStateCache;
+
+    private final List<String> ignoredPaths = ImmutableList.of("/jcr:system/jcr:nodeTypes");
+
+    public static JackrabbitNodeState createRootNodeState(
+            RepositoryContext context,
+            String workspaceName,
+            NodeState root,
+            Map<String, String> uriToPrefix,
+            Map<String, String> versionablePaths,
+            boolean copyBinariesByReference,
+            boolean skipOnError
+    ) throws RepositoryException {
+
+        final Map<NodeId, JackrabbitNodeState> emptyMountPoints = ImmutableMap.of();
+        final PersistenceManager versionPM = context.getInternalVersionManager().getPersistenceManager();
+        final JackrabbitNodeState versionStorage = new JackrabbitNodeState(
+                versionPM, root, uriToPrefix,
+                VERSION_STORAGE_NODE_ID, "/jcr:system/jcr:versionStorage",
+                null,
+                versionablePaths,
+                emptyMountPoints,
+                copyBinariesByReference,
+                skipOnError
+        );
+
+        final JackrabbitNodeState activities = new JackrabbitNodeState(
+                versionPM, root, uriToPrefix,
+                ACTIVITIES_NODE_ID, "/jcr:system/jcr:activities",
+                null,
+                versionablePaths,
+                emptyMountPoints,
+                copyBinariesByReference,
+                skipOnError
+        );
+
+
+        PersistenceManager pm = context.getWorkspaceInfo(workspaceName).getPersistenceManager();
+        final Map<NodeId, JackrabbitNodeState> mountPoints = ImmutableMap.of(
+                VERSION_STORAGE_NODE_ID, versionStorage,
+                ACTIVITIES_NODE_ID, activities
+        );
+        return new JackrabbitNodeState(
+                pm, root, uriToPrefix, ROOT_NODE_ID, "/",
+                workspaceName, versionablePaths, mountPoints, copyBinariesByReference, skipOnError);
+    }
+
     private JackrabbitNodeState(
-            JackrabbitNodeState parent, String name, NodePropBundle bundle,
-            boolean skipOnError) {
+            JackrabbitNodeState parent, String name, NodePropBundle bundle) {
         this.parent = parent;
         this.name = name;
         this.path = null;
@@ -154,7 +213,9 @@ class JackrabbitNodeState extends Abstra
         this.useBinaryReferences = parent.useBinaryReferences;
         this.properties = createProperties(bundle);
         this.nodes = createNodes(bundle);
-        this.skipOnError = skipOnError;
+        this.skipOnError = parent.skipOnError;
+        this.mountPoints = parent.mountPoints;
+        this.nodeStateCache = parent.nodeStateCache;
         setChildOrder();
         setVersionablePaths();
         fixFrozenUuid();
@@ -165,9 +226,10 @@ class JackrabbitNodeState extends Abstra
             PersistenceManager source, NodeState root,
             Map<String, String> uriToPrefix, NodeId id, String path,
             String workspaceName, Map<String, String> versionablePaths,
+            Map<NodeId, JackrabbitNodeState> mountPoints,
             boolean useBinaryReferences, boolean skipOnError) {
         this.parent = null;
-        this.name = null;
+        this.name = PathUtils.getName(path);
         this.path = path;
         this.loader = new BundleLoader(source);
         this.workspaceName = workspaceName;
@@ -178,6 +240,14 @@ class JackrabbitNodeState extends Abstra
         this.isFrozenNode = new TypePredicate(root, NT_FROZENNODE);
         this.uriToPrefix = uriToPrefix;
         this.versionablePaths = versionablePaths;
+        this.mountPoints = mountPoints;
+        final int cacheSize = 50; // cache size 50 results in > 25% cache hits during version copy
+        this.nodeStateCache = new LinkedHashMap<NodeId, JackrabbitNodeState>(cacheSize, 0.75f, true) {
+            @Override
+            protected boolean removeEldestEntry(Map.Entry<NodeId, JackrabbitNodeState> eldest) {
+                return size() >= cacheSize;
+            }
+        };
         this.useBinaryReferences = useBinaryReferences;
         this.skipOnError = skipOnError;
         try {
@@ -240,8 +310,7 @@ class JackrabbitNodeState extends Abstra
         NodeId id = nodes.get(name);
         if (id != null) {
             try {
-                return new JackrabbitNodeState(
-                        this, name, loader.loadBundle(id), skipOnError);
+                return createChildNodeState(id, name);
             } catch (ItemStateException e) {
                 if (!skipOnError) {
                     throw new IllegalStateException(
@@ -269,8 +338,7 @@ class JackrabbitNodeState extends Abstra
         for (Map.Entry<String, NodeId> entry : nodes.entrySet()) {
             String name = entry.getKey();
             try {
-                JackrabbitNodeState child = new JackrabbitNodeState(
-                        this, name, loader.loadBundle(entry.getValue()), skipOnError);
+                final JackrabbitNodeState child = createChildNodeState(entry.getValue(), name);
                 entries.add(new MemoryChildNodeEntry(name, child));
             } catch (ItemStateException e) {
                 warn("Skipping broken child node entry " + name, e);
@@ -287,6 +355,24 @@ class JackrabbitNodeState extends Abstra
 
     //-----------------------------------------------------------< private >--
 
+    private JackrabbitNodeState createChildNodeState(NodeId id, String name) throws ItemStateException {
+        if (mountPoints.containsKey(id)) {
+            final JackrabbitNodeState nodeState = mountPoints.get(id);
+            checkState(name.equals(nodeState.name),
+                    "Expected mounted node " + id + " to be called " + nodeState.name +
+                            " instead of " + name);
+            nodeState.parent = this;
+            return nodeState;
+        }
+
+        JackrabbitNodeState state = nodeStateCache.get(id);
+        if (state == null) {
+            state = new JackrabbitNodeState(this, name, loader.loadBundle(id));
+            nodeStateCache.put(id, state);
+        }
+        return state;
+    }
+
     private void setChildOrder() {
         if (isOrderable.apply(this)) {
             properties.put(OAK_CHILD_ORDER, PropertyStates.createProperty(
@@ -324,7 +410,10 @@ class JackrabbitNodeState extends Abstra
             for (int i = 2; children.containsKey(name); i++) {
                 name = base + '[' + i + ']';
             }
-            children.put(name, entry.getId());
+
+            if (!ignoredPaths.contains(PathUtils.concat(getPath(), name))) {
+                children.put(name, entry.getId());
+            }
         }
         return children;
     }
@@ -639,4 +728,4 @@ class JackrabbitNodeState extends Abstra
         log.warn(getPath() + ": " + message, cause);
     }
 
-}
+}
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java?rev=1694289&r1=1694288&r2=1694289&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java (original)
+++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java Wed Aug  5 19:15:56 2015
@@ -16,28 +16,32 @@
  */
 package org.apache.jackrabbit.oak.upgrade;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableSet.copyOf;
 import static com.google.common.collect.ImmutableSet.of;
 import static com.google.common.collect.Lists.newArrayList;
 import static com.google.common.collect.Lists.newArrayListWithCapacity;
 import static com.google.common.collect.Maps.newHashMap;
+import static com.google.common.collect.Sets.newHashSet;
+import static com.google.common.collect.Sets.union;
 import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM;
-import static org.apache.jackrabbit.core.RepositoryImpl.ACTIVITIES_NODE_ID;
-import static org.apache.jackrabbit.core.RepositoryImpl.ROOT_NODE_ID;
-import static org.apache.jackrabbit.core.RepositoryImpl.VERSION_STORAGE_NODE_ID;
 import static org.apache.jackrabbit.oak.plugins.name.Namespaces.addCustomMapping;
 import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.NODE_TYPES_PATH;
 import static org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants.JCR_ALL;
+import static org.apache.jackrabbit.oak.upgrade.nodestate.FilteringNodeState.ALL;
+import static org.apache.jackrabbit.oak.upgrade.nodestate.FilteringNodeState.NONE;
 
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -51,7 +55,6 @@ import javax.jcr.nodetype.NodeTypeTempla
 import javax.jcr.nodetype.PropertyDefinitionTemplate;
 import javax.jcr.security.Privilege;
 
-import com.google.common.base.Charsets;
 import com.google.common.base.Function;
 import com.google.common.base.Stopwatch;
 import com.google.common.collect.HashBiMap;
@@ -67,14 +70,11 @@ import org.apache.jackrabbit.core.config
 import org.apache.jackrabbit.core.fs.FileSystem;
 import org.apache.jackrabbit.core.fs.FileSystemException;
 import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
-import org.apache.jackrabbit.core.persistence.PersistenceManager;
 import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry;
 import org.apache.jackrabbit.core.security.user.UserManagerImpl;
 import org.apache.jackrabbit.oak.api.CommitFailedException;
-import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Root;
 import org.apache.jackrabbit.oak.api.Tree;
-import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.namepath.NamePathMapper;
 import org.apache.jackrabbit.oak.plugins.index.CompositeIndexEditorProvider;
 import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider;
@@ -104,7 +104,6 @@ import org.apache.jackrabbit.oak.spi.sec
 import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConfiguration;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
-import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
@@ -127,6 +126,12 @@ public class RepositoryUpgrade {
 
     private static final Logger logger = LoggerFactory.getLogger(RepositoryUpgrade.class);
 
+    public static final Set<String> DEFAULT_INCLUDE_PATHS = ALL;
+
+    public static final Set<String> DEFAULT_EXCLUDE_PATHS = NONE;
+
+    public static final Set<String> DEFAULT_MERGE_PATHS = NONE;
+
     /**
      * Source repository context.
      */
@@ -137,6 +142,24 @@ public class RepositoryUpgrade {
      */
     private final NodeStore target;
 
+    /**
+     * Paths to include during the copy process. Defaults to the root path "/".
+     */
+    private Set<String> includePaths = DEFAULT_INCLUDE_PATHS;
+
+    /**
+     * Paths to exclude during the copy process. Empty by default.
+     */
+    private Set<String> excludePaths = DEFAULT_EXCLUDE_PATHS;
+
+    /**
+     * Paths to merge during the copy process. Empty by default.
+     */
+    private Set<String> mergePaths = DEFAULT_MERGE_PATHS;
+
+    /**
+     * Whether or not to copy binaries by reference. Defaults to false.
+     */
     private boolean copyBinariesByReference = false;
 
     private boolean skipOnError = false;
@@ -234,6 +257,37 @@ public class RepositoryUpgrade {
     }
 
     /**
+     * Sets the paths that should be included when the source repository
+     * is copied to the target repository.
+     *
+     * @param includes Paths to be included in the copy.
+     */
+    public void setIncludes(@Nonnull String... includes) {
+        this.includePaths = copyOf(checkNotNull(includes));
+    }
+
+    /**
+     * Sets the paths that should be excluded when the source repository
+     * is copied to the target repository.
+     *
+     * @param excludes Paths to be excluded from the copy.
+     */
+    public void setExcludes(@Nonnull String... excludes) {
+        this.excludePaths = copyOf(checkNotNull(excludes));
+    }
+
+
+    /**
+     * Sets the paths that should be merged when the source repository
+     * is copied to the target repository.
+     *
+     * @param merges Paths to be merged during copy.
+     */
+    public void setMerges(@Nonnull String... merges) {
+        this.mergePaths = copyOf(checkNotNull(merges));
+    }
+
+    /**
      * Copies the full content from the source to the target repository.
      * <p>
      * The source repository <strong>must not be modified</strong> while
@@ -313,14 +367,22 @@ public class RepositoryUpgrade {
             Map<String, String> versionablePaths = newHashMap();
             NodeState root = builder.getNodeState();
 
+            final NodeState sourceState = JackrabbitNodeState.createRootNodeState(
+                    source, workspaceName, root, uriToPrefix, versionablePaths, copyBinariesByReference, skipOnError);
+
+            final Stopwatch watch = Stopwatch.createStarted();
+
             logger.info("Copying workspace content");
-            copyWorkspace(builder, root, workspaceName, uriToPrefix, versionablePaths);
-            logger.debug("Upgrading workspace content completed.");
+            copyWorkspace(sourceState, builder, workspaceName);
+            builder.getNodeState(); // on TarMK this does call triggers the actual copy
+            logger.info("Upgrading workspace content completed in {}s ({})", watch.elapsed(TimeUnit.SECONDS), watch);
 
+            watch.reset().start();
             logger.info("Copying version store content");
-            copyVersionStore(builder, root, workspaceName, uriToPrefix, versionablePaths);
-            logger.debug("Upgrading version store content completed.");
+            copyVersionStore(sourceState, builder);
+            logger.debug("Upgrading version store content completed in {}s ({}).", watch.elapsed(TimeUnit.SECONDS), watch);
 
+            watch.reset().start();
             logger.info("Applying default commit hooks");
             // TODO: default hooks?
             List<CommitHook> hooks = newArrayList();
@@ -334,7 +396,8 @@ public class RepositoryUpgrade {
             // hooks specific to the upgrade, need to run first
             hooks.add(new EditorHook(new CompositeEditorProvider(
                     new RestrictionEditorProvider(),
-                    new GroupEditorProvider(groupsPath))));
+                    new GroupEditorProvider(groupsPath)
+            )));
 
             // security-related hooks
             for (SecurityConfiguration sc : security.getConfigurations()) {
@@ -352,6 +415,7 @@ public class RepositoryUpgrade {
             )));
 
             target.merge(builder, new LoggingCompositeHook(hooks, source, earlyShutdown), CommitInfo.EMPTY);
+            logger.info("Processing commit hooks completed in {}s ({})", watch.elapsed(TimeUnit.SECONDS), watch);
             logger.debug("Repository upgrade completed.");
         } catch (Exception e) {
             throw new RepositoryException("Failed to copy content", e);
@@ -704,71 +768,40 @@ public class RepositoryUpgrade {
         return tmpl;
     }
 
-    private void copyVersionStore(
-            NodeBuilder builder, NodeState root, String workspaceName,
-            Map<String, String> uriToPrefix,
-            Map<String, String> versionablePaths) {
-        PersistenceManager pm = source.getInternalVersionManager().getPersistenceManager();
-        NodeBuilder system = builder.child(JCR_SYSTEM);
-
-        logger.info("Copying version histories");
-        copyState(system, "/jcr:system/jcr:versionStorage", new JackrabbitNodeState(
-                pm, root, uriToPrefix, VERSION_STORAGE_NODE_ID,
-                "/jcr:system/jcr:versionStorage",
-                workspaceName, versionablePaths, copyBinariesByReference, skipOnError),
-                true);
-
-        logger.info("Copying activities");
-        copyState(system, "/jcr:system/jcr:activities", new JackrabbitNodeState(
-                pm, root, uriToPrefix, ACTIVITIES_NODE_ID,
-                "/jcr:system/jcr:activities",
-                workspaceName, versionablePaths, copyBinariesByReference, skipOnError),
-                true);
-    }
-
-    private String copyWorkspace(
-            NodeBuilder builder, NodeState root, String workspaceName,
-            Map<String, String> uriToPrefix, Map<String, String> versionablePaths)
+    private void copyWorkspace(NodeState sourceState, NodeBuilder builder, String workspaceName)
             throws RepositoryException {
-        logger.info("Copying workspace {}", workspaceName);
-
-        PersistenceManager pm =
-                source.getWorkspaceInfo(workspaceName).getPersistenceManager();
+        final Set<String> includes = calculateEffectiveIncludePaths(sourceState);
+        final Set<String> excludes = union(copyOf(this.excludePaths), of("/jcr:system/jcr:versionStorage", "/jcr:system/jcr:activities"));
+        final Set<String> merges = union(copyOf(this.mergePaths), of("/jcr:system"));
 
-        NodeState state = new JackrabbitNodeState(
-                pm, root, uriToPrefix, ROOT_NODE_ID, "/",
-                workspaceName, versionablePaths, copyBinariesByReference, skipOnError);
+        logger.info("Copying workspace {} [i: {}, e: {}, m: {}]", workspaceName, includes, excludes, merges);
 
-        for (PropertyState property : state.getProperties()) {
-            builder.setProperty(property);
-        }
-        for (ChildNodeEntry child : state.getChildNodeEntries()) {
-            String childName = child.getName();
-            if (!JCR_SYSTEM.equals(childName)) {
-                final String path = PathUtils.concat("/", childName);
-                logger.info("Copying subtree {}", path);
-                copyState(builder, path, child.getNodeState(), false);
-            }
-        }
+        NodeStateCopier.builder()
+                .include(includes)
+                .exclude(excludes)
+                .merge(merges)
+                .copy(sourceState, builder);
+    }
 
-        return workspaceName;
+    private void copyVersionStore(NodeState sourceState, NodeBuilder builder)
+            throws RepositoryException {
+        NodeStateCopier.builder()
+                .include("/jcr:system/jcr:versionStorage", "/jcr:system/jcr:activities")
+                .merge("/jcr:system")
+                .copy(sourceState, builder);
     }
 
-    private void copyState(NodeBuilder targetParent, String path, NodeState source, boolean merge) {
-        final String name = PathUtils.getName(path);
-        // OAK-1589: maximum supported length of name for DocumentNodeStore
-        // is 150 bytes. Skip the sub tree if the the name is too long
-        if (name.length() > 37 && name.getBytes(Charsets.UTF_8).length > 150) {
-            logger.warn("Node name too long. Skipping {}", source);
-            return;
+    private Set<String> calculateEffectiveIncludePaths(NodeState state) {
+        if (!this.includePaths.contains("/")) {
+            return copyOf(this.includePaths);
+        }
+
+        // include child nodes from source individually to avoid deleting other initialized content
+        final Set<String> includes = newHashSet();
+        for (String childNodeName : state.getChildNodeNames()) {
+            includes.add("/" + childNodeName);
         }
-        NodeBuilder target = targetParent.child(name);
-        NodeStateCopier.copyNodeState(
-                source,
-                target,
-                path,
-                merge ? of(path) : Collections.<String>emptySet()
-        );
+        return includes;
     }
 
     private static class LoggingCompositeHook implements CommitHook {

Added: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeState.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeState.java?rev=1694289&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeState.java (added)
+++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeState.java Wed Aug  5 19:15:56 2015
@@ -0,0 +1,334 @@
+package org.apache.jackrabbit.oak.upgrade.nodestate;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryChildNodeEntry;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder;
+import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
+import org.apache.jackrabbit.oak.spi.state.AbstractNodeState;
+import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.Set;
+
+import static org.apache.jackrabbit.oak.plugins.tree.impl.TreeConstants.OAK_CHILD_ORDER;
+
+/**
+ * NodeState implementation that decorates another node-state instance
+ * in order to hide subtrees or partial subtrees from the consumer of
+ * the API.
+ * <br>
+ * The set of visible subtrees is defined by two parameters: include paths
+ * and exclude paths, both of which are sets of absolute paths.
+ * <br>
+ * Any paths that are equal or are descendants of one of the
+ * <b>excluded paths</b> are hidden by this implementation.
+ * <br>
+ * For all <b>included paths</b>, the direct ancestors, the node-state at
+ * the path itself and all descendants are visible. Any siblings of the
+ * defined path or its ancestors are implicitly hidden (unless made visible
+ * by another include path).
+ * <br>
+ * The implementation delegates to the decorated node-state instance and
+ * filters out hidden node-states in the following methods:
+ * <ul>
+ *     <li>{@link #exists()}</li>
+ *     <li>{@link #hasChildNode(String)}</li>
+ *     <li>{@link #getChildNodeEntries()}</li>
+ * </ul>
+ * Additionally, hidden node-state names are removed from the property
+ * {@code :childOrder} in the following two methods:
+ * <ul>
+ *     <li>{@link #getProperties()}</li>
+ *     <li>{@link #getProperty(String)}</li>
+ * </ul>
+ */
+public class FilteringNodeState extends AbstractNodeState {
+
+    public static final Set<String> ALL = ImmutableSet.of("/");
+
+    public static final Set<String> NONE = ImmutableSet.of();
+
+    private final NodeState delegate;
+
+    private final String path;
+
+    private final Set<String> includedPaths;
+
+    private final Set<String> excludedPaths;
+
+    /**
+     * Factory method that conditionally decorates the given node-state
+     * iff the node-state is (a) hidden itself or (b) has hidden descendants.
+     *
+     * @param path The path where the node-state should be assumed to be located.
+     * @param delegate The node-state to decorate.
+     * @param includePaths A Set of paths that should be visible. Defaults to ["/"] if {@code null).
+     * @param excludePaths A Set of paths that should be hidden. Empty if {@code null).
+     * @return The decorated node-state if required, the original node-state if decoration is unnecessary.
+     * @param excludePaths
+     */
+    @Nonnull
+    public static NodeState wrap(
+            @Nonnull final String path,
+            @Nonnull final NodeState delegate,
+            @Nullable final Set<String> includePaths,
+            @Nullable final Set<String> excludePaths
+    ) {
+        final Set<String> includes = defaultIfEmpty(includePaths, ALL);
+        final Set<String> excludes = defaultIfEmpty(excludePaths, NONE);
+        if (hasHiddenDescendants(path, includes, excludes)) {
+            return new FilteringNodeState(path, delegate, includes, excludes);
+        }
+        return delegate;
+    }
+
+    private FilteringNodeState(
+            @Nonnull final String path,
+            @Nonnull final NodeState delegate,
+            @Nonnull final Set<String> includedPaths,
+            @Nonnull final Set<String> excludedPaths
+    ) {
+        this.path = path;
+        this.delegate = delegate;
+        this.includedPaths = includedPaths;
+        this.excludedPaths = excludedPaths;
+    }
+
+    @Override
+    @Nonnull
+    public NodeBuilder builder() {
+        return new MemoryNodeBuilder(this);
+    }
+
+    @Override
+    public boolean exists() {
+        return !isHidden(path, includedPaths, excludedPaths) && delegate.exists();
+    }
+
+    @Override
+    @Nonnull
+    public NodeState getChildNode(@Nonnull final String name) throws IllegalArgumentException {
+        final String childPath = PathUtils.concat(path, name);
+        return wrap(childPath, delegate.getChildNode(name), includedPaths, excludedPaths);
+    }
+
+    @Override
+    public boolean hasChildNode(@Nonnull final String name) {
+        final String childPath = PathUtils.concat(path, name);
+        return !isHidden(childPath, includedPaths, excludedPaths) && delegate.hasChildNode(name);
+    }
+
+    @Override
+    @Nonnull
+    public Iterable<? extends ChildNodeEntry> getChildNodeEntries() {
+        final Iterable<ChildNodeEntry> transformed = Iterables.transform(
+                delegate.getChildNodeEntries(),
+                new Function<ChildNodeEntry, ChildNodeEntry>() {
+                    @Nullable
+                    @Override
+                    public ChildNodeEntry apply(@Nullable final ChildNodeEntry childNodeEntry) {
+                        if (childNodeEntry != null) {
+                            final String name = childNodeEntry.getName();
+                            final String childPath = PathUtils.concat(path, name);
+                            if (!isHidden(childPath, includedPaths, excludedPaths)) {
+                                final NodeState nodeState = childNodeEntry.getNodeState();
+                                final NodeState state = wrap(childPath, nodeState, includedPaths, excludedPaths);
+                                return new MemoryChildNodeEntry(name, state);
+                            }
+                        }
+                        return null;
+                    }
+                }
+        );
+        return Iterables.filter(transformed, new Predicate<ChildNodeEntry>() {
+            @Override
+            public boolean apply(@Nullable final ChildNodeEntry childNodeEntry) {
+                return childNodeEntry != null;
+            }
+        });
+    }
+
+    @Override
+    public long getPropertyCount() {
+        return delegate.getPropertyCount();
+    }
+
+    @Override
+    @Nonnull
+    public Iterable<? extends PropertyState> getProperties() {
+        return Iterables.transform(delegate.getProperties(), new Function<PropertyState, PropertyState>() {
+            @Nullable
+            @Override
+            public PropertyState apply(@Nullable final PropertyState propertyState) {
+                return fixChildOrderPropertyState(propertyState);
+            }
+        });
+    }
+
+    @Override
+    public PropertyState getProperty(String name) {
+        return fixChildOrderPropertyState(delegate.getProperty(name));
+    }
+
+    @Override
+    public boolean hasProperty(String name) {
+        return delegate.getProperty(name) != null;
+    }
+
+    /**
+     * Utility method to fix the PropertyState of properties called {@code :childOrder}.
+     *
+     * @param propertyState A property-state.
+     * @return The original property-state or if the property name is {@code :childOrder}, a
+     *         property-state with hidden child names removed from the value.
+     */
+    @CheckForNull
+    private PropertyState fixChildOrderPropertyState(@Nullable final PropertyState propertyState) {
+        if (propertyState != null && OAK_CHILD_ORDER.equals(propertyState.getName())) {
+            final Iterable<String> values = Iterables.filter(propertyState.getValue(Type.NAMES), new Predicate<String>() {
+                @Override
+                public boolean apply(@Nullable final String name) {
+                    if (name == null) {
+                        return false;
+                    }
+                    final String childPath = PathUtils.concat(path, name);
+                    return !isHidden(childPath, includedPaths, excludedPaths);
+                }
+            });
+            return PropertyStates.createProperty(OAK_CHILD_ORDER, values, Type.NAMES);
+        }
+        return propertyState;
+    }
+
+    /**
+     * Utility method to determine whether a given path should is hidden given the
+     * include paths and exclude paths.
+     *
+     * @param path Path to be checked
+     * @param includes Include paths
+     * @param excludes Exclude paths
+     * @return Whether the {@code path} is hidden or not.
+     */
+    public static boolean isHidden(
+            @Nonnull final String path,
+            @Nonnull final Set<String> includes,
+            @Nonnull final Set<String> excludes
+    ) {
+        return isExcluded(path, excludes) || !isIncluded(path, includes);
+    }
+
+    /**
+     * Utility method to determine whether the path itself or any of its descendants should
+     * be hidden given the include paths and exclude paths.
+     *
+     * @param path Path to be checked
+     * @param includePaths Include paths
+     * @param excludePaths Exclude paths
+     * @return Whether the {@code path} or any of its descendants are hidden or not.
+     */
+    private static boolean hasHiddenDescendants(
+            @Nonnull final String path,
+            @Nonnull final Set<String> includePaths,
+            @Nonnull final Set<String> excludePaths
+    ) {
+        return isHidden(path, includePaths, excludePaths)
+                || isAncestorOfAnyPath(path, excludePaths)
+                || isAncestorOfAnyPath(path, includePaths);
+    }
+
+    /**
+     * Utility method to check whether a given set of include paths cover the given
+     * {@code path}. I.e. whether the path is visible or implicitly hidden due to the
+     * lack of a matching include path.
+     * <br>
+     * Note: the ancestors of every include path are considered visible.
+     *
+     * @param path Path to be checked
+     * @param includePaths Include paths
+     * @return Whether the path is covered by the include paths or not.
+     */
+    private static boolean isIncluded(@Nonnull final String path, @Nonnull final Set<String> includePaths) {
+        return isAncestorOfAnyPath(path, includePaths)
+                || includePaths.contains(path)
+                || isDescendantOfAnyPath(path, includePaths);
+    }
+
+    /**
+     * Utility method to check whether a given set of exclude paths cover the given
+     * {@code path}. I.e. whether the path is hidden due to the presence of a
+     * matching exclude path.
+     *
+     * @param path Path to be checked
+     * @param excludePaths Exclude paths
+     * @return Whether the path is covered by the excldue paths or not.
+     */
+    private static boolean isExcluded(@Nonnull final String path, @Nonnull final Set<String> excludePaths) {
+        return excludePaths.contains(path) || isDescendantOfAnyPath(path, excludePaths);
+    }
+
+    /**
+     * Utility method to check whether any of the provided {@code paths} is a descendant
+     * of the given ancestor path.
+     *
+     * @param ancestor Ancestor path
+     * @param paths Paths that may be descendants of {@code ancestor}.
+     * @return true if {@code paths} contains a descendant of {@code ancestor}, false otherwise.
+     */
+    private static boolean isAncestorOfAnyPath(@Nonnull final String ancestor, @Nonnull final Set<String> paths) {
+        for (final String p : paths) {
+            if (PathUtils.isAncestor(ancestor, p)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Utility method to check whether any of the provided {@code paths} is an ancestor
+     * of the given descendant path.
+     *
+     * @param descendant Descendant path
+     * @param paths Paths that may be ancestors of {@code descendant}.
+     * @return true if {@code paths} contains an ancestor of {@code descendant}, false otherwise.
+     */
+    private static boolean isDescendantOfAnyPath(@Nonnull final String descendant, @Nonnull final Set<String> paths) {
+        for (final String p : paths) {
+            if (PathUtils.isAncestor(p, descendant)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Utility method to return the given {@code Set} if it is not empty and a default Set otherwise.
+     *
+     * @param value Value to check for emptiness
+     * @param defaultValue Default value
+     * @return return the given {@code Set} if it is not empty and a default Set otherwise
+     */
+    @Nonnull
+    private static <T> Set<T> defaultIfEmpty(@Nullable Set<T> value, @Nonnull Set<T> defaultValue) {
+        return !isEmpty(value) ? value : defaultValue;
+    }
+
+    /**
+     * Utility method to check whether a Set is empty, i.e. null or of size 0.
+     *
+     * @param set The Set to check.
+     * @return true if empty, false otherwise
+     */
+    private static <T> boolean isEmpty(@Nullable final Set<T> set) {
+        return set == null || set.isEmpty();
+    }
+}

Modified: jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java?rev=1694289&r1=1694288&r2=1694289&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java (original)
+++ jackrabbit/oak/trunk/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java Wed Aug  5 19:15:56 2015
@@ -16,24 +16,29 @@
  */
 package org.apache.jackrabbit.oak.upgrade.nodestate;
 
+import com.google.common.base.Charsets;
 import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
+import org.apache.jackrabbit.oak.spi.commit.CommitHook;
 import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
 import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
 import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nonnull;
-import java.util.Collections;
 import java.util.Set;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableSet.copyOf;
+import static com.google.common.collect.ImmutableSet.of;
+import static java.util.Collections.emptySet;
 
 /**
  * The NodeStateCopier and NodeStateCopier.Builder classes allow
@@ -49,14 +54,46 @@ import static com.google.common.base.Pre
  * The work for a traversal without any differences between
  * {@code source} and {@code target} is equivalent to the single
  * execution of a naive equals implementation.
+ * <br>
+ * <b>Usage:</b> For most use-cases the Builder API should be
+ * preferred. It allows setting {@code includePaths},
+ * {@code excludePaths} and {@code mergePaths}.
+ * <br>
+ * <b>Include paths:</b> if include paths are set, only these paths
+ * and their sub-trees are copied. Any nodes that are not within the
+ * scope of an include path are <i>implicitly excluded</i>.
+ * <br>
+ * <b>Exclude paths:</b> if exclude paths are set, any nodes matching
+ * or below the excluded path are not copied. If an excluded node does
+ * exist in the target, it is removed (see also merge paths).
+ * <b>Merge paths:</b> if merge paths are set, any nodes matching or
+ * below the merged path will not be deleted from target, even if they
+ * are missing in (or excluded from) the source.
  */
 public class NodeStateCopier {
 
     private static final Logger LOG = LoggerFactory.getLogger(NodeStateCopier.class);
 
+    private final Set<String> includePaths;
+
+    private final Set<String> excludePaths;
+
+    private final Set<String> mergePaths;
 
-    private NodeStateCopier() {
-        // no instances
+    private NodeStateCopier(Set<String> includePaths, Set<String> excludePaths, Set<String> mergePaths) {
+        this.includePaths = includePaths;
+        this.excludePaths = excludePaths;
+        this.mergePaths = mergePaths;
+    }
+
+    /**
+     * Create a NodeStateCopier.Builder.
+     *
+     * @return a NodeStateCopier.Builder
+     * @see org.apache.jackrabbit.oak.upgrade.nodestate.NodeStateCopier.Builder
+     */
+    public static Builder builder() {
+        return new Builder();
     }
 
     /**
@@ -66,15 +103,11 @@ public class NodeStateCopier {
      * @param source NodeStore to copy from.
      * @param target NodeStore to copy to.
      * @throws CommitFailedException
+     * @see org.apache.jackrabbit.oak.upgrade.nodestate.NodeStateCopier.Builder#copy(NodeStore, NodeStore)
      */
     public static boolean copyNodeStore(@Nonnull final NodeStore source, @Nonnull final NodeStore target)
             throws CommitFailedException {
-        final NodeBuilder builder = checkNotNull(target).getRoot().builder();
-        final boolean hasChanges = copyNodeState(checkNotNull(source).getRoot(), builder, "/", Collections.<String>emptySet());
-        if (hasChanges) {
-            source.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
-        }
-        return hasChanges;
+        return builder().copy(checkNotNull(source), checkNotNull(target));
     }
 
     /**
@@ -107,6 +140,21 @@ public class NodeStateCopier {
         return hasChanges;
     }
 
+
+    private boolean copyNodeState(@Nonnull final NodeState source, @Nonnull final NodeBuilder target) {
+        final NodeState wrappedSource = FilteringNodeState.wrap("/", source, this.includePaths, this.excludePaths);
+        boolean hasChanges = false;
+        for (String includePath : this.includePaths) {
+            hasChanges = copyMissingAncestors(source, target, includePath) || hasChanges;
+            final NodeState sourceState = NodeStateUtils.getNode(wrappedSource, includePath);
+            if (sourceState.exists()) {
+                final NodeBuilder targetBuilder = getChildNodeBuilder(target, includePath);
+                hasChanges = copyNodeState(sourceState, targetBuilder, includePath, this.mergePaths) || hasChanges;
+            }
+        }
+        return hasChanges;
+    }
+
     /**
      * Recursively copies the source NodeState to the target NodeBuilder.
      * <br>
@@ -124,8 +172,8 @@ public class NodeStateCopier {
      *                   preserved, even if the do not exist in the source.
      * @return An indication of whether there were changes or not.
      */
-    public static boolean copyNodeState(@Nonnull final NodeState source, @Nonnull final NodeBuilder target,
-                                        @Nonnull final String currentPath, @Nonnull final Set<String> mergePaths) {
+    private static boolean copyNodeState(@Nonnull final NodeState source, @Nonnull final NodeBuilder target,
+                                         @Nonnull final String currentPath, @Nonnull final Set<String> mergePaths) {
 
 
         boolean hasChanges = false;
@@ -140,6 +188,12 @@ public class NodeStateCopier {
 
         for (ChildNodeEntry child : source.getChildNodeEntries()) {
             final String childName = child.getName();
+            // OAK-1589: maximum supported length of name for DocumentNodeStore
+            // is 150 bytes. Skip the sub tree if the the name is too long
+            if (childName.length() > 37 && childName.getBytes(Charsets.UTF_8).length > 150) {
+                LOG.warn("Node name too long. Skipping {}", source);
+                continue;
+            }
             final NodeState childSource = child.getNodeState();
             if (!target.hasChildNode(childName)) {
                 // add new children
@@ -170,4 +224,208 @@ public class NodeStateCopier {
         }
         return false;
     }
+
+    /**
+     * Ensure that all ancestors of {@code path} are present in {@code target}. Copies any
+     * missing ancestors from {@code source}.
+     *
+     * @param source NodeState to copy from
+     * @param target NodeBuilder to copy to
+     * @param path The path along which ancestors should be copied.
+     */
+    private static boolean copyMissingAncestors(final NodeState source, final NodeBuilder target, final String path) {
+        NodeState current = source;
+        NodeBuilder currentBuilder = target;
+        boolean hasChanges = false;
+        for (String name : PathUtils.elements(path)) {
+            if (current.hasChildNode(name)) {
+                final boolean targetHasChild = currentBuilder.hasChildNode(name);
+                current = current.getChildNode(name);
+                currentBuilder = currentBuilder.child(name);
+                if (!targetHasChild) {
+                    hasChanges = copyProperties(current, currentBuilder) || hasChanges;
+                }
+            }
+        }
+        return hasChanges;
+    }
+
+    /**
+     * Allows retrieving a NodeBuilder by path relative to the given root NodeBuilder.
+     *
+     * All NodeBuilders are created via {@link NodeBuilder#child(String)} and are thus
+     * implicitly created.
+     *
+     * @param root The NodeBuilder to consider the root node.
+     * @param path An absolute or relative path, which is evaluated as a relative path under the root NodeBuilder.
+     * @return a NodeBuilder instance, never null
+     */
+    @Nonnull
+    private static NodeBuilder getChildNodeBuilder(@Nonnull final NodeBuilder root, @Nonnull final String path) {
+        NodeBuilder child = root;
+        for (String name : PathUtils.elements(path)) {
+            child = child.child(name);
+        }
+        return child;
+    }
+
+    /**
+     * The NodeStateCopier.Builder allows configuring a NodeState copy operation with
+     * {@code includePaths}, {@code excludePaths} and {@code mergePaths}.
+     * <br>
+     * <b>Include paths</b> can define which paths should be copied from the source to the
+     * target.
+     * <br>
+     * <b>Exclude paths</b> allow restricting which paths should be copied. This is
+     * especially useful when there are individual nodes in an included path that
+     * should not be copied.
+     * <br>
+     * By default copying will remove items that already exist in the target but do
+     * not exist in the source. If this behaviour is undesired that is where merge
+     * paths come in.
+     * <br>
+     * <b>Merge paths</b> dictate in which parts of the tree the copy operation should
+     * be <i>additive</i>, i.e. the content from source is merged with the content
+     * in the target. Nodes that are present in the target but not in the source are
+     * then not deleted. However, in the case where nodes are present in both the source
+     * and the target, the node from the source is copied with its properties and any
+     * properties previously present on the target's node are lost.
+     * <br>
+     * Finally, using one of the {@code copy} methods, NodeStores or NodeStates can
+     * be copied.
+     */
+    public static class Builder {
+
+        private Set<String> includePaths = of("/");
+
+        private Set<String> excludePaths = emptySet();
+
+        private Set<String> mergePaths = emptySet();
+
+        private Builder() {}
+
+
+        /**
+         * Set include paths.
+         *
+         * @param paths include paths
+         * @return this Builder instance
+         * @see NodeStateCopier#NodeStateCopier(Set, Set, Set)
+         */
+        @Nonnull
+        public Builder include(@Nonnull Set<String> paths) {
+            if (!checkNotNull(paths).isEmpty()) {
+                this.includePaths = copyOf(paths);
+            }
+            return this;
+        }
+
+        /**
+         * Convenience wrapper for {@link #include(Set)}.
+         *
+         * @param paths include paths
+         * @return this Builder instance
+         * @see NodeStateCopier#NodeStateCopier(Set, Set, Set)
+         */
+        @Nonnull
+        public Builder include(@Nonnull String... paths) {
+            return include(copyOf(checkNotNull(paths)));
+        }
+
+        /**
+         * Set exclude paths.
+         *
+         * @param paths exclude paths
+         * @return this Builder instance
+         * @see NodeStateCopier#NodeStateCopier(Set, Set, Set)
+         */
+        @Nonnull
+        public Builder exclude(@Nonnull Set<String> paths) {
+            if (!checkNotNull(paths).isEmpty()) {
+                this.excludePaths = copyOf(paths);
+            }
+            return this;
+        }
+
+        /**
+         * Convenience wrapper for {@link #exclude(Set)}.
+         *
+         * @param paths exclude paths
+         * @return this Builder instance
+         * @see NodeStateCopier#NodeStateCopier(Set, Set, Set)
+         */
+        @Nonnull
+        public Builder exclude(@Nonnull String... paths) {
+            return exclude(copyOf(checkNotNull(paths)));
+        }
+
+        /**
+         * Set merge paths.
+         *
+         * @param paths merge paths
+         * @return this Builder instance
+         * @see NodeStateCopier#NodeStateCopier(Set, Set, Set)
+         */
+        @Nonnull
+        public Builder merge(@Nonnull Set<String> paths) {
+            if (!checkNotNull(paths).isEmpty()) {
+                this.mergePaths = copyOf(paths);
+            }
+            return this;
+        }
+
+        /**
+         * Convenience wrapper for {@link #merge(Set)}.
+         *
+         * @param paths merge paths
+         * @return this Builder instance
+         * @see NodeStateCopier#NodeStateCopier(Set, Set, Set)
+         */
+        @Nonnull
+        public Builder merge(@Nonnull String... paths) {
+            return merge(copyOf(checkNotNull(paths)));
+        }
+
+        /**
+         * Creates a NodeStateCopier to copy the {@code source} NodeState to the
+         * {@code target} NodeBuilder, using any include, exclude and merge paths
+         * set on this NodeStateCopier.Builder.
+         * <br>
+         * It is the responsibility of the caller to persist any changes using e.g.
+         * {@link NodeStore#merge(NodeBuilder, CommitHook, CommitInfo)}.
+         *
+         * @param source NodeState to copy from
+         * @param target NodeBuilder to copy to
+         * @return true if there were any changes, false if source and target represent
+         *         the same content
+         */
+        public boolean copy(@Nonnull final NodeState source, @Nonnull final NodeBuilder target) {
+            final NodeStateCopier copier = new NodeStateCopier(includePaths, excludePaths, mergePaths);
+            return copier.copyNodeState(checkNotNull(source), checkNotNull(target));
+        }
+
+        /**
+         * Creates a NodeStateCopier to copy the {@code source} NodeStore to the
+         * {@code target} NodeStore, using any include, exclude and merge paths
+         * set on this NodeStateCopier.Builder.
+         * <br>
+         * Changes are automatically persisted with empty CommitHooks and CommitInfo
+         * via {@link NodeStore#merge(NodeBuilder, CommitHook, CommitInfo)}.
+         *
+         * @param source NodeStore to copy from
+         * @param target NodeStore to copy to
+         * @return true if there were any changes, false if source and target represent
+         *         the same content
+         * @throws CommitFailedException
+         */
+        public boolean copy(@Nonnull final NodeStore source, @Nonnull final NodeStore target)
+                throws CommitFailedException {
+            final NodeBuilder targetBuilder = checkNotNull(target).getRoot().builder();
+            if (copy(checkNotNull(source).getRoot(), targetBuilder)) {
+                target.merge(targetBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+                return true;
+            }
+            return false;
+        }
+    }
 }

Added: jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/IncludeExcludeUpgradeTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/IncludeExcludeUpgradeTest.java?rev=1694289&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/IncludeExcludeUpgradeTest.java (added)
+++ jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/IncludeExcludeUpgradeTest.java Wed Aug  5 19:15:56 2015
@@ -0,0 +1,87 @@
+package org.apache.jackrabbit.oak.upgrade;
+
+import org.apache.jackrabbit.commons.JcrUtils;
+import org.apache.jackrabbit.core.RepositoryContext;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.Test;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import java.io.File;
+
+public class IncludeExcludeUpgradeTest extends AbstractRepositoryUpgradeTest {
+
+    @Override
+    protected void createSourceContent(Repository repository) throws Exception {
+        final Session session = repository.login(CREDENTIALS);
+        JcrUtils.getOrCreateByPath("/content/foo/de", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/foo/en", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/foo/fr", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/foo/it", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/assets/foo", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/assets/foo/2015", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/assets/foo/2015/02", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/assets/foo/2015/01", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/assets/foo/2014", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/assets/foo/2013", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/assets/foo/2012", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/assets/foo/2011", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/assets/foo/2010", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/assets/foo/2010/12", "nt:folder", session);
+        JcrUtils.getOrCreateByPath("/content/assets/foo/2010/11", "nt:folder", session);
+        session.save();
+    }
+
+    @Override
+    protected void doUpgradeRepository(File source, NodeStore target) throws RepositoryException {
+        final RepositoryConfig config = RepositoryConfig.create(source);
+        final RepositoryContext context = RepositoryContext.create(config);
+        try {
+            final RepositoryUpgrade upgrade = new RepositoryUpgrade(context, target);
+            upgrade.setIncludes(
+                    "/content/foo/en",
+                    "/content/assets/foo"
+            );
+            upgrade.setExcludes(
+                    "/content/assets/foo/2013",
+                    "/content/assets/foo/2012",
+                    "/content/assets/foo/2011",
+                    "/content/assets/foo/2010"
+            );
+            upgrade.copy(null);
+        } finally {
+            context.getRepository().shutdown();
+        }
+    }
+
+    @Test
+    public void shouldHaveIncludedPaths() throws RepositoryException {
+        assertExisting(
+                "/content/foo/en",
+                "/content/assets/foo/2015/02",
+                "/content/assets/foo/2015/01",
+                "/content/assets/foo/2014"
+        );
+    }
+
+    @Test
+    public void shouldLackPathsThatWereNotIncluded() throws RepositoryException {
+        assertMissing(
+                "/content/foo/de",
+                "/content/foo/fr",
+                "/content/foo/it"
+        );
+    }
+
+    @Test
+    public void shouldLackExcludedPaths() throws RepositoryException {
+        assertMissing(
+                "/content/assets/foo/2013",
+                "/content/assets/foo/2012",
+                "/content/assets/foo/2011",
+                "/content/assets/foo/2010"
+        );
+    }
+}

Added: jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/UpgradeFromTwoSourcesTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/UpgradeFromTwoSourcesTest.java?rev=1694289&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/UpgradeFromTwoSourcesTest.java (added)
+++ jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/UpgradeFromTwoSourcesTest.java Wed Aug  5 19:15:56 2015
@@ -0,0 +1,155 @@
+package org.apache.jackrabbit.oak.upgrade;
+
+import org.apache.jackrabbit.commons.JcrUtils;
+import org.apache.jackrabbit.core.RepositoryContext;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.apache.jackrabbit.oak.plugins.segment.SegmentNodeStore;
+import org.apache.jackrabbit.oak.plugins.segment.file.FileStore;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Test case that simulates copying different paths from two source repositories
+ * into a single target repository.
+ */
+public class UpgradeFromTwoSourcesTest extends AbstractRepositoryUpgradeTest {
+
+    private static boolean upgradeComplete;
+    private static FileStore fileStore;
+
+    @Override
+    protected NodeStore createTargetNodeStore() {
+        return new SegmentNodeStore(fileStore);
+    }
+
+    @BeforeClass
+    public static void initialize() {
+        final File dir = new File(getTestDirectory(), "segments");
+        dir.mkdirs();
+        try {
+            fileStore = FileStore.newFileStore(dir).withMaxFileSize(128).create();
+            upgradeComplete = false;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @AfterClass
+    public static void cleanup() {
+        fileStore.close();
+        fileStore = null;
+    }
+
+    @Before
+    public synchronized void upgradeRepository() throws Exception {
+        if (!upgradeComplete) {
+            final File sourceDir1 = new File(getTestDirectory(), "source1");
+            final File sourceDir2 = new File(getTestDirectory(), "source2");
+
+            sourceDir1.mkdirs();
+            sourceDir2.mkdirs();
+
+            final RepositoryImpl source1 = createSourceRepository(sourceDir1);
+            final RepositoryImpl source2 = createSourceRepository(sourceDir2);
+
+            try {
+                createSourceContent(source1);
+                createSourceContent2(source2);
+            } finally {
+                source1.shutdown();
+                source2.shutdown();
+            }
+
+            final NodeStore target = getTargetNodeStore();
+            doUpgradeRepository(sourceDir1, target, "/left");
+            doUpgradeRepository(sourceDir2, target, "/right", "/left/child2", "/left/child3");
+            fileStore.flush();
+            upgradeComplete = true;
+        }
+    }
+
+    private void doUpgradeRepository(File source, NodeStore target, String... includes) throws RepositoryException {
+        final RepositoryConfig config = RepositoryConfig.create(source);
+        final RepositoryContext context = RepositoryContext.create(config);
+        try {
+            final RepositoryUpgrade upgrade = new RepositoryUpgrade(context, target);
+            upgrade.setIncludes(includes);
+            upgrade.copy(null);
+        } finally {
+            context.getRepository().shutdown();
+        }
+    }
+
+    @Override
+    protected void createSourceContent(Repository repository) throws RepositoryException {
+        Session session = null;
+        try {
+            session = repository.login(CREDENTIALS);
+
+            JcrUtils.getOrCreateByPath("/left/child1/grandchild1", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/left/child1/grandchild2", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/left/child1/grandchild3", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/left/child2/grandchild1", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/left/child2/grandchild2", "nt:unstructured", session);
+
+            session.save();
+        } finally {
+            if (session != null && session.isLive()) {
+                session.logout();
+            }
+        }
+    }
+
+    protected void createSourceContent2(Repository repository) throws RepositoryException {
+        Session session = null;
+        try {
+            session = repository.login(CREDENTIALS);
+
+            JcrUtils.getOrCreateByPath("/left/child2/grandchild3", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/left/child2/grandchild2", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/left/child3", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/right/child1/grandchild1", "nt:unstructured", session);
+            JcrUtils.getOrCreateByPath("/right/child1/grandchild2", "nt:unstructured", session);
+
+            session.save();
+        } finally {
+            if (session != null && session.isLive()) {
+                session.logout();
+            }
+        }
+    }
+
+    @Test
+    public void shouldContainNodesFromBothSources() throws Exception {
+        assertExisting(
+                "/",
+                "/left",
+                "/left/child1",
+                "/left/child2",
+                "/left/child3",
+                "/left/child1/grandchild1",
+                "/left/child1/grandchild2",
+                "/left/child1/grandchild3",
+                "/left/child2/grandchild2",
+                "/left/child2/grandchild3",
+                "/right",
+                "/right/child1",
+                "/right/child1/grandchild1",
+                "/right/child1/grandchild2"
+        );
+
+        assertMissing(
+                "/left/child2/grandchild1"
+        );
+    }
+}
\ No newline at end of file

Added: jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeStateTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeStateTest.java?rev=1694289&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeStateTest.java (added)
+++ jackrabbit/oak/trunk/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeStateTest.java Wed Aug  5 19:15:56 2015
@@ -0,0 +1,304 @@
+package org.apache.jackrabbit.oak.upgrade.nodestate;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.jcr.RepositoryException;
+import java.util.Set;
+
+import static com.google.common.collect.ImmutableSet.of;
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Arrays.asList;
+import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty;
+import static org.apache.jackrabbit.oak.plugins.tree.impl.TreeConstants.OAK_CHILD_ORDER;
+import static org.apache.jackrabbit.oak.upgrade.nodestate.FilteringNodeState.wrap;
+import static org.apache.jackrabbit.oak.upgrade.util.NodeStateTestUtils.assertExists;
+import static org.apache.jackrabbit.oak.upgrade.util.NodeStateTestUtils.assertMissing;
+import static org.apache.jackrabbit.oak.upgrade.util.NodeStateTestUtils.commit;
+import static org.apache.jackrabbit.oak.upgrade.util.NodeStateTestUtils.create;
+import static org.apache.jackrabbit.oak.upgrade.util.NodeStateTestUtils.createNodeStoreWithContent;
+import static org.apache.jackrabbit.oak.upgrade.util.NodeStateTestUtils.getNodeState;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class FilteringNodeStateTest {
+
+    private static final Set<String> DEFAULT_INCLUDES = FilteringNodeState.ALL;
+
+    private static final Set<String> DEFAULT_EXCLUDES = FilteringNodeState.NONE;
+
+    private NodeState rootNodeState;
+
+    @Before
+    public void setup() throws RepositoryException, CommitFailedException {
+        final NodeStore nodeStore = createNodeStoreWithContent(
+                "/content/foo/de",
+                "/content/foo/en",
+                "/content/football/en",
+                "/apps/foo/install",
+                "/libs/foo/install"
+        );
+
+        final PropertyState childOrder = createProperty(OAK_CHILD_ORDER, asList("foo", "football"), Type.NAMES);
+        final NodeBuilder builder = nodeStore.getRoot().builder();
+        create(builder, "/content", childOrder);
+        commit(nodeStore, builder);
+
+        rootNodeState = nodeStore.getRoot();
+    }
+
+    @Test
+    public void shouldNotDecorateForNullArgs() {
+        final NodeState decorated = wrap("/", rootNodeState, null, null);
+        assertSame("root should be identical to decorated", rootNodeState, decorated);
+    }
+
+    @Test
+    public void shouldNotDecorateForDefaultIncludes() {
+        final NodeState decorated = wrap("/", rootNodeState, DEFAULT_INCLUDES, null);
+        assertSame("root should be identical to decorated", rootNodeState, decorated);
+    }
+
+    @Test
+    public void shouldNotDecorateForDefaultExcludes() {
+        final NodeState decorated = wrap("/", rootNodeState, null, DEFAULT_EXCLUDES);
+        assertSame("root should be identical to decorated", rootNodeState, decorated);
+    }
+
+    @Test
+    public void shouldNotDecorateForDefaultIncludesAndExcludes() {
+        final NodeState decorated = wrap("/", rootNodeState, DEFAULT_INCLUDES, DEFAULT_EXCLUDES);
+        assertSame("root should be identical to decorated", rootNodeState, decorated);
+    }
+
+    @Test
+    public void shouldNotDecorateIncludedPath() {
+        final NodeState content = getNodeState(rootNodeState, "/content");
+        final NodeState decorated = wrap("/content", content, of("/content"), null);
+        assertSame("content should be identical to decorated", content, decorated);
+    }
+
+    @Test
+    public void shouldNotDecorateIncludedDescendants() {
+        final NodeState foo = getNodeState(rootNodeState, "/content/foo");
+        final NodeState decorated = wrap("/content/foo", foo, of("/content"), null);
+        assertSame("foo should be identical to decorated", foo, decorated);
+    }
+
+    @Test
+    public void shouldDecorateAncestorOfExcludedDescendants() {
+        final NodeState foo = getNodeState(rootNodeState, "/content/foo");
+        final NodeState decorated = wrap("/content/foo", foo, of("/content"), of("/content/foo/de"));
+        assertNotSame("foo should not be identical to decorated", foo, decorated);
+
+        assertMissing(decorated, "de");
+        assertExists(decorated, "en");
+        assertFalse("child nodes \"de\" should not be equal",
+                getNodeState(foo, "de").equals(getNodeState(decorated, "de")));
+
+        final NodeState en = getNodeState(decorated, "en");
+        assertEquals("child nodes \"en\" should be equal", getNodeState(foo, "en"), en);
+        assertTrue("child node \"en\" should not be decorated", !(en instanceof FilteringNodeState));
+    }
+
+    @Test
+    public void shouldHaveCorrectChildOrderProperty() throws CommitFailedException {
+        final NodeState content = rootNodeState.getChildNode("content");
+        final NodeState decorated = wrap("/content", content, null, of("/content/foo"));
+
+        assertTrue(decorated.hasProperty(OAK_CHILD_ORDER));
+
+        { // access via getProperty()
+            final PropertyState childOrder = decorated.getProperty(OAK_CHILD_ORDER);
+            final Iterable<String> values = childOrder.getValue(Type.STRINGS);
+            assertEquals(newArrayList("football"), newArrayList(values));
+        }
+
+        { // access via getProperties()
+            final Predicate<PropertyState> isChildOrderProperty = new Predicate<PropertyState>() {
+                @Override
+                public boolean apply(PropertyState propertyState) {
+                    return OAK_CHILD_ORDER.equals(propertyState.getName());
+                }
+            };
+            final PropertyState childOrder = Iterables.find(decorated.getProperties(), isChildOrderProperty);
+            final Iterable<String> values = childOrder.getValue(Type.STRINGS);
+            assertEquals(newArrayList("football"), newArrayList(values));
+        }
+    }
+
+    @Test
+    public void shouldDecorateExcludedNode() {
+        final NodeState decoratedRoot = wrap("/", rootNodeState, of("/content"), of("/content/foo/de"));
+        final NodeState de = getNodeState(rootNodeState, "/content/foo/de");
+        final NodeState decorated = getNodeState(decoratedRoot, "/content/foo/de");
+        assertFalse("de should not be equal to decorated", de.equals(decorated));
+        assertFalse("decorated should not exist", decorated.exists());
+    }
+
+    @Test
+    public void shouldDecorateImplicitlyExcludedNode() {
+        final NodeState content = getNodeState(rootNodeState, "/content");
+        final NodeState decorated = wrap("/content", content, of("/apps"), null);
+        assertNotSame("content should not be identical to decorated", content, decorated);
+    }
+
+
+    @Test
+    public void shouldHideExcludedPathsViaExists() {
+        final NodeState decorated = wrap("/", rootNodeState, null, of("/apps", "/libs"));
+        assertMissing(decorated, "apps");
+        assertMissing(decorated, "libs/foo/install");
+
+        assertExists(decorated, "content/foo/de");
+        assertExists(decorated, "content/foo/en");
+    }
+
+    @Test
+    public void shouldHideExcludedPathsViaHasChildNode() {
+        final NodeState decorated = wrap("/", rootNodeState, null, of("/apps", "/libs"));
+
+        assertExistingHasChildNode(decorated, "content");
+        assertMissingHasChildNode(decorated, "apps");
+        assertMissingHasChildNode(decorated, "libs");
+    }
+
+    @Test
+    public void shouldHideExcludedPathsViaGetChildNodeNames() {
+        final NodeState decorated = wrap("/", rootNodeState, null, of("/apps", "/libs"));
+
+        assertExistingChildNodeName(decorated, "content");
+        assertMissingChildNodeName(decorated, "apps");
+        assertMissingChildNodeName(decorated, "libs");
+    }
+
+    @Test
+    public void shouldHideMissingIncludedPathsViaExists() {
+        final NodeState decorated = wrap("/", rootNodeState, of("/content"), null);
+        assertMissing(decorated, "apps");
+        assertMissing(decorated, "libs/foo/install");
+
+        assertExists(decorated, "content/foo/de");
+        assertExists(decorated, "content/foo/en");
+    }
+
+    @Test
+    public void shouldHideMissingIncludedPathsViaHasChildNode() {
+        final NodeState decorated = wrap("/", rootNodeState, of("/content"), null);
+
+        assertExistingHasChildNode(decorated, "content");
+        assertMissingHasChildNode(decorated, "apps");
+        assertMissingHasChildNode(decorated, "libs");
+    }
+
+    @Test
+    public void shouldHideMissingIncludedPathsViaGetChildNodeNames() {
+        final NodeState decorated = wrap("/", rootNodeState, of("/content"), null);
+
+        assertExistingChildNodeName(decorated, "content");
+        assertMissingChildNodeName(decorated, "apps");
+        assertMissingChildNodeName(decorated, "libs");
+    }
+
+    @Test
+    public void shouldGivePrecedenceForExcludesOverIncludes() {
+        final NodeState conflictingRules = wrap("/", rootNodeState, of("/content"), of("/content"));
+        assertMissingChildNodeName(conflictingRules, "content");
+
+        final NodeState overlappingRules = wrap("/", rootNodeState, of("/content"), of("/content/foo"));
+        assertExistingChildNodeName(overlappingRules, "content");
+        assertMissingChildNodeName(overlappingRules.getChildNode("content"), "foo");
+
+
+        final NodeState overlappingRules2 = wrap("/", rootNodeState, of("/content/foo"), of("/content"));
+        assertMissingChildNodeName(overlappingRules2, "content");
+        assertMissingChildNodeName(overlappingRules2.getChildNode("content"), "foo");
+
+    }
+
+    @Test
+    public void shouldRespectPathBoundariesForIncludes() {
+        final NodeState decorated = wrap("/", rootNodeState, of("/content/foo"), null);
+
+        assertExistingChildNodeName(decorated, "content");
+        assertExistingChildNodeName(decorated.getChildNode("content"), "foo");
+        assertMissingChildNodeName(decorated.getChildNode("content"), "football");
+    }
+
+    @Test
+    public void shouldRespectPathBoundariesForExcludes() {
+        final NodeState decorated = wrap("/", rootNodeState, null, of("/content/foo"));
+
+        assertExistingChildNodeName(decorated, "content");
+        assertMissingChildNodeName(decorated.getChildNode("content"), "foo");
+        assertExistingChildNodeName(decorated.getChildNode("content"), "football");
+    }
+
+    @Test
+    public void shouldDelegatePropertyCount() {
+        final NodeState decorated = wrap("/", rootNodeState, null, of("/content/foo/de"));
+
+        assertEquals(1, getNodeState(decorated, "/content").getPropertyCount());
+        assertEquals(0, getNodeState(decorated, "/content/foo").getPropertyCount());
+    }
+
+
+    @Test
+    public void shouldDelegateGetProperty() {
+        final NodeState decorated = wrap("/", rootNodeState, null, of("/content/foo"));
+        final NodeState content = getNodeState(decorated, "/content");
+
+        assertNotNull(content.getProperty(OAK_CHILD_ORDER));
+        assertNull(content.getProperty("nonexisting"));
+    }
+
+
+    @Test
+    public void shouldDelegateHasProperty() {
+        final NodeState decorated = wrap("/", rootNodeState, null, of("/content/foo/de"));
+
+        assertTrue(getNodeState(decorated, "/content").hasProperty(OAK_CHILD_ORDER));
+        assertFalse(getNodeState(decorated, "/content").hasProperty("foo"));
+    }
+
+
+    @Test
+    public void exists() {
+        final NodeState decorated = wrap("/", rootNodeState, null, of("/content/foo"));
+        assertTrue("/content should exist and be visible", getNodeState(decorated, "/content").exists());
+        assertFalse("/content/foo should be hidden", getNodeState(decorated, "/content/foo").exists());
+        assertFalse("/nonexisting should not exist", getNodeState(decorated, "/nonexisting").exists());
+    }
+
+
+    private void assertExistingHasChildNode(NodeState decorated, String name) {
+        assertTrue("should have child \"" + name + "\"", decorated.hasChildNode(name));
+    }
+
+    private void assertMissingHasChildNode(NodeState decorated, String name) {
+        assertFalse("should not have child \"" + name + "\"", decorated.hasChildNode(name));
+    }
+
+    private void assertExistingChildNodeName(NodeState decorated, String name) {
+        final Iterable<String> childNodeNames = decorated.getChildNodeNames();
+        assertTrue("should list child \"" + name + "\"", Iterables.contains(childNodeNames, name));
+    }
+
+    private void assertMissingChildNodeName(NodeState decorated, String name) {
+        final Iterable<String> childNodeNames = decorated.getChildNodeNames();
+        assertFalse("should not list child \"" + name + "\"", Iterables.contains(childNodeNames, name));
+    }
+}