diff --git a/vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/IsSubtreeFullyOverwrittenIT.java b/vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/IsSubtreeFullyOverwrittenIT.java new file mode 100644 index 000000000..30663455a --- /dev/null +++ b/vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/IsSubtreeFullyOverwrittenIT.java @@ -0,0 +1,326 @@ +/* + * 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.vault.packaging.integration; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import java.io.IOException; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.vault.fs.api.ImportMode; +import org.apache.jackrabbit.vault.fs.api.PathFilter; +import org.apache.jackrabbit.vault.fs.api.PathFilterSet; +import org.apache.jackrabbit.vault.fs.config.ConfigurationException; +import org.apache.jackrabbit.vault.fs.config.DefaultWorkspaceFilter; +import org.apache.jackrabbit.vault.fs.filter.DefaultPathFilter; +import org.apache.jackrabbit.vault.fs.io.Archive; +import org.apache.jackrabbit.vault.fs.io.ImportOptions; +import org.apache.jackrabbit.vault.fs.io.Importer; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Integration tests for WorkspaceFilter#isSubtreeFullyOverwritten() + */ +public class IsSubtreeFullyOverwrittenIT extends IntegrationTestBase { + + private static final String TEST_ROOT = "/tmp/isSubtreeFullyOverwritten"; + private Node rootNode; + + @Before + public void setUp() throws Exception { + super.setUp(); + clean(TEST_ROOT); + + rootNode = JcrUtils.getOrCreateByPath(TEST_ROOT, JcrConstants.NT_UNSTRUCTURED, admin); + } + + /** + * Path is outside all filter roots: no covering filter set. + * Expects false (early exit, no repository traversal). + */ + @Test + public void returnsFalseWhenPathNotCoveredByAnyFilter() throws RepositoryException, ConfigurationException { + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet("/other/root"); + set.addInclude(new DefaultPathFilter("/other/root(/.*)?")); + filter.add(set); + + Node root = JcrUtils.getOrCreateByPath(TEST_ROOT + "/content", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertFalse(filter.isSubtreeFullyCovered(rootNode)); + } + + /** + * Filter has MERGE_PROPERTIES (not REPLACE). Subtree must not be considered fully overwritten. + * Expects false (import mode check). + */ + @Test + public void returnsFalseWhenImportModeIsNotReplace() throws RepositoryException, ConfigurationException { + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet(TEST_ROOT); + set.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); + set.setImportMode(ImportMode.MERGE_PROPERTIES); + filter.add(set); + + Node n = JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertFalse(filter.isSubtreeFullyCovered(n)); + } + + /** + * Path matches global-ignored filter. Must not traverse or consider overwritten. + * Expects false (global ignored check). + */ + @Test + public void returnsFalseWhenPathIsGloballyIgnored() throws RepositoryException, ConfigurationException { + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet(TEST_ROOT); + set.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); + filter.add(set); + filter.setGlobalIgnored(PathFilter.ALL); + + Node n = JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertFalse(filter.isSubtreeFullyCovered(n)); + } + + /** + * Parent is included, but a child is excluded by filter. Recursive check finds child not contained. + * Expects false (contains() fails for excluded descendant). + */ + @Test + public void returnsFalseWhenChildNodeIsExcludedByFilter() throws RepositoryException, ConfigurationException { + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet(TEST_ROOT); + set.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); + set.addExclude(new DefaultPathFilter(TEST_ROOT + "/parent/excluded(/.*)?")); + filter.add(set); + + Node p = JcrUtils.getOrCreateByPath(TEST_ROOT + "/parent", JcrConstants.NT_UNSTRUCTURED, admin); + JcrUtils.getOrCreateByPath(TEST_ROOT + "/parent/excluded", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertFalse(filter.isSubtreeFullyCovered(p)); + } + + /** + * Subtree exists, REPLACE mode, all nodes and properties included. Recursive traversal succeeds. + * Expects true (full overwrite allowed). + */ + @Test + public void returnsTrueWhenSubtreeExistsAndFullyIncluded() throws RepositoryException, ConfigurationException { + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet(TEST_ROOT); + set.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); + filter.add(set); + + Node p = JcrUtils.getOrCreateByPath(TEST_ROOT + "/parent", JcrConstants.NT_UNSTRUCTURED, admin); + JcrUtils.getOrCreateByPath(TEST_ROOT + "/parent/child", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertTrue(filter.isSubtreeFullyCovered(p)); + } + + /** + * Single node, no children. All properties (e.g. jcr:primaryType) included. Edge case for recursion. + * Expects true. + */ + @Test + public void returnsTrueWhenLeafNodeHasNoChildren() throws RepositoryException, ConfigurationException { + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet(TEST_ROOT); + set.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); + filter.add(set); + + Node l = JcrUtils.getOrCreateByPath(TEST_ROOT + "/leaf", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertTrue(filter.isSubtreeFullyCovered(l)); + } + + /** + * Global-ignored filter matches a different path; test path is not ignored. Check proceeds normally. + * Expects true (global ignored does not apply). + */ + @Test + public void returnsTrueWhenGlobalIgnoredDoesNotMatchPath() throws RepositoryException, ConfigurationException { + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet(TEST_ROOT); + set.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); + filter.add(set); + filter.setGlobalIgnored(new DefaultPathFilter("/other/ignored(/.*)?")); + + Node n = JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertTrue(filter.isSubtreeFullyCovered(n)); + } + + /** + * Property filter excludes a property on the node. includesProperty() fails during traversal. + * Expects false (property exclusion prevents full overwrite). + */ + @Test + public void returnsFalseWhenPropertyIsExcludedByFilter() throws RepositoryException, ConfigurationException { + PathFilterSet nodeSet = new PathFilterSet(TEST_ROOT); + nodeSet.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); + PathFilterSet propSet = new PathFilterSet(TEST_ROOT); + propSet.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); + propSet.addExclude(new DefaultPathFilter(".*/customProp")); + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + filter.add(nodeSet, propSet); + + Node node = JcrUtils.getOrCreateByPath(TEST_ROOT + "/withProp", JcrConstants.NT_UNSTRUCTURED, admin); + node.setProperty("customProp", "value"); + admin.save(); + + assertFalse(filter.isSubtreeFullyCovered(node)); + } + + /** + * JCRVLT-830: Repo has a parent (e.g. content/mysite/en) and a child (page) that is excluded by the filter. + * When importing a package that does not contain that child, the importer may only remove it if the subtree + * is fully overwritten. Here the child is excluded, so the subtree is not fully overwritten. + * Expects false so the importer keeps the existing child instead of removing it. + */ + @Test + public void jcrvlt830ReturnsFalseWhenExistingChildInRepoIsExcludedByFilter() + throws RepositoryException, ConfigurationException { + String contentRoot = TEST_ROOT + "/content/mysite"; + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet(contentRoot); + set.addInclude(new DefaultPathFilter(contentRoot + "(/.*)?")); + set.addExclude(new DefaultPathFilter(contentRoot + "/en/page(/.*)?")); + filter.add(set); + filter.setExtraValidationBeforeSubtreeRemoval(true); + + Node n = JcrUtils.getOrCreateByPath(contentRoot + "/en", JcrConstants.NT_UNSTRUCTURED, admin); + JcrUtils.getOrCreateByPath(contentRoot + "/en/page", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertFalse(filter.isSubtreeFullyCovered(n)); + } + + /** + * Same tree as {@link #jcrvlt830ReturnsFalseWhenExistingChildInRepoIsExcludedByFilter} but with extra validation + * disabled: {@link DefaultWorkspaceFilter#setExtraValidationBeforeSubtreeRemoval(boolean)} {@code false}. + */ + @Test + public void whenExtraValidationBeforeSubtreeRemovalDisabled_excludedChildReportsSubtreeCovered() + throws RepositoryException, ConfigurationException { + String contentRoot = TEST_ROOT + "/content/mysite/disabled"; + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet(contentRoot); + set.addInclude(new DefaultPathFilter(contentRoot + "(/.*)?")); + set.addExclude(new DefaultPathFilter(contentRoot + "/en/page(/.*)?")); + filter.add(set); + filter.setExtraValidationBeforeSubtreeRemoval(false); + + Node n = JcrUtils.getOrCreateByPath(contentRoot + "/en", JcrConstants.NT_UNSTRUCTURED, admin); + JcrUtils.getOrCreateByPath(contentRoot + "/en/page", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertTrue(filter.isSubtreeFullyCovered(n)); + } + + /** + * JCRVLT-830: When extra validation before subtree removal is disabled, a full import run must actually remove + * nodes that are no longer in the package, even if a sibling is excluded by the filter (legacy behavior). + */ + @Test + public void whenExtraValidationBeforeSubtreeRemovalDisabled_subtreeIsRemovedOnReimport() + throws IOException, RepositoryException, ConfigurationException { + String importRoot = "/tmp"; + clean(importRoot); + + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet(importRoot); + set.addInclude(new DefaultPathFilter(importRoot + "(/.*)?")); + set.addExclude(new DefaultPathFilter(importRoot + "/foo/bar/excluded(/.*)?")); + filter.add(set); + filter.setExtraValidationBeforeSubtreeRemoval(false); + + ImportOptions opts = getDefaultOptions(); + opts.setFilter(filter); + Importer importer = new Importer(opts); + Node rootNode = admin.getRootNode(); + + try (Archive archive = getFileArchive("/test-packages/tmp.zip")) { + archive.open(true); + importer.run(archive, rootNode); + } + assertNodeExists("/tmp/foo/bar/tobi"); + + JcrUtils.getOrCreateByPath("/tmp/foo/bar/excluded", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + try (Archive archive = getFileArchive("/test-packages/tmp_less.zip")) { + archive.open(true); + importer.run(archive, rootNode); + } + assertNodeMissing("/tmp/foo/bar/tobi"); + } + + /** + * JCRVLT-830: When extra validation before subtree removal is enabled (default), a full import run must not remove + * nodes when a sibling is excluded by the filter (subtree not fully covered). + */ + @Test + public void whenExtraValidationBeforeSubtreeRemovalEnabled_subtreeIsPreservedOnReimport() + throws IOException, RepositoryException, ConfigurationException { + String importRoot = "/tmp"; + clean(importRoot); + + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet(importRoot); + set.addInclude(new DefaultPathFilter(importRoot + "(/.*)?")); + set.addExclude(new DefaultPathFilter(importRoot + "/foo/bar/excluded(/.*)?")); + filter.add(set); + filter.setExtraValidationBeforeSubtreeRemoval(true); + + ImportOptions opts = getDefaultOptions(); + opts.setFilter(filter); + Importer importer = new Importer(opts); + Node rootNode = admin.getRootNode(); + + try (Archive archive = getFileArchive("/test-packages/tmp.zip")) { + archive.open(true); + importer.run(archive, rootNode); + } + assertNodeExists("/tmp/foo/bar/tobi"); + + JcrUtils.getOrCreateByPath("/tmp/foo/bar/excluded", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + try (Archive archive = getFileArchive("/test-packages/tmp_less.zip")) { + archive.open(true); + importer.run(archive, rootNode); + } + assertNodeExists("/tmp/foo/bar/tobi"); + } +} diff --git a/vault-core-it/vault-core-it-execution/vault-core-it-execution-oak-min/pom.xml b/vault-core-it/vault-core-it-execution/vault-core-it-execution-oak-min/pom.xml index 00e32bab5..66654128c 100644 --- a/vault-core-it/vault-core-it-execution/vault-core-it-execution-oak-min/pom.xml +++ b/vault-core-it/vault-core-it-execution/vault-core-it-execution-oak-min/pom.xml @@ -1,4 +1,5 @@ - - 4.0.0 - - - - - org.apache.jackrabbit.vault - vault-core-it-execution - ${revision} - + 4.0.0 + + + + + org.apache.jackrabbit.vault + vault-core-it-execution + ${revision} + - - - - vault-core-it-execution-oak-min + + + + vault-core-it-execution-oak-min - Apache Jackrabbit FileVault Core IT Execution Oak Minimum Version - Executes Core ITs with minimally supported Oak version + Apache Jackrabbit FileVault Core IT Execution Oak Minimum Version + Executes Core ITs with minimally supported Oak version - - - - - - - maven-failsafe-plugin - - - + + + + + + org.apache.jackrabbit.vault + vault-core-it-support-oak + ${project.version} + runtime + + + org.apache.jackrabbit + oak-core + ${oak.min.version} + runtime + + + org.apache.jackrabbit + oak-core + ${oak.min.version} + tests + runtime + + + org.apache.jackrabbit + jackrabbit-data + 2.20.1 + runtime + + + org.apache.jackrabbit + oak-jcr + ${oak.min.version} + runtime + + + org.apache.jackrabbit + oak-segment-tar + ${oak.min.version} + runtime + + + + io.dropwizard.metrics + metrics-core + 3.2.3 + runtime + + + org.apache.jackrabbit + oak-authorization-principalbased + ${oak.min.version} + runtime + + + org.apache.jackrabbit + oak-authorization-cug + ${oak.min.version} + runtime + + - - - - - - org.apache.jackrabbit.vault - vault-core-it-support-oak - ${project.version} - runtime - - - org.apache.jackrabbit - oak-core - ${oak.min.version} - runtime - - - org.apache.jackrabbit - oak-core - ${oak.min.version} - tests - runtime - - - org.apache.jackrabbit - jackrabbit-data - 2.20.1 - runtime - - - org.apache.jackrabbit - oak-jcr - ${oak.min.version} - runtime - - - org.apache.jackrabbit - oak-segment-tar - ${oak.min.version} - runtime - - - - io.dropwizard.metrics - metrics-core - 3.2.3 - runtime - - - org.apache.jackrabbit - oak-authorization-principalbased - ${oak.min.version} - runtime - - - org.apache.jackrabbit - oak-authorization-cug - ${oak.min.version} - runtime - - + + + + + + + maven-failsafe-plugin + + + diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/WorkspaceFilter.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/WorkspaceFilter.java index 8b41fb250..e17657af9 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/WorkspaceFilter.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/WorkspaceFilter.java @@ -182,4 +182,24 @@ void dumpCoverage(@NotNull Session session, @NotNull ProgressTrackerListener lis * @return {@code true} if the property is included in the filter */ boolean includesProperty(String propertyPath); + + /** + * Returns whether the given path's subtree is supposed to be fully covered during import, + * by traversing the repository and checking that every node and property in the subtree is + * included by this filter. Returns {@code true} only when: + * + * When this method returns {@code true}, an importer may safely remove the existing node at + * the path and replace it and its children with the package content. When it returns {@code false}, + * removal should be avoided or done selectively. + * @param subTree TODO + * + * @return {@code true} if every node and property on the node {subTree} and its children is included and mode is REPLACE + * @throws RepositoryException if the path does not exist or traversal fails + */ + boolean isSubtreeFullyCovered(@NotNull Node subTree) throws RepositoryException; } diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/package-info.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/package-info.java index 82607baef..d9655674a 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/package-info.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/package-info.java @@ -15,7 +15,7 @@ * limitations under the License. */ -@Version("2.13.0") +@Version("2.14.0") package org.apache.jackrabbit.vault.fs.api; import org.osgi.annotation.versioning.Version; diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/config/DefaultWorkspaceFilter.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/config/DefaultWorkspaceFilter.java index da970466e..5defc016d 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/config/DefaultWorkspaceFilter.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/config/DefaultWorkspaceFilter.java @@ -19,6 +19,8 @@ package org.apache.jackrabbit.vault.fs.config; import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.xml.parsers.DocumentBuilder; @@ -106,6 +108,13 @@ public class DefaultWorkspaceFilter implements Dumpable, WorkspaceFilter { */ private ImportMode importMode; + /** + * When {@code true} (default), {@link #isSubtreeFullyCovered(javax.jcr.Node)} performs the full subtree check + * (JCRVLT-830). When {@code false}, that method always returns {@code true} so importers behave as before the + * extra validation. Not persisted; external configuration will be wired in a later step. + */ + private boolean extraValidationBeforeSubtreeRemoval = true; + /** * Add a #PathFilterSet for nodes items. * @param set the set of filters to add. @@ -278,6 +287,57 @@ public boolean includesProperty(String propertyPath) { return false; } + /** + * Enables or disables extra validation reflected by {@link #isSubtreeFullyCovered(javax.jcr.Node)} (JCRVLT-830). + * @param extraValidationBeforeSubtreeRemoval {@code true} to perform the subtree check; {@code false} to always + * report fully covered (legacy behavior for removal gating) + */ + public void setExtraValidationBeforeSubtreeRemoval(boolean extraValidationBeforeSubtreeRemoval) { + this.extraValidationBeforeSubtreeRemoval = extraValidationBeforeSubtreeRemoval; + } + + @Override + public boolean isSubtreeFullyCovered(javax.jcr.Node subTree) throws RepositoryException { + if (!extraValidationBeforeSubtreeRemoval) { + return true; + } + if (subTree == null) { + return false; + } + String path = subTree.getPath(); + if (isGloballyIgnored(path)) { + return false; + } + if (getCoveringFilterSet(path) == null) { + return false; + } + if (getImportMode(path) != ImportMode.REPLACE) { + return false; + } + return isSubtreeFullyOverwrittenRecursive(subTree); + } + + private boolean isSubtreeFullyOverwrittenRecursive(javax.jcr.Node node) throws RepositoryException { + String nodePath = node.getPath(); + if (!contains(nodePath)) { + return false; + } + PropertyIterator props = node.getProperties(); + while (props.hasNext()) { + Property prop = props.nextProperty(); + if (!includesProperty(prop.getPath())) { + return false; + } + } + NodeIterator children = node.getNodes(); + while (children.hasNext()) { + if (!isSubtreeFullyOverwrittenRecursive((javax.jcr.Node) children.nextNode())) { + return false; + } + } + return true; + } + /** * {@inheritDoc} */ @@ -333,6 +393,7 @@ public WorkspaceFilter translate(PathMapping mapping) { for (PathFilterSet set : propsFilterSets) { mapped.propsFilterSets.add(set.translate(mapping)); } + mapped.setExtraValidationBeforeSubtreeRemoval(extraValidationBeforeSubtreeRemoval); return mapped; } diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/config/package-info.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/config/package-info.java index a29b5cbfa..5794b22a8 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/config/package-info.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/config/package-info.java @@ -15,7 +15,7 @@ * limitations under the License. */ -@Version("2.9.0") +@Version("2.10.0") package org.apache.jackrabbit.vault.fs.config; import org.osgi.annotation.versioning.Version; diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/DocViewImporter.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/DocViewImporter.java index 983febdba..ed4e93aff 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/DocViewImporter.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/DocViewImporter.java @@ -379,6 +379,7 @@ public void startDocViewNode( throws IOException, RepositoryException { stack.addName(docViewNode.getSnsAwareName()); Node node = stack.getNode(); + log.debug("startDocViewNode(), nodePath= {}, node={}", nodePath, node != null ? node.getPath() : null); if (node == null) { stack = stack.push(); DocViewAdapter xform = stack.getAdapter(); @@ -494,6 +495,7 @@ public void endDocViewNode( log.trace("Sysview transformation complete."); } } else { + log.debug("endDocViewNode(), nodePath= {}, node={}", nodePath, node.getPath()); NodeIterator iter = node.getNodes(); EffectiveNodeType entParent = null; // initialize once when required while (iter.hasNext()) { @@ -505,9 +507,12 @@ public void endDocViewNode( if (!childNames.contains(label) && !hints.contains(path) && isIncluded(child, child.getDepth() - rootDepth)) { - // if the child is in the filter, it belongs to - // this aggregate and needs to be removed - if (aclManagement.isACLNode(child)) { + // Only remove or clear when the parent's subtree is fully overwritten by the filter (JCRVLT-830) + if (!wspFilter.isSubtreeFullyCovered(node)) { + log.debug( + "Skipping removal of child node {} because parent's subtree is not fully overwritten", + path); + } else if (aclManagement.isACLNode(child)) { if (acHandling == AccessControlHandling.OVERWRITE || acHandling == AccessControlHandling.CLEAR) { importInfo.onDeleted(path); diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/FolderArtifactHandler.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/FolderArtifactHandler.java index 969bf306a..ebb0cc709 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/FolderArtifactHandler.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/FolderArtifactHandler.java @@ -158,7 +158,10 @@ public ImportInfoImpl accept( while (iter.hasNext()) { Node child = iter.nextNode(); String path = child.getPath(); - if (wspFilter.contains(path) && wspFilter.getImportMode(path) == ImportMode.REPLACE) { + // Only remove when parent's subtree is fully overwritten (JCRVLT-830) + if (wspFilter.contains(path) + && wspFilter.getImportMode(path) == ImportMode.REPLACE + && wspFilter.isSubtreeFullyCovered(node)) { if (!hints.contains(path)) { // if the child is in the filter, it belongs to // this aggregate and needs to be removed diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/Importer.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/Importer.java index 2fba58fad..a94cfad9c 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/Importer.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/Importer.java @@ -285,6 +285,12 @@ public class Importer { private final boolean isStrictByDefault; private final boolean overwritePrimaryTypesOfFoldersByDefault; + /** + * When non-null, applied to {@link DefaultWorkspaceFilter} during import (JCRVLT-830 / OSGi). {@code null} leaves the + * filter unchanged (e.g. direct {@code Importer} use with {@link ImportOptions#setFilter}). + */ + private final Boolean extraValidationBeforeSubtreeRemoval; + /** * JCRVLT-683 feature flag. This variable is used to enable the new behavior of stashing principal policies when an * Archive's package properties do not specify a {@code vault.feature.stashPrincipalPolicies} property. @@ -341,10 +347,23 @@ public Importer( boolean isStrictByDefault, boolean overwritePrimaryTypesOfFoldersByDefault, IdConflictPolicy defaultIdConflictPolicy) { + this(opts, isStrictByDefault, overwritePrimaryTypesOfFoldersByDefault, defaultIdConflictPolicy, null); + } + + /** + * @param extraValidationBeforeSubtreeRemoval if non-null, applied to the workspace filter (OSGi / package install path) + */ + public Importer( + ImportOptions opts, + boolean isStrictByDefault, + boolean overwritePrimaryTypesOfFoldersByDefault, + IdConflictPolicy defaultIdConflictPolicy, + Boolean extraValidationBeforeSubtreeRemoval) { this.opts = opts; this.isStrict = opts.isStrict(isStrictByDefault); this.isStrictByDefault = isStrictByDefault; this.overwritePrimaryTypesOfFoldersByDefault = overwritePrimaryTypesOfFoldersByDefault; + this.extraValidationBeforeSubtreeRemoval = extraValidationBeforeSubtreeRemoval; if (!this.opts.hasIdConflictPolicyBeenSet() && defaultIdConflictPolicy != null) { this.opts.setIdConflictPolicy(defaultIdConflictPolicy); } @@ -492,6 +511,10 @@ public void run(Archive archive, Session session, String parentPath) filter.getClass().getName()); } } + if (filter instanceof DefaultWorkspaceFilter && extraValidationBeforeSubtreeRemoval != null) { + ((DefaultWorkspaceFilter) filter) + .setExtraValidationBeforeSubtreeRemoval(extraValidationBeforeSubtreeRemoval); + } // build filter tree for (PathFilterSet set : filter.getFilterSets()) { filterTree.put(set.getRoot(), set); diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/package-info.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/package-info.java index 9783d907f..8f991720e 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/package-info.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/package-info.java @@ -15,7 +15,7 @@ * limitations under the License. */ -@Version("2.16.0") +@Version("2.17.0") package org.apache.jackrabbit.vault.fs.io; import org.osgi.annotation.versioning.Version; diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/JcrPackageImpl.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/JcrPackageImpl.java index 293194f9c..7695591f4 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/JcrPackageImpl.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/JcrPackageImpl.java @@ -391,7 +391,8 @@ private void extract( mgr.getSecurityConfig(), mgr.isStrictByDefault(), mgr.overwritePrimaryTypesOfFoldersByDefault(), - mgr.getDefaultIdConflictPolicy()); + mgr.getDefaultIdConflictPolicy(), + mgr.isExtraValidationBeforeSubtreeRemovalByDefault()); JcrPackage snap = null; if (!opts.isDryRun() && createSnapshot) { ExportOptions eOpts = new ExportOptions(); diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/JcrPackageManagerImpl.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/JcrPackageManagerImpl.java index 559a23e51..2cac422c9 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/JcrPackageManagerImpl.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/JcrPackageManagerImpl.java @@ -107,12 +107,33 @@ public JcrPackageManagerImpl( boolean isStrict, boolean overwritePrimaryTypesOfFoldersByDefault, IdConflictPolicy idConflictPolicy) { + this( + session, + roots, + authIdsForHookExecution, + authIdsForRootInstallation, + isStrict, + overwritePrimaryTypesOfFoldersByDefault, + idConflictPolicy, + true); + } + + public JcrPackageManagerImpl( + @NotNull Session session, + @Nullable String[] roots, + @Nullable String[] authIdsForHookExecution, + @Nullable String[] authIdsForRootInstallation, + boolean isStrict, + boolean overwritePrimaryTypesOfFoldersByDefault, + IdConflictPolicy idConflictPolicy, + boolean extraValidationBeforeSubtreeRemovalByDefault) { this(new JcrPackageRegistry( session, new AbstractPackageRegistry.SecurityConfig(authIdsForHookExecution, authIdsForRootInstallation), isStrict, overwritePrimaryTypesOfFoldersByDefault, idConflictPolicy, + extraValidationBeforeSubtreeRemovalByDefault, roots)); } diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/PackagingImpl.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/PackagingImpl.java index ce47f24bc..3cce42ff3 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/PackagingImpl.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/PackagingImpl.java @@ -123,6 +123,14 @@ public PackagingImpl() {} name = "Default ID Conflict Policy", description = "Default node id conflict policy to use during import") IdConflictPolicy defaultIdConflictPolicy() default IdConflictPolicy.FAIL; + + @AttributeDefinition( + name = "Extra Validation Before Subtree Removal", + description = + "When enabled (default), nodes are only removed during import if the parent's subtree is fully " + + "covered by the filter (JCRVLT-830). When disabled, legacy behavior: remove when path is in " + + "filter and in REPLACE mode.") + boolean extraValidationBeforeSubtreeRemoval() default true; } @Activate @@ -150,7 +158,8 @@ public JcrPackageManager getPackageManager(Session session) { config.authIdsForRootInstallation(), config.isStrict(), config.overwritePrimaryTypesOfFolders(), - config.defaultIdConflictPolicy()); + config.defaultIdConflictPolicy(), + config.extraValidationBeforeSubtreeRemoval()); mgr.setDispatcher(eventDispatcher); setBaseRegistry(mgr.getInternalRegistry(), registries); return mgr; @@ -212,6 +221,7 @@ private JcrPackageRegistry getJcrPackageRegistry(Session session, boolean useBas config.isStrict(), config.overwritePrimaryTypesOfFolders(), config.defaultIdConflictPolicy(), + config.extraValidationBeforeSubtreeRemoval(), config.packageRoots()); registry.setDispatcher(eventDispatcher); if (useBaseRegistry) { diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/ZipVaultPackage.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/ZipVaultPackage.java index 28a16cccd..2c41e8c59 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/ZipVaultPackage.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/impl/ZipVaultPackage.java @@ -46,6 +46,7 @@ import org.apache.jackrabbit.vault.packaging.VaultPackage; import org.apache.jackrabbit.vault.packaging.registry.impl.AbstractPackageRegistry; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -177,6 +178,25 @@ public void extract( boolean isOverwritePrimaryTypesOfFolders, IdConflictPolicy defaultIdConflictPolicy) throws PackageException, RepositoryException { + extract( + session, + opts, + securityConfig, + isStrict, + isOverwritePrimaryTypesOfFolders, + defaultIdConflictPolicy, + null); + } + + public void extract( + Session session, + ImportOptions opts, + @NotNull AbstractPackageRegistry.SecurityConfig securityConfig, + boolean isStrict, + boolean isOverwritePrimaryTypesOfFolders, + IdConflictPolicy defaultIdConflictPolicy, + @Nullable Boolean extraValidationBeforeSubtreeRemoval) + throws PackageException, RepositoryException { extract( prepareExtract( session, @@ -184,7 +204,8 @@ public void extract( securityConfig, isStrict, isOverwritePrimaryTypesOfFolders, - defaultIdConflictPolicy), + defaultIdConflictPolicy, + extraValidationBeforeSubtreeRemoval), null); } @@ -211,6 +232,7 @@ public PackageProperties getProperties() { * @param isStrictByDefault is true if packages should be installed in strict mode by default (if not set otherwise in {@code opts}) * @param overwritePrimaryTypesOfFoldersByDefault if folder aggregates' JCR primary type should be changed if the node is already existing or not * @param defaultIdConflictPolicy the default {@link IdConflictPolicy} to use if no policy is set in {@code opts}. May be {@code null}. + * @param extraValidationBeforeSubtreeRemoval JCRVLT-830 from OSGi when non-null; {@code null} leaves filter default * * @throws javax.jcr.RepositoryException if a repository error during installation occurs. * @throws org.apache.jackrabbit.vault.packaging.PackageException if an error during packaging occurs @@ -223,7 +245,8 @@ protected InstallContextImpl prepareExtract( @NotNull AbstractPackageRegistry.SecurityConfig securityConfig, boolean isStrictByDefault, boolean overwritePrimaryTypesOfFoldersByDefault, - IdConflictPolicy defaultIdConflictPolicy) + IdConflictPolicy defaultIdConflictPolicy, + @Nullable Boolean extraValidationBeforeSubtreeRemoval) throws PackageException, RepositoryException { if (!isValid()) { throw new IllegalStateException("Package not valid."); @@ -245,7 +268,11 @@ protected InstallContextImpl prepareExtract( } Importer importer = new Importer( - opts, isStrictByDefault, overwritePrimaryTypesOfFoldersByDefault, defaultIdConflictPolicy); + opts, + isStrictByDefault, + overwritePrimaryTypesOfFoldersByDefault, + defaultIdConflictPolicy, + extraValidationBeforeSubtreeRemoval); AccessControlHandling ac = getACHandling(); if (opts.getAccessControlHandling() == null) { opts.setAccessControlHandling(ac); diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/package-info.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/package-info.java index ae88c4a99..b9b1ec4eb 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/package-info.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/package-info.java @@ -15,7 +15,7 @@ * limitations under the License. */ -@Version("2.16.0") +@Version("2.17.0") package org.apache.jackrabbit.vault.packaging; import org.osgi.annotation.versioning.Version; diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/AbstractPackageRegistry.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/AbstractPackageRegistry.java index 3f10d057b..a749a34a8 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/AbstractPackageRegistry.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/AbstractPackageRegistry.java @@ -90,11 +90,22 @@ public String[] getAuthIdsForRootInstallation() { private final IdConflictPolicy defaultIdConflictPolicy; + private final boolean extraValidationBeforeSubtreeRemovalByDefault; + public AbstractPackageRegistry( SecurityConfig securityConfig, boolean isStrictByDefault, boolean overwritePrimaryTypesOfFoldersByDefault, IdConflictPolicy defaultIdConflictPolicy) { + this(securityConfig, isStrictByDefault, overwritePrimaryTypesOfFoldersByDefault, defaultIdConflictPolicy, true); + } + + public AbstractPackageRegistry( + SecurityConfig securityConfig, + boolean isStrictByDefault, + boolean overwritePrimaryTypesOfFoldersByDefault, + IdConflictPolicy defaultIdConflictPolicy, + boolean extraValidationBeforeSubtreeRemovalByDefault) { if (securityConfig != null) { this.securityConfig = securityConfig; } else { @@ -103,6 +114,7 @@ public AbstractPackageRegistry( this.isStrictByDefault = isStrictByDefault; this.overwritePrimaryTypesOfFoldersByDefault = overwritePrimaryTypesOfFoldersByDefault; this.defaultIdConflictPolicy = defaultIdConflictPolicy; + this.extraValidationBeforeSubtreeRemovalByDefault = extraValidationBeforeSubtreeRemovalByDefault; } public boolean isStrictByDefault() { @@ -117,6 +129,10 @@ public IdConflictPolicy getDefaultIdConflictPolicy() { return defaultIdConflictPolicy; } + public boolean isExtraValidationBeforeSubtreeRemovalByDefault() { + return extraValidationBeforeSubtreeRemovalByDefault; + } + /** * {@inheritDoc} */ diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistry.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistry.java index 779854750..52c5c52cc 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistry.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/FSPackageRegistry.java @@ -103,6 +103,8 @@ public class FSPackageRegistry extends AbstractPackageRegistry { private InstallationScope scope = InstallationScope.UNSCOPED; + private boolean extraValidationBeforeSubtreeRemoval = true; + /** * Creates a new FSPackageRegistry based on the given home directory. * @@ -179,6 +181,7 @@ public void activate(BundleContext context, Config config) throws IOException { } log.info("Jackrabbit Filevault FS Package Registry initialized with home location {}", homeDir.getPath()); this.scope = InstallationScope.valueOf(config.scope()); + this.extraValidationBeforeSubtreeRemoval = config.extraValidationBeforeSubtreeRemoval(); this.securityConfig = new AbstractPackageRegistry.SecurityConfig( config.authIdsForHookExecution(), config.authIdsForRootInstallation()); this.stateCache = new FSInstallStateCache(homeDir.toPath()); @@ -213,6 +216,13 @@ public void activate(BundleContext context, Config config) throws IOException { description = "The authorizable ids which are allowed to install packages with the 'requireRoot' flag (in addition to 'admin', 'administrators' and 'system'") String[] authIdsForRootInstallation(); + + @AttributeDefinition( + name = "Extra Validation Before Subtree Removal", + description = + "When enabled (default), nodes are only removed during import if the parent's subtree is fully " + + "covered by the filter (JCRVLT-830). When disabled, legacy behavior applies.") + boolean extraValidationBeforeSubtreeRemoval() default true; } /** @@ -670,7 +680,8 @@ public void installPackage( getSecurityConfig(), isStrictByDefault(), overwritePrimaryTypesOfFoldersByDefault(), - getDefaultIdConflictPolicy()); + getDefaultIdConflictPolicy(), + extraValidationBeforeSubtreeRemoval); dispatch(PackageEvent.Type.EXTRACT, pkg.getId(), null); stateCache.updatePackageStatus(vltPkg.getId(), FSPackageStatus.EXTRACTED); } else { diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/JcrPackageRegistry.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/JcrPackageRegistry.java index d6e11eea2..7e1e059d9 100644 --- a/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/JcrPackageRegistry.java +++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/packaging/registry/impl/JcrPackageRegistry.java @@ -132,7 +132,30 @@ public JcrPackageRegistry( boolean overwritePrimaryTypesOfFoldersByDefault, IdConflictPolicy defaultIdConflictPolicy, @Nullable String... roots) { - super(securityConfig, isStrict, overwritePrimaryTypesOfFoldersByDefault, defaultIdConflictPolicy); + this( + session, + securityConfig, + isStrict, + overwritePrimaryTypesOfFoldersByDefault, + defaultIdConflictPolicy, + true, + roots); + } + + public JcrPackageRegistry( + @NotNull Session session, + @Nullable AbstractPackageRegistry.SecurityConfig securityConfig, + boolean isStrict, + boolean overwritePrimaryTypesOfFoldersByDefault, + IdConflictPolicy defaultIdConflictPolicy, + boolean extraValidationBeforeSubtreeRemovalByDefault, + @Nullable String... roots) { + super( + securityConfig, + isStrict, + overwritePrimaryTypesOfFoldersByDefault, + defaultIdConflictPolicy, + extraValidationBeforeSubtreeRemovalByDefault); this.session = session; if (roots == null || roots.length == 0) { packRootPaths = new String[] {DEFAULT_PACKAGE_ROOT_PATH}; diff --git a/vault-core/src/test/java/org/apache/jackrabbit/vault/fs/filter/WorkspaceFilterTest.java b/vault-core/src/test/java/org/apache/jackrabbit/vault/fs/filter/WorkspaceFilterTest.java index 3270dd28e..204f53ed2 100644 --- a/vault-core/src/test/java/org/apache/jackrabbit/vault/fs/filter/WorkspaceFilterTest.java +++ b/vault-core/src/test/java/org/apache/jackrabbit/vault/fs/filter/WorkspaceFilterTest.java @@ -18,6 +18,9 @@ */ package org.apache.jackrabbit.vault.fs.filter; +import javax.jcr.Node; +import javax.jcr.RepositoryException; + import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -36,6 +39,7 @@ import org.apache.jackrabbit.vault.fs.config.ConfigurationException; import org.apache.jackrabbit.vault.fs.config.DefaultWorkspaceFilter; import org.junit.Test; +import org.mockito.Mockito; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -310,4 +314,25 @@ private static void assertSetsEquals(Set expected, Set actual) { assertTrue("Sets differ: " + diff2 + " unexpected", diff2.isEmpty()); } } + + @Test + public void extraValidationBeforeSubtreeRemovalDisabled_shortCircuitsIsSubtreeFullyCovered() + throws RepositoryException { + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + filter.setExtraValidationBeforeSubtreeRemoval(false); + Node anyNode = Mockito.mock(Node.class); + assertTrue(filter.isSubtreeFullyCovered(anyNode)); + } + + @Test + public void translatePreservesExtraValidationBeforeSubtreeRemovalFlag() + throws ConfigurationException, RepositoryException { + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet("/a"); + filter.add(set); + filter.setExtraValidationBeforeSubtreeRemoval(false); + DefaultWorkspaceFilter mapped = (DefaultWorkspaceFilter) filter.translate(new SimplePathMapping("/a", "/b")); + Node anyNode = Mockito.mock(Node.class); + assertTrue(mapped.isSubtreeFullyCovered(anyNode)); + } }