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 }