| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * 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 org.json; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| // Note: this class was written without inspecting the non-free org.json sourcecode. |
| |
| /** |
| * Implements {@link JSONObject#toString} and {@link JSONArray#toString}. Most |
| * application developers should use those methods directly and disregard this |
| * API. For example:<pre> |
| * JSONObject object = ... |
| * String json = object.toString();</pre> |
| * |
| * <p>Stringers only encode well-formed JSON strings. In particular: |
| * <ul> |
| * <li>The stringer must have exactly one top-level array or object. |
| * <li>Lexical scopes must be balanced: every call to {@link #array} must |
| * have a matching call to {@link #endArray} and every call to {@link |
| * #object} must have a matching call to {@link #endObject}. |
| * <li>Arrays may not contain keys (property names). |
| * <li>Objects must alternate keys (property names) and values. |
| * <li>Values are inserted with either literal {@link #value(Object) value} |
| * calls, or by nesting arrays or objects. |
| * </ul> |
| * Calls that would result in a malformed JSON string will fail with a |
| * {@link JSONException}. |
| * |
| * <p>This class provides no facility for pretty-printing (ie. indenting) |
| * output. To encode indented output, use {@link JSONObject#toString(int)} or |
| * {@link JSONArray#toString(int)}. |
| * |
| * <p>Some implementations of the API support at most 20 levels of nesting. |
| * Attempts to create more than 20 levels of nesting may fail with a {@link |
| * JSONException}. |
| * |
| * <p>Each stringer may be used to encode a single top level value. Instances of |
| * this class are not thread safe. Although this class is nonfinal, it was not |
| * designed for inheritance and should not be subclassed. In particular, |
| * self-use by overrideable methods is not specified. See <i>Effective Java</i> |
| * Item 17, "Design and Document or inheritance or else prohibit it" for further |
| * information. |
| */ |
| public class JSONStringer { |
| |
| /** The output data, containing at most one top-level array or object. */ |
| final StringBuilder out = new StringBuilder(); |
| |
| /** |
| * Lexical scoping elements within this stringer, necessary to insert the |
| * appropriate separator characters (ie. commas and colons) and to detect |
| * nesting errors. |
| */ |
| enum Scope { |
| |
| /** |
| * An array with no elements requires no separators or newlines before |
| * it is closed. |
| */ |
| EMPTY_ARRAY, |
| |
| /** |
| * A array with at least one value requires a comma and newline before |
| * the next element. |
| */ |
| NONEMPTY_ARRAY, |
| |
| /** |
| * An object with no keys or values requires no separators or newlines |
| * before it is closed. |
| */ |
| EMPTY_OBJECT, |
| |
| /** |
| * An object whose most recent element is a key. The next element must |
| * be a value. |
| */ |
| DANGLING_KEY, |
| |
| /** |
| * An object with at least one name/value pair requires a comma and |
| * newline before the next element. |
| */ |
| NONEMPTY_OBJECT, |
| |
| /** |
| * A special bracketless array needed by JSONStringer.join() and |
| * JSONObject.quote() only. Not used for JSON encoding. |
| */ |
| NULL, |
| } |
| |
| /** |
| * Unlike the original implementation, this stack isn't limited to 20 |
| * levels of nesting. |
| */ |
| private final List<Scope> stack = new ArrayList<Scope>(); |
| |
| /** |
| * A string containing a full set of spaces for a single level of |
| * indentation, or null for no pretty printing. |
| */ |
| private final String indent; |
| |
| public JSONStringer() { |
| indent = null; |
| } |
| |
| JSONStringer(int indentSpaces) { |
| char[] indentChars = new char[indentSpaces]; |
| Arrays.fill(indentChars, ' '); |
| indent = new String(indentChars); |
| } |
| |
| /** |
| * Begins encoding a new array. Each call to this method must be paired with |
| * a call to {@link #endArray}. |
| * |
| * @return this stringer. |
| */ |
| public JSONStringer array() throws JSONException { |
| return open(Scope.EMPTY_ARRAY, "["); |
| } |
| |
| /** |
| * Ends encoding the current array. |
| * |
| * @return this stringer. |
| */ |
| public JSONStringer endArray() throws JSONException { |
| return close(Scope.EMPTY_ARRAY, Scope.NONEMPTY_ARRAY, "]"); |
| } |
| |
| /** |
| * Begins encoding a new object. Each call to this method must be paired |
| * with a call to {@link #endObject}. |
| * |
| * @return this stringer. |
| */ |
| public JSONStringer object() throws JSONException { |
| return open(Scope.EMPTY_OBJECT, "{"); |
| } |
| |
| /** |
| * Ends encoding the current object. |
| * |
| * @return this stringer. |
| */ |
| public JSONStringer endObject() throws JSONException { |
| return close(Scope.EMPTY_OBJECT, Scope.NONEMPTY_OBJECT, "}"); |
| } |
| |
| /** |
| * Enters a new scope by appending any necessary whitespace and the given |
| * bracket. |
| */ |
| JSONStringer open(Scope empty, String openBracket) throws JSONException { |
| if (stack.isEmpty() && out.length() > 0) { |
| throw new JSONException("Nesting problem: multiple top-level roots"); |
| } |
| beforeValue(); |
| stack.add(empty); |
| out.append(openBracket); |
| return this; |
| } |
| |
| /** |
| * Closes the current scope by appending any necessary whitespace and the |
| * given bracket. |
| */ |
| JSONStringer close(Scope empty, Scope nonempty, String closeBracket) throws JSONException { |
| Scope context = peek(); |
| if (context != nonempty && context != empty) { |
| throw new JSONException("Nesting problem"); |
| } |
| |
| stack.remove(stack.size() - 1); |
| if (context == nonempty) { |
| newline(); |
| } |
| out.append(closeBracket); |
| return this; |
| } |
| |
| /** |
| * Returns the value on the top of the stack. |
| */ |
| private Scope peek() throws JSONException { |
| if (stack.isEmpty()) { |
| throw new JSONException("Nesting problem"); |
| } |
| return stack.get(stack.size() - 1); |
| } |
| |
| /** |
| * Replace the value on the top of the stack with the given value. |
| */ |
| private void replaceTop(Scope topOfStack) { |
| stack.set(stack.size() - 1, topOfStack); |
| } |
| |
| /** |
| * Encodes {@code value}. |
| * |
| * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean, |
| * Integer, Long, Double or null. May not be {@link Double#isNaN() NaNs} |
| * or {@link Double#isInfinite() infinities}. |
| * @return this stringer. |
| */ |
| public JSONStringer value(Object value) throws JSONException { |
| if (stack.isEmpty()) { |
| throw new JSONException("Nesting problem"); |
| } |
| |
| if (value instanceof JSONArray) { |
| ((JSONArray) value).writeTo(this); |
| return this; |
| |
| } else if (value instanceof JSONObject) { |
| ((JSONObject) value).writeTo(this); |
| return this; |
| } |
| |
| beforeValue(); |
| |
| if (value == null |
| || value instanceof Boolean |
| || value == JSONObject.NULL) { |
| out.append(value); |
| |
| } else if (value instanceof Number) { |
| out.append(JSONObject.numberToString((Number) value)); |
| |
| } else { |
| string(value.toString()); |
| } |
| |
| return this; |
| } |
| |
| /** |
| * Encodes {@code value} to this stringer. |
| * |
| * @return this stringer. |
| */ |
| public JSONStringer value(boolean value) throws JSONException { |
| if (stack.isEmpty()) { |
| throw new JSONException("Nesting problem"); |
| } |
| beforeValue(); |
| out.append(value); |
| return this; |
| } |
| |
| /** |
| * Encodes {@code value} to this stringer. |
| * |
| * @param value a finite value. May not be {@link Double#isNaN() NaNs} or |
| * {@link Double#isInfinite() infinities}. |
| * @return this stringer. |
| */ |
| public JSONStringer value(double value) throws JSONException { |
| if (stack.isEmpty()) { |
| throw new JSONException("Nesting problem"); |
| } |
| beforeValue(); |
| out.append(JSONObject.numberToString(value)); |
| return this; |
| } |
| |
| /** |
| * Encodes {@code value} to this stringer. |
| * |
| * @return this stringer. |
| */ |
| public JSONStringer value(long value) throws JSONException { |
| if (stack.isEmpty()) { |
| throw new JSONException("Nesting problem"); |
| } |
| beforeValue(); |
| out.append(value); |
| return this; |
| } |
| |
| private void string(String value) { |
| out.append("\""); |
| for (int i = 0, length = value.length(); i < length; i++) { |
| char c = value.charAt(i); |
| |
| /* |
| * From RFC 4627, "All Unicode characters may be placed within the |
| * quotation marks except for the characters that must be escaped: |
| * quotation mark, reverse solidus, and the control characters |
| * (U+0000 through U+001F)." |
| */ |
| switch (c) { |
| case '"': |
| case '\\': |
| case '/': |
| out.append('\\').append(c); |
| break; |
| |
| case '\t': |
| out.append("\\t"); |
| break; |
| |
| case '\b': |
| out.append("\\b"); |
| break; |
| |
| case '\n': |
| out.append("\\n"); |
| break; |
| |
| case '\r': |
| out.append("\\r"); |
| break; |
| |
| case '\f': |
| out.append("\\f"); |
| break; |
| |
| default: |
| if (c <= 0x1F) { |
| out.append(String.format("\\u%04x", (int) c)); |
| } else { |
| out.append(c); |
| } |
| break; |
| } |
| |
| } |
| out.append("\""); |
| } |
| |
| private void newline() { |
| if (indent == null) { |
| return; |
| } |
| |
| out.append("\n"); |
| for (int i = 0; i < stack.size(); i++) { |
| out.append(indent); |
| } |
| } |
| |
| /** |
| * Encodes the key (property name) to this stringer. |
| * |
| * @param name the name of the forthcoming value. May not be null. |
| * @return this stringer. |
| */ |
| public JSONStringer key(String name) throws JSONException { |
| if (name == null) { |
| throw new JSONException("Names must be non-null"); |
| } |
| beforeKey(); |
| string(name); |
| return this; |
| } |
| |
| /** |
| * Inserts any necessary separators and whitespace before a name. Also |
| * adjusts the stack to expect the key's value. |
| */ |
| private void beforeKey() throws JSONException { |
| Scope context = peek(); |
| if (context == Scope.NONEMPTY_OBJECT) { // first in object |
| out.append(','); |
| } else if (context != Scope.EMPTY_OBJECT) { // not in an object! |
| throw new JSONException("Nesting problem"); |
| } |
| newline(); |
| replaceTop(Scope.DANGLING_KEY); |
| } |
| |
| /** |
| * Inserts any necessary separators and whitespace before a literal value, |
| * inline array, or inline object. Also adjusts the stack to expect either a |
| * closing bracket or another element. |
| */ |
| private void beforeValue() throws JSONException { |
| if (stack.isEmpty()) { |
| return; |
| } |
| |
| Scope context = peek(); |
| if (context == Scope.EMPTY_ARRAY) { // first in array |
| replaceTop(Scope.NONEMPTY_ARRAY); |
| newline(); |
| } else if (context == Scope.NONEMPTY_ARRAY) { // another in array |
| out.append(','); |
| newline(); |
| } else if (context == Scope.DANGLING_KEY) { // value for key |
| out.append(indent == null ? ":" : ": "); |
| replaceTop(Scope.NONEMPTY_OBJECT); |
| } else if (context != Scope.NULL) { |
| throw new JSONException("Nesting problem"); |
| } |
| } |
| |
| /** |
| * Returns the encoded JSON string. |
| * |
| * <p>If invoked with unterminated arrays or unclosed objects, this method's |
| * return value is undefined. |
| * |
| * <p><strong>Warning:</strong> although it contradicts the general contract |
| * of {@link Object#toString}, this method returns null if the stringer |
| * contains no data. |
| */ |
| @Override public String toString() { |
| return out.length() == 0 ? null : out.toString(); |
| } |
| } |