/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.sling.servlets.get.impl.util; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.Iterator; import java.util.Map; import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonArrayBuilder; import javax.json.JsonObject; import javax.json.JsonObjectBuilder; import javax.json.JsonString; import javax.json.JsonValue; public class JsonRenderer { /** Rendering options */ static public class Options { int indent; private boolean indentIsPositive; int initialIndent; boolean arraysForChildren; public static final String DEFAULT_CHILDREN_KEY = "__children__"; public static final String DEFAULT_CHILD_NAME_KEY = "__name__"; String childrenKey = DEFAULT_CHILDREN_KEY; String childNameKey = DEFAULT_CHILD_NAME_KEY; /** Clients use JSONRenderer.options() to create objects */ private Options() { } Options(Options opt) { this.indent = opt.indent; this.indentIsPositive = opt.indentIsPositive; this.initialIndent = opt.initialIndent; this.arraysForChildren = opt.arraysForChildren; } public Options withIndent(int n) { indent = n; indentIsPositive = indent > 0; return this; } public Options withInitialIndent(int n) { initialIndent = n; return this; } public Options withArraysForChildren(boolean b) { arraysForChildren = b; return this; } public Options withChildNameKey(String key) { childNameKey = key; return this; } public Options withChildrenKey(String key) { childrenKey = key; return this; } boolean hasIndent() { return indentIsPositive; } } /** Return an Options object with default values */ public Options options() { return new Options(); } /** Write N spaces to sb for indentation */ private void indent(StringBuilder sb, int howMuch) { for (int i=0; i < howMuch; i++) { sb.append(' '); } } /** Render the supplied JSONObject to a String, in * the simplest possible way. */ public String toString(JsonObject jo) { try { StringWriter writer = new StringWriter(); Json.createGenerator(writer).write(jo).close(); return writer.toString(); } catch (Exception e) { return null; } } /** Make a JSON text of the supplied JSONArray. For compactness, no * unnecessary whitespace is added. If it is not possible to produce a * syntactically correct JSON text then null will be returned instead. This * could occur if the array contains an invalid number. * <p>Warning: This method assumes that the data structure is acyclical. * * @return a printable, displayable, transmittable * representation of the array. */ public String toString(JsonArray ja) { try { return '[' + join(ja,",") + ']'; } catch (Exception e) { return null; } } /** Quote the supplied string for JSON */ public String quote(String string) { if (string == null || string.length() == 0) { return "\"\""; } char b; char c = 0; int i; int len = string.length(); StringBuilder sb = new StringBuilder(len + 2); String t; sb.append('"'); for (i = 0; i < len; i += 1) { b = c; c = string.charAt(i); switch (c) { case '\\': case '"': sb.append('\\'); sb.append(c); break; case '/': if (b == '<') { sb.append('\\'); } sb.append(c); break; case '\b': sb.append("\\b"); break; case '\t': sb.append("\\t"); break; case '\n': sb.append("\\n"); break; case '\f': sb.append("\\f"); break; case '\r': sb.append("\\r"); break; default: if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= '\u2000' && c < '\u2100')) { t = "000" + Integer.toHexString(c); sb.append("\\u").append(t.substring(t.length() - 4)); } else { sb.append(c); } } } sb.append('"'); return sb.toString(); } /** Quote the supplied string for JSON, to the supplied Writer */ public void quote(Writer w, String string) throws IOException { w.write(quote(string)); } /** * Make a JSON text of an Object value. * <p> * Warning: This method assumes that the data structure is acyclical. * @param value The value to be serialized. * @return a printable, displayable, transmittable * representation of the object, beginning * with <code>{</code> <small>(left brace)</small> and ending * with <code>}</code> <small>(right brace)</small>. * @throws JSONException If the value is or contains an invalid number. */ public String valueToString(Object value) { // TODO call the other valueToString instead if (value == null || value.equals(null)) { return "null"; } if (value instanceof JsonString) { quote(((JsonString)value).getString()); } if (value instanceof Number) { return numberToString((Number) value); } if (value instanceof Boolean) { return value.toString(); } if (value instanceof JsonObject || value instanceof JsonArray) { StringWriter writer = new StringWriter(); Json.createGenerator(writer).write((JsonValue) value).close(); return writer.toString(); } return quote(value.toString()); } /** Make a JSON String of an Object value, with rendering options * <p> * Warning: This method assumes that the data structure is acyclical. * @param value The value to be serialized. * @return a printable, displayable, transmittable * representation of the object, beginning * with <code>{</code> <small>(left brace)</small> and ending * with <code>}</code> <small>(right brace)</small>. * @throws JSONException If the object contains an invalid number. */ public String valueToString(Object value, Options opt) { if (value == null || value.equals(null)) { return "null"; } if (value instanceof JsonString) { return quote(((JsonString)value).getString()); } if (value instanceof Number) { return numberToString((Number) value); } if (value instanceof Boolean) { return value.toString(); } if (value instanceof JsonObject) { return prettyPrint((JsonObject)value, opt); } if (value instanceof JsonArray) { return prettyPrint((JsonArray)value, opt); } return quote(value.toString()); } /** * Produce a string from a Number. * @param n A Number * @return A String. * @throws JSONException If n is a non-finite number. */ public String numberToString(Number n) { if (n == null) { throw new NullPointerException("Null pointer"); } testNumberValidity(n); // Shave off trailing zeros and decimal point, if possible. String s = n.toString(); if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0) { while (s.endsWith("0")) { s = s.substring(0, s.length() - 1); } if (s.endsWith(".")) { s = s.substring(0, s.length() - 1); } } return s; } /** Decide whether o must be skipped and added to a, when rendering a JSONObject */ private boolean skipChildObject(JsonArrayBuilder a, Options opt, String key, Object value) { if(opt.arraysForChildren && (value instanceof JsonObject)) { JsonObjectBuilder builder = Json.createObjectBuilder(); builder.add(opt.childNameKey, key); for (Map.Entry<String, JsonValue> entry : ((JsonObject) value).entrySet()) { builder.add(entry.getKey(), entry.getValue()); } a.add(builder); return true; } return false; } /** * Make a prettyprinted JSON text of this JSONObject. * <p> * Warning: This method assumes that the data structure is acyclical. * @return a printable, displayable, transmittable * representation of the object, beginning * with <code>{</code> <small>(left brace)</small> and ending * with <code>}</code> <small>(right brace)</small>. * @throws IllegalArgumentException If the object contains an invalid number. */ public String prettyPrint(JsonObject jo, Options opt) { int n = jo.size(); if (n == 0) { return "{}"; } final JsonArrayBuilder children = Json.createArrayBuilder(); Iterator<String> keys = jo.keySet().iterator(); StringBuilder sb = new StringBuilder("{"); int newindent = opt.initialIndent + opt.indent; String o; if (n == 1) { o = keys.next(); final Object v = jo.get(o); if(!skipChildObject(children, opt, o, v)) { sb.append(quote(o)); sb.append(": "); sb.append(valueToString(v, opt)); } } else { while (keys.hasNext()) { o = keys.next(); final Object v = jo.get(o); if(skipChildObject(children, opt, o, v)) { continue; } if (sb.length() > 1) { sb.append(",\n"); } else { sb.append('\n'); } indent(sb, newindent); sb.append(quote(o.toString())); sb.append(": "); sb.append(valueToString(v, options().withIndent(opt.indent).withInitialIndent(newindent))); } if (sb.length() > 1) { sb.append('\n'); indent(sb, newindent); } } /** Render children if any were skipped (in "children in arrays" mode) */ JsonArray childrenArray = children.build(); if(childrenArray.size() > 0) { if (sb.length() > 1) { sb.append(",\n"); } else { sb.append('\n'); } final Options childOpt = new Options(opt); childOpt.withInitialIndent(childOpt.initialIndent + newindent); indent(sb, childOpt.initialIndent); sb.append(quote(opt.childrenKey)).append(":"); sb.append(prettyPrint(childrenArray, childOpt)); } sb.append('}'); return sb.toString(); } /** Pretty-print a JSONArray */ public String prettyPrint(JsonArray ja, Options opt) { int len = ja.size(); if (len == 0) { return "[]"; } int i; StringBuilder sb = new StringBuilder("["); if (len == 1) { sb.append(valueToString(ja.get(0), opt)); } else { final int newindent = opt.initialIndent + opt.indent; if(opt.hasIndent()) { sb.append('\n'); } for (i = 0; i < len; i += 1) { if (i > 0) { sb.append(','); if(opt.hasIndent()) { sb.append('\n'); } } indent(sb, newindent); sb.append(valueToString(ja.get(i), opt)); } if(opt.hasIndent()) { sb.append('\n'); } indent(sb, opt.initialIndent); } sb.append(']'); return sb.toString(); } /** * Throw an exception if the object is an NaN or infinite number. * @param o The object to test. * @throws IllegalArgumentException If o is a non-finite number. */ public void testNumberValidity(Object o) { if (o != null) { if (o instanceof Double) { if (((Double)o).isInfinite() || ((Double)o).isNaN()) { throw new IllegalArgumentException( "JSON does not allow non-finite numbers"); } } else if (o instanceof Float) { if (((Float)o).isInfinite() || ((Float)o).isNaN()) { throw new IllegalArgumentException( "JSON does not allow non-finite numbers."); } } } } /** * Make a string from the contents of this JSONArray. The * <code>separator</code> string is inserted between each element. * Warning: This method assumes that the data structure is acyclical. * @param separator A string that will be inserted between the elements. * @return a string. * @throws JSONException If the array contains an invalid number. */ public String join(JsonArray ja, String separator) { final int len = ja.size(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < len; i += 1) { if (i > 0) { sb.append(separator); } sb.append(valueToString(ja.get(i))); } return sb.toString(); } /** * Write the contents of the supplied JSONObject as JSON text to a writer. * For compactness, no whitespace is added. * <p> * Warning: This method assumes that the data structure is acyclical. * * @return The writer. * @throws IOException */ public Writer write(Writer writer, JsonObject jo) throws IOException{ Json.createGenerator(writer).write(jo).flush(); return writer; } /** * Write the contents of the supplied JSONArray as JSON text to a writer. * For compactness, no whitespace is added. * <p> * Warning: This method assumes that the data structure is acyclical. * * @return The writer. * @throws IOException */ public Writer write(Writer writer, JsonArray ja) throws IOException { Json.createGenerator(writer).write(ja).flush(); return writer; } /** * Produce a string from a double. The string "null" will be returned if * the number is not finite. * @param d A double. * @return A String. */ public String doubleToString(double d) { if (Double.isInfinite(d) || Double.isNaN(d)) { return "null"; } // Shave off trailing zeros and decimal point, if possible. String s = Double.toString(d); if (s.indexOf('.') > 0 && s.indexOf('e') < 0 && s.indexOf('E') < 0) { while (s.endsWith("0")) { s = s.substring(0, s.length() - 1); } if (s.endsWith(".")) { s = s.substring(0, s.length() - 1); } } return s; } }