changeset 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 dd0992bd51aa
children a119766f44bc
files common/pom.xml common/src/main/java/com/redhat/thermostat/common/json/JsonUtil.java common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombine.java common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineContext.java common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineContextBuilder.java common/src/main/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineMain.java common/src/main/java/com/redhat/thermostat/common/yaml/JsonToYaml.java common/src/main/java/com/redhat/thermostat/common/yaml/YamlToJson.java common/src/main/resources/swagger-template.json common/src/test/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineContextTest.java common/src/test/java/com/redhat/thermostat/common/swaggercombine/SwaggerCombineTest.java common/src/test/java/com/redhat/thermostat/common/yaml/JsonToYamlTest.java common/src/test/java/com/redhat/thermostat/common/yaml/RefResolverTest.java common/src/test/java/com/redhat/thermostat/common/yaml/YamlToJsonTest.java
diffstat 14 files changed, 1096 insertions(+), 36 deletions(-) [+]
line wrap: on
line diff
--- 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 @@
             <plugin>
                 <groupId>org.apache.felix</groupId>
                 <artifactId>maven-scr-plugin</artifactId>
+                <version>1.21.0</version>
                 <executions>
                     <execution>
                         <id>generate-scr-scrdescriptor</id>
--- 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<br>
+     * "foo.bar.2.fred" is equivalent to<br>
      * 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<br>
+     * "foo.bar.2.fred" is equivalent to<br>
      * 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<String, JsonElement> entry : src.entrySet()) {
+            if (overwrite || !dest.has(entry.getKey())) {
+                dest.add(entry.getKey(), entry.getValue());
+            }
+        }
+        return dest;
+    }
 }
 
--- /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
+ * <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);
+    }
+}
--- /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
+ * <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 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<JsonObject> 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<JsonObject> getAPIList() {
+        return apiList;
+    }
+
+}
--- /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
+ * <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.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;
+    }
+
+}
--- /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
+ * <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.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();
+        }
+    }
+}
--- 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<String, JsonElement> 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);
             }
         }
     }
--- 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;
--- /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
--- /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
+ * <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 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;
+    }
+}
--- /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
+ * <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 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<JsonObject> toJsonList(File[] files) throws IOException {
+        List<JsonObject> list = new ArrayList<>(files.length);
+        for (File fn : files) {
+            JsonObject spec = SwaggerCombine.readSwaggerFile(fn);
+            list.add(spec);
+        }
+        return list;
+    }
+}
--- 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
--- 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");
--- 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