org.everit.json.schema.IssueTest.java Source code

Java tutorial

Introduction

Here is the source code for org.everit.json.schema.IssueTest.java

Source

/*
 * Copyright (C) 2011 Everit Kft. (http://www.everit.org)
 *
 * Licensed 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.everit.json.schema;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import org.eclipse.jetty.server.Server;
import org.everit.json.schema.loader.SchemaLoader;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class IssueTest {

    @Parameters(name = "{1}")
    public static List<Object[]> params() {
        List<Object[]> rval = new ArrayList<>();
        try {
            File issuesDir = new File(IssueTest.class.getResource("/org/everit/json/schema/issues").toURI());
            for (File issue : issuesDir.listFiles()) {
                rval.add(new Object[] { issue, issue.getName() });
            }
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
        return rval;
    }

    private final File issueDir;

    private Server server;

    private ServletSupport servletSupport;

    private List<String> validationFailureList;
    private List<String> expectedFailureList;

    public IssueTest(final File issueDir, final String ignored) {
        this.issueDir = Objects.requireNonNull(issueDir, "issueDir cannot be null");
    }

    private Optional<File> fileByName(final String fileName) {
        return Arrays.stream(issueDir.listFiles()).filter(file -> file.getName().equals(fileName)).findFirst();
    }

    private void initJetty(final File documentRoot) {
        servletSupport = new ServletSupport(documentRoot);
        servletSupport.initJetty();
    }

    private Schema loadSchema() {
        Optional<File> schemaFile = fileByName("schema.json");
        try {
            if (schemaFile.isPresent()) {
                JSONObject schemaObj = new JSONObject(new JSONTokener(new FileInputStream(schemaFile.get())));
                return SchemaLoader.load(schemaObj);
            }
            throw new RuntimeException(issueDir.getCanonicalPath() + "/schema.json is not found");
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private void stopJetty() {
        if (servletSupport != null) {
            servletSupport.stopJetty();
        }
    }

    @Test
    public void test() {
        Assume.assumeFalse("issue dir starts with 'x' - ignoring", issueDir.getName().startsWith("x"));
        fileByName("remotes").ifPresent(this::initJetty);
        Schema schema = loadSchema();
        fileByName("subject-valid.json").ifPresent(file -> validate(file, schema, true));
        fileByName("subject-invalid.json").ifPresent(file -> validate(file, schema, false));
        stopJetty();
    }

    private void validate(final File file, final Schema schema, final boolean shouldBeValid) {
        ValidationException thrown = null;

        Object subject = loadJsonFile(file);

        try {
            schema.validate(subject);
        } catch (ValidationException e) {
            thrown = e;
        }

        if (shouldBeValid && thrown != null) {
            StringBuilder failureBuilder = new StringBuilder("validation failed with: " + thrown);
            for (ValidationException e : thrown.getCausingExceptions()) {
                failureBuilder.append("\n\t").append(e.getMessage());
            }
            Assert.fail(failureBuilder.toString());
        }
        if (!shouldBeValid && thrown != null) {
            Optional<File> expectedFile = fileByName("expectedException.json");
            if (expectedFile.isPresent()) {
                if (!checkExpectedValues(expectedFile.get(), thrown)) {
                    Assert.fail("Validation failures do not match expected values: \n" + "Expected: "
                            + expectedFailureList + ",\nActual:   " + validationFailureList);
                }
            }
        }
        if (!shouldBeValid && thrown == null) {
            Assert.fail("did not throw ValidationException for invalid subject");
        }
    }

    // TODO - it would be nice to see this moved out of tests to the main
    // source so that it can be used as a convenience method by users also...
    private Object loadJsonFile(final File file) {

        Object subject = null;

        try {
            JSONTokener jsonTok = new JSONTokener(new FileInputStream(file));

            // Determine if we have a single JSON object or an array of them
            Object jsonTest = jsonTok.nextValue();
            if (jsonTest instanceof JSONObject) {
                // The message contains a single JSON object
                subject = jsonTest;
            } else if (jsonTest instanceof JSONArray) {
                // The message contains a JSON array
                subject = jsonTest;
            }
        } catch (JSONException e) {
            throw new RuntimeException("failed to parse subject json file", e);
        } catch (FileNotFoundException e) {
            throw new UncheckedIOException(e);
        }
        return subject;
    }

    /**
     * Allow users to provide expected values for validation failures. This method reads and parses
     * files formatted like the following:
     *
     * { "message": "#: 2 schema violations found", "causingExceptions": [ { "message": "#/0/name:
     * expected type: String, found: JSONArray", "causingExceptions": [] }, { "message": "#/1:
     * required key [price] not found", "causingExceptions": [] } ] }
     *
     * The expected contents are then compared against the actual validation failures reported in the
     * ValidationException and nested causingExceptions.
     *
     */
    private boolean checkExpectedValues(final File expectedExceptionsFile, final ValidationException ve) {

        // Read the expected values from user supplied file
        Object expected = loadJsonFile(expectedExceptionsFile);
        expectedFailureList = new ArrayList<String>();
        // NOTE: readExpectedValues() will update expectedFailureList
        readExpectedValues((JSONObject) expected);

        // Read the actual validation failures into a list
        validationFailureList = new ArrayList<String>();
        // NOTE: processValidationFailures() will update validationFailureList
        processValidationFailures(ve);

        // Compare expected to actual
        return expectedFailureList.equals(validationFailureList);
    }

    // Recursively process the ValidationExceptions, which can contain lists
    // of sub-exceptions...
    // TODO - it would be nice to see this moved out of tests to the main
    // source so that it can be used as a convenience method by users also...
    private void processValidationFailures(final ValidationException ve) {
        List<ValidationException> causes = ve.getCausingExceptions();
        if (causes.isEmpty()) {
            // This was a leaf node, i.e. only one validation failure
            validationFailureList.add(ve.getMessage());
        } else {
            // Multiple validation failures exist, so process the sub-exceptions
            // to obtain them. NOTE: Not sure we should keep the message from
            // the current exception in this case. When there are causing
            // exceptions, the message in the containing exception is merely
            // summary information, e.g. "2 schema violations found".
            validationFailureList.add(ve.getMessage());
            causes.forEach(this::processValidationFailures);
        }
    }

    // Recursively process the expected values, which can contain nested arrays
    private void readExpectedValues(final JSONObject expected) {
        expectedFailureList.add((String) expected.get("message"));
        if (expected.has("causingExceptions")) {
            JSONArray causingEx = expected.getJSONArray("causingExceptions");
            for (Object subJson : causingEx) {
                readExpectedValues((JSONObject) subJson);
            }
        }
    }

}