blob: ef1b47c2fe68be6f0b4f11e3ce8e9a25a783aebc [file] [log] [blame]
Alan Viverette3da604b2020-06-10 18:34:39 +00001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.json;
18
19import android.compat.annotation.UnsupportedAppUsage;
20import java.util.ArrayList;
21import java.util.Arrays;
22import java.util.List;
23
24// Note: this class was written without inspecting the non-free org.json sourcecode.
25
26/**
27 * Implements {@link JSONObject#toString} and {@link JSONArray#toString}. Most
28 * application developers should use those methods directly and disregard this
29 * API. For example:<pre>
30 * JSONObject object = ...
31 * String json = object.toString();</pre>
32 *
33 * <p>Stringers only encode well-formed JSON strings. In particular:
34 * <ul>
35 * <li>The stringer must have exactly one top-level array or object.
36 * <li>Lexical scopes must be balanced: every call to {@link #array} must
37 * have a matching call to {@link #endArray} and every call to {@link
38 * #object} must have a matching call to {@link #endObject}.
39 * <li>Arrays may not contain keys (property names).
40 * <li>Objects must alternate keys (property names) and values.
41 * <li>Values are inserted with either literal {@link #value(Object) value}
42 * calls, or by nesting arrays or objects.
43 * </ul>
44 * Calls that would result in a malformed JSON string will fail with a
45 * {@link JSONException}.
46 *
47 * <p>This class provides no facility for pretty-printing (ie. indenting)
48 * output. To encode indented output, use {@link JSONObject#toString(int)} or
49 * {@link JSONArray#toString(int)}.
50 *
51 * <p>Some implementations of the API support at most 20 levels of nesting.
52 * Attempts to create more than 20 levels of nesting may fail with a {@link
53 * JSONException}.
54 *
55 * <p>Each stringer may be used to encode a single top level value. Instances of
56 * this class are not thread safe. Although this class is nonfinal, it was not
57 * designed for inheritance and should not be subclassed. In particular,
58 * self-use by overrideable methods is not specified. See <i>Effective Java</i>
59 * Item 17, "Design and Document or inheritance or else prohibit it" for further
60 * information.
61 */
62public class JSONStringer {
63
64 /** The output data, containing at most one top-level array or object. */
65 @UnsupportedAppUsage
66 final StringBuilder out = new StringBuilder();
67
68 /**
69 * Lexical scoping elements within this stringer, necessary to insert the
70 * appropriate separator characters (ie. commas and colons) and to detect
71 * nesting errors.
72 */
73 enum Scope {
74
75 /**
76 * An array with no elements requires no separators or newlines before
77 * it is closed.
78 */
79 EMPTY_ARRAY,
80
81 /**
82 * A array with at least one value requires a comma and newline before
83 * the next element.
84 */
85 NONEMPTY_ARRAY,
86
87 /**
88 * An object with no keys or values requires no separators or newlines
89 * before it is closed.
90 */
91 EMPTY_OBJECT,
92
93 /**
94 * An object whose most recent element is a key. The next element must
95 * be a value.
96 */
97 DANGLING_KEY,
98
99 /**
100 * An object with at least one name/value pair requires a comma and
101 * newline before the next element.
102 */
103 NONEMPTY_OBJECT,
104
105 /**
106 * A special bracketless array needed by JSONStringer.join() and
107 * JSONObject.quote() only. Not used for JSON encoding.
108 */
109 NULL,
110 }
111
112 /**
113 * Unlike the original implementation, this stack isn't limited to 20
114 * levels of nesting.
115 */
116 @UnsupportedAppUsage
117 private final List<Scope> stack = new ArrayList<Scope>();
118
119 /**
120 * A string containing a full set of spaces for a single level of
121 * indentation, or null for no pretty printing.
122 */
123 @UnsupportedAppUsage
124 private final String indent;
125
126 public JSONStringer() {
127 indent = null;
128 }
129
130 @UnsupportedAppUsage
131 JSONStringer(int indentSpaces) {
132 char[] indentChars = new char[indentSpaces];
133 Arrays.fill(indentChars, ' ');
134 indent = new String(indentChars);
135 }
136
137 /**
138 * Begins encoding a new array. Each call to this method must be paired with
139 * a call to {@link #endArray}.
140 *
141 * @return this stringer.
142 */
143 public JSONStringer array() throws JSONException {
144 return open(Scope.EMPTY_ARRAY, "[");
145 }
146
147 /**
148 * Ends encoding the current array.
149 *
150 * @return this stringer.
151 */
152 public JSONStringer endArray() throws JSONException {
153 return close(Scope.EMPTY_ARRAY, Scope.NONEMPTY_ARRAY, "]");
154 }
155
156 /**
157 * Begins encoding a new object. Each call to this method must be paired
158 * with a call to {@link #endObject}.
159 *
160 * @return this stringer.
161 */
162 public JSONStringer object() throws JSONException {
163 return open(Scope.EMPTY_OBJECT, "{");
164 }
165
166 /**
167 * Ends encoding the current object.
168 *
169 * @return this stringer.
170 */
171 public JSONStringer endObject() throws JSONException {
172 return close(Scope.EMPTY_OBJECT, Scope.NONEMPTY_OBJECT, "}");
173 }
174
175 /**
176 * Enters a new scope by appending any necessary whitespace and the given
177 * bracket.
178 */
179 @UnsupportedAppUsage
180 JSONStringer open(Scope empty, String openBracket) throws JSONException {
181 if (stack.isEmpty() && out.length() > 0) {
182 throw new JSONException("Nesting problem: multiple top-level roots");
183 }
184 beforeValue();
185 stack.add(empty);
186 out.append(openBracket);
187 return this;
188 }
189
190 /**
191 * Closes the current scope by appending any necessary whitespace and the
192 * given bracket.
193 */
194 @UnsupportedAppUsage
195 JSONStringer close(Scope empty, Scope nonempty, String closeBracket) throws JSONException {
196 Scope context = peek();
197 if (context != nonempty && context != empty) {
198 throw new JSONException("Nesting problem");
199 }
200
201 stack.remove(stack.size() - 1);
202 if (context == nonempty) {
203 newline();
204 }
205 out.append(closeBracket);
206 return this;
207 }
208
209 /**
210 * Returns the value on the top of the stack.
211 */
212 @UnsupportedAppUsage
213 private Scope peek() throws JSONException {
214 if (stack.isEmpty()) {
215 throw new JSONException("Nesting problem");
216 }
217 return stack.get(stack.size() - 1);
218 }
219
220 /**
221 * Replace the value on the top of the stack with the given value.
222 */
223 @UnsupportedAppUsage
224 private void replaceTop(Scope topOfStack) {
225 stack.set(stack.size() - 1, topOfStack);
226 }
227
228 /**
229 * Encodes {@code value}.
230 *
231 * @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
232 * Integer, Long, Double or null. May not be {@link Double#isNaN() NaNs}
233 * or {@link Double#isInfinite() infinities}.
234 * @return this stringer.
235 */
236 public JSONStringer value(Object value) throws JSONException {
237 if (stack.isEmpty()) {
238 throw new JSONException("Nesting problem");
239 }
240
241 if (value instanceof JSONArray) {
242 ((JSONArray) value).writeTo(this);
243 return this;
244
245 } else if (value instanceof JSONObject) {
246 ((JSONObject) value).writeTo(this);
247 return this;
248 }
249
250 beforeValue();
251
252 if (value == null
253 || value instanceof Boolean
254 || value == JSONObject.NULL) {
255 out.append(value);
256
257 } else if (value instanceof Number) {
258 out.append(JSONObject.numberToString((Number) value));
259
260 } else {
261 string(value.toString());
262 }
263
264 return this;
265 }
266
267 /**
268 * Encodes {@code value} to this stringer.
269 *
270 * @return this stringer.
271 */
272 public JSONStringer value(boolean value) throws JSONException {
273 if (stack.isEmpty()) {
274 throw new JSONException("Nesting problem");
275 }
276 beforeValue();
277 out.append(value);
278 return this;
279 }
280
281 /**
282 * Encodes {@code value} to this stringer.
283 *
284 * @param value a finite value. May not be {@link Double#isNaN() NaNs} or
285 * {@link Double#isInfinite() infinities}.
286 * @return this stringer.
287 */
288 public JSONStringer value(double value) throws JSONException {
289 if (stack.isEmpty()) {
290 throw new JSONException("Nesting problem");
291 }
292 beforeValue();
293 out.append(JSONObject.numberToString(value));
294 return this;
295 }
296
297 /**
298 * Encodes {@code value} to this stringer.
299 *
300 * @return this stringer.
301 */
302 public JSONStringer value(long value) throws JSONException {
303 if (stack.isEmpty()) {
304 throw new JSONException("Nesting problem");
305 }
306 beforeValue();
307 out.append(value);
308 return this;
309 }
310
311 @UnsupportedAppUsage
312 private void string(String value) {
313 out.append("\"");
314 for (int i = 0, length = value.length(); i < length; i++) {
315 char c = value.charAt(i);
316
317 /*
318 * From RFC 4627, "All Unicode characters may be placed within the
319 * quotation marks except for the characters that must be escaped:
320 * quotation mark, reverse solidus, and the control characters
321 * (U+0000 through U+001F)."
322 */
323 switch (c) {
324 case '"':
325 case '\\':
326 case '/':
327 out.append('\\').append(c);
328 break;
329
330 case '\t':
331 out.append("\\t");
332 break;
333
334 case '\b':
335 out.append("\\b");
336 break;
337
338 case '\n':
339 out.append("\\n");
340 break;
341
342 case '\r':
343 out.append("\\r");
344 break;
345
346 case '\f':
347 out.append("\\f");
348 break;
349
350 default:
351 if (c <= 0x1F) {
352 out.append(String.format("\\u%04x", (int) c));
353 } else {
354 out.append(c);
355 }
356 break;
357 }
358
359 }
360 out.append("\"");
361 }
362
363 @UnsupportedAppUsage
364 private void newline() {
365 if (indent == null) {
366 return;
367 }
368
369 out.append("\n");
370 for (int i = 0; i < stack.size(); i++) {
371 out.append(indent);
372 }
373 }
374
375 /**
376 * Encodes the key (property name) to this stringer.
377 *
378 * @param name the name of the forthcoming value. May not be null.
379 * @return this stringer.
380 */
381 public JSONStringer key(String name) throws JSONException {
382 if (name == null) {
383 throw new JSONException("Names must be non-null");
384 }
385 beforeKey();
386 string(name);
387 return this;
388 }
389
390 /**
391 * Inserts any necessary separators and whitespace before a name. Also
392 * adjusts the stack to expect the key's value.
393 */
394 @UnsupportedAppUsage
395 private void beforeKey() throws JSONException {
396 Scope context = peek();
397 if (context == Scope.NONEMPTY_OBJECT) { // first in object
398 out.append(',');
399 } else if (context != Scope.EMPTY_OBJECT) { // not in an object!
400 throw new JSONException("Nesting problem");
401 }
402 newline();
403 replaceTop(Scope.DANGLING_KEY);
404 }
405
406 /**
407 * Inserts any necessary separators and whitespace before a literal value,
408 * inline array, or inline object. Also adjusts the stack to expect either a
409 * closing bracket or another element.
410 */
411 @UnsupportedAppUsage
412 private void beforeValue() throws JSONException {
413 if (stack.isEmpty()) {
414 return;
415 }
416
417 Scope context = peek();
418 if (context == Scope.EMPTY_ARRAY) { // first in array
419 replaceTop(Scope.NONEMPTY_ARRAY);
420 newline();
421 } else if (context == Scope.NONEMPTY_ARRAY) { // another in array
422 out.append(',');
423 newline();
424 } else if (context == Scope.DANGLING_KEY) { // value for key
425 out.append(indent == null ? ":" : ": ");
426 replaceTop(Scope.NONEMPTY_OBJECT);
427 } else if (context != Scope.NULL) {
428 throw new JSONException("Nesting problem");
429 }
430 }
431
432 /**
433 * Returns the encoded JSON string.
434 *
435 * <p>If invoked with unterminated arrays or unclosed objects, this method's
436 * return value is undefined.
437 *
438 * <p><strong>Warning:</strong> although it contradicts the general contract
439 * of {@link Object#toString}, this method returns null if the stringer
440 * contains no data.
441 */
442 @Override public String toString() {
443 return out.length() == 0 ? null : out.toString();
444 }
445}