1 package atg.taglib.json.util;
2
3 import java.io.IOException;
4 import java.io.Writer;
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 /**
31 * JSONWriter provides a quick and convenient way of producing JSON text.
32 * The texts produced strictly conform to JSON syntax rules. No whitespace is
33 * added, so the results are ready for transmission or storage. Each instance of
34 * JSONWriter can produce one JSON text.
35 * <p>
36 * A JSONWriter instance provides a <code>value</code> method for appending
37 * values to the
38 * text, and a <code>key</code>
39 * method for adding keys before values in objects. There are <code>array</code>
40 * and <code>endArray</code> methods that make and bound array values, and
41 * <code>object</code> and <code>endObject</code> methods which make and bound
42 * object values. All of these methods return the JSONWriter instance,
43 * permitting a cascade style. For example, <pre>
44 * new JSONWriter(myWriter)
45 * .object()
46 * .key("JSON")
47 * .value("Hello, World!")
48 * .endObject();</pre> which writes <pre>
49 * {"JSON":"Hello, World!"}</pre>
50 * <p>
51 * The first method called must be <code>array</code> or <code>object</code>.
52 * There are no methods for adding commas or colons. JSONWriter adds them for
53 * you. Objects and arrays can be nested up to 20 levels deep.
54 * <p>
55 * This can sometimes be easier than using a JSONObject to build a string.
56 * @author JSON.org
57 * @version 2
58 */
59 public class JSONWriter {
60 private static final int maxdepth = 20;
61
62 /**
63 * The comma flag determines if a comma should be output before the next
64 * value.
65 */
66 private boolean comma;
67
68 /**
69 * The current mode. Values:
70 * 'a' (array),
71 * 'd' (done),
72 * 'i' (initial),
73 * 'k' (key),
74 * 'o' (object).
75 */
76 protected char mode;
77
78 /**
79 * The object/array stack.
80 */
81 private char stack[];
82
83 /**
84 * The stack top index. A value of 0 indicates that the stack is empty.
85 */
86 private int top;
87
88 /**
89 * The writer that will receive the output.
90 */
91 protected Writer writer;
92
93 /**
94 * Make a fresh JSONWriter. It can be used to build one JSON text.
95 */
96 public JSONWriter(Writer w) {
97 this.comma = false;
98 this.mode = 'i';
99 this.stack = new char[maxdepth];
100 this.top = 0;
101 this.writer = w;
102 }
103
104 /**
105 * Append a value.
106 * @param s A string value.
107 * @return this
108 * @throws JSONException If the value is out of sequence.
109 */
110 private JSONWriter append(String s) throws JSONException {
111 if (s == null) {
112 throw new JSONException("Null pointer");
113 }
114 if (this.mode == 'o' || this.mode == 'a') {
115 try {
116 if (this.comma && this.mode == 'a') {
117 this.writer.write(',');
118 }
119 this.writer.write(s);
120 } catch (IOException e) {
121 throw new JSONException(e);
122 }
123 if (this.mode == 'o') {
124 this.mode = 'k';
125 }
126 this.comma = true;
127 return this;
128 }
129 throw new JSONException("Value out of sequence.");
130 }
131
132 /**
133 * Begin appending a new array. All values until the balancing
134 * <code>endArray</code> will be appended to this array. The
135 * <code>endArray</code> method must be called to mark the array's end.
136 * @return this
137 * @throws JSONException If the nesting is too deep, or if the object is
138 * started in the wrong place (for example as a key or after the end of the
139 * outermost array or object).
140 */
141 public JSONWriter array() throws JSONException {
142 if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') {
143 this.push('a');
144 this.append("[");
145 this.comma = false;
146 return this;
147 }
148 throw new JSONException("Misplaced array.");
149 }
150
151 /**
152 * End something.
153 * @param m Mode
154 * @param c Closing character
155 * @return this
156 * @throws JSONException If unbalanced.
157 */
158 private JSONWriter end(char m, char c) throws JSONException {
159 if (this.mode != m) {
160 throw new JSONException(m == 'o' ? "Misplaced endObject." :
161 "Misplaced endArray.");
162 }
163 this.pop(m);
164 try {
165 this.writer.write(c);
166 } catch (IOException e) {
167 throw new JSONException(e);
168 }
169 this.comma = true;
170 return this;
171 }
172
173 /**
174 * End an array. This method most be called to balance calls to
175 * <code>array</code>.
176 * @return this
177 * @throws JSONException If incorrectly nested.
178 */
179 public JSONWriter endArray() throws JSONException {
180 return this.end('a', ']');
181 }
182
183 /**
184 * End an object. This method most be called to balance calls to
185 * <code>object</code>.
186 * @return this
187 * @throws JSONException If incorrectly nested.
188 */
189 public JSONWriter endObject() throws JSONException {
190 return this.end('k', '}');
191 }
192
193 /**
194 * Append a key. The key will be associated with the next value. In an
195 * object, every value must be preceded by a key.
196 * @param s A key string.
197 * @return this
198 * @throws JSONException If the key is out of place. For example, keys
199 * do not belong in arrays or if the key is null.
200 */
201 public JSONWriter key(String s) throws JSONException {
202 if (s == null) {
203 throw new JSONException("Null key.");
204 }
205 if (this.mode == 'k') {
206 try {
207 if (this.comma) {
208 this.writer.write(',');
209 }
210 this.writer.write(JSONObject.quote(s));
211 this.writer.write(':');
212 this.comma = false;
213 this.mode = 'o';
214 return this;
215 } catch (IOException e) {
216 throw new JSONException(e);
217 }
218 }
219 throw new JSONException("Misplaced key.");
220 }
221
222
223 /**
224 * Begin appending a new object. All keys and values until the balancing
225 * <code>endObject</code> will be appended to this object. The
226 * <code>endObject</code> method must be called to mark the object's end.
227 * @return this
228 * @throws JSONException If the nesting is too deep, or if the object is
229 * started in the wrong place (for example as a key or after the end of the
230 * outermost array or object).
231 */
232 public JSONWriter object() throws JSONException {
233 if (this.mode == 'i') {
234 this.mode = 'o';
235 }
236 if (this.mode == 'o' || this.mode == 'a') {
237 this.append("{");
238 this.push('k');
239 this.comma = false;
240 return this;
241 }
242 throw new JSONException("Misplaced object.");
243
244 }
245
246
247 /**
248 * Pop an array or object scope.
249 * @param c The scope to close.
250 * @throws JSONException If nesting is wrong.
251 */
252 private void pop(char c) throws JSONException {
253 if (this.top <= 0 || this.stack[this.top - 1] != c) {
254 throw new JSONException("Nesting error.");
255 }
256 this.top -= 1;
257 this.mode = this.top == 0 ? 'd' : this.stack[this.top - 1];
258 }
259
260 /**
261 * Push an array or object scope.
262 * @param c The scope to open.
263 * @throws JSONException If nesting is too deep.
264 */
265 private void push(char c) throws JSONException {
266 if (this.top >= maxdepth) {
267 throw new JSONException("Nesting too deep.");
268 }
269 this.stack[this.top] = c;
270 this.mode = c;
271 this.top += 1;
272 }
273
274
275 /**
276 * Append either the value <code>true</code> or the value
277 * <code>false</code>.
278 * @param b A boolean.
279 * @return this
280 * @throws JSONException
281 */
282 public JSONWriter value(boolean b) throws JSONException {
283 return this.append(b ? "true" : "false");
284 }
285
286 /**
287 * Append a double value.
288 * @param d A double.
289 * @return this
290 * @throws JSONException If the number is not finite.
291 */
292 public JSONWriter value(double d) throws JSONException {
293 return this.value(new Double(d));
294 }
295
296 /**
297 * Append a long value.
298 * @param l A long.
299 * @return this
300 * @throws JSONException
301 */
302 public JSONWriter value(long l) throws JSONException {
303 return this.append(Long.toString(l));
304 }
305
306
307 /**
308 * Append an object value.
309 * @param o The object to append. It can be null, or a Boolean, Number,
310 * String, JSONObject, or JSONArray.
311 * @return this
312 * @throws JSONException If the value is out of sequence.
313 */
314 public JSONWriter value(Object o) throws JSONException {
315 return this.append(JSONObject.valueToString(o));
316 }
317 }