001 // Copyright 2008 Waterken Inc. under the terms of the MIT X license
002 // found at http://www.opensource.org/licenses/mit-license.html
003 package org.waterken.syntax.json;
004
005 import java.io.IOException;
006 import java.io.Writer;
007
008 /**
009 * A SAX-like API for generating JSON text.
010 * <p>
011 * A client can only output a syntactically correct JSON text, or leave the
012 * {@link JSONWriter} in a {@linkplain #isWritten detectable} error state. The
013 * implementation does <em>not</em> enforce the constraint that names within an
014 * object SHOULD be unique.
015 * </p>
016 * <p>For example, to output the JSON text:</p>
017 * <pre>
018 * {
019 * "title" : "I Can Has Cheezburger?",
020 * "src" : { "@" : "http://www.example.com/image/481989943" },
021 * "height" : 125,
022 * "width" : 100,
023 * "tags" : [ "lolcat", "food" ],
024 * "score" : 9.5
025 * }
026 * </pre>
027 * <p>, write code:</p>
028 * <pre>
029 * final Writer text = …
030 * final JSONWriter top = JSONWriter.make(text);
031 * final JSONWriter.ObjectWriter o = top.startObject();
032 * o.startMember("title").writeString("I Can Has Cheezburger?");
033 * o.startMember("src").writeLink("http://www.example.com/image/481989943");
034 * o.startMember("height").writeInt(125);
035 * o.startMember("width").writeInt(100);
036 * final JSONWriter.ArrayWriter tags = o.startMember("tags").startArray();
037 * tags.startElement().writeString("lolcat");
038 * tags.startElement().writeString("food");
039 * tags.finish();
040 * o.startMember("score").writeDouble(9.5);
041 * o.finish();
042 * if (!top.isWritten()) { throw new NullPointerException(); }
043 * text.flush();
044 * text.close();
045 * </pre>
046 * <p>
047 * An invalid sequence of calls to this API will cause a
048 * {@link NullPointerException} to be thrown. For example, calling
049 * writeString() twice in succession will throw a {@link NullPointerException}.
050 * </p>
051 */
052 public final class
053 JSONWriter {
054 static private final String newLine = "\r\n";
055 static private final String tab = " ";
056
057 static private final class
058 Prize<T> {
059 private T value;
060
061 protected
062 Prize(final T value) {
063 this.value = value;
064 }
065
066 protected T
067 claim() {
068 final T r = value;
069 value = null;
070 return r;
071 }
072 }
073
074 static private final class
075 Milestone {
076 private boolean marked;
077
078 protected
079 Milestone(final boolean marked) {
080 this.marked = marked;
081 }
082
083 protected boolean
084 is() { return marked; }
085
086 protected void
087 mark() { marked = true; }
088 }
089
090 protected final boolean top; // Is this the top level JSON container?
091 protected final String indent; // indentation for this JSON value
092 private final Prize<Writer> output; // claimed by 1st called output method
093 protected final Milestone written; // marked after output method is done
094
095 protected
096 JSONWriter(final boolean top, final String indent, final Writer out) {
097 this.top = top;
098 this.indent = indent;
099 output = new Prize<Writer>(out);
100 written = new Milestone(null == out);
101 }
102
103 /**
104 * Constructs a JSON writer.
105 * @param out UTF-8 output stream
106 */
107 static public JSONWriter
108 make(final Writer out) { return new JSONWriter(true, "", out); }
109
110 /**
111 * @return {@code true} if a single JSON value was successfully written,
112 * else {@code false}
113 */
114 public boolean
115 isWritten() { return written.is(); }
116
117 public ObjectWriter
118 startObject() throws IOException {
119 final Writer out = output.claim();
120 out.write('{');
121 return new ObjectWriter(out);
122 }
123
124 /**
125 * A JSON <em>object</em> writer.
126 */
127 public final class
128 ObjectWriter {
129 static private final String comma = "," + newLine;
130
131 private final String inset; // indentation for each member
132 private final Writer out;
133 private String prefix; // current member separator prefix
134 private JSONWriter member; // most recent member started, or
135 // null if object is finished
136
137 protected
138 ObjectWriter(final Writer out) {
139 inset = indent + tab;
140 this.out = out;
141 prefix = newLine;
142 member = new JSONWriter(false, inset, null);
143 }
144
145 public void
146 finish() throws IOException {
147 if (!member.isWritten()) { throw new NullPointerException(); }
148
149 member = null; // prevent future calls to this object
150 out.write(newLine);
151 out.write(indent);
152 out.write('}');
153 if (top) { out.write(newLine); }
154 written.mark(); // mark the containing value successful
155 }
156
157 public JSONWriter
158 startMember(final String name) throws IOException {
159 if (!member.isWritten()) { throw new NullPointerException(); }
160
161 member = new JSONWriter(false, inset, out); // prevent calls until
162 // member is completed
163 out.write(prefix);
164 out.write(inset);
165 writeStringTo(name, out);
166 out.write(" : ");
167 prefix = comma;
168 return member;
169 }
170
171 /*
172 * Every output method on a JSONWriter begins by deleting the output
173 * stream from that writer. If the created JSON value is an object or an
174 * array, the output stream is handed off to either an ObjectWriter or
175 * an ArrayWriter. These JSON container writers hold onto the output
176 * stream forever, but know whether or not they should be allowed to
177 * write to it. Each container does this by remembering its most
178 * recently created child value and only writing to the output stream if
179 * that child has been written and the container itself has not been
180 * finished. At any time, there may be multiple unfinished containers,
181 * but only one of them could have a written child but not itself be
182 * finished, since a JSON structure is a tree and an unfinished
183 * container value is not marked as written.
184 */
185 }
186
187 public ArrayWriter
188 startArray() throws IOException {
189 final Writer out = output.claim();
190 out.write('[');
191 return new ArrayWriter(out);
192 }
193
194 /**
195 * A JSON <em>array</em> writer.
196 */
197 public final class
198 ArrayWriter {
199 static private final String comma = ", ";
200
201 private final String inset; // indentation for each element
202 private final Writer out;
203 private String prefix; // current element separator prefix
204 private JSONWriter element; // most recent element started, or
205 // null if array is finished
206
207 protected
208 ArrayWriter(final Writer out) {
209 inset = indent + tab;
210 this.out = out;
211 prefix = " ";
212 element = new JSONWriter(false, inset, null);
213 }
214
215 public void
216 finish() throws IOException {
217 if (!element.isWritten()) { throw new NullPointerException(); }
218
219 element = null; // prevent future calls to this object
220 out.write(" ]");
221 if (top) { out.write(newLine); }
222 written.mark(); // mark the containing value successful
223 }
224
225 public JSONWriter
226 startElement() throws IOException {
227 if (!element.isWritten()) { throw new NullPointerException(); }
228
229 element = new JSONWriter(false, inset, out); // prevent calls until
230 // element is completed
231 out.write(prefix);
232 prefix = comma;
233 return element;
234 }
235 }
236
237 public void
238 writeLink(final String URL) throws IOException {
239 final Writer out = output.claim();
240 out.write("{ \"@\" : ");
241 writeStringTo(URL, out);
242 out.write(" }");
243 if (top) { out.write(newLine); }
244 written.mark();
245 }
246
247 public void
248 writeNull() throws IOException {
249 writePrimitive("null");
250 }
251
252 public void
253 writeBoolean(final boolean value) throws IOException {
254 writePrimitive(value ? "true" : "false");
255 }
256
257 public void
258 writeInt(final int value) throws IOException {
259 writePrimitive(Integer.toString(value));
260 }
261
262 /**
263 * maximum magnitude of a JavaScript number: {@value}
264 */
265 static public final long maxMagnitude = 1L << 53; // = 2^53
266
267 /**
268 * Signals a number that cannot be represented in JavaScript.
269 */
270 static public final ArithmeticException NaN = new ArithmeticException();
271
272 public void
273 writeLong(final long value) throws ArithmeticException, IOException {
274 if (value > maxMagnitude) { throw NaN; }
275 if (value < -maxMagnitude) { throw NaN; }
276
277 writePrimitive(Long.toString(value));
278 }
279
280 public void
281 writeFloat(final float value) throws ArithmeticException, IOException {
282 if (Float.isNaN(value)) { throw NaN; }
283 if (Float.isInfinite(value)) { throw NaN; }
284
285 writePrimitive(Float.toString(value));
286 }
287
288 public void
289 writeDouble(final double value) throws ArithmeticException, IOException {
290 if (Double.isNaN(value)) { throw NaN; }
291 if (Double.isInfinite(value)) { throw NaN; }
292
293 writePrimitive(Double.toString(value));
294 }
295
296 private void
297 writePrimitive(final String value) throws IOException {
298 final Writer out = output.claim();
299 if (top) {
300 out.write("{ \"=\" : ");
301 out.write(value);
302 out.write(" }");
303 out.write(newLine);
304 } else {
305 out.write(value);
306 }
307 written.mark();
308 }
309
310 public void
311 writeString(final String value) throws IOException {
312 final Writer out = output.claim();
313 if (top) {
314 out.write("{ \"=\" : ");
315 writeStringTo(value, out);
316 out.write(" }");
317 out.write(newLine);
318 } else {
319 writeStringTo(value, out);
320 }
321 written.mark();
322 }
323
324 static protected void
325 writeStringTo(final String value, final Writer out) throws IOException {
326 out.write('\"');
327 char previous = '\0';
328 final int len = value.length();
329 for (int i = 0; i != len; ++i) {
330 final char c = value.charAt(i);
331 switch (c) {
332 case '\"':
333 out.write("\\\"");
334 break;
335 case '\\':
336 out.write("\\\\");
337 break;
338 case '\b':
339 out.write("\\b");
340 break;
341 case '\f':
342 out.write("\\f");
343 break;
344 case '\n':
345 out.write("\\n");
346 break;
347 case '\r':
348 out.write("\\r");
349 break;
350 case '\t':
351 out.write("\\t");
352 break;
353 // begin: HTML escaping
354 case '/':
355 if ('<' == previous) { out.write('\\'); }
356 out.write(c);
357 break;
358 // need at least the above check, but paranoia demands more
359 case '<':
360 out.write("\\u003C");
361 break;
362 case '>':
363 out.write("\\u003E");
364 break;
365 // end: HTML escaping
366 case ' ':
367 out.write(c);
368 break;
369 default:
370 switch (Character.getType(c)) {
371 case Character.UPPERCASE_LETTER:
372 case Character.LOWERCASE_LETTER:
373 case Character.TITLECASE_LETTER:
374 case Character.MODIFIER_LETTER:
375 case Character.OTHER_LETTER:
376 case Character.NON_SPACING_MARK:
377 case Character.ENCLOSING_MARK:
378 case Character.COMBINING_SPACING_MARK:
379 case Character.DECIMAL_DIGIT_NUMBER:
380 case Character.LETTER_NUMBER:
381 case Character.OTHER_NUMBER:
382 case Character.DASH_PUNCTUATION:
383 case Character.START_PUNCTUATION:
384 case Character.END_PUNCTUATION:
385 case Character.CONNECTOR_PUNCTUATION:
386 case Character.OTHER_PUNCTUATION:
387 case Character.MATH_SYMBOL:
388 case Character.CURRENCY_SYMBOL:
389 case Character.MODIFIER_SYMBOL:
390 case Character.INITIAL_QUOTE_PUNCTUATION:
391 case Character.FINAL_QUOTE_PUNCTUATION:
392 out.write(c);
393 break;
394 case Character.UNASSIGNED:
395 case Character.SPACE_SEPARATOR:
396 case Character.LINE_SEPARATOR:
397 case Character.PARAGRAPH_SEPARATOR:
398 case Character.CONTROL: // includes '\u0085'
399 case Character.FORMAT:
400 case Character.PRIVATE_USE:
401 case Character.SURROGATE:
402 case Character.OTHER_SYMBOL:
403 default:
404 out.write("\\u");
405 final int unicode = c;
406 for (int shift = Character.SIZE; 0 != shift;) {
407 shift -= 4;
408 final int hex = (unicode >> shift) & 0x0F;
409 out.write(hex < 10 ? '0' + hex : 'A' + (hex - 10));
410 }
411 }
412 }
413 previous = c;
414 }
415 out.write('\"');
416 }
417 }