view common/json/src/main/java/com/redhat/thermostat/common/yaml/YamlToJson.java @ 23:ab2706b9b1e3

Add common YAML utilities (The previous commit was missing the new files for this functionality.) This patch adds YAML reading and writing utilities to the base common packages. The intent is we'll be able to compare (at build time) our Swagger API definitions to our Java models (or even build them), or convert the YAML to Java schema for use with the schema validation utilities. Reviewed-by: sgehwolf, neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/024988.html
author stooke@redhat.com
date Fri, 15 Sep 2017 11:41:22 -0400
parents
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.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;

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.io.StringWriter;
import java.io.Writer;
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() {
    }

    public JsonObject yamlToJsonObject(final File fn) throws IOException {
        try (final Reader in = new FileReader(fn)) {
            return yamlToJsonObject(in);
        }
    }

    public JsonObject yamlToJsonObject(final String yaml) throws IOException {
        try (final Reader in = new StringReader(yaml)) {
            return yamlToJsonObject(in);
        }
    }

    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, currentIndent));
                    } 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()) {
                if (line.hasDashPrefix) {
                    JsonArray parray = parent.getAsJsonArray();
                    parray.add(child);
                } else {
                    System.err.println("Array member must begin with dash: " + s);
                }
            } 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;
        }
    }

    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
                    }

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

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

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

    /*
     * utility function to convert YAML file to JSON stdout
     */
    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();
                }
            }
        }
    }
}