You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by kw...@apache.org on 2021/08/23 15:35:05 UTC
[jackrabbit-filevault] 01/01: JCRVLT-551 try to set UUID from
package
This is an automated email from the ASF dual-hosted git repository.
kwin pushed a commit to branch feature/JCRVLT-551-keep-jcr-uuid
in repository https://gitbox.apache.org/repos/asf/jackrabbit-filevault.git
commit 0506000fcc5a3213fc26a1047a05159d6b309896
Author: Konrad Windszus <kw...@apache.org>
AuthorDate: Mon Aug 23 17:34:48 2021 +0200
JCRVLT-551 try to set UUID from package
add additional import options to tweak UUID collision behavior
---
.../jackrabbit/vault/fs/api/IdConflictPolicy.java | 29 ++
.../jackrabbit/vault/fs/api/package-info.java | 2 +-
.../vault/fs/impl/io/AbstractArtifactHandler.java | 9 +-
.../vault/fs/impl/io/DocViewSAXImporter.java | 334 +++++++++++----------
.../vault/fs/impl/io/FileArtifactHandler.java | 12 +-
.../vault/fs/impl/io/FolderArtifactHandler.java | 5 +-
.../vault/fs/impl/io/GenericArtifactHandler.java | 5 +-
.../vault/fs/impl/io/JcrSysViewTransformer.java | 7 +-
.../jackrabbit/vault/fs/impl/io/NodeStash.java | 76 +++--
.../apache/jackrabbit/vault/fs/io/AutoSave.java | 2 +-
.../jackrabbit/vault/fs/io/ImportOptions.java | 30 +-
.../apache/jackrabbit/vault/fs/io/Importer.java | 11 +-
.../vault/packaging/impl/ZipVaultPackage.java | 6 +-
.../apache/jackrabbit/vault/util/DocViewNode.java | 1 +
.../vault/packaging/integration/ImportIT.java | 3 +-
.../ReferenceableIdentifiersImportIT.java | 283 +++++++++++++++++
.../referenceable.zip/META-INF/vault/config.xml | 93 ++++++
.../META-INF/vault/definition/.content.xml | 25 ++
.../referenceable.zip/META-INF/vault/filter.xml | 4 +
.../referenceable.zip/META-INF/vault/nodetypes.cnd | 17 ++
.../META-INF/vault/properties.xml | 18 ++
.../referenceable.zip/jcr_root/.content.xml | 6 +
.../referenceable.zip/jcr_root/tmp/.content.xml | 4 +
.../jcr_root/tmp/referenceable.xml | 12 +
24 files changed, 786 insertions(+), 208 deletions(-)
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/IdConflictPolicy.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/IdConflictPolicy.java
new file mode 100644
index 0000000..83abb3a
--- /dev/null
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/IdConflictPolicy.java
@@ -0,0 +1,29 @@
+/*
+ * 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.fs.api;
+
+import javax.jcr.ImportUUIDBehavior;
+
+public enum IdConflictPolicy {
+ /** Default handling, fail in case of unresolvable conflicts. Conflicts are automatically resolved in case the conflicting UUID and all its references are inside the package filter. */
+ FAIL,
+ /** Create a new ID for the imported node (for all referenceable nodes), this may break existing references in the package. This fails if a referenced node is overwritten by a node with a different id */
+ CREATE_NEW_ID,
+ /** Remove the node with the conflicting id along with its references (even if outside the filters). This goes beyond {@link ImportUUIDBehavior#IMPORT_UUID_COLLISION_REMOVE_EXISTING}, as it also does not only resolve UUID collisions but also replacements of referenceable nodes with different ids.
+ * Use with care, as this may remove references outside the filter. */
+ FORCE_REMOVE_CONFLICTING_ID
+}
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 38c4265..48709de 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.9.0")
+@Version("2.10.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/impl/io/AbstractArtifactHandler.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/AbstractArtifactHandler.java
index 4cf7f94..40eab88 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/AbstractArtifactHandler.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/AbstractArtifactHandler.java
@@ -36,8 +36,10 @@ import org.apache.jackrabbit.vault.fs.api.ImportInfo;
import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
import org.apache.jackrabbit.vault.fs.impl.ArtifactSetImpl;
import org.apache.jackrabbit.vault.fs.io.AccessControlHandling;
+import org.apache.jackrabbit.vault.fs.io.ImportOptions;
import org.apache.jackrabbit.vault.fs.spi.ACLManagement;
import org.apache.jackrabbit.vault.fs.spi.ServiceProviderFactory;
+import org.jetbrains.annotations.NotNull;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
@@ -119,7 +121,7 @@ public abstract class AbstractArtifactHandler implements ArtifactHandler, Dumpab
throws RepositoryException, IOException {
Node node = file.getNode();
String name = node.getName();
- return accept(file.getManager().getWorkspaceFilter(),
+ return accept(new ImportOptions(), file.getManager().getWorkspaceFilter(),
name.length() == 0 ? node : node.getParent(),
name, (ArtifactSetImpl) artifacts);
}
@@ -131,13 +133,14 @@ public abstract class AbstractArtifactHandler implements ArtifactHandler, Dumpab
ArtifactSet artifacts)
throws RepositoryException, IOException {
Node node = parent.getNode();
- return accept(parent.getManager().getWorkspaceFilter(),
+ return accept(new ImportOptions(), parent.getManager().getWorkspaceFilter(),
node, name, (ArtifactSetImpl) artifacts);
}
/**
* Imports an artifact set below the node.
*
+ * @param option the import options
* @param wspFilter the workspace filter
* @param parent the parent node
* @param name the name of the (new) import
@@ -146,7 +149,7 @@ public abstract class AbstractArtifactHandler implements ArtifactHandler, Dumpab
* @throws RepositoryException if an error occurs.
* @throws IOException if an I/O error occurs.
*/
- protected abstract ImportInfoImpl accept(WorkspaceFilter wspFilter, Node parent,
+ protected abstract ImportInfoImpl accept(@NotNull ImportOptions options, WorkspaceFilter wspFilter, Node parent,
String name, ArtifactSetImpl artifacts)
throws RepositoryException, IOException;
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/DocViewSAXImporter.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/DocViewSAXImporter.java
index e2aa8a2..f9405c8 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/DocViewSAXImporter.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/DocViewSAXImporter.java
@@ -20,8 +20,8 @@ package org.apache.jackrabbit.vault.fs.impl.io;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Calendar;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -29,8 +29,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.Item;
@@ -41,6 +39,7 @@ import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
+import javax.jcr.ReferentialIntegrityException;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
@@ -49,13 +48,16 @@ import javax.jcr.ValueFactory;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.PropertyDefinition;
+
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
import org.apache.jackrabbit.util.ISO9075;
+import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.vault.fs.api.Artifact;
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.ItemFilterSet;
import org.apache.jackrabbit.vault.fs.api.NodeNameList;
@@ -74,7 +76,6 @@ import org.apache.jackrabbit.vault.util.EffectiveNodeType;
import org.apache.jackrabbit.vault.util.JcrConstants;
import org.apache.jackrabbit.vault.util.MimeTypes;
import org.apache.jackrabbit.vault.util.RejectingEntityDefaultHandler;
-import org.apache.jackrabbit.util.Text;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
@@ -240,6 +241,8 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
*/
private final JcrNamespaceHelper nsHelper;
+ private final IdConflictPolicy idConflictPolicy;
+
/**
* Creates a new importer that will receive SAX events and imports the
* items below the given root.
@@ -251,7 +254,7 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
* @throws RepositoryException if an error occurs.
*/
public DocViewSAXImporter(Node parentNode, String rootNodeName,
- ArtifactSetImpl artifacts, WorkspaceFilter wspFilter)
+ ArtifactSetImpl artifacts, WorkspaceFilter wspFilter, IdConflictPolicy idConflictPolicy)
throws RepositoryException {
this.filter = artifacts.getCoverage();
this.wspFilter = wspFilter;
@@ -264,6 +267,7 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
this.snsSupported = session.getRepository().
getDescriptorValue(Repository.NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED).getBoolean();
this.nsHelper = new JcrNamespaceHelper(session, null);
+ this.idConflictPolicy = idConflictPolicy;
String rootPath = parentNode.getPath();
if (!rootPath.equals("/")) {
@@ -809,81 +813,39 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
private StackElement addNode(DocViewNode ni) throws RepositoryException, IOException {
final Node currentNode = stack.getNode();
- // find old node
- Node oldNode = null;
- Node node = null;
+ Node existingNode = null;
if ("".equals(ni.label)) {
// special case for root node update
- node = currentNode;
- } else if (ni.uuid == null) {
- if (stack.checkForNode() && currentNode.hasNode(ni.label)) {
- node = currentNode.getNode(ni.label);
- }
+ existingNode = currentNode;
} else {
- try {
- node = session.getNodeByUUID(ni.uuid);
- if (!node.getParent().isSame(currentNode)) {
- log.warn("Packaged node at {} is referenceable and collides with existing node at {}. Will create new UUID.",
- currentNode.getPath() + "/" + ni.label,
- node.getPath());
- ni.uuid = null;
- ni.props.remove(JcrConstants.JCR_UUID);
- ni.props.remove(JcrConstants.JCR_BASEVERSION);
- ni.props.remove(JcrConstants.JCR_PREDECESSORS);
- ni.props.remove(JcrConstants.JCR_SUCCESSORS);
- ni.props.remove(JcrConstants.JCR_VERSIONHISTORY);
- node = null;
- }
- } catch (ItemNotFoundException e) {
- // ignore
- }
- if (node == null) {
- if (stack.checkForNode() && currentNode.hasNode(ni.label)) {
- node = currentNode.getNode(ni.label);
- }
- } else {
- if (!node.getName().equals(ni.name)) {
- // if names mismatches => replace
- oldNode = node;
- node = null;
- }
-
+ if (stack.checkForNode() && currentNode.hasNode(ni.label)) {
+ existingNode = currentNode.getNode(ni.label);
}
- }
- // if old node is not included in the package, ignore rewrite
- if (oldNode != null && !isIncluded(oldNode, oldNode.getDepth() - rootDepth)) {
- node = oldNode;
- oldNode = null;
- }
-
- // TODO: under which condition do we end up here?
- if (oldNode != null) {
- // check versionable
- new VersioningState(stack, oldNode).ensureCheckedOut();
-
- String oldNodePath = oldNode.getPath();
- ImportMode importMode = wspFilter.getImportMode(oldNodePath);
- NodeStash recovery = new NodeStash(session, oldNodePath, importMode);
- recovery.stash();
-
- // ensure that existing binaries are not sourced from a property
- // that is about to be removed
- Map<String, DocViewSAXImporter.BlobInfo> blobs = binaries.get(oldNode.getPath());
- if (blobs != null) {
- for (DocViewSAXImporter.BlobInfo info : blobs.values()) {
- info.detach();
+ if (ni.uuid != null) {
+ if (idConflictPolicy == IdConflictPolicy.FAIL) {
+ try {
+ // does uuid already exist in the repo?
+ Node sameIdNode = session.getNodeByIdentifier(ni.uuid);
+ // edge-case: same node path -> uuid is kept
+ if (existingNode != null && existingNode.getPath().equals(sameIdNode.getPath())) {
+ log.debug("Node with existing identifier is being updated without modifying its uuid");
+ } else {
+ // uuid found in path covered by filter
+ if (isIncluded(sameIdNode, 0)) {
+ log.warn("Node identifier {} for to-be imported node {} already taken by {}, trying to release it.", ni.uuid, currentNode.getPath() + '/' + ni.label, sameIdNode.getPath());
+ removeReferences(sameIdNode);
+ session.removeItem(sameIdNode.getPath());
+ existingNode = null;
+ } else {
+ // uuid found in path not-covered by filter
+ throw new ReferentialIntegrityException("UUID already taken by node " + sameIdNode.getPath());
+ }
+ }
+ } catch (ItemNotFoundException e) {
+ // ignore
+ }
}
}
-
- oldNode.remove();
- // now create the new node
- node = createNewNode(currentNode, ni);
-
- // move the children back
- recovery.recover(importInfo);
-
- importInfo.onReplaced(node.getPath());
- return new StackElement(node, false);
}
// check if new node needs to be checked in
@@ -891,7 +853,7 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
boolean isCheckedIn = coProp != null && "false".equals(coProp.values[0]);
// create or update node
- boolean isNew = node == null;
+ boolean isNew = existingNode == null;
if (isNew) {
// workaround for bug in jcr2spi if mixins are empty
if (!ni.props.containsKey(JcrConstants.JCR_MIXINTYPES)) {
@@ -900,126 +862,178 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
}
stack.ensureCheckedOut();
- node = createNewNode(currentNode, ni);
- if (node.getDefinition() == null) {
+ existingNode = createNewNode(currentNode, ni);
+ if (existingNode.getDefinition() == null) {
throw new RepositoryException("Child node not allowed.");
}
- if (node.isNodeType(JcrConstants.NT_RESOURCE)) {
- if (!node.hasProperty(JcrConstants.JCR_DATA)) {
- importInfo.onMissing(node.getPath() + "/" + JcrConstants.JCR_DATA);
+ if (existingNode.isNodeType(JcrConstants.NT_RESOURCE)) {
+ if (!existingNode.hasProperty(JcrConstants.JCR_DATA)) {
+ importInfo.onMissing(existingNode.getPath() + "/" + JcrConstants.JCR_DATA);
}
} else if (isCheckedIn) {
// don't rely on isVersionable here, since SPI might not have this info yet
- importInfo.registerToVersion(node.getPath());
+ importInfo.registerToVersion(existingNode.getPath());
}
- importInfo.onCreated(node.getPath());
+ importInfo.onCreated(existingNode.getPath());
- } else if (isIncluded(node, node.getDepth() - rootDepth)) {
+ } else if (isIncluded(existingNode, existingNode.getDepth() - rootDepth)) {
if (isCheckedIn) {
// don't rely on isVersionable here, since SPI might not have this info yet
- importInfo.registerToVersion(node.getPath());
+ importInfo.registerToVersion(existingNode.getPath());
}
- ImportMode importMode = wspFilter.getImportMode(node.getPath());
- if (updateExistingNode(node, ni, importMode)) {
- if (node.isNodeType(JcrConstants.NT_RESOURCE)) {
- if (!node.hasProperty(JcrConstants.JCR_DATA)) {
- importInfo.onMissing(node.getPath() + "/" + JcrConstants.JCR_DATA);
+ ImportMode importMode = wspFilter.getImportMode(existingNode.getPath());
+ Node updatedNode = updateExistingNode(existingNode, ni, importMode);
+ if (updatedNode != null) {
+ if (updatedNode.isNodeType(JcrConstants.NT_RESOURCE)) {
+ if (!updatedNode.hasProperty(JcrConstants.JCR_DATA)) {
+ importInfo.onMissing(existingNode.getPath() + "/" + JcrConstants.JCR_DATA);
}
}
- importInfo.onModified(node.getPath());
+ importInfo.onModified(updatedNode.getPath());
+ existingNode = updatedNode;
} else {
- importInfo.onNop(node.getPath());
+ importInfo.onNop(existingNode.getPath());
}
} else {
// remove registered binaries outside of the filter (JCR-126)
- binaries.remove(node.getPath());
+ binaries.remove(existingNode.getPath());
}
- return new StackElement(node, isNew);
+ return new StackElement(existingNode, isNew);
}
- private boolean updateExistingNode(@NotNull Node node, @NotNull DocViewNode ni, @NotNull ImportMode importMode) throws RepositoryException {
- VersioningState vs = new VersioningState(stack, node);
- boolean modified = false;
- // set new primary type (but never set rep:root)
- if (importMode == ImportMode.REPLACE && !"rep:root".equals(ni.primary)) {
- if (!node.getPrimaryNodeType().getName().equals(ni.primary)) {
- vs.ensureCheckedOut();
- node.setPrimaryType(ni.primary);
- modified = true;
+ /**
+ * Tries to remove references to the given node but only in case they are included in the filters.
+ * @param node the referenced node
+ * @throws ReferentialIntegrityException in case some references can not be removed (outside filters)
+ * @throws RepositoryException in case some other error occurs
+ */
+ public void removeReferences(@NotNull Node node) throws ReferentialIntegrityException, RepositoryException {
+ Collection<String> removableReferencePaths = new ArrayList<>();
+ PropertyIterator pIter = node.getReferences();
+ while (pIter.hasNext()) {
+ Property referenceProperty = pIter.nextProperty();
+ if (isIncluded(referenceProperty, 0) || idConflictPolicy == IdConflictPolicy.FORCE_REMOVE_CONFLICTING_ID) {
+ removableReferencePaths.add(referenceProperty.getPath());
+ } else {
+ throw new ReferentialIntegrityException("Found non-removable reference for conflicting UUID " + node.getIdentifier() + " (" + node.getPath() + ") at " + referenceProperty.getPath());
}
}
+ for (String referencePath : removableReferencePaths) {
+ log.info("Remove reference towards {} at {}", node.getIdentifier(), referencePath);
+ session.removeItem(referencePath);
+ }
+ }
- // calculate mixins
- Set<String> newMixins = new HashSet<>();
- AccessControlHandling acHandling = getAcHandling(ni.name);
- if (ni.mixins != null) {
- for (String mixin : ni.mixins) {
- // omit name if mix:AccessControllable and CLEAR
- if (!aclManagement.isAccessControllableMixin(mixin)
- || acHandling != AccessControlHandling.CLEAR) {
- newMixins.add(mixin);
+ private @Nullable Node updateExistingNode(@NotNull Node node, @NotNull DocViewNode ni, @NotNull ImportMode importMode) throws RepositoryException {
+ VersioningState vs = new VersioningState(stack, node);
+ Node updatedNode = null;
+ // try to set uuid via sysview import if it differs from existing one
+ if (ni.uuid != null && !node.getIdentifier().equals(ni.uuid)) {
+ NodeStash stash = new NodeStash(session, node.getPath());
+ stash.stash();
+ Node parent = node.getParent();
+ removeReferences(node);
+ node.remove();
+ updatedNode = createNewNode(parent, ni);
+ stash.recover(importMode, importInfo);
+ } else {
+ // TODO: is this faster than using sysview import?
+ // set new primary type (but never set rep:root)
+ if (importMode == ImportMode.REPLACE && !"rep:root".equals(ni.primary)) {
+ if (!node.getPrimaryNodeType().getName().equals(ni.primary)) {
+ vs.ensureCheckedOut();
+ node.setPrimaryType(ni.primary);
+ updatedNode = node;
}
}
- }
- // remove mixin not in package
- if (importMode == ImportMode.REPLACE) {
- for (NodeType mix : node.getMixinNodeTypes()) {
- String name = mix.getName();
- if (!newMixins.remove(name)) {
- // special check for mix:AccessControllable
- if (!aclManagement.isAccessControllableMixin(name)
- || acHandling == AccessControlHandling.CLEAR
- || acHandling == AccessControlHandling.OVERWRITE) {
- vs.ensureCheckedOut();
- node.removeMixin(name);
- modified = true;
+ // calculate mixins to be added
+ Set<String> newMixins = new HashSet<>();
+ AccessControlHandling acHandling = getAcHandling(ni.name);
+ if (ni.mixins != null) {
+ for (String mixin : ni.mixins) {
+ // omit name if mix:AccessControllable and CLEAR
+ if (!aclManagement.isAccessControllableMixin(mixin)
+ || acHandling != AccessControlHandling.CLEAR) {
+ newMixins.add(mixin);
}
}
}
- }
-
- // add remaining mixins (for all import modes)
- for (String mixin : newMixins) {
- vs.ensureCheckedOut();
- node.addMixin(mixin);
- modified = true;
- }
-
- // remove properties not in package (which are not protected)
- if (importMode == ImportMode.REPLACE) {
- PropertyIterator pIter = node.getProperties();
- while (pIter.hasNext()) {
- Property p = pIter.nextProperty();
- String propName = p.getName();
- if (!p.getDefinition().isProtected()
- && !ni.props.containsKey(propName)
- && !preserveProperties.contains(p.getPath())
- && wspFilter.includesProperty(p.getPath())) {
- vs.ensureCheckedOut();
- p.remove();
- modified = true;
+ // remove mixins not in package (only for mode = replace)
+ if (importMode == ImportMode.REPLACE) {
+ for (NodeType mix : node.getMixinNodeTypes()) {
+ String name = mix.getName();
+ if (!newMixins.remove(name)) {
+ // special check for mix:AccessControllable
+ if (!aclManagement.isAccessControllableMixin(name)
+ || acHandling == AccessControlHandling.CLEAR
+ || acHandling == AccessControlHandling.OVERWRITE) {
+ vs.ensureCheckedOut();
+ node.removeMixin(name);
+ updatedNode = node;
+ }
+ }
+ }
+ }
+ // add remaining mixins (for all import modes)
+ for (String mixin : newMixins) {
+ vs.ensureCheckedOut();
+ node.addMixin(mixin);
+ updatedNode = node;
+ }
+
+ // remove unprotected properties not in package (only for mode = replace)
+ if (importMode == ImportMode.REPLACE) {
+ PropertyIterator pIter = node.getProperties();
+ while (pIter.hasNext()) {
+ Property p = pIter.nextProperty();
+ String propName = p.getName();
+ if (!p.getDefinition().isProtected()
+ && !ni.props.containsKey(propName)
+ && !preserveProperties.contains(p.getPath())
+ && wspFilter.includesProperty(p.getPath())) {
+ vs.ensureCheckedOut();
+ p.remove();
+ updatedNode = node;
+ }
}
}
+
+ // add/modify properties contained in package
+ if (setUnprotectedProperties(node, ni, importMode == ImportMode.REPLACE|| importMode == ImportMode.UPDATE || importMode == ImportMode.UPDATE_PROPERTIES, vs)) {
+ updatedNode = node;
+ }
}
- // add/modify properties contained in package
- modified |= setUnprotectedProperties(node, ni, importMode == ImportMode.REPLACE|| importMode == ImportMode.UPDATE || importMode == ImportMode.UPDATE_PROPERTIES, vs);
- return modified;
+ return updatedNode;
}
+
/**
* Creates a new node via system view XML and {@link Session#importXML(String, InputStream, int)} to be able to set protected properties.
- * Afterwards uses regular JCR API to set unprotected properties.
- * @param currentNode
- * @param ni
- * @return
+ * Afterwards uses regular JCR API to set unprotected properties (on a best-effort basis as this depends on the repo implementation).
+ * @param parentNode the parent node below which the new node should be created
+ * @param ni the information about the new node to be created
+ * @return the newly created node
* @throws RepositoryException
*/
- private @NotNull Node createNewNode(Node currentNode, DocViewNode ni)
+ private @NotNull Node createNewNode(Node parentNode, DocViewNode ni)
throws RepositoryException {
+ final int importUuidBehavior;
+ switch(idConflictPolicy) {
+ case CREATE_NEW_ID:
+ // what happens to references?
+ importUuidBehavior = ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW;
+ break;
+ case FORCE_REMOVE_CONFLICTING_ID:
+ importUuidBehavior = ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING;
+ break;
+ default:
+ importUuidBehavior = ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW;
+ break;
+ }
try {
- String parentPath = currentNode.getPath();
+ String parentPath = parentNode.getPath();
final ContentHandler handler = session.getImportContentHandler(
parentPath,
- ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
+ importUuidBehavior);
// first define the current namespaces
String[] prefixes = session.getNamespacePrefixes();
handler.startDocument();
@@ -1080,7 +1094,7 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
handler.endDocument();
// retrieve newly created node either by uuid, label or name
- Node node = getNodeByUUIDLabelOrName(currentNode, ni);
+ Node node = getNodeByUUIDLabelOrName(parentNode, ni, importUuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
setUnprotectedProperties(node, ni, true, null);
// remove mix referenceable if it was temporarily added
if (addMixRef) {
@@ -1094,7 +1108,7 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
if (root instanceof ConstraintViolationException) {
// potentially rollback changes in the transient space (only relevant for Oak, https://issues.apache.org/jira/browse/OAK-9436), as otherwise the same exception is thrown again at Session.save()
try {
- Node node = getNodeByUUIDLabelOrName(currentNode, ni);
+ Node node = getNodeByUUIDLabelOrName(parentNode, ni, importUuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
node.remove();
} catch (RepositoryException re) {
// ignore as no node found when the transient space is clean already
@@ -1121,9 +1135,9 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
return effectiveNodeType.getApplicablePropertyDefinition(docViewProperty.name, docViewProperty.isMulti, docViewProperty.type).map(PropertyDefinition::isProtected).orElse(false);
}
- private Node getNodeByUUIDLabelOrName(@NotNull Node currentNode, @NotNull DocViewNode ni) throws RepositoryException {
+ private Node getNodeByUUIDLabelOrName(@NotNull Node currentNode, @NotNull DocViewNode ni, boolean isUuidNewlyAssigned) throws RepositoryException {
Node node = null;
- if (ni.uuid != null) {
+ if (ni.uuid != null && !isUuidNewlyAssigned) {
try {
node = currentNode.getSession().getNodeByUUID(ni.uuid);
} catch (RepositoryException e) {
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/FileArtifactHandler.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/FileArtifactHandler.java
index 6baaa84..7486ad7 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/FileArtifactHandler.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/FileArtifactHandler.java
@@ -40,9 +40,11 @@ import org.apache.jackrabbit.vault.fs.api.ItemFilterSet;
import org.apache.jackrabbit.vault.fs.api.SerializationType;
import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
import org.apache.jackrabbit.vault.fs.impl.ArtifactSetImpl;
+import org.apache.jackrabbit.vault.fs.io.ImportOptions;
import org.apache.jackrabbit.vault.util.JcrConstants;
import org.apache.jackrabbit.vault.util.MimeTypes;
import org.apache.jackrabbit.vault.util.PathUtil;
+import org.jetbrains.annotations.NotNull;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
@@ -111,7 +113,7 @@ public class FileArtifactHandler extends AbstractArtifactHandler {
* <p>
* Handles generic artifact sets
*/
- public ImportInfoImpl accept(WorkspaceFilter wspFilter, Node parent,
+ public ImportInfoImpl accept(@NotNull ImportOptions options, WorkspaceFilter wspFilter, Node parent,
String name, ArtifactSetImpl artifacts)
throws RepositoryException, IOException {
// check if any file artifacts was removed
@@ -155,7 +157,7 @@ public class FileArtifactHandler extends AbstractArtifactHandler {
if (mode != ImportMode.MERGE) {
InputSource source = primary.getInputSource();
if (source != null) {
- info.merge(importDocView(parent, source, artifacts, wspFilter));
+ info.merge(importDocView(parent, source, artifacts, wspFilter, options));
}
} else {
info.onNop(path);
@@ -226,7 +228,7 @@ public class FileArtifactHandler extends AbstractArtifactHandler {
}
if (mode != ImportMode.MERGE) {
try {
- DocViewSAXImporter handler = new DocViewSAXImporter(newParent, newName, newSet, wspFilter);
+ DocViewSAXImporter handler = new DocViewSAXImporter(newParent, newName, newSet, wspFilter, options.getIdConflictPolicy());
handler.setAclHandling(getAcHandling());
handler.setCugHandling(getCugHandling());
parseXmlWithSaxHandler(file.getInputSource(), handler);
@@ -313,14 +315,14 @@ public class FileArtifactHandler extends AbstractArtifactHandler {
}
private ImportInfoImpl importDocView(Node parent, InputSource source,
- ArtifactSetImpl artifacts, WorkspaceFilter wspFilter)
+ ArtifactSetImpl artifacts, WorkspaceFilter wspFilter, ImportOptions options)
throws RepositoryException, IOException {
String rootName = artifacts.getPrimaryData().getRelativePath();
int idx = rootName.indexOf('/');
if (idx > 0) {
rootName = rootName.substring(0, idx);
}
- DocViewSAXImporter handler = new DocViewSAXImporter(parent, rootName, artifacts, wspFilter);
+ DocViewSAXImporter handler = new DocViewSAXImporter(parent, rootName, artifacts, wspFilter, options.getIdConflictPolicy());
handler.setAclHandling(getAcHandling());
handler.setCugHandling(getCugHandling());
try {
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 c0a9119..f64cc0c 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
@@ -36,8 +36,10 @@ import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
import org.apache.jackrabbit.vault.fs.impl.ArtifactSetImpl;
import org.apache.jackrabbit.vault.fs.io.AccessControlHandling;
+import org.apache.jackrabbit.vault.fs.io.ImportOptions;
import org.apache.jackrabbit.vault.util.EffectiveNodeType;
import org.apache.jackrabbit.vault.util.JcrConstants;
+import org.jetbrains.annotations.NotNull;
/**
* Handles artifact sets with just a directory.
@@ -88,7 +90,8 @@ public class FolderArtifactHandler extends AbstractArtifactHandler {
*
* Handles generic artifact sets
*/
- public ImportInfoImpl accept(WorkspaceFilter wspFilter, Node parent, String name,
+ @Override
+ public ImportInfoImpl accept(@NotNull ImportOptions options, WorkspaceFilter wspFilter, Node parent, String name,
ArtifactSetImpl artifacts)
throws RepositoryException, IOException {
Artifact dir = artifacts.getDirectory();
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/GenericArtifactHandler.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/GenericArtifactHandler.java
index b8763b9..518bbac 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/GenericArtifactHandler.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/GenericArtifactHandler.java
@@ -28,6 +28,7 @@ import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.api.SerializationType;
import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
import org.apache.jackrabbit.vault.fs.impl.ArtifactSetImpl;
+import org.apache.jackrabbit.vault.fs.io.ImportOptions;
import org.apache.jackrabbit.vault.fs.spi.ACLManagement;
import org.apache.jackrabbit.vault.fs.spi.ServiceProviderFactory;
import org.apache.jackrabbit.vault.fs.spi.UserManagement;
@@ -57,7 +58,7 @@ public class GenericArtifactHandler extends AbstractArtifactHandler {
*
* Handles generic artifact sets
*/
- public ImportInfoImpl accept(WorkspaceFilter wspFilter, Node parent,
+ public ImportInfoImpl accept(ImportOptions options, WorkspaceFilter wspFilter, Node parent,
String name, ArtifactSetImpl artifacts)
throws RepositoryException, IOException {
Artifact primary = artifacts.getPrimaryData();
@@ -88,7 +89,7 @@ public class GenericArtifactHandler extends AbstractArtifactHandler {
}
}
try {
- DocViewSAXImporter handler = new DocViewSAXImporter(parent, name, artifacts, wspFilter);
+ DocViewSAXImporter handler = new DocViewSAXImporter(parent, name, artifacts, wspFilter, options.getIdConflictPolicy());
handler.setAclHandling(getAcHandling());
handler.setCugHandling(getCugHandling());
parseXmlWithSaxHandler(source, handler);
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/JcrSysViewTransformer.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/JcrSysViewTransformer.java
index 0fce44e..11dea93 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/JcrSysViewTransformer.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/JcrSysViewTransformer.java
@@ -70,6 +70,8 @@ public class JcrSysViewTransformer implements DocViewAdapter {
private final String existingPath;
private final Set<String> excludedNodeNames = new HashSet<String>();
+
+ private final @NotNull ImportMode importMode;
private long ignoreLevel = 0;
@@ -96,10 +98,11 @@ public class JcrSysViewTransformer implements DocViewAdapter {
this.existingPath = existingPath;
if (existingPath != null) {
// check if there is an existing node with the name
- recovery = new NodeStash(session, existingPath, importMode).excludeName("rep:cache");
+ recovery = new NodeStash(session, existingPath).excludeName("rep:cache");
recovery.stash();
}
excludeNode("rep:cache");
+ this.importMode = importMode;
}
public List<String> close() throws SAXException {
@@ -120,7 +123,7 @@ public class JcrSysViewTransformer implements DocViewAdapter {
// check for rescued child nodes
if (recovery != null) {
try {
- recovery.recover(null);
+ recovery.recover(importMode, null);
} catch (RepositoryException e) {
log.error("Error while processing rescued child nodes");
} finally {
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/NodeStash.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/NodeStash.java
index b44c7c0..2028c9b 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/NodeStash.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/NodeStash.java
@@ -16,7 +16,9 @@
*/
package org.apache.jackrabbit.vault.fs.impl.io;
+import java.util.Arrays;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import javax.jcr.Node;
@@ -25,10 +27,12 @@ import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
+import javax.jcr.Value;
import org.apache.jackrabbit.vault.fs.api.ImportInfo;
import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.util.JcrConstants;
+import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -47,8 +51,6 @@ public class NodeStash {
private final String path;
- private final ImportMode importMode;
-
private Node tmpNode;
private final Set<String> excludedNodeName = new HashSet<>();
@@ -60,14 +62,18 @@ public class NodeStash {
private static final String[] ROOTS = {"/", "/tmp", "/var", "/etc", "/content"};
+ /** The property names of those protected properties which should be stashed (and later restored) */
+ private static final List<String> PROTECTED_PROPERTIES_TO_STASH = Arrays.asList(JcrConstants.JCR_MIXINTYPES);
+
+ private static final String PROTECTED_PROPERTIES_SUFFIX = "-stashed";
+
/**
* Creates a new stash utility class which takes care of child nodes and properties in {@code path}
* @param session session to operate on
*/
- public NodeStash(Session session, String path, ImportMode importMode) {
+ public NodeStash(Session session, String path) {
this.session = session;
this.path = path;
- this.importMode = importMode;
}
/**
@@ -91,7 +97,7 @@ public class NodeStash {
}
/**
- * Adds the given name to the set of excluded child node names. the nodes that are excluded are not saved
+ * Adds the given name to the set of excluded child node names. The nodes that are excluded are not saved
* in the stash.
* @param name The name of the node to exclude
* @return "this" suitable for chaining.
@@ -103,11 +109,9 @@ public class NodeStash {
/**
* Moves the child nodes and optionally properties of the path to a temporary location.
+ * @return the stashed node's primary type (if it needs to be kept)
*/
- public void stash() {
- if (importMode == ImportMode.REPLACE) {
- return;
- }
+ public @Nullable String stash() {
try {
Node parent = session.getNode(path);
Node tmp = getOrCreateTemporaryNode();
@@ -129,26 +133,38 @@ public class NodeStash {
PropertyIterator propIterator = parent.getProperties();
while (propIterator.hasNext()) {
Property property = propIterator.nextProperty();
+ // all unprotected and some protected properties are relevant
+ String stashPropertyName;
if (!property.getDefinition().isProtected()) {
+ stashPropertyName = property.getName();
+ } else if (PROTECTED_PROPERTIES_TO_STASH.contains(property.getName())) {
+ stashPropertyName = property.getName() + PROTECTED_PROPERTIES_SUFFIX;
+ } else {
+ stashPropertyName = null;
+ }
+ if (stashPropertyName != null) {
if (property.isMultiple()) {
- tmp.setProperty(property.getName(), property.getValues(), property.getType());
+ tmp.setProperty(stashPropertyName, property.getValues(), property.getType());
} else {
- tmp.setProperty(property.getName(), property.getValue(), property.getType());
+ tmp.setProperty(stashPropertyName, property.getValue(), property.getType());
}
- }
+ }
}
+ return parent.getPrimaryNodeType().getName();
} catch (RepositoryException e) {
log.warn("error while moving child nodes (ignored)", e);
+ return null;
}
}
/**
* Moves the stashed nodes/properties back below the original path.
+ * @param importMode the import mode for the node this stash refers to
* @param importInfo the import info to record the changes
* @throws RepositoryException if an error occurs
*/
- public void recover(@Nullable ImportInfo importInfo) throws RepositoryException {
- // move the old child nodes back
+ public void recover(@NotNull ImportMode importMode, @Nullable ImportInfo importInfo) throws RepositoryException {
+ // move the old child nodes back (independent of importMode)
if (tmpNode != null) {
Node parent = session.getNode(path);
NodeIterator iter = tmpNode.getNodes();
@@ -171,14 +187,16 @@ public class NodeStash {
}
}
}
- try {
- recoverProperties(importMode==ImportMode.MERGE || importMode == ImportMode.MERGE_PROPERTIES);
- } catch (RepositoryException e) {
- log.warn("Unable to restore properties at {} due to: {}. Properties will remain in temporary location: {}",
- path, e.getMessage(), tmpNode.getPath());
- if (importInfo != null) {
- importInfo.onError(path, e);
- hasErrors = true;
+ if (importMode != ImportMode.REPLACE) {
+ try {
+ recoverProperties(importMode==ImportMode.MERGE || importMode == ImportMode.MERGE_PROPERTIES);
+ } catch (RepositoryException e) {
+ log.warn("Unable to restore properties at {} due to: {}. Properties will remain in temporary location: {}",
+ path, e.getMessage(), tmpNode.getPath());
+ if (importInfo != null) {
+ importInfo.onError(path, e);
+ hasErrors = true;
+ }
}
}
if (!hasErrors) {
@@ -188,11 +206,19 @@ public class NodeStash {
}
private void recoverProperties(boolean overwriteNewOnes) throws RepositoryException {
- PropertyIterator propIterator = tmpNode.getProperties();
Node destNode = session.getNode(path);
+
+ // restore mixins
+ Property mixinProperty = tmpNode.hasProperty(JcrConstants.JCR_MIXINTYPES + PROTECTED_PROPERTIES_SUFFIX) ? tmpNode.getProperty(JcrConstants.JCR_MIXINTYPES + PROTECTED_PROPERTIES_SUFFIX) : null;
+ if (mixinProperty != null) {
+ for (Value value : mixinProperty.getValues()) {
+ tmpNode.addMixin(value.getString());
+ }
+ }
+ PropertyIterator propIterator = tmpNode.getProperties();
while (propIterator.hasNext()) {
Property property = propIterator.nextProperty();
- if (!property.getDefinition().isProtected()) {
+ if (!property.getDefinition().isProtected() && !property.getName().endsWith(PROTECTED_PROPERTIES_SUFFIX)) {
if (!overwriteNewOnes && destNode.hasProperty(property.getName())) {
log.debug("Skipping restore property {} as it has been updated", property.getPath());
continue;
@@ -202,7 +228,7 @@ public class NodeStash {
} else {
destNode.setProperty(property.getName(), property.getValue(), property.getType());
}
- }
+ }
}
}
}
\ No newline at end of file
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/AutoSave.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/AutoSave.java
index c2bf40b..6c7d23a 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/AutoSave.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/AutoSave.java
@@ -194,7 +194,7 @@ public class AutoSave {
try {
session.save();
} catch (RepositoryException e) {
- log.error("error during auto save - retrying after refresh...");
+ log.error("error during auto save: {} - retrying after refresh...", e.getMessage());
session.refresh(true);
session.save();
}
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/ImportOptions.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/ImportOptions.java
index 73edd7d..54f3acd 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/ImportOptions.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/io/ImportOptions.java
@@ -22,11 +22,13 @@ import java.io.IOException;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
+import org.apache.jackrabbit.vault.fs.api.IdConflictPolicy;
import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.fs.api.PathMapping;
import org.apache.jackrabbit.vault.fs.api.ProgressTrackerListener;
import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
import org.apache.jackrabbit.vault.packaging.DependencyHandling;
+import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
@@ -66,6 +68,8 @@ public class ImportOptions {
private DependencyHandling dependencyHandling = null;
+ private @NotNull IdConflictPolicy idConflictPolicy = IdConflictPolicy.FAIL;
+
/**
* Default constructor.
*/
@@ -96,6 +100,7 @@ public class ImportOptions {
hookClassLoader = base.hookClassLoader;
pathMapping = base.pathMapping;
dependencyHandling = base.dependencyHandling;
+ idConflictPolicy = base.idConflictPolicy;
}
}
@@ -121,6 +126,7 @@ public class ImportOptions {
ret.hookClassLoader = hookClassLoader;
ret.pathMapping = pathMapping;
ret.dependencyHandling = dependencyHandling;
+ ret.idConflictPolicy = idConflictPolicy;
return ret;
}
@@ -421,6 +427,24 @@ public class ImportOptions {
this.dependencyHandling = dependencyHandling;
}
+ /**
+ *
+ * @return the id conflict policy
+ * @since 3.5.1
+ */
+ public @NotNull IdConflictPolicy getIdConflictPolicy() {
+ return idConflictPolicy;
+ }
+
+ /**
+ * Sets the id conflict policy (in case of unresolveable conflicts).
+ * @param idConflictPolicy the conflict policy
+ * @since 3.5.1
+ */
+ public void setIdConflictPolicy(@NotNull IdConflictPolicy idConflictPolicy) {
+ this.idConflictPolicy = idConflictPolicy;
+ }
+
@Override
public int hashCode() {
final int prime = 31;
@@ -440,6 +464,7 @@ public class ImportOptions {
result = prime * result + (patchKeepInRepo ? 1231 : 1237);
result = prime * result + ((patchParentPath == null) ? 0 : patchParentPath.hashCode());
result = prime * result + ((pathMapping == null) ? 0 : pathMapping.hashCode());
+ result = prime * result + ((idConflictPolicy == null) ? 0 : idConflictPolicy.hashCode());
result = prime * result + (strict ? 1231 : 1237);
return result;
}
@@ -506,6 +531,8 @@ public class ImportOptions {
return false;
if (strict != other.strict)
return false;
+ if (!idConflictPolicy.equals(other.idConflictPolicy))
+ return false;
return true;
}
@@ -521,7 +548,8 @@ public class ImportOptions {
+ (cndPattern != null ? "cndPattern=" + cndPattern + ", " : "") + (filter != null ? "filter=" + filter + ", " : "")
+ (hookClassLoader != null ? "hookClassLoader=" + hookClassLoader + ", " : "")
+ (pathMapping != null ? "pathMapping=" + pathMapping + ", " : "")
- + (dependencyHandling != null ? "dependencyHandling=" + dependencyHandling : "") + "]";
+ + (dependencyHandling != null ? "dependencyHandling=" + dependencyHandling + ", " : "")
+ + "idConflictPolicy=" + idConflictPolicy + "]";
}
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 2e4f533..4777004 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
@@ -78,6 +78,7 @@ import org.apache.jackrabbit.vault.fs.spi.PrivilegeInstaller;
import org.apache.jackrabbit.vault.fs.spi.ProgressTracker;
import org.apache.jackrabbit.vault.fs.spi.ServiceProviderFactory;
import org.apache.jackrabbit.vault.fs.spi.UserManagement;
+import org.apache.jackrabbit.vault.packaging.PackageException;
import org.apache.jackrabbit.vault.packaging.impl.ActivityLog;
import org.apache.jackrabbit.vault.packaging.registry.impl.JcrPackageRegistry;
import org.apache.jackrabbit.vault.util.Constants;
@@ -915,7 +916,7 @@ public class Importer {
imp = new ImportInfoImpl();
imp.onError(info.path, new IllegalStateException("Parent node not found."));
} else {
- imp = genericHandler.accept(filter, node, info.artifacts.getPrimaryData().getRelativePath(), info.artifacts);
+ imp = genericHandler.accept(opts, filter, node, info.artifacts.getPrimaryData().getRelativePath(), info.artifacts);
if (imp == null) {
throw new IllegalStateException("generic handler did not accept " + info.path);
}
@@ -946,12 +947,12 @@ public class Importer {
log.trace("skipping intermediate node at {}", info.path);
} else if (info.artifacts.getPrimaryData() == null) {
// create nt:folder node if not exists
- imp = folderHandler.accept(filter, node, info.name, info.artifacts);
+ imp = folderHandler.accept(opts, filter, node, info.name, info.artifacts);
if (imp == null) {
throw new IllegalStateException("folder handler did not accept " + info.path);
}
} else {
- imp = genericHandler.accept(filter, node, info.artifacts.getDirectory().getRelativePath(), info.artifacts);
+ imp = genericHandler.accept(opts, filter, node, info.artifacts.getDirectory().getRelativePath(), info.artifacts);
if (imp == null) {
throw new IllegalStateException("generic handler did not accept " + info.path);
}
@@ -963,7 +964,7 @@ public class Importer {
imp = new ImportInfoImpl();
imp.onError(info.path, new IllegalStateException("Parent node not found."));
} else {
- imp = fileHandler.accept(filter, node, info.name, info.artifacts);
+ imp = fileHandler.accept(opts, filter, node, info.name, info.artifacts);
if (imp == null) {
throw new IllegalStateException("file handler did not accept " + info.path);
}
@@ -1013,7 +1014,7 @@ public class Importer {
}
hasErrors = true;
if (firstException == null) {
- firstException = error;
+ firstException = new PackageException("Error creating/updating node " + path, error);
}
break;
}
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 610c0ca..11642d5 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
@@ -279,7 +279,11 @@ public class ZipVaultPackage extends PackagePropertiesImpl implements VaultPacka
log.error("Error during install.", e);
ctx.setPhase(InstallContext.Phase.INSTALL_FAILED);
hooks.execute(ctx);
- throw new PackageException(e);
+ if (e instanceof RepositoryException) {
+ throw (RepositoryException)e;
+ } else {
+ throw new PackageException(e);
+ }
}
ctx.setPhase(InstallContext.Phase.INSTALLED);
if (!hooks.execute(ctx)) {
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/util/DocViewNode.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/util/DocViewNode.java
index a7a7b90..09c0771 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/util/DocViewNode.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/util/DocViewNode.java
@@ -38,6 +38,7 @@ import org.xml.sax.Attributes;
public class DocViewNode {
public final @NotNull String name;
+ /** usually equal to {@link #name} except when this node has a same name sibling, in that case label has format {@code <name>[index]}, https://docs.adobe.com/content/docs/en/spec/jcr/2.0/22_Same-Name_Siblings.html#22.2%20Addressing%20Same-Name%20Siblings%20by%20Path */
public final @NotNull String label;
public final @NotNull Map<String, DocViewProperty> props = new HashMap<>();
public @Nullable String uuid;
diff --git a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/ImportIT.java b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/ImportIT.java
index 9a66bbf..42ffe08 100644
--- a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/ImportIT.java
+++ b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/ImportIT.java
@@ -44,6 +44,7 @@ import org.apache.jackrabbit.vault.fs.io.Importer;
import org.apache.jackrabbit.vault.fs.io.JcrArchive;
import org.apache.jackrabbit.vault.fs.io.ZipArchive;
import org.apache.jackrabbit.vault.packaging.PackageException;
+import org.codehaus.plexus.util.ExceptionUtils;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
@@ -377,7 +378,7 @@ public class ImportIT extends IntegrationTestBase {
archive.open(true);
// we don't care whether constraint is immediately enforced or only on save() as both is valid according to JCR spec
RepositoryException e = Assert.assertThrows(RepositoryException.class, () -> { importer.run(archive, rootNode); admin.save(); });
- assertEquals(ConstraintViolationException.class, e.getCause().getClass());
+ assertEquals(ConstraintViolationException.class, ExceptionUtils.getRootCause(e).getClass());
}
}
}
\ No newline at end of file
diff --git a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/ReferenceableIdentifiersImportIT.java b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/ReferenceableIdentifiersImportIT.java
new file mode 100644
index 0000000..14469c9
--- /dev/null
+++ b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/ReferenceableIdentifiersImportIT.java
@@ -0,0 +1,283 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.jcr.Node;
+import javax.jcr.PropertyIterator;
+import javax.jcr.ReferentialIntegrityException;
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.commons.JcrUtils;
+import org.apache.jackrabbit.vault.fs.api.IdConflictPolicy;
+import org.apache.jackrabbit.vault.fs.io.ImportOptions;
+import org.apache.jackrabbit.vault.packaging.PackageException;
+import org.codehaus.plexus.util.ExceptionUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Installs a package with the filter: "/tmp/referenceable", mode="replace"
+ * The package contains two referenceable nodes:
+ * {@code /tmp/referenceable} and
+ * {@code /tmp/referenceable/child-referenceable}.
+ * Both are setting property {@code someproperty="somevalue"}.
+ */
+public class ReferenceableIdentifiersImportIT extends IntegrationTestBase {
+
+ private static final String PROPERTY_NAME = "someproperty";
+ private static final String PROPERTY_VALUE = "somevalue";
+ private static final String UUID_REFERENCEABLE = "352c89a4-304f-4b87-9bed-e09275597df1";
+ private static final String UUID_REFERENCEABLE_CHILD = "a201bd6b-25b9-4255-b7db-6fc4c3ddb32d";
+
+ @Test
+ public void testOverwriteIdentifierOfReplacedNode() throws RepositoryException, IOException, PackageException {
+ // create referenceable node manually
+ Node referenceableNode = JcrUtils.getOrCreateByPath("/tmp/referenceable", null, JcrConstants.NT_UNSTRUCTURED, admin, true);
+ referenceableNode.addMixin(JcrConstants.MIX_REFERENCEABLE);
+
+ // create (non-referenceable) child node
+ JcrUtils.getOrCreateByPath("/tmp/referenceable/child", null, JcrConstants.NT_UNSTRUCTURED, admin, true);
+ admin.save();
+
+ // check UUID prior package installation
+ assertNotEquals(UUID_REFERENCEABLE, referenceableNode.getIdentifier());
+
+ // overwrite with new referenceable node (different UUID)
+ extractVaultPackageStrict("/test-packages/referenceable.zip");
+
+ assertNodeExists("/tmp/referenceable");
+ assertProperty("/tmp/referenceable/" + PROPERTY_NAME, PROPERTY_VALUE);
+ // check its UUID
+ Node node = admin.getNode("/tmp/referenceable");
+ //assertEquals(UUID_REFERENCEABLE, node.getIdentifier());
+ node = admin.getNode("/tmp/referenceable/child");
+ assertEquals(UUID_REFERENCEABLE_CHILD, node.getIdentifier());
+ }
+
+ @Test
+ public void testIdentifierCollisionOutsideFilter() throws RepositoryException, IOException, PackageException {
+ extractVaultPackageStrict("/test-packages/referenceable.zip");
+
+ assertNodeExists("/tmp/referenceable");
+ // check its UUID
+ Node node = admin.getNode("/tmp/referenceable");
+ assertEquals(UUID_REFERENCEABLE, node.getIdentifier());
+
+ // now move node (it keeps its old ID)
+ admin.move("/tmp/referenceable", "/tmp/referenceable-old");
+ admin.save();
+
+ // install package again (with default policy IdConflictPolicy.FAIL)
+ Exception e = assertThrows(Exception.class, () -> { extractVaultPackageStrict("/test-packages/referenceable.zip"); });
+ assertEquals(ReferentialIntegrityException.class, ExceptionUtils.getRootCause(e).getClass());
+ admin.refresh(false);
+
+ // now try to remove the referenced node (with policy IdConflictPolicy.CREATE_NEW_ID)
+ ImportOptions options = getDefaultOptions();
+ options.setStrict(true);
+ options.setIdConflictPolicy(IdConflictPolicy.CREATE_NEW_ID);
+ extractVaultPackage("/test-packages/referenceable.zip", options);
+ // check its UUID
+ node = admin.getNode("/tmp/referenceable");
+ assertNotEquals(UUID_REFERENCEABLE, node.getIdentifier());
+
+ // now try to remove the referenced node (with default policy IdConflictPolicy.FORCE_REMOVE_CONFLICTING_ID)
+ options.setIdConflictPolicy(IdConflictPolicy.FORCE_REMOVE_CONFLICTING_ID);
+ extractVaultPackage("/test-packages/referenceable.zip", options);
+ }
+
+ @Test
+ public void testIdentifierCollisionInsideFilter() throws RepositoryException, IOException, PackageException {
+ extractVaultPackageStrict("/test-packages/referenceable.zip");
+
+ assertNodeExists("/tmp/referenceable");
+ // check its UUID
+ Node node = admin.getNode("/tmp/referenceable/child");
+ assertEquals(UUID_REFERENCEABLE_CHILD, node.getIdentifier());
+
+ // now move node (it keeps its old ID)
+ admin.move("/tmp/referenceable/child", "/tmp/referenceable/collision");
+ admin.save();
+
+ // now add a reference to the node (which is covered by the filter)
+ Node referenceNode = JcrUtils.getOrCreateByPath("/tmp/referenceable/reference", JcrConstants.NT_UNSTRUCTURED, admin);
+ Node referenceableNode = admin.getNode("/tmp/referenceable/collision");
+ referenceNode.setProperty(PROPERTY_NAME, referenceableNode);
+ admin.save();
+ assertProperty("/tmp/referenceable/reference/" + PROPERTY_NAME, UUID_REFERENCEABLE_CHILD);
+
+ PropertyIterator propIter = referenceableNode.getReferences();
+ assertTrue(propIter.hasNext());
+
+ // install package again (with default policy IdConflictPolicy.FAIL)
+ extractVaultPackageStrict("/test-packages/referenceable.zip");
+ assertNodeExists("/tmp/referenceable");
+ // check its UUID
+ node = admin.getNode("/tmp/referenceable/child");
+ assertEquals(UUID_REFERENCEABLE_CHILD, node.getIdentifier());
+ // now move node (it keeps its old ID)
+ admin.move("/tmp/referenceable/child", "/tmp/referenceable/collision");
+ admin.save();
+
+ // now try to remove the referenced node (with policy IdConflictPolicy.CREATE_NEW_ID)
+ ImportOptions options = getDefaultOptions();
+ options.setStrict(true);
+ options.setIdConflictPolicy(IdConflictPolicy.CREATE_NEW_ID);
+ extractVaultPackage("/test-packages/referenceable.zip", options);
+ // check its UUID
+ node = admin.getNode("/tmp/referenceable/child");
+ assertNotEquals(UUID_REFERENCEABLE_CHILD, node.getIdentifier()); // is a new id
+ // now move node (it keeps its old ID)
+ admin.move("/tmp/referenceable/child", "/tmp/referenceable/collision");
+ admin.save();
+
+ // now try to remove the referenced node (with default policy IdConflictPolicy.FORCE_REMOVE_CONFLICTING_ID)
+ options.setIdConflictPolicy(IdConflictPolicy.FORCE_REMOVE_CONFLICTING_ID);
+ extractVaultPackage("/test-packages/referenceable.zip", options);
+ // check its UUID
+ node = admin.getNode("/tmp/referenceable/child");
+ assertEquals(UUID_REFERENCEABLE_CHILD, node.getIdentifier());
+ }
+
+ @Test
+ public void testIdentifierCollisionInsideFilterWithReferencesOutsideFilter() throws RepositoryException, IOException, PackageException {
+ // initial installation
+ extractVaultPackageStrict("/test-packages/referenceable.zip");
+
+ assertNodeExists("/tmp/referenceable");
+ // check its UUID
+ Node node = admin.getNode("/tmp/referenceable/child");
+ assertEquals(UUID_REFERENCEABLE_CHILD, node.getIdentifier());
+
+ // now move node (it keeps its old ID)
+ admin.move("/tmp/referenceable/child", "/tmp/referenceable/collision");
+ admin.save();
+
+ // now add a reference to the node (which is not covered by the filter)
+ Node referenceNode = JcrUtils.getOrCreateByPath("/tmp/reference", JcrConstants.NT_UNSTRUCTURED, admin);
+ Node referenceableNode = admin.getNode("/tmp/referenceable/collision");
+ referenceNode.setProperty(PROPERTY_NAME, referenceableNode);
+ admin.save();
+ assertProperty("/tmp/reference/" + PROPERTY_NAME, UUID_REFERENCEABLE_CHILD);
+
+ PropertyIterator propIter = referenceableNode.getReferences();
+ assertTrue(propIter.hasNext());
+
+ // install package again (with default policy IdConflictPolicy.FAIL)
+ Exception e = assertThrows(Exception.class, () -> { extractVaultPackageStrict("/test-packages/referenceable.zip"); } );
+ assertEquals(ReferentialIntegrityException.class, e.getClass());
+ admin.refresh(false);
+
+ // now try to remove the referenced node (with policy IdConflictPolicy.CREATE_NEW_ID)
+ ImportOptions options = getDefaultOptions();
+ options.setStrict(true);
+ options.setIdConflictPolicy(IdConflictPolicy.CREATE_NEW_ID);
+ e = assertThrows(Exception.class, () -> { extractVaultPackage("/test-packages/referenceable.zip", options);});
+ assertEquals(ReferentialIntegrityException.class, e.getClass());
+ admin.refresh(false);
+
+ // now try to remove the referenced node (with default policy IdConflictPolicy.FORCE_REMOVE_CONFLICTING_ID)
+ options.setIdConflictPolicy(IdConflictPolicy.FORCE_REMOVE_CONFLICTING_ID);
+ extractVaultPackage("/test-packages/referenceable.zip", options);
+ }
+
+ @Test
+ public void testReplaceReferencedNonConflictingIdentifier() throws RepositoryException, IOException, PackageException {
+ // create referenceable node manually (other identifier)
+ Node referenceableNode = JcrUtils.getOrCreateByPath("/tmp/referenceable", JcrConstants.NT_UNSTRUCTURED, admin);
+ referenceableNode.addMixin(JcrConstants.MIX_REFERENCEABLE);
+ admin.save();
+ String identifier = referenceableNode.getIdentifier();
+
+ // now create REFERENCE property towards referenceable node
+ Node referenceNode = JcrUtils.getOrCreateByPath("/tmp/reference", JcrConstants.NT_UNSTRUCTURED, admin);
+ referenceNode.setProperty(PROPERTY_NAME, referenceableNode);
+ assertProperty("/tmp/reference/" + PROPERTY_NAME, identifier);
+ admin.save();
+
+ // now try to remove the referenced node (with default policy IdConflictPolicy.FAIL)
+ Exception e = assertThrows(Exception.class, () -> { extractVaultPackageStrict("/test-packages/referenceable.zip");});
+ assertEquals(ReferentialIntegrityException.class, ExceptionUtils.getRootCause(e).getClass());
+ admin.refresh(false);
+
+ // now try to remove the referenced node (with policy IdConflictPolicy.CREATE_NEW_ID)
+ ImportOptions options = getDefaultOptions();
+ options.setStrict(true);
+ options.setIdConflictPolicy(IdConflictPolicy.CREATE_NEW_ID);
+ e = assertThrows(Exception.class, () -> { extractVaultPackage("/test-packages/referenceable.zip", options);});
+ assertEquals(ReferentialIntegrityException.class, ExceptionUtils.getRootCause(e).getClass());
+ admin.refresh(false);
+
+ // now try to remove the referenced node (with default policy IdConflictPolicy.FORCE_REMOVE_CONFLICTING_ID)
+ options.setIdConflictPolicy(IdConflictPolicy.FORCE_REMOVE_CONFLICTING_ID);
+ extractVaultPackage("/test-packages/referenceable.zip", options);
+ }
+
+ @Test
+ public void testOverwriteSameReferencedIdentifier() throws RepositoryException, IOException, PackageException {
+ extractVaultPackageStrict("/test-packages/referenceable.zip");
+
+ assertNodeExists("/tmp/referenceable");
+ Node node = admin.getNode("/tmp/referenceable");
+
+ // now create REFERENCE property towards referenceable node
+ Node referenceNode = JcrUtils.getOrCreateByPath("/tmp/reference", JcrConstants.NT_UNSTRUCTURED, admin);
+ referenceNode.setProperty(PROPERTY_NAME, node);
+ assertProperty("/tmp/reference/" + PROPERTY_NAME, UUID_REFERENCEABLE);
+ admin.save();
+
+ // now import again (with default policy IdConflictPolicy.FAIL)
+ extractVaultPackageStrict("/test-packages/referenceable.zip");
+
+ // now try to remove the referenced node (with policy IdConflictPolicy.CREATE_NEW_ID)
+ ImportOptions options = getDefaultOptions();
+ options.setStrict(true);
+ options.setIdConflictPolicy(IdConflictPolicy.CREATE_NEW_ID);
+ extractVaultPackage("/test-packages/referenceable.zip", options);
+
+ // now try to remove the referenced node (with default policy IdConflictPolicy.FORCE_REMOVE_CONFLICTING_ID)
+ options.setIdConflictPolicy(IdConflictPolicy.FORCE_REMOVE_CONFLICTING_ID);
+ extractVaultPackage("/test-packages/referenceable.zip", options);
+ }
+
+ @Test
+ public void testReferentialIntegrity() throws RepositoryException {
+ // create referenceable node manually
+ Node referenceableNode = JcrUtils.getOrCreateByPath("/tmp/referenceable", JcrConstants.NT_UNSTRUCTURED, admin);
+ referenceableNode.addMixin(JcrConstants.MIX_REFERENCEABLE);
+ admin.save();
+ String identifier = referenceableNode.getIdentifier();
+
+ // now create REFERENCE property towards referenceable node
+ Node referenceNode = JcrUtils.getOrCreateByPath("/tmp/reference", JcrConstants.NT_UNSTRUCTURED, admin);
+ referenceNode.setProperty(PROPERTY_NAME, referenceableNode);
+ assertProperty("/tmp/reference/" + PROPERTY_NAME, identifier);
+ admin.save();
+
+ // try to remove referenceable node -> fails with RIE
+ Assert.assertThrows(ReferentialIntegrityException.class, () -> { referenceableNode.remove(); admin.save();});
+ }
+}
\ No newline at end of file
diff --git a/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/config.xml b/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/config.xml
new file mode 100644
index 0000000..b525f1c
--- /dev/null
+++ b/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/config.xml
@@ -0,0 +1,93 @@
+<!--
+ 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.
+ -->
+<vaultfs version="1.1">
+ <!--
+ Defines the content aggregation. The order of the defined aggregates
+ is important for finding the correct aggregator.
+ -->
+ <aggregates>
+ <!--
+ Defines an aggregate that handles nt:file and nt:resource nodes.
+ -->
+ <aggregate type="file" title="File Aggregate"/>
+
+ <!--
+ Defines an aggregate that handles file/folder like nodes. It matches
+ all nt:hierarchyNode nodes that have or define a jcr:content
+ child node and excludes child nodes that are nt:hierarchyNodes.
+ -->
+ <aggregate type="filefolder" title="File/Folder Aggregate"/>
+
+ <!--
+ Defines an aggregate that handles nt:nodeType nodes and serializes
+ them into .cnd notation.
+ -->
+ <aggregate type="nodetype" title="Node Type Aggregate" />
+
+ <!--
+ Defines an aggregate that defines full coverage for certain node
+ types that cannot be covered by the default aggregator.
+ -->
+ <aggregate type="full" title="Full Coverage Aggregate">
+ <matches>
+ <include nodeType="rep:AccessControl" respectSupertype="true" />
+ <include nodeType="rep:Policy" respectSupertype="true" />
+ <include nodeType="cq:Widget" respectSupertype="true" />
+ <include nodeType="cq:EditConfig" respectSupertype="true" />
+ <include nodeType="cq:WorkflowModel" respectSupertype="true" />
+ <include nodeType="vlt:FullCoverage" respectSupertype="true" />
+ <include nodeType="mix:language" respectSupertype="true" />
+ <include nodeType="sling:OsgiConfig" respectSupertype="true" />
+ </matches>
+ </aggregate>
+
+ <!--
+ Defines an aggregate that handles nt:folder like nodes.
+ -->
+ <aggregate type="generic" title="Folder Aggregate">
+ <matches>
+ <include nodeType="nt:folder" respectSupertype="true" />
+ </matches>
+ <contains>
+ <exclude isNode="true" />
+ </contains>
+ </aggregate>
+
+ <!--
+ Defines the default aggregate
+ -->
+ <aggregate type="generic" title="Default Aggregator" isDefault="true">
+ <matches>
+ <!-- all -->
+ </matches>
+ <contains>
+ <exclude nodeType="nt:hierarchyNode" respectSupertype="true" />
+ </contains>
+ </aggregate>
+
+ </aggregates>
+
+ <!--
+ defines the input handlers
+ -->
+ <handlers>
+ <handler type="folder"/>
+ <handler type="file"/>
+ <handler type="nodetype"/>
+ <handler type="generic"/>
+ </handlers>
+</vaultfs>
diff --git a/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/definition/.content.xml b/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/definition/.content.xml
new file mode 100644
index 0000000..c57b6a8
--- /dev/null
+++ b/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/definition/.content.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jcr:root xmlns:vlt="http://www.day.com/jcr/vault/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
+ jcr:created="{Date}2015-12-16T16:59:10.779+01:00"
+ jcr:createdBy="admin"
+ jcr:description=""
+ jcr:lastModified="{Date}2015-12-16T16:59:10.779+01:00"
+ jcr:lastModifiedBy="admin"
+ jcr:primaryType="vlt:PackageDefinition"
+ buildCount="5"
+ builtWith="Adobe Experience Manager-6.1.0.20150507"
+ group="my_packages"
+ lastUnwrapped="{Date}2015-12-16T16:59:10.779+01:00"
+ lastUnwrappedBy="admin"
+ lastWrapped="{Date}2015-12-16T16:59:10.779+01:00"
+ lastWrappedBy="admin"
+ name="test_referenceable"
+ version="">
+ <filter jcr:primaryType="nt:unstructured">
+ <f0
+ jcr:primaryType="nt:unstructured"
+ mode="replace"
+ root="/tmp/referenceable"
+ rules="[]"/>
+ </filter>
+</jcr:root>
diff --git a/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/filter.xml b/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/filter.xml
new file mode 100644
index 0000000..5258896
--- /dev/null
+++ b/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/filter.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<workspaceFilter version="1.0">
+ <filter root="/tmp/referenceable"/>
+</workspaceFilter>
diff --git a/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/nodetypes.cnd b/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/nodetypes.cnd
new file mode 100644
index 0000000..c348e28
--- /dev/null
+++ b/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/nodetypes.cnd
@@ -0,0 +1,17 @@
+<'vlt'='http://www.day.com/jcr/vault/1.0'>
+<'sling'='http://sling.apache.org/jcr/sling/1.0'>
+<'nt'='http://www.jcp.org/jcr/nt/1.0'>
+<'rep'='internal'>
+
+[vlt:FullCoverage]
+ mixin
+
+[sling:Folder] > nt:folder
+ - * (undefined) multiple
+ - * (undefined)
+ + * (nt:base) = sling:Folder version
+
+[rep:RepoAccessControllable]
+ mixin
+ + rep:repoPolicy (rep:Policy) protected ignore
+
diff --git a/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/properties.xml b/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/properties.xml
new file mode 100644
index 0000000..2494f50
--- /dev/null
+++ b/vault-core/src/test/resources/test-packages/referenceable.zip/META-INF/vault/properties.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
+<properties>
+<comment>FileVault Package Properties</comment>
+<entry key="createdBy">admin</entry>
+<entry key="name">test_referenceable</entry>
+<entry key="lastModified">2015-12-16T16:59:10.779+01:00</entry>
+<entry key="lastModifiedBy">admin</entry>
+<entry key="created">2015-12-16T16:59:10.795+01:00</entry>
+<entry key="buildCount">5</entry>
+<entry key="version"/>
+<entry key="dependencies"/>
+<entry key="packageFormatVersion">2</entry>
+<entry key="description"/>
+<entry key="lastWrapped">2015-12-16T16:59:10.779+01:00</entry>
+<entry key="group">my_packages</entry>
+<entry key="lastWrappedBy">admin</entry>
+</properties>
diff --git a/vault-core/src/test/resources/test-packages/referenceable.zip/jcr_root/.content.xml b/vault-core/src/test/resources/test-packages/referenceable.zip/jcr_root/.content.xml
new file mode 100644
index 0000000..8ea9f2a
--- /dev/null
+++ b/vault-core/src/test/resources/test-packages/referenceable.zip/jcr_root/.content.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:rep="internal"
+ jcr:mixinTypes="[rep:AccessControllable,rep:RepoAccessControllable]"
+ jcr:primaryType="rep:root"
+ sling:resourceType="sling:redirect"
+ sling:target="/index.html"/>
diff --git a/vault-core/src/test/resources/test-packages/referenceable.zip/jcr_root/tmp/.content.xml b/vault-core/src/test/resources/test-packages/referenceable.zip/jcr_root/tmp/.content.xml
new file mode 100644
index 0000000..54084a8
--- /dev/null
+++ b/vault-core/src/test/resources/test-packages/referenceable.zip/jcr_root/tmp/.content.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:rep="internal"
+ jcr:mixinTypes="[rep:AccessControllable]"
+ jcr:primaryType="sling:Folder"/>
diff --git a/vault-core/src/test/resources/test-packages/referenceable.zip/jcr_root/tmp/referenceable.xml b/vault-core/src/test/resources/test-packages/referenceable.zip/jcr_root/tmp/referenceable.xml
new file mode 100644
index 0000000..2d1995c
--- /dev/null
+++ b/vault-core/src/test/resources/test-packages/referenceable.zip/jcr_root/tmp/referenceable.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:vlt="http://www.day.com/jcr/vault/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:mix="http://www.jcp.org/jcr/mix/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
+ jcr:mixinTypes="[mix:referenceable,vlt:FullCoverage]"
+ jcr:primaryType="sling:Folder"
+ jcr:uuid="352c89a4-304f-4b87-9bed-e09275597df1"
+ someproperty="somevalue">
+ <child
+ jcr:mixinTypes="[mix:referenceable]"
+ jcr:primaryType="nt:unstructured"
+ jcr:uuid="a201bd6b-25b9-4255-b7db-6fc4c3ddb32d"
+ someproperty="somevalue"/>
+</jcr:root>