eu.sonata.nfv.nec.validate.cli.Main.java Source code

Java tutorial

Introduction

Here is the source code for eu.sonata.nfv.nec.validate.cli.Main.java

Source

/*
 * Copyright (C) 2015, SONATA-NFV, NEC
 * ALL RIGHTS RESERVED
 *
 * This software may be modified and distributed under the terms
 * of the BSD license. See the LICENSE file for details.
 *
 * Neither the name of the SONATA-NFV and NEC nor the names of 
 * its contributors may be used to endorse or promote products 
 * derived from this software without specific prior written 
 * permission.
 * 
 * This work has been performed in the framework of the SONATA project,
 * funded by the European Commission under Grant number 671517 through 
 * the Horizon 2020 and 5G-PPP programmes. The authors would like to 
 * acknowledge the contributions of their colleagues of the SONATA 
 * partner consortium (www.sonata-nfv.eu).
 */
package eu.sonata.nfv.nec.validate.cli;

import ch.qos.logback.classic.Level;
import com.google.inject.Guice;
import com.google.inject.Injector;
import eu.sonata.nfv.nec.convert.ConversionService;
import eu.sonata.nfv.nec.validate.ValidationService;
import eu.sonata.nfv.nec.convert.modules.ConversionModule;
import eu.sonata.nfv.nec.validate.modules.ValidationModule;
import org.apache.commons.cli.*;
import org.apache.commons.io.IOUtils;
import org.json.JSONObject;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * A CLI implementation to validate JSON/YAML files against a given schema.
 *
 * The official JSON schema files to test for valid JSON can be
 * found at: https://github.com/json-schema/json-schema
 *
 * @author Michael Bredel
 */
public class Main {
    /** The exit code if the procedure succeeded. */
    public static final int EXIT_CODE_SUCCESS = 0;
    /** The exit code of the procedure failed. */
    public static final int EXIT_CODE_ERROR = 1;
    /** The help message. */
    public static final String HELP_MSG = "son-validate [OPTIONS] FILE";
    /** The path to the default schemas on the file system. */
    public static final String JSON_DRAFT_PATH = "./src/resources/";
    /** The file name to the default JSON draft 0.4 schema. */
    public static final String JSON_DRAFT_04 = "json-schema-draft-04.json";
    /** The key in a file that points to the corresponding schema. */
    public static final String DEFAULT_SCHEMA_KEY = "$schema";
    /** The ANSI code for the default color. */
    public static final String COLOR_DEFAULT = "\033[39m";
    /** The ANSI code for red. */
    public static final String COLOR_RED = "\033[31m";
    /** The ANSI code for green. */
    public static final String COLOR_GREEN = "\033[32m";
    /** The logger. */
    private static final org.slf4j.Logger Logger = LoggerFactory.getLogger(Main.class);

    /** The validation service. */
    private static ValidationService validator;
    /** The conversion service. */
    private static ConversionService converter;
    /** The path to the schema file. */
    private static String schemaFile;
    /** The path to the JSON/YAML file to verify or convert. */
    private static String jsonFile;
    /** The key that is used to locate schema URI's within a given JSON/YAML file to check. */
    private static String schemaKey;
    /** Should the given schema file be checked against the schema draft? */
    private static boolean checkSchema = true;
    /** Should we have colored output to the CLI? */
    private static boolean coloredOutput = false;

