/******************************************************************************* * Copyright 2011 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package com.badlogic.gdx.utils; import java.io.IOException; import java.io.Writer; import java.math.BigDecimal; import java.math.BigInteger; import java.util.regex.Pattern; /** Builder style API for emitting JSON. * @author Nathan Sweet */ public class JsonWriter extends Writer { final Writer writer; private final Array<JsonObject> stack = new Array(); private JsonObject current; private boolean named; private OutputType outputType = OutputType.json; private boolean quoteLongValues = false; public JsonWriter (Writer writer) { this.writer = writer; } public Writer getWriter () { return writer; } /** Sets the type of JSON output. Default is {@link OutputType#minimal}. */ public void setOutputType (OutputType outputType) { this.outputType = outputType; } /** When true, quotes long, double, BigInteger, BigDecimal types to prevent truncation in languages like JavaScript and PHP. * This is not necessary when using libgdx, which handles these types without truncation. Default is false. */ public void setQuoteLongValues (boolean quoteLongValues) { this.quoteLongValues = quoteLongValues; } public JsonWriter name (String name) throws IOException { if (current == null || current.array) throw new IllegalStateException("Current item must be an object."); if (!current.needsComma) current.needsComma = true; else writer.write(','); writer.write(outputType.quoteName(name)); writer.write(':'); named = true; return this; } public JsonWriter object () throws IOException { requireCommaOrName(); stack.add(current = new JsonObject(false)); return this; } public JsonWriter array () throws IOException { requireCommaOrName(); stack.add(current = new JsonObject(true)); return this; } public JsonWriter value (Object value) throws IOException { if (quoteLongValues && (value instanceof Long || value instanceof Double || value instanceof BigDecimal || value instanceof BigInteger)) { value = value.toString(); } else if (value instanceof Number) { Number number = (Number)value; long longValue = number.longValue(); if (number.doubleValue() == longValue) value = longValue; } requireCommaOrName(); writer.write(outputType.quoteValue(value)); return this; } /** Writes the specified JSON value, without quoting or escaping. */ public JsonWriter json (String json) throws IOException { requireCommaOrName(); writer.write(json); return this; } private void requireCommaOrName () throws IOException { if (current == null) return; if (current.array) { if (!current.needsComma) current.needsComma = true; else writer.write(','); } else { if (!named) throw new IllegalStateException("Name must be set."); named = false; } } public JsonWriter object (String name) throws IOException { return name(name).object(); } public JsonWriter array (String name) throws IOException { return name(name).array(); } public JsonWriter set (String name, Object value) throws IOException { return name(name).value(value); } /** Writes the specified JSON value, without quoting or escaping. */ public JsonWriter json (String name, String json) throws IOException { return name(name).json(json); } public JsonWriter pop () throws IOException { if (named) throw new IllegalStateException("Expected an object, array, or value since a name was set."); stack.pop().close(); current = stack.size == 0 ? null : stack.peek(); return this; } public void write (char[] cbuf, int off, int len) throws IOException { writer.write(cbuf, off, len); } public void flush () throws IOException { writer.flush(); } public void close () throws IOException { while (stack.size > 0) pop(); writer.close(); } private class JsonObject { final boolean array; boolean needsComma; JsonObject (boolean array) throws IOException { this.array = array; writer.write(array ? '[' : '{'); } void close () throws IOException { writer.write(array ? ']' : '}'); } } static public enum OutputType { /** Normal JSON, with all its double quotes. */ json, /** Like JSON, but names are only double quoted if necessary. */ javascript, /** Like JSON, but: * <ul> * <li>Names only require double quotes if they start with <code>space</code> or any of <code>":,}/</code> or they contain * <code>//</code> or <code>/*</code> or <code>:</code>. * <li>Values only require double quotes if they start with <code>space</code> or any of <code>":,{[]/</code> or they * contain <code>//</code> or <code>/*</code> or any of <code>}],</code> or they are equal to <code>true</code>, * <code>false</code> , or <code>null</code>. * <li>Newlines are treated as commas, making commas optional in many cases. * <li>C style comments may be used: <code>//...</code> or <code>/*...*<b></b>/</code> * </ul> */ minimal; static private Pattern javascriptPattern = Pattern.compile("^[a-zA-Z_$][a-zA-Z_$0-9]*$"); static private Pattern minimalNamePattern = Pattern.compile("^[^\":,}/ ][^:]*$"); static private Pattern minimalValuePattern = Pattern.compile("^[^\":,{\\[\\]/ ][^}\\],]*$"); public String quoteValue (Object value) { if (value == null) return "null"; String string = value.toString(); if (value instanceof Number || value instanceof Boolean) return string; StringBuilder buffer = new StringBuilder(string); buffer.replace('\\', "\\\\").replace('\r', "\\r").replace('\n', "\\n").replace('\t', "\\t"); if (this == OutputType.minimal && !string.equals("true") && !string.equals("false") && !string.equals("null") && !string.contains("//") && !string.contains("/*")) { int length = buffer.length(); if (length > 0 && buffer.charAt(length - 1) != ' ' && minimalValuePattern.matcher(buffer).matches()) return buffer.toString(); } return '"' + buffer.replace('"', "\\\"").toString() + '"'; } public String quoteName (String value) { StringBuilder buffer = new StringBuilder(value); buffer.replace('\\', "\\\\").replace('\r', "\\r").replace('\n', "\\n").replace('\t', "\\t"); switch (this) { case minimal: if (!value.contains("//") && !value.contains("/*") && minimalNamePattern.matcher(buffer).matches()) return buffer.toString(); case javascript: if (javascriptPattern.matcher(buffer).matches()) return buffer.toString(); } return '"' + buffer.replace('"', "\\\"").toString() + '"'; } } }