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/05/07 11:25:12 UTC
[jackrabbit-filevault] 01/01: add new import modes,
clarify existing ones.
This is an automated email from the ASF dual-hosted git repository.
kwin pushed a commit to branch bugfix/JCRVLT-255-importmodes-for-docview
in repository https://gitbox.apache.org/repos/asf/jackrabbit-filevault.git
commit d3d12978efcf78b9eb1800afdc953c5e17f80440
Author: Konrad Windszus <kw...@apache.org>
AuthorDate: Thu Apr 22 15:34:45 2021 +0200
add new import modes, clarify existing ones.
---
.../apache/jackrabbit/vault/fs/api/ImportMode.java | 49 +++-
.../jackrabbit/vault/fs/api/package-info.java | 2 +-
.../vault/fs/impl/io/DocViewSAXImporter.java | 281 ++++++++++-----------
.../vault/fs/impl/io/FileArtifactHandler.java | 62 ++---
.../vault/fs/impl/io/FolderArtifactHandler.java | 35 +--
.../vault/fs/impl/io/JcrSysViewTransformer.java | 18 +-
.../io/{ChildNodeStash.java => NodeStash.java} | 111 +++++---
.../apache/jackrabbit/vault/util/DocViewNode.java | 13 +-
.../vault/packaging/integration/ImportModeIT.java | 47 +++-
.../META-INF/vault/filter.xml | 2 +
.../jcr_root/testroot/.content.xml | 18 +-
11 files changed, 372 insertions(+), 266 deletions(-)
diff --git a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/ImportMode.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/ImportMode.java
index e19418b..eab8d65 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/ImportMode.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/api/ImportMode.java
@@ -20,6 +20,22 @@ package org.apache.jackrabbit.vault.fs.api;
/**
* {@code ImportMode} is used to define how importing content is applied
* to the existing content in the repository.
+ *
+ * <table border="1">
+ * <tr><th rowspan="2">Import Mode</th><th colspan="3">Property/Node (at a specific path)</th></tr>
+ * <tr><th>In Package</th><th>In Repository Before Installation</th><th>In Repository After Installation</th></tr>
+ * <tr><td rowspan="3">{@link #REPLACE}</td><td>non-existing</td><td>existing</td><td>removed</td></tr>
+ * <tr><td>existing</td><td>existing</td><td>replaced</td></tr>
+ * <tr><td>existing</td><td>non-existing</td><td>created</td></tr>
+ * <tr><td rowspan="3">{@link #MERGE_PROPERTIES}</td><td>non-existing</td><td>existing</td><td>not touched</td></tr>
+ * <tr><td>existing</td><td>existing</td><td>not touched</td></tr>
+ * <tr><td>existing</td><td>non-existing</td><td>created</td></tr>
+ * <tr><td rowspan="3">{@link #UPDATE_PROPERTIES}</td><td>non-existing</td><td>existing</td><td>not touched</td></tr>
+ * <tr><td>existing</td><td>existing</td><td>replaced</td></tr>
+ * <tr><td>existing</td><td>non-existing</td><td>created</td></tr>
+ * </table>
+ *
+ * TODO: Handling of {code oak:incrementalCounter}.
*/
public enum ImportMode {
@@ -31,12 +47,39 @@ public enum ImportMode {
/**
* Existing content is not modified, i.e. only new content is added and
- * none is deleted or modified.
+ * none is deleted or modified
+ * <p>
+ * <strong>Only considered for
+ * <ul>
+ * <li>Binaries, i.e. they will never be imported if the parent node has this import mode.</li>
+ * <li>Authorizable nodes: only {@code rep:members} of existing authorizables is updated, no other property.</li>
+ * <li>Simple files: i.e. they will never be imported in case the repo has this file already.
+ * <li>Other docview files: It will ignore them in case the docview's root node does already exist in the repo (both full coverage and .content.xml). It skips non-existing child nodes/properties in the docview as well.</li>
+ * </ul>
+ * <strong></p>
+ *
+ * @deprecated As this behaves inconsistently for the different serialization formats, rather use {@link #MERGE_PROPERTIES}.
*/
+ @Deprecated()
MERGE,
/**
- * Existing content is updated, new content is added and none is deleted.
+ * Existing properties are replaced (except for {@code jcr:primaryType}), new properties and nodes are added and no existing properties or nodes are deleted.
+ * <strong>Only affects authorizable nodes (not their child nodes). Other nodes are imported in mode {@link #REPLACE}.</strong>
+ * @deprecated rather use {@link #UPDATE_PROPERTIES}
*/
- UPDATE
+ @Deprecated()
+ UPDATE,
+
+ /**
+ * Existing properties are not touched, new nodes/properties are added, no existing nodes/properties are deleted
+ * The only existing properties potentially touched is the multi value property {@code jcr:mixinType}.
+ */
+ MERGE_PROPERTIES,
+
+ /**
+ * Existing properties are replaced, new nodes/properties are added, no existing nodes/properties are deleted
+ * Existing multi-value properties are replaced and not extended except for {@code jcr:mixinType} which is extended.
+ */
+ UPDATE_PROPERTIES
}
\ No newline at end of file
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 3995610..38c4265 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.8.0")
+@Version("2.9.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/DocViewSAXImporter.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/DocViewSAXImporter.java
index 383ff90..b3d31ae 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
@@ -43,7 +43,12 @@ import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
+import javax.jcr.ValueFormatException;
+import javax.jcr.lock.LockException;
+import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.jcr.nodetype.NodeType;
+import javax.jcr.version.VersionException;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver;
@@ -71,6 +76,7 @@ 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;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
@@ -642,19 +648,21 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
log.trace("Skipping ignored element {}", name);
}
} else {
+ final String path = (!node.getPath().equals("/") ? node.getPath() : "")+ "/" + name;
if (attributes.getLength() == 0) {
// only ordering node. skip
- log.trace("Skipping empty node {}", node.getPath() + "/" + name);
+ log.trace("Skipping empty node {}", path);
stack = stack.push();
} else if (snsNode) {
// skip SNS nodes with index > 1
- log.warn("Skipping unsupported SNS node with index > 1. Some content will be missing after import: {}", node.getPath() + "/" + label);
+ log.warn("Skipping unsupported SNS node with index > 1. Some content will be missing after import: {}", path);
stack = stack.push();
} else {
try {
- AccessControlHandling acHandling = getAcHandling(label);
DocViewNode ni = new DocViewNode(name, label, attributes, npResolver);
+ // is policy node?
if (aclManagement.isACLNodeType(ni.primary)) {
+ AccessControlHandling acHandling = getAcHandling(label);
if (acHandling != AccessControlHandling.CLEAR && acHandling != AccessControlHandling.IGNORE) {
log.trace("Access control policy element detected. starting special transformation {}/{}", node.getPath(), name);
if (aclManagement.ensureAccessControllable(node, ni.primary)) {
@@ -676,18 +684,15 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
stack = stack.push();
}
} else if (userManagement != null && userManagement.isAuthorizableNodeType(ni.primary)) {
+ // is authorizable node?
handleAuthorizable(node, ni);
} else {
+ // regular node
stack = stack.push(addNode(ni));
}
} catch (RepositoryException e) {
- String errPath = node.getPath();
- if (errPath.length() > 1) {
- errPath += "/";
- }
- errPath += name;
- log.error("Error during processing of {}: {}", errPath, e.toString());
- importInfo.onError(errPath, e);
+ log.error("Error during processing of {}: {}", path, e.toString());
+ importInfo.onError(path, e);
stack = stack.push();
}
}
@@ -718,7 +723,7 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
// just import the authorizable node
log.trace("Authorizable element detected. starting sysview transformation {}", newPath);
stack = stack.push();
- stack.adapter = new JcrSysViewTransformer(node);
+ stack.adapter = new JcrSysViewTransformer(node, wspFilter.getImportMode(newPath));
stack.adapter.startNode(ni);
importInfo.onCreated(newPath);
return;
@@ -761,7 +766,7 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
// just replace the entire subtree for now.
log.trace("Authorizable element detected. starting sysview transformation {}", newPath);
stack = stack.push();
- stack.adapter = new JcrSysViewTransformer(node);
+ stack.adapter = new JcrSysViewTransformer(node, mode);
stack.adapter.startNode(ni);
importInfo.onReplaced(newPath);
break;
@@ -769,7 +774,7 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
case UPDATE:
log.trace("Authorizable element detected. starting sysview transformation {}", newPath);
stack = stack.push();
- stack.adapter = new JcrSysViewTransformer(node, oldPath);
+ stack.adapter = new JcrSysViewTransformer(node, oldPath, mode);
// we need to tweak the ni.name so that the sysview import does not
// rename the authorizable node name
String newName = Text.getName(oldPath);
@@ -847,12 +852,15 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
oldNode = null;
}
+ // TODO: under which condition do we end up here?
if (oldNode != null) {
// check versionable
new VersioningState(stack, oldNode).ensureCheckedOut();
- ChildNodeStash recovery = new ChildNodeStash(session);
- recovery.stashChildren(oldNode);
+ 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
@@ -865,10 +873,11 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
oldNode.remove();
// now create the new node
- node = createNode(currentNode, ni);
+ node = createNewNode(currentNode, ni);
+
// move the children back
- recovery.recoverChildren(node, importInfo);
+ recovery.recover(importInfo);
importInfo.onReplaced(node.getPath());
return new StackElement(node, false);
@@ -879,8 +888,8 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
boolean isCheckedIn = coProp != null && "false".equals(coProp.values[0]);
// create or update node
- boolean isNew = false;
- if (node == null) {
+ boolean isNew = node == null;
+ if (isNew) {
// workaround for bug in jcr2spi if mixins are empty
if (!ni.props.containsKey(JcrConstants.JCR_MIXINTYPES)) {
ni.props.put(JcrConstants.JCR_MIXINTYPES,
@@ -888,7 +897,7 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
}
stack.ensureCheckedOut();
- node = createNode(currentNode, ni);
+ node = createNewNode(currentNode, ni);
if (node.isNodeType(JcrConstants.NT_RESOURCE)) {
if (!node.hasProperty(JcrConstants.JCR_DATA)) {
importInfo.onMissing(node.getPath() + "/" + JcrConstants.JCR_DATA);
@@ -898,53 +907,56 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
importInfo.registerToVersion(node.getPath());
}
importInfo.onCreated(node.getPath());
- isNew = true;
} else if (isIncluded(node, node.getDepth() - rootDepth)) {
- boolean modified = false;
-
if (isCheckedIn) {
// don't rely on isVersionable here, since SPI might not have this info yet
importInfo.registerToVersion(node.getPath());
}
- VersioningState vs = new VersioningState(stack, node);
-
- // set new primary type (but never set rep:root)
- if (!"rep:root".equals(ni.primary)) {
- if (!node.getPrimaryNodeType().getName().equals(ni.primary)) {
- vs.ensureCheckedOut();
- node.setPrimaryType(ni.primary);
- modified = true;
+ 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);
+ }
}
+ importInfo.onModified(node.getPath());
+ } else {
+ importInfo.onNop(node.getPath());
}
+ } else {
+ // remove registered binaries outside of the filter (JCR-126)
+ binaries.remove(node.getPath());
+ }
+ return new StackElement(node, isNew);
+ }
- // remove the 'system' properties from the set
- ni.props.remove(JcrConstants.JCR_PRIMARYTYPE);
- ni.props.remove(JcrConstants.JCR_MIXINTYPES);
- 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);
-
- // adjust mixins
- Set<String> newMixins = new HashSet<String>();
- boolean isAtomicCounter = false;
- 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);
-
- if ("mix:atomicCounter".equals(mixin)) {
- isAtomicCounter = true;
- }
- }
+ 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;
+ }
+ }
+
+ // 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);
}
}
- // remove mixin not in package
+ }
+ // remove mixin not in package
+ if (importMode == ImportMode.REPLACE) {
for (NodeType mix : node.getMixinNodeTypes()) {
String name = mix.getName();
if (!newMixins.remove(name)) {
@@ -958,15 +970,17 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
}
}
}
+ }
+
+ // add remaining mixins (for all import modes)
+ for (String mixin : newMixins) {
+ vs.ensureCheckedOut();
+ node.addMixin(mixin);
+ modified = true;
+ }
- // add remaining mixins
- for (String mixin : newMixins) {
- vs.ensureCheckedOut();
- node.addMixin(mixin);
- modified = true;
- }
-
- // remove properties not in the set
+ // remove properties not in package
+ if (importMode == ImportMode.REPLACE) {
PropertyIterator pIter = node.getProperties();
while (pIter.hasNext()) {
Property p = pIter.nextProperty();
@@ -984,56 +998,19 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
}
}
}
- // add properties
- for (DocViewProperty prop : ni.props.values()) {
- if (prop != null && !PROTECTED_PROPERTIES.contains(prop.name)) {
- try {
- modified |= prop.apply(node);
- } catch (RepositoryException e) {
- try {
- // try again with checked out node
- vs.ensureCheckedOut();
- modified |= prop.apply(node);
- } catch (RepositoryException e1) {
- log.warn("Error while setting property (ignore): " + e1);
- }
- }
- }
- }
- // adjust oak atomic counter
- if (isAtomicCounter) {
- long previous = 0;
- if (node.hasProperty("oak:counter")) {
- previous = node.getProperty("oak:counter").getLong();
- }
- long counter = 0;
- try {
- counter = Long.valueOf(ni.getValue("oak:counter"));
- } catch (NumberFormatException e) {
- // ignore
- }
- node.setProperty("oak:increment", counter - previous);
- modified = true;
- }
-
- if (modified) {
- if (node.isNodeType(JcrConstants.NT_RESOURCE)) {
- if (!node.hasProperty(JcrConstants.JCR_DATA)) {
- importInfo.onMissing(node.getPath() + "/" + JcrConstants.JCR_DATA);
- }
- }
- importInfo.onModified(node.getPath());
- } else {
- importInfo.onNop(node.getPath());
- }
- } else {
- // remove registered binaries outside of the filter (JCR-126)
- binaries.remove(node.getPath());
}
- return new StackElement(node, isNew);
+ // add properties
+ modified |= setUnprotectedProperties(node, ni, importMode == ImportMode.REPLACE|| importMode == ImportMode.UPDATE || importMode == ImportMode.UPDATE_PROPERTIES, vs);
+ return modified;
}
-
- private Node createNode(Node currentNode, DocViewNode ni)
+ /**
+ * Creates a new node via system view XML and {@link Session#importXML(String, InputStream, int)} to be able to set protected properties as well
+ * @param currentNode
+ * @param ni
+ * @return
+ * @throws RepositoryException
+ */
+ private @NotNull Node createNewNode(Node currentNode, DocViewNode ni)
throws RepositoryException {
try {
String parentPath = currentNode.getPath();
@@ -1049,7 +1026,7 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
AttributesImpl attrs = new AttributesImpl();
attrs.addAttribute(Name.NS_SV_URI, "name", "sv:name", "CDATA", ni.name);
handler.startElement(Name.NS_SV_URI, "node", "sv:node", attrs);
-
+
// check if SNS and a helper uuid if needed
boolean addMixRef = false;
if (!ni.label.equals(ni.name) && ni.uuid == null) {
@@ -1098,7 +1075,7 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
}
handler.endElement(Name.NS_SV_URI, "node", "sv:node");
handler.endDocument();
-
+
// retrieve newly created node either by uuid, label or name
Node node = null;
if (ni.uuid != null) {
@@ -1123,39 +1100,13 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
throw e;
}
}
-
- // handle non protected properties
- for (DocViewProperty p : ni.props.values()) {
- if (p != null && p.values != null) {
- if (!PROTECTED_PROPERTIES.contains(p.name)) {
- try {
- p.apply(node);
- } catch (RepositoryException e) {
- log.warn("Error while setting property (ignore): " + e);
- }
- }
- }
- }
-
- // check for atomic counter
- if (ni.mixins != null) {
- for (String mixin : ni.mixins) {
- if ("mix:atomicCounter".equals(mixin)) {
- String counter = ni.getValue("oak:counter");
- if (counter != null) {
- node.setProperty("oak:increment", counter, PropertyType.LONG);
- }
- break;
- }
- }
- }
-
+ setUnprotectedProperties(node, ni, true, null);
// remove mix referenceable if it was temporarily added
if (addMixRef) {
node.removeMixin(JcrConstants.MIX_REFERENCEABLE);
}
return node;
-
+
} catch (SAXException e) {
Exception root = e.getException();
if (root instanceof RepositoryException) {
@@ -1168,6 +1119,52 @@ public class DocViewSAXImporter extends RejectingEntityDefaultHandler implements
}
}
+ private boolean setUnprotectedProperties(@NotNull Node node, @NotNull DocViewNode ni, boolean overwriteExistingProperties, @Nullable VersioningState vs) throws RepositoryException {
+ boolean isAtomicCounter = false;
+ for (String mixin : ni.mixins) {
+ if ("mix:atomicCounter".equals(mixin)) {
+ isAtomicCounter = true;
+ }
+ }
+ boolean modified = false;
+ // add properties
+ for (DocViewProperty prop : ni.props.values()) {
+ if (prop != null && !PROTECTED_PROPERTIES.contains(prop.name) && (overwriteExistingProperties || !node.hasProperty(prop.name))) {
+ try {
+ modified |= prop.apply(node);
+ } catch (RepositoryException e) {
+ if (vs == null) {
+ throw e;
+ }
+ try {
+ // try again with checked out node
+ vs.ensureCheckedOut();
+ modified |= prop.apply(node);
+ } catch (RepositoryException e1) {
+ log.warn("Error while setting property (ignore): " + e1);
+ }
+ }
+ }
+ }
+
+ // adjust oak atomic counter
+ if (isAtomicCounter) {
+ long previous = 0;
+ if (node.hasProperty("oak:counter")) {
+ previous = node.getProperty("oak:counter").getLong();
+ }
+ long counter = 0;
+ try {
+ counter = Long.valueOf(ni.getValue("oak:counter"));
+ } catch (NumberFormatException e) {
+ // ignore
+ }
+ node.setProperty("oak:increment", counter - previous);
+ modified = true;
+ }
+ return modified;
+ }
+
/**
* {@inheritDoc}
*/
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 516b08d..7ef37bd 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
@@ -32,6 +32,7 @@ import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
+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.ImportArtifact;
@@ -43,14 +44,13 @@ import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
import org.apache.jackrabbit.vault.fs.impl.ArtifactSetImpl;
import org.apache.jackrabbit.vault.util.JcrConstants;
import org.apache.jackrabbit.vault.util.MimeTypes;
-import org.apache.jackrabbit.vault.util.PathUtil;
-import org.apache.jackrabbit.util.Text;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Creates nt:file structures from {@link SerializationType#XML_GENERIC} or
- * {@link SerializationType#GENERIC} artifacts.
+ * {@link SerializationType#GENERIC} artifacts as well complex node structure for full-coverage artifacts
+ * with {@link SerializationType#XML_DOCVIEW}.
*
*/
public class FileArtifactHandler extends AbstractArtifactHandler {
@@ -146,20 +146,9 @@ public class FileArtifactHandler extends AbstractArtifactHandler {
if (info == null) {
info = new ImportInfoImpl();
}
- // check import mode
- ImportMode mode = ImportMode.REPLACE;
- String path = PathUtil.getPath(parent, primary.getRelativePath());
- if (primary.getRelativePath().length() == 0 || parent.hasNode(primary.getRelativePath())) {
- mode = wspFilter.getImportMode(path);
- }
- // only update if not MERGE (i.e. is REPLACE or UPDATE)
- if (mode != ImportMode.MERGE) {
- InputSource source = primary.getInputSource();
- if (source != null) {
- info.merge(importDocView(parent, source, artifacts, wspFilter));
- }
- } else {
- info.onNop(path);
+ InputSource source = primary.getInputSource();
+ if (source != null) {
+ info.merge(importDocView(parent, source, artifacts, wspFilter));
}
}
// handle files
@@ -216,31 +205,20 @@ public class FileArtifactHandler extends AbstractArtifactHandler {
}
ArtifactSetImpl newSet = new ArtifactSetImpl();
newSet.setCoverage(ItemFilterSet.INCLUDE_ALL);
-
- // check import mode
- ImportMode mode = ImportMode.REPLACE;
- String path = PathUtil.getPath(newParent, newName);
- if (newName.length() == 0 || newParent.hasNode(newName)) {
- mode = wspFilter.getImportMode(path);
- }
- if (mode != ImportMode.MERGE) {
- try {
- DocViewSAXImporter handler = new DocViewSAXImporter(newParent, newName, newSet, wspFilter);
- handler.setAclHandling(getAcHandling());
- handler.setCugHandling(getCugHandling());
- SAXParserFactory factory = SAXParserFactory.newInstance();
- factory.setNamespaceAware(true);
- factory.setFeature("http://xml.org/sax/features/namespace-prefixes", false);
- SAXParser parser = factory.newSAXParser();
- parser.parse(file.getInputSource(), handler);
- info.merge(handler.getInfo());
- } catch (ParserConfigurationException e) {
- throw new RepositoryException(e);
- } catch (SAXException e) {
- throw new RepositoryException(e);
- }
- } else {
- info.onNop(path);
+ try {
+ DocViewSAXImporter handler = new DocViewSAXImporter(newParent, newName, newSet, wspFilter);
+ handler.setAclHandling(getAcHandling());
+ handler.setCugHandling(getCugHandling());
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ factory.setFeature("http://xml.org/sax/features/namespace-prefixes", false);
+ SAXParser parser = factory.newSAXParser();
+ parser.parse(file.getInputSource(), handler);
+ info.merge(handler.getInfo());
+ } catch (ParserConfigurationException e) {
+ throw new RepositoryException(e);
+ } catch (SAXException e) {
+ throw new RepositoryException(e);
}
} else {
throw new IllegalArgumentException("Files of type " + file.getSerializationType() + " can't be handled by this handler " + this);
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 1e637e7..9dd27d0 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
@@ -23,6 +23,8 @@ import java.util.Set;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import org.apache.jackrabbit.vault.fs.api.Artifact;
@@ -104,7 +106,7 @@ public class FolderArtifactHandler extends AbstractArtifactHandler {
Node node = parent.getNode(dir.getRelativePath());
if (wspFilter.contains(node.getPath()) && !nodeType.equals(node.getPrimaryNodeType().getName())) {
- node = modifyPrimaryType(node, info);
+ modifyPrimaryType(node, info);
}
NodeIterator iter = node.getNodes();
while (iter.hasNext()) {
@@ -138,23 +140,26 @@ public class FolderArtifactHandler extends AbstractArtifactHandler {
return info;
}
- private Node modifyPrimaryType(Node node, ImportInfoImpl info) throws RepositoryException {
- String name = node.getName();
- Node parent = node.getParent();
-
+ /**
+ * This is potentially a destructive operation as it will remove all (non-protected) properties before doing the conversion
+ * @param node
+ * @param info
+ * @throws RepositoryException
+ */
+ private void modifyPrimaryType(Node node, ImportInfoImpl info) throws RepositoryException {
// check versionable
ensureCheckedOut(node, info);
- ChildNodeStash recovery = new ChildNodeStash(node.getSession());
- recovery.stashChildren(node);
- node.remove();
-
- // now create the new node
- Node newNode = parent.addNode(name, nodeType);
- info.onReplaced(newNode.getPath());
- // move the children back
- recovery.recoverChildren(newNode, info);
- return newNode;
+ // remove all non-allowed properties
+ PropertyIterator propertyIterator = node.getProperties();
+ while (propertyIterator.hasNext()) {
+ Property property = propertyIterator.nextProperty();
+ if (!property.getDefinition().isProtected()) {
+ property.remove();
+ }
+ }
+ node.setPrimaryType(nodeType);
+
}
private void ensureCheckedOut(Node node, ImportInfoImpl info) throws RepositoryException {
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 cee7bd9..0fce44e 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
@@ -20,7 +20,6 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.Stack;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.Node;
@@ -30,8 +29,11 @@ import javax.jcr.RepositoryException;
import javax.jcr.Session;
import org.apache.jackrabbit.spi.Name;
+import org.apache.jackrabbit.vault.fs.api.ImportMode;
import org.apache.jackrabbit.vault.util.DocViewNode;
import org.apache.jackrabbit.vault.util.DocViewProperty;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;
@@ -59,7 +61,7 @@ public class JcrSysViewTransformer implements DocViewAdapter {
/**
* temporary recovery helper when 'rescuing' the child nodes
*/
- private ChildNodeStash recovery;
+ private NodeStash recovery;
private String rootName;
@@ -71,11 +73,11 @@ public class JcrSysViewTransformer implements DocViewAdapter {
private long ignoreLevel = 0;
- public JcrSysViewTransformer(Node node) throws SAXException, RepositoryException {
- this(node, null);
+ public JcrSysViewTransformer(@NotNull Node node, @NotNull ImportMode importMode) throws SAXException, RepositoryException {
+ this(node, null, importMode);
}
- JcrSysViewTransformer(Node node, String existingPath) throws RepositoryException, SAXException {
+ JcrSysViewTransformer(@NotNull Node node, @Nullable String existingPath, @NotNull ImportMode importMode) throws RepositoryException, SAXException {
Session session = node.getSession();
parent = node;
handler = session.getImportContentHandler(
@@ -94,8 +96,8 @@ public class JcrSysViewTransformer implements DocViewAdapter {
this.existingPath = existingPath;
if (existingPath != null) {
// check if there is an existing node with the name
- recovery = new ChildNodeStash(session).excludeName("rep:cache");
- recovery.stashChildren(existingPath);
+ recovery = new NodeStash(session, existingPath, importMode).excludeName("rep:cache");
+ recovery.stash();
}
excludeNode("rep:cache");
}
@@ -118,7 +120,7 @@ public class JcrSysViewTransformer implements DocViewAdapter {
// check for rescued child nodes
if (recovery != null) {
try {
- recovery.recoverChildren(existingPath);
+ recovery.recover(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/ChildNodeStash.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/NodeStash.java
similarity index 56%
rename from vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/ChildNodeStash.java
rename to vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/NodeStash.java
index bf19c69..b44c7c0 100644
--- a/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/ChildNodeStash.java
+++ b/vault-core/src/main/java/org/apache/jackrabbit/vault/fs/impl/io/NodeStash.java
@@ -21,28 +21,37 @@ import java.util.Set;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
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.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
- * Helper class isolating the task of temporarily moving child nodes to a
+ * Helper class isolating the task of temporarily moving child nodes and properties to a
* different location in order to be able to recover (and properly merge) them
* later on.
+ * This is useful when sysview xml is about to be imported, as that clears everything not explicitly mentioned
*/
-public class ChildNodeStash {
+public class NodeStash {
- static final Logger log = LoggerFactory.getLogger(ChildNodeStash.class);
+ static final Logger log = LoggerFactory.getLogger(NodeStash.class);
private final Session session;
+ private final String path;
+
+ private final ImportMode importMode;
+
private Node tmpNode;
- private final Set<String> excludedNodeName = new HashSet<String>();
+ private final Set<String> excludedNodeName = new HashSet<>();
/**
* List of potential roots where the transient temporary node will be created.
@@ -52,11 +61,13 @@ public class ChildNodeStash {
/**
- * Creates a new child node stash utility class
+ * Creates a new stash utility class which takes care of child nodes and properties in {@code path}
* @param session session to operate on
*/
- public ChildNodeStash(Session session) {
+ public NodeStash(Session session, String path, ImportMode importMode) {
this.session = session;
+ this.path = path;
+ this.importMode = importMode;
}
/**
@@ -85,64 +96,61 @@ public class ChildNodeStash {
* @param name The name of the node to exclude
* @return "this" suitable for chaining.
*/
- public ChildNodeStash excludeName(String name) {
+ public NodeStash excludeName(String name) {
excludedNodeName.add(name);
return this;
}
/**
- * Moves the nodes below the given parent path to a temporary location.
- * @param parentPath the path of the parent node.
- * @throws RepositoryException if an error occurrs
- */
- public void stashChildren(String parentPath) throws RepositoryException {
- stashChildren(session.getNode(parentPath));
- }
-
- /**
- * Moves the nodes below the given parent to a temporary location.
- * @param parent the parent node.
+ * Moves the child nodes and optionally properties of the path to a temporary location.
*/
- public void stashChildren(Node parent) {
+ public void stash() {
+ if (importMode == ImportMode.REPLACE) {
+ return;
+ }
try {
- NodeIterator iter = parent.getNodes();
- while (iter.hasNext()) {
- Node child = iter.nextNode();
+ Node parent = session.getNode(path);
+ Node tmp = getOrCreateTemporaryNode();
+ NodeIterator nodeIterator = parent.getNodes();
+ while (nodeIterator.hasNext()) {
+ Node child = nodeIterator.nextNode();
String name = child.getName();
if (excludedNodeName.contains(name)) {
log.debug("skipping excluded child node from stash: {}", child.getPath());
continue;
}
- Node tmp = getOrCreateTemporaryNode();
try {
session.move(child.getPath(), tmp.getPath() + "/" + name);
} catch (RepositoryException e) {
log.error("Error while moving child node to temporary location. Child will be removed.", e);
}
}
+ // save properties
+ PropertyIterator propIterator = parent.getProperties();
+ while (propIterator.hasNext()) {
+ Property property = propIterator.nextProperty();
+ if (!property.getDefinition().isProtected()) {
+ if (property.isMultiple()) {
+ tmp.setProperty(property.getName(), property.getValues(), property.getType());
+ } else {
+ tmp.setProperty(property.getName(), property.getValue(), property.getType());
+ }
+ }
+ }
} catch (RepositoryException e) {
log.warn("error while moving child nodes (ignored)", e);
}
}
/**
- * Moves the stashed nodes back below the given parent path.
- * @param parentPath the path of the new parent node
- * @throws RepositoryException if an error occurrs
- */
- public void recoverChildren(String parentPath) throws RepositoryException {
- recoverChildren(session.getNode(parentPath), null);
- }
-
- /**
- * Moves the stashed nodes back below the given parent path.
- * @param parent the new parent node
+ * Moves the stashed nodes/properties back below the original path.
* @param importInfo the import info to record the changes
- * @throws RepositoryException if an error occurrs
+ * @throws RepositoryException if an error occurs
*/
- public void recoverChildren(Node parent, ImportInfo importInfo) throws RepositoryException {
+ public void recover(@Nullable ImportInfo importInfo) throws RepositoryException {
// move the old child nodes back
if (tmpNode != null) {
+ Node parent = session.getNode(path);
NodeIterator iter = tmpNode.getNodes();
boolean hasErrors = false;
while (iter.hasNext()) {
@@ -156,16 +164,45 @@ public class ChildNodeStash {
}
} catch (RepositoryException e) {
log.warn("Unable to move child back to new location at {} due to: {}. Node will remain in temporary location: {}",
- new Object[]{newPath, e.getMessage(), child.getPath()});
+ newPath, e.getMessage(), child.getPath());
if (importInfo != null) {
importInfo.onError(newPath, e);
hasErrors = true;
}
}
}
+ 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) {
tmpNode.remove();
}
}
}
+
+ private void recoverProperties(boolean overwriteNewOnes) throws RepositoryException {
+ PropertyIterator propIterator = tmpNode.getProperties();
+ Node destNode = session.getNode(path);
+ while (propIterator.hasNext()) {
+ Property property = propIterator.nextProperty();
+ if (!property.getDefinition().isProtected()) {
+ if (!overwriteNewOnes && destNode.hasProperty(property.getName())) {
+ log.debug("Skipping restore property {} as it has been updated", property.getPath());
+ continue;
+ }
+ if (property.isMultiple()) {
+ destNode.setProperty(property.getName(), property.getValues(), property.getType());
+ } 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/util/DocViewNode.java b/vault-core/src/main/java/org/apache/jackrabbit/vault/util/DocViewNode.java
index fa2adb0..3da5eb9 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
@@ -27,6 +27,7 @@ import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
import org.apache.jackrabbit.util.ISO9075;
+import org.jetbrains.annotations.NotNull;
import org.xml.sax.Attributes;
/**
@@ -37,16 +38,20 @@ public class DocViewNode {
public final String name;
public final String label;
- public final Map<String, DocViewProperty> props = new HashMap<String, DocViewProperty>();
+ public final @NotNull Map<String, DocViewProperty> props = new HashMap<String, DocViewProperty>();
public String uuid;
- public final String[] mixins;
+ public final @NotNull String[] mixins;
public final String primary;
public DocViewNode(String name, String label, String uuid, Map<String, DocViewProperty> props, String[] mixins, String primary) {
this.name = name;
this.label = label;
this.uuid = uuid;
- this.mixins = mixins;
+ if (mixins == null) {
+ this.mixins = new String[0];
+ } else {
+ this.mixins = mixins;
+ }
this.primary = primary;
this.props.putAll(props);
}
@@ -79,7 +84,7 @@ public class DocViewNode {
}
}
this.uuid = uuid;
- this.mixins = mixins;
+ this.mixins = (mixins != null) ? mixins : new String[0];
this.primary = primary;
}
diff --git a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/ImportModeIT.java b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/ImportModeIT.java
index 0a8f59f..f3fe7e0 100644
--- a/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/ImportModeIT.java
+++ b/vault-core/src/test/java/org/apache/jackrabbit/vault/packaging/integration/ImportModeIT.java
@@ -24,6 +24,7 @@ import javax.jcr.RepositoryException;
import org.apache.jackrabbit.vault.packaging.JcrPackage;
import org.apache.jackrabbit.vault.packaging.PackageException;
+import org.junit.Ignore;
import org.junit.Test;
/**
@@ -104,9 +105,14 @@ public class ImportModeIT extends IntegrationTestBase {
setUpNode(parent, "replace");
setUpNode(parent, "merge");
setUpNode(parent, "update");
+ setUpNode(parent, "merge_properties");
+ setUpNode(parent, "update_properties");
admin.save();
- assertProperty("/testroot/merge/propertyold", "old");
- assertProperty("/testroot/update/propertyold", "old");
+
+ assertProperty("/testroot/replace/propertyold", "old");
+ assertProperty("/testroot/replace/propertyupdate", "old");
+ assertNodeExists("/testroot/replace/old");
+ assertProperty("/testroot/replace/existing/propertyold", "old");
extractVaultPackage("/test-packages/import_modes_test_a.zip");
@@ -116,28 +122,47 @@ public class ImportModeIT extends IntegrationTestBase {
assertProperty("/testroot/replace/propertyupdate", "new");
assertProperty("/testroot/replace/propertynew", "new");
assertPropertyMissing("/testroot/replace/propertyold");
- assertProperty("/testroot/replace/update/propertynew", "new");
- assertPropertyMissing("/testroot/replace/update/propertyold");
+ assertProperty("/testroot/replace/existing/propertynew", "new");
+ assertPropertyMissing("/testroot/replace/existing/propertyold");
assertNodeExists("/testroot/replace/new");
assertNodeMissing("/testroot/replace/old");
+ /*
// Update (neither delete existing nodes nor properties)
assertProperty("/testroot/update/propertyupdate", "new");
assertProperty("/testroot/update/propertynew", "new");
assertProperty("/testroot/update/propertyold", "old");
- assertProperty("/testroot/update/update/propertynew", "new");
- assertProperty("/testroot/update/update/propertyold", "old");
+ assertProperty("/testroot/update/existing/propertynew", "new");
+ assertProperty("/testroot/update/existing/propertyold", "old");
assertNodeExists("/testroot/update/new");
assertNodeExists("/testroot/update/old");
// Merge (don't touch existing nodes, except for adding new children)
assertProperty("/testroot/merge/propertyupdate", "old");
- assertPropertyMissing("/testroot/merge/propertynew");
+ assertProperty("/testroot/merge/propertynew", "new");
assertProperty("/testroot/merge/propertyold", "old");
- assertPropertyMissing("/testroot/merge/update/propertynew");
- assertProperty("/testroot/merge/update/propertyold", "old");
+ assertProperty("/testroot/merge/existing/propertynew", "new");
+ assertProperty("/testroot/merge/existing/propertyold", "old");
assertNodeMissing("/testroot/merge/new");
assertNodeExists("/testroot/merge/old");
+ */
+ // Property Update (neither delete existing nodes nor properties, but update them and add new properties/nodes)
+ assertProperty("/testroot/update_properties/propertyupdate", "new");
+ assertProperty("/testroot/update_properties/propertynew", "new");
+ assertProperty("/testroot/update_properties/propertyold", "old");
+ assertProperty("/testroot/update_properties/existing/propertynew", "new");
+ assertProperty("/testroot/update_properties/existing/propertyold", "old");
+ assertNodeExists("/testroot/update_properties/new");
+ assertNodeExists("/testroot/update_properties/old");
+
+ // Property Merge (don't touch existing nodes nor properties, only add new properties/nodes)
+ assertProperty("/testroot/merge_properties/propertyupdate", "old");
+ assertProperty("/testroot/merge_properties/propertynew", "new");
+ assertProperty("/testroot/merge_properties/propertyold", "old");
+ assertProperty("/testroot/merge_properties/existing/propertynew", "new");
+ assertProperty("/testroot/merge_properties/existing/propertyold", "old");
+ assertNodeExists("/testroot/merge_properties/new");
+ assertNodeExists("/testroot/merge_properties/old");
}
private void setUpNode(Node parent, String name) throws RepositoryException {
@@ -145,8 +170,8 @@ public class ImportModeIT extends IntegrationTestBase {
node.setProperty("propertyold", "old");
node.setProperty("propertyupdate", "old");
node.addNode("old");
- Node update = node.addNode("update");
- update.setProperty("propertyold", "old");
+ Node existing = node.addNode("existing");
+ existing.setProperty("propertyold", "old");
}
}
\ No newline at end of file
diff --git a/vault-core/src/test/resources/test-packages/import_modes_test_a.zip/META-INF/vault/filter.xml b/vault-core/src/test/resources/test-packages/import_modes_test_a.zip/META-INF/vault/filter.xml
index bcd93e6..572c30b 100644
--- a/vault-core/src/test/resources/test-packages/import_modes_test_a.zip/META-INF/vault/filter.xml
+++ b/vault-core/src/test/resources/test-packages/import_modes_test_a.zip/META-INF/vault/filter.xml
@@ -3,4 +3,6 @@
<filter root="/testroot/replace" mode="replace"/>
<filter root="/testroot/merge" mode="merge"/>
<filter root="/testroot/update" mode="update"/>
+ <filter root="/testroot/merge_properties" mode="merge_properties"/>
+ <filter root="/testroot/update_properties" mode="update_properties"/>
</workspaceFilter>
diff --git a/vault-core/src/test/resources/test-packages/import_modes_test_a.zip/jcr_root/testroot/.content.xml b/vault-core/src/test/resources/test-packages/import_modes_test_a.zip/jcr_root/testroot/.content.xml
index aea4bc7..da0fa1c 100644
--- a/vault-core/src/test/resources/test-packages/import_modes_test_a.zip/jcr_root/testroot/.content.xml
+++ b/vault-core/src/test/resources/test-packages/import_modes_test_a.zip/jcr_root/testroot/.content.xml
@@ -4,19 +4,31 @@
<replace jcr:primaryType="sling:Folder"
propertyupdate="new"
propertynew="new">
- <update jcr:primaryType="sling:Folder" propertynew="new" />
+ <existing jcr:primaryType="sling:Folder" propertynew="new" />
<new jcr:primaryType="sling:Folder" />
</replace>
<merge jcr:primaryType="sling:Folder"
propertyupdate="new"
propertynew="new">
- <update jcr:primaryType="sling:Folder" propertynew="new" />
+ <existing jcr:primaryType="sling:Folder" propertynew="new" />
<new jcr:primaryType="sling:Folder" />
</merge>
<update jcr:primaryType="sling:Folder"
propertyupdate="new"
propertynew="new">
- <update jcr:primaryType="sling:Folder" propertynew="new"/>
+ <existing jcr:primaryType="sling:Folder" propertynew="new"/>
<new jcr:primaryType="sling:Folder" />
</update>
+ <merge_properties jcr:primaryType="sling:Folder"
+ propertyupdate="new"
+ propertynew="new">
+ <existing jcr:primaryType="sling:Folder" propertynew="new" />
+ <new jcr:primaryType="sling:Folder" />
+ </merge_properties>
+ <update_properties jcr:primaryType="sling:Folder"
+ propertyupdate="new"
+ propertynew="new">
+ <existing jcr:primaryType="sling:Folder" propertynew="new"/>
+ <new jcr:primaryType="sling:Folder" />
+ </update_properties>
</jcr:root>