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>