blob: a45b5b40077e82fec82d00e1b9a5c86e4237a933 [file] [log] [blame]
Stuart McCullochf3173222012-06-07 21:57:32 +00001package aQute.lib.json;
2
3import java.io.*;
4import java.lang.reflect.*;
5import java.util.*;
6import java.util.regex.*;
7
8/**
9 * This is a simple JSON Coder and Encoder that uses the Java type system to
10 * convert data objects to JSON and JSON to (type safe) Java objects. The
11 * conversion is very much driven by classes and their public fields. Generic
12 * information, when present is taken into account. </p> Usage patterns to
13 * encode:
14 *
15 * <pre>
16 * JSONCoder codec = new JSONCodec(); //
17 * assert "1".equals( codec.enc().to().put(1).toString());
18 * assert "[1,2,3]".equals( codec.enc().to().put(Arrays.asList(1,2,3).toString());
19 *
20 * Map m = new HashMap();
21 * m.put("a", "A");
22 * assert "{\"a\":\"A\"}".equals( codec.enc().to().put(m).toString());
23 *
24 * static class D { public int a; }
25 * D d = new D();
26 * d.a = 41;
27 * assert "{\"a\":41}".equals( codec.enc().to().put(d).toString());
28 * </pre>
29 *
30 * It is possible to redirect the encoder to another output (default is a
31 * string). See {@link Encoder#to()},{@link Encoder#to(File))},
32 * {@link Encoder#to(OutputStream)}, {@link Encoder#to(Appendable))}. To reset
33 * the string output call {@link Encoder#to()}.
34 * <p/>
35 * This Codec class can be used in a concurrent environment. The Decoders and
36 * Encoders, however, must only be used in a single thread.
37 */
38public class JSONCodec {
39 final static String START_CHARACTERS = "[{\"-0123456789tfn";
40
41 // Handlers
42 private final static WeakHashMap<Type, Handler> handlers = new WeakHashMap<Type, Handler>();
43 private static StringHandler sh = new StringHandler();
44 private static BooleanHandler bh = new BooleanHandler();
45 private static CharacterHandler ch = new CharacterHandler();
46 private static CollectionHandler dch = new CollectionHandler(
47 ArrayList.class,
48 Object.class);
49 private static SpecialHandler sph = new SpecialHandler(
50 Pattern.class,
51 null, null);
52 private static DateHandler sdh = new DateHandler();
53 private static FileHandler fh = new FileHandler();
54 private static ByteArrayHandler byteh = new ByteArrayHandler();
55
56 /**
57 * Create a new Encoder with the state and appropriate API.
58 *
59 * @return an Encoder
60 */
61 public Encoder enc() {
62 return new Encoder(this);
63 }
64
65 /**
66 * Create a new Decoder with the state and appropriate API.
67 *
68 * @return a Decoder
69 */
70 public Decoder dec() {
71 return new Decoder(this);
72 }
73
74 /*
75 * Work horse encode methods, all encoding ends up here.
76 */
77 void encode(Encoder app, Object object, Type type, Map<Object, Type> visited) throws Exception {
78
79 // Get the null out of the way
80
81 if (object == null) {
82 app.append("null");
83 return;
84 }
85
86 // If we have no type or the type is Object.class
87 // we take the type of the object itself. Normally types
88 // come from declaration sites (returns, fields, methods, etc)
89 // and contain generic info.
90
91 if (type == null || type == Object.class)
92 type = object.getClass();
93
94 // Dispatch to the handler who knows how to handle the given type.
95 Handler h = getHandler(type);
96 h.encode(app, object, visited);
97 }
98
99 /*
100 * This method figures out which handler should handle the type specific
101 * stuff. It returns a handler for each type. If no appropriate handler
102 * exists, it will create one for the given type. There are actually quite a
103 * lot of handlers since Java is not very object oriented.
104 *
105 * @param type
106 *
107 * @return
108 *
109 * @throws Exception
110 */
111 Handler getHandler(Type type) throws Exception {
112
113 // First the static hard coded handlers for the common types.
114
115 if (type == String.class)
116 return sh;
117
118 if (type == Boolean.class || type == boolean.class)
119 return bh;
120
121 if (type == byte[].class)
122 return byteh;
123
124 if (Character.class == type || char.class == type)
125 return ch;
126
127 if (Pattern.class == type)
128 return sph;
129
130 if (Date.class == type)
131 return sdh;
132
133 if (File.class == type)
134 return fh;
135
136 Handler h;
137 synchronized (handlers) {
138 h = handlers.get(type);
139 }
140
141 if (h != null)
142 return h;
143
144 if (type instanceof Class) {
145
146 Class<?> clazz = (Class<?>) type;
147
148 if (Enum.class.isAssignableFrom(clazz))
149 h = new EnumHandler(clazz);
150 else if (Collection.class.isAssignableFrom(clazz)) // A Non Generic
151 // collection
152
153 h = dch;
154 else if (clazz.isArray()) // Non generic array
155 h = new ArrayHandler(clazz, clazz.getComponentType());
156 else if (Map.class.isAssignableFrom(clazz)) // A Non Generic map
157 h = new MapHandler(clazz, Object.class, Object.class);
158 else if (Number.class.isAssignableFrom(clazz) || clazz.isPrimitive())
159 h = new NumberHandler(clazz);
160 else {
161 Method valueOf = null;
162 Constructor<?> constructor = null;
163
164 try {
165 constructor = clazz.getConstructor(String.class);
166 } catch (Exception e) {
167 // Ignore
168 }
169 try {
170 valueOf = clazz.getMethod("valueOf", String.class);
171 } catch (Exception e) {
172 // Ignore
173 }
174 if (constructor != null || valueOf != null)
175 h = new SpecialHandler(clazz, constructor, valueOf);
176 else
177 h = new ObjectHandler(this, clazz); // Hmm, might not be a
178 // data class ...
179 }
180
181 } else {
182
183 // We have generic information available
184 // We only support generics on Collection, Map, and arrays
185
186 if (type instanceof ParameterizedType) {
187 ParameterizedType pt = (ParameterizedType) type;
188 Type rawType = pt.getRawType();
189 if (rawType instanceof Class) {
190 Class<?> rawClass = (Class<?>) rawType;
191 if (Collection.class.isAssignableFrom(rawClass))
192 h = new CollectionHandler(rawClass, pt.getActualTypeArguments()[0]);
193 else if (Map.class.isAssignableFrom(rawClass))
194 h = new MapHandler(rawClass, pt.getActualTypeArguments()[0],
195 pt.getActualTypeArguments()[1]);
196 else
197 throw new IllegalArgumentException(
198 "Found a parameterized type that is not a map or collection");
199 }
200 } else if (type instanceof GenericArrayType) {
201 GenericArrayType gat = (GenericArrayType) type;
202 h = new ArrayHandler(getRawClass(type), gat.getGenericComponentType());
203 } else
204 throw new IllegalArgumentException(
205 "Found a parameterized type that is not a map or collection");
206 }
207 synchronized (handlers) {
208 // We might actually have duplicates
209 // but who cares? They should be identical
210 handlers.put(type, h);
211 }
212 return h;
213 }
214
215 Object decode(Type type, Decoder isr) throws Exception {
216 int c = isr.skipWs();
217 Handler h;
218
219 if (type == null || type == Object.class) {
220
221 // Establish default behavior when we run without
222 // type information
223
224 switch (c) {
225 case '{':
226 type = LinkedHashMap.class;
227 break;
228
229 case '[':
230 type = ArrayList.class;
231 break;
232
233 case '"':
234 return parseString(isr);
235
236 case 'n':
237 isr.expect("ull");
238 return null;
239
240 case 't':
241 isr.expect("rue");
242 return true;
243
244 case 'f':
245 isr.expect("alse");
246 return false;
247
248 case '0':
249 case '1':
250 case '2':
251 case '3':
252 case '4':
253 case '5':
254 case '6':
255 case '7':
256 case '8':
257 case '9':
258 case '-':
259 return parseNumber(isr);
260
261 default:
262 throw new IllegalArgumentException("Invalid character at begin of token: "
263 + (char) c);
264 }
265 }
266
267 h = getHandler(type);
268
269 switch (c) {
270 case '{':
271 return h.decodeObject(isr);
272
273 case '[':
274 return h.decodeArray(isr);
275
276 case '"':
277 return h.decode(parseString(isr));
278
279 case 'n':
280 isr.expect("ull");
281 return h.decode();
282
283 case 't':
284 isr.expect("rue");
285 return h.decode(Boolean.TRUE);
286
287 case 'f':
288 isr.expect("alse");
289 return h.decode(Boolean.FALSE);
290
291 case '0':
292 case '1':
293 case '2':
294 case '3':
295 case '4':
296 case '5':
297 case '6':
298 case '7':
299 case '8':
300 case '9':
301 case '-':
302 return h.decode(parseNumber(isr));
303
304 default:
305 throw new IllegalArgumentException("Unexpected character in input stream: " + (char) c);
306 }
307 }
308
309 String parseString(Decoder r) throws Exception {
310 assert r.current() == '"';
311
312 int c = r.next(); // skip first "
313
314 StringBuilder sb = new StringBuilder();
315 while (c != '"') {
316 if (c < 0 || Character.isISOControl(c))
317 throw new IllegalArgumentException(
318 "JSON strings may not contain control characters: " + r.current());
319
320 if (c == '\\') {
321 c = r.read();
322 switch (c) {
323 case '"':
324 case '\\':
325 case '/':
326 sb.append((char)c);
327 break;
328
329 case 'b':
330 sb.append('\b');
331 break;
332
333 case 'f':
334 sb.append('\f');
335 break;
336 case 'n':
337 sb.append('\n');
338 break;
339 case 'r':
340 sb.append('\r');
341 break;
342 case 't':
343 sb.append('\t');
344 break;
345 case 'u':
346 int a3 = hexDigit(r.read()) << 12;
347 int a2 = hexDigit(r.read()) << 8;
348 int a1 = hexDigit(r.read()) << 4;
349 int a0 = hexDigit(r.read()) << 0;
350 c = a3 + a2 + a1 + a0;
351 sb.append((char) c);
352 break;
353
354 default:
355 throw new IllegalArgumentException(
356 "The only characters after a backslash are \", \\, b, f, n, r, t, and u but got "
357 + c);
358 }
359 } else
360 sb.append((char) c);
361
362 c = r.read();
363 }
364 assert c == '"';
365 r.read(); // skip quote
366 return sb.toString();
367 }
368
369 private int hexDigit(int c) throws EOFException {
370 if (c >= '0' && c <= '9')
371 return c - '0';
372
373 if (c >= 'A' && c <= 'F')
374 return c - 'A' + 10;
375
376 if (c >= 'a' && c <= 'f')
377 return c - 'a' + 10;
378
379 throw new IllegalArgumentException("Invalid hex character: " + c);
380 }
381
382 private Number parseNumber(Decoder r) throws Exception {
383 StringBuilder sb = new StringBuilder();
384 boolean d = false;
385
386 if (r.current() == '-') {
387 sb.append('-');
388 r.read();
389 }
390
391 int c = r.current();
392 if (c == '0') {
393 sb.append('0');
394 c = r.read();
395 } else if (c >= '1' && c <= '9') {
396 sb.append((char) c);
397 c = r.read();
398
399 while (c >= '0' && c <= '9') {
400 sb.append((char) c);
401 c = r.read();
402 }
403 } else
404 throw new IllegalArgumentException("Expected digit");
405
406 if (c == '.') {
407 d = true;
408 sb.append('.');
409 c = r.read();
410 while (c >= '0' && c <= '9') {
411 sb.append((char) c);
412 c = r.read();
413 }
414 }
415 if (c == 'e' || c == 'E') {
416 d = true;
417 sb.append('e');
418 c = r.read();
419 if (c == '+') {
420 sb.append('+');
421 c = r.read();
422 } else if (c == '-') {
423 sb.append('-');
424 c = r.read();
425 }
426 while (c >= '0' && c <= '9') {
427 sb.append((char) c);
428 c = r.read();
429 }
430 }
431 if (d)
432 return Double.parseDouble(sb.toString());
433 long l = Long.parseLong(sb.toString());
434 if (l > Integer.MAX_VALUE || l < Integer.MIN_VALUE)
435 return l;
436 return (int) l;
437 }
438
439 void parseArray(Collection<Object> list, Type componentType, Decoder r) throws Exception {
440 assert r.current() == '[';
441 int c = r.next();
442 while (START_CHARACTERS.indexOf(c) >= 0) {
443 Object o = decode(componentType, r);
444 list.add(o);
445
446 c = r.skipWs();
447 if (c == ']')
448 break;
449
450 if (c == ',') {
451 c = r.next();
452 continue;
453 }
454
455 throw new IllegalArgumentException(
456 "Invalid character in parsing list, expected ] or , but found " + (char) c);
457 }
458 assert r.current() == ']';
459 r.read(); // skip closing
460 }
461
462 @SuppressWarnings("rawtypes")
463 Class<?> getRawClass(Type type) {
464 if (type instanceof Class)
465 return (Class) type;
466
467 if (type instanceof ParameterizedType)
468 return getRawClass(((ParameterizedType) type).getRawType());
469
470 if (type instanceof GenericArrayType) {
471 Type subType = ((GenericArrayType) type).getGenericComponentType();
472 Class c = getRawClass(subType);
473 return Array.newInstance(c, 0).getClass();
474 }
475
476 throw new IllegalArgumentException(
477 "Does not support generics beyond Parameterized Type and GenericArrayType, got "
478 + type);
479 }
480
481}