diff --git oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java index fa9aada..b86a5e6 100644 --- oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java +++ oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/RepositoryUpgrade.java @@ -12,49 +12,56 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.jackrabbit.oak.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.JcrConstants.JCR_VERSIONSTORAGE; 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.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.jcr.NamespaceException; import javax.jcr.RepositoryException; import javax.jcr.Value; import javax.jcr.ValueFactory; import javax.jcr.nodetype.NodeDefinitionTemplate; import javax.jcr.nodetype.NodeTypeManager; 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; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; @@ -66,30 +73,28 @@ 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.namepath.NamePathMapper; import org.apache.jackrabbit.oak.plugins.index.CompositeIndexEditorProvider; import org.apache.jackrabbit.oak.plugins.index.IndexEditorProvider; import org.apache.jackrabbit.oak.plugins.index.IndexUpdate; import org.apache.jackrabbit.oak.plugins.index.IndexUpdateCallback; import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider; import org.apache.jackrabbit.oak.plugins.index.reference.ReferenceEditorProvider; import org.apache.jackrabbit.oak.plugins.name.NamespaceConstants; import org.apache.jackrabbit.oak.plugins.name.Namespaces; import org.apache.jackrabbit.oak.plugins.nodetype.TypeEditorProvider; import org.apache.jackrabbit.oak.plugins.nodetype.write.InitialContent; import org.apache.jackrabbit.oak.plugins.nodetype.write.ReadWriteNodeTypeManager; -import org.apache.jackrabbit.oak.plugins.segment.SegmentNodeBuilder; import org.apache.jackrabbit.oak.plugins.value.ValueFactoryImpl; import org.apache.jackrabbit.oak.security.SecurityProviderImpl; import org.apache.jackrabbit.oak.spi.commit.CommitHook; import org.apache.jackrabbit.oak.spi.commit.CommitInfo; import org.apache.jackrabbit.oak.spi.commit.CompositeEditorProvider; import org.apache.jackrabbit.oak.spi.commit.Editor; import org.apache.jackrabbit.oak.spi.commit.EditorHook; @@ -99,18 +104,18 @@ import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer; import org.apache.jackrabbit.oak.spi.lifecycle.WorkspaceInitializer; import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters; import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration; 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; +import org.apache.jackrabbit.oak.upgrade.nodestate.NodeStateCopier; import org.apache.jackrabbit.oak.upgrade.security.GroupEditorProvider; import org.apache.jackrabbit.oak.upgrade.security.RestrictionEditorProvider; import org.apache.jackrabbit.spi.Name; import org.apache.jackrabbit.spi.QNodeDefinition; import org.apache.jackrabbit.spi.QNodeTypeDefinition; import org.apache.jackrabbit.spi.QPropertyDefinition; import org.apache.jackrabbit.spi.QValue; @@ -121,24 +126,41 @@ import org.apache.jackrabbit.spi.commons.value.ValueFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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; + /** * Source repository context. */ private final RepositoryContext source; /** * Target node store. */ 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; + + /** + * Whether or not to copy binaries by reference. Defaults to false. + */ private boolean copyBinariesByReference = false; /** * Copies the contents of the repository in the given source directory * to the given target node store. * * @param source source repository directory @@ -186,14 +208,34 @@ public class RepositoryUpgrade { } public void setCopyBinariesByReference(boolean copyBinariesByReference) { this.copyBinariesByReference = copyBinariesByReference; } /** + * 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)); + } + + /** * Copies the full content from the source to the target repository. *

* The source repository must not be modified while * the copy operation is running to avoid an inconsistent copy. *

* Note that both the source and the target repository must be closed * during the copy operation as this method requires exclusive access @@ -660,79 +702,72 @@ public class RepositoryUpgrade { NodeBuilder builder, NodeState root, String workspaceName, Map uriToPrefix, Map versionablePaths) { PersistenceManager pm = source.getInternalVersionManager().getPersistenceManager(); NodeBuilder system = builder.child(JCR_SYSTEM); logger.info("Copying version histories"); - copyState(system, JCR_VERSIONSTORAGE, new JackrabbitNodeState( - pm, root, uriToPrefix, VERSION_STORAGE_NODE_ID, - "/jcr:system/jcr:versionStorage", - workspaceName, versionablePaths, copyBinariesByReference)); + NodeStateCopier.builder().copy( + new JackrabbitNodeState( + pm, root, uriToPrefix, VERSION_STORAGE_NODE_ID, + "/jcr:system/jcr:versionStorage", + workspaceName, versionablePaths, copyBinariesByReference + ), + system.child(JCR_VERSIONSTORAGE)); logger.info("Copying activities"); - copyState(system, "jcr:activities", new JackrabbitNodeState( - pm, root, uriToPrefix, ACTIVITIES_NODE_ID, - "/jcr:system/jcr:activities", - workspaceName, versionablePaths, copyBinariesByReference)); + NodeStateCopier.builder().copy( + new JackrabbitNodeState( + pm, root, uriToPrefix, ACTIVITIES_NODE_ID, + "/jcr:system/jcr:activities", + workspaceName, versionablePaths, copyBinariesByReference + ), + system.child("jcr:activities")); } private String copyWorkspace( NodeBuilder builder, NodeState root, String workspaceName, Map uriToPrefix, Map versionablePaths) 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); - for (PropertyState property : state.getProperties()) { - builder.setProperty(property); - } - for (ChildNodeEntry child : state.getChildNodeEntries()) { - String childName = child.getName(); - if (!JCR_SYSTEM.equals(childName)) { - logger.info("Copying subtree /{}", childName); - copyState(builder, childName, child.getNodeState()); - } - } + final Set includes = calculateEffectiveIncludePaths(state); + final Set excludes = union(copyOf(this.excludePaths), of("/jcr:system")); + final Set merges = of("/jcr:system"); + + logger.info("Copying workspace {} [i: {}, e: {}, m: {}]", workspaceName, includes, excludes, merges); + + NodeStateCopier.builder() + .include(includes) + .exclude(excludes) + .merge(merges) + .copy(state, builder); return workspaceName; } - private void copyState(NodeBuilder parent, String name, NodeState state) { - if (parent instanceof SegmentNodeBuilder) { - parent.setChildNode(name, state); - } else { - setChildNode(parent, name, state); + private Set calculateEffectiveIncludePaths(NodeState state) { + if (!this.includePaths.contains("/")) { + return copyOf(this.includePaths); } - } - /** - * NodeState are copied by value by recursing down the complete tree - * This is a temporary approach for OAK-1760 for 1.0 branch. - */ - private void setChildNode(NodeBuilder parent, String name, NodeState state) { - // 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 {}", state); - return; - } - NodeBuilder builder = parent.setChildNode(name); - for (PropertyState property : state.getProperties()) { - builder.setProperty(property); - } - for (ChildNodeEntry child : state.getChildNodeEntries()) { - setChildNode(builder, child.getName(), child.getNodeState()); + // include child nodes from source individually to avoid deleting other initialized content + final Set includes = newHashSet(); + for (String childNodeName : state.getChildNodeNames()) { + includes.add("/" + childNodeName); } + // jcr:system is handled explicitly + includes.remove("/jcr:system"); + return includes; } private static class LoggingCompositeHook implements CommitHook { private final Collection hooks; public LoggingCompositeHook(Collection hooks) { this.hooks = hooks; diff --git oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeState.java oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeState.java new file mode 100644 index 0000000..5307a93 --- /dev/null +++ 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 oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java index 46d3ad5..f1e3b7a 100644 --- oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java +++ oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java @@ -12,15 +12,14 @@ 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; @@ -34,40 +33,68 @@ import static java.util.Collections.emptySet; *
* The algorithm does a post-order traversal. I.e. it copies * changed leaf-nodes first. *
* 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(); } /** * Shorthand method to copy one NodeStore to another. The changes in the * target NodeStore are automatically persisted. * * @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)); } /** * Copies all changed properties from the source NodeState to the target * NodeBuilder instance. * * @param source The NodeState to copy from. @@ -92,14 +119,29 @@ public class NodeStateCopier { target.setProperty(property); hasChanges = true; } } 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. *
* Nodes that exist in the {@code target} but not in the {@code source} * are removed, unless they are descendants of one of the {@code mergePaths}. * This is determined by checking if the {@code currentPath} is a descendant * of any of the {@code mergePaths}. @@ -109,16 +151,16 @@ public class NodeStateCopier { * @param source NodeState to copy from * @param target NodeBuilder to copy to * @param currentPath The path of both the source and target arguments. * @param mergePaths A Set of paths under which existing nodes should be * 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; // delete deleted children for (final String childName : target.getChildNodeNames()) { if (!source.hasChildNode(childName) && !isMerge(PathUtils.concat(currentPath, childName), mergePaths)) { @@ -155,8 +197,212 @@ public class NodeStateCopier { for (String mergePath : mergePaths) { if (PathUtils.isAncestor(mergePath, path) || mergePath.equals(path)) { return true; } } 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 oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/IncludeExcludeUpgradeTest.java oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/IncludeExcludeUpgradeTest.java new file mode 100644 index 0000000..82c2783 --- /dev/null +++ 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 oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/UpgradeFromTwoSourcesTest.java oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/UpgradeFromTwoSourcesTest.java new file mode 100644 index 0000000..b716894 --- /dev/null +++ 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 oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeStateTest.java oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/FilteringNodeStateTest.java new file mode 100644 index 0000000..3615d32 --- /dev/null +++ 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 oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java index a5b6747..421a6d1 100644 --- oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java +++ oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java @@ -1,31 +1,63 @@ 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; 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.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; import static org.apache.jackrabbit.oak.upgrade.util.NodeStateTestUtils.expectDifference; public class NodeStateCopierTest { private final PropertyState primaryType = 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(); final NodeState before = target.getRoot(); NodeStateCopier.copyNodeStore(source, target); final NodeState after = target.getRoot(); @@ -46,62 +78,125 @@ public class NodeStateCopierTest { create(builder, "/a/b/c/e", primaryType); create(builder, "/a/b/c/f", primaryType); commit(store, builder); return store; } @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() .strict() .childNodeAdded("/content/foo/de") .childNodeChanged("/content", "/content/foo") .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 { 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, "/", ImmutableSet.of()); - commit(target, builder); + builder().copy(source, target); final NodeState after = target.getRoot(); expectDifference() .strict() .childNodeAdded("/content/foo") .childNodeChanged("/content") .childNodeDeleted("/content/bar") .verify(before, after); } @Test 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() .strict() .propertyDeleted("/a/jcr:primaryType") .childNodeChanged("/a") .verify(before, after); @@ -109,17 +204,17 @@ public class NodeStateCopierTest { @Test public void shouldNotDeleteExistingNodesIfMerged() 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("/content/bar")); - commit(target, builder); + builder() + .merge("/content/bar") + .copy(source, target); final NodeState after = target.getRoot(); expectDifference() .strict() .childNodeAdded("/content/foo") .childNodeChanged("/content") .verify(before, after); @@ -127,35 +222,36 @@ public class NodeStateCopierTest { @Test public void shouldNotDeleteExistingNodesIfDescendantsOfMerged() 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("/content")); - commit(target, builder); + builder() + .merge("/content") + .copy(source, target); final NodeState after = target.getRoot(); expectDifference() .strict() .childNodeAdded("/content/foo") .childNodeChanged("/content") .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() .strict() .childNodeAdded("/content/foo") .childNodeChanged("/content") .childNodeDeleted("/content/bar") diff --git oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java index 474f4ce..aa28646 100644 --- oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java +++ oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java @@ -16,14 +16,16 @@ import org.apache.jackrabbit.oak.spi.state.NodeStore; import java.util.Collections; import java.util.HashMap; import java.util.Map; 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 { private NodeStateTestUtils() { // no instances } @@ -44,22 +46,38 @@ public class NodeStateTestUtils { } } public static void commit(NodeStore store, NodeBuilder rootBuilder) throws CommitFailedException { 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)) { current = current.child(name); } 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(); } public static class ExpectedDifference { private ExpectedDifference() {