001: // ========================================================================
002: // Copyright 2006 Mort Bay Consulting Pty. Ltd.
003: // ------------------------------------------------------------------------
004: // Licensed under the Apache License, Version 2.0 (the "License");
005: // you may not use this file except in compliance with the License.
006: // You may obtain a copy of the License at
007: // http://www.apache.org/licenses/LICENSE-2.0
008: // Unless required by applicable law or agreed to in writing, software
009: // distributed under the License is distributed on an "AS IS" BASIS,
010: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011: // See the License for the specific language governing permissions and
012: // limitations under the License.
013: // ========================================================================
014:
015: package org.mortbay.cometd;
016:
017: import java.io.IOException;
018: import java.io.InputStream;
019: import java.lang.reflect.Array;
020: import java.util.ArrayList;
021: import java.util.HashMap;
022: import java.util.Iterator;
023: import java.util.List;
024: import java.util.Map;
025:
026: import org.mortbay.util.IO;
027: import org.mortbay.util.LazyList;
028: import org.mortbay.util.QuotedStringTokenizer;
029: import org.mortbay.util.TypeUtil;
030:
031: /** JSON Parser and Generator.
032: *
033: * <p>This class provides some static methods to convert POJOs to and from JSON
034: * notation. The mapping from JSON to java is:<pre>
035: * object ==> Map
036: * array ==> Object[]
037: * number ==> Double or Long
038: * string ==> String
039: * null ==> null
040: * bool ==> Boolean
041: * </pre>
042: * </p><p>
043: * The java to JSON mapping is:<pre>
044: * String --> string
045: * Number --> number
046: * Map --> object
047: * List --> array
048: * Array --> array
049: * null --> null
050: * Boolean--> boolean
051: * Object --> string (dubious!)
052: * </pre>
053: * </p><p>
054: * The interface {@link JSON.Generator} may be implemented by classes that know how to render themselves as JSON and
055: * the {@link #toString(Object)} method will use {@link JSON.Generator#addJSON(StringBuffer)} to generate the JSON.
056: * The class {@link JSON.Literal} may be used to hold pre-gnerated JSON object.
057: * </p>
058: * @author gregw
059: *
060: */
061: public class JSON {
062: private JSON() {
063: }
064:
065: public static String toString(Object object) {
066: StringBuffer buffer = new StringBuffer();
067: append(buffer, object);
068: return buffer.toString();
069: }
070:
071: public static String toString(Map object) {
072: StringBuffer buffer = new StringBuffer();
073: appendMap(buffer, object);
074: return buffer.toString();
075: }
076:
077: public static String toString(Object[] array) {
078: StringBuffer buffer = new StringBuffer();
079: appendArray(buffer, array);
080: return buffer.toString();
081: }
082:
083: /**
084: * @param s String containing JSON object or array.
085: * @return A Map, Object array or primitive array parsed from the JSON.
086: */
087: public static Object parse(String s) {
088: return parse(new Source(s));
089: }
090:
091: /**
092: * @param s Stream containing JSON object or array.
093: * @return A Map, Object array or primitive array parsed from the JSON.
094: */
095: public static Object parse(InputStream in) throws IOException {
096: String s = IO.toString(in);
097: return parse(new Source(s));
098: }
099:
100: /**
101: * Append object as JSON to string buffer.
102: * @param buffer
103: * @param object
104: */
105: public static void append(StringBuffer buffer, Object object) {
106: if (object == null)
107: buffer.append("null");
108: else if (object instanceof Generator)
109: appendJSON(buffer, (Generator) object);
110: else if (object instanceof Map)
111: appendMap(buffer, (Map) object);
112: else if (object instanceof List)
113: appendArray(buffer, LazyList.toArray(object, Object.class));
114: else if (object.getClass().isArray())
115: appendArray(buffer, object);
116: else if (object instanceof Number)
117: appendNumber(buffer, (Number) object);
118: else if (object instanceof Boolean)
119: appendBoolean(buffer, (Boolean) object);
120: else if (object instanceof String)
121: appendString(buffer, (String) object);
122: else
123: // TODO - maybe some bean stuff?
124: appendString(buffer, object.toString());
125: }
126:
127: private static void appendNull(StringBuffer buffer) {
128: buffer.append("null");
129: }
130:
131: private static void appendJSON(StringBuffer buffer,
132: Generator generator) {
133: generator.addJSON(buffer);
134: }
135:
136: private static void appendMap(StringBuffer buffer, Map object) {
137: if (object == null) {
138: appendNull(buffer);
139: return;
140: }
141:
142: buffer.append('{');
143: Iterator iter = object.entrySet().iterator();
144: while (iter.hasNext()) {
145: Map.Entry entry = (Map.Entry) iter.next();
146: QuotedStringTokenizer.quote(buffer, entry.getKey()
147: .toString());
148: buffer.append(':');
149: append(buffer, entry.getValue());
150: if (iter.hasNext())
151: buffer.append(',');
152: }
153:
154: buffer.append('}');
155: }
156:
157: private static void appendArray(StringBuffer buffer, Object array) {
158: if (array == null) {
159: appendNull(buffer);
160: return;
161: }
162:
163: buffer.append('[');
164: int length = Array.getLength(array);
165:
166: for (int i = 0; i < length; i++) {
167: if (i != 0)
168: buffer.append(',');
169: append(buffer, Array.get(array, i));
170: }
171:
172: buffer.append(']');
173: }
174:
175: private static void appendBoolean(StringBuffer buffer, Boolean b) {
176: if (b == null) {
177: appendNull(buffer);
178: return;
179: }
180: buffer.append(b.booleanValue() ? "true" : "false");
181: }
182:
183: private static void appendNumber(StringBuffer buffer, Number number) {
184: if (number == null) {
185: appendNull(buffer);
186: return;
187: }
188: buffer.append(number);
189: }
190:
191: private static void appendString(StringBuffer buffer, String string) {
192: if (string == null) {
193: appendNull(buffer);
194: return;
195: }
196:
197: QuotedStringTokenizer.quote(buffer, string);
198: }
199:
200: private static Object parse(Source source) {
201: int comment_state = 0;
202:
203: while (source.hasNext()) {
204: char c = source.peek();
205:
206: // handle // or /* comment
207: if (comment_state == 1) {
208: switch (c) {
209: case '/':
210: comment_state = -1;
211: break;
212: case '*':
213: comment_state = 2;
214: }
215: }
216: // handle /* */ comment
217: else if (comment_state > 1) {
218: switch (c) {
219: case '*':
220: comment_state = 3;
221: break;
222: case '/':
223: if (comment_state == 3)
224: comment_state = 0;
225: else
226: comment_state = 2;
227: break;
228: default:
229: comment_state = 2;
230: }
231: }
232: // handle // comment
233: else if (comment_state < 0) {
234: switch (c) {
235: case '\r':
236: case '\n':
237: comment_state = 0;
238: break;
239: default:
240: break;
241: }
242: }
243: // handle unknown
244: else {
245: switch (c) {
246: case '{':
247: return parseObject(source);
248: case '[':
249: return parseArray(source);
250: case '"':
251: return parseString(source);
252: case '-':
253: return parseNumber(source);
254:
255: case 'n':
256: complete("null", source);
257: return null;
258: case 't':
259: complete("true", source);
260: return Boolean.TRUE;
261: case 'f':
262: complete("false", source);
263: return Boolean.FALSE;
264:
265: case '/':
266: comment_state = 1;
267: break;
268:
269: default:
270: if (Character.isDigit(c))
271: return parseNumber(source);
272: else if (Character.isWhitespace(c))
273: break;
274:
275: throw new IllegalStateException("unknown char " + c);
276: }
277: }
278: source.next();
279: }
280:
281: return null;
282: }
283:
284: private static Map parseObject(Source source) {
285: if (source.next() != '{')
286: throw new IllegalStateException();
287: Map map = new HashMap();
288:
289: char next = seekTo("\"}", source);
290:
291: while (source.hasNext()) {
292: if (next == '}') {
293: source.next();
294: break;
295: }
296:
297: String name = parseString(source);
298: seekTo(':', source);
299: source.next();
300:
301: Object value = parse(source);
302: map.put(name, value);
303:
304: seekTo(",}", source);
305: next = source.next();
306: if (next == '}')
307: break;
308: else
309: next = seekTo("\"}", source);
310: }
311:
312: return map;
313: }
314:
315: private static Object parseArray(Source source) {
316: if (source.next() != '[')
317: throw new IllegalStateException();
318:
319: ArrayList list = new ArrayList();
320: boolean coma = true;
321:
322: while (source.hasNext()) {
323: char c = source.peek();
324: switch (c) {
325: case ']':
326: source.next();
327: return list.toArray(new Object[list.size()]);
328:
329: case ',':
330: if (coma)
331: throw new IllegalStateException();
332: coma = true;
333: source.next();
334:
335: default:
336: if (Character.isWhitespace(c))
337: source.next();
338: else {
339: coma = false;
340: list.add(parse(source));
341: }
342: }
343:
344: }
345:
346: throw new IllegalStateException("unexpected end of array");
347: }
348:
349: private static String parseString(Source source) {
350: if (source.next() != '"')
351: throw new IllegalStateException();
352:
353: boolean escape = false;
354: StringBuffer b = new StringBuffer();
355: while (source.hasNext()) {
356: char c = source.next();
357:
358: if (escape) {
359: escape = false;
360: switch (c) {
361: case 'n':
362: b.append('\n');
363: break;
364: case 'r':
365: b.append('\r');
366: break;
367: case 't':
368: b.append('\t');
369: break;
370: case 'f':
371: b.append('\f');
372: break;
373: case 'b':
374: b.append('\b');
375: break;
376: case 'u':
377: b
378: .append((char) ((TypeUtil
379: .convertHexDigit((byte) source
380: .next()) << 24)
381: + (TypeUtil
382: .convertHexDigit((byte) source
383: .next()) << 16)
384: + (TypeUtil
385: .convertHexDigit((byte) source
386: .next()) << 8) + (TypeUtil
387: .convertHexDigit((byte) source
388: .next()))));
389: break;
390: default:
391: b.append(c);
392: }
393: } else if (c == '\\') {
394: escape = true;
395: continue;
396: } else if (c == '\"')
397: break;
398: else
399: b.append(c);
400: }
401:
402: return b.toString();
403: }
404:
405: private static Number parseNumber(Source source) {
406: int start = source.index();
407: int end = -1;
408: boolean is_double = false;
409: while (source.hasNext() && end < 0) {
410: char c = source.peek();
411: switch (c) {
412: case '0':
413: case '1':
414: case '2':
415: case '3':
416: case '4':
417: case '5':
418: case '6':
419: case '7':
420: case '8':
421: case '9':
422: case '-':
423: source.next();
424: break;
425:
426: case '.':
427: case 'e':
428: case 'E':
429: is_double = true;
430: source.next();
431: break;
432:
433: default:
434: end = source.index();
435: }
436: }
437: String s = end >= 0 ? source.from(start, end) : source
438: .from(start);
439: if (is_double)
440: return new Double(s);
441: else
442: return new Long(s);
443: }
444:
445: private static void seekTo(char seek, Source source) {
446: while (source.hasNext()) {
447: char c = source.peek();
448: if (c == seek)
449: return;
450:
451: if (!Character.isWhitespace(c))
452: throw new IllegalStateException("Unexpected '" + c
453: + " while seeking '" + seek + "'");
454: source.next();
455: }
456:
457: throw new IllegalStateException("Expected '" + seek + "'");
458: }
459:
460: private static char seekTo(String seek, Source source) {
461: while (source.hasNext()) {
462: char c = source.peek();
463: if (seek.indexOf(c) >= 0) {
464: return c;
465: }
466:
467: if (!Character.isWhitespace(c))
468: throw new IllegalStateException("Unexpected '" + c
469: + "' while seeking one of '" + seek + "'");
470: source.next();
471: }
472:
473: throw new IllegalStateException("Expected one of '" + seek
474: + "'");
475: }
476:
477: private static void complete(String seek, Source source) {
478: int i = 0;
479: while (source.hasNext() && i < seek.length()) {
480: char c = source.next();
481: if (c != seek.charAt(i++))
482: throw new IllegalStateException("Unexpected '" + c
483: + " while seeking \"" + seek + "\"");
484: }
485:
486: if (i < seek.length())
487: throw new IllegalStateException("Expected \"" + seek + "\"");
488: }
489:
490: private static class Source {
491: private final String string;
492: private int index;
493:
494: Source(String s) {
495: string = s;
496: }
497:
498: boolean hasNext() {
499: return (index < string.length());
500: }
501:
502: char next() {
503: return string.charAt(index++);
504: }
505:
506: char peek() {
507: return string.charAt(index);
508: }
509:
510: int index() {
511: return index;
512: }
513:
514: String from(int mark) {
515: return string.substring(mark, index);
516: }
517:
518: String from(int mark, int end) {
519: return string.substring(mark, end);
520: }
521: }
522:
523: public interface Generator {
524: public void addJSON(StringBuffer buffer);
525: }
526:
527: /* ------------------------------------------------------------ */
528: /** A Literal JSON generator
529: * A utility instance of {@link JSON.Generator} that holds a pre-generated string on JSON text.
530: */
531: public static class Literal implements Generator {
532: private String _json;
533:
534: /* ------------------------------------------------------------ */
535: /** Construct a literal JSON instance for use by {@link JSON#toString(Object)}.
536: * @param json A literal JSON string that will be parsed to check validity.
537: */
538: public Literal(String json) {
539: parse(json);
540: _json = json;
541: }
542:
543: public String toString() {
544: return _json;
545: }
546:
547: public void addJSON(StringBuffer buffer) {
548: buffer.append(_json);
549: }
550: }
551: }
|