From 56f45f2e544239242028f4da1783b4909c60d136 Mon Sep 17 00:00:00 2001 From: Joerg Hoh Date: Mon, 2 Feb 2026 11:22:12 +0100 Subject: [PATCH 01/10] JCRVLT-830 IT on DocView level to showcase the problem --- .../integration/AggregationJCRVLT830_2.java | 226 ++++++++++++++++++ .../vault/fs/impl/io/DocViewImporter.java | 2 + 2 files changed, 228 insertions(+) create mode 100644 vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/AggregationJCRVLT830_2.java diff --git a/vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/AggregationJCRVLT830_2.java b/vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/AggregationJCRVLT830_2.java new file mode 100644 index 000000000..db40f1c4d --- /dev/null +++ b/vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/AggregationJCRVLT830_2.java @@ -0,0 +1,226 @@ +/* + * 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.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.apache.jackrabbit.vault.fs.api.ArtifactType; +import org.apache.jackrabbit.vault.fs.api.IdConflictPolicy; +import org.apache.jackrabbit.vault.fs.api.ImportMode; +import org.apache.jackrabbit.vault.fs.api.PathFilterSet; +import org.apache.jackrabbit.vault.fs.api.SerializationType; +import org.apache.jackrabbit.vault.fs.api.VaultInputSource; +import org.apache.jackrabbit.vault.fs.config.DefaultWorkspaceFilter; +import org.apache.jackrabbit.vault.fs.filter.DefaultPathFilter; +import org.apache.jackrabbit.vault.fs.impl.ArtifactSetImpl; +import org.apache.jackrabbit.vault.fs.impl.io.GenericArtifactHandler; +import org.apache.jackrabbit.vault.fs.impl.io.InputSourceArtifact; +import org.apache.jackrabbit.vault.fs.io.AccessControlHandling; +import org.apache.jackrabbit.vault.fs.io.ImportOptions; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class AggregationJCRVLT830_2 extends IntegrationTestBase { + + private GenericArtifactHandler handler; + DefaultWorkspaceFilter wspFilter; + private Node contentNode; + + private static final String NODETYPES = "<'cq'='http://www.day.com/jcr/cq/1.0'>\n" + + "<'sling'='http://sling.apache.org/jcr/sling/1.0'>\n" + + "<'nt'='http://www.jcp.org/jcr/nt/1.0'>\n" + + "<'rep'='internal'>\n" + + "\n" + + "[sling:OrderedFolder] > sling:Folder\n" + + " orderable\n" + + " + * (nt:base) = sling:OrderedFolder version\n" + + "\n" + + "[sling:Folder] > nt:folder\n" + + " - * (undefined) multiple\n" + + " - * (undefined)\n" + + " + * (nt:base) = sling:Folder version\n"; + + @Before + public void setup() throws Exception { + // create the necessary nodetypes + CndImporter.registerNodeTypes(new InputStreamReader(new ByteArrayInputStream(NODETYPES.getBytes())), admin); + + handler = new GenericArtifactHandler(); + handler.setAcHandling(AccessControlHandling.OVERWRITE); + + // create a filter accepting everything + wspFilter = new DefaultWorkspaceFilter(); + PathFilterSet filterSet = new PathFilterSet(); + filterSet.addInclude(new DefaultPathFilter(".*")); + wspFilter.add(filterSet); + } + + @Test + public void importContent() throws Exception { + String xml = "\n" + + "\n" + + " \n" + + ""; + + // create basic node structure /content/mysite/en/page + contentNode = admin.getRootNode().addNode("content", "nt:unstructured"); + Node mysite = contentNode.addNode("mysite", "sling:Folder"); + Node en = mysite.addNode("en", "sling:Folder"); + Node page = en.addNode("page", "nt:unstructured"); + admin.save(); + + // import the docview + importDocViewXml(xml, contentNode, "mysite"); + admin.save(); + dumpRepositoryStructure(contentNode); + + assertNodeExists("/content/mysite/en"); + assertNodeExists("/content/mysite/en/page"); + assertEquals( + "bar", admin.getNode("/content/mysite/en").getProperty("foo").toString()); + assertProperty("/content/mysite/en/foo", "bar"); + } + + /** + * Helper method to import DocView XML using GenericArtifactHandler + */ + private void importDocViewXml(String xml, Node parentNode, String nodeName) + throws RepositoryException, IOException { + ArtifactSetImpl artifacts = new ArtifactSetImpl(); + + // Create VaultInputSource from the XML string + VaultInputSource vaultSource = new VaultInputSource() { + @Override + public InputStream getByteStream() { + return new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public long getContentLength() { + return xml.length(); + } + + @Override + public long getLastModified() { + return System.currentTimeMillis(); + } + }; + + // Create the artifact using InputSourceArtifact + InputSourceArtifact artifact = new InputSourceArtifact( + null, // no parent + nodeName, // name + ".xml", // extension + ArtifactType.PRIMARY, // type + vaultSource, // input source + SerializationType.XML_DOCVIEW // serialization type + ); + + artifacts.add(artifact); + + ImportOptions options = new ImportOptions(); + options.setStrict(true); + options.setIdConflictPolicy(IdConflictPolicy.FAIL); + options.setImportMode(ImportMode.REPLACE); + options.setListener(getLoggingProgressTrackerListener()); + assertNotNull(handler.accept(options, true, wspFilter, parentNode, nodeName, artifacts)); + } + + /** + * Dumps the repository structure below the provided Node (including the node itself). + * Recursively prints all nodes, their properties and values with proper indentation. + * + * @param node the node to start dumping from + * @throws RepositoryException if an error occurs accessing the repository + */ + private void dumpRepositoryStructure(Node node) throws RepositoryException { + dumpNode(node, 0); + } + + /** + * Recursively dumps a node and its descendants with indentation. + * + * @param node the node to dump + * @param depth the current depth for indentation + * @throws RepositoryException if an error occurs accessing the repository + */ + private void dumpNode(Node node, int depth) throws RepositoryException { + String indent = " ".repeat(depth); + + // Print node path and primary type + System.out.println(indent + "+ " + node.getName() + " [" + + node.getPrimaryNodeType().getName() + "]"); + + // Print properties + PropertyIterator properties = node.getProperties(); + while (properties.hasNext()) { + Property property = properties.nextProperty(); + String propertyIndent = " ".repeat(depth + 1); + + // Skip jcr:primaryType as it's already shown + if ("jcr:primaryType".equals(property.getName())) { + continue; + } + + // Handle multi-value properties + if (property.isMultiple()) { + Value[] values = property.getValues(); + if (values.length > 0) { + System.out.print(propertyIndent + "- " + property.getName() + " = ["); + for (int i = 0; i < values.length; i++) { + if (i > 0) System.out.print(", "); + System.out.print(values[i].getString()); + } + System.out.println("]"); + } else { + System.out.println(propertyIndent + "- " + property.getName() + " = []"); + } + } else { + System.out.println(propertyIndent + "- " + property.getName() + " = " + property.getString()); + } + } + + // Recursively dump child nodes + NodeIterator children = node.getNodes(); + while (children.hasNext()) { + Node child = children.nextNode(); + dumpNode(child, depth + 1); + } + } +} 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..018b6e224 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.getPath()); 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()) { From 7d2557d7eb2a4f867777986cf75ac09f4fabe90e Mon Sep 17 00:00:00 2001 From: Joerg Hoh Date: Thu, 26 Feb 2026 13:30:53 +0100 Subject: [PATCH 02/10] JCRVLT-830 add an extension of the WorkspaceFilter --- .../IsSubtreeFullyOverwrittenIT.java | 233 ++++++++++++++++++ .../vault/fs/api/WorkspaceFilter.java | 21 ++ .../jackrabbit/vault/fs/api/package-info.java | 2 +- .../fs/config/DefaultWorkspaceFilter.java | 43 ++++ .../vault/fs/config/package-info.java | 2 +- .../vault/fs/impl/io/DocViewImporter.java | 2 +- .../vault/packaging/package-info.java | 2 +- 7 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/IsSubtreeFullyOverwrittenIT.java 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..57728d2a8 --- /dev/null +++ b/vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/IsSubtreeFullyOverwrittenIT.java @@ -0,0 +1,233 @@ +/* + * 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 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.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"; + + @Before + public void setUp() throws Exception { + super.setUp(); + clean(TEST_ROOT); + } + + /** + * 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.isSubtreeFullyOverwritten(admin, root.getPath())); + } + + /** + * Path is covered by filter but node does not exist in repository. + * Expects false (nodeExists check). + */ + @Test + public void returnsFalseWhenNodeDoesNotExist() throws RepositoryException, ConfigurationException { + DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); + PathFilterSet set = new PathFilterSet(TEST_ROOT); + set.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); + filter.add(set); + + assertFalse(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/nonexistent")); + } + + /** + * 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); + + JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertFalse(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/node")); + } + + /** + * 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); + + JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertFalse(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/node")); + } + + /** + * 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); + + JcrUtils.getOrCreateByPath(TEST_ROOT + "/parent", JcrConstants.NT_UNSTRUCTURED, admin); + JcrUtils.getOrCreateByPath(TEST_ROOT + "/parent/excluded", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertFalse(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/parent")); + } + + /** + * 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); + + JcrUtils.getOrCreateByPath(TEST_ROOT + "/parent", JcrConstants.NT_UNSTRUCTURED, admin); + JcrUtils.getOrCreateByPath(TEST_ROOT + "/parent/child", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertTrue(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/parent")); + } + + /** + * 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); + + JcrUtils.getOrCreateByPath(TEST_ROOT + "/leaf", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertTrue(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/leaf")); + } + + /** + * 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(/.*)?")); + + JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertTrue(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/node")); + } + + /** + * 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.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/withProp")); + } + + /** + * 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); + + JcrUtils.getOrCreateByPath(contentRoot + "/en", JcrConstants.NT_UNSTRUCTURED, admin); + JcrUtils.getOrCreateByPath(contentRoot + "/en/page", JcrConstants.NT_UNSTRUCTURED, admin); + admin.save(); + + assertFalse(filter.isSubtreeFullyOverwritten(admin, contentRoot + "/en")); + } +} 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..9c7c31d49 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,25 @@ 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 overwritten 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 session the session to use for traversing the subtree + * @param path the node path to check (subtree root, must exist) + * @return {@code true} if every node and property in the subtree is included and mode is REPLACE + * @throws RepositoryException if the path does not exist or traversal fails + */ + boolean isSubtreeFullyOverwritten(@NotNull Session session, @NotNull String path) 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..17526e893 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; @@ -60,6 +62,7 @@ import org.apache.jackrabbit.vault.util.RejectingEntityResolver; import org.apache.jackrabbit.vault.util.xml.serialize.FormattingXmlStreamWriter; import org.apache.jackrabbit.vault.util.xml.serialize.OutputFormat; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -278,6 +281,46 @@ public boolean includesProperty(String propertyPath) { return false; } + @Override + public boolean isSubtreeFullyOverwritten(@NotNull Session session, @NotNull String path) + throws RepositoryException { + if (isGloballyIgnored(path)) { + return false; + } + if (getCoveringFilterSet(path) == null) { + return false; + } + if (getImportMode(path) != ImportMode.REPLACE) { + return false; + } + if (!session.nodeExists(path)) { + return false; + } + javax.jcr.Node node = session.getNode(path); + return isSubtreeFullyOverwrittenRecursive(node); + } + + 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} */ 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 018b6e224..b8015bfc1 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,7 +379,7 @@ public void startDocViewNode( throws IOException, RepositoryException { stack.addName(docViewNode.getSnsAwareName()); Node node = stack.getNode(); - log.debug("startDocViewNode(), nodePath= {}, node={}", nodePath, node.getPath()); + log.debug("startDocViewNode(), nodePath= {}, node={}", nodePath, node != null ? node.getPath() : null); if (node == null) { stack = stack.push(); DocViewAdapter xform = stack.getAdapter(); 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; From b16c845c360a80aea8acff6bba5d02ac6cd47871 Mon Sep 17 00:00:00 2001 From: Joerg Hoh Date: Thu, 26 Feb 2026 14:46:21 +0100 Subject: [PATCH 03/10] JCRVLT-830 integrate this new check into the importer --- .../jackrabbit/vault/fs/impl/io/DocViewImporter.java | 9 ++++++--- .../vault/fs/impl/io/FolderArtifactHandler.java | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) 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 b8015bfc1..2ba2a05af 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 @@ -507,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.isSubtreeFullyOverwritten(session, node.getPath())) { + 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..90a92947d 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.isSubtreeFullyOverwritten(node.getSession(), node.getPath())) { if (!hints.contains(path)) { // if the child is in the filter, it belongs to // this aggregate and needs to be removed From 72b8e2507f03a16748fe7c55b3642caaf65b4d3c Mon Sep 17 00:00:00 2001 From: Joerg Hoh Date: Thu, 26 Feb 2026 14:50:51 +0100 Subject: [PATCH 04/10] remove unnecessary demo file --- .../integration/AggregationJCRVLT830_2.java | 226 ------------------ 1 file changed, 226 deletions(-) delete mode 100644 vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/AggregationJCRVLT830_2.java diff --git a/vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/AggregationJCRVLT830_2.java b/vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/AggregationJCRVLT830_2.java deleted file mode 100644 index db40f1c4d..000000000 --- a/vault-core-it/vault-core-integration-tests/src/main/java/org/apache/jackrabbit/vault/packaging/integration/AggregationJCRVLT830_2.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * 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.NodeIterator; -import javax.jcr.Property; -import javax.jcr.PropertyIterator; -import javax.jcr.RepositoryException; -import javax.jcr.Value; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; - -import org.apache.jackrabbit.commons.cnd.CndImporter; -import org.apache.jackrabbit.vault.fs.api.ArtifactType; -import org.apache.jackrabbit.vault.fs.api.IdConflictPolicy; -import org.apache.jackrabbit.vault.fs.api.ImportMode; -import org.apache.jackrabbit.vault.fs.api.PathFilterSet; -import org.apache.jackrabbit.vault.fs.api.SerializationType; -import org.apache.jackrabbit.vault.fs.api.VaultInputSource; -import org.apache.jackrabbit.vault.fs.config.DefaultWorkspaceFilter; -import org.apache.jackrabbit.vault.fs.filter.DefaultPathFilter; -import org.apache.jackrabbit.vault.fs.impl.ArtifactSetImpl; -import org.apache.jackrabbit.vault.fs.impl.io.GenericArtifactHandler; -import org.apache.jackrabbit.vault.fs.impl.io.InputSourceArtifact; -import org.apache.jackrabbit.vault.fs.io.AccessControlHandling; -import org.apache.jackrabbit.vault.fs.io.ImportOptions; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -public class AggregationJCRVLT830_2 extends IntegrationTestBase { - - private GenericArtifactHandler handler; - DefaultWorkspaceFilter wspFilter; - private Node contentNode; - - private static final String NODETYPES = "<'cq'='http://www.day.com/jcr/cq/1.0'>\n" - + "<'sling'='http://sling.apache.org/jcr/sling/1.0'>\n" - + "<'nt'='http://www.jcp.org/jcr/nt/1.0'>\n" - + "<'rep'='internal'>\n" - + "\n" - + "[sling:OrderedFolder] > sling:Folder\n" - + " orderable\n" - + " + * (nt:base) = sling:OrderedFolder version\n" - + "\n" - + "[sling:Folder] > nt:folder\n" - + " - * (undefined) multiple\n" - + " - * (undefined)\n" - + " + * (nt:base) = sling:Folder version\n"; - - @Before - public void setup() throws Exception { - // create the necessary nodetypes - CndImporter.registerNodeTypes(new InputStreamReader(new ByteArrayInputStream(NODETYPES.getBytes())), admin); - - handler = new GenericArtifactHandler(); - handler.setAcHandling(AccessControlHandling.OVERWRITE); - - // create a filter accepting everything - wspFilter = new DefaultWorkspaceFilter(); - PathFilterSet filterSet = new PathFilterSet(); - filterSet.addInclude(new DefaultPathFilter(".*")); - wspFilter.add(filterSet); - } - - @Test - public void importContent() throws Exception { - String xml = "\n" - + "\n" - + " \n" - + ""; - - // create basic node structure /content/mysite/en/page - contentNode = admin.getRootNode().addNode("content", "nt:unstructured"); - Node mysite = contentNode.addNode("mysite", "sling:Folder"); - Node en = mysite.addNode("en", "sling:Folder"); - Node page = en.addNode("page", "nt:unstructured"); - admin.save(); - - // import the docview - importDocViewXml(xml, contentNode, "mysite"); - admin.save(); - dumpRepositoryStructure(contentNode); - - assertNodeExists("/content/mysite/en"); - assertNodeExists("/content/mysite/en/page"); - assertEquals( - "bar", admin.getNode("/content/mysite/en").getProperty("foo").toString()); - assertProperty("/content/mysite/en/foo", "bar"); - } - - /** - * Helper method to import DocView XML using GenericArtifactHandler - */ - private void importDocViewXml(String xml, Node parentNode, String nodeName) - throws RepositoryException, IOException { - ArtifactSetImpl artifacts = new ArtifactSetImpl(); - - // Create VaultInputSource from the XML string - VaultInputSource vaultSource = new VaultInputSource() { - @Override - public InputStream getByteStream() { - return new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)); - } - - @Override - public long getContentLength() { - return xml.length(); - } - - @Override - public long getLastModified() { - return System.currentTimeMillis(); - } - }; - - // Create the artifact using InputSourceArtifact - InputSourceArtifact artifact = new InputSourceArtifact( - null, // no parent - nodeName, // name - ".xml", // extension - ArtifactType.PRIMARY, // type - vaultSource, // input source - SerializationType.XML_DOCVIEW // serialization type - ); - - artifacts.add(artifact); - - ImportOptions options = new ImportOptions(); - options.setStrict(true); - options.setIdConflictPolicy(IdConflictPolicy.FAIL); - options.setImportMode(ImportMode.REPLACE); - options.setListener(getLoggingProgressTrackerListener()); - assertNotNull(handler.accept(options, true, wspFilter, parentNode, nodeName, artifacts)); - } - - /** - * Dumps the repository structure below the provided Node (including the node itself). - * Recursively prints all nodes, their properties and values with proper indentation. - * - * @param node the node to start dumping from - * @throws RepositoryException if an error occurs accessing the repository - */ - private void dumpRepositoryStructure(Node node) throws RepositoryException { - dumpNode(node, 0); - } - - /** - * Recursively dumps a node and its descendants with indentation. - * - * @param node the node to dump - * @param depth the current depth for indentation - * @throws RepositoryException if an error occurs accessing the repository - */ - private void dumpNode(Node node, int depth) throws RepositoryException { - String indent = " ".repeat(depth); - - // Print node path and primary type - System.out.println(indent + "+ " + node.getName() + " [" - + node.getPrimaryNodeType().getName() + "]"); - - // Print properties - PropertyIterator properties = node.getProperties(); - while (properties.hasNext()) { - Property property = properties.nextProperty(); - String propertyIndent = " ".repeat(depth + 1); - - // Skip jcr:primaryType as it's already shown - if ("jcr:primaryType".equals(property.getName())) { - continue; - } - - // Handle multi-value properties - if (property.isMultiple()) { - Value[] values = property.getValues(); - if (values.length > 0) { - System.out.print(propertyIndent + "- " + property.getName() + " = ["); - for (int i = 0; i < values.length; i++) { - if (i > 0) System.out.print(", "); - System.out.print(values[i].getString()); - } - System.out.println("]"); - } else { - System.out.println(propertyIndent + "- " + property.getName() + " = []"); - } - } else { - System.out.println(propertyIndent + "- " + property.getName() + " = " + property.getString()); - } - } - - // Recursively dump child nodes - NodeIterator children = node.getNodes(); - while (children.hasNext()) { - Node child = children.nextNode(); - dumpNode(child, depth + 1); - } - } -} From 08af40cf3dde36bb2ec214bd29fd6bc7df29d42a Mon Sep 17 00:00:00 2001 From: Joerg Hoh Date: Thu, 26 Feb 2026 15:41:49 +0100 Subject: [PATCH 05/10] renamed method (PR feedback) --- .../IsSubtreeFullyOverwrittenIT.java | 20 +++++++++---------- .../vault/fs/api/WorkspaceFilter.java | 4 ++-- .../fs/config/DefaultWorkspaceFilter.java | 2 +- .../vault/fs/impl/io/DocViewImporter.java | 2 +- .../fs/impl/io/FolderArtifactHandler.java | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) 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 index 57728d2a8..e193e0600 100644 --- 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 @@ -62,7 +62,7 @@ public void returnsFalseWhenPathNotCoveredByAnyFilter() throws RepositoryExcepti Node root = JcrUtils.getOrCreateByPath(TEST_ROOT + "/content", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertFalse(filter.isSubtreeFullyOverwritten(admin, root.getPath())); + assertFalse(filter.isSubtreeFullyCovered(admin, root.getPath())); } /** @@ -76,7 +76,7 @@ public void returnsFalseWhenNodeDoesNotExist() throws RepositoryException, Confi set.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); filter.add(set); - assertFalse(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/nonexistent")); + assertFalse(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/nonexistent")); } /** @@ -94,7 +94,7 @@ public void returnsFalseWhenImportModeIsNotReplace() throws RepositoryException, JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertFalse(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/node")); + assertFalse(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/node")); } /** @@ -112,7 +112,7 @@ public void returnsFalseWhenPathIsGloballyIgnored() throws RepositoryException, JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertFalse(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/node")); + assertFalse(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/node")); } /** @@ -131,7 +131,7 @@ public void returnsFalseWhenChildNodeIsExcludedByFilter() throws RepositoryExcep JcrUtils.getOrCreateByPath(TEST_ROOT + "/parent/excluded", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertFalse(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/parent")); + assertFalse(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/parent")); } /** @@ -149,7 +149,7 @@ public void returnsTrueWhenSubtreeExistsAndFullyIncluded() throws RepositoryExce JcrUtils.getOrCreateByPath(TEST_ROOT + "/parent/child", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertTrue(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/parent")); + assertTrue(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/parent")); } /** @@ -166,7 +166,7 @@ public void returnsTrueWhenLeafNodeHasNoChildren() throws RepositoryException, C JcrUtils.getOrCreateByPath(TEST_ROOT + "/leaf", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertTrue(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/leaf")); + assertTrue(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/leaf")); } /** @@ -184,7 +184,7 @@ public void returnsTrueWhenGlobalIgnoredDoesNotMatchPath() throws RepositoryExce JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertTrue(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/node")); + assertTrue(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/node")); } /** @@ -205,7 +205,7 @@ public void returnsFalseWhenPropertyIsExcludedByFilter() throws RepositoryExcept node.setProperty("customProp", "value"); admin.save(); - assertFalse(filter.isSubtreeFullyOverwritten(admin, TEST_ROOT + "/withProp")); + assertFalse(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/withProp")); } /** @@ -228,6 +228,6 @@ public void jcrvlt830ReturnsFalseWhenExistingChildInRepoIsExcludedByFilter() JcrUtils.getOrCreateByPath(contentRoot + "/en/page", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertFalse(filter.isSubtreeFullyOverwritten(admin, contentRoot + "/en")); + assertFalse(filter.isSubtreeFullyCovered(admin, contentRoot + "/en")); } } 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 9c7c31d49..2371c6275 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 @@ -184,7 +184,7 @@ void dumpCoverage(@NotNull Session session, @NotNull ProgressTrackerListener lis boolean includesProperty(String propertyPath); /** - * Returns whether the given path's subtree is supposed to be fully overwritten during import, + * 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: *
    @@ -202,5 +202,5 @@ void dumpCoverage(@NotNull Session session, @NotNull ProgressTrackerListener lis * @return {@code true} if every node and property in the subtree is included and mode is REPLACE * @throws RepositoryException if the path does not exist or traversal fails */ - boolean isSubtreeFullyOverwritten(@NotNull Session session, @NotNull String path) throws RepositoryException; + boolean isSubtreeFullyCovered(@NotNull Session session, @NotNull String path) throws RepositoryException; } 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 17526e893..55c004973 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 @@ -282,7 +282,7 @@ public boolean includesProperty(String propertyPath) { } @Override - public boolean isSubtreeFullyOverwritten(@NotNull Session session, @NotNull String path) + public boolean isSubtreeFullyCovered(@NotNull Session session, @NotNull String path) throws RepositoryException { if (isGloballyIgnored(path)) { return false; 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 2ba2a05af..14dd64a56 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 @@ -508,7 +508,7 @@ public void endDocViewNode( && !hints.contains(path) && isIncluded(child, child.getDepth() - rootDepth)) { // Only remove or clear when the parent's subtree is fully overwritten by the filter (JCRVLT-830) - if (!wspFilter.isSubtreeFullyOverwritten(session, node.getPath())) { + if (!wspFilter.isSubtreeFullyCovered(session, node.getPath())) { log.debug( "Skipping removal of child node {} because parent's subtree is not fully overwritten", 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 90a92947d..ee19ca216 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 @@ -161,7 +161,7 @@ public ImportInfoImpl accept( // Only remove when parent's subtree is fully overwritten (JCRVLT-830) if (wspFilter.contains(path) && wspFilter.getImportMode(path) == ImportMode.REPLACE - && wspFilter.isSubtreeFullyOverwritten(node.getSession(), node.getPath())) { + && wspFilter.isSubtreeFullyCovered(node.getSession(), node.getPath())) { if (!hints.contains(path)) { // if the child is in the filter, it belongs to // this aggregate and needs to be removed From b5275ef9bc2cc45672821c3e62b7d8712530e418 Mon Sep 17 00:00:00 2001 From: Joerg Hoh Date: Thu, 26 Feb 2026 15:46:36 +0100 Subject: [PATCH 06/10] spotless --- .../jackrabbit/vault/fs/config/DefaultWorkspaceFilter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 55c004973..1d7cbbaaf 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 @@ -282,8 +282,7 @@ public boolean includesProperty(String propertyPath) { } @Override - public boolean isSubtreeFullyCovered(@NotNull Session session, @NotNull String path) - throws RepositoryException { + public boolean isSubtreeFullyCovered(@NotNull Session session, @NotNull String path) throws RepositoryException { if (isGloballyIgnored(path)) { return false; } From 8711844f9d3df2872b50bf6254da6c4e99039f61 Mon Sep 17 00:00:00 2001 From: Joerg Hoh Date: Fri, 27 Feb 2026 09:01:02 +0100 Subject: [PATCH 07/10] improve signature --- .../IsSubtreeFullyOverwrittenIT.java | 49 +++++++------------ .../vault/fs/api/WorkspaceFilter.java | 7 ++- .../fs/config/DefaultWorkspaceFilter.java | 13 +++-- .../vault/fs/impl/io/DocViewImporter.java | 2 +- .../fs/impl/io/FolderArtifactHandler.java | 2 +- 5 files changed, 30 insertions(+), 43 deletions(-) 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 index e193e0600..aa72c215e 100644 --- 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 @@ -41,11 +41,14 @@ 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); } /** @@ -62,21 +65,7 @@ public void returnsFalseWhenPathNotCoveredByAnyFilter() throws RepositoryExcepti Node root = JcrUtils.getOrCreateByPath(TEST_ROOT + "/content", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertFalse(filter.isSubtreeFullyCovered(admin, root.getPath())); - } - - /** - * Path is covered by filter but node does not exist in repository. - * Expects false (nodeExists check). - */ - @Test - public void returnsFalseWhenNodeDoesNotExist() throws RepositoryException, ConfigurationException { - DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter(); - PathFilterSet set = new PathFilterSet(TEST_ROOT); - set.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); - filter.add(set); - - assertFalse(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/nonexistent")); + assertFalse(filter.isSubtreeFullyCovered(rootNode)); } /** @@ -91,10 +80,10 @@ public void returnsFalseWhenImportModeIsNotReplace() throws RepositoryException, set.setImportMode(ImportMode.MERGE_PROPERTIES); filter.add(set); - JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); + Node n = JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertFalse(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/node")); + assertFalse(filter.isSubtreeFullyCovered(n)); } /** @@ -109,10 +98,10 @@ public void returnsFalseWhenPathIsGloballyIgnored() throws RepositoryException, filter.add(set); filter.setGlobalIgnored(PathFilter.ALL); - JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); + Node n = JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertFalse(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/node")); + assertFalse(filter.isSubtreeFullyCovered(n)); } /** @@ -127,11 +116,11 @@ public void returnsFalseWhenChildNodeIsExcludedByFilter() throws RepositoryExcep set.addExclude(new DefaultPathFilter(TEST_ROOT + "/parent/excluded(/.*)?")); filter.add(set); - JcrUtils.getOrCreateByPath(TEST_ROOT + "/parent", JcrConstants.NT_UNSTRUCTURED, admin); + 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(admin, TEST_ROOT + "/parent")); + assertFalse(filter.isSubtreeFullyCovered(p)); } /** @@ -145,11 +134,11 @@ public void returnsTrueWhenSubtreeExistsAndFullyIncluded() throws RepositoryExce set.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); filter.add(set); - JcrUtils.getOrCreateByPath(TEST_ROOT + "/parent", JcrConstants.NT_UNSTRUCTURED, admin); + 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(admin, TEST_ROOT + "/parent")); + assertTrue(filter.isSubtreeFullyCovered(p)); } /** @@ -163,10 +152,10 @@ public void returnsTrueWhenLeafNodeHasNoChildren() throws RepositoryException, C set.addInclude(new DefaultPathFilter(TEST_ROOT + "(/.*)?")); filter.add(set); - JcrUtils.getOrCreateByPath(TEST_ROOT + "/leaf", JcrConstants.NT_UNSTRUCTURED, admin); + Node l = JcrUtils.getOrCreateByPath(TEST_ROOT + "/leaf", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertTrue(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/leaf")); + assertTrue(filter.isSubtreeFullyCovered(l)); } /** @@ -181,10 +170,10 @@ public void returnsTrueWhenGlobalIgnoredDoesNotMatchPath() throws RepositoryExce filter.add(set); filter.setGlobalIgnored(new DefaultPathFilter("/other/ignored(/.*)?")); - JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); + Node n = JcrUtils.getOrCreateByPath(TEST_ROOT + "/node", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertTrue(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/node")); + assertTrue(filter.isSubtreeFullyCovered(n)); } /** @@ -205,7 +194,7 @@ public void returnsFalseWhenPropertyIsExcludedByFilter() throws RepositoryExcept node.setProperty("customProp", "value"); admin.save(); - assertFalse(filter.isSubtreeFullyCovered(admin, TEST_ROOT + "/withProp")); + assertFalse(filter.isSubtreeFullyCovered(node)); } /** @@ -224,10 +213,10 @@ public void jcrvlt830ReturnsFalseWhenExistingChildInRepoIsExcludedByFilter() set.addExclude(new DefaultPathFilter(contentRoot + "/en/page(/.*)?")); filter.add(set); - JcrUtils.getOrCreateByPath(contentRoot + "/en", JcrConstants.NT_UNSTRUCTURED, admin); + Node n = JcrUtils.getOrCreateByPath(contentRoot + "/en", JcrConstants.NT_UNSTRUCTURED, admin); JcrUtils.getOrCreateByPath(contentRoot + "/en/page", JcrConstants.NT_UNSTRUCTURED, admin); admin.save(); - assertFalse(filter.isSubtreeFullyCovered(admin, contentRoot + "/en")); + assertFalse(filter.isSubtreeFullyCovered(n)); } } 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 2371c6275..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 @@ -196,11 +196,10 @@ void dumpCoverage(@NotNull Session session, @NotNull ProgressTrackerListener lis * 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 * - * @param session the session to use for traversing the subtree - * @param path the node path to check (subtree root, must exist) - * @return {@code true} if every node and property in the subtree is included and mode is REPLACE + * @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 Session session, @NotNull String path) throws RepositoryException; + boolean isSubtreeFullyCovered(@NotNull Node subTree) throws RepositoryException; } 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 1d7cbbaaf..f2b8b82f9 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 @@ -62,7 +62,6 @@ import org.apache.jackrabbit.vault.util.RejectingEntityResolver; import org.apache.jackrabbit.vault.util.xml.serialize.FormattingXmlStreamWriter; import org.apache.jackrabbit.vault.util.xml.serialize.OutputFormat; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -282,7 +281,11 @@ public boolean includesProperty(String propertyPath) { } @Override - public boolean isSubtreeFullyCovered(@NotNull Session session, @NotNull String path) throws RepositoryException { + public boolean isSubtreeFullyCovered(javax.jcr.Node subTree) throws RepositoryException { + if (subTree == null) { + return false; + } + String path = subTree.getPath(); if (isGloballyIgnored(path)) { return false; } @@ -292,11 +295,7 @@ public boolean isSubtreeFullyCovered(@NotNull Session session, @NotNull String p if (getImportMode(path) != ImportMode.REPLACE) { return false; } - if (!session.nodeExists(path)) { - return false; - } - javax.jcr.Node node = session.getNode(path); - return isSubtreeFullyOverwrittenRecursive(node); + return isSubtreeFullyOverwrittenRecursive(subTree); } private boolean isSubtreeFullyOverwrittenRecursive(javax.jcr.Node node) throws RepositoryException { 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 14dd64a56..2588ed89d 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 @@ -508,7 +508,7 @@ public void endDocViewNode( && !hints.contains(path) && isIncluded(child, child.getDepth() - rootDepth)) { // Only remove or clear when the parent's subtree is fully overwritten by the filter (JCRVLT-830) - if (!wspFilter.isSubtreeFullyCovered(session, node.getPath())) { + if (!wspFilter.isSubtreeFullyCovered(child)) { log.debug( "Skipping removal of child node {} because parent's subtree is not fully overwritten", 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 ee19ca216..1f4949768 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 @@ -161,7 +161,7 @@ public ImportInfoImpl accept( // Only remove when parent's subtree is fully overwritten (JCRVLT-830) if (wspFilter.contains(path) && wspFilter.getImportMode(path) == ImportMode.REPLACE - && wspFilter.isSubtreeFullyCovered(node.getSession(), node.getPath())) { + && wspFilter.isSubtreeFullyCovered(child)) { if (!hints.contains(path)) { // if the child is in the filter, it belongs to // this aggregate and needs to be removed From 4b2462cd3aede3192f0e1a07f28aed87c4d6ad76 Mon Sep 17 00:00:00 2001 From: Joerg Hoh Date: Wed, 18 Mar 2026 17:59:10 +0100 Subject: [PATCH 08/10] JCRVLT-830 fix incorrect parameter --- .../org/apache/jackrabbit/vault/fs/impl/io/DocViewImporter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2588ed89d..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 @@ -508,7 +508,7 @@ public void endDocViewNode( && !hints.contains(path) && isIncluded(child, child.getDepth() - rootDepth)) { // Only remove or clear when the parent's subtree is fully overwritten by the filter (JCRVLT-830) - if (!wspFilter.isSubtreeFullyCovered(child)) { + if (!wspFilter.isSubtreeFullyCovered(node)) { log.debug( "Skipping removal of child node {} because parent's subtree is not fully overwritten", path); From 24f55dbbf1796df11d3562dfa6f6d1f25d03798e Mon Sep 17 00:00:00 2001 From: Joerg Hoh Date: Wed, 18 Mar 2026 18:03:32 +0100 Subject: [PATCH 09/10] JCRVLT-830 make this new behavior configurable (not yet exposed) --- .../IsSubtreeFullyOverwrittenIT.java | 104 +++++++++++ .../vault-core-it-execution-oak-min/pom.xml | 175 +++++++++--------- .../fs/config/DefaultWorkspaceFilter.java | 20 ++ .../fs/impl/io/FolderArtifactHandler.java | 2 +- .../vault/fs/filter/WorkspaceFilterTest.java | 25 +++ 5 files changed, 238 insertions(+), 88 deletions(-) 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 index aa72c215e..30663455a 100644 --- 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 @@ -21,6 +21,8 @@ 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; @@ -29,6 +31,9 @@ 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; @@ -212,6 +217,7 @@ public void jcrvlt830ReturnsFalseWhenExistingChildInRepoIsExcludedByFilter() 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); @@ -219,4 +225,102 @@ public void jcrvlt830ReturnsFalseWhenExistingChildInRepoIsExcludedByFilter() 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/config/DefaultWorkspaceFilter.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/config/DefaultWorkspaceFilter.java index f2b8b82f9..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 @@ -108,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. @@ -280,8 +287,20 @@ 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; } @@ -374,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/impl/io/FolderArtifactHandler.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/FolderArtifactHandler.java index 1f4949768..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 @@ -161,7 +161,7 @@ public ImportInfoImpl accept( // Only remove when parent's subtree is fully overwritten (JCRVLT-830) if (wspFilter.contains(path) && wspFilter.getImportMode(path) == ImportMode.REPLACE - && wspFilter.isSubtreeFullyCovered(child)) { + && 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/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)); + } } From 966b78abfd3e188d715595ecfcca5da2b93a0965 Mon Sep 17 00:00:00 2001 From: Joerg Hoh Date: Wed, 18 Mar 2026 21:28:04 +0100 Subject: [PATCH 10/10] JCRVLT-830 allow to configure the behavior via OSGI --- .../jackrabbit/vault/fs/io/Importer.java | 23 +++++++++++++ .../jackrabbit/vault/fs/io/package-info.java | 2 +- .../vault/packaging/impl/JcrPackageImpl.java | 3 +- .../packaging/impl/JcrPackageManagerImpl.java | 21 ++++++++++++ .../vault/packaging/impl/PackagingImpl.java | 12 ++++++- .../vault/packaging/impl/ZipVaultPackage.java | 33 +++++++++++++++++-- .../impl/AbstractPackageRegistry.java | 16 +++++++++ .../registry/impl/FSPackageRegistry.java | 13 +++++++- .../registry/impl/JcrPackageRegistry.java | 25 +++++++++++++- 9 files changed, 140 insertions(+), 8 deletions(-) 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/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};