PageRenderTime 43ms CodeModel.GetById 19ms app.highlight 20ms RepoModel.GetById 1ms app.codeStats 0ms

/interpreter/tags/at2dist220411/src/edu/vub/at/trace/JSONWriter.java

http://ambienttalk.googlecode.com/
Java | 424 lines | 270 code | 44 blank | 110 comment | 21 complexity | 6227e64283ebb0dfd2333b1e6dd1177c MD5 | raw file
  1/**
  2 * AmbientTalk/2 Project
  3 * (c) Software Languages Lab, 2006 - 2009
  4 * Authors: Ambient Group at SOFT
  5 * 
  6 * The source code in this file is based on source code from Tyler Close's
  7 * Waterken server, Copyright 2008 Waterken Inc. Waterken's code is published
  8 * under the MIT license.
  9 * 
 10 * Permission is hereby granted, free of charge, to any person
 11 * obtaining a copy of this software and associated documentation
 12 * files (the "Software"), to deal in the Software without
 13 * restriction, including without limitation the rights to use,
 14 * copy, modify, merge, publish, distribute, sublicense, and/or
 15 * sell copies of the Software, and to permit persons to whom the
 16 * Software is furnished to do so, subject to the following
 17 * conditions:
 18 *
 19 * The above copyright notice and this permission notice shall be
 20 * included in all copies or substantial portions of the Software.
 21 *
 22 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 23 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 24 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 25 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 26 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 27 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 28 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 29 * OTHER DEALINGS IN THE SOFTWARE.
 30 */
 31package edu.vub.at.trace;
 32
 33import java.io.IOException;
 34import java.io.Writer;
 35
 36/**
 37 * A SAX-like API for generating JSON text.
 38 * <p>
 39 * A client can only output a syntactically correct JSON text, or leave the
 40 * {@link JSONWriter} in a {@linkplain #isWritten detectable} error state. The
 41 * implementation does <em>not</em> enforce the constraint that names within an
 42 * object SHOULD be unique.
 43 * </p>
 44 * <p>For example, to output the JSON text:</p>
 45 * <pre>
 46 * {
 47 *   "title" : "I Can Has Cheezburger?",
 48 *   "src" : { "@" : "http://www.example.com/image/481989943" },
 49 *   "height" : 125,
 50 *   "width" : 100,
 51 *   "tags" : [ "lolcat", "food" ],
 52 *   "score" : 9.5
 53 * }
 54 * </pre>
 55 * <p>, write code:</p>
 56 * <pre>
 57 * final Writer text = &hellip;
 58 * final JSONWriter top = JSONWriter.make(text);
 59 * final JSONWriter.ObjectWriter o = top.startObject();
 60 * o.startMember("title").writeString("I Can Has Cheezburger?");
 61 * o.startMember("src").writeLink("http://www.example.com/image/481989943");
 62 * o.startMember("height").writeInt(125);
 63 * o.startMember("width").writeInt(100);
 64 * final JSONWriter.ArrayWriter tags = o.startMember("tags").startArray();
 65 * tags.startElement().writeString("lolcat");
 66 * tags.startElement().writeString("food");
 67 * tags.finish();
 68 * o.startMember("score").writeDouble(9.5);
 69 * o.finish();
 70 * if (!top.isWritten()) { throw new NullPointerException(); }
 71 * text.flush();
 72 * text.close();
 73 * </pre>
 74 * <p>
 75 * An invalid sequence of calls to this API will cause a
 76 * {@link NullPointerException} to be thrown. For example, calling
 77 * writeString() twice in succession will throw a {@link NullPointerException}.
 78 * </p>
 79 */
 80public final class JSONWriter {
 81    static private final String newLine = "\r\n";
 82    static private final String tab = "  ";
 83
 84    static private final class Prize {
 85        private Object value;
 86
 87        protected Prize(final Object value) {
 88            this.value = value;
 89        }
 90
 91        protected Object claim() {
 92            final Object r = value;
 93            value = null;
 94            return r;
 95        }
 96    }
 97
 98    static private final class Milestone {
 99        private boolean marked;
100
101        protected
102        Milestone(final boolean marked) {
103            this.marked = marked;
104        }
105
106        protected boolean
107        is() { return marked; }
108
109        protected void
110        mark() { marked = true; }
111    }
112
113    private final boolean top;          // Is this the top level JSON container?
114    private final String indent;        // indentation for this JSON value
115    // Prize<Writer> output;
116    private final Prize output;         // claimed by 1st called output method
117    private final Milestone written;    // marked after output method is done  
118
119    private JSONWriter(final boolean top, final String indent, final Writer out) {
120        this.top = top;
121        this.indent = indent;
122        output = new Prize(out);
123        written = new Milestone(null == out);
124    }
125    
126    /**
127     * Constructs a JSON writer.
128     * @param out   UTF-8 output stream
129     */
130    static public JSONWriter make(final Writer out) { return new JSONWriter(true, "", out); }
131
132    /**
133     * @return <code>true</code> if a single JSON value was successfully
134     *         written, else <code>false</code>
135     */
136    public boolean isWritten() { return written.is(); }
137
138    public ObjectWriter startObject() throws IOException {
139        final Writer out = (Writer) output.claim();
140        out.write('{');
141        return new ObjectWriter(out);
142    }
143
144    /**
145     * A JSON <em>object</em> writer.
146     */
147    public final class ObjectWriter {
148        static private final String comma = "," + newLine;
149
150        private final String inset;         // indentation for each member
151        private final Writer out;
152        private       String prefix;        // current member separator prefix
153        private       JSONWriter member;    // most recent member started, or
154                                            // null if object is finished
155
156        protected ObjectWriter(final Writer out) {
157            inset = indent + tab;
158            this.out = out;
159            prefix = newLine;
160            member = new JSONWriter(false, inset, null);
161        }
162
163        public void finish() throws IOException {
164            if (!member.isWritten()) { throw new NullPointerException(); }
165
166            member = null;      // prevent future calls to this object
167            out.write(newLine);
168            out.write(indent);
169            out.write('}');
170            if (top) { out.write(newLine); }
171            written.mark();     // mark the containing value successful
172        }
173
174        public JSONWriter startMember(final String name) throws IOException {
175            if (!member.isWritten()) { throw new NullPointerException(); }
176
177            member = new JSONWriter(false, inset, out); // prevent calls until
178                                                        // member is completed
179            out.write(prefix);
180            out.write(inset);
181            writeStringTo(name, out);
182            out.write(" : ");
183            prefix = comma;
184            return member;
185        }
186        
187        /*
188         * Every output method on a JSONWriter begins by deleting the output
189         * stream from that writer. If the created JSON value is an object or an
190         * array, the output stream is handed off to either an ObjectWriter or
191         * an ArrayWriter. These JSON container writers hold onto the output
192         * stream forever, but know whether or not they should be allowed to
193         * write to it. Each container does this by remembering its most
194         * recently created child value and only writing to the output stream if
195         * that child has been written and the container itself has not been
196         * finished. At any time, there may be multiple unfinished containers,
197         * but only one of them could have a written child, since a JSON
198         * structure is a tree and an unfinished container value is not marked
199         * as written.
200         */
201    }
202
203    public ArrayWriter startArray() throws IOException {
204        final Writer out = (Writer) output.claim();
205        out.write('[');
206        return new ArrayWriter(out);
207    }
208
209    /**
210     * A JSON <em>array</em> writer.
211     */
212    public final class ArrayWriter {
213        static private final String comma = ", ";
214
215        private final String inset;         // indentation for each element
216        private final Writer out;
217        private       String prefix;        // current element separator prefix
218        private       JSONWriter element;   // most recent element started, or
219                                            // null if array is finished
220
221        protected ArrayWriter(final Writer out) {
222            inset = indent + tab;
223            this.out = out;
224            prefix = " ";
225            element = new JSONWriter(false, inset, null);
226        }
227
228        public void finish() throws IOException {
229            if (!element.isWritten()) { throw new NullPointerException(); }
230
231            element = null;     // prevent future calls to this object
232            out.write(" ]");
233            if (top) { out.write(newLine); }
234            written.mark();     // mark the containing value successful
235        }
236
237        public JSONWriter
238        startElement() throws IOException {
239            if (!element.isWritten()) { throw new NullPointerException(); }
240
241            element = new JSONWriter(false, inset, out); // prevent calls until
242                                                         // element is completed
243            out.write(prefix);
244            prefix = comma;
245            return element;
246        }
247    }
248
249    public void
250    writeLink(final String URL) throws IOException {
251        final Writer out = (Writer) output.claim();
252        out.write("{ \"@\" : ");
253        writeStringTo(URL, out);
254        out.write(" }");
255        if (top) { out.write(newLine); }
256        written.mark();
257    }
258
259    public void
260    writeNull() throws IOException {
261        writePrimitive("null");
262    }
263
264    public void
265    writeBoolean(final boolean value) throws IOException {
266        writePrimitive(value ? "true" : "false");
267    }
268
269    public void
270    writeInt(final int value) throws IOException {
271        writePrimitive(Integer.toString(value));
272    }
273    
274    /**
275     * maximum magnitude of a Javascript number: {@value}
276     */
277    static public final long maxMagnitude = (1L << 53) - 1; // = 2^53 - 1
278
279    public void
280    writeLong(final long value) throws ArithmeticException, IOException {
281        if (value > maxMagnitude) { throw new ArithmeticException(); }
282        if (value < -maxMagnitude) { throw new ArithmeticException(); }
283
284        writePrimitive(Long.toString(value));
285    }
286
287    public void
288    writeFloat(final float value) throws ArithmeticException, IOException {
289        if (Float.isNaN(value)) { throw new ArithmeticException(); }
290        if (Float.isInfinite(value)) { throw new ArithmeticException(); }
291        
292        writePrimitive(Float.toString(value));
293    }
294
295    public void
296    writeDouble(final double value) throws ArithmeticException, IOException {
297        if (Double.isNaN(value)) { throw new ArithmeticException(); }
298        if (Double.isInfinite(value)) { throw new ArithmeticException(); }
299        
300        writePrimitive(Double.toString(value));
301    }
302    
303    private void
304    writePrimitive(final String value) throws IOException {
305        final Writer out = (Writer) output.claim();
306        if (top) {
307            out.write("{ \"=\" : ");
308            out.write(value);
309            out.write(" }");
310            out.write(newLine);
311        } else {
312            out.write(value);
313        }
314        written.mark();
315    }
316
317    public void
318    writeString(final String value) throws IOException {
319        final Writer out = (Writer) output.claim();
320        if (top) {
321            out.write("{ \"=\" : ");
322            writeStringTo(value, out);
323            out.write(" }");
324            out.write(newLine);
325        } else {
326            writeStringTo(value, out);
327        }
328        written.mark();
329    }
330
331    static private void
332    writeStringTo(final String value, final Writer out) throws IOException {
333        out.write('\"');
334        char previous = '\0';
335        final int len = value.length();
336        for (int i = 0; i != len; ++i) {
337            final char c = value.charAt(i);
338            switch (c) {
339            case '\"':
340                out.write("\\\"");
341                break;
342            case '\\':
343                out.write("\\\\");
344                break;
345            case '\b':
346                out.write("\\b");
347                break;
348            case '\f':
349                out.write("\\f");
350                break;
351            case '\n':
352                out.write("\\n");
353                break;
354            case '\r':
355                out.write("\\r");
356                break;
357            case '\t':
358                out.write("\\t");
359                break;
360            // begin: HTML escaping
361            case '/':
362                if ('<' == previous) { out.write('\\'); }
363                out.write(c);
364                break;
365            // need at least the above check, but paranoia demands more
366            case '<':
367                out.write("\\u003C");
368                break;
369            case '>':
370                out.write("\\u003E");
371                break;
372            // end: HTML escaping
373            case ' ':
374                out.write(c);
375                break;
376            default:
377                switch (Character.getType(c)) {
378                case Character.UPPERCASE_LETTER:
379                case Character.LOWERCASE_LETTER:
380                case Character.TITLECASE_LETTER:
381                case Character.MODIFIER_LETTER:
382                case Character.OTHER_LETTER:
383                case Character.NON_SPACING_MARK:
384                case Character.ENCLOSING_MARK:
385                case Character.COMBINING_SPACING_MARK:
386                case Character.DECIMAL_DIGIT_NUMBER:
387                case Character.LETTER_NUMBER:
388                case Character.OTHER_NUMBER:
389                case Character.DASH_PUNCTUATION:
390                case Character.START_PUNCTUATION:
391                case Character.END_PUNCTUATION:
392                case Character.CONNECTOR_PUNCTUATION:
393                case Character.OTHER_PUNCTUATION:
394                case Character.MATH_SYMBOL:
395                case Character.CURRENCY_SYMBOL:
396                case Character.MODIFIER_SYMBOL:
397                case Character.INITIAL_QUOTE_PUNCTUATION:
398                case Character.FINAL_QUOTE_PUNCTUATION:
399                    out.write(c);
400                    break;
401                case Character.UNASSIGNED:
402                case Character.SPACE_SEPARATOR:
403                case Character.LINE_SEPARATOR:
404                case Character.PARAGRAPH_SEPARATOR:
405                case Character.CONTROL:                 // includes '\u0085'
406                case Character.FORMAT:
407                case Character.PRIVATE_USE:
408                case Character.SURROGATE:
409                case Character.OTHER_SYMBOL:
410                default:
411                    out.write("\\u");
412                    final int unicode = c;
413                    for (int shift = Character.SIZE; 0 != shift;) {
414                        shift -= 4;
415                        final int hex = (unicode >> shift) & 0x0F;
416                        out.write(hex < 10 ? '0' + hex : 'A' + (hex - 10));
417                    }
418                }
419            }
420            previous = c;
421        }
422        out.write('\"');
423    }
424}