com.reprezen.swagedit.core.validation.Validator.java Source code

Java tutorial

Introduction

Here is the source code for com.reprezen.swagedit.core.validation.Validator.java

Source

/*******************************************************************************
 * Copyright (c) 2016 ModelSolv, Inc. and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    ModelSolv, Inc. - initial API and implementation and/or initial documentation
 *******************************************************************************/
package com.reprezen.swagedit.core.validation;

import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.core.resources.IMarker;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.ui.IFileEditorInput;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeId;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.ScalarNode;
import org.yaml.snakeyaml.nodes.SequenceNode;
import org.yaml.snakeyaml.parser.ParserException;

import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.reprezen.swagedit.core.editor.JsonDocument;
import com.reprezen.swagedit.core.json.references.JsonReference;
import com.reprezen.swagedit.core.json.references.JsonReferenceValidator;
import com.reprezen.swagedit.core.model.AbstractNode;
import com.reprezen.swagedit.core.model.ArrayNode;
import com.reprezen.swagedit.core.model.Model;
import com.reprezen.swagedit.core.model.ObjectNode;
import com.reprezen.swagedit.core.model.ValueNode;
import com.reprezen.swagedit.core.providers.ValidationProvider;
import com.reprezen.swagedit.core.utils.ExtensionUtils;

/**
 * This class contains methods for validating a Swagger YAML document.
 * 
 * Validation is done against the Swagger JSON Schema.
 * 
 * @see SwaggerError
 */
public abstract class Validator {

    private final JsonNode schemaRefTemplate = new ObjectMapper().createObjectNode() //
            .put("$ref", "#/definitions/schema");

    private final Set<ValidationProvider> providers;
    private final IPreferenceStore preferenceStore;

    public Validator(IPreferenceStore preferenceStore) {
        this.preferenceStore = preferenceStore;
        this.providers = ExtensionUtils.getValidationProviders();
    }

    public abstract JsonSchemaValidator getSchemaValidator();

    public abstract JsonReferenceValidator getReferenceValidator();

    protected IPreferenceStore getPreferenceStore() {
        return preferenceStore;
    }

    /**
     * Returns a list or errors if validation fails.
     * 
     * This method accepts as input a swagger YAML document and validates it against the swagger JSON Schema.
     * 
     * @param content
     * @param editorInput
     *            current input
     * @return list or errors
     * @throws IOException
     * @throws ParserException
     */
    public Set<SwaggerError> validate(JsonDocument document, IFileEditorInput editorInput) {
        URI baseURI = editorInput != null ? editorInput.getFile().getLocationURI() : null;
        return validate(document, baseURI);
    }

    public Set<SwaggerError> validate(JsonDocument document, URI baseURI) {
        Set<SwaggerError> errors = new HashSet<>();

        JsonNode jsonContent = null;
        try {
            jsonContent = document.asJson();
        } catch (Exception e) {
            jsonContent = null;
        }

        if (jsonContent != null) {
            Node yaml = document.getYaml();
            Model model = document.getModel();
            if (yaml != null && model != null) {
                errors.addAll(getSchemaValidator().validate(document));
                errors.addAll(validateDocument(baseURI, document));
                errors.addAll(checkDuplicateKeys(yaml));
                errors.addAll(getReferenceValidator().validate(baseURI, document, model));
            }
        }

        return errors;
    }

    /**
     * Validates the model against with different rules that cannot be verified only by JSON schema validation.
     * 
     * @param baseURI
     * @param model
     * @return errors
     */
    protected Set<SwaggerError> validateDocument(URI baseURI, JsonDocument document) {
        final Set<SwaggerError> errors = new HashSet<>();
        final Model model = document.getModel();

        if (model != null && model.getRoot() != null) {
            for (AbstractNode node : model.allNodes()) {
                executeModelValidation(model, node, errors);
                // execute validation from each providers
                providers.forEach(provider -> {

                    if (provider.isActive(document)) {
                        Set<SwaggerError> result = provider.validate(document, baseURI, node);
                        if (result != null) {
                            errors.addAll(result);
                        }
                    }
                });
            }
        }
        return errors;
    }

    protected void executeModelValidation(Model model, AbstractNode node, Set<SwaggerError> errors) {
        checkArrayTypeDefinition(errors, node);
        checkObjectTypeDefinition(errors, node);
    }

    /**
     * This method checks that the node if an array type definitions includes an items field.
     * 
     * @param errors
     * @param model
     */
    protected void checkArrayTypeDefinition(Set<SwaggerError> errors, AbstractNode node) {
        if (hasArrayType(node)) {
            AbstractNode items = node.get("items");
            if (items == null) {
                errors.add(error(node, IMarker.SEVERITY_ERROR, Messages.error_array_missing_items));
            } else {
                if (!items.isObject()) {
                    errors.add(error(items, IMarker.SEVERITY_ERROR, Messages.error_array_items_should_be_object));
                }
            }
        }
    }

