org.eel.kitchen.jsonschema.report.ValidationReport.java Source code

Java tutorial

Introduction

Here is the source code for org.eel.kitchen.jsonschema.report.ValidationReport.java

Source

/*
 * Copyright (c) 2012, Francis Galiegue <fgaliegue@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the Lesser GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * Lesser GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.eel.kitchen.jsonschema.report;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Ordering;
import org.eel.kitchen.jsonschema.main.JsonSchema;
import org.eel.kitchen.jsonschema.main.JsonSchemaException;
import org.eel.kitchen.jsonschema.ref.JsonPointer;

import java.util.Collection;
import java.util.Comparator;
import java.util.List;

/**
 * A validation report
 *
 * <p>A report is a map of {@link Message}s, with the path into the validated
 * instance where the error occurred as a supplementary information.</p>
 *
 * <p>You can retrieve messages either as a list of plain strings or JSON
 * (either an object or an array).</p>
 *
 * @see JsonSchema#validate(JsonNode)
 */
public final class ValidationReport {
    /**
     * Root JSON Pointer (ie, {@code #})
     */
    private static final JsonPointer ROOT;

    private static final Ordering<Message> MESSAGE_ORDER = Ordering.from(MessageComparator.instance);

    static {
        try {
            ROOT = new JsonPointer("");
        } catch (JsonSchemaException e) {
            throw new RuntimeException("WTF??", e);
        }
    }

    /**
     * Message list
     */
    private final ListMultimap<JsonPointer, Message> msgMap = ArrayListMultimap.create();

    /**
     * The current path
     */
    private JsonPointer path;

    private boolean fatal = false;

    /**
     * Create a new validation report with {@link #ROOT} as an instance path
     */
    public ValidationReport() {
        this(ROOT);
    }

    /**
     * Create a new validation report with an arbitraty path
     *
     * @param path the JSON Pointer
     */
    private ValidationReport(final JsonPointer path) {
        this.path = path;
    }

    /**
     * Get the current path of this report
     *
     * @return the path
     */
    public JsonPointer getPath() {
        return path;
    }

    /**
     * Set the current path of this report
     *
     * @param path the path
     */
    public void setPath(final JsonPointer path) {
        this.path = path;
    }

    /**
     * Add one validation message to the report
     *
     * <p>The message will be added to the already existing list of messages for
     * the current path (see {@link #getPath()}, {@link #setPath(JsonPointer)}).
     * </p>
     *
     * @param message the message
     * @return true if the added message is fatal, or if {@link #fatal} is
     * already {@code true}
     */
    public boolean addMessage(final Message message) {
        if (fatal)
            return true;

        if (message.isFatal()) {
            fatal = true;
            msgMap.clear();
        }
        msgMap.put(path, message);
        return fatal;
    }

    /**
     * Add several validation messages to the report
     *
     * @param messages the collection of messages
     */
    public void addMessages(final Collection<Message> messages) {
        for (final Message message : messages)
            if (addMessage(message))
                return;
    }

    @VisibleForTesting
    int size() {
        return msgMap.size();
    }

    /**
     * Is this report a success?
     *
     * @return true if the message map is empty
     */
    public boolean isSuccess() {
        return msgMap.isEmpty();
    }

    /**
     * Was there a fatal error during validation?
     *
     * <p>This implementation currently considers that all URI resolution
     * failures are fatal errors.</p>
     *
     * @return true if a fatal error has been encountered
     */
    public boolean hasFatalError() {
        return fatal;
    }

    /**
     * Merge with another validation report
     *
     * <p>Note that if a fatal error has been encountered, only the message
     * describing this fatal error will make it into the report. Other messages
     * are <b>discarded</b>.</p>
     *
     * @param other the report to merge with
     */
    public void mergeWith(final ValidationReport other) {
        if (fatal)
            return;

        if (other.fatal) {
            msgMap.clear();
            fatal = true;
        }

        msgMap.putAll(other.msgMap);
    }

    /**
     * Make a copy of this validation report, with an empty message map and
     * the current path.
     *
     * @return the new report
     */
    public ValidationReport copy() {
        return new ValidationReport(path);
    }

