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 = &hellip;
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    }