    /**
     * Returns true if the node is an array type definition
     * 
     * @param node
     * @return true if array definition
     */
    protected boolean hasArrayType(AbstractNode node) {
        if (node.isObject() && node.get("type") instanceof ValueNode) {
            ValueNode typeValue = node.get("type").asValue();
            return typeValue.getValue() != null && "array".equalsIgnoreCase(typeValue.getValue().toString());
        }
        return false;
    }

    /**
     * Validates an object type definition.
     * 
     * @param errors
     * @param node
     */
    protected void checkObjectTypeDefinition(Set<SwaggerError> errors, AbstractNode node) {
        if (node instanceof ObjectNode) {
            JsonPointer ptr = node.getPointer();
            if (ptr != null && ValidationUtil.isInDefinition(ptr.toString())) {
                checkMissingType(errors, node);
                checkMissingRequiredProperties(errors, node);
            }
        }
    }

    /**
     * This method checks that the node if an object definition includes a type field.
     * 
     * @param errors
     * @param node
     */
    protected void checkMissingType(Set<SwaggerError> errors, AbstractNode node) {
        // object
        if (node.get("properties") != null) {
            // bypass this node, it is a property whose name is `properties`

            if ("properties".equals(node.getProperty())) {
                return;
            }

            if (node.get("type") == null) {
                errors.add(error(node, IMarker.SEVERITY_WARNING, Messages.error_object_type_missing));
            } else {
                AbstractNode typeValue = node.get("type");
                if (!(typeValue instanceof ValueNode)
                        || !Objects.equals("object", typeValue.asValue().getValue())) {
                    errors.add(error(node, IMarker.SEVERITY_ERROR, Messages.error_wrong_type));
                }
            }
        } else if (isSchemaDefinition(node) && node.get("type") == null) {
            errors.add(error(node, IMarker.SEVERITY_WARNING, Messages.error_type_missing));
        }
    }

    private boolean isSchemaDefinition(AbstractNode node) {
        // need to use getContent() because asJson() returns resolvedValue is some
        // subclasses
        return schemaRefTemplate.equals(node.getType().getContent()) //
                && node.get(JsonReference.PROPERTY) == null //
                && node.get("allOf") == null;
    }

    /**
     * This method checks that the required values for the object type definition contains only valid properties.
     * 
     * @param errors
     * @param node
     */
    protected void checkMissingRequiredProperties(Set<SwaggerError> errors, AbstractNode node) {
        if (node.get("required") instanceof ArrayNode) {
            ArrayNode required = node.get("required").asArray();

            AbstractNode properties = node.get("properties");
            if (properties == null) {
                errors.add(error(node, IMarker.SEVERITY_WARNING, Messages.warning_missing_properties));
            } else {
                for (AbstractNode prop : required.elements()) {
                    if (prop instanceof ValueNode) {
                        ValueNode valueNode = prop.asValue();
                        String value = valueNode.getValue().toString();

                        if (properties.get(value) == null) {
                            errors.add(error(valueNode, IMarker.SEVERITY_WARNING,
                                    String.format(Messages.warning_required_properties, value)));
                        }
                    }
                }
            }
        }
    }

    protected SwaggerError error(AbstractNode node, int level, String message) {
        return new SwaggerError(node.getStart().getLine() + 1, level, message);
    }

    /*
     * Finds all duplicate keys in all objects present in the YAML document.
     */
    protected Set<SwaggerError> checkDuplicateKeys(Node document) {
        Map<Pair<Node, String>, Set<Node>> acc = new HashMap<>();

        collectDuplicates(document, acc);

        Set<SwaggerError> errors = new HashSet<>();
        for (Pair<Node, String> key : acc.keySet()) {
            Set<Node> duplicates = acc.get(key);

            if (duplicates.size() > 1) {
                for (Node duplicate : duplicates) {
                    errors.add(createDuplicateError(key.getValue(), duplicate));
                }
            }
        }

        return errors;
    }

    /*
     * This method iterates through the YAML tree to collect the pairs of Node x String representing an object and one
     * of it's keys. Each pair is associated to a Set of Nodes which contains all nodes being a key to the pair's Node
     * and having for value the pair's key. Once the iteration is done, the resulting map should be traversed. Each pair
     * having more than one element in its associated Set are duplicate keys.
     */
    protected void collectDuplicates(Node parent, Map<Pair<Node, String>, Set<Node>> acc) {
        switch (parent.getNodeId()) {
        case mapping: {
            for (NodeTuple value : ((MappingNode) parent).getValue()) {
                Node keyNode = value.getKeyNode();

                if (keyNode.getNodeId() == NodeId.scalar) {
                    Pair<Node, String> key = Pair.of(parent, ((ScalarNode) keyNode).getValue());
                    acc.putIfAbsent(key, new HashSet<>());
                    acc.get(key).add(keyNode);
                }

                collectDuplicates(value.getValueNode(), acc);
            }
        }
            break;
        case sequence: {
            for (Node value : ((SequenceNode) parent).getValue()) {
                collectDuplicates(value, acc);
            }
        }
            break;
        default:
            break;
        }
    }

    protected SwaggerError createDuplicateError(String key, Node node) {
        return new SwaggerError(node.getStartMark().getLine() + 1, IMarker.SEVERITY_WARNING,
                String.format(Messages.error_duplicate_keys, key));
    }

}