view common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombine.java @ 35:76d1ce01cc7a default tip master

Fail build on YAML lint error This patch will cause SwaggerCombine to throw an exception if "--throw" is specified on the command line and there is a lint error in the YAML files. Currently there are no such messages. Reviewed-by: neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-October/025574.html
author Simon Tooke <stooke@redhat.com>
date Tue, 31 Oct 2017 09:49:01 -0400
parents 00540d33ce40
children
line wrap: on
line source

/*
 * Copyright 2012-2017 Red Hat, Inc.
 *
 * This file is part of Thermostat.
 *
 * Thermostat is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation; either version 2, or (at your
 * option) any later version.
 *
 * Thermostat 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 GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Thermostat; see the file COPYING.  If not see
 * <http://www.gnu.org/licenses/>.
 *
 * Linking this code with other modules is making a combined work
 * based on this code.  Thus, the terms and conditions of the GNU
 * General Public License cover the whole combination.
 *
 * As a special exception, the copyright holders of this code give
 * you permission to link this code with independent modules to
 * produce an executable, regardless of the license terms of these
 * independent modules, and to copy and distribute the resulting
 * executable under terms of your choice, provided that you also
 * meet, for each linked independent module, the terms and conditions
 * of the license of that module.  An independent module is a module
 * which is not derived from or based on this code.  If you modify
 * this code, you may extend this exception to your version of the
 * library, but you are not obligated to do so.  If you do not wish
 * to do so, delete this exception statement from your version.
 */

package com.redhat.thermostat.common.swaggercombine;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.Writer;
import java.util.List;
import java.util.Map;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import com.redhat.thermostat.common.json.JsonUtil;
import com.redhat.thermostat.common.yaml.JsonToYaml;
import com.redhat.thermostat.common.yaml.RefResolver;
import com.redhat.thermostat.common.yaml.YamlToJson;

/**
 * Utility to combine multiple Swagger API definitions into one
 */
public class SwaggerCombine {

    private Gson gson = null;
    private Gson prettyGson = null;
    private int lintCount = 0;

    private static final String SWAGGER_TEMPLATE = "/swagger-template.json";

    public SwaggerCombine() {
    }

    /**
     * perform combine, writing output to a Writer
     * @param ctx input context object
     * @param out output writer, to which YAML or JSON is writen depending on the context
     * @return object representing the combined context
     * @throws IOException on i/o or syntax error
     */
    public JsonObject run(final SwaggerCombineContext ctx, Writer out) throws IOException {

        JsonObject root = run(ctx);
        writeObject(root, out, ctx.getFmt() == SwaggerCombineContext.OutputFormat.YAML, ctx.isPretty());
        return root;
    }

    /**
     * perform combine
     * @param ctx input context object
     * @return object representing the combined context
     * @throws IOException on i/o or syntax error
     */
    public JsonObject run(final SwaggerCombineContext ctx) throws IOException {
        return processAPIs(ctx.getAPIList(), ctx.isQuiet(), ctx.isUseTemplate(), ctx.isLint());
    }

    /**
     * return number of lint messages
     * @return number of lint messages
     */
    public int getLintCount() {
        return lintCount;
    }

    private void writeObject(JsonObject root, Writer out, boolean toYaml, boolean pretty) throws IOException {

        if (root != null) {
            if (toYaml) {
                new JsonToYaml().jsonToYaml(root, out);
            } else {
                getGsonInstance(pretty).toJson(root, out);
            }
        }
    }


