blob: b14e95d8ad6e14a9ded3b12a6993399500c566e4 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.oak.plugins.migration;
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.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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
* recursively copying a NodeState to a NodeBuilder.
* <br>
* The copy algorithm is optimized for copying nodes between two
* different NodeStore instances, i.e. where comparing NodeStates
* is imprecise and/or expensive.
* <br>
* The algorithm does a post-order traversal. I.e. it copies
* changed leaf-nodes first.
* <br>
* The work for a traversal without any differences between
* {@code source} and {@code target} is equivalent to the single
* execution of a naive equals implementation.
* <br>
* <b>Usage:</b> For most use-cases the Builder API should be
* preferred. It allows setting {@code includePaths},
* {@code excludePaths} and {@code mergePaths}.
* <br>
* <b>Include paths:</b> if include paths are set, only these paths
* and their sub-trees are copied. Any nodes that are not within the
* scope of an include path are <i>implicitly excluded</i>.
* <br>
* <b>Exclude paths:</b> if exclude paths are set, any nodes matching
* or below the excluded path are not copied. If an excluded node does
* exist in the target, it is removed (see also merge paths).
* <b>Exclude fragments:</b> if exclude fragments are set, nodes with names
* matching any of the fragments (and their subtrees) are not copied. If an
* excluded node does exist in the target, it is removed.
* <b>Merge paths:</b> if merge paths are set, any nodes matching or
* below the merged path will not be deleted from target, even if they
* are missing in (or excluded from) the source.
*/
public class NodeStateCopier {
private static final Logger LOG = LoggerFactory.getLogger(NodeStateCopier.class);
private final Set<String> includePaths;
private final Set<String> excludePaths;
private final Set<String> fragmentPaths;
private final Set<String> excludeFragments;
private final Set<String> mergePaths;
private NodeStateCopier(Set<String> includePaths, Set<String> excludePaths, Set<String> fragmentPaths, Set<String> excludeFragments, Set<String> mergePaths) {
this.includePaths = includePaths;
this.excludePaths = excludePaths;
this.fragmentPaths = fragmentPaths;
this.excludeFragments = excludeFragments;
this.mergePaths = mergePaths;
}
/**
* Create a NodeStateCopier.Builder.
*
* @return a NodeStateCopier.Builder
* @see 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.
* @return true if the target has been modified
* @throws CommitFailedException if the operation fails
* @see NodeStateCopier.Builder#copy(NodeStore, NodeStore)
*/
public static boolean copyNodeStore(@NotNull final NodeStore source, @NotNull final NodeStore target)
throws CommitFailedException {
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.
* @param target The NodeBuilder to copy to.
* @return Whether changes were made or not.
*/
public static boolean copyProperties(NodeState source, NodeBuilder target) {
boolean hasChanges = false;
// remove removed properties
for (final PropertyState property : target.getProperties()) {
final String name = property.getName();
if (!source.hasProperty(name)) {
target.removeProperty(name);
hasChanges = true;
}
}
// add new properties and change changed properties
for (PropertyState property : source.getProperties()) {
if (!property.equals(target.getProperty(property.getName()))) {
target.setProperty(property);
hasChanges = true;
}
}
return hasChanges;
}
private boolean copyNodeState(@NotNull final NodeState sourceRoot, @NotNull final NodeBuilder targetRoot) {
final NodeState wrappedSource = FilteringNodeState.wrap("/", sourceRoot, this.includePaths, this.excludePaths, this.fragmentPaths, this.excludeFragments);
boolean hasChanges = false;
for (String includePath : this.includePaths) {
hasChanges = copyMissingAncestors(sourceRoot, targetRoot, includePath) || hasChanges;
final NodeState sourceState = NodeStateUtils.getNode(wrappedSource, includePath);
if (sourceState.exists()) {
final NodeBuilder targetBuilder = getChildNodeBuilder(targetRoot, includePath);
hasChanges = copyNodeState(sourceState, targetBuilder, includePath, this.mergePaths) || hasChanges;
}
}
return hasChanges;
}
/**
* Recursively copies the source NodeState to the target NodeBuilder.
* <br>
* 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}.
* <br>
* <b>Note:</b> changes are not persisted.
*
* @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.
*/
private static boolean copyNodeState(@NotNull final NodeState source, @NotNull final NodeBuilder target,
@NotNull final String currentPath, @NotNull final Set<String> mergePaths) {
boolean hasChanges = false;
// delete deleted children
for (final String childName : target.getChildNodeNames()) {
if (!source.hasChildNode(childName) && !isMerge(PathUtils.concat(currentPath, childName), mergePaths)) {
target.setChildNode(childName, EmptyNodeState.MISSING_NODE);
hasChanges = true;
}
}
for (ChildNodeEntry child : source.getChildNodeEntries()) {
final String childName = child.getName();
final NodeState childSource = child.getNodeState();
if (!target.hasChildNode(childName)) {
// add new children
target.setChildNode(childName, childSource);
hasChanges = true;
} else {
// recurse into existing children
final NodeBuilder childTarget = target.getChildNode(childName);
final String childPath = PathUtils.concat(currentPath, childName);
hasChanges = copyNodeState(childSource, childTarget, childPath, mergePaths) || hasChanges;
}
}
hasChanges = copyProperties(source, target) || hasChanges;
if (hasChanges) {
LOG.trace("Node {} has changes", target);
}
return hasChanges;
}
private static boolean isMerge(String path, Set<String> mergePaths) {
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 targetRoot}. Copies any
* missing ancestors from {@code sourceRoot}.
*
* @param sourceRoot NodeState to copy from
* @param targetRoot NodeBuilder to copy to
* @param path The path along which ancestors should be copied.
*/
private static boolean copyMissingAncestors(
final NodeState sourceRoot, final NodeBuilder targetRoot, final String path) {
NodeState current = sourceRoot;
NodeBuilder currentBuilder = targetRoot;
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
*/
@NotNull
private static NodeBuilder getChildNodeBuilder(@NotNull final NodeBuilder root, @NotNull final String path) {
NodeBuilder child = root;
for (String name : PathUtils.elements(path)) {
child = child.child(name);
}
return child;
}
/**
* The NodeStateCopier.Builder allows configuring a NodeState copy operation with
* {@code includePaths}, {@code excludePaths} and {@code mergePaths}.
* <br>
* <b>Include paths</b> can define which paths should be copied from the source to the
* target.
* <br>
* <b>Exclude paths</b> allow restricting which paths should be copied. This is
* especially useful when there are individual nodes in an included path that
* should not be copied.
* <br>
* By default copying will remove items that already exist in the target but do
* not exist in the source. If this behaviour is undesired that is where merge
* paths come in.
* <br>
* <b>Merge paths</b> dictate in which parts of the tree the copy operation should
* be <i>additive</i>, i.e. the content from source is merged with the content
* in the target. Nodes that are present in the target but not in the source are
* then not deleted. However, in the case where nodes are present in both the source
* and the target, the node from the source is copied with its properties and any
* properties previously present on the target's node are lost.
* <br>
* Finally, using one of the {@code copy} methods, NodeStores or NodeStates can
* be copied.
*/
public static class Builder {
private Set<String> includePaths = of("/");
private Set<String> excludePaths = emptySet();
private Set<String> fragmentPaths = emptySet();
private Set<String> excludeFragments = emptySet();
private Set<String> mergePaths = emptySet();
private Builder() {}
/**
* Set include paths.
*
* @param paths include paths
* @return this Builder instance
* @see NodeStateCopier#NodeStateCopier(Set, Set, Set, Set, Set)
*/
@NotNull
public Builder include(@NotNull Set<String> paths) {
if (!checkNotNull(paths).isEmpty()) {
this.includePaths = copyOf(paths);
}
return this;
}
/**
* Convenience wrapper for {@link #include(Set)}.
*
* @param paths include paths
* @return this Builder instance
* @see NodeStateCopier#NodeStateCopier(Set, Set, Set, Set, Set)
*/
@NotNull
public Builder include(@NotNull String... paths) {
return include(copyOf(checkNotNull(paths)));
}
/**
* Set exclude paths.
*
* @param paths exclude paths
* @return this Builder instance
* @see NodeStateCopier#NodeStateCopier(Set, Set, Set, Set, Set)
*/
@NotNull
public Builder exclude(@NotNull Set<String> paths) {
if (!checkNotNull(paths).isEmpty()) {
this.excludePaths = copyOf(paths);
}
return this;
}
/**
* Convenience wrapper for {@link #exclude(Set)}.
*
* @param paths exclude paths
* @return this Builder instance
* @see NodeStateCopier#NodeStateCopier(Set, Set, Set, Set, Set)
*/
@NotNull
public Builder exclude(@NotNull String... paths) {
return exclude(copyOf(checkNotNull(paths)));
}
/**
* Set fragment paths.
*
* @param paths fragment paths
* @return this Builder instance
* @see NodeStateCopier#NodeStateCopier(Set, Set, Set, Set, Set)
*/
@NotNull
public Builder supportFragment(@NotNull Set<String> paths) {
if (!checkNotNull(paths).isEmpty()) {
this.fragmentPaths = copyOf(paths);
}
return this;
}
/**
* Convenience wrapper for {@link #supportFragment(Set)}.
*
* @param paths fragment paths
* @return this Builder instance
* @see NodeStateCopier#NodeStateCopier(Set, Set, Set, Set, Set)
*/
@NotNull
public Builder supportFragment(@NotNull String... paths) {
return supportFragment(copyOf(checkNotNull(paths)));
}
/**
* Set exclude fragments.
*
* @param fragments exclude fragments
* @return this Builder instance
* @see NodeStateCopier#NodeStateCopier(Set, Set, Set, Set, Set)
*/
@NotNull
public Builder excludeFragments(@NotNull Set<String> fragments) {
if (!checkNotNull(fragments).isEmpty()) {
this.excludeFragments = copyOf(fragments);
}
return this;
}
/**
* Convenience wrapper for {@link #exclude(Set)}.
*
* @param fragments exclude fragments
* @return this Builder instance
* @see NodeStateCopier#NodeStateCopier(Set, Set, Set, Set, Set)
*/
@NotNull
public Builder excludeFragments(@NotNull String... fragments) {
return exclude(copyOf(checkNotNull(fragments)));
}
/**
* Set merge paths.
*
* @param paths merge paths
* @return this Builder instance
* @see NodeStateCopier#NodeStateCopier(Set, Set, Set, Set, Set)
*/
@NotNull
public Builder merge(@NotNull Set<String> paths) {
if (!checkNotNull(paths).isEmpty()) {
this.mergePaths = copyOf(paths);
}
return this;
}
/**
* Convenience wrapper for {@link #merge(Set)}.
*
* @param paths merge paths
* @return this Builder instance
* @see NodeStateCopier#NodeStateCopier(Set, Set, Set, Set, Set)
*/
@NotNull
public Builder merge(@NotNull String... paths) {
return merge(copyOf(checkNotNull(paths)));
}
/**
* Creates a NodeStateCopier to copy the {@code sourceRoot} NodeState to the
* {@code targetRoot} NodeBuilder, using any include, exclude and merge paths
* set on this NodeStateCopier.Builder.
* <br>
* It is the responsibility of the caller to persist any changes using e.g.
* {@link NodeStore#merge(NodeBuilder, CommitHook, CommitInfo)}.
*
* @param sourceRoot NodeState to copy from
* @param targetRoot NodeBuilder to copy to
* @return true if there were any changes, false if sourceRoot and targetRoot represent
* the same content
*/
public boolean copy(@NotNull final NodeState sourceRoot, @NotNull final NodeBuilder targetRoot) {
final NodeStateCopier copier = new NodeStateCopier(includePaths, excludePaths, fragmentPaths, excludeFragments, mergePaths);
return copier.copyNodeState(checkNotNull(sourceRoot), checkNotNull(targetRoot));
}
/**
* Creates a NodeStateCopier to copy the {@code source} NodeStore to the
* {@code target} NodeStore, using any include, exclude and merge paths
* set on this NodeStateCopier.Builder.
* <br>
* Changes are automatically persisted with empty CommitHooks and CommitInfo
* via {@link NodeStore#merge(NodeBuilder, CommitHook, CommitInfo)}.
*
* @param source NodeStore to copy from
* @param target NodeStore to copy to
* @return true if there were any changes, false if source and target represent
* the same content
* @throws CommitFailedException if the copy operation fails
*/
public boolean copy(@NotNull final NodeStore source, @NotNull final NodeStore target)
throws CommitFailedException {
final NodeBuilder targetRoot = checkNotNull(target).getRoot().builder();
if (copy(checkNotNull(source).getRoot(), targetRoot)) {
target.merge(targetRoot, EmptyHook.INSTANCE, CommitInfo.EMPTY);
return true;
}
return false;
}
}
}