view common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombine.java @ 26:33cf0b946e3d

Add SwaggerCombine utility This patch adds a new utility class SwaggerCombine, that, given a list of Swagger files, will create one massive Swagger file. Any differing elements that do not have the key "description" are flagged - descriptions are allow to differ. The basic usage is: SwaggerCombine [--yaml|--json] [--pretty] [--quiet] infile1 [infile2]... --yaml or --json write the combined file to stdout, --pretty indents JSON, and --quiet omitsprocessing messages. The default is to combine all files and only output error and processing messages. Reviewed-by: sgehwolf Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/025103.html Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-October/025262.html
author stooke@redhat.com
date Wed, 11 Oct 2017 10:35:59 -0400
parents
children 00540d33ce40
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.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 static final String SWAGGER_TEMPLATE = "/swagger-template.json";

    public SwaggerCombine() {
    }

    public JsonObject run(final SwaggerCombineContext ctx, Writer out) throws IOException {

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

    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);
            }
        }
    }

    private JsonObject processAPIs(final SwaggerCombineContext ctx) throws IOException {
        return processAPIs(ctx.getAPIList(), ctx.isQuiet(), ctx.isUseTemplate(), ctx.isLint());
    }

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

        JsonObject root = null;

        if (useTemplate) {
            final File templateFile = new File(getClass().getResource(SWAGGER_TEMPLATE).getFile());
            root = processAPI(null, readSwaggerFile(templateFile), doLint);
        }
        for (JsonObject apiDef : apiList) {
            // read in the Swagger YAML
            if (!quiet) {
                info("processing " + apiDef.get(""));
            }
            root = processAPI(root, apiDef, 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
            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) {
                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());
        }
    }

    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) {
        System.err.println("error: " + msg);
    }
}