diff --git a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java index d25b28d..e632930 100644 --- a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java +++ b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/JackrabbitNodeState.java @@ -18,8 +18,8 @@ package org.apache.jackrabbit.oak.upgrade; 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.JcrConstants.NT_BASE; 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 AbstractNodeState { } } - private final JackrabbitNodeState parent; + private JackrabbitNodeState parent; private final String name; @@ -109,6 +116,10 @@ class JackrabbitNodeState extends AbstractNodeState { */ 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 AbstractNodeState { private final Map properties; + private final Map mountPoints; + + private final Map nodeStateCache; + + private final List ignoredPaths = ImmutableList.of("/jcr:system/jcr:nodeTypes"); + + public static JackrabbitNodeState createRootNodeState( + RepositoryContext context, + String workspaceName, + NodeState root, + Map uriToPrefix, + Map versionablePaths, + boolean copyBinariesByReference, + boolean skipOnError + ) throws RepositoryException { + + final Map 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 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 AbstractNodeState { 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 AbstractNodeState { PersistenceManager source, NodeState root, Map uriToPrefix, NodeId id, String path, String workspaceName, Map versionablePaths, + Map 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 AbstractNodeState { 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(cacheSize, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() >= cacheSize; + } + }; this.useBinaryReferences = useBinaryReferences; this.skipOnError = skipOnError; try { @@ -240,8 +310,7 @@ class JackrabbitNodeState extends AbstractNodeState { 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 AbstractNodeState { for (Map.Entry 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 AbstractNodeState { //-----------------------------------------------------------< 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 AbstractNodeState { 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 AbstractNodeState { log.warn(getPath() + ": " + message, cause); } -} +} \ No newline at end of file diff --git a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java index 857896d..20f5bf0 100644 --- a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java +++ b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java @@ -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.NodeTypeTemplate; 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.SecurityConfig; 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.security.privilege.PrivilegeBits; 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 DEFAULT_INCLUDE_PATHS = ALL; + + public static final Set DEFAULT_EXCLUDE_PATHS = NONE; + + public static final Set 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 includePaths = DEFAULT_INCLUDE_PATHS; + + /** + * Paths to exclude during the copy process. Empty by default. + */ + private Set excludePaths = DEFAULT_EXCLUDE_PATHS; + + /** + * Paths to merge during the copy process. Empty by default. + */ + private Set 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. *

* The source repository must not be modified while @@ -313,14 +367,22 @@ public class RepositoryUpgrade { Map 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 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 uriToPrefix, - Map versionablePaths) { - PersistenceManager pm = source.getInternalVersionManager().getPersistenceManager(); - NodeBuilder system = builder.child(JCR_SYSTEM); + private void copyWorkspace(NodeState sourceState, NodeBuilder builder, String workspaceName) + throws RepositoryException { + final Set includes = calculateEffectiveIncludePaths(sourceState); + final Set excludes = union(copyOf(this.excludePaths), of("/jcr:system/jcr:versionStorage", "/jcr:system/jcr:activities")); + final Set merges = union(copyOf(this.mergePaths), of("/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 workspace {} [i: {}, e: {}, m: {}]", workspaceName, includes, excludes, merges); - 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); + NodeStateCopier.builder() + .include(includes) + .exclude(excludes) + .merge(merges) + .copy(sourceState, builder); } - private String copyWorkspace( - NodeBuilder builder, NodeState root, String workspaceName, - Map uriToPrefix, Map versionablePaths) + private void copyVersionStore(NodeState sourceState, NodeBuilder builder) throws RepositoryException { - logger.info("Copying workspace {}", workspaceName); - - PersistenceManager pm = - source.getWorkspaceInfo(workspaceName).getPersistenceManager(); - - NodeState state = new JackrabbitNodeState( - pm, root, uriToPrefix, ROOT_NODE_ID, "/", - workspaceName, versionablePaths, copyBinariesByReference, skipOnError); + NodeStateCopier.builder() + .include("/jcr:system/jcr:versionStorage", "/jcr:system/jcr:activities") + .merge("/jcr:system") + .copy(sourceState, builder); + } - 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); - } + private Set calculateEffectiveIncludePaths(NodeState state) { + if (!this.includePaths.contains("/")) { + return copyOf(this.includePaths); } - return workspaceName; - } - - 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; + // include child nodes from source individually to avoid deleting other initialized content + final Set includes = newHashSet(); + for (String childNodeName : state.getChildNodeNames()) { + includes.add("/" + childNodeName); } - NodeBuilder target = targetParent.child(name); - NodeStateCopier.copyNodeState( - source, - target, - path, - merge ? of(path) : Collections.emptySet() - ); + return includes; } private static class LoggingCompositeHook implements CommitHook { diff --git a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeState.java b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeState.java new file mode 100644 index 0000000..5307a93 --- /dev/null +++ b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeState.java @@ -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. + *
+ * The set of visible subtrees is defined by two parameters: include paths + * and exclude paths, both of which are sets of absolute paths. + *
+ * Any paths that are equal or are descendants of one of the + * excluded paths are hidden by this implementation. + *
+ * For all included paths, 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). + *
+ * The implementation delegates to the decorated node-state instance and + * filters out hidden node-states in the following methods: + *

    + *
  • {@link #exists()}
  • + *
  • {@link #hasChildNode(String)}
  • + *
  • {@link #getChildNodeEntries()}
  • + *
+ * Additionally, hidden node-state names are removed from the property + * {@code :childOrder} in the following two methods: + *
    + *
  • {@link #getProperties()}
  • + *
  • {@link #getProperty(String)}
  • + *
+ */ +public class FilteringNodeState extends AbstractNodeState { + + public static final Set ALL = ImmutableSet.of("/"); + + public static final Set NONE = ImmutableSet.of(); + + private final NodeState delegate; + + private final String path; + + private final Set includedPaths; + + private final Set 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 includePaths, + @Nullable final Set excludePaths + ) { + final Set includes = defaultIfEmpty(includePaths, ALL); + final Set 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 includedPaths, + @Nonnull final Set 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 getChildNodeEntries() { + final Iterable transformed = Iterables.transform( + delegate.getChildNodeEntries(), + new Function() { + @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() { + @Override + public boolean apply(@Nullable final ChildNodeEntry childNodeEntry) { + return childNodeEntry != null; + } + }); + } + + @Override + public long getPropertyCount() { + return delegate.getPropertyCount(); + } + + @Override + @Nonnull + public Iterable getProperties() { + return Iterables.transform(delegate.getProperties(), new Function() { + @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 values = Iterables.filter(propertyState.getValue(Type.NAMES), new Predicate() { + @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 includes, + @Nonnull final Set 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 includePaths, + @Nonnull final Set 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. + *
+ * 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 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 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 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 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 Set defaultIfEmpty(@Nullable Set value, @Nonnull Set 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 boolean isEmpty(@Nullable final Set set) { + return set == null || set.isEmpty(); + } +} diff --git a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java index f71058c..5a3705e 100644 --- a/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java +++ b/oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java @@ -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.Preconditions.checkNotNull; * 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. + *
+ * Usage: For most use-cases the Builder API should be + * preferred. It allows setting {@code includePaths}, + * {@code excludePaths} and {@code mergePaths}. + *
+ * Include paths: 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 implicitly excluded. + *
+ * Exclude paths: 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). + * Merge paths: 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 includePaths; + + private final Set excludePaths; + + private final Set mergePaths; - private NodeStateCopier() { - // no instances + private NodeStateCopier(Set includePaths, Set excludePaths, Set 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.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. *
@@ -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 mergePaths) { + private static boolean copyNodeState(@Nonnull final NodeState source, @Nonnull final NodeBuilder target, + @Nonnull final String currentPath, @Nonnull final Set 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}. + *
+ * Include paths can define which paths should be copied from the source to the + * target. + *
+ * Exclude paths 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. + *
+ * 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. + *
+ * Merge paths dictate in which parts of the tree the copy operation should + * be additive, 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. + *
+ * Finally, using one of the {@code copy} methods, NodeStores or NodeStates can + * be copied. + */ + public static class Builder { + + private Set includePaths = of("/"); + + private Set excludePaths = emptySet(); + + private Set 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 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 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 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. + *
+ * 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. + *
+ * 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; + } + } } diff --git a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/IncludeExcludeUpgradeTest.java b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/IncludeExcludeUpgradeTest.java new file mode 100644 index 0000000..82c2783 --- /dev/null +++ b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/IncludeExcludeUpgradeTest.java @@ -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" + ); + } +} diff --git a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/UpgradeFromTwoSourcesTest.java b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/UpgradeFromTwoSourcesTest.java new file mode 100644 index 0000000..b716894 --- /dev/null +++ b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/UpgradeFromTwoSourcesTest.java @@ -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 diff --git a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeStateTest.java b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeStateTest.java new file mode 100644 index 0000000..3615d32 --- /dev/null +++ b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeStateTest.java @@ -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 DEFAULT_INCLUDES = FilteringNodeState.ALL; + + private static final Set 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 values = childOrder.getValue(Type.STRINGS); + assertEquals(newArrayList("football"), newArrayList(values)); + } + + { // access via getProperties() + final Predicate isChildOrderProperty = new Predicate() { + @Override + public boolean apply(PropertyState propertyState) { + return OAK_CHILD_ORDER.equals(propertyState.getName()); + } + }; + final PropertyState childOrder = Iterables.find(decorated.getProperties(), isChildOrderProperty); + final Iterable 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 childNodeNames = decorated.getChildNodeNames(); + assertTrue("should list child \"" + name + "\"", Iterables.contains(childNodeNames, name)); + } + + private void assertMissingChildNodeName(NodeState decorated, String name) { + final Iterable childNodeNames = decorated.getChildNodeNames(); + assertFalse("should not list child \"" + name + "\"", Iterables.contains(childNodeNames, name)); + } +} diff --git a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java index 22311d2..f442eb9 100644 --- a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java +++ b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java @@ -18,7 +18,6 @@ */ package org.apache.jackrabbit.oak.upgrade.nodestate; -import com.google.common.collect.ImmutableSet; import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.api.PropertyState; import org.apache.jackrabbit.oak.api.Type; @@ -27,8 +26,8 @@ import org.apache.jackrabbit.oak.spi.state.NodeState; import org.apache.jackrabbit.oak.spi.state.NodeStore; import org.junit.Test; -import static com.google.common.collect.ImmutableSet.of; import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty; +import static org.apache.jackrabbit.oak.upgrade.nodestate.NodeStateCopier.builder; 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; @@ -40,6 +39,39 @@ public class NodeStateCopierTest { createProperty("jcr:primaryType", "nt:unstructured", Type.NAME); @Test + public void shouldCreateMissingAncestors() throws CommitFailedException { + final NodeStore source = createPrefilledNodeStore(); + final NodeStore target = createNodeStoreWithContent(); + + builder() + .include("/a/b/c") + .copy(source, target); + + expectDifference() + .childNodeChanged("/a", "/a/b") + .childNodeDeleted("/excluded", "/a/b/excluded") + .strict() + .verify(source.getRoot(), target.getRoot()); + } + + @Test + public void shouldIncludeMultiplePaths() throws CommitFailedException { + final NodeStore source = createPrefilledNodeStore(); + final NodeStore target = createNodeStoreWithContent(); + + builder() + .include("/a/b/c/d", "/a/b/c/e") + .copy(source, target); + + expectDifference() + .propertyDeleted("/a/b/c/f/jcr:primaryType") + .childNodeChanged("/a", "/a/b", "/a/b/c") + .childNodeDeleted("/excluded", "/a/b/excluded", "/a/b/c/f") + .strict() + .verify(source.getRoot(), target.getRoot()); + } + + @Test public void shouldMergeIdenticalContent() throws CommitFailedException { final NodeStore source = createPrefilledNodeStore(); final NodeStore target = createPrefilledNodeStore(); @@ -68,13 +100,63 @@ public class NodeStateCopierTest { } @Test + public void shouldSkipNonMatchingIncludes() throws CommitFailedException { + final NodeStore source = createNodeStoreWithContent(); + final NodeBuilder builder = source.getRoot().builder(); + create(builder, "/a", primaryType); + create(builder, "/a/b", primaryType); + create(builder, "/a/b/c", primaryType); + commit(source, builder); + + final NodeStore target = createNodeStoreWithContent(); + builder() + .include("/a", "/z") + .copy(source, target); + + expectDifference() + .strict() + .verify(source.getRoot(), target.getRoot()); + } + + @Test + public void shouldCopyFromMultipleSources() throws CommitFailedException { + final NodeStore source1 = createNodeStoreWithContent( + "/content/foo/en", "/content/foo/de"); + final NodeStore source2 = createNodeStoreWithContent( + "/content/bar/en", "/content/bar/de", "/content/baz/en"); + final NodeStore target = createNodeStoreWithContent(); + + final NodeState before = target.getRoot(); + builder() + .include("/content/foo") + .copy(source1, target); + builder() + .include("/content/bar") + .exclude("/content/bar/de") + .copy(source2, target); + final NodeState after = target.getRoot(); + + expectDifference() + .strict() + .childNodeAdded( + "/content", + "/content/foo", + "/content/foo/en", + "/content/foo/de", + "/content/bar", + "/content/bar/en" + ) + .verify(before, after); + } + + @Test public void shouldRespectMergePaths() throws CommitFailedException { final NodeStore source = createNodeStoreWithContent("/content/foo/en", "/content/bar/en"); final NodeStore target = createNodeStoreWithContent("/content/foo/de"); - final NodeBuilder builder = target.getRoot().builder(); - NodeStateCopier.copyNodeState(source.getRoot(), builder, "/", of("/content")); - commit(target, builder); + builder() + .merge("/content") + .copy(source, target); final NodeState after = target.getRoot(); expectDifference() @@ -84,6 +166,23 @@ public class NodeStateCopierTest { .verify(source.getRoot(), after); } + @Test + public void shouldNotDeleteMergedExcludedPaths() throws CommitFailedException { + final NodeStore source = createNodeStoreWithContent("/content/foo/en", "/jcr:system"); + final NodeStore target = createNodeStoreWithContent("/jcr:system/jcr:versionStorage"); + + final NodeState before = target.getRoot(); + builder() + .merge("/jcr:system") + .exclude("/jcr:system") + .copy(source, target); + final NodeState after = target.getRoot(); + + expectDifference() + .strict() + .childNodeAdded("/content", "/content/foo", "/content/foo/en") + .verify(before, after); + } @Test public void shouldDeleteExistingNodes() throws CommitFailedException { @@ -91,9 +190,7 @@ public class NodeStateCopierTest { final NodeStore target = createNodeStoreWithContent("/content/bar"); final NodeState before = target.getRoot(); - final NodeBuilder builder = before.builder(); - NodeStateCopier.copyNodeState(source.getRoot(), builder, "/", ImmutableSet.of()); - commit(target, builder); + builder().copy(source, target); final NodeState after = target.getRoot(); expectDifference() @@ -108,14 +205,12 @@ public class NodeStateCopierTest { public void shouldDeleteExistingPropertyIfMissingInSource() throws CommitFailedException { final NodeStore source = createNodeStoreWithContent("/a"); final NodeStore target = createNodeStoreWithContent(); - NodeBuilder builder = target.getRoot().builder(); + final NodeBuilder builder = target.getRoot().builder(); create(builder, "/a", primaryType); commit(target, builder); final NodeState before = target.getRoot(); - builder = before.builder(); - NodeStateCopier.copyNodeState(source.getRoot(), builder, "/", ImmutableSet.of()); - commit(target, builder); + builder().copy(source, target); final NodeState after = target.getRoot(); expectDifference() @@ -131,9 +226,9 @@ public class NodeStateCopierTest { final NodeStore target = createNodeStoreWithContent("/content/bar"); final NodeState before = target.getRoot(); - final NodeBuilder builder = before.builder(); - NodeStateCopier.copyNodeState(source.getRoot(), builder, "/", of("/content/bar")); - commit(target, builder); + builder() + .merge("/content/bar") + .copy(source, target); final NodeState after = target.getRoot(); expectDifference() @@ -149,9 +244,9 @@ public class NodeStateCopierTest { final NodeStore target = createNodeStoreWithContent("/content/bar"); final NodeState before = target.getRoot(); - final NodeBuilder builder = before.builder(); - NodeStateCopier.copyNodeState(source.getRoot(), builder, "/", of("/content")); - commit(target, builder); + builder() + .merge("/content") + .copy(source, target); final NodeState after = target.getRoot(); expectDifference() @@ -161,15 +256,16 @@ public class NodeStateCopierTest { .verify(before, after); } + @Test public void shouldIgnoreNonMatchingMergePaths() throws CommitFailedException { final NodeStore source = createNodeStoreWithContent("/content/foo"); final NodeStore target = createNodeStoreWithContent("/content/bar"); final NodeState before = target.getRoot(); - final NodeBuilder builder = before.builder(); - NodeStateCopier.copyNodeState(source.getRoot(), builder, "/", of("/con")); - commit(target, builder); + builder() + .merge("/con") + .copy(source, target); final NodeState after = target.getRoot(); expectDifference() diff --git a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java index ce668df..454bd58 100644 --- a/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java +++ b/oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java @@ -38,6 +38,8 @@ import java.util.Set; import java.util.TreeSet; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; public class NodeStateTestUtils { @@ -66,6 +68,14 @@ public class NodeStateTestUtils { store.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY); } + public static NodeState getNodeState(NodeState state, String path) { + NodeState current = state; + for (final String name : PathUtils.elements(path)) { + current = current.getChildNode(name); + } + return current; + } + public static NodeBuilder createOrGetBuilder(NodeBuilder builder, String path) { NodeBuilder current = builder; for (final String name : PathUtils.elements(path)) { @@ -74,6 +84,14 @@ public class NodeStateTestUtils { return current; } + public static void assertExists(NodeState state, String relPath) { + assertTrue(relPath + " should exist", getNodeState(state, relPath).exists()); + } + + public static void assertMissing(NodeState state, String relPath) { + assertFalse(relPath + " should not exist", getNodeState(state, relPath).exists()); + } + public static ExpectedDifference expectDifference() { return new ExpectedDifference(); }