# HG changeset patch # User stooke@redhat.com # Date 1507732559 14400 # Node ID 33cf0b946e3ddac3bbb898290caaa2c2bf7d9c42 # Parent dd0992bd51aa208c769a1de0a5ec11afe79c7c7e 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 diff -r dd0992bd51aa -r 33cf0b946e3d common/pom.xml --- a/common/pom.xml Fri Sep 29 14:48:00 2017 -0400 +++ b/common/pom.xml Wed Oct 11 10:35:59 2017 -0400 @@ -132,6 +132,7 @@ com.redhat.thermostat.common.json, com.redhat.thermostat.common.json.models, com.redhat.thermostat.common.yaml, + com.redhat.thermostat.common.swaggercombine, com.redhat.thermostat.lang.schema, com.redhat.thermostat.lang.schema.models, com.redhat.thermostat.lang.schema.annotations @@ -148,6 +149,7 @@ org.apache.felix maven-scr-plugin + 1.21.0 generate-scr-scrdescriptor diff -r dd0992bd51aa -r 33cf0b946e3d common/src/main/java/com/redhat/thermostat/common/json/JsonUtil.java --- a/common/src/main/java/com/redhat/thermostat/common/json/JsonUtil.java Fri Sep 29 14:48:00 2017 -0400 +++ b/common/src/main/java/com/redhat/thermostat/common/json/JsonUtil.java Wed Oct 11 10:35:59 2017 -0400 @@ -48,13 +48,13 @@ */ public class JsonUtil { - static String capitalize(final String s) { + public static String capitalize(final String s) { return s.substring(0, 1).toUpperCase() + s.substring(1); } /** - * access a nested JSON object via a path string - * "foo.bar.2.fred" is equivalent to + * access a nested JSON object via a path string
+ * "foo.bar.2.fred" is equivalent to
* root.getAsJsonObject().get("foo").getAsJsonObject().get("bar").getAsJsonArray().get(2).getAsJsonObject().get("fred") * * @param root object to access @@ -62,19 +62,18 @@ * @return element within root */ public static JsonElement fetch(final JsonElement root, final String path) { - String[] pathelements = path.split("[.]"); // NOTE - it would be nice to parse foo[N] to foo.N to allow normal subscripting - return fetch(root, pathelements, 0); + return fetch(root, path, "[.]"); } /** - * access a nexted JSON object via a path string - * "foo.bar.2.fred" is equivalent to + * access a nested JSON object via a path string
+ * "foo.bar.2.fred" is equivalent to
* root.getAsJsonObject().get("foo").getAsJsonObject().get("bar").getAsJsonArray().get(2).getAsJsonObject().get("fred") * * @param root object to access * @param path path into object - * @param delimiter delimiter regex for path antries (default '.') + * @param delimiter delimiter regex for path entries (default '.') * @return element within root */ public static JsonElement fetch(final JsonElement root, final String path, final String delimiter) { @@ -145,5 +144,21 @@ } return result; } + + /** + * add all properties from one object to another object + * @param dest the destination object + * @param src the source object + * @param overwrite if true, overwrite existing elements with same key + * @return dest, now with the union of (dest, src) members + */ + public static JsonObject addAll(JsonObject dest, JsonObject src, boolean overwrite) { + for (Map.Entry entry : src.entrySet()) { + if (overwrite || !dest.has(entry.getKey())) { + dest.add(entry.getKey(), entry.getValue()); + } + } + return dest; + } } diff -r dd0992bd51aa -r 33cf0b946e3d common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombine.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombine.java Wed Oct 11 10:35:59 2017 -0400 @@ -0,0 +1,300 @@ +/* + * 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 + * . + * + * 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 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 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 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 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 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); + } +} diff -r dd0992bd51aa -r 33cf0b946e3d common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineContext.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineContext.java Wed Oct 11 10:35:59 2017 -0400 @@ -0,0 +1,199 @@ +/* + * 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 + * . + * + * 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 com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.redhat.thermostat.common.yaml.YamlToJson; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class to build a SwaggerCombine object + */ +public class SwaggerCombineContext { + + public enum OutputFormat { NONE, YAML, JSON }; + + private OutputFormat fmt = OutputFormat.NONE; + private boolean pretty = false; + private boolean quiet = false; + private boolean useTemplate = false; + private boolean lint = true; + private boolean printUsage = false; + private final List apiList = new ArrayList<>(); + + // potential extra args to allow for when building arg array + private static final int EXTRA_ARG_COUNT = 5; + + + public SwaggerCombineContext() { + } + + /** + * defint the output format + * @param fmt one of OutputFormat.YAML | JSON or NONE + * @return this + */ + public SwaggerCombineContext produce(OutputFormat fmt) { + this.fmt = fmt; + return this; + } + + /** + * set pretty printing on or off + * @param pp true to set pretty printing on + * @return this + */ + public SwaggerCombineContext prettyPrint(boolean pp) { + this.pretty = pp; + return this; + } + + /** + * set quiet mode + * @param pp troue to set quiet mode (no progress messages) + * @return this + */ + public SwaggerCombineContext quiet(boolean pp) { + this.quiet = pp; + return this; + } + + /** + * set initial document to built in template + * @param pp true to use built in template as initial document + * If not used, the root elements (version, title, etc) of the first document specified are used. + * @return this + */ + public SwaggerCombineContext useTemplate(boolean pp) { + this.useTemplate = pp; + return this; + } + + /** + * run basic tests on input documents + * @param pp true to run initial tests + * @return this + */ + public SwaggerCombineContext lint(boolean pp) { + this.lint = pp; + return this; + } + + /** + * add an input file (representing a microAPI defined by Swagger) + * @param fin input micro API definitions + * @return this + * @throws IOException if an I/O error occurred + */ + public SwaggerCombineContext addMicroAPI(File fin) throws IOException { + final JsonObject json = SwaggerCombine.readSwaggerFile(fin); + if (json != null) { + this.getAPIList().add(json); + } + return this; + } + + /** + * add an input file (representing a microAPI defined by Swagger) + * @param spec input micro API definitions + * @param isYaml true if format of input file is YAML, false if gormat is JSON + * @return this + * @throws IOException if an I/O error occurred + */ + public SwaggerCombineContext addMicroAPI(String spec, boolean isYaml) throws IOException { + final JsonObject json; + if (isYaml) { + final YamlToJson y2j = new YamlToJson(); + json = y2j.yamlToJsonObject(spec); + } else { + final JsonParser parser = new JsonParser(); + json = parser.parse(new BufferedReader(new StringReader(spec))).getAsJsonObject(); + } + this.getAPIList().add(json); + return this; + } + + /** + * set quiet mode + * @param pp troue to set quiet mode (no progress messages) + * @return this + */ + public SwaggerCombineContext usage(boolean pp) { + this.printUsage = pp; + return this; + } + + public static int getExtraArgCount() { + return EXTRA_ARG_COUNT; + } + + public OutputFormat getFmt() { + return fmt; + } + + public boolean isPretty() { + return pretty; + } + + public boolean isQuiet() { + return quiet; + } + + public boolean isUseTemplate() { + return useTemplate; + } + + public boolean isLint() { + return lint; + } + + public boolean printUsage() { + return printUsage; + } + + public List getAPIList() { + return apiList; + } + +} diff -r dd0992bd51aa -r 33cf0b946e3d common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineContextBuilder.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineContextBuilder.java Wed Oct 11 10:35:59 2017 -0400 @@ -0,0 +1,72 @@ +/* + * 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 + * . + * + * 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.File; +import java.io.IOException; + +public class SwaggerCombineContextBuilder { + + public SwaggerCombineContext buildContext(String[] args) throws IOException { + + final SwaggerCombineContext ctx = new SwaggerCombineContext(); + for (final String arg : args) { + if ("--quiet".equals(arg)) { + ctx.quiet(true); + } else if ("--pretty".equals(arg)) { + ctx.prettyPrint(true); + } else if ("--json".equals(arg)) { + ctx.produce(SwaggerCombineContext.OutputFormat.JSON); + } else if ("--yaml".equals(arg)) { + ctx.produce(SwaggerCombineContext.OutputFormat.YAML); + } else if ("--help".equals(arg)) { + ctx.usage(true); + } else if ("--lint".equals(arg)) { + ctx.lint(true); + } else if ("--use-template".equals(arg)) { + ctx.useTemplate(true); + } else if (arg.endsWith(".yaml") || arg.endsWith(".json")) { + ctx.addMicroAPI(new File(arg)); + } else { + System.err.println("invalid argument: " + arg); + return null; + } + } + return ctx; + } + +} diff -r dd0992bd51aa -r 33cf0b946e3d common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineMain.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineMain.java Wed Oct 11 10:35:59 2017 -0400 @@ -0,0 +1,66 @@ +/* + * 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 + * . + * + * 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.BufferedWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; + +public class SwaggerCombineMain { + + /** + * command line Main method + * @param args command line arguments + * [--yaml] [--json [--pretty]] [--lint] [--use-template] [--quiet] [--help] infile1 [infile2] ... + */ + public static void main(String[] args) { + + try { + final SwaggerCombine combine = new SwaggerCombine(); + final SwaggerCombineContext ctx = new SwaggerCombineContextBuilder().buildContext(args); + if (ctx.printUsage()) { + System.err.println("SwaggerCombine [--yaml] [--json [--pretty]] [--lint] [--use-template] [--quiet] [--help] infile1 [infile2] ..."); + } else { + final Writer out = new BufferedWriter(new PrintWriter(System.out)); + combine.run(ctx, out); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff -r dd0992bd51aa -r 33cf0b946e3d common/src/main/java/com/redhat/thermostat/common/yaml/JsonToYaml.java --- a/common/src/main/java/com/redhat/thermostat/common/yaml/JsonToYaml.java Fri Sep 29 14:48:00 2017 -0400 +++ b/common/src/main/java/com/redhat/thermostat/common/yaml/JsonToYaml.java Wed Oct 11 10:35:59 2017 -0400 @@ -54,19 +54,35 @@ private Writer out; private int indent = 2; - public void setIndentation(int i) { - this.indent = i; - } - + /** + * Convert JSON object to YAML string + * @param root object to convert to YAML + * @return the YAML string + * @throws IOException in the unlikely event of a I/O error + */ String jsonToYamlString(JsonObject root) throws IOException { this.out = new StringWriter(); jsonToYaml(root, out); return out.toString(); } + /** + * Convert and write JSON object to YAML + * @param root object to convert to YAML + * @param out Writer to write YAML to + * @throws IOException in the event of a I/O error + */ public void jsonToYaml(JsonObject root, Writer out) throws IOException { this.out = out; - jsonToYaml(root, 0); + jsonToYaml(root, 0, false); + } + + /** + * set the indentation for YAML output + * @param indent the number of spaces to indent nested elements by + */ + public void setIndentation(int indent) { + this.indent = indent; } private void dispatch(JsonElement el, int currentIndent) throws IOException { @@ -75,38 +91,44 @@ if (pr.isBoolean() || pr.isNumber()) { out.append(el.getAsString()).append('\n'); } else { - out.append("'").append(el.getAsString()).append("'\n"); + String escapedString = el.getAsString().replace("'", "''"); + out.append("'").append(escapedString).append("'\n"); } } else if (el.isJsonArray()) { out.append('\n'); jsonToYaml(el.getAsJsonArray(), currentIndent); } else if (el.isJsonObject()) { out.append('\n'); - jsonToYaml(el.getAsJsonObject(), currentIndent); + jsonToYaml(el.getAsJsonObject(), currentIndent, false); } else { throw new UnsupportedOperationException("Unsupported element: " + el); } } - private void jsonToYaml(JsonObject obj, int currentIndent) throws IOException { + private void jsonToYaml(JsonObject obj, int currentIndent, boolean skipFirstIndent) throws IOException { final int newindent = currentIndent + indent; + boolean isFirst = true; for (final Map.Entry entry : obj.entrySet()) { - doIndent(currentIndent); + if (!(isFirst && skipFirstIndent)) { + doIndent(currentIndent); + } out.append(entry.getKey()).append(": "); dispatch(entry.getValue(), newindent); + isFirst = false; } } private void jsonToYaml(JsonArray array, int currentIndent) throws IOException { + final int newindent = currentIndent + indent; for (final JsonElement el : array) { doIndent(currentIndent); out.append("- "); if (el.isJsonArray()) { - jsonToYaml(el.getAsJsonArray(), 0); + jsonToYaml(el.getAsJsonArray(), newindent); } else if (el.isJsonObject()) { - jsonToYaml(el.getAsJsonObject(), 0); + jsonToYaml(el.getAsJsonObject(), newindent, true); } else { - dispatch(el, 0); + dispatch(el, newindent); } } } diff -r dd0992bd51aa -r 33cf0b946e3d common/src/main/java/com/redhat/thermostat/common/yaml/YamlToJson.java --- a/common/src/main/java/com/redhat/thermostat/common/yaml/YamlToJson.java Fri Sep 29 14:48:00 2017 -0400 +++ b/common/src/main/java/com/redhat/thermostat/common/yaml/YamlToJson.java Wed Oct 11 10:35:59 2017 -0400 @@ -40,9 +40,9 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; -import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import com.redhat.thermostat.common.json.JsonUtil; import java.io.BufferedReader; import java.io.File; @@ -68,18 +68,36 @@ public YamlToJson() { } + /** + * read YAML file and convert to JSON object + * @param fn input file + * @return output JSON object + * @throws IOException if a syntax error, or an I/O error + */ public JsonObject yamlToJsonObject(final File fn) throws IOException { try (final Reader in = new FileReader(fn)) { return yamlToJsonObject(in); } } + /** + * parse YAML string to JSON object + * @param yaml YAML string to parse + * @return output JSON object + * @throws IOException if an I/O error + */ public JsonObject yamlToJsonObject(final String yaml) throws IOException { try (final Reader in = new StringReader(yaml)) { return yamlToJsonObject(in); } } + /** + * read YAML and convert to JSON object + * @param in Reader providing YAML input + * @return output JSON object + * @throws IOException if a syntax error, or an I/O error + */ public JsonObject yamlToJsonObject(final Reader in) throws IOException { try (final PushableReader pr = new PushableReader(in instanceof BufferedReader ? (BufferedReader)in : new BufferedReader(in));) { final JsonObject obj = new JsonObject(); @@ -105,7 +123,7 @@ if (line.value != null && !line.value.isEmpty()) { if (">-".equals(line.value)) { //the next few lines are the value for the current line - child = makeUnquotedPrimitive(absorb(in, currentIndent)); + child = makeUnquotedPrimitive(absorb(in, Integer.max(currentIndent, line.indent))); } else { JsonElement ce = makeUnquotedPrimitive(line.value); if (parent.isJsonArray()) { @@ -133,11 +151,27 @@ child = (line.value != null) ? makeUnquotedPrimitive(line.value) : new JsonPrimitive(""); } if (parent.isJsonArray()) { + JsonArray parray = parent.getAsJsonArray(); if (line.hasDashPrefix) { - JsonArray parray = parent.getAsJsonArray(); parray.add(child); } else { - System.err.println("Array member must begin with dash: " + s); + // the current element should be an object + JsonElement currentArrayElement = parray.get(parray.size() - 1); + if (currentArrayElement.isJsonPrimitive()) { + System.err.println("Array member is being treated as object and primitive"); + } else if (currentArrayElement.isJsonObject()) { + if (child.isJsonPrimitive()) { + currentArrayElement.getAsJsonObject().add(makeUnquotedString(line.name), child); + } else if (child.isJsonObject()) { + if (line.value.isEmpty()) { + currentArrayElement.getAsJsonObject().add(makeUnquotedString(line.name), child); + } else { + JsonUtil.addAll(currentArrayElement.getAsJsonObject(), child.getAsJsonObject(), true); + } + } + } else if (currentArrayElement.isJsonArray()) { + currentArrayElement.getAsJsonArray().add(child); + } } } else if (parent.isJsonObject()) { JsonObject pobject = parent.getAsJsonObject(); @@ -182,7 +216,14 @@ } } + } else if (s.length() >= 4) { + // avoid strings that aren't long enough to have surrounding quotes plus escaped quote + // convert '' to ' in single quoted strings, "" to " in double quoted strings + final char quotechar = s.charAt(0); + String ns = uqString.replaceAll("[" + quotechar + "][" + quotechar + "]", "" + quotechar); + return new JsonPrimitive(ns); } + return new JsonPrimitive(uqString); } @@ -277,8 +318,9 @@ } } - /* - * utility function to convert YAML file to JSON stdout + /** + * command line utility to convert YAML to JSON + * @param args command line arguments [--pretty] [--help] infile1 [infile2] ... */ public static void main(String[] args) { boolean pretty = false; diff -r dd0992bd51aa -r 33cf0b946e3d common/src/main/resources/swagger-template.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/src/main/resources/swagger-template.json Wed Oct 11 10:35:59 2017 -0400 @@ -0,0 +1,110 @@ +{ + "swagger": "2.0", + "info": { + "version": "0.0.1", + "title": "Thermostat Combined API", + "license": { + "name": "GPL v2 with Classpath Exception", + "url": "http://www.gnu.org/licenses" + } + }, + "consumes": [ + "application/json" + ], + "produces": [ + "application/json", + "text/html; charset=utf-8" + ], + "basePath": "/", + "paths": { + }, + "definitions": { + "environment-items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "metric": { + "type": "object", + "properties": { + "$numberLong": { + "type": "string" + } + } + } + }, + "parameters": { + "system-id": { + "name": "systemId", + "in": "path", + "required": true, + "type": "string", + "description": "The system ID for the current request." + }, + "jvm-id": { + "name": "jvmId", + "in": "path", + "required": true, + "type": "string", + "description": "The JVM ID for the current request." + }, + "timestamp": { + "name": "timeStamp", + "in": "path", + "required": true, + "type": "integer", + "format": "int64", + "description": "The UNIX timestamp in milliseconds to set the last_updated field for." + }, + "limit": { + "name": "limit", + "in": "query", + "description": "Maximum number of items to return. Example '1'", + "type": "integer", + "required": false, + "default": 1 + }, + "offset": { + "name": "offset", + "in": "query", + "description": "Offset of first item to return. Example '0'", + "type": "integer", + "required": false, + "default": 0 + }, + "sort": { + "name": "sort", + "in": "query", + "description": "Sort string. Comma separated list of fields prefixed with '+' for ascending or '-' for descending. Example '?sort=+a,-b' Fields use dot notation for embedded documents. Example 'outer.inner' refers to field inner contained in field outer.", + "type": "string", + "required": false + }, + "query": { + "name": "query", + "in": "query", + "description": "Query string. Comma separated list of key, comparator, value pairs. Comparator supports '==', '<=', '>=', '<', '>', '!='. Example '?query=a==b,c!=d'. Keys are fields in documents and use dot notation for embedded documents. Example 'outer.inner' refers to field inner contained in field outer.", + "type": "string", + "required": false + }, + "include": { + "name": "include", + "in": "query", + "description": "Inclusion string. Comma separated list of fields to include in the response. Example '?include=a,b' Fields use dot notation for embedded documents. Example 'outer.inner' refers to field inner contained in field outer. Cannot be used in combination with 'exclude' parameter Overriden by 'exclude' parameter", + "type": "string", + "required": false + }, + "exclude": { + "name": "exclude", + "in": "query", + "description": "Exclusion string. Comma separated list of fields to exclude in the response. Example '?exclude=a,b' Fields use dot notation for embedded documents. Example 'outer.inner' refers to field inner contained in field outer. Cannot be used in combination with 'include' parameter; takes precedence over 'include' parameter", + "type": "string", + "required": false + } + } +} \ No newline at end of file diff -r dd0992bd51aa -r 33cf0b946e3d common/src/test/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineContextTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/src/test/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineContextTest.java Wed Oct 11 10:35:59 2017 -0400 @@ -0,0 +1,134 @@ +/* + * 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 + * . + * + * 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 com.redhat.thermostat.common.swaggercombine.SwaggerCombineContext; +import org.junit.Test; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class SwaggerCombineContextTest { + + private static String SOME_INVALID_FILE = "/some/file/name"; + private static String SOME_MISSING_FILE = "/some_missing_file.yaml"; + private static final String SOME_VALID_FILE = "/systems-swagger.yaml"; + @Test + public void testQuiet() { + final SwaggerCombineContext ctx = new SwaggerCombineContext(); + ctx.quiet(true); + assertTrue(ctx.isQuiet()); + ctx.quiet(false); + assertFalse(ctx.isQuiet()); + } + + @Test + public void testUseTemplate() { + final SwaggerCombineContext ctx = new SwaggerCombineContext(); + ctx.useTemplate(true); + assertTrue(ctx.isUseTemplate()); + ctx.useTemplate(false); + assertFalse(ctx.isUseTemplate()); + } + + @Test + public void testPretty() { + final SwaggerCombineContext ctx = new SwaggerCombineContext(); + ctx.prettyPrint(true); + assertTrue(ctx.isPretty()); + ctx.prettyPrint(false); + assertFalse(ctx.isPretty()); + } + + @Test + public void testLint() { + final SwaggerCombineContext ctx = new SwaggerCombineContext(); + ctx.lint(true); + assertTrue(ctx.isLint()); + ctx.lint(false); + assertFalse(ctx.isLint()); + } + + @Test + public void testFormat() { + final SwaggerCombineContext ctx = new SwaggerCombineContext(); + assertEquals(SwaggerCombineContext.OutputFormat.NONE, ctx.getFmt()); + ctx.produce(SwaggerCombineContext.OutputFormat.JSON); + assertEquals(SwaggerCombineContext.OutputFormat.JSON, ctx.getFmt()); + ctx.produce(SwaggerCombineContext.OutputFormat.YAML); + assertEquals(SwaggerCombineContext.OutputFormat.YAML, ctx.getFmt()); + } + + @Test(expected = IOException.class) + public void testInvalidFiles() throws IOException { + final SwaggerCombineContext ctx = new SwaggerCombineContext(); + assertEquals(0, ctx.getAPIList().size()); + ctx.addMicroAPI(new File(SOME_INVALID_FILE)); + fail(); + } + + @Test(expected = FileNotFoundException.class) + public void testMissingFiles() throws IOException { + final SwaggerCombineContext ctx = new SwaggerCombineContext(); + assertEquals(0, ctx.getAPIList().size()); + ctx.addMicroAPI(new File(SOME_MISSING_FILE)); + fail(); + } + + @Test + public void testFiles() throws IOException { + final SwaggerCombineContext ctx = new SwaggerCombineContext(); + assertEquals(0, ctx.getAPIList().size()); + ctx.addMicroAPI(new File(getClass().getResource(SOME_VALID_FILE).getFile())); + assertEquals(1, ctx.getAPIList().size()); + } + + private boolean contains(String[] array, String s) { + for (final String s2 : array) { + if (s2.equals(s)) { + return true; + } + } + return false; + } +} diff -r dd0992bd51aa -r 33cf0b946e3d common/src/test/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/src/test/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineTest.java Wed Oct 11 10:35:59 2017 -0400 @@ -0,0 +1,92 @@ +/* + * 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 + * . + * + * 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 com.google.gson.JsonObject; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertTrue; + +public class SwaggerCombineTest { + + private static final String API1_YAML_FILE = "/systems-swagger.yaml"; + private static final String API2_YAML_FILE = "/jvms-swagger.yaml"; + + @Test + public void testCombine2() throws IOException { + final File yamlFile1 = new File(getClass().getResource(API1_YAML_FILE).getFile()); + final File yamlFile2 = new File(getClass().getResource(API2_YAML_FILE).getFile()); + final File[] args = { yamlFile1, yamlFile2 }; + // combines two swagger definitions, and tests they actually have definitions for paths from each + final SwaggerCombine sc = new SwaggerCombine(); + JsonObject combined = sc.processAPIs(toJsonList(args), true, false, false); + assertTrue(combined.has("paths")); + JsonObject paths = combined.getAsJsonObject("paths"); + assertTrue(paths.has("/jvms/0.0.1/systems/{systemId}")); + assertTrue(paths.has("/systems/0.0.1/systems/{systemId}")); + } + + @Test + public void testDiff() throws IOException { + final File yamlFile1 = new File(getClass().getResource(API1_YAML_FILE).getFile()); + final File yamlFile2 = new File(getClass().getResource(API2_YAML_FILE).getFile()); + final File[] args = { yamlFile1, yamlFile2 }; + // combines two swaggewr definitions, and tests the 'offset' key differs (in production it should always be optional) + final StringBuffer buf = new StringBuffer(); + final SwaggerCombine sc = new SwaggerCombine() { + protected void error(String msg) { + buf.append(msg); + } + }; + sc.processAPIs(toJsonList(args), true, false, false); + assertTrue(buf.toString().contains("key offset differs")); + } + + private List toJsonList(File[] files) throws IOException { + List list = new ArrayList<>(files.length); + for (File fn : files) { + JsonObject spec = SwaggerCombine.readSwaggerFile(fn); + list.add(spec); + } + return list; + } +} diff -r dd0992bd51aa -r 33cf0b946e3d common/src/test/java/com/redhat/thermostat/common/yaml/JsonToYamlTest.java --- a/common/src/test/java/com/redhat/thermostat/common/yaml/JsonToYamlTest.java Fri Sep 29 14:48:00 2017 -0400 +++ b/common/src/test/java/com/redhat/thermostat/common/yaml/JsonToYamlTest.java Wed Oct 11 10:35:59 2017 -0400 @@ -50,20 +50,23 @@ import java.io.IOException; import java.io.Reader; import java.io.Writer; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; import static org.junit.Assert.assertEquals; public class JsonToYamlTest { - private static final File API1_YAML_FILE = new File("./src/test/resources/systems-swagger.yaml"); - private static final File API1_JSON_FILE = new File("./src/test/resources/systems-swagger.json"); + private static final String API1_YAML_FILE = "/systems-swagger.yaml"; + private static final String API1_JSON_FILE = "/systems-swagger.json"; @Test - public void testConvert() throws IOException { + public void testConvert() throws IOException, URISyntaxException { // read source JSON file - JsonObject goodjson = readFileToJson(API1_JSON_FILE).getAsJsonObject(); + JsonObject goodjson = readFileToJson(new File(getClass().getResource(API1_JSON_FILE).getFile())).getAsJsonObject(); final JsonToYaml j2y = new JsonToYaml(); @@ -78,7 +81,7 @@ // read the known good YAML file final YamlToJson y2j = new YamlToJson(); - final JsonObject goodyaml = y2j.yamlToJsonObject(API1_YAML_FILE); + final JsonObject goodyaml = y2j.yamlToJsonObject(new File(getClass().getResource(API1_YAML_FILE).getFile())); assertEquals(goodjson, goodyaml); // read the new YAML file diff -r dd0992bd51aa -r 33cf0b946e3d common/src/test/java/com/redhat/thermostat/common/yaml/RefResolverTest.java --- a/common/src/test/java/com/redhat/thermostat/common/yaml/RefResolverTest.java Fri Sep 29 14:48:00 2017 -0400 +++ b/common/src/test/java/com/redhat/thermostat/common/yaml/RefResolverTest.java Wed Oct 11 10:35:59 2017 -0400 @@ -50,12 +50,13 @@ public class RefResolverTest { - private static final File API1_YAML_FILE = new File("./src/test/resources/systems-swagger.yaml"); + private static final String API1_YAML_FILE = "/systems-swagger.yaml"; @Test public void resolverTest() throws IOException { YamlToJson y2j = new YamlToJson(); - JsonObject yaml = y2j.yamlToJsonObject(API1_YAML_FILE); + File yamlFile = new File(getClass().getResource(API1_YAML_FILE).getFile()); + JsonObject yaml = y2j.yamlToJsonObject(yamlFile); assertNotNull(yaml); JsonElement getRefType = JsonUtil.fetch(yaml, "paths./systems/{systemId}.get.responses.200.schema.$ref"); diff -r dd0992bd51aa -r 33cf0b946e3d common/src/test/java/com/redhat/thermostat/common/yaml/YamlToJsonTest.java --- a/common/src/test/java/com/redhat/thermostat/common/yaml/YamlToJsonTest.java Fri Sep 29 14:48:00 2017 -0400 +++ b/common/src/test/java/com/redhat/thermostat/common/yaml/YamlToJsonTest.java Wed Oct 11 10:35:59 2017 -0400 @@ -52,12 +52,13 @@ public class YamlToJsonTest { - private static final File API1_YAML_FILE = new File("./src/test/resources/systems-swagger.yaml"); + private static final String API1_YAML_FILE = "/systems-swagger.yaml"; @Test public void readYamlTest() throws IOException { + final File yamlFile1 = new File(getClass().getResource(API1_YAML_FILE).getFile()); YamlToJson y2j = new YamlToJson(); - JsonObject yaml = y2j.yamlToJsonObject(API1_YAML_FILE); + JsonObject yaml = y2j.yamlToJsonObject(yamlFile1); assertNotNull(yaml); JsonElement getDescr = JsonUtil.fetch(yaml, "paths./systems/{systemId}.get.description"); assertNotNull(getDescr); @@ -68,7 +69,8 @@ @Test public void yamlToModelTest() throws IOException { YamlToJson y2j = new YamlToJson(); - JsonObject yaml = y2j.yamlToJsonObject(API1_YAML_FILE); + final File yamlFile = new File(getClass().getResource(API1_YAML_FILE).getFile()); + JsonObject yaml = y2j.yamlToJsonObject(yamlFile); assertNotNull(yaml); // resolve all refs