f520c0c8fed64d1c7784b1e61bd70aa78b800a7c
[jalview.git] / src / org / json / JSONWriter.java
1 package org.json;
2
3 import java.io.IOException;
4 import java.math.BigDecimal;
5 import java.util.Collection;
6 import java.util.Map;
7
8 /*
9 Copyright (c) 2006 JSON.org
10
11 Permission is hereby granted, free of charge, to any person obtaining a copy
12 of this software and associated documentation files (the "Software"), to deal
13 in the Software without restriction, including without limitation the rights
14 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 copies of the Software, and to permit persons to whom the Software is
16 furnished to do so, subject to the following conditions:
17
18 The above copyright notice and this permission notice shall be included in all
19 copies or substantial portions of the Software.
20
21 The Software shall be used for Good, not Evil.
22
23 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29 SOFTWARE.
30 */
31
32 /**
33  * JSONWriter provides a quick and convenient way of producing JSON text.
34  * The texts produced strictly conform to JSON syntax rules. No whitespace is
35  * added, so the results are ready for transmission or storage. Each instance of
36  * JSONWriter can produce one JSON text.
37  * <p>
38  * A JSONWriter instance provides a <code>value</code> method for appending
39  * values to the
40  * text, and a <code>key</code>
41  * method for adding keys before values in objects. There are <code>array</code>
42  * and <code>endArray</code> methods that make and bound array values, and
43  * <code>object</code> and <code>endObject</code> methods which make and bound
44  * object values. All of these methods return the JSONWriter instance,
45  * permitting a cascade style. For example, <pre>
46  * new JSONWriter(myWriter)
47  *     .object()
48  *         .key("JSON")
49  *         .value("Hello, World!")
50  *     .endObject();</pre> which writes <pre>
51  * {"JSON":"Hello, World!"}</pre>
52  * <p>
53  * The first method called must be <code>array</code> or <code>object</code>.
54  * There are no methods for adding commas or colons. JSONWriter adds them for
55  * you. Objects and arrays can be nested up to 200 levels deep.
56  * <p>
57  * This can sometimes be easier than using a JSONObject to build a string.
58  * @author JSON.org
59  * @version 2016-08-08
60  */
61 public class JSONWriter {
62     private static final int maxdepth = 200;
63
64     /**
65      * The comma flag determines if a comma should be output before the next
66      * value.
67      */
68     private boolean comma;
69
70     /**
71      * The current mode. Values:
72      * 'a' (array),
73      * 'd' (done),
74      * 'i' (initial),
75      * 'k' (key),
76      * 'o' (object).
77      */
78     protected char mode;
79
80     /**
81      * The object/array stack.
82      */
83     private final JSONObject stack[];
84
85     /**
86      * The stack top index. A value of 0 indicates that the stack is empty.
87      */
88     private int top;
89
90     /**
91      * The writer that will receive the output.
92      */
93     protected Appendable writer;
94
95     /**
96      * Make a fresh JSONWriter. It can be used to build one JSON text.
97      */
98     public JSONWriter(Appendable w) {
99         this.comma = false;
100         this.mode = 'i';
101         this.stack = new JSONObject[maxdepth];
102         this.top = 0;
103         this.writer = w;
104     }
105
106     /**
107      * Append a value.
108      * @param string A string value.
109      * @return this
110      * @throws JSONException If the value is out of sequence.
111      */
112     private JSONWriter append(String string) throws JSONException {
113         if (string == null) {
114             throw new JSONException("Null pointer");
115         }
116         if (this.mode == 'o' || this.mode == 'a') {
117             try {
118                 if (this.comma && this.mode == 'a') {
119                     this.writer.append(',');
120                 }
121                 this.writer.append(string);
122             } catch (IOException e) {
123                 // Android as of API 25 does not support this exception constructor
124                 // however we won't worry about it. If an exception is happening here
125                 // it will just throw a "Method not found" exception instead.
126                 throw new JSONException(e);
127             }
128             if (this.mode == 'o') {
129                 this.mode = 'k';
130             }
131             this.comma = true;
132             return this;
133         }
134         throw new JSONException("Value out of sequence.");
135     }
136
137     /**
138      * Begin appending a new array. All values until the balancing
139      * <code>endArray</code> will be appended to this array. The
140      * <code>endArray</code> method must be called to mark the array's end.
141      * @return this
142      * @throws JSONException If the nesting is too deep, or if the object is
143      * started in the wrong place (for example as a key or after the end of the
144      * outermost array or object).
145      */
146     public JSONWriter array() throws JSONException {
147         if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') {
148             this.push(null);
149             this.append("[");
150             this.comma = false;
151             return this;
152         }
153         throw new JSONException("Misplaced array.");
154     }
155
156     /**
157      * End something.
158      * @param m Mode
159      * @param c Closing character
160      * @return this
161      * @throws JSONException If unbalanced.
162      */
163     private JSONWriter end(char m, char c) throws JSONException {
164         if (this.mode != m) {
165             throw new JSONException(m == 'a'
166                 ? "Misplaced endArray."
167                 : "Misplaced endObject.");
168         }
169         this.pop(m);
170         try {
171             this.writer.append(c);
172         } catch (IOException e) {
173                 // Android as of API 25 does not support this exception constructor
174                 // however we won't worry about it. If an exception is happening here
175                 // it will just throw a "Method not found" exception instead.
176             throw new JSONException(e);
177         }
178         this.comma = true;
179         return this;
180     }
181
182     /**
183      * End an array. This method most be called to balance calls to
184      * <code>array</code>.
185      * @return this
186      * @throws JSONException If incorrectly nested.
187      */
188     public JSONWriter endArray() throws JSONException {
189         return this.end('a', ']');
190     }
191
192     /**
193      * End an object. This method most be called to balance calls to
194      * <code>object</code>.
195      * @return this
196      * @throws JSONException If incorrectly nested.
197      */
198     public JSONWriter endObject() throws JSONException {
199         return this.end('k', '}');
200     }
201
202     /**
203      * Append a key. The key will be associated with the next value. In an
204      * object, every value must be preceded by a key.
205      * @param string A key string.
206      * @return this
207      * @throws JSONException If the key is out of place. For example, keys
208      *  do not belong in arrays or if the key is null.
209      */
210     public JSONWriter key(String string) throws JSONException {
211         if (string == null) {
212             throw new JSONException("Null key.");
213         }
214         if (this.mode == 'k') {
215             try {
216                 JSONObject topObject = this.stack[this.top - 1];
217                 // don't use the built in putOnce method to maintain Android support
218                                 if(topObject.has(string)) {
219                                         throw new JSONException("Duplicate key \"" + string + "\"");
220                                 }
221                 topObject.put(string, true);
222                 if (this.comma) {
223                     this.writer.append(',');
224                 }
225                 this.writer.append(JSONObject.quote(string));
226                 this.writer.append(':');
227                 this.comma = false;
228                 this.mode = 'o';
229                 return this;
230             } catch (IOException e) {
231                 // Android as of API 25 does not support this exception constructor
232                 // however we won't worry about it. If an exception is happening here
233                 // it will just throw a "Method not found" exception instead.
234                 throw new JSONException(e);
235             }
236         }
237         throw new JSONException("Misplaced key.");
238     }
239
240
241     /**
242      * Begin appending a new object. All keys and values until the balancing
243      * <code>endObject</code> will be appended to this object. The
244      * <code>endObject</code> method must be called to mark the object's end.
245      * @return this
246      * @throws JSONException If the nesting is too deep, or if the object is
247      * started in the wrong place (for example as a key or after the end of the
248      * outermost array or object).
249      */
250     public JSONWriter object() throws JSONException {
251         if (this.mode == 'i') {
252             this.mode = 'o';
253         }
254         if (this.mode == 'o' || this.mode == 'a') {
255             this.append("{");
256             this.push(new JSONObject());
257             this.comma = false;
258             return this;
259         }
260         throw new JSONException("Misplaced object.");
261
262     }
263
264
265     /**
266      * Pop an array or object scope.
267      * @param c The scope to close.
268      * @throws JSONException If nesting is wrong.
269      */
270     private void pop(char c) throws JSONException {
271         if (this.top <= 0) {
272             throw new JSONException("Nesting error.");
273         }
274         char m = this.stack[this.top - 1] == null ? 'a' : 'k';
275         if (m != c) {
276             throw new JSONException("Nesting error.");
277         }
278         this.top -= 1;
279         this.mode = this.top == 0
280             ? 'd'
281             : this.stack[this.top - 1] == null
282             ? 'a'
283             : 'k';
284     }
285
286     /**
287      * Push an array or object scope.
288      * @param jo The scope to open.
289      * @throws JSONException If nesting is too deep.
290      */
291     private void push(JSONObject jo) throws JSONException {
292         if (this.top >= maxdepth) {
293             throw new JSONException("Nesting too deep.");
294         }
295         this.stack[this.top] = jo;
296         this.mode = jo == null ? 'a' : 'k';
297         this.top += 1;
298     }
299
300     /**
301      * Make a JSON text of an Object value. If the object has an
302      * value.toJSONString() method, then that method will be used to produce the
303      * JSON text. The method is required to produce a strictly conforming text.
304      * If the object does not contain a toJSONString method (which is the most
305      * common case), then a text will be produced by other means. If the value
306      * is an array or Collection, then a JSONArray will be made from it and its
307      * toJSONString method will be called. If the value is a MAP, then a
308      * JSONObject will be made from it and its toJSONString method will be
309      * called. Otherwise, the value's toString method will be called, and the
310      * result will be quoted.
311      *
312      * <p>
313      * Warning: This method assumes that the data structure is acyclical.
314      *
315      * @param value
316      *            The value to be serialized.
317      * @return a printable, displayable, transmittable representation of the
318      *         object, beginning with <code>{</code>&nbsp;<small>(left
319      *         brace)</small> and ending with <code>}</code>&nbsp;<small>(right
320      *         brace)</small>.
321      * @throws JSONException
322      *             If the value is or contains an invalid number.
323      */
324     public static String valueToString(Object value) throws JSONException {
325         if (value == null || value.equals(null)) {
326             return "null";
327         }
328         if (value instanceof JSONString) {
329             Object object;
330             try {
331                 object = ((JSONString) value).toJSONString();
332             } catch (Exception e) {
333                 throw new JSONException(e);
334             }
335             if (object instanceof String) {
336                 return (String) object;
337             }
338             throw new JSONException("Bad value from toJSONString: " + object);
339         }
340         if (value instanceof Number) {
341             // not all Numbers may match actual JSON Numbers. i.e. Fractions or Complex
342             final String numberAsString = JSONObject.numberToString((Number) value);
343             try {
344                 // Use the BigDecimal constructor for it's parser to validate the format.
345                 @SuppressWarnings("unused")
346                 BigDecimal unused = new BigDecimal(numberAsString);
347                 // Close enough to a JSON number that we will return it unquoted
348                 return numberAsString;
349             } catch (NumberFormatException ex){
350                 // The Number value is not a valid JSON number.
351                 // Instead we will quote it as a string
352                 return JSONObject.quote(numberAsString);
353             }
354         }
355         if (value instanceof Boolean || value instanceof JSONObject
356                 || value instanceof JSONArray) {
357             return value.toString();
358         }
359         if (value instanceof Map) {
360             Map<?, ?> map = (Map<?, ?>) value;
361             return new JSONObject(map).toString();
362         }
363         if (value instanceof Collection) {
364             Collection<?> coll = (Collection<?>) value;
365             return new JSONArray(coll).toString();
366         }
367         if (value.getClass().isArray()) {
368             return new JSONArray(value).toString();
369         }
370         if(value instanceof Enum<?>){
371             return JSONObject.quote(((Enum<?>)value).name());
372         }
373         return JSONObject.quote(value.toString());
374     }
375
376     /**
377      * Append either the value <code>true</code> or the value
378      * <code>false</code>.
379      * @param b A boolean.
380      * @return this
381      * @throws JSONException
382      */
383     public JSONWriter value(boolean b) throws JSONException {
384         return this.append(b ? "true" : "false");
385     }
386
387     /**
388      * Append a double value.
389      * @param d A double.
390      * @return this
391      * @throws JSONException If the number is not finite.
392      */
393     public JSONWriter value(double d) throws JSONException {
394         return this.value(Double.valueOf(d));
395     }
396
397     /**
398      * Append a long value.
399      * @param l A long.
400      * @return this
401      * @throws JSONException
402      */
403     public JSONWriter value(long l) throws JSONException {
404         return this.append(Long.toString(l));
405     }
406
407
408     /**
409      * Append an object value.
410      * @param object The object to append. It can be null, or a Boolean, Number,
411      *   String, JSONObject, or JSONArray, or an object that implements JSONString.
412      * @return this
413      * @throws JSONException If the value is out of sequence.
414      */
415     public JSONWriter value(Object object) throws JSONException {
416         return this.append(valueToString(object));
417     }
418 }