001    /*
002     * Copyright (c) 2007 Mozilla Foundation
003     *
004     * Permission is hereby granted, free of charge, to any person obtaining a 
005     * copy of this software and associated documentation files (the "Software"), 
006     * to deal in the Software without restriction, including without limitation 
007     * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
008     * and/or sell copies of the Software, and to permit persons to whom the 
009     * Software is furnished to do so, subject to the following conditions:
010     *
011     * The above copyright notice and this permission notice shall be included in 
012     * all copies or substantial portions of the Software.
013     *
014     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
015     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
016     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
017     * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
018     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
019     * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
020     * DEALINGS IN THE SOFTWARE.
021     */
022    
023    package nu.validator.json;
024    
025    import java.io.IOException;
026    import java.io.OutputStream;
027    import java.io.OutputStreamWriter;
028    import java.io.Writer;
029    import java.nio.charset.Charset;
030    import java.nio.charset.CharsetEncoder;
031    import java.nio.charset.CodingErrorAction;
032    import java.util.ArrayList;
033    import java.util.List;
034    
035    import org.xml.sax.SAXException;
036    
037    public class Serializer implements JsonHandler {
038    
039        private enum State {
040            INITIAL, DOCUMENT, ARRAY, OBJECT, VALUE, STRING
041        }
042    
043        private final List<State> stack = new ArrayList<State>();
044    
045        private boolean hadCallback = false;
046    
047        private boolean first = false;
048    
049        private final Writer writer;
050    
051        private static Writer newOutputStreamWriter(OutputStream out) {
052            CharsetEncoder enc = Charset.forName("UTF-8").newEncoder();
053            enc.onMalformedInput(CodingErrorAction.REPLACE);
054            enc.onUnmappableCharacter(CodingErrorAction.REPLACE);
055            return new OutputStreamWriter(out, enc);
056        }
057        
058        public Serializer(OutputStream out) {
059            this.writer = newOutputStreamWriter(out);
060            push(State.INITIAL);
061        }
062    
063        private void push(State state) {
064            stack.add(state);
065        }
066    
067        private void pop() {
068            stack.remove(stack.size() - 1);
069        }
070    
071        private State peek() {
072            int size = stack.size();
073            if (size == 0) {
074                return null;
075            } else {
076                return stack.get(size - 1);
077            }
078        }
079    
080        public void bool(boolean bool) throws SAXException {
081            try {
082                State state = peek();
083                switch (state) {
084                    case ARRAY:
085                        if (!first) {
086                            writer.write(',');
087                        }
088                        // fall thru
089                    case DOCUMENT:
090                    case VALUE:
091                        writer.write(Boolean.toString(bool));
092                        if (state == State.VALUE) {
093                            pop();
094                        }
095                        first = false;
096                        break;
097                    default:
098                        throw new SAXException("Illegal state for callback.");
099                }
100            } catch (IOException e) {
101                throw new SAXException(e.getMessage(), e);
102            }
103        }
104    
105        private void charactersImpl(char[] ch, int start, int length)
106                throws IOException {
107            int s = start;
108            int end = start + length;
109            for (int i = start; i < end; i++) {
110                char c = ch[i];
111                if (c <= '\u001F' || c == '\"' || c == '\\') {
112                    if (s < i) {
113                        writer.write(ch, s, i - s);
114                    }
115                    s = i + 1;
116                    writer.write('\\');
117                    switch (c) {
118                        case '\"':
119                            writer.write('\"');
120                            break;
121                        case '\\':
122                            writer.write('\\');
123                            break;
124                        case '\u0008':
125                            writer.write('b');
126                            break;
127                        case '\u000C':
128                            writer.write('f');
129                            break;
130                        case '\n':
131                            writer.write('n');
132                            break;
133                        case '\r':
134                            writer.write('r');
135                            break;
136                        case '\t':
137                            writer.write('t');
138                            break;
139                        default:
140                            String hex = Integer.toHexString(c);
141                            if (hex.length() == 1) {
142                                writer.write("u000");
143                                writer.write(hex);
144                            } else {
145                                writer.write("u00");
146                                writer.write(hex);                            
147                            }
148                            break;
149                    }
150                }
151            }
152            if (s < end) {
153                writer.write(ch, s, end - s);
154            }
155        }
156    
157        public void characters(char[] ch, int start, int length)
158                throws SAXException {
159            try {
160                State state = peek();
161                switch (state) {
162                    case STRING:
163                        charactersImpl(ch, start, length);
164                        break;
165                    default:
166                        throw new SAXException("Illegal state for callback.");
167                }
168            } catch (IOException e) {
169                throw new SAXException(e.getMessage(), e);
170            }
171        }
172    
173        public void endArray() throws SAXException {
174            try {
175                State state = peek();
176                switch (state) {
177                    case ARRAY:
178                        writer.write(']');
179                        pop();
180                        first = false;
181                        if (peek() == State.VALUE) {
182                            pop();
183                        }
184                        break;
185                    default:
186                        throw new SAXException("Illegal state for callback.");
187                }
188            } catch (IOException e) {
189                throw new SAXException(e.getMessage(), e);
190            }
191        }
192    
193        public void endDocument() throws SAXException {
194            try {
195                State state = peek();
196                switch (state) {
197                    case DOCUMENT:
198                        if (hadCallback) {
199                            writer.write(')');
200                        }
201                        writer.write('\n');
202                        writer.flush();
203                        writer.close();
204                        pop();
205                        break;
206                    default:
207                        throw new SAXException("Illegal state for callback.");
208                }
209            } catch (IOException e) {
210                throw new SAXException(e.getMessage(), e);
211            }
212        }
213    
214        public void endObject() throws SAXException {
215            try {
216                State state = peek();
217                switch (state) {
218                    case OBJECT:
219                        writer.write('}');
220                        pop();
221                        first = false;
222                        if (peek() == State.VALUE) {
223                            pop();
224                        }
225                        break;
226                    default:
227                        throw new SAXException("Illegal state for callback.");
228                }
229            } catch (IOException e) {
230                throw new SAXException(e.getMessage(), e);
231            }
232        }
233    
234        public void endString() throws SAXException {
235            try {
236                State state = peek();
237                switch (state) {
238                    case STRING:
239                        writer.write('\"');
240                        pop();
241                        first = false;
242                        if (peek() == State.VALUE) {
243                            pop();
244                        }
245                        break;
246                    default:
247                        throw new SAXException("Illegal state for callback.");
248                }
249            } catch (IOException e) {
250                throw new SAXException(e.getMessage(), e);
251            }
252        }
253    
254        public void key(String key) throws SAXException {
255            try {
256                State state = peek();
257                switch (state) {
258                    case OBJECT:
259                        if (!first) {
260                            writer.write(',');
261                        }
262                        writer.write('\"');
263                        charactersImpl(key.toCharArray(), 0, key.length());
264                        writer.write('\"');
265                        writer.write(':');
266                        push(State.VALUE);
267                        break;
268                    default:
269                        throw new SAXException("Illegal state for callback.");
270                }
271            } catch (IOException e) {
272                throw new SAXException(e.getMessage(), e);
273            }
274        }
275    
276        public void number(int number) throws SAXException {
277            try {
278                State state = peek();
279                switch (state) {
280                    case ARRAY:
281                        if (!first) {
282                            writer.write(',');
283                        }
284                        // fall thru
285                    case DOCUMENT:
286                    case VALUE:
287                        writer.write(Integer.toString(number));
288                        if (state == State.VALUE) {
289                            pop();
290                        }
291                        first = false;
292                        break;
293                    default:
294                        throw new SAXException("Illegal state for callback.");
295                }
296            } catch (IOException e) {
297                throw new SAXException(e.getMessage(), e);
298            }
299        }
300    
301        public void number(long number) throws SAXException {
302            try {
303                State state = peek();
304                switch (state) {
305                    case ARRAY:
306                        if (!first) {
307                            writer.write(',');
308                        }
309                        // fall thru
310                    case DOCUMENT:
311                    case VALUE:
312                        writer.write(Long.toString(number));
313                        if (state == State.VALUE) {
314                            pop();
315                        }
316                        first = false;
317                        break;
318                    default:
319                        throw new SAXException("Illegal state for callback.");
320                }
321            } catch (IOException e) {
322                throw new SAXException(e.getMessage(), e);
323            }
324        }
325    
326        public void number(float number) throws SAXException {
327            try {
328                State state = peek();
329                switch (state) {
330                    case ARRAY:
331                        if (!first) {
332                            writer.write(',');
333                        }
334                        // fall thru
335                    case DOCUMENT:
336                    case VALUE:
337                        writer.write(Float.toString(number));
338                        if (state == State.VALUE) {
339                            pop();
340                        }
341                        first = false;
342                        break;
343                    default:
344                        throw new SAXException("Illegal state for callback.");
345                }
346            } catch (IOException e) {
347                throw new SAXException(e.getMessage(), e);
348            }
349        }
350    
351        public void number(double number) throws SAXException {
352            try {
353                State state = peek();
354                switch (state) {
355                    case ARRAY:
356                        if (!first) {
357                            writer.write(',');
358                        }
359                        // fall thru
360                    case DOCUMENT:
361                    case VALUE:
362                        writer.write(Double.toString(number));
363                        if (state == State.VALUE) {
364                            pop();
365                        }
366                        first = false;
367                        break;
368                    default:
369                        throw new SAXException("Illegal state for callback.");
370                }
371            } catch (IOException e) {
372                throw new SAXException(e.getMessage(), e);
373            }
374        }
375    
376        public void startArray() throws SAXException {
377            try {
378                State state = peek();
379                switch (state) {
380                    case ARRAY:
381                        if (!first) {
382                            writer.write(',');
383                        }
384                        // fall thru
385                    case DOCUMENT:
386                    case VALUE:
387                        writer.write('[');
388                        push(State.ARRAY);
389                        first = true;
390                        break;
391                    default:
392                        throw new SAXException("Illegal state for callback.");
393                }
394            } catch (IOException e) {
395                throw new SAXException(e.getMessage(), e);
396            }
397        }
398    
399        public void startDocument(String callback) throws SAXException {
400            try {
401                State state = peek();
402                switch (state) {
403                    case INITIAL:
404                        if (callback == null) {
405                            hadCallback = false;
406                        } else {
407                            hadCallback = true;
408                            writer.write(callback);
409                            writer.write('(');
410                        }
411                        push(State.DOCUMENT);
412                        first = true;
413                        break;
414                    default:
415                        throw new SAXException("Illegal state for callback.");
416                }
417            } catch (IOException e) {
418                throw new SAXException(e.getMessage(), e);
419            }
420        }
421    
422        public void startObject() throws SAXException {
423            try {
424                State state = peek();
425                switch (state) {
426                    case ARRAY:
427                        if (!first) {
428                            writer.write(',');
429                        }
430                        // fall thru
431                    case DOCUMENT:
432                    case VALUE:
433                        writer.write('{');
434                        push(State.OBJECT);
435                        first = true;
436                        break;
437                    default:
438                        throw new SAXException("Illegal state for callback.");
439                }
440            } catch (IOException e) {
441                throw new SAXException(e.getMessage(), e);
442            }
443        }
444    
445        public void startString() throws SAXException {
446            try {
447                State state = peek();
448                switch (state) {
449                    case ARRAY:
450                        if (!first) {
451                            writer.write(',');
452                        }
453                        // fall thru
454                    case DOCUMENT:
455                    case VALUE:
456                        writer.write('\"');
457                        push(State.STRING);
458                        break;
459                    default:
460                        throw new SAXException("Illegal state for callback.");
461                }
462            } catch (IOException e) {
463                throw new SAXException(e.getMessage(), e);
464            }
465        }
466    
467        public void string(String string) throws SAXException {
468            try {
469                State state = peek();
470                switch (state) {
471                    case ARRAY:
472                        if (!first) {
473                            writer.write(',');
474                        }
475                        // fall thru
476                    case DOCUMENT:
477                    case VALUE:
478                        if (string == null) {
479                            writer.write("null");
480                        } else {
481                            writer.write('\"');
482                            charactersImpl(string.toCharArray(), 0, string.length());
483                            writer.write('\"');
484                        }
485                        if (state == State.VALUE) {
486                            pop();
487                        }
488                        first = false;
489                        break;
490                    default:
491                        throw new SAXException("Illegal state for callback.");
492                }
493            } catch (IOException e) {
494                throw new SAXException(e.getMessage(), e);
495            }
496        }
497    
498    }