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 2020/07/02 18:33:30 UTC

[jackrabbit-filevault] branch master updated: JCRVLT-426 add node type validator (#75)

This is an automated email from the ASF dual-hosted git repository.

kwin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/jackrabbit-filevault.git


The following commit(s) were added to refs/heads/master by this push:
     new 127fa8a  JCRVLT-426 add node type validator (#75)
127fa8a is described below

commit 127fa8ab80d3dcaff7c615afedc25a0293559408
Author: Konrad Windszus <kw...@apache.org>
AuthorDate: Thu Jul 2 20:33:21 2020 +0200

    JCRVLT-426 add node type validator (#75)
---
 vault-doc/src/site/markdown/validation.md          |   2 +-
 vault-validation/pom.xml                           |   5 +
 .../impl/util/DocumentViewXmlContentHandler.java   |  12 +
 .../validation/impl/util/ValidatorException.java   |   2 +-
 .../validation/spi/DocumentViewXmlValidator.java   |  18 +
 .../vault/validation/spi/JcrPathValidator.java     |   1 +
 .../vault/validation/spi/NodePathValidator.java    |   2 +-
 .../spi/impl/DocumentViewParserValidator.java      |   8 +-
 .../spi/impl/PrimaryNodeTypeValidator.java         |  62 ---
 .../spi/impl/PrimaryNodeTypeValidatorFactory.java  |  51 ---
 .../impl/nodetype/DocViewPropertyValueFactory.java |  56 +++
 .../spi/impl/nodetype/NodeNameAndType.java         | 100 +++++
 .../spi/impl/nodetype/NodeTypeManagerProvider.java | 204 ++++++++++
 .../spi/impl/nodetype/NodeTypeValidator.java       | 424 ++++++++++++++++++++
 .../impl/nodetype/NodeTypeValidatorFactory.java    | 155 +++++++
 .../vault/validation/spi/package-info.java         |   2 +-
 .../classloaderurl/ClassLoaderUrlConnection.java   |  45 +++
 .../ThreadContextClassLoaderURLStreamHandler.java} |  19 +-
 .../classloaderurl/URLFactory.java}                |  29 +-
 .../{ => util/classloaderurl}/package-info.java    |   8 +-
 .../src/main/resources/default-nodetypes.cnd       | 446 +++++++++++++++++++++
 .../vault/validation/ValidationExecutorTest.java   |  10 +-
 .../spi/impl/DocumentViewParserValidatorTest.java  |  41 ++
 .../spi/impl/PrimaryNodeTypeValidatorTest.java     |  72 ----
 .../spi/impl/nodetype/NodeTypeValidatorTest.java   | 209 ++++++++++
 25 files changed, 1771 insertions(+), 212 deletions(-)

diff --git a/vault-doc/src/site/markdown/validation.md b/vault-doc/src/site/markdown/validation.md
index cd206ab..a009152 100644
--- a/vault-doc/src/site/markdown/validation.md
+++ b/vault-doc/src/site/markdown/validation.md
@@ -55,7 +55,7 @@ ID  |  Description | Options
 `jackrabbit-mergelimitations` | Checks for the limitation of import mode=merge outlined at [JCRVLT-255][jcrvlt-255]. | none
 `jackrabbit-oakindex` |  Checks if the package (potentially) modifies/creates an OakIndexDefinition. This is done by evaluating both the filter.xml for potential matches as well as the actual content for nodes with jcr:primaryType  `oak:indexDefinition`. | none
 `jackrabbit-packagetype` | Checks if the package type is correctly set for this package, i.e. is compliant with all rules outlined at [JCRVLT-170][jcrvlt-170]. | *jcrInstallerNodePathRegex*: the regex of the node paths which all OSGi bundles and configurations within packages must match ([JCR Installer](https://sling.apache.org/documentation/bundles/jcr-installer-provider.html)) (default=`/([^/]*/){0,4}?(install|config)(\\.[^/]*)*/(\\d{1,3}/)?.+?\\.`).<br/>*additionalJcrInstallerFileNode [...]
-`jackrabbit-primarynodetype` | Checks if all non empty elements within [DocView files](docview.html) have the mandatory property `jcr:primaryType` set. | none
+`jackrabbit-nodetypes` | Checks if all non empty elements within [DocView files](docview.html) have the mandatory property `jcr:primaryType` set and follow the [node type definition of their given type](https://jackrabbit.apache.org/jcr/node-types.html). | *cnds*: A URI pointing to one or multiple [CNDs](https://jackrabbit.apache.org/jcr/node-type-notation.html) (separated by `,`) which define the additional namespaces and nodetypes used apart from the [default ones defined in JCR 2.0](h [...]
 
 
 ### Custom Validators
diff --git a/vault-validation/pom.xml b/vault-validation/pom.xml
index fe12cb7..73bfe92 100644
--- a/vault-validation/pom.xml
+++ b/vault-validation/pom.xml
@@ -101,6 +101,11 @@
         </dependency>
 
         <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>jackrabbit-jcr2spi</artifactId>
+        </dependency>
+
+        <dependency>
             <groupId>org.jetbrains</groupId>
             <artifactId>annotations</artifactId>
             <scope>provided</scope>
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/impl/util/DocumentViewXmlContentHandler.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/impl/util/DocumentViewXmlContentHandler.java
index f65172c..beb25a7 100644
--- a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/impl/util/DocumentViewXmlContentHandler.java
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/impl/util/DocumentViewXmlContentHandler.java
@@ -256,6 +256,18 @@ public class DocumentViewXmlContentHandler extends DefaultHandler implements Nam
         if (node == null || nodePath == null) {
             throw new IllegalStateException("Seems that the XML is not well formed");
         }
+        violations.add(new ValidationViolation(ValidationMessageSeverity.DEBUG, "Validate node '" + node + "' end"));
+        for (Map.Entry<String, DocumentViewXmlValidator> entry : validators.entrySet()) {
+            try {
+                Collection<ValidationMessage> messages = entry.getValue().validateEnd(node, new NodeContextImpl(nodePath, filePath, basePath), elementNameStack.size() < 1);
+                if (messages != null && !messages.isEmpty()) {
+                    violations.addAll(ValidationViolation.wrapMessages(entry.getKey(), messages, filePath, null, nodePath.toString(),
+                            locator.getLineNumber(), locator.getColumnNumber()));
+                }
+            } catch (RuntimeException e) {
+                throw new ValidatorException(entry.getKey(), e, filePath, locator.getLineNumber(), locator.getColumnNumber(), e);
+            }
+        }
     }
 
     @Override
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/impl/util/ValidatorException.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/impl/util/ValidatorException.java
index 8ccdc8e..c707176 100644
--- a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/impl/util/ValidatorException.java
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/impl/util/ValidatorException.java
@@ -24,7 +24,7 @@ import java.nio.file.Path;
 public class ValidatorException extends RuntimeException {
 
     private ValidatorException(String id, String messageSuffix, Throwable cause) {
-        super("Exception in validator '" + id + "'" + messageSuffix, cause);
+        super("Exception in validator '" + id + "'" + messageSuffix + ": " + cause.getMessage(), cause);
     }
 
     public ValidatorException(String id, Throwable cause) {
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/DocumentViewXmlValidator.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/DocumentViewXmlValidator.java
index f14a1d9..2527372 100644
--- a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/DocumentViewXmlValidator.java
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/DocumentViewXmlValidator.java
@@ -72,4 +72,22 @@ public interface DocumentViewXmlValidator extends Validator {
     default @Nullable Collection<ValidationMessage> validate(@NotNull DocViewNode node, @NotNull NodeContext nodeContext, boolean isRoot) {
         return validate(node, nodeContext.getNodePath(), nodeContext.getFilePath(), isRoot);
     }
+    
+    
+    /**
+     * Called for the end of each new JCR document view node.
+     * Deserialization of the node information was already done when this method is called as well as all child nodes within the same docview file have been processed.
+     * The node and attribute names have the string representation outlined in {@link Name} (i.e. including the namespace uri in the format <code>{namespaceURI}localPart</code>).
+     * This is also referred to as <a href="https://docs.adobe.com/docs/en/spec/jcr/2.0/3_Repository_Model.html#3.2.5.1%20Expanded%20Form">JCR name expanded form</a>.
+     * To construct such names either use {@link NameUtil} or use the constants from {@link NameConstants}.
+     * 
+     * The node's label refers to the XML element name specifying the node. There shouldn't be any checks derived from it, but only from the expanded name.
+     * @param node the node which should be validated
+     * @param nodeContext the information about the node context (like path)
+     * @param isRoot {@code true} in case this is the root node of the docview file otherwise {@code false}
+     * @return validation messages or {@code null}
+     */
+    default @Nullable Collection<ValidationMessage> validateEnd(@NotNull DocViewNode node, @NotNull NodeContext nodeContext, boolean isRoot) {
+        return null;
+    }
 }
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/JcrPathValidator.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/JcrPathValidator.java
index 06e730f..6a2cd9f 100644
--- a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/JcrPathValidator.java
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/JcrPathValidator.java
@@ -28,6 +28,7 @@ import org.osgi.annotation.versioning.ProviderType;
  * Validator interface for validating file paths for files and folders
  * below jcr_root.
  * Called after {@link GenericJcrDataValidator}.
+ * In contrast to {@link NodePathValidator} only called once per file and folder (even if those are covering multiple node paths).
  */
 @ProviderType
 public interface JcrPathValidator extends Validator {
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/NodePathValidator.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/NodePathValidator.java
index 6beb987..94a7816 100644
--- a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/NodePathValidator.java
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/NodePathValidator.java
@@ -26,7 +26,7 @@ import org.osgi.annotation.versioning.ProviderType;
 /**
  * Validator interface for validating node paths.
  * For validators interested in the actual properties use either {@link DocumentViewXmlValidator} or {@link GenericJcrDataValidator}.
- * 
+ * In contrast to {@link JcrPathValidator} might be called multiple times per file in case it covers multiple nodes.
  */
 @ProviderType
 public interface NodePathValidator extends Validator {
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/DocumentViewParserValidator.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/DocumentViewParserValidator.java
index 1c00219..479db5e 100644
--- a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/DocumentViewParserValidator.java
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/DocumentViewParserValidator.java
@@ -48,6 +48,7 @@ import org.xml.sax.XMLReader;
 
 public class DocumentViewParserValidator implements GenericJcrDataValidator {
 
+    public static final String EXTENDED_FILE_AGGREGATE_FOLDER_SUFFIX = ".dir";
     private final Map<String, DocumentViewXmlValidator> docViewValidators;
     private final SAXParser saxParser;
     private final @NotNull ValidationMessageSeverity severity;
@@ -99,12 +100,11 @@ public class DocumentViewParserValidator implements GenericJcrDataValidator {
        return messages;
     }
 
-
     /** @param input the given input stream must be reset later on
      * @param path
      * @return either the path of the root node of the given docview xml or {@code null} if no docview xml given
      * @throws IOException */
-    private static Path getDocumentViewXmlRootPath(BufferedInputStream input, Path path) throws IOException {
+    static Path getDocumentViewXmlRootPath(BufferedInputStream input, Path path) throws IOException {
         Path name = path.getFileName();
         Path rootPath = null;
 
@@ -112,6 +112,10 @@ public class DocumentViewParserValidator implements GenericJcrDataValidator {
         if (name.equals(Paths.get(Constants.DOT_CONTENT_XML))) {
             if (nameCount > 1) {
                 rootPath = path.subpath(0, nameCount - 1);
+                // fix root mapping for http://jackrabbit.apache.org/filevault/vaultfs.html#Extended_File_aggregates
+                if (rootPath.toString().endsWith(EXTENDED_FILE_AGGREGATE_FOLDER_SUFFIX)) {
+                    rootPath = Paths.get(rootPath.toString().substring(0, rootPath.toString().length() - EXTENDED_FILE_AGGREGATE_FOLDER_SUFFIX.length()));
+                }
             } else {
                 rootPath = Paths.get("");
             }
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/PrimaryNodeTypeValidator.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/PrimaryNodeTypeValidator.java
deleted file mode 100644
index ac79971..0000000
--- a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/PrimaryNodeTypeValidator.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.jackrabbit.vault.validation.spi.impl;
-
-import java.nio.file.Path;
-import java.util.Collection;
-import java.util.Collections;
-
-import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
-import org.apache.jackrabbit.vault.util.DocViewNode;
-import org.apache.jackrabbit.vault.validation.spi.DocumentViewXmlValidator;
-import org.apache.jackrabbit.vault.validation.spi.ValidationMessage;
-import org.apache.jackrabbit.vault.validation.spi.ValidationMessageSeverity;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Makes sure that each node in a docview file containing at least one other property defines the primary type
- */
-public class PrimaryNodeTypeValidator implements DocumentViewXmlValidator {
-
-    protected static final String MESSAGE_MISSING_PRIMARY_TYPE = "Mandatory jcr:primaryType missing on node '%s'";
-    private final @NotNull ValidationMessageSeverity severity;
-    private final @NotNull WorkspaceFilter filter;
-
-    public PrimaryNodeTypeValidator(@NotNull ValidationMessageSeverity severity, @NotNull WorkspaceFilter filter) {
-        super();
-        this.severity = severity;
-        this.filter = filter;
-    }
-
-    @Override
-    public Collection<ValidationMessage> done() {
-        return null;
-    }
-
-    @Override
-    public Collection<ValidationMessage> validate(@NotNull DocViewNode node, @NotNull String nodePath, @NotNull Path filePath, boolean isRoot) {
-        if (node.primary == null) {
-            // only an issue if contained in the filter
-            // if other properties are set this node is not only used for ordering purposes
-            if (filter.contains(nodePath) && !node.props.isEmpty()) {
-                return Collections.singleton(new ValidationMessage(severity,  String.format(MESSAGE_MISSING_PRIMARY_TYPE, nodePath)));
-            }
-        }
-        return null;
-    }
-
-}
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/PrimaryNodeTypeValidatorFactory.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/PrimaryNodeTypeValidatorFactory.java
deleted file mode 100644
index 3e39770..0000000
--- a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/PrimaryNodeTypeValidatorFactory.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.jackrabbit.vault.validation.spi.impl;
-
-import org.apache.jackrabbit.vault.validation.spi.ValidationContext;
-import org.apache.jackrabbit.vault.validation.spi.Validator;
-import org.apache.jackrabbit.vault.validation.spi.ValidatorFactory;
-import org.apache.jackrabbit.vault.validation.spi.ValidatorSettings;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.kohsuke.MetaInfServices;
-
-@MetaInfServices
-public final class PrimaryNodeTypeValidatorFactory implements ValidatorFactory {
-
-    @Override
-    public @Nullable Validator createValidator(@NotNull ValidationContext context, @NotNull ValidatorSettings settings) {
-        return new PrimaryNodeTypeValidator(settings.getDefaultSeverity(), context.getFilter());
-    }
-
-    @Override
-    public boolean shouldValidateSubpackages() {
-        return false;
-    }
-
-    @Override
-    public @NotNull String getId() {
-        return ValidatorFactory.ID_PREFIX_JACKRABBIT + "primarynodetype";
-    }
-
-    @Override
-    public int getServiceRanking() {
-        return 0;
-    }
-
-    
-}
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/DocViewPropertyValueFactory.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/DocViewPropertyValueFactory.java
new file mode 100644
index 0000000..a9704ba
--- /dev/null
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/DocViewPropertyValueFactory.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.vault.validation.spi.impl.nodetype;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+import javax.jcr.PropertyType;
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+import javax.jcr.ValueFormatException;
+
+import org.apache.jackrabbit.value.ValueFactoryImpl;
+import org.apache.jackrabbit.vault.util.DocViewProperty;
+
+public class DocViewPropertyValueFactory {
+
+    private final ValueFactory valueFactory;
+    public DocViewPropertyValueFactory() {
+        valueFactory = ValueFactoryImpl.getInstance();
+    }
+    
+    private Value getValue(String value, int type) throws ValueFormatException {
+        if (type == PropertyType.UNDEFINED) {
+            type = PropertyType.STRING;
+        }
+        return valueFactory.createValue(value, type);
+    }
+
+    public Value getValue(DocViewProperty property) throws ValueFormatException {
+        return getValue(property.values[0], property.type);
+    }
+
+    public Value[] getValues(DocViewProperty property) throws ValueFormatException {
+        Collection<Value> values = new LinkedList<>();
+        for (String value : property.values) {
+            values.add(getValue(value, property.type));
+        }
+        return values.toArray(new Value[values.size()]);
+    }
+
+}
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeNameAndType.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeNameAndType.java
new file mode 100644
index 0000000..e71a523
--- /dev/null
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeNameAndType.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.vault.validation.spi.impl.nodetype;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.jcr.NamespaceException;
+import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.nodetype.NoSuchNodeTypeException;
+
+import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeType;
+import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeTypeProvider;
+import org.apache.jackrabbit.spi.Name;
+import org.apache.jackrabbit.spi.QNodeDefinition;
+import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException;
+import org.apache.jackrabbit.spi.commons.conversion.NameResolver;
+import org.apache.jackrabbit.spi.commons.name.NameConstants;
+import org.apache.jackrabbit.vault.util.DocViewNode;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/** 
+ * Encapsulates the expanded name and its underlying effective node type.
+ * In addition stores references to the child node names and types.
+ */
+final class NodeNameAndType {
+    private final @NotNull Name name;
+    private final @NotNull EffectiveNodeType effectiveNodeType;
+    private final @NotNull List<NodeNameAndType> children;
+    private final @Nullable NodeNameAndType parent;
+
+    @SuppressWarnings("null")
+    public NodeNameAndType(@Nullable NodeNameAndType parent, @NotNull NameResolver nameResolver, @NotNull EffectiveNodeTypeProvider effectiveNodeTypeProvider, @NotNull DocViewNode node) throws IllegalNameException, NamespaceException, ConstraintViolationException, NoSuchNodeTypeException {
+        this.name = nameResolver.getQName(node.name);
+        Collection<Name> types = new LinkedList<>();
+        types.add(nameResolver.getQName(node.primary));
+        if (node.mixins != null) {
+            for (String mixin : node.mixins) {
+                types.add(nameResolver.getQName(mixin));
+            }
+        }
+        effectiveNodeType = effectiveNodeTypeProvider.getEffectiveNodeType(types.toArray(new Name[0]));
+        children = new LinkedList<>();
+        if (parent != null) {
+            parent.addChild(this);
+        }
+        this.parent = parent;
+    }
+
+    public boolean fulfillsNodeDefinition(QNodeDefinition nodeDefinition) {
+        // name must match
+        if (!nodeDefinition.getName().equals(NameConstants.ANY_NAME) && !nodeDefinition.getName().equals(name)) {
+            return false;
+        }
+
+        for (Name requiredType : nodeDefinition.getRequiredPrimaryTypes()) {
+            // type must match one of the given types
+            if (!effectiveNodeType.includesNodeType(requiredType)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    void addChild(NodeNameAndType nodeNameAndTypes) {
+        children.add(nodeNameAndTypes);
+    }
+
+    public List<NodeNameAndType> getChildren() {
+        return children;
+    }
+
+    public Name getName() {
+        return name;
+    }
+
+    public EffectiveNodeType getEffectiveNodeType() {
+        return effectiveNodeType;
+    }
+
+    public NodeNameAndType getParent() {
+        return parent;
+    }
+}
\ No newline at end of file
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeTypeManagerProvider.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeTypeManagerProvider.java
new file mode 100644
index 0000000..1cc04af
--- /dev/null
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeTypeManagerProvider.java
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.vault.validation.spi.impl.nodetype;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.NamespaceException;
+import javax.jcr.NamespaceRegistry;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.ValueFactory;
+import javax.jcr.nodetype.InvalidNodeTypeDefinitionException;
+import javax.jcr.nodetype.NodeTypeExistsException;
+import javax.jcr.nodetype.NodeTypeManager;
+
+import org.apache.jackrabbit.commons.cnd.CndImporter;
+import org.apache.jackrabbit.commons.cnd.ParseException;
+import org.apache.jackrabbit.jcr2spi.ManagerProvider;
+import org.apache.jackrabbit.jcr2spi.NamespaceRegistryImpl;
+import org.apache.jackrabbit.jcr2spi.NamespaceStorage;
+import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager;
+import org.apache.jackrabbit.jcr2spi.lock.LockStateManager;
+import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeTypeProvider;
+import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider;
+import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProviderImpl;
+import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeDefinitionProvider;
+import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeManagerImpl;
+import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeRegistryImpl;
+import org.apache.jackrabbit.jcr2spi.security.AccessManager;
+import org.apache.jackrabbit.jcr2spi.security.authorization.AccessControlProvider;
+import org.apache.jackrabbit.jcr2spi.version.VersionManager;
+import org.apache.jackrabbit.spi.QValueFactory;
+import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver;
+import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
+import org.apache.jackrabbit.spi.commons.conversion.NameResolver;
+import org.apache.jackrabbit.spi.commons.conversion.PathResolver;
+import org.apache.jackrabbit.spi.commons.namespace.NamespaceMapping;
+import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
+import org.apache.jackrabbit.spi.commons.namespace.RegistryNamespaceResolver;
+import org.apache.jackrabbit.spi.commons.nodetype.NodeTypeStorage;
+import org.apache.jackrabbit.spi.commons.nodetype.NodeTypeStorageImpl;
+import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl;
+import org.apache.jackrabbit.value.ValueFactoryImpl;
+
+public class NodeTypeManagerProvider implements ManagerProvider, NamespaceStorage {
+
+    
+    // namespace related helpers
+    private final NamespaceMapping namespaceMapping;
+    private final NamespaceRegistry namespaceRegistry;
+    private final NamespaceResolver namespaceResolver;
+    private final NamePathResolver npResolver;
+    
+    // nodetype related helpers
+    private final NodeTypeStorage nodeTypeStorage;
+    private final NodeTypeRegistryImpl nodeTypeRegistry;
+    private final NodeTypeManagerImpl nodeTypeManager;
+    
+    private final ItemDefinitionProvider itemDefinitionProvider;
+
+    public NodeTypeManagerProvider() throws IOException, RepositoryException, ParseException {
+        namespaceMapping = new NamespaceMapping();
+        // add default mapping, the rest comes from the CDN provided via the reader
+        namespaceMapping.setMapping(NamespaceRegistry.PREFIX_EMPTY, NamespaceRegistry.NAMESPACE_EMPTY);
+        namespaceRegistry = new NamespaceRegistryImpl(this);
+        namespaceResolver = new RegistryNamespaceResolver(namespaceRegistry);
+        npResolver = new DefaultNamePathResolver(namespaceResolver);
+        nodeTypeStorage = new NodeTypeStorageImpl();
+        nodeTypeRegistry = NodeTypeRegistryImpl.create(nodeTypeStorage, namespaceRegistry);
+        nodeTypeManager = new NodeTypeManagerImpl(nodeTypeRegistry, this);
+        itemDefinitionProvider = new ItemDefinitionProviderImpl(nodeTypeRegistry, null, null);
+        // always provide default
+        try (Reader reader = new InputStreamReader(
+                this.getClass().getResourceAsStream("/default-nodetypes.cnd"),
+                StandardCharsets.US_ASCII)) {
+            registerNodeTypes(reader);
+        }
+    }
+
+    public void registerNodeTypes(Reader reader) throws InvalidNodeTypeDefinitionException, NodeTypeExistsException, UnsupportedRepositoryOperationException, ParseException, RepositoryException, IOException {
+        CndImporter.registerNodeTypes(reader, null, nodeTypeManager, namespaceRegistry, getJcrValueFactory(), false);
+    }
+
+    @Override
+    public NamePathResolver getNamePathResolver() {
+        return npResolver;
+    }
+
+    @Override
+    public NameResolver getNameResolver() {
+        return npResolver;
+    }
+
+    @Override
+    public PathResolver getPathResolver() {
+        return npResolver;
+    }
+
+    @Override
+    public NamespaceResolver getNamespaceResolver() {
+        return namespaceResolver;
+    }
+
+    public NodeTypeManager getNodeTypeManager() {
+        return nodeTypeManager;
+    }
+
+    @Override
+    public HierarchyManager getHierarchyManager() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public AccessManager getAccessManager() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public LockStateManager getLockStateManager() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public VersionManager getVersionStateManager() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ItemDefinitionProvider getItemDefinitionProvider() {
+        return itemDefinitionProvider;
+    }
+
+    @Override
+    public NodeTypeDefinitionProvider getNodeTypeDefinitionProvider() {
+        return nodeTypeManager;
+    }
+
+    @Override
+    public EffectiveNodeTypeProvider getEffectiveNodeTypeProvider() {
+        return nodeTypeRegistry;
+    }
+
+    @Override
+    public ValueFactory getJcrValueFactory() throws RepositoryException {
+        return ValueFactoryImpl.getInstance();
+    }
+
+    @Override
+    public QValueFactory getQValueFactory() throws RepositoryException {
+        return QValueFactoryImpl.getInstance();
+    }
+
+    @Override
+    public AccessControlProvider getAccessControlProvider() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Map<String, String> getRegisteredNamespaces() throws RepositoryException {
+        return namespaceMapping.getPrefixToURIMapping();
+    }
+
+    @Override
+    public String getPrefix(String uri) throws NamespaceException, RepositoryException {
+        return namespaceMapping.getPrefix(uri);
+    }
+
+    @Override
+    public String getURI(String prefix) throws NamespaceException, RepositoryException {
+        return namespaceMapping.getURI(prefix);
+    }
+
+    @Override
+    public void registerNamespace(String prefix, String uri)
+            throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException {
+        namespaceMapping.setMapping(prefix, uri);
+    }
+
+    @Override
+    public void unregisterNamespace(String uri)
+            throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException {
+        namespaceMapping.removeMapping(uri);
+    }
+
+}
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeTypeValidator.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeTypeValidator.java
new file mode 100644
index 0000000..287f39a
--- /dev/null
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeTypeValidator.java
@@ -0,0 +1,424 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.vault.validation.spi.impl.nodetype;
+
+import java.util.AbstractMap.SimpleEntry;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.jcr.NamespaceException;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+import javax.jcr.ValueFormatException;
+import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.nodetype.NoSuchNodeTypeException;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeType;
+import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider;
+import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeDefinitionProvider;
+import org.apache.jackrabbit.spi.Name;
+import org.apache.jackrabbit.spi.QNodeDefinition;
+import org.apache.jackrabbit.spi.QNodeTypeDefinition;
+import org.apache.jackrabbit.spi.QPropertyDefinition;
+import org.apache.jackrabbit.spi.QValue;
+import org.apache.jackrabbit.spi.QValueFactory;
+import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException;
+import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
+import org.apache.jackrabbit.spi.commons.conversion.NameResolver;
+import org.apache.jackrabbit.spi.commons.name.NameConstants;
+import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
+import org.apache.jackrabbit.spi.commons.nodetype.constraint.ValueConstraint;
+import org.apache.jackrabbit.spi.commons.value.ValueFormat;
+import org.apache.jackrabbit.value.ValueHelper;
+import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
+import org.apache.jackrabbit.vault.fs.spi.ACLManagement;
+import org.apache.jackrabbit.vault.fs.spi.UserManagement;
+import org.apache.jackrabbit.vault.fs.spi.impl.jcr20.JackrabbitUserManagement;
+import org.apache.jackrabbit.vault.fs.spi.impl.jcr20.JcrACLManagement;
+import org.apache.jackrabbit.vault.util.DocViewNode;
+import org.apache.jackrabbit.vault.util.DocViewProperty;
+import org.apache.jackrabbit.vault.util.Text;
+import org.apache.jackrabbit.vault.validation.spi.DocumentViewXmlValidator;
+import org.apache.jackrabbit.vault.validation.spi.NodeContext;
+import org.apache.jackrabbit.vault.validation.spi.ValidationMessage;
+import org.apache.jackrabbit.vault.validation.spi.ValidationMessageSeverity;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class NodeTypeValidator implements DocumentViewXmlValidator {
+
+    static final String MESSAGE_MANDATORY_CHILD_NODE_MISSING = "Mandatory child node missing: %s";
+    static final String MESSAGE_PROPERTY_ERROR = "Error while retrieving property '%s': %s";
+    static final String MESSAGE_UNKNOWN_NODE_TYPE_OR_NAMESPACE = "Unknown node type or namespace: %s";
+    static final String MESSAGE_MISSING_PRIMARY_TYPE = "Mandatory jcr:primaryType missing on node '%s'";
+    static final String MESSAGE_PROPERTY_NOT_ALLOWED = "Property '%s' is not allowed in node with types '[%s]': %s";
+    static final String MESSAGE_MANDATORY_PROPERTY_MISSING = "Mandatory property '%s' missing in node with types [%s]";
+    static final String MESSAGE_CHILD_NODE_OF_NOT_CONTAINED_PARENT_POTENTIALLY_NOT_ALLOWED = "Node '%s' is not allowed as child of not contained node with potential default types '[%s]': %s";
+    static final String MESSAGE_CHILD_NODE_NOT_ALLOWED = "Node '%s' is not allowed as child of node with types '[%s]': %s";
+    private final WorkspaceFilter filter;
+    private final ValidationMessageSeverity defaultSeverity;
+    private final ValidationMessageSeverity severityForUnknownNodeTypes;
+    private final DocViewPropertyValueFactory docViewPropertyValueFactory;
+    private final NodeTypeManagerProvider ntManagerProvider;
+    private final Set<String> loggedUnknownNodeTypeMessages;
+
+    private final EffectiveNodeType defaultType;
+    private final UserManagement userManagement;
+    private final ACLManagement aclManagement;
+    private NodeContext protectedNodeContext;
+    private NodeNameAndType currentNodeNameAndType = null;
+
+    private static final Collection<Name> ALLOWED_PROTECTED_PROPERTIES = Arrays.asList(NameConstants.JCR_PRIMARYTYPE,
+            NameConstants.JCR_MIXINTYPES);
+    
+    // properties being set by the {@link FileArtifactHandler} (they are part of another file) are ignored
+    private static final Map<Name, List<Name>> IGNORED_MANDATORY_PROPERTIES_PER_NODE_TYPE = Stream.of(
+            new SimpleEntry<>(NameConstants.NT_RESOURCE, 
+                    Arrays.asList(NameConstants.JCR_DATA)))
+            .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue));
+
+    public NodeTypeValidator(@NotNull WorkspaceFilter filter, @NotNull NodeTypeManagerProvider ntManagerProvider,
+            @NotNull EffectiveNodeType defaultEffectiveNodeType, @NotNull ValidationMessageSeverity defaultSeverity,
+            @NotNull ValidationMessageSeverity severityForUnknownNodeTypes) {
+        this.filter = filter;
+        this.ntManagerProvider = ntManagerProvider;
+        this.defaultType = defaultEffectiveNodeType;
+        this.defaultSeverity = defaultSeverity;
+        this.severityForUnknownNodeTypes = severityForUnknownNodeTypes;
+        this.docViewPropertyValueFactory = new DocViewPropertyValueFactory();
+        this.userManagement = new JackrabbitUserManagement();
+        this.aclManagement = new JcrACLManagement();
+        this.loggedUnknownNodeTypeMessages = new HashSet<>();
+    }
+
+    static String getDocViewNodeLabel(DocViewNode node) {
+        StringBuilder sb = new StringBuilder(node.name);
+        sb.append(" [").append(node.primary);
+        if (node.mixins != null && node.mixins.length > 0) {
+            sb.append(" (").append(StringUtils.join(node.mixins, ", ")).append(")");
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+
+    @Override
+    public @Nullable Collection<ValidationMessage> validate(@NotNull DocViewNode node, @NotNull NodeContext nodeContext,
+            boolean isRoot) {
+
+        if (node.primary == null) {
+            // only an issue if contained in the filter
+            // if other properties are set this node is not only used for ordering purposes
+            if (filter.contains(nodeContext.getNodePath()) && !node.props.isEmpty()) {
+                return Collections.singleton(
+                        new ValidationMessage(defaultSeverity, String.format(MESSAGE_MISSING_PRIMARY_TYPE, nodeContext.getNodePath())));
+            } else {
+                // order node only or outside filter
+                return null;
+            }
+        }
+
+        // special handling for users and acls
+        if (aclManagement.isACLNodeType(node.primary) || userManagement.isAuthorizableNodeType(node.primary)) {
+            protectedNodeContext = nodeContext;
+        }
+
+        boolean allowProtectedSubNodesAndProperties = protectedNodeContext != null;
+
+        Collection<ValidationMessage> messages = new LinkedList<>();
+
+        try {
+            // check node itself against parent node type
+            if (!aclManagement.isACLNodeType(node.primary)) {
+                final EffectiveNodeType parentNodeType;
+                final boolean useDefaultNodeType;
+                String parentNodePath = Text.getRelativeParent(nodeContext.getNodePath(), 1);
+
+                if (currentNodeNameAndType == null || !filter.contains(parentNodePath)) {
+                    parentNodeType = defaultType;
+                    useDefaultNodeType = true;
+                } else {
+                    parentNodeType = currentNodeNameAndType.getEffectiveNodeType();
+                    useDefaultNodeType = false;
+                }
+
+                String constraintViolation = getChildNodeConstraintViolation(node, parentNodeType,
+                        ntManagerProvider.getNodeTypeDefinitionProvider(),
+                        ntManagerProvider.getNameResolver(), ntManagerProvider.getItemDefinitionProvider(),
+                        allowProtectedSubNodesAndProperties);
+                if (constraintViolation != null) {
+                    messages.add(new ValidationMessage(defaultSeverity,
+                            String.format(
+                                    useDefaultNodeType ? MESSAGE_CHILD_NODE_OF_NOT_CONTAINED_PARENT_POTENTIALLY_NOT_ALLOWED
+                                            : MESSAGE_CHILD_NODE_NOT_ALLOWED,
+                                    getDocViewNodeLabel(node),
+                                    effectiveNodeTypeToString(ntManagerProvider.getNameResolver(), parentNodeType),
+                                    constraintViolation)));
+
+                }
+            }
+
+            // get current node's node type and name and register in tree
+            NodeNameAndType newNodeNameAndType = new NodeNameAndType(currentNodeNameAndType, ntManagerProvider.getNameResolver(),
+                    ntManagerProvider.getEffectiveNodeTypeProvider(), node);
+
+            // check all properties
+            Collection<Name> foundProperties = new ArrayList<>(node.props.size());
+            for (DocViewProperty property : node.props.values()) {
+                String constraintViolation = getPropertyConstraintViolation(property, newNodeNameAndType.getEffectiveNodeType(),
+                        allowProtectedSubNodesAndProperties);
+                if (constraintViolation != null) {
+                    messages.add(new ValidationMessage(defaultSeverity, String.format(MESSAGE_PROPERTY_NOT_ALLOWED, property,
+                            effectiveNodeTypeToString(ntManagerProvider.getNameResolver(), newNodeNameAndType.getEffectiveNodeType()),
+                            constraintViolation)));
+                }
+                foundProperties.add(NameFactoryImpl.getInstance().create(property.name));
+            }
+            // are all mandatory properties covered?
+            for (QPropertyDefinition mandatoryPropertyDefinition : newNodeNameAndType.getEffectiveNodeType()
+                    .getMandatoryQPropertyDefinitions()) {
+                // ignore auto-created properties as they are created on-demand
+                if (!mandatoryPropertyDefinition.isAutoCreated() && !foundProperties.contains(mandatoryPropertyDefinition.getName())) {
+                    
+                    // ignore propertes which may be provided by the {@link FileArtifactHandler} (they are part of another file)
+                    List<Name> ignoredProperties = IGNORED_MANDATORY_PROPERTIES_PER_NODE_TYPE.get(mandatoryPropertyDefinition.getDeclaringNodeType());
+                    if (ignoredProperties != null && ignoredProperties.contains(mandatoryPropertyDefinition.getName())) {
+                        // TODO: skipping for now as validating those from other files requires major effort
+                        continue;
+                    }
+                    messages.add(new ValidationMessage(defaultSeverity,
+                            String.format(MESSAGE_MANDATORY_PROPERTY_MISSING, mandatoryPropertyDefinition.getName(),
+                                    effectiveNodeTypeToString(ntManagerProvider.getNameResolver(),
+                                            newNodeNameAndType.getEffectiveNodeType()))));
+                }
+            }
+
+            currentNodeNameAndType = newNodeNameAndType;
+        } catch (NoSuchNodeTypeException | IllegalNameException | NamespaceException e) {
+            // log each unknown node type/namespace only once!
+            if (!loggedUnknownNodeTypeMessages.contains(e.getMessage())) {
+                messages.add(new ValidationMessage(severityForUnknownNodeTypes,
+                        String.format(MESSAGE_UNKNOWN_NODE_TYPE_OR_NAMESPACE, e.getMessage()), e));
+                loggedUnknownNodeTypeMessages.add(e.getMessage());
+            }
+        } catch (RepositoryException e) {
+            throw new IllegalStateException("Could not validate nodes/properties against node types: " + e.getMessage(), e);
+        }
+        return messages;
+    }
+
+    @Override
+    public @Nullable Collection<ValidationMessage> validateEnd(@NotNull DocViewNode node, @NotNull NodeContext nodeContext,
+            boolean isRoot) {
+
+        if (nodeContext.equals(protectedNodeContext)) {
+            protectedNodeContext = null;
+        }
+
+        try {
+            if (currentNodeNameAndType != null) {
+                Collection<ValidationMessage> messages = new LinkedList<>();
+                for (QNodeDefinition mandatoryNodeType : currentNodeNameAndType.getEffectiveNodeType().getMandatoryQNodeDefinitions()) {
+                    boolean foundRequiredChildNode = currentNodeNameAndType.getChildren().stream()
+                            .anyMatch(childNamesAndTypes -> childNamesAndTypes.fulfillsNodeDefinition(mandatoryNodeType));
+                    if (!foundRequiredChildNode) {
+                        try {
+                            messages.add(new ValidationMessage(defaultSeverity, String.format(MESSAGE_MANDATORY_CHILD_NODE_MISSING,
+                                    nodeDefinitionToString(ntManagerProvider.getNameResolver(), mandatoryNodeType))));
+                        } catch (NamespaceException e) {
+                            throw new IllegalStateException("Could not give out node types and name for " + mandatoryNodeType, e);
+                        }
+                    }
+                }
+                return messages;
+            } else {
+                return null;
+            }
+        } finally {
+            if (currentNodeNameAndType != null) {
+                currentNodeNameAndType = currentNodeNameAndType.getParent();
+            }
+        }
+    }
+
+    static String effectiveNodeTypeToString(NameResolver nameResolver, EffectiveNodeType nodeType) throws NamespaceException {
+        return joinAsQualifiedJcrName(nameResolver, nodeType.getMergedNodeTypes());
+    }
+
+    static String nodeDefinitionToString(NameResolver nameResolver, QNodeDefinition nodeDefinition) throws NamespaceException {
+        return nameResolver.getJCRName(nodeDefinition.getName()) + " ["
+                + joinAsQualifiedJcrName(nameResolver, nodeDefinition.getRequiredPrimaryTypes()) + "]";
+    }
+
+    private static String joinAsQualifiedJcrName(NameResolver nameResolver, Name[] names) throws NamespaceException {
+        StringBuilder types = new StringBuilder();
+        String delimiter = "";
+        for (Name name : names) {
+            types.append(delimiter).append(nameResolver.getJCRName(name));
+            delimiter = ", ";
+        }
+        return types.toString();
+    }
+
+    private static QPropertyDefinition getPropertyDefinition(Name name, int type, EffectiveNodeType effectiveNodeType,
+            ItemDefinitionProvider itemDefinitionProvider, boolean multiValued)
+            throws NoSuchNodeTypeException, ConstraintViolationException {
+        QPropertyDefinition def;
+        try {
+            def = itemDefinitionProvider.getQPropertyDefinition(effectiveNodeType.getAllNodeTypes(), name, type,
+                    multiValued);
+        } catch (ConstraintViolationException e) {
+            if (type != PropertyType.UNDEFINED) {
+                def = itemDefinitionProvider.getQPropertyDefinition(effectiveNodeType.getAllNodeTypes(), name, PropertyType.UNDEFINED,
+                        multiValued);
+            } else {
+                throw e;
+            }
+        }
+        return def;
+    }
+
+    private static void validateValueConstraints(Value value, QPropertyDefinition def, ValueFactory valueFactory,
+            QValueFactory qValueFactory, NamePathResolver namePathResolver) throws ValueFormatException, RepositoryException {
+        final Value v;
+        if (def.getRequiredType() != 0 && def.getRequiredType() != value.getType()) {
+            v = ValueHelper.convert(value, def.getRequiredType(), valueFactory);
+        } else {
+            v = value;
+        }
+        QValue qValue = ValueFormat.getQValue(v, namePathResolver, qValueFactory);
+        ValueConstraint.checkValueConstraints(def, new QValue[] { qValue });
+    }
+
+    String getPropertyConstraintViolation(DocViewProperty property, EffectiveNodeType effectiveNodeType, boolean allowProtected)
+            throws RepositoryException {
+        Name name = ntManagerProvider.getNameResolver().getQName(property.name);
+
+        try {
+            if (property.isMulti) {
+                return getPropertyConstraintViolation(name, docViewPropertyValueFactory.getValues(property), effectiveNodeType,
+                        ntManagerProvider.getItemDefinitionProvider(), ntManagerProvider.getJcrValueFactory(),
+                        ntManagerProvider.getQValueFactory(), ntManagerProvider.getNamePathResolver(), allowProtected);
+            } else {
+                return getPropertyConstraintViolation(name, docViewPropertyValueFactory.getValue(property), effectiveNodeType,
+                        ntManagerProvider.getItemDefinitionProvider(), ntManagerProvider.getJcrValueFactory(),
+                        ntManagerProvider.getQValueFactory(), ntManagerProvider.getNamePathResolver(), allowProtected);
+            }
+        } catch (RepositoryException e) {
+            throw new RepositoryException(String.format(MESSAGE_PROPERTY_ERROR, property.name, e.getMessage()), e);
+        }
+    }
+
+    static String getPropertyConstraintViolation(Name name, Value value, EffectiveNodeType effectiveNodeType,
+            ItemDefinitionProvider itemDefinitionProvider, ValueFactory valueFactory, QValueFactory qValueFactory,
+            NamePathResolver namePathResolver, boolean allowProtected) throws RepositoryException {
+        QPropertyDefinition def;
+        try {
+            def = getPropertyDefinition(name, value.getType(), effectiveNodeType, itemDefinitionProvider, false);
+        } catch (ConstraintViolationException t) {
+            return "No property definition found for name!";
+        }
+
+        if (def.isProtected() && !allowProtected && !ALLOWED_PROTECTED_PROPERTIES.contains(name)) {
+            return "Property is protected!";
+        }
+
+        // single values are valid for multi and single value
+        try {
+            validateValueConstraints(value, def, valueFactory, qValueFactory, namePathResolver);
+        } catch (ConstraintViolationException e) {
+            return "Property value does not satisfy constraints: " + e.getLocalizedMessage();
+        } catch (ValueFormatException e) {
+            return "Cannot convert property into type '" + def.getRequiredType() + "': " + e.getLocalizedMessage();
+        }
+        return null;
+    }
+
+    static String getPropertyConstraintViolation(Name name, Value[] values, EffectiveNodeType effectiveNodeType,
+            ItemDefinitionProvider itemDefinitionProvider, ValueFactory valueFactory, QValueFactory qValueFactory,
+            NamePathResolver namePathResolver, boolean allowProtected) throws RepositoryException {
+        QPropertyDefinition def;
+        int type = values.length > 0 ? values[0].getType() : PropertyType.UNDEFINED;
+        try {
+            def = getPropertyDefinition(name, type, effectiveNodeType, itemDefinitionProvider, true);
+        } catch (ConstraintViolationException t) {
+            return "No property definition found for name!";
+        }
+        if (def.isProtected() && !allowProtected && !ALLOWED_PROTECTED_PROPERTIES.contains(name)) {
+            return "Property is protected!";
+        }
+        if (!def.isMultiple()) {
+            return "Property must be single-value!";
+        }
+        for (Value value : values) {
+            try {
+                validateValueConstraints(value, def, valueFactory, qValueFactory, namePathResolver);
+            } catch (ConstraintViolationException e) {
+                return "Property value does not satisfy constraints: " + e.getLocalizedMessage();
+            } catch (ValueFormatException e) {
+                return "Cannot convert property into type '" + def.getRequiredType() + "': " + e.getLocalizedMessage();
+            }
+        }
+        return null;
+    }
+
+    static String getChildNodeConstraintViolation(DocViewNode node, EffectiveNodeType nodeType,
+            NodeTypeDefinitionProvider nodeTypeDefinitionProvider,
+            NameResolver nameResolver, ItemDefinitionProvider itemDefinitionProvider, boolean allowProtected)
+            throws RepositoryException {
+        Name nodeName = nameResolver.getQName(node.name);
+        QNodeTypeDefinition nodeTypeDefinition = nodeTypeDefinitionProvider.getNodeTypeDefinition(nameResolver.getQName(node.primary));
+        if (nodeTypeDefinition.isAbstract()) {
+            return "Not allowed to add node with abstract node type as primary type";
+        }
+        if (nodeTypeDefinition.isMixin()) {
+            return "Not allowed to add node with a mixin as primary node type";
+        }
+        try {
+            QNodeDefinition nd = itemDefinitionProvider.getQNodeDefinition(nodeType, nodeName, nodeTypeDefinition.getName());
+
+            if (!allowProtected && nd.isProtected()) {
+                return "Node is protected and can not be manually added";
+            }
+
+            if (nd.isAutoCreated()) {
+                return "Node is auto-created and can not be manually added";
+            }
+        } catch (ConstraintViolationException e) {
+            return "Could not find matching child node definition in parent's node type";
+        }
+
+        return null;
+    }
+
+    @Override
+    public @Nullable Collection<ValidationMessage> done() {
+        return null;
+    }
+
+}
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeTypeValidatorFactory.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeTypeValidatorFactory.java
new file mode 100644
index 0000000..be0ebb9
--- /dev/null
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeTypeValidatorFactory.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.vault.validation.spi.impl.nodetype;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.jar.Manifest;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.commons.cnd.ParseException;
+import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeType;
+import org.apache.jackrabbit.vault.validation.spi.ValidationContext;
+import org.apache.jackrabbit.vault.validation.spi.ValidationMessageSeverity;
+import org.apache.jackrabbit.vault.validation.spi.Validator;
+import org.apache.jackrabbit.vault.validation.spi.ValidatorFactory;
+import org.apache.jackrabbit.vault.validation.spi.ValidatorSettings;
+import org.apache.jackrabbit.vault.validation.spi.util.classloaderurl.URLFactory;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.kohsuke.MetaInfServices;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@MetaInfServices
+public class NodeTypeValidatorFactory implements ValidatorFactory {
+
+    public static final String OPTION_CNDS = "cnds";
+    /** The default node type to assume if no other node type is given */
+    public static final String OPTION_DEFAULT_NODE_TYPES = "defaultNodeType";
+    public static final String OPTION_SEVERITY_FOR_UNKNOWN_NODETYPES = "severityForUnknownNodetypes";
+
+    static final @NotNull String DEFAULT_DEFAULT_NODE_TYPE = JcrConstants.NT_FOLDER;
+
+    static final @NotNull ValidationMessageSeverity DEFAULT_SEVERITY_FOR_UNKNOWN_NODETYPE = ValidationMessageSeverity.WARN;
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(NodeTypeValidatorFactory.class);
+
+    @Override
+    public @Nullable Validator createValidator(@NotNull ValidationContext context, @NotNull ValidatorSettings settings) {
+
+        String cndUrls = settings.getOptions().get(OPTION_CNDS);
+        // either load map from classloader, from filesystem or from generic url
+        if (StringUtils.isBlank(cndUrls)) {
+            cndUrls = this.getClass().getClassLoader().getResource("default-nodetypes.cnd").toString();
+            LOGGER.warn("Using default nodetypes, consider specifying the nodetypes from the distribution you use!");
+        }
+
+        final String defaultNodeType;
+        if (settings.getOptions().containsKey(OPTION_DEFAULT_NODE_TYPES)) {
+            defaultNodeType = settings.getOptions().get(OPTION_DEFAULT_NODE_TYPES);
+        } else {
+            defaultNodeType = DEFAULT_DEFAULT_NODE_TYPE;
+        }
+
+        final @NotNull ValidationMessageSeverity severityForUnknownNodetypes;
+        if (settings.getOptions().containsKey(OPTION_SEVERITY_FOR_UNKNOWN_NODETYPES)) {
+            String optionValue = settings.getOptions().get(OPTION_SEVERITY_FOR_UNKNOWN_NODETYPES);
+            severityForUnknownNodetypes = ValidationMessageSeverity.valueOf(optionValue.toUpperCase());
+        } else {
+            severityForUnknownNodetypes = DEFAULT_SEVERITY_FOR_UNKNOWN_NODETYPE;
+        }
+
+        try {
+            NodeTypeManagerProvider ntManagerProvider = null;
+            ntManagerProvider = new NodeTypeManagerProvider();
+            for (String cndUrl : resolveJarUrls(cndUrls.split(","))) {
+                try (Reader reader = new InputStreamReader(URLFactory.createURL(cndUrl).openStream(), StandardCharsets.US_ASCII)) {
+                    LOGGER.info("Register additional node types from {}", cndUrl);
+                    ntManagerProvider.registerNodeTypes(reader);
+                } catch (RepositoryException | IOException | ParseException e) {
+                    throw new IllegalArgumentException("Error loading node types from CND at " + cndUrl, e);
+                }
+            }
+            EffectiveNodeType defaultEffectiveNodeType = ntManagerProvider.getEffectiveNodeTypeProvider()
+                    .getEffectiveNodeType(ntManagerProvider.getNameResolver().getQName(defaultNodeType));
+            return new NodeTypeValidator(context.getFilter(), ntManagerProvider, defaultEffectiveNodeType, settings.getDefaultSeverity(),
+                    severityForUnknownNodetypes);
+        } catch (IOException | RepositoryException | ParseException e) {
+            throw new IllegalArgumentException("Error loading default node type " + defaultNodeType, e);
+        }
+    }
+
+    /**
+     * Resolve URLs pointing to JARs with META-INF/MANIFEST carrying a {@code Sling-Nodetypes} header
+     * @param urls
+     * @return
+     */
+    static List<String> resolveJarUrls(String... urls) {
+        List<String> resolvedUrls = new LinkedList<>();
+        for (String url : urls) {
+            if (url.endsWith(".jar")) {
+                // https://docs.oracle.com/javase/7/docs/api/java/net/JarURLConnection.html
+                URL jarUrl;
+                try {
+                    jarUrl = URLFactory.createURL("jar:" + url + "!/");
+                    JarURLConnection jarConnection = (JarURLConnection)jarUrl.openConnection();
+                    Manifest manifest = jarConnection.getManifest();
+                    String slingNodetypes = manifest.getMainAttributes().getValue("Sling-Nodetypes");
+                    // split by "," and generate new JAR Urls
+                    if (slingNodetypes == null) {
+                        LOGGER.warn("No 'Sling-Nodetypes' header found in manifest of '{}'", jarUrl);
+                    } else {
+                        for (String nodetype : slingNodetypes.split(",")) {
+                            resolvedUrls.add(jarUrl.toString() + nodetype);
+                        }
+                    }
+                } catch (IOException e) {
+                    throw new IllegalArgumentException("Could not read from JAR " + url, e);
+                }
+            } else {
+                resolvedUrls.add(url);
+            }
+        }
+        return resolvedUrls;
+    }
+
+    @Override
+    public boolean shouldValidateSubpackages() {
+        return false;
+    }
+
+    @Override
+    public @NotNull String getId() {
+        return ValidatorFactory.ID_PREFIX_JACKRABBIT + "nodetypes";
+    }
+
+    @Override
+    public int getServiceRanking() {
+        return 0;
+    }
+
+}
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java
index c72bdef..5a64445 100644
--- a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java
@@ -18,7 +18,7 @@
 /**
  * The FileVault validation framework SPI. Provides classes/interfaces to implement validators on FileVault packages.
  */
-@Version("1.1.0")
+@Version("1.2.0")
 package org.apache.jackrabbit.vault.validation.spi;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/util/classloaderurl/ClassLoaderUrlConnection.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/util/classloaderurl/ClassLoaderUrlConnection.java
new file mode 100644
index 0000000..5c1e569
--- /dev/null
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/util/classloaderurl/ClassLoaderUrlConnection.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.vault.validation.spi.util.classloaderurl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+
+public class ClassLoaderUrlConnection extends URLConnection {
+    private final ClassLoader classLoader;
+
+    protected ClassLoaderUrlConnection(ClassLoader classLoader, URL url) {
+        super(url);
+        this.classLoader = classLoader;
+    }
+
+    @Override
+    public void connect() throws IOException {
+        
+    }
+
+    @Override
+    public InputStream getInputStream() throws IOException {
+        InputStream input = classLoader.getResourceAsStream(url.getFile());
+        if (input == null) {
+            throw new IOException("Could not load resource '" + url.getFile() + "' from classLoader '" + classLoader + "'");
+        }
+        return input;
+    }
+}
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/util/classloaderurl/ThreadContextClassLoaderURLStreamHandler.java
similarity index 63%
copy from vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java
copy to vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/util/classloaderurl/ThreadContextClassLoaderURLStreamHandler.java
index c72bdef..3ca3713 100644
--- a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/util/classloaderurl/ThreadContextClassLoaderURLStreamHandler.java
@@ -14,11 +14,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.jackrabbit.vault.validation.spi.util.classloaderurl;
 
-/**
- * The FileVault validation framework SPI. Provides classes/interfaces to implement validators on FileVault packages.
- */
-@Version("1.1.0")
-package org.apache.jackrabbit.vault.validation.spi;
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+public class ThreadContextClassLoaderURLStreamHandler extends URLStreamHandler {
+
+    @Override
+    protected URLConnection openConnection(URL url) throws IOException {
+        return new ClassLoaderUrlConnection(Thread.currentThread().getContextClassLoader(), url);
+    }
 
-import org.osgi.annotation.versioning.Version;
+}
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/util/classloaderurl/URLFactory.java
similarity index 52%
copy from vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java
copy to vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/util/classloaderurl/URLFactory.java
index c72bdef..517168a 100644
--- a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/util/classloaderurl/URLFactory.java
@@ -14,11 +14,28 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.jackrabbit.vault.validation.spi.util.classloaderurl;
 
-/**
- * The FileVault validation framework SPI. Provides classes/interfaces to implement validators on FileVault packages.
- */
-@Version("1.1.0")
-package org.apache.jackrabbit.vault.validation.spi;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class URLFactory {
+    public static final String TCCL_PROTOCOL_PREFIX = "tccl:";
+    
+    private URLFactory() {
+        
+    }
 
-import org.osgi.annotation.versioning.Version;
+    public static URL createURL(String spec) throws MalformedURLException {
+        final URL url;
+        // which URLHandler to take
+        if (spec.startsWith(TCCL_PROTOCOL_PREFIX)) {
+            // use custom UrlStreamHandler
+            url = new URL(null, spec, new ThreadContextClassLoaderURLStreamHandler());
+        } else {
+            // use default UrlStreamHandler
+            url = new URL(spec);
+        }
+        return url;
+    }
+}
diff --git a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/util/classloaderurl/package-info.java
similarity index 81%
copy from vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java
copy to vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/util/classloaderurl/package-info.java
index c72bdef..c23cd0a 100644
--- a/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/package-info.java
+++ b/vault-validation/src/main/java/org/apache/jackrabbit/vault/validation/spi/util/classloaderurl/package-info.java
@@ -14,11 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-/**
- * The FileVault validation framework SPI. Provides classes/interfaces to implement validators on FileVault packages.
- */
-@Version("1.1.0")
-package org.apache.jackrabbit.vault.validation.spi;
+@Version("1.0.0")
+package org.apache.jackrabbit.vault.validation.spi.util.classloaderurl;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/vault-validation/src/main/resources/default-nodetypes.cnd b/vault-validation/src/main/resources/default-nodetypes.cnd
new file mode 100644
index 0000000..bb31f77
--- /dev/null
+++ b/vault-validation/src/main/resources/default-nodetypes.cnd
@@ -0,0 +1,446 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// all node types defined by https://docs.adobe.com/docs/en/spec/jcr/2.0/3_Repository_Model.html#3.7.11%20Standard%20Application%20Node%20Types
+// as well as some Jackrabbit/Oak specific ones
+<'nt'='http://www.jcp.org/jcr/nt/1.0'>
+<'oak'='http://jackrabbit.apache.org/oak/ns/1.0'>
+<'jcr'='http://www.jcp.org/jcr/1.0'>
+<'mix'='http://www.jcp.org/jcr/mix/1.0'>
+<'rep'='internal'>
+<'vlt'='http://www.day.com/jcr/vault/1.0'>
+
+[oak:Unstructured]
+  - * (undefined) multiple
+  - * (undefined)
+  + * (nt:base) = oak:Unstructured version
+
+[nt:linkedFile] > nt:hierarchyNode
+  primaryitem jcr:content
+  - jcr:content (reference) mandatory
+
+[rep:Group] > rep:Authorizable, rep:MemberReferences
+  + rep:members (rep:Members) = rep:Members protected multiple version
+  + rep:membersList (rep:MemberReferencesList) = rep:MemberReferencesList protected
+
+[mix:lifecycle]
+  mixin
+  - jcr:lifecyclePolicy (reference) protected initialize
+  - jcr:currentLifecycleState (string) protected initialize
+
+[rep:User] > rep:Authorizable, rep:Impersonatable
+  - rep:password (string) protected
+  - rep:disabled (string) protected
+  + rep:pwd (rep:Password) = rep:Password protected
+
+[rep:Privileges]
+  - rep:next (long) mandatory protected multiple
+  + * (rep:Privilege) = rep:Privilege protected abort
+
+[nt:activity] > mix:referenceable
+  - jcr:activityTitle (string) mandatory autocreated protected
+
+[nt:childNodeDefinition]
+  - jcr:name (name) protected
+  - jcr:autoCreated (boolean) mandatory protected
+  - jcr:mandatory (boolean) mandatory protected
+  - jcr:onParentVersion (string) mandatory protected < 'COPY', 'VERSION', 'INITIALIZE', 'COMPUTE', 'IGNORE', 'ABORT'
+  - jcr:protected (boolean) mandatory protected
+  - jcr:requiredPrimaryTypes (name) = 'nt:base' mandatory protected multiple
+  - jcr:defaultPrimaryType (name) protected
+  - jcr:sameNameSiblings (boolean) mandatory protected
+
+[rep:PropertyDefinition] > nt:propertyDefinition
+  - rep:declaringNodeType (name) mandatory protected
+
+[rep:CugPolicy] > rep:Policy
+  - rep:principalNames (string) mandatory protected multiple ignore
+
+[nt:configuration] > mix:versionable
+  - jcr:root (reference) mandatory autocreated protected
+
+[mix:simpleVersionable]
+  mixin
+  - jcr:isCheckedOut (boolean) = 'true' mandatory autocreated protected ignore
+
+[mix:mimeType]
+  mixin
+  - jcr:mimeType (string)
+  - jcr:encoding (string)
+
+[nt:query]
+  - jcr:statement (string)
+  - jcr:language (string)
+
+[nt:nodeType]
+  - jcr:nodeTypeName (name) mandatory protected
+  - jcr:supertypes (name) protected multiple
+  - jcr:isAbstract (boolean) mandatory protected
+  - jcr:isQueryable (boolean) mandatory protected
+  - jcr:isMixin (boolean) mandatory protected
+  - jcr:hasOrderableChildNodes (boolean) mandatory protected
+  - jcr:primaryItemName (name) protected
+  + jcr:propertyDefinition (nt:propertyDefinition) = nt:propertyDefinition protected multiple
+  + jcr:childNodeDefinition (nt:childNodeDefinition) = nt:childNodeDefinition protected multiple
+
+[rep:system]
+  orderable
+  + jcr:versionStorage (rep:versionStorage) = rep:versionStorage mandatory protected abort
+  + jcr:nodeTypes (rep:nodeTypes) = rep:nodeTypes mandatory protected abort
+  + jcr:activities (rep:Activities) = rep:Activities mandatory protected abort
+  + jcr:configurations (rep:Configurations) = rep:Configurations protected abort
+  + * (nt:base) = nt:base ignore
+  + rep:privileges (rep:Privileges) = rep:Privileges protected abort
+
+[rep:Password]
+  - * (undefined) protected
+  - * (undefined) protected multiple
+
+[nt:unstructured]
+  orderable
+  - * (undefined) multiple
+  - * (undefined)
+  + * (nt:base) = nt:unstructured multiple version
+
+[mix:atomicCounter]
+  mixin
+  - oak:counter (long) = '0' autocreated protected
+
+[rep:ACE]
+  - rep:principalName (string) mandatory protected
+  - rep:privileges (name) mandatory protected multiple
+  - rep:nodePath (path) protected
+  - rep:glob (string) protected
+  - * (undefined) protected
+  + rep:restrictions (rep:Restrictions) = rep:Restrictions protected
+
+[rep:versionStorage]
+  - * (undefined) protected abort
+  - * (undefined) protected multiple abort
+  + * (nt:versionHistory) = nt:versionHistory protected abort
+  + * (rep:versionStorage) = rep:versionStorage protected abort
+
+[mix:indexable]
+  mixin
+  + oak:index (nt:base) = nt:unstructured
+
+[rep:Authorizable] > mix:referenceable, nt:hierarchyNode
+  abstract
+  - rep:principalName (string) mandatory protected
+  - rep:authorizableId (string) protected
+  - * (undefined)
+  - * (undefined) multiple
+  + * (nt:base) = nt:unstructured version
+
+[nt:frozenNode] > mix:referenceable
+  orderable
+  - jcr:frozenPrimaryType (name) mandatory autocreated protected abort
+  - jcr:frozenMixinTypes (name) protected multiple abort
+  - jcr:frozenUuid (string) mandatory autocreated protected abort
+  - * (undefined) protected abort
+  - * (undefined) protected multiple abort
+  + * (nt:base) protected multiple abort
+
+[mix:etag]
+  mixin
+  - jcr:etag (string) autocreated protected
+
+[rep:ChildNodeDefinition] > nt:childNodeDefinition
+  - rep:declaringNodeType (name) mandatory protected
+
+[nt:version] > mix:referenceable
+  - jcr:created (date) mandatory autocreated protected abort
+  - jcr:predecessors (reference) protected multiple abort < 'nt:version'
+  - jcr:successors (reference) protected multiple abort < 'nt:version'
+  - jcr:activity (reference) protected abort < 'nt:activity'
+  + jcr:frozenNode (nt:frozenNode) protected abort
+
+[rep:MemberReferencesList]
+  + * (rep:MemberReferences) = rep:MemberReferences protected
+
+[nt:versionLabels]
+  - * (reference) protected abort < 'nt:version'
+
+[mix:versionable] > mix:referenceable, mix:simpleVersionable
+  mixin
+  - jcr:versionHistory (reference) mandatory protected ignore < 'nt:versionHistory'
+  - jcr:baseVersion (reference) mandatory protected ignore < 'nt:version'
+  - jcr:predecessors (reference) mandatory protected multiple ignore < 'nt:version'
+  - jcr:mergeFailed (reference) protected multiple abort < 'nt:version'
+  - jcr:activity (reference) protected < 'nt:activity'
+  - jcr:configuration (reference) protected ignore < 'nt:configuration'
+
+[rep:PrincipalAccessControl] > rep:AccessControl
+  + rep:policy (rep:Policy) protected ignore
+
+[rep:Policy]
+  abstract
+
+[rep:Configurations]
+  + * (nt:configuration) = nt:configuration abort
+  + * (rep:Configurations) = rep:Configurations abort
+
+[rep:Activities]
+  + * (nt:activity) = nt:activity protected abort
+  + * (rep:Activities) = rep:Activities protected abort
+
+[rep:Token] > mix:referenceable
+  - rep:token.key (string) mandatory protected
+  - rep:token.exp (date) mandatory protected
+  - * (undefined) protected
+  - * (undefined) protected multiple
+
+[rep:Impersonatable]
+  mixin
+  - rep:impersonators (string) protected multiple
+
+[nt:hierarchyNode] > mix:created
+  abstract
+
+[vlt:PackageDefinition] > nt:unstructured
+  orderable
+  - artifactId (string)
+  - jcr:created (date)
+  - jcr:createdBy (string)
+  - jcr:lastModified (date)
+  - lastUnpackedBy (string)
+  - jcr:description (string)
+  - groupId (string)
+  - lastUnpacked (date)
+  - version (string)
+  - jcr:lastModifiedBy (string)
+  - dependencies (string) multiple
+  + thumbnail (nt:base) = nt:unstructured
+  + filter (nt:base) = nt:unstructured
+
+[nt:resource] > mix:lastModified, mix:mimeType, mix:referenceable
+  primaryitem jcr:data
+  - jcr:data (binary) mandatory
+
+[nt:file] > nt:hierarchyNode
+  primaryitem jcr:content
+  + jcr:content (nt:base) mandatory
+
+[rep:VersionablePaths]
+  mixin
+  - * (path) protected abort
+
+[mix:lockable]
+  mixin
+  - jcr:lockOwner (string) protected ignore
+  - jcr:lockIsDeep (boolean) protected ignore
+
+[rep:MergeConflict]
+  mixin primaryitem rep:ours
+  + rep:ours (rep:Unstructured) protected ignore
+
+[oak:QueryIndexDefinition] > oak:Unstructured
+  - type (string) mandatory
+  - async (string)
+  - reindex (boolean) ignore
+
+[nt:base]
+  abstract
+  - jcr:primaryType (name) mandatory autocreated protected compute
+  - jcr:mixinTypes (name) protected multiple compute
+
+[rep:Cache] > rep:UnstructuredProtected
+  - rep:expiration (long) protected ignore
+
+[mix:title]
+  mixin
+  - jcr:title (string)
+  - jcr:description (string)
+
+[rep:root] > nt:unstructured
+  + jcr:system (rep:system) = rep:system mandatory ignore
+
+[nt:address]
+  - jcr:protocol (string)
+  - jcr:host (string)
+  - jcr:port (string)
+  - jcr:repository (string)
+  - jcr:workspace (string)
+  - jcr:path (path)
+  - jcr:id (weakreference)
+
+[rep:Unstructured]
+  - * (undefined) multiple ignore
+  - * (undefined) ignore
+  + * (nt:base) = rep:Unstructured ignore
+
+[rep:ACL] > rep:Policy
+  orderable
+  + * (rep:ACE) = rep:GrantACE protected ignore
+
+[rep:Privilege]
+  - rep:isAbstract (boolean) protected
+  - rep:aggregates (name) protected multiple
+  - rep:bits (long) mandatory protected multiple
+
+[rep:MemberReferences]
+  - rep:members (weakreference) protected multiple < 'rep:Authorizable'
+
+[rep:PropertyDefinitions]
+  + * (rep:PropertyDefinition) = rep:PropertyDefinition protected
+
+[mix:referenceable]
+  mixin
+  - jcr:uuid (string) mandatory autocreated protected initialize
+
+[mix:lastModified]
+  mixin
+  - jcr:lastModified (date) autocreated
+  - jcr:lastModifiedBy (string) autocreated
+
+[rep:Members]
+  orderable
+  - * (weakreference) protected < 'rep:Authorizable'
+  + * (rep:Members) = rep:Members protected multiple
+
+[vlt:FullCoverage]
+  mixin
+
+[rep:Permissions]
+  - * (undefined) protected ignore
+  - * (undefined) protected multiple ignore
+  + * (rep:Permissions) = rep:Permissions protected ignore
+
+[mix:created]
+  mixin
+  - jcr:created (date) autocreated protected
+  - jcr:createdBy (string) autocreated protected
+
+[nt:folder] > nt:hierarchyNode
+  + * (nt:hierarchyNode) version
+
+[nt:propertyDefinition]
+  - jcr:name (name) protected
+  - jcr:autoCreated (boolean) mandatory protected
+  - jcr:mandatory (boolean) mandatory protected
+  - jcr:onParentVersion (string) mandatory protected < 'COPY', 'VERSION', 'INITIALIZE', 'COMPUTE', 'IGNORE', 'ABORT'
+  - jcr:protected (boolean) mandatory protected
+  - jcr:requiredType (string) mandatory protected < 'STRING', 'URI', 'BINARY', 'LONG', 'DOUBLE', 'DECIMAL', 'BOOLEAN', 'DATE', 'NAME', 'PATH', 'REFERENCE', 'WEAKREFERENCE', 'UNDEFINED'
+  - jcr:valueConstraints (string) protected multiple
+  - jcr:defaultValues (undefined) protected multiple
+  - jcr:multiple (boolean) mandatory protected
+  - jcr:availableQueryOperators (name) mandatory protected multiple
+  - jcr:isFullTextSearchable (boolean) mandatory protected
+  - jcr:isQueryOrderable (boolean) mandatory protected
+
+[rep:NodeType] > nt:nodeType
+  - rep:supertypes (name) autocreated protected multiple
+  - rep:primarySubtypes (name) autocreated protected multiple
+  - rep:mixinSubtypes (name) autocreated protected multiple
+  - rep:mandatoryProperties (name) autocreated protected multiple
+  - rep:mandatoryChildNodes (name) autocreated protected multiple
+  - rep:protectedProperties (name) autocreated protected multiple
+  - rep:protectedChildNodes (name) autocreated protected multiple
+  - rep:hasProtectedResidualProperties (boolean) autocreated protected
+  - rep:hasProtectedResidualChildNodes (boolean) autocreated protected
+  - rep:namedSingleValuedProperties (name) autocreated protected multiple
+  + rep:namedPropertyDefinitions (rep:NamedPropertyDefinitions) = rep:NamedPropertyDefinitions protected
+  + rep:residualPropertyDefinitions (rep:PropertyDefinitions) = rep:PropertyDefinitions protected
+  + rep:namedChildNodeDefinitions (rep:NamedChildNodeDefinitions) = rep:NamedChildNodeDefinitions protected
+  + rep:residualChildNodeDefinitions (rep:ChildNodeDefinitions) = rep:ChildNodeDefinitions protected
+
+[mix:shareable] > mix:referenceable
+  mixin
+
+[rep:AccessControl]
+  + * (rep:AccessControl) protected ignore
+  + * (rep:PrincipalAccessControl) protected ignore
+
+[rep:SystemUser] > rep:User
+
+[mix:language]
+  mixin
+  - jcr:language (string)
+
+[rep:VersionReference]
+  mixin
+  - rep:versions (reference) protected multiple
+
+[rep:PermissionStore]
+  - rep:accessControlledPath (string) protected ignore
+  - rep:numPermissions (long) protected ignore
+  - rep:modCount (long) protected ignore
+  + * (rep:PermissionStore) = rep:PermissionStore protected ignore
+  + * (rep:Permissions) = rep:Permissions protected ignore
+
+[rep:UnstructuredProtected]
+  abstract
+  - * (undefined) protected multiple ignore
+  - * (undefined) protected ignore
+  + * (rep:UnstructuredProtected) protected ignore
+
+[oak:Resource] > mix:lastModified, mix:mimeType
+  primaryitem jcr:data
+  - jcr:data (binary) mandatory
+
+[rep:NamedPropertyDefinitions]
+  + * (rep:PropertyDefinitions) = rep:PropertyDefinitions protected
+
+[rep:nodeTypes]
+  + * (nt:nodeType) = nt:nodeType protected abort
+
+[rep:AccessControllable]
+  mixin
+  + rep:policy (rep:Policy) protected ignore
+
+[rep:NamedChildNodeDefinitions]
+  + * (rep:ChildNodeDefinitions) = rep:ChildNodeDefinitions protected
+
+[nt:versionHistory] > mix:referenceable
+  - jcr:versionableUuid (string) mandatory autocreated protected abort
+  - jcr:copiedFrom (weakreference) protected abort < 'nt:version'
+  + jcr:rootVersion (nt:version) = nt:version mandatory autocreated protected abort
+  + jcr:versionLabels (nt:versionLabels) = nt:versionLabels mandatory autocreated protected abort
+  + * (nt:version) = nt:version protected abort
+
+[rep:RetentionManageable]
+  mixin
+  - rep:hold (undefined) protected multiple ignore
+  - rep:retentionPolicy (undefined) protected ignore
+
+[rep:RepoAccessControllable]
+  mixin
+  + rep:repoPolicy (rep:Policy) protected ignore
+
+[rep:GrantACE] > rep:ACE
+
+[rep:ChildNodeDefinitions]
+  + * (rep:ChildNodeDefinition) = rep:ChildNodeDefinition protected
+
+[vlt:HierarchyNode] > nt:hierarchyNode
+  mixin
+
+[nt:versionedChild]
+  - jcr:childVersionHistory (reference) mandatory autocreated protected abort < 'nt:versionHistory'
+
+[vlt:Package]
+  orderable mixin
+  + vlt:definition (nt:base) = vlt:PackageDefinition
+
+[rep:DenyACE] > rep:ACE
+
+[rep:AuthorizableFolder] > nt:hierarchyNode
+  + * (rep:Authorizable) = rep:User version
+  + * (rep:AuthorizableFolder) = rep:AuthorizableFolder version
+
+[rep:Restrictions]
+  - * (undefined) protected
+  - * (undefined) protected multiple
diff --git a/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/ValidationExecutorTest.java b/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/ValidationExecutorTest.java
index 606456f..4477a3f 100644
--- a/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/ValidationExecutorTest.java
+++ b/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/ValidationExecutorTest.java
@@ -247,9 +247,13 @@ public class ValidationExecutorTest {
     }
 
     public static void assertViolation(Collection<? extends ValidationMessage> messages, ValidationMessageSeverity thresholdSeverity, ValidationMessage... violations) {
-        List<ValidationMessage> filteredMessages = messages.stream()
-                .filter(m -> m.getSeverity().ordinal() >= thresholdSeverity.ordinal()).collect(Collectors.toList());
-        Assert.assertThat(filteredMessages, Matchers.contains(violations));
+        if (messages == null) {
+            Assert.fail("No violations found at all!");
+        } else {
+            List<ValidationMessage> filteredMessages = messages.stream()
+                    .filter(m -> m.getSeverity().ordinal() >= thresholdSeverity.ordinal()).collect(Collectors.toList());
+            Assert.assertThat(filteredMessages, Matchers.contains(violations));
+        }
     }
 
     public static void assertViolation(Collection<? extends ValidationMessage> messages, ValidationMessage... violations) {
diff --git a/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/spi/impl/DocumentViewParserValidatorTest.java b/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/spi/impl/DocumentViewParserValidatorTest.java
new file mode 100644
index 0000000..6dbdb9f
--- /dev/null
+++ b/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/spi/impl/DocumentViewParserValidatorTest.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.vault.validation.spi.impl;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+
+public class DocumentViewParserValidatorTest {
+
+    @Test
+    public void testGetDocumentViewXmlRootPathFromContentXml() throws IOException {
+        Path filePath = Paths.get("test", "parent", ".content.xml");
+        Assert.assertEquals(Paths.get("test", "parent"), DocumentViewParserValidator.getDocumentViewXmlRootPath(null, filePath));
+    }
+
+    @Test
+    public void testGetDocumentViewXmlRootPathFromContentXmlBelowDotDir() throws IOException {
+        // http://jackrabbit.apache.org/filevault/vaultfs.html#Extended_File_aggregates
+        Path filePath = Paths.get("test", "parent.dir", ".content.xml");
+        Assert.assertEquals(Paths.get("test", "parent"), DocumentViewParserValidator.getDocumentViewXmlRootPath(null, filePath));
+    }
+}
diff --git a/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/spi/impl/PrimaryNodeTypeValidatorTest.java b/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/spi/impl/PrimaryNodeTypeValidatorTest.java
deleted file mode 100644
index 6b4467c..0000000
--- a/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/spi/impl/PrimaryNodeTypeValidatorTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.jackrabbit.vault.validation.spi.impl;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Paths;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-import javax.jcr.PropertyType;
-
-import org.apache.jackrabbit.vault.fs.config.ConfigurationException;
-import org.apache.jackrabbit.vault.fs.config.DefaultWorkspaceFilter;
-import org.apache.jackrabbit.vault.util.DocViewNode;
-import org.apache.jackrabbit.vault.util.DocViewProperty;
-import org.apache.jackrabbit.vault.validation.AnyValidationMessageMatcher;
-import org.apache.jackrabbit.vault.validation.ValidationExecutorTest;
-import org.apache.jackrabbit.vault.validation.spi.ValidationMessage;
-import org.apache.jackrabbit.vault.validation.spi.ValidationMessageSeverity;
-import org.apache.jackrabbit.vault.validation.spi.impl.PrimaryNodeTypeValidator;
-import org.junit.Assert;
-import org.junit.Test;
-
-
-public class PrimaryNodeTypeValidatorTest {
-
-    private PrimaryNodeTypeValidator validator;
-
-    @Test
-    public void testNodeTypes() throws IOException, ConfigurationException {
-        DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter();
-        try (InputStream input = this.getClass().getResourceAsStream("/filter.xml")) {
-            filter.load(input);
-        }
-        validator = new PrimaryNodeTypeValidator(ValidationMessageSeverity.ERROR, filter);
-        Map<String, DocViewProperty> props = new HashMap<>();
-        props.put("prop1", new DocViewProperty("prop1", new String[] { "value1" } , false, PropertyType.STRING));
-
-        // order node only (no other property)
-        DocViewNode node = new DocViewNode("jcr:root", "jcr:root", null, Collections.emptyMap(), null, null);
-        Assert.assertThat(validator.validate(node, "/apps/test", Paths.get("/some/path"), false), AnyValidationMessageMatcher.noValidationInCollection());
-
-        // primary node type set with additional properties
-        node = new DocViewNode("jcr:root", "jcr:root", null, props, null, "nt:unstructured");
-        Assert.assertThat(validator.validate(node, "/apps/test", Paths.get("/some/path"), false), AnyValidationMessageMatcher.noValidationInCollection());
-
-        // missing node type but not contained in filter (with properties)
-        node = new DocViewNode("jcr:root", "jcr:root", null, props, null, null);
-        Assert.assertThat(validator.validate(node, "/apps/test2/invalid", Paths.get("/some/path"), false), AnyValidationMessageMatcher.noValidationInCollection());
-
-        // missing node type and contained in filter (with properties)
-        ValidationExecutorTest.assertViolation(
-                        validator.validate(node, "/apps/test", Paths.get("/some/path"), false),
-                        new ValidationMessage(ValidationMessageSeverity.ERROR, String.format(PrimaryNodeTypeValidator.MESSAGE_MISSING_PRIMARY_TYPE, "/apps/test")));
-    }
-}
diff --git a/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeTypeValidatorTest.java b/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeTypeValidatorTest.java
new file mode 100644
index 0000000..b0a7220
--- /dev/null
+++ b/vault-validation/src/test/java/org/apache/jackrabbit/vault/validation/spi/impl/nodetype/NodeTypeValidatorTest.java
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.vault.validation.spi.impl.nodetype;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.commons.cnd.ParseException;
+import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeType;
+import org.apache.jackrabbit.spi.commons.name.NameConstants;
+import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
+import org.apache.jackrabbit.vault.fs.config.ConfigurationException;
+import org.apache.jackrabbit.vault.fs.config.DefaultWorkspaceFilter;
+import org.apache.jackrabbit.vault.util.DocViewNode;
+import org.apache.jackrabbit.vault.util.DocViewProperty;
+import org.apache.jackrabbit.vault.validation.AnyValidationMessageMatcher;
+import org.apache.jackrabbit.vault.validation.ValidationExecutorTest;
+import org.apache.jackrabbit.vault.validation.spi.NodeContext;
+import org.apache.jackrabbit.vault.validation.spi.ValidationMessage;
+import org.apache.jackrabbit.vault.validation.spi.ValidationMessageSeverity;
+import org.apache.jackrabbit.vault.validation.spi.util.NodeContextImpl;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class NodeTypeValidatorTest {
+
+    private NodeTypeValidator validator;
+    private DefaultWorkspaceFilter filter;
+
+    @Before
+    public void setUp() throws IOException, ConfigurationException, RepositoryException, ParseException {
+        filter = new DefaultWorkspaceFilter();
+        try (InputStream input = this.getClass().getResourceAsStream("/filter.xml")) {
+            filter.load(input);
+        }
+        validator = createValidator(filter, JcrConstants.NT_FOLDER);
+    }
+
+    static NodeTypeValidator createValidator(WorkspaceFilter filter, String defaultNodeType)
+            throws IOException, RepositoryException, ParseException {
+        NodeTypeManagerProvider ntManagerProvider = new NodeTypeManagerProvider();
+        EffectiveNodeType defaultEffectiveNodeType = ntManagerProvider.getEffectiveNodeTypeProvider()
+                .getEffectiveNodeType(ntManagerProvider.getNameResolver().getQName(defaultNodeType));
+        return new NodeTypeValidator(filter, ntManagerProvider, defaultEffectiveNodeType, ValidationMessageSeverity.ERROR,
+                ValidationMessageSeverity.WARN);
+    }
+
+    @Test
+    @Ignore
+    public void testValidateComplexUnstructuredNodeTypes() throws IOException, RepositoryException, ParseException, ConfigurationException {
+        NodeContext nodeContext = new NodeContextImpl("/apps/test/node4", Paths.get("node4"), Paths.get(""));
+
+        Map<String, DocViewProperty> props = new HashMap<>();
+        DocViewProperty property = new DocViewProperty("{}prop1", new String[] { "value1" }, false, PropertyType.STRING);
+        props.put("{}prop1", property);
+        props.put(NameConstants.JCR_PRIMARYTYPE.toString(), new DocViewProperty(NameConstants.JCR_PRIMARYTYPE.toString(),
+                new String[] { "nt:unstructured" }, false, PropertyType.STRING));
+
+        // no primary type
+        DocViewNode node = new DocViewNode("jcr:root", "jcr:root", null, props, null, "sling:Folder");
+        Assert.assertNull(validator.validate(node, nodeContext, false));
+
+        props.put(NameConstants.JCR_PRIMARYTYPE.toString(),
+                new DocViewProperty(NameConstants.JCR_PRIMARYTYPE.toString(), new String[] { "value1" }, false, PropertyType.STRING));
+        node = new DocViewNode("test", "test", null, props, null, "nt:folder");
+        ValidationExecutorTest.assertViolation(validator.validate(node, nodeContext, false),
+                new ValidationMessage(ValidationMessageSeverity.ERROR,
+                        String.format(NodeTypeValidator.MESSAGE_PROPERTY_NOT_ALLOWED, property, "nt:folder",
+                                "No property definition found for name!")));
+    }
+
+    @Test
+    public void testInvalidChildNodeTypeBelowDefault() {
+        NodeContext nodeContext = new NodeContextImpl("/apps/test/node4", Paths.get("node4"), Paths.get(""));
+
+        Map<String, DocViewProperty> props = new HashMap<>();
+        props.put(NameConstants.JCR_PRIMARYTYPE.toString(), new DocViewProperty(NameConstants.JCR_PRIMARYTYPE.toString(),
+                new String[] { JcrConstants.NT_UNSTRUCTURED }, false, PropertyType.STRING));
+        // nt:unstructured below nt:folder is not allowed
+        DocViewNode node = new DocViewNode("jcr:root", "jcr:root", null, props, null, JcrConstants.NT_UNSTRUCTURED);
+        ValidationExecutorTest.assertViolation(validator.validate(node, nodeContext, false),
+                new ValidationMessage(ValidationMessageSeverity.ERROR,
+                        String.format(NodeTypeValidator.MESSAGE_CHILD_NODE_OF_NOT_CONTAINED_PARENT_POTENTIALLY_NOT_ALLOWED,
+                                "jcr:root [nt:unstructured]", JcrConstants.NT_FOLDER,
+                                "Could not find matching child node definition in parent's node type")));
+    }
+
+    @Test
+    public void testMissingMandatoryChildNode() {
+        NodeContext nodeContext = new NodeContextImpl("/apps/test/node4", Paths.get("node4"), Paths.get(""));
+
+        Map<String, DocViewProperty> props = new HashMap<>();
+        props.put(NameConstants.JCR_PRIMARYTYPE.toString(), new DocViewProperty(NameConstants.JCR_PRIMARYTYPE.toString(),
+                new String[] { JcrConstants.NT_FILE }, false, PropertyType.STRING));
+        DocViewNode node = new DocViewNode("jcr:root", "jcr:root", null, props, null, JcrConstants.NT_FILE);
+        Assert.assertThat(validator.validate(node, nodeContext, false), AnyValidationMessageMatcher.noValidationInCollection());
+
+        ValidationExecutorTest.assertViolation(validator.validateEnd(node, nodeContext, false),
+                new ValidationMessage(ValidationMessageSeverity.ERROR,
+                        String.format(NodeTypeValidator.MESSAGE_MANDATORY_CHILD_NODE_MISSING,
+                                "jcr:content [nt:base]")));
+    }
+
+    @Test
+    public void testNotAllowedProperty() {
+        NodeContext nodeContext = new NodeContextImpl("/apps/test/node4", Paths.get("node4"), Paths.get(""));
+
+        Map<String, DocViewProperty> props = new HashMap<>();
+        DocViewProperty prop = new DocViewProperty("{}invalid-prop", new String[] { "some-value" }, false, PropertyType.STRING);
+        props.put("{}invalid-prop", prop);
+        props.put(NameConstants.JCR_PRIMARYTYPE.toString(), new DocViewProperty(NameConstants.JCR_PRIMARYTYPE.toString(),
+                new String[] { JcrConstants.NT_FILE }, false, PropertyType.STRING));
+        // nt:file is only supposed to have jcr:created property
+        DocViewNode node = new DocViewNode("jcr:root", "jcr:root", null, props, null, JcrConstants.NT_FILE);
+        ValidationExecutorTest.assertViolation(validator.validate(node, nodeContext, false),
+                new ValidationMessage(ValidationMessageSeverity.ERROR,
+                        String.format(NodeTypeValidator.MESSAGE_PROPERTY_NOT_ALLOWED, prop, JcrConstants.NT_FILE,
+                                "No property definition found for name!")));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testPropertyWitInconvertibleValue() {
+        NodeContext nodeContext = new NodeContextImpl("/apps/test/node4", Paths.get("node4"), Paths.get(""));
+
+        Map<String, DocViewProperty> props = new HashMap<>();
+        DocViewProperty prop = new DocViewProperty(Property.JCR_CREATED, new String[] { "some-invalid-value" }, true, PropertyType.DATE);
+        props.put(Property.JCR_CREATED, prop);
+        props.put(NameConstants.JCR_PRIMARYTYPE.toString(), new DocViewProperty(NameConstants.JCR_PRIMARYTYPE.toString(),
+                new String[] { JcrConstants.NT_FILE }, false, PropertyType.STRING));
+        // nt:file is only supposed to have jcr:created property
+        DocViewNode node = new DocViewNode("jcr:root", "jcr:root", null, props, null, JcrConstants.NT_FILE);
+        validator.validate(node, nodeContext, false);
+    }
+
+    @Test
+    public void testUnknownNamespace() {
+        NodeContext nodeContext = new NodeContextImpl("/apps/test/node4", Paths.get("node4"), Paths.get(""));
+
+        Map<String, DocViewProperty> props = new HashMap<>();
+        DocViewProperty prop = new DocViewProperty("{}invalid-prop", new String[] { "some-value" }, false, PropertyType.STRING);
+        props.put("{}invalid-prop", prop);
+        props.put(NameConstants.JCR_PRIMARYTYPE.toString(), new DocViewProperty(NameConstants.JCR_PRIMARYTYPE.toString(),
+                new String[] { "sling:Folder" }, false, PropertyType.STRING));
+        // nt:file is only supposed to have jcr:created property
+        DocViewNode node = new DocViewNode("jcr:root", "jcr:root", null, props, null, "sling:Folder");
+        ValidationExecutorTest.assertViolation(validator.validate(node, nodeContext, false),
+                new ValidationMessage(ValidationMessageSeverity.WARN,
+                        String.format(NodeTypeValidator.MESSAGE_UNKNOWN_NODE_TYPE_OR_NAMESPACE,
+                                "sling: is not a registered namespace prefix.")));
+    }
+
+    @Test
+    public void testExistenceOfPrimaryNodeTypes() throws IOException, ConfigurationException, RepositoryException, ParseException {
+        validator = createValidator(filter, NodeType.NT_UNSTRUCTURED);
+        Map<String, DocViewProperty> props = new HashMap<>();
+        props.put("{}prop1", new DocViewProperty("{}prop1", new String[] { "value1" }, false, PropertyType.STRING));
+
+        // order node only (no other property)
+        DocViewNode node = new DocViewNode("jcr:root", "jcr:root", null, Collections.emptyMap(), null, null);
+        Assert.assertThat(validator.validate(node, new NodeContextImpl("/apps/test", Paths.get("/some/path"), Paths.get("")), false),
+                AnyValidationMessageMatcher.noValidationInCollection());
+
+        // missing node type but not contained in filter (with properties)
+        node = new DocViewNode("jcr:root", "jcr:root", null, props, null, null);
+        Assert.assertThat(
+                validator.validate(node, new NodeContextImpl("/apps/test2/invalid", Paths.get("/some/path"), Paths.get("")), false),
+                AnyValidationMessageMatcher.noValidationInCollection());
+
+        // missing node type and contained in filter (with properties)
+        ValidationExecutorTest.assertViolation(
+                validator.validate(node, new NodeContextImpl("/apps/test", Paths.get("/some/path"), Paths.get("")), false),
+                new ValidationMessage(ValidationMessageSeverity.ERROR,
+                        String.format(NodeTypeValidator.MESSAGE_MISSING_PRIMARY_TYPE, "/apps/test")));
+
+        // primary node type set with additional properties
+        props.put(NameConstants.JCR_PRIMARYTYPE.toString(), new DocViewProperty(NameConstants.JCR_PRIMARYTYPE.toString(),
+                new String[] { "nt:unstructured" }, false, PropertyType.STRING));
+        node = new DocViewNode("jcr:root", "jcr:root", null, props, null, "nt:unstructured");
+        Assert.assertThat(validator.validate(node, new NodeContextImpl("/apps/test", Paths.get("/some/path"), Paths.get("")), false),
+                AnyValidationMessageMatcher.noValidationInCollection());
+
+    }
+}