    /**
     * Get a flat list of validation messages as strings
     *
     * <p>One message has the form:</p>
     *
     * <pre>
     *     #/pointer/here: message here
     * </pre>
     *
     * <p>The list is sorted by pointer.</p>
     *
     * @return the list of messages
     */
    public List<String> getMessages() {
        final Iterable<JsonPointer> paths = Ordering.natural().sortedCopy(msgMap.keySet());

        final ImmutableList.Builder<String> builder = ImmutableList.builder();

        List<Message> messages;

        for (final JsonPointer path : paths) {
            messages = MESSAGE_ORDER.sortedCopy(msgMap.get(path));
            for (final Message msg : messages)
                builder.add(path + ": " + msg);
        }

        return builder.build();
    }

    /**
     * Retrieve all messages as a JSON object
     *
     * <p>The retrieved JSON document is an object where:</p>
     *
     * <ul>
     *     <li>keys are string representations of {@link JsonPointer}s,</li>
     *     <li>values are arrays of objects where each individual object is the
     *     JSON representation of one message.</li>
     * </ul>
     *
     * <p>Note: the returned {@link JsonNode} is mutable.</p>
     *
     * @see Message#toJsonNode()
     *
     * @return a JSON object with all validation messages
     */
    public JsonNode asJsonObject() {
        final ObjectNode ret = JsonNodeFactory.instance.objectNode();

        ArrayNode node;
        List<Message> messages;

        for (final JsonPointer ptr : msgMap.keySet()) {
            node = JsonNodeFactory.instance.arrayNode();
            messages = MESSAGE_ORDER.sortedCopy(msgMap.get(ptr));
            for (final Message message : messages)
                node.add(message.toJsonNode());
            ret.put(ptr.toString(), node);
        }

        return ret;
    }

    /**
     * Return the list of validation messages as a JSON array
     *
     * <p>This method makes its best to order validation messages correctly.</p>
     *
     * <p>Each message in the resulting array is a JSON object, with the
     * contents of the {@link Message} and with an added member named {@code
     * path}, which contains the path into the instance where the error has
     * occurred (as a {@link JsonPointer}).</p>
     *
     * @see Message#toJsonNode()
     *
     * @return a JSON array with all validation messages
     */
    public JsonNode asJsonArray() {
        final ArrayNode ret = JsonNodeFactory.instance.arrayNode();
        ObjectNode node;

        final Iterable<JsonPointer> paths = Ordering.natural().sortedCopy(msgMap.keySet());

        List<Message> messages;

        for (final JsonPointer ptr : paths) {
            messages = MESSAGE_ORDER.sortedCopy(msgMap.get(ptr));
            for (final Message msg : messages) {
                node = JsonNodeFactory.instance.objectNode().put("path", ptr.toString());
                // I hate to do that...
                node.putAll((ObjectNode) msg.toJsonNode());
                ret.add(node);
            }
        }

        return ret;
    }

    @Override
    public String toString() {
        return "current path: \"" + path + "\"; " + msgMap.size() + " messages";
    }

    private static class MessageComparator implements Comparator<Message> {
        private static final Comparator<Message> instance = new MessageComparator();

        @Override
        public int compare(final Message msg1, final Message msg2) {
            int ret;

            ret = msg1.getDomain().compareTo(msg2.getDomain());
            if (ret != 0)
                return ret;
            ret = msg1.getKeyword().compareTo(msg2.getKeyword());
            if (ret != 0)
                return ret;
            return msg1.getMessage().compareTo(msg2.getMessage());
        }
    }

    /*
     * Deprecated stuff
     */

    /**
     * Retrieve all messages as a {@link JsonNode}
     *
     * <p>The retrieved JSON document is an object where:</p>
     *
     * <ul>
     *     <li>keys are string representations of {@link JsonPointer}s,</li>
     *     <li>values are arrays of objects where each individual object is the
     *     JSON representation of one message.</li>
     * </ul>
     *
     * @see Message#toJsonNode()
     *
     * @return a JSON document with all validation messages
     *
     * @deprecated use {@link #asJsonObject()} instead (this method just calls
     * the latter anyway); scheduled for removal in 1.3+
     */
    @Deprecated
    public JsonNode asJsonNode() {
        return asJsonObject();
    }
}