view common/src/main/java/com/redhat/thermostat/common/yaml/YamlToJson.java @ 28:00540d33ce40 version 0.1.1

Changes to SwaggerCombine for web-gateway schema API This patch modifies the thermostat-common module by upgrading the functionality of SwaggerCombine and the YAML parser (which is NOT a full YAML parser, but only the subset we use) to handle the use cases about to be implemented in the web-gateway. - better parsing of YAML arrays - output and @atfile support in SwaggerCombine - misc bug fixes The version number is also ungraded to 0.1.1 Reviewed-by: sgehwolf Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-October/025364.html
author Simon Tooke <stooke@redhat.com>
date Wed, 18 Oct 2017 10:28:09 -0400
parents 33cf0b946e3d
children
line wrap: on
line source

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

package com.redhat.thermostat.common.yaml;

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.JsonPrimitive;
import com.redhat.thermostat.common.json.JsonUtil;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Utility class to read YAML files and convert to GSON objects
 *
 * Limitations:
 * Only enough code to read Swagger YAML is implemented
 * $ref only works within the same document, not across the network
 */
public class YamlToJson {

    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();
            yamlToJson(pr, obj, 0);
            pr.close();
            return obj;
        }
    }

    private void yamlToJson(PushableReader in, JsonElement parent, int currentIndent) throws IOException {
        for (String s = in.readLine(); s != null; s = in.readLine()) {
            YamlLine line = new YamlLine(s);
            if (line.indent == 0 && line.value == null && line.name == null) {
                // blsnk or unparseable line
                continue;
            }
            if (line.indent < currentIndent) {
                in.push(s);
                return;
            }
            final JsonElement child;
            if (line.hasColon) {
                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, Math.max(currentIndent, line.indent)));
                    } else if (line.value.startsWith("[")) {
                        // [ array elements ] (may be on separate lines
                        in.push(line.value);
                        child = absorbArray(in, line.indent);
                    } else {
                        JsonElement ce = makeUnquotedPrimitive(line.value);
                        if (parent.isJsonArray()) {
                            JsonObject childobj = new JsonObject();
                            childobj.add(makeUnquotedString(line.name), ce);
                            child = childobj;
                        } else {
                            child = ce;
                        }
                    }
                } else {
                    // no value specified; this could be the start of an object or array
                    final String next = in.readLine();
                    boolean isArray = false;
                    if (next != null) {
                        YamlLine nextLine = new YamlLine(next);
                        isArray = nextLine.hasDashPrefix;
                        in.push(next);
                    }
                    child = isArray ? new JsonArray() : new JsonObject();
                    yamlToJson(in, child, line.indent);
                }
            }
            else {
                child = (line.value != null) ? makeUnquotedPrimitive(line.value) : new JsonPrimitive("");
            }
            if (parent.isJsonArray()) {
                JsonArray parray = parent.getAsJsonArray();
                if (line.hasDashPrefix) {
                    parray.add(child);
                } else {
                    // 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();
                pobject.add(makeUnquotedString(line.name), child);
            } else {
                System.err.println("Error: parent is neither object nor array");
            }
            currentIndent = line.indent;
        }
    }

    // allows for arrays on one line:  foo: [one, two, threee]
    private JsonArray absorbArray(PushableReader in, int currentIndent) throws IOException {
        // create an array and read primitives into it until we hit ']'
        final JsonArray array = new JsonArray();
        in.resetTokenizer();
        // skip past '['
        in.nextTokenOnLine();
        String t = in.nextTokenOnLine();
        while (t != null) {
            if ("]".equals(t)) {
                break;
            } else {
                array.add(makeUnquotedPrimitive(t));
            }
            t = in.nextTokenOnLine();
        }
        if (!"]".equals(t)) {
            // read following lines as name value pairs
            for (String s = in.readLine(); s != null; s = in.readLine()) {
                YamlLine line = new YamlLine(s);
                if (line.indent == 0 && line.value == null && line.name == null) {
                    // blsnk or unparseable line
                    continue;
                }
                if (line.indent < currentIndent) {
                    in.push(s);
                    return array;
                }
                if (line.value != null) {
                    if (line.name != null) {
                        JsonObject obj = new JsonObject();
                        obj.add(makeUnquotedString(line.name), makeUnquotedPrimitive(line.value));
                        array.add(obj);
                    } else if ("]".equals(line.value)) {
                        break;
                    } else {
                        array.add(makeUnquotedPrimitive(t));
                    }
                }
            }
        }
        return array;
    }

    private static String makeUnquotedString(final String s) {
        String ret = s;
        if (s.length() > 2) {
            if ((s.charAt(0) == '\'') && (s.charAt(s.length() - 1) == '\'')
                    || (s.charAt(0) == '"') && (s.charAt(s.length() - 1) == '"')) {
                ret = s.substring(1, s.length() - 1);
            }
        }
        return ret;
    }

    private static JsonPrimitive makeUnquotedPrimitive(final String s) {
        final String uqString = makeUnquotedString(s);
        // anything quoted will return a string, otherwise try to parse
        if (uqString.length() == s.length()) {
            if ("true".equals(uqString))
                return new JsonPrimitive(true);
            else if ("false".equals(uqString))
                return new JsonPrimitive(false);
            else if ("null".equals(uqString))
                return null;
            else {
                // test for number
                final char c0 = uqString.charAt(0);
                if (Character.isDigit(c0) || c0 == '+' || c0 == '-') {
                    try {
                        return new JsonPrimitive(new Long(uqString));
                    } catch (NumberFormatException ignored) {
                        // fall through to return String
                    }

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

    private static String absorb(PushableReader in, int currentIndent) throws IOException {
        // append lines until the indentation is the same as the currentIndent
        StringBuilder a = new StringBuilder();
        for (String s = in.readLine(); s != null; s = in.readLine()) {
            int wsCount;
            for (wsCount = 0; wsCount < s.length(); wsCount++) {
                if (!Character.isWhitespace(s.charAt(wsCount))) {
                    break;
                }
            }
            if (wsCount > currentIndent) {
                a.append(a.length() == 0 ? s.trim() : ' ' + s.trim());
            } else {
                in.push(s);
                break;
            }
        }
        return a.toString();
    }

    /**
     * YamlLine - parse a single line of YAML file
     */
    static class YamlLine {
        int indent;
        String name;
        String value;
        boolean hasColon;
        boolean hasDashPrefix;

        private static final String regexpr = "^([\\s]*)([-]?[\\s]*)([^:]+)([:]?)(.*)$";
        private static final Pattern p = Pattern.compile(regexpr);

        YamlLine(String line) throws IOException {
            Matcher m = p.matcher(line);
            if (m.matches()) {
                final String s1 = m.group(1); // leading whitespace
                final String s2 = m.group(2); // optional '-' indicating array element
                final String s3 = m.group(3); // key
                final String s4 = m.group(4); // ':'
                final String s5 = m.group(5).trim(); // value or optional '>-'
                hasColon = !(s4 == null || s4.isEmpty());
                hasDashPrefix = !(s2 == null || s2.isEmpty());
                indent = s1.length() + (hasDashPrefix ? s2.length() : 0);
                if (hasColon) {
                    name = s3.trim();
                    value = s5.trim();
                } else {
                    name = null;
                    value = s3 + s5;
                    value = value.trim();
                }
            } else {
                if (line.isEmpty()) {
                    name = null;
                    value = null;
                    indent = 0;
                } else {
                    throw new IOException("parse error in YAML '" + line + "'");
                }
            }
        }
    }

    static class PushableReader implements AutoCloseable {

        final Stack<String> stack = new Stack<>();
        final BufferedReader in;

        Matcher matcher;
        static final Pattern pattern = Pattern.compile("(\\[|\\]|[\"'\\w]+)");

        PushableReader(BufferedReader in) {
            this.in = in;
        }

        String readLine() throws IOException {
            if (stack.isEmpty()) {
                return in.readLine();
            } else {
                return stack.pop();
            }
        }

        void push(String s) {
            stack.push(s);
        }

        void resetTokenizer() {
            matcher = null;
        }

        @Override
        public void close() throws IOException {
            in.close();
        }

        String nextToken() throws IOException {
            // tokens are separated by space or comma
            if (matcher == null) {
                final String line = readLine();
                matcher = (line != null) ? pattern.matcher(line) : null;
            }
            if (matcher != null) {
                if (matcher.find()) {
                    return matcher.group();
                } else {
                    // ran out of tokens - try next line
                    matcher = null;
                    return nextToken();
                }
            }
            return null;
        }

        String nextTokenOnLine() throws IOException {
            // tokens are separated by space or comma
            if (matcher == null) {
                final String line = readLine();
                matcher = (line != null) ? pattern.matcher(line) : null;
            }
            if (matcher != null) {
                if (matcher.find()) {
                    return matcher.group();
                } else {
                    matcher = null;
                }
            }
            return null;
        }
    }

    /**
     * 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;
        for (final String fn : args) {
            if ("--pretty".equals(fn)) {
                pretty = true;
            } else if ("--help".equals("fn")) {
                System.err.println("YamlToJason [--pretty] [--help] infile1 [infile2] ...");
            } else {
                try {
                    final YamlToJson y2j = new YamlToJson();
                    final File fin = new File(fn);
                    final JsonObject json = y2j.yamlToJsonObject(fin);
                    if (pretty) {
                        final GsonBuilder gsonBuilder = new GsonBuilder();
                        gsonBuilder.setPrettyPrinting();
                        final Gson gson = gsonBuilder.create();
                        System.out.println(gson.toJson(json));
                    } else {
                        System.out.println(json.toString());
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}