All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.boot.configurationprocessor.json.JSONStringer Maven / Gradle / Ivy

There is a newer version: 3.3.0
Show newest version
/*
 * 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.springframework.boot.configurationprocessor.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 source code.

/**
 * Implements {@link JSONObject#toString} and {@link JSONArray#toString}. Most application
 * developers should use those methods directly and disregard this API. For example:
 * JSONObject object = ...
 * String json = object.toString();
*

* Stringers only encode well-formed JSON strings. In particular: *

    *
  • The stringer must have exactly one top-level array or object. *
  • 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}. *
  • Arrays may not contain keys (property names). *
  • Objects must alternate keys (property names) and values. *
  • Values are inserted with either literal {@link #value(Object) value} calls, or by * nesting arrays or objects. *
* Calls that would result in a malformed JSON string will fail with a * {@link JSONException}. *

* 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)}. *

* 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}. *

* 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 Effective Java 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, /** * An 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 stack = new ArrayList<>(); /** * 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() { this.indent = null; } JSONStringer(int indentSpaces) { char[] indentChars = new char[indentSpaces]; Arrays.fill(indentChars, ' '); this.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. * @throws JSONException if processing of json failed */ public JSONStringer array() throws JSONException { return open(Scope.EMPTY_ARRAY, "["); } /** * Ends encoding the current array. * @return this stringer. * @throws JSONException if processing of json failed */ 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. * @throws JSONException if processing of json failed */ public JSONStringer object() throws JSONException { return open(Scope.EMPTY_OBJECT, "{"); } /** * Ends encoding the current object. * @return this stringer. * @throws JSONException if processing of json failed */ 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. * @param empty any necessary whitespace * @param openBracket the open bracket * @return this object * @throws JSONException if processing of json failed */ JSONStringer open(Scope empty, String openBracket) throws JSONException { if (this.stack.isEmpty() && this.out.length() > 0) { throw new JSONException("Nesting problem: multiple top-level roots"); } beforeValue(); this.stack.add(empty); this.out.append(openBracket); return this; } /** * Closes the current scope by appending any necessary whitespace and the given * bracket. * @param empty any necessary whitespace * @param nonempty the current scope * @param closeBracket the close bracket * @return the JSON stringer * @throws JSONException if processing of json failed */ JSONStringer close(Scope empty, Scope nonempty, String closeBracket) throws JSONException { Scope context = peek(); if (context != nonempty && context != empty) { throw new JSONException("Nesting problem"); } this.stack.remove(this.stack.size() - 1); if (context == nonempty) { newline(); } this.out.append(closeBracket); return this; } /** * Returns the value on the top of the stack. * @return the scope * @throws JSONException if processing of json failed */ private Scope peek() throws JSONException { if (this.stack.isEmpty()) { throw new JSONException("Nesting problem"); } return this.stack.get(this.stack.size() - 1); } /** * Replace the value on the top of the stack with the given value. * @param topOfStack the scope at the top of the stack */ private void replaceTop(Scope topOfStack) { this.stack.set(this.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. * @throws JSONException if processing of json failed */ public JSONStringer value(Object value) throws JSONException { if (this.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) { this.out.append(value); } else if (value instanceof Number) { this.out.append(JSONObject.numberToString((Number) value)); } else { string(value.toString()); } return this; } /** * Encodes {@code value} to this stringer. * @param value the value to encode * @return this stringer. * @throws JSONException if processing of json failed */ public JSONStringer value(boolean value) throws JSONException { if (this.stack.isEmpty()) { throw new JSONException("Nesting problem"); } beforeValue(); this.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. * @throws JSONException if processing of json failed */ public JSONStringer value(double value) throws JSONException { if (this.stack.isEmpty()) { throw new JSONException("Nesting problem"); } beforeValue(); this.out.append(JSONObject.numberToString(value)); return this; } /** * Encodes {@code value} to this stringer. * @param value the value to encode * @return this stringer. * @throws JSONException if processing of json failed */ public JSONStringer value(long value) throws JSONException { if (this.stack.isEmpty()) { throw new JSONException("Nesting problem"); } beforeValue(); this.out.append(value); return this; } private void string(String value) { this.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 '/': this.out.append('\\').append(c); break; case '\t': this.out.append("\\t"); break; case '\b': this.out.append("\\b"); break; case '\n': this.out.append("\\n"); break; case '\r': this.out.append("\\r"); break; case '\f': this.out.append("\\f"); break; default: if (c <= 0x1F) { this.out.append(String.format("\\u%04x", (int) c)); } else { this.out.append(c); } break; } } this.out.append("\""); } private void newline() { if (this.indent == null) { return; } this.out.append("\n"); for (int i = 0; i < this.stack.size(); i++) { this.out.append(this.indent); } } /** * Encodes the key (property name) to this stringer. * @param name the name of the forthcoming value. May not be null. * @return this stringer. * @throws JSONException if processing of json failed */ 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. * @throws JSONException if processing of json failed */ private void beforeKey() throws JSONException { Scope context = peek(); if (context == Scope.NONEMPTY_OBJECT) { // first in object this.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. * @throws JSONException if processing of json failed */ private void beforeValue() throws JSONException { if (this.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 this.out.append(','); newline(); } else if (context == Scope.DANGLING_KEY) { // value for key this.out.append(this.indent == null ? ":" : ": "); replaceTop(Scope.NONEMPTY_OBJECT); } else if (context != Scope.NULL) { throw new JSONException("Nesting problem"); } } /** * Returns the encoded JSON string. *

* If invoked with unterminated arrays or unclosed objects, this method's return value * is undefined. *

* Warning: although it contradicts the general contract of * {@link Object#toString}, this method returns null if the stringer contains no data. * @return the encoded JSON string. */ @Override public String toString() { return this.out.length() == 0 ? null : this.out.toString(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy