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:
+ *
+ * - the path is covered by this filter and import mode is {@link ImportMode#REPLACE},
+ * - the node at {@code path} exists in the repository, and
+ * - for every descendant node, {@link #contains(String)} is {@code true}, and
+ * - for every property in the subtree, {@link #includesProperty(String)} is {@code true}.
+ *
+ * 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));
+ }
}