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));
+ }
+}