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 0347599..fa9aada 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 @@ -456,14 +456,20 @@ public class RepositoryUpgrade { PrivilegeRegistry registry = source.getPrivilegeRegistry(); List customAggrPrivs = Lists.newArrayList(); logger.debug("Registering custom non-aggregated privileges"); for (Privilege privilege : registry.getRegisteredPrivileges()) { String privilegeName = privilege.getName(); + + if (hasPrivilege(pMgr, privilegeName)) { + logger.debug("Privilege {} already exists", privilegeName); + continue; + } + if (PrivilegeBits.BUILT_IN.containsKey(privilegeName) || JCR_ALL.equals(privilegeName)) { // Ignore built in privileges as those have been installed by the PrivilegesInitializer already logger.debug("Built-in privilege -> ignore."); } else if (privilege.isAggregate()) { // postpone customAggrPrivs.add(privilege); } else { @@ -507,14 +513,24 @@ public class RepositoryUpgrade { for (Privilege p : customAggrPrivs) { invalid.append(p.getName()).append('|'); } throw new RepositoryException("Failed to register custom privileges. The following privileges contained an invalid aggregation:" + invalid); } } + private boolean hasPrivilege(PrivilegeManager pMgr, String privilegeName) throws RepositoryException { + final Privilege[] registeredPrivileges = pMgr.getRegisteredPrivileges(); + for (Privilege registeredPrivilege : registeredPrivileges) { + if (registeredPrivilege.getName().equals(privilegeName)) { + return true; + } + } + return false; + } + private static boolean allAggregatesRegistered(PrivilegeManager privilegeManager, List aggrNames) { for (String name : aggrNames) { try { privilegeManager.getPrivilege(name); } catch (RepositoryException e) { return false; } 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 new file mode 100644 index 0000000..46d3ad5 --- /dev/null +++ oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopier.java @@ -0,0 +1,162 @@ +package org.apache.jackrabbit.oak.upgrade.nodestate; + +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 + * recursively copying a NodeState to a NodeBuilder. + *
+ * The copy algorithm is optimized for copying nodes between two + * different NodeStore instances, i.e. where comparing NodeStates + * is imprecise and/or expensive. + *
+ * 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. + */ +public class NodeStateCopier { + + private static final Logger LOG = LoggerFactory.getLogger(NodeStateCopier.class); + + + private NodeStateCopier() { + // no instances + } + + /** + * 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 + */ + 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; + } + + /** + * 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; + } + + /** + * 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}. + *
+ * Note: 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. + */ + public 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)) { + 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 mergePaths) { + for (String mergePath : mergePaths) { + if (PathUtils.isAncestor(mergePath, path) || mergePath.equals(path)) { + return true; + } + } + return false; + } +} diff --git oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/security/GroupEditor.java oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/security/GroupEditor.java index b733661..9380aae 100644 --- oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/security/GroupEditor.java +++ oak-upgrade/src/main/java/org/apache/jackrabbit/oak/upgrade/security/GroupEditor.java @@ -104,15 +104,15 @@ class GroupEditor extends DefaultEditor { } } return this; } @Override public Editor childNodeChanged(String name, NodeState before, NodeState after) { - throw new IllegalStateException("changed node during upgrade copy not expected: " + state.path + "/" + name); + return null; } @Override public Editor childNodeDeleted(String name, NodeState before) { throw new IllegalStateException("deleted node during upgrade copy not expected: " + state.path + "/" + name); } diff --git oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java index 839ff87..ad7c9d1 100644 --- oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java +++ oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/AbstractRepositoryUpgradeTest.java @@ -15,20 +15,22 @@ * 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 java.io.File; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.jcr.Credentials; import javax.jcr.Repository; import javax.jcr.RepositoryException; +import javax.jcr.Session; import javax.jcr.SimpleCredentials; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.jackrabbit.api.JackrabbitSession; import org.apache.jackrabbit.core.RepositoryImpl; import org.apache.jackrabbit.core.config.RepositoryConfig; @@ -36,67 +38,122 @@ import org.apache.jackrabbit.oak.Oak; import org.apache.jackrabbit.oak.jcr.Jcr; import org.apache.jackrabbit.oak.plugins.segment.SegmentNodeStore; import org.apache.jackrabbit.oak.spi.state.NodeStore; import org.apache.jackrabbit.oak.stats.Clock; import org.junit.Before; import org.junit.BeforeClass; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + public abstract class AbstractRepositoryUpgradeTest { protected static final Credentials CREDENTIALS = new SimpleCredentials("admin", "admin".toCharArray()); - private static Repository targetRepository; + private static NodeStore targetNodeStore; + + private static File testDirectory; @BeforeClass - public static void init() { + public static void init() throws InterruptedException { // ensure that we create a new repository for the next test - targetRepository = null; + targetNodeStore = null; + testDirectory = createTestDirectory(); + } + + protected static File createTestDirectory() throws InterruptedException { + final File dir = new File("target", "upgrade-" + Clock.SIMPLE.getTimeIncreasing()); + FileUtils.deleteQuietly(dir); + return dir; + } + + protected NodeStore createTargetNodeStore() { + return new SegmentNodeStore(); } @Before public synchronized void upgradeRepository() throws Exception { - if (targetRepository == null) { - File directory = new File( - "target", "upgrade-" + Clock.SIMPLE.getTimeIncreasing()); - FileUtils.deleteQuietly(directory); - + if (targetNodeStore == null) { + File directory = getTestDirectory(); File source = new File(directory, "source"); source.mkdirs(); - - InputStream repoConfig = getRepositoryConfig(); - RepositoryConfig config; - if (repoConfig == null) { - config = RepositoryConfig.install(source); - } else { - OutputStream out = FileUtils.openOutputStream(new File(source, "repository.xml")); - IOUtils.copy(repoConfig, out); - out.close(); - repoConfig.close(); - config = RepositoryConfig.create(source); - } - RepositoryImpl repository = RepositoryImpl.create(config); + RepositoryImpl repository = createSourceRepository(source); try { createSourceContent(repository); } finally { repository.shutdown(); } - NodeStore target = new SegmentNodeStore(); - RepositoryUpgrade.copy(source, target); - targetRepository = new Jcr(new Oak(target)).createRepository(); + final NodeStore target = getTargetNodeStore(); + doUpgradeRepository(source, target); + targetNodeStore = target; } } - public InputStream getRepositoryConfig() { + protected synchronized NodeStore getTargetNodeStore() { + if (targetNodeStore == null) { + targetNodeStore = createTargetNodeStore(); + } + return targetNodeStore; + } + + protected static File getTestDirectory() { + return testDirectory; + } + + protected RepositoryImpl createSourceRepository(File repositoryHome) throws IOException, RepositoryException { + InputStream repoConfig = getRepositoryConfig(); + RepositoryConfig config; + if (repoConfig == null) { + config = RepositoryConfig.install(repositoryHome); + } else { + OutputStream out = FileUtils.openOutputStream(new File(repositoryHome, "repository.xml")); + IOUtils.copy(repoConfig, out); + out.close(); + repoConfig.close(); + config = RepositoryConfig.create(repositoryHome); + } + return RepositoryImpl.create(config); + } + + + protected void doUpgradeRepository(File source, NodeStore target)throws RepositoryException{ + RepositoryUpgrade.copy(source, target); + } + + public InputStream getRepositoryConfig(){ return null; } - public Repository getTargetRepository() { - return targetRepository; + public Repository getTargetRepository(){ + return new Jcr(new Oak(targetNodeStore)).createRepository(); } - public JackrabbitSession createAdminSession() throws RepositoryException { - return (JackrabbitSession) getTargetRepository().login(CREDENTIALS); + public JackrabbitSession createAdminSession()throws RepositoryException{ + return(JackrabbitSession)getTargetRepository().login(CREDENTIALS); } protected abstract void createSourceContent(Repository repository) throws Exception; + protected void assertExisting(final String... paths) throws RepositoryException { + final Session session = createAdminSession(); + try { + for (final String path : paths) { + final String relPath = path.substring(1); + assertTrue("node " + path + " should exist", session.getRootNode().hasNode(relPath)); + } + } finally { + session.logout(); + } + } + + protected void assertMissing(final String... paths) throws RepositoryException { + final Session session = createAdminSession(); + try { + for (final String path : paths) { + final String relPath = path.substring(1); + assertFalse("node " + path + " should not exist", session.getRootNode().hasNode(relPath)); + } + } finally { + session.logout(); + } + } } diff --git oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/RepeatedRepositoryUpgradeTest.java oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/RepeatedRepositoryUpgradeTest.java new file mode 100644 index 0000000..9ae58fe --- /dev/null +++ oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/RepeatedRepositoryUpgradeTest.java @@ -0,0 +1,203 @@ +package org.apache.jackrabbit.oak.upgrade; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +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.lifecycle.RepositoryInitializer; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +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.annotation.Nonnull; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.io.File; +import java.io.IOException; + +/** + * Test case to simulate an incremental upgrade, where a source repository is + * copied to target initially. Then some modifications are made in the source + * repository and these are (incrementally) copied to the target repository. + *
+ * The expectation is that in the end the state in the target repository is + * identical to the state in the source repository, with the exception of any + * initial content that the upgrade tool created. + */ +public class RepeatedRepositoryUpgradeTest 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 sourceDir = new File(getTestDirectory(), "jackrabbit2"); + + sourceDir.mkdirs(); + + RepositoryImpl source = createSourceRepository(sourceDir); + + try { + createSourceContent(source); + } finally { + source.shutdown(); + } + + final NodeStore target = getTargetNodeStore(); + doUpgradeRepository(sourceDir, target); + fileStore.flush(); + + // re-create source repo + source = createSourceRepository(sourceDir); + try { + modifySourceContent(source); + } finally { + source.shutdown(); + } + + doUpgradeRepository(sourceDir, target); + fileStore.flush(); + + upgradeComplete = true; + } + } + + @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 repositoryUpgrade = new RepositoryUpgrade(context, target); + repositoryUpgrade.copy(new RepositoryInitializer() { + @Override + public void initialize(@Nonnull NodeBuilder builder) { + builder.child("foo").child("bar"); + } + }); + } finally { + context.getRepository().shutdown(); + } + } + + @Override + protected void createSourceContent(Repository repository) throws RepositoryException { + Session session = null; + try { + session = repository.login(CREDENTIALS); + + registerCustomPrivileges(session); + + JcrUtils.getOrCreateByPath("/content/child1/grandchild1", "nt:unstructured", session); + JcrUtils.getOrCreateByPath("/content/child1/grandchild2", "nt:unstructured", session); + JcrUtils.getOrCreateByPath("/content/child1/grandchild3", "nt:unstructured", session); + JcrUtils.getOrCreateByPath("/content/child2/grandchild1", "nt:unstructured", session); + JcrUtils.getOrCreateByPath("/content/child2/grandchild2", "nt:unstructured", session); + + session.save(); + } finally { + if (session != null && session.isLive()) { + session.logout(); + } + } + } + + private void modifySourceContent(Repository repository) throws RepositoryException { + Session session = null; + try { + session = repository.login(CREDENTIALS); + + JcrUtils.getOrCreateByPath("/content/child2/grandchild3", "nt:unstructured", session); + JcrUtils.getOrCreateByPath("/content/child3", "nt:unstructured", session); + + final Node child1 = JcrUtils.getOrCreateByPath("/content/child1", "nt:unstructured", session); + child1.remove(); + + session.save(); + } finally { + if (session != null && session.isLive()) { + session.logout(); + } + } + } + + private void registerCustomPrivileges(Session session) throws RepositoryException { + final JackrabbitWorkspace workspace = (JackrabbitWorkspace) session.getWorkspace(); + + final NamespaceRegistry registry = workspace.getNamespaceRegistry(); + registry.registerNamespace("test", "http://www.example.org/"); + + final PrivilegeManager privilegeManager = workspace.getPrivilegeManager(); + privilegeManager.registerPrivilege("test:privilege", false, null); + privilegeManager.registerPrivilege( + "test:aggregate", false, new String[]{"jcr:read", "test:privilege"}); + } + + @Test + public void shouldReflectSourceAfterModifications() throws Exception { + + assertExisting( + "/", + "/content", + "/content/child2", + "/content/child2/grandchild1", + "/content/child2/grandchild2", + "/content/child2/grandchild3", + "/content/child3" + ); + + assertMissing( + "/content/child1" + ); + } + + @Test + public void shouldContainCustomInitializerContent() throws Exception { + assertExisting( + "/foo", + "/foo/bar" + ); + } + + @Test + public void shouldContainUpgradeInitializedContent() throws Exception { + assertExisting( + "/rep:security", + "/oak:index" + ); + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000..a5b6747 --- /dev/null +++ oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/nodestate/NodeStateCopierTest.java @@ -0,0 +1,165 @@ +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.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 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(); + + expectDifference().strict().verify(before, after); + expectDifference().strict().verify(source.getRoot(), after); + } + + private NodeStore createPrefilledNodeStore() throws CommitFailedException { + final NodeStore store = createNodeStoreWithContent(); + final NodeBuilder builder = store.getRoot().builder(); + create(builder, "/excluded"); + create(builder, "/a", primaryType, createProperty("name", "a")); + create(builder, "/a/b", primaryType, createProperty("name", "b")); + create(builder, "/a/b/excluded"); + create(builder, "/a/b/c", primaryType, createProperty("name", "c")); + create(builder, "/a/b/c/d", primaryType); + create(builder, "/a/b/c/e", primaryType); + create(builder, "/a/b/c/f", primaryType); + commit(store, builder); + return store; + } + + @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); + final NodeState after = target.getRoot(); + + expectDifference() + .strict() + .childNodeAdded("/content/foo/de") + .childNodeChanged("/content", "/content/foo") + .verify(source.getRoot(), 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); + 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(); + 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); + final NodeState after = target.getRoot(); + + expectDifference() + .strict() + .propertyDeleted("/a/jcr:primaryType") + .childNodeChanged("/a") + .verify(before, after); + } + + @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); + final NodeState after = target.getRoot(); + + expectDifference() + .strict() + .childNodeAdded("/content/foo") + .childNodeChanged("/content") + .verify(before, after); + } + + @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); + 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); + final NodeState after = target.getRoot(); + + expectDifference() + .strict() + .childNodeAdded("/content/foo") + .childNodeChanged("/content") + .childNodeDeleted("/content/bar") + .verify(before, after); + } + +} 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 new file mode 100644 index 0000000..474f4ce --- /dev/null +++ oak-upgrade/src/test/java/org/apache/jackrabbit/oak/upgrade/util/NodeStateTestUtils.java @@ -0,0 +1,187 @@ +package org.apache.jackrabbit.oak.upgrade.util; + +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.segment.SegmentNodeStore; +import org.apache.jackrabbit.oak.spi.commit.CommitInfo; +import org.apache.jackrabbit.oak.spi.commit.DefaultValidator; +import org.apache.jackrabbit.oak.spi.commit.EditorDiff; +import org.apache.jackrabbit.oak.spi.commit.EmptyHook; +import org.apache.jackrabbit.oak.spi.commit.Validator; +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 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; + +public class NodeStateTestUtils { + + private NodeStateTestUtils() { + // no instances + } + + public static NodeStore createNodeStoreWithContent(String... paths) throws CommitFailedException { + final SegmentNodeStore store = new SegmentNodeStore(); + final NodeBuilder builder = store.getRoot().builder(); + for (String path : paths) { + create(builder, path); + } + commit(store, builder); + return store; + } + + public static void create(NodeBuilder rootBuilder, String path, PropertyState... properties) { + final NodeBuilder builder = createOrGetBuilder(rootBuilder, path); + for (PropertyState property : properties) { + builder.setProperty(property); + } + } + + public static void commit(NodeStore store, NodeBuilder rootBuilder) throws CommitFailedException { + store.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY); + } + + 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 ExpectedDifference expectDifference() { + return new ExpectedDifference(); + } + + public static class ExpectedDifference { + + private ExpectedDifference() { + } + + private final Map> expected = new HashMap>(); + + public void verify(NodeState before, NodeState after) { + final Map> actual = TestValidator.compare(before, after); + for (String type : expected.keySet()) { + if (!actual.containsKey(type)) { + actual.put(type, Collections.emptySet()); + } + assertEquals(type, expected.get(type), actual.get(type)); + } + } + + public ExpectedDifference propertyAdded(String... paths) { + return expect("propertyAdded", paths); + } + + public ExpectedDifference propertyChanged(String... paths) { + return expect("propertyChanged", paths); + } + + public ExpectedDifference propertyDeleted(String... paths) { + return expect("propertyDeleted", paths); + } + + public ExpectedDifference childNodeAdded(String... paths) { + return expect("childNodeAdded", paths); + } + + public ExpectedDifference childNodeChanged(String... paths) { + return expect("childNodeChanged", paths); + } + + public ExpectedDifference childNodeDeleted(String... paths) { + return expect("childNodeDeleted", paths); + } + + public ExpectedDifference strict() { + return this.propertyAdded() + .propertyChanged() + .propertyDeleted() + .childNodeAdded() + .childNodeChanged() + .childNodeDeleted(); + } + + private ExpectedDifference expect(String type, String... paths) { + if (!expected.containsKey(type)) { + expected.put(type, new TreeSet()); + } + Collections.addAll(expected.get(type), paths); + return this; + } + } + + private static class TestValidator extends DefaultValidator { + + final Map> actual = new HashMap>(); + + String path = "/"; + + public static Map> compare(NodeState before, NodeState after) { + final TestValidator validator = new TestValidator(); + EditorDiff.process(validator, before, after); + return validator.actual; + } + + @Override + public void leave(NodeState before, NodeState after) throws CommitFailedException { + path = PathUtils.getParentPath(path); + } + + @Override + public void propertyAdded(PropertyState after) throws CommitFailedException { + record("propertyAdded", PathUtils.concat(path, after.getName())); + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) throws CommitFailedException { + record("propertyChanged", PathUtils.concat(path, after.getName())); + } + + @Override + public void propertyDeleted(PropertyState before) throws CommitFailedException { + record("propertyDeleted", PathUtils.concat(path, before.getName())); + } + + @Override + public Validator childNodeAdded(String name, NodeState after) throws CommitFailedException { + path = PathUtils.concat(path, name); + record("childNodeAdded", path); + return this; + } + + @Override + public Validator childNodeChanged(String name, NodeState before, NodeState after) + throws CommitFailedException { + // make sure not to record false positives (inefficient for large trees) + if (!before.equals(after)) { + path = PathUtils.concat(path, name); + record("childNodeChanged", path); + return this; + } + return null; + } + + @Override + public Validator childNodeDeleted(String name, NodeState before) throws CommitFailedException { + path = PathUtils.concat(path, name); + record("childNodeDeleted", path); + return this; + } + + private void record(String type, String path) { + if (!actual.containsKey(type)) { + actual.put(type, new TreeSet()); + } + actual.get(type).add(path); + } + } +}