    /**
     * Go, go, go, ... move it!
     *
     * @param args The command line arguments.
     */
    public static void main(String[] args) {
        // Set the log level to ERROR.
        setLogLevel(1);

        // Parse the command line.
        parseCliOptions(args);

        // Initialize the Guice modules.
        initGuice();

        // Read the JSON schema specifications.
        String jsonSchemaDraft04 = null;
        try {
            jsonSchemaDraft04 = loadJsonSchema(JSON_DRAFT_04);
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }

        // Read the actual file.
        String fileContent = loadAndSanitizeFile(jsonFile);
        if (fileContent == null) {
            System.err.println(getFAILURE() + " Could not read file from path: " + jsonFile);
            System.exit(EXIT_CODE_ERROR);
        }

        String schemaContent;
        // If a schema file was provided, read the schema file. With a lot of error handling.
        if (schemaFile != null) {
            schemaContent = loadAndSanitizeFile(schemaFile);
            if (schemaContent == null) {
                System.err.println(getFAILURE() + " Could not read schema file from path: " + schemaFile);
                System.exit(EXIT_CODE_ERROR);
            }
            if (checkSchema && jsonSchemaDraft04 == null) {
                System.err.println(getFAILURE() + " The default schema file '" + JSON_DRAFT_04 + "' is null.");
                System.exit(EXIT_CODE_ERROR);
            }
            if (checkSchema && !validator.isValid(schemaContent, jsonSchemaDraft04)) {
                System.err.println(getFAILURE() + " The schema file is no valid JSON.");
                System.exit(EXIT_CODE_ERROR);
            }
            // If no schema was provided, but the 's' option was used, just use the default schema.
            // We re-use the checkSchema here to state whether to check the default or not.
        } else if (!checkSchema) {
            if (jsonSchemaDraft04 == null) {
                System.err.println(getFAILURE() + " The default schema file '" + JSON_DRAFT_04 + "' is null.");
                System.exit(EXIT_CODE_ERROR);
            }
            schemaContent = jsonSchemaDraft04;
            // Try to get the schema file from a $schema URL in the json file.
        } else {
            JSONObject jSONObject = new JSONObject(fileContent);
            // If there was an schema key provided explicitly, check ...
            if (!schemaKey.equals(DEFAULT_SCHEMA_KEY) && !jSONObject.has(schemaKey)) {
                System.err.println(getFAILURE() + " The file does not contain the given schema key: " + schemaKey);
                System.exit(EXIT_CODE_ERROR);
            }
            // We re-use schemaFile here to store the URI of the schema.
            schemaFile = jSONObject.get(schemaKey).toString();
            schemaContent = loadAndSanitizeFile(schemaFile);
            if (schemaContent == null) {
                System.err.println(getFAILURE() + " Could not read schema from path: " + schemaFile);
                System.exit(EXIT_CODE_ERROR);
            }
        }

        // Check and output.
        if (validator.isValid(fileContent, schemaContent)) {
            if (schemaFile != null) {
                System.out.println(getSUCCESS() + " The file '" + jsonFile
                        + "' is valid according to the given schema '" + schemaFile + "'.");
            } else {
                System.out.println(getSUCCESS() + " The file '" + jsonFile
                        + "' is valid according to the default schema '" + JSON_DRAFT_04 + "'.");
            }
            System.exit(EXIT_CODE_SUCCESS);
        } else {
            if (schemaFile != null) {
                System.out.println(getFAILURE() + " The file '" + jsonFile + "' does NOT meet the schema '"
                        + schemaFile + "'.");
            } else {
                System.out.println(getFAILURE() + " The file '" + jsonFile + "' does NOT meet the schema '"
                        + JSON_DRAFT_04 + "'.");
            }
            System.exit(EXIT_CODE_ERROR);
        }
    }

    /**
     * Creates the command line options for the
     * program.
     *
     * @return An Options object containing all the command line options of the program.
     */
    private static Options createCliOptions() {
        // A helper option
        Option help = Option.builder("h").longOpt("help").desc("Give this help list.").build();
        // The schema option to provide the JSON/YAML schema file.
        Option schema = Option.builder("s").longOpt("schema").desc("A schema file used to check FILE.").hasArg()
                .argName("SCHEMA_FILE").optionalArg(true).build();
        // The schema option to provide the JSON/YAML schema file.
        Option verbose = Option.builder("v").longOpt("verbose").desc("Increase the verbose level.").build();
        // The schema option to provide the JSON/YAML schema file.
        Option skipSchemaCheck = Option.builder("d").longOpt("skip-schema-check")
                .desc("Skip the schema check. Default is 'false'.").build();
        // The schema option to provide the JSON/YAML schema file.
        Option schemaKey = Option.builder("k").longOpt("schema-key")
                .desc("The key that is used to identify the schema URI in FILE. Default is '$schema'.").hasArg()
                .argName("SCHEMA_KEY").build();
        // The option to have colored output.
        Option coloredOutput = Option.builder("c").longOpt("colored-output")
                .desc("Have colored output, i.e. green for success, and red for errors.").build();

        // Create options.
        Options options = new Options();
        options.addOption(help);
        options.addOption(schema);
        options.addOption(verbose);
        options.addOption(skipSchemaCheck);
        options.addOption(schemaKey);
        options.addOption(coloredOutput);

        // Return options.
        return options;
    }

    /**
     * Parses the command line arguments.
     *
     * @param args The command line arguments.
     */
    private static void parseCliOptions(String[] args) {
        // Command line options.
        Options options = createCliOptions();
        // Command line parser.
        CommandLineParser parser = new DefaultParser();

        try {
            // Parse the command line arguments
            CommandLine line = parser.parse(options, args);

            if (line.hasOption("h")) {
                printHelp(options);
                System.exit(EXIT_CODE_SUCCESS);
            }
            if (line.hasOption("s")) {
                schemaFile = normalizePath(line.getOptionValue('s'));
                if (schemaFile == null)
                    checkSchema = false;
            }
            if (line.hasOption("v")) {
                setLogLevel(2);
            }
            if (line.hasOption("d")) {
                checkSchema = false;
            }
            if (line.hasOption("c")) {
                coloredOutput = true;
            }
            if (line.hasOption("k")) {
                schemaKey = line.getOptionValue('k');
            } else {
                schemaKey = DEFAULT_SCHEMA_KEY;
            }
            // Get whatever ist left, after the options have been processed.
            if (line.getArgList() == null || line.getArgList().isEmpty()) {
                throw new MissingArgumentException("JSON/YAML file to validate is missing.");
            } else {
                jsonFile = normalizePath(line.getArgList().get(0));
            }
        } catch (MissingOptionException | MissingArgumentException e) {
            System.err.println("ERROR: " + e.getMessage() + "\n");
            printHelp(options);
            System.exit(EXIT_CODE_ERROR);
        } catch (ParseException e) {
            // Oops, something went wrong
            System.err.println("ERROR: Parsing failed. Reason: " + e.getMessage());
        }
    }

    /**
     * Reads a file from the given path with a given charset.
     *
     * @param path The path to the file.
     * @return A string that contains the file content.
     */
    private static String readFile(String path) throws IOException {
        return readFile(path, StandardCharsets.UTF_8);
    }

    /**
     * Reads a file from the given path with a given charset.
     *
     * @param path The path to the file.
     * @param encoding The Charset of the file.
     * @return A string that contains the file content.
     */
    private static String readFile(String path, Charset encoding) throws IOException {
        return new String(Files.readAllBytes(Paths.get(path)), encoding);
    }

    /**
     * Reads file from a given URL.
     *
     * @param urlString The URL to the file.
     * @return A string that contains the file content.
     */
    private static String loadFileFromUrl(String urlString) throws IOException {
        URL url = new URL(urlString);
        return IOUtils.toString(url);
    }

    /**
     * Reads a file from the given path and parses its content. It tries
     * to convert the file content to a JSON string.
     *
     * @param uri The URI to identify the JSON file.
     * @return The JSON String.
     */
    private static String loadAndSanitizeFile(@Nonnull String uri) {
        String fileContent;
        // This is needed, since there is not http provider for nio.files yet.
        try {
            if (uri.toLowerCase().startsWith("http://") || uri.toLowerCase().startsWith("https://")) {
                fileContent = loadFileFromUrl(uri);
            } else {
                fileContent = readFile(uri);
            }
        } catch (IOException e) {
            return null;
        }

        // Try to convert to JSON (to make sure we have a JSON string).
        String jsonString = null;
        try {
            jsonString = converter.convertToJson(fileContent);
        } catch (Exception e) {
            System.err.println(getFAILURE() + " Could not parse the file content of: " + uri);
            System.exit(EXIT_CODE_ERROR);
        }

        // Return the sanitized JSON string loaded from the file.
        return jsonString;
    }

    /**
     * Reads the default schema file either from the file system (e.g. in
     * case of unit tests) or the executable JAR file.
     *
     * @param schema The name of the schema file.
     * @return A JSON string representing the schema file.
     * @throws IOException If the schema file cannot be read.
     */
    private static String loadJsonSchema(String schema) throws IOException {
        // Try to read from the file system.
        try {
            return readFile(JSON_DRAFT_PATH + schema);
        } catch (IOException e) {
            if (Logger.isDebugEnabled()) {
                Logger.debug("Cannot read draft schema from file system. Is it a JAR file?\n" + e);
            }
        }

        // Try to read from the JAR file.
        try {
            InputStream is = Main.class.getClassLoader().getResourceAsStream(schema);
            return IOUtils.toString(is, StandardCharsets.UTF_8);
        } catch (IOException e) {
            if (Logger.isDebugEnabled()) {
                Logger.debug("Cannot read draft schema from JAR file.\n" + e);
            }
        }

        throw new IOException("Cannot read the draft file.");
    }

    /**
     * Prints the help of the command.
     *
     * @param options The command's options.
     */
    private static void printHelp(Options options) {
        // A help formatter.
        HelpFormatter formatter = new HelpFormatter();
        // Print help.
        formatter.printHelp(HELP_MSG, options);
    }

    /**
     * Sets the log level.
     *
     * @param level The log level to set.
     */
    private static void setLogLevel(int level) {
        ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory
                .getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
        switch (level) {
        case 3:
            rootLogger.setLevel(Level.DEBUG);
            break;
        case 2:
            rootLogger.setLevel(Level.INFO);
            break;
        case 1:
            rootLogger.setLevel(Level.ERROR);
            break;
        default:
            rootLogger.setLevel(Level.OFF);
        }
    }

    /**
     * Bootstrap the Guice modules.
     */
    private static void initGuice() {
        Injector guiceInjector = Guice.createInjector(new ValidationModule(), new ConversionModule());
        validator = guiceInjector.getInstance(ValidationService.class);
        converter = guiceInjector.getInstance(ConversionService.class);
    }

    /**
     * Produces colored outputs for the shell (bash).
     *
     * @param string The String to colorize.
     * @param color The color to use.
     * @return The colorized string.
     */
    private static String getColoredString(String string, String color) {
        if (coloredOutput)
            return color + string + COLOR_DEFAULT;
        else
            return string;
    }

    /**
     * Convenience method to produce a colorized "FAILURE" string.
     * @return The failure string.
     */
    private static String getFAILURE() {
        return getColoredString("[FAILURE]", COLOR_RED);
    }

    /**
     * Convenience method to produce a colorized "SUCCESS" string.
     * @return The success string.
     */
    private static String getSUCCESS() {
        return getColoredString("[SUCCESS]", COLOR_GREEN);
    }

    /**
     * Normalizes a path.
     *
     * @param path The path to normalize.
     * @return A normalized path string.
     */
    private static String normalizePath(String path) {
        if (path != null) {
            path = Paths.get(path).normalize().toString();
            if (!path.startsWith(File.separator))
                path = "." + File.separator + path;
            return path;
        } else {
            return null;
        }
    }
}