    JsonObject processAPIs(final List<SwaggerCombineContext.MicroAPI> apiList, boolean quiet, boolean useTemplate, boolean doLint) throws IOException {

        JsonObject root = null;

        if (useTemplate) {
            root = processAPI(null, readSwaggerStream(getClass().getResourceAsStream(SWAGGER_TEMPLATE), false), doLint);
        }
        for (SwaggerCombineContext.MicroAPI apiArg : apiList) {
            final JsonObject json;
            switch (apiArg.getFormat()) {
                case YAML_STRING:
                    final YamlToJson y2j = new YamlToJson();
                    json = y2j.yamlToJsonObject(apiArg.getData());
                    break;
                case JSON_STRING:
                    final JsonParser parser = new JsonParser();
                    json = parser.parse(new BufferedReader(new StringReader(apiArg.getData()))).getAsJsonObject();
                    break;
                case FILE:
                    // read in the Swagger YAML
                    if (!quiet) {
                        info("processing " + apiArg.getInFile());
                    }
                    json = readSwaggerFile(apiArg.getInFile());
                    break;
                default:
                    // just to get rid of compiler warning
                    json = null;
            }
            root = processAPI(root, json, doLint);
        }
        return root;
    }

    private JsonObject processAPI(JsonObject root, final JsonObject apiDef, boolean doLint) throws IOException {

        // see if we should run some tests
        if (doLint && apiDef != null) {
            lint(apiDef);
        }

        // if it's the first, take it directly
        if (root == null) {
            root = apiDef;
        } else {
            combineSwagger(root, apiDef);
        }
        return root;
    }

    private void lint(JsonObject in) {
        // mke a copy because we want to test reference resolving (combineSwagger doesn't want references resolved)
        final JsonObject copy = JsonUtil.deepCopy(in).getAsJsonObject();
        try {
            RefResolver.resolveRefs(in, copy);
        } catch (IOException e) {
            // exceptiuon here means there were issues
            lintCount++;
            System.err.println("lint: " + e.getMessage());
        }
        // TODO: test for mandatory elements
        // TODO: test for unused definitions
    }

    private void combineSwagger(JsonObject root, JsonObject in) {
        // assumes no references are resolved

        /*
         * first, compare header for compatiblity
         *
         * - the info section is probably different (but the license should be the same)
         * - comsumes and produces become unions
         * - basepath should be longest common path but instead we make it '/' and prepend it to every path in the API spec
         */

        // only once, we fix up the root swagger files basePath
        if (! "/".equals(root.get("basePath").getAsString())) {
            prependBasepath(root);
        }
        prependBasepath(in);
        addGently(root, "consumes", in.getAsJsonArray("consumes"));
        addGently(root, "produces", in.getAsJsonArray("produces"));
        addGently(root, "paths", in.getAsJsonObject("paths"));
        addGently(root, "definitions", in.getAsJsonObject("definitions"));
        addGently(root, "parameters", in.getAsJsonObject("parameters"));
    }

    private static class Pair {
        final JsonElement e1;
        final JsonElement e2;
        Pair(JsonElement e1, JsonElement e2) {
            this.e1 = e1;
            this.e2 = e2;
        }
    }

    private Pair compareGently(JsonElement e1, JsonElement e2) {
        if (!e1.getClass().equals(e2.getClass()))
            return new Pair(e1, e2);
        if (e1.isJsonObject()) {
            // compare all elements but ignore "description"
            JsonObject o1 = e1.getAsJsonObject();
            JsonObject o2 = e2.getAsJsonObject();
            // look for keys in o2 but not e1 (flag 'em) or if they both have them ,compare recursively
            for (final Map.Entry<String, JsonElement> entry2 : o2.entrySet()) {
                if (entry2.getKey().equals("description"))
                    continue;
                if (o1.has(entry2.getKey())) {
                    Pair p = compareGently(o1.get(entry2.getKey()), entry2.getValue());
                    if (p != null) {
                        return p;
                    }
                } else {
                    return new Pair(e1, e2);
                }
            }
            // now look for keys in e1 but not e2
            for (final Map.Entry<String, JsonElement> entry3 : o1.entrySet()) {
                if (entry3.getKey().equals("description"))
                    continue;
                if (!o2.has(entry3.getKey())) {
                    return new Pair(e1, e2);
                }
            }
        } else {
            if (!e1.equals(e2)) {
                return new Pair(e1, e2);
            }
        }
        return null;
    }

    private void addGently(JsonObject destroot, String name, JsonObject src) {
        if (src == null) {
            return; // nothing to check
        }
        if (!destroot.has(name)) {
            destroot.add(name, new JsonObject());
        }
        JsonObject dest = destroot.getAsJsonObject(name);
        for (final Map.Entry<String, JsonElement> entry : src.entrySet()) {
            if (entry.getKey() == null) {
                lintCount++;
                error("key is null");
            }
            if (dest.has(entry.getKey())) {
                JsonElement haveAlready = dest.get(entry.getKey());
                Pair pair = compareGently(haveAlready, entry.getValue());
                if (pair != null) {
                    error("key " + entry.getKey() + " differs: " + pair.e1 + " and " + pair.e2);
                }
            } else {
                dest.add(entry.getKey(), entry.getValue());
            }
        }
    }

    private void addGently(JsonObject destroot, String name, JsonArray src) {
        if (src == null) {
            return; // nothing to check
        }
        if (!destroot.has(name)) {
            destroot.add(name, new JsonArray());
        }
        JsonArray dest = destroot.getAsJsonArray(name);
        for (final JsonElement entry : src) {
            if (!dest.contains(entry)) {
                dest.add(entry);
            }
        }
    }

    private void prependBasepath(JsonObject root) {
        final String basePath = root.get("basePath").getAsString();
        prependBasepath(root, basePath);
        root.addProperty("basePath", "/");
    }

    private void prependBasepath(JsonObject root, String basePath) {
        JsonObject originalPaths = root.getAsJsonObject("paths");
        JsonObject paths = JsonUtil.deepCopy(originalPaths).getAsJsonObject();
        for (final Map.Entry<String, JsonElement> entry : paths.entrySet()) {
            originalPaths.remove(entry.getKey());
            originalPaths.add(basePath + entry.getKey(), entry.getValue());
        }
    }

    /**
     * read a JSON or YAML stream into a JsonObject
     * @param in input stream
     * @param isYaml true if YAML format, false if JSON format
     * @return a JsonObject representing the input stream contents
     * @throws IOException if there was an error reading, or an error in format
     */
    public static JsonObject readSwaggerStream(InputStream in, boolean isYaml) throws IOException {
        final JsonObject json;
        if (isYaml) {
            final YamlToJson y2j = new YamlToJson();
            json = y2j.yamlToJsonObject(new BufferedReader(new InputStreamReader(in)));
        } else {
            final JsonParser parser = new JsonParser();
            json = parser.parse(new BufferedReader(new InputStreamReader(in))).getAsJsonObject();
        }
        return json;
    }

    /**
     * read a JSON or YAML file into a JsonObject
     * @param fin input file (must end in ".yaml" or ".json")
     * @return a JsonObject representing the filecontents
     * @throws IOException if there was an error reading, or an error in format
     */
    public static JsonObject readSwaggerFile(File fin) throws IOException {
        final JsonObject json;
        if (fin.getName().endsWith(".yaml")) {
            final YamlToJson y2j = new YamlToJson();
            json = y2j.yamlToJsonObject(fin);
        } else if (fin.getName().endsWith(".json")) {
            final JsonParser parser = new JsonParser();
            json = parser.parse(new BufferedReader(new FileReader(fin))).getAsJsonObject();
        } else {
            // only to clean up warning message - we never get here as we filteres out all invalid filenames earlier
            throw new IOException("file '" + fin + "' has neither .yaml nor .json extension");
        }
        return json;
    }

    private Gson getGsonInstance(boolean isPretty) {
        if (isPretty) {
            if (prettyGson == null) {
                final GsonBuilder gsonBuilder = new GsonBuilder();
                gsonBuilder.setPrettyPrinting();
                prettyGson = gsonBuilder.create();
            }
            return prettyGson;
        } else {
            if (gson == null) {
                gson = new Gson();
            }
            return gson;
        }
    }

    private void info(String msg) {
        System.err.println(msg);
    }

    protected void error(String msg) {
        lintCount++;
        System.err.println("error: " + msg);
    }
}