/* * 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.commons.json.io; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.apache.sling.commons.json.JSONArray; import org.apache.sling.commons.json.JSONException; import org.apache.sling.commons.json.JSONObject; import org.apache.sling.commons.json.JSONString; /** Various JSON-to-String primitives, used by other classes * when outputting/formatting JSON. * * Streaming variants of some methods are provided. * The existing code in this module is often not streaming, but * we should write newer code using streams, as much as * possible. */ @Deprecated 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; } } /** JSONObject that has a name - overrides just what we * need for our rendering purposes. */ static private class NamedJSONObject extends JSONObject { final String name; final JSONObject jsonObject; final String nameKey; final List<String> keysWithName; NamedJSONObject(String name, JSONObject jsonObject, Options opt) { this.name = name; this.jsonObject = jsonObject; this.nameKey = opt.childNameKey; keysWithName = new ArrayList<String>(); keysWithName.add(nameKey); final Iterator<String> it = jsonObject.keys(); while(it.hasNext()) { keysWithName.add(it.next()); } } @Override public int length() { return keysWithName.size(); } @Override public Object get(String key) throws JSONException { if(key.equals(nameKey)) { return name; } return jsonObject.get(key); } @Override public Iterator<String> keys() { return keysWithName.iterator(); } } /** 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 { final Iterator<String> keys = jo.keys(); final StringBuffer sb = new StringBuffer("{"); while (keys.hasNext()) { if (sb.length() > 1) { sb.append(','); } String o = keys.next(); sb.append(quote(o)); sb.append(':'); sb.append(valueToString(jo.opt(o))); } sb.append('}'); return sb.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. If the object has an * value.toJSONString() method, then that method will be used to produce * the JSON text. The method is required to produce a strictly * conforming text. If the object does not contain a toJSONString * method (which is the most common case), then a text will be * produced by the rules. * <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) throws JSONException { // TODO call the other valueToString instead if (value == null || value.equals(null)) { return "null"; } if (value instanceof JSONString) { Object o; try { o = ((JSONString)value).toJSONString(); } catch (Exception e) { throw new JSONException(e); } if (o instanceof String) { return (String)o; } throw new JSONException("Bad value from toJSONString: " + o); } if (value instanceof Number) { return numberToString((Number) value); } if (value instanceof Boolean || value instanceof JSONObject || value instanceof JSONArray) { return value.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) throws JSONException { if (value == null || value.equals(null)) { return "null"; } try { if (value instanceof JSONString) { Object o = ((JSONString)value).toJSONString(); if (o instanceof String) { return (String)o; } } } catch (Exception e) { /* forget about it */ } 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) throws JSONException { if (n == null) { throw new JSONException("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(JSONArray a, Options opt, String key, Object value) { if(opt.arraysForChildren && (value instanceof JSONObject)) { a.put(new NamedJSONObject(key, (JSONObject)value, opt)); 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 JSONException If the object contains an invalid number. */ public String prettyPrint(JSONObject jo, Options opt) throws JSONException { int n = jo.length(); if (n == 0) { return "{}"; } final JSONArray children = new JSONArray(); Iterator<String> keys = jo.keys(); 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) */ if(children.length() > 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(children, childOpt)); } sb.append('}'); return sb.toString(); } /** Pretty-print a JSONArray */ public String prettyPrint(JSONArray ja, Options opt) throws JSONException { int len = ja.length(); 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 JSONException If o is a non-finite number. */ public void testNumberValidity(Object o) throws JSONException { if (o != null) { if (o instanceof Double) { if (((Double)o).isInfinite() || ((Double)o).isNaN()) { throw new JSONException( "JSON does not allow non-finite numbers"); } } else if (o instanceof Float) { if (((Float)o).isInfinite() || ((Float)o).isNaN()) { throw new JSONException( "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) throws JSONException { final int len = ja.length(); StringBuffer sb = new StringBuffer(); for (int i = 0; i < len; i += 1) { if (i > 0) { sb.append(separator); } sb.append(JSONObject.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 JSONException */ public Writer write(Writer writer, JSONObject jo) throws JSONException { try { boolean b = false; Iterator<String> keys = jo.keys(); writer.write('{'); while (keys.hasNext()) { if (b) { writer.write(','); } String k = keys.next(); writer.write(quote(k)); writer.write(':'); final Object v = jo.get(k); if (v instanceof JSONObject) { ((JSONObject)v).write(writer); } else if (v instanceof JSONArray) { ((JSONArray)v).write(writer); } else { writer.write(valueToString(v)); } b = true; } writer.write('}'); return writer; } catch (IOException e) { throw new JSONException(e); } } /** * 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 JSONException */ public Writer write(Writer writer, JSONArray ja) throws JSONException { try { boolean b = false; int len = ja.length(); writer.write('['); for (int i = 0; i < len; i += 1) { if (b) { writer.write(','); } final Object v = ja.get(i); if (v instanceof JSONObject) { ((JSONObject)v).write(writer); } else if (v instanceof JSONArray) { ((JSONArray)v).write(writer); } else { writer.write(JSONObject.valueToString(v)); } b = true; } writer.write(']'); return writer; } catch (IOException e) { throw new JSONException(e); } } /** * 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; } }