class NodeTypeValidator implements DocumentViewXmlValidator, JcrPathValidator {

    @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<>();
        String parentNodePath = Text.getRelativeParent(nodeContext.getNodePath(), 1);
        NodeNameAndType parentNodeNameAndType = nodeTypePerPath.get(parentNodePath);
        NodeNameAndType newNodeNameAndType = null;
        try {
            // check node itself against parent node type
            if (!aclManagement.isACLNodeType(node.primary)) {
                final EffectiveNodeType parentNodeType;
                final boolean useDefaultNodeType;

                if (parentNodeNameAndType == null || !filter.contains(parentNodePath)) {
                    parentNodeType = defaultType;
                    useDefaultNodeType = true;
                } else if (!parentNodeNameAndType.isUnknown()) {
                    parentNodeType = parentNodeNameAndType.getEffectiveNodeType();
                    useDefaultNodeType = false;
                } else {
                    parentNodeType = null;
                    useDefaultNodeType = false;
                }

                if (parentNodeType != null) {
                    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
            newNodeNameAndType = new NodeNameAndType(ntManagerProvider.getNameResolver(),
                    ntManagerProvider.getEffectiveNodeTypeProvider(), node);
            nodeTypePerPath.put(nodeContext.getNodePath(), newNodeNameAndType);

            // 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()))));
                }
            }

        } 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());
            }
            if (newNodeNameAndType == null) {
                nodeTypePerPath.put(nodeContext.getNodePath(), NodeNameAndType.createUnknownNodeNameAndType(parentNodeNameAndType));
            }
        } catch (RepositoryException e) {
            throw new IllegalStateException("Could not validate nodes/properties against node types: " + e.getMessage(), e);
        }
        return messages;
    }

    public  NodeTypeValidator(@NotNull WorkspaceFilter filter, @NotNull NodeTypeManagerProvider ntManagerProvider,
            @NotNull EffectiveNodeType defaultEffectiveNodeType, @NotNull ValidationMessageSeverity defaultSeverity,
            @NotNull ValidationMessageSeverity severityForUnknownNodeTypes);

}