blob: 032f08f2412d1d71d16b2b47a476bc605f6a908c [file] [log] [blame]
Stuart McCullochbb014372012-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
Stuart McCulloch2286f232012-06-15 13:27:53 +000042 private final static WeakHashMap<Type,Handler> handlers = new WeakHashMap<Type,Handler>();
Stuart McCullochbb014372012-06-07 21:57:32 +000043 private static StringHandler sh = new StringHandler();
44 private static BooleanHandler bh = new BooleanHandler();
45 private static CharacterHandler ch = new CharacterHandler();
Stuart McCulloch2286f232012-06-15 13:27:53 +000046 private static CollectionHandler dch = new CollectionHandler(ArrayList.class,
Stuart McCullochbb014372012-06-07 21:57:32 +000047 Object.class);
Stuart McCulloch2286f232012-06-15 13:27:53 +000048 private static SpecialHandler sph = new SpecialHandler(Pattern.class, null, null);
Stuart McCullochbb014372012-06-07 21:57:32 +000049 private static DateHandler sdh = new DateHandler();
50 private static FileHandler fh = new FileHandler();
51 private static ByteArrayHandler byteh = new ByteArrayHandler();
52
Stuart McCulloch2286f232012-06-15 13:27:53 +000053 boolean ignorenull;
Stuart McCulloch285034f2012-06-12 12:41:16 +000054
Stuart McCullochbb014372012-06-07 21:57:32 +000055 /**
56 * Create a new Encoder with the state and appropriate API.
57 *
58 * @return an Encoder
59 */
60 public Encoder enc() {
61 return new Encoder(this);
62 }
63
64 /**
65 * Create a new Decoder with the state and appropriate API.
66 *
67 * @return a Decoder
68 */
69 public Decoder dec() {
70 return new Decoder(this);
71 }
72
73 /*
74 * Work horse encode methods, all encoding ends up here.
75 */
Stuart McCulloch2286f232012-06-15 13:27:53 +000076 void encode(Encoder app, Object object, Type type, Map<Object,Type> visited) throws Exception {
Stuart McCullochbb014372012-06-07 21:57:32 +000077
78 // Get the null out of the way
79
80 if (object == null) {
81 app.append("null");
82 return;
83 }
84
85 // If we have no type or the type is Object.class
86 // we take the type of the object itself. Normally types
87 // come from declaration sites (returns, fields, methods, etc)
88 // and contain generic info.
89
90 if (type == null || type == Object.class)
91 type = object.getClass();
92
93 // Dispatch to the handler who knows how to handle the given type.
94 Handler h = getHandler(type);
95 h.encode(app, object, visited);
96 }
97
98 /*
99 * This method figures out which handler should handle the type specific
100 * stuff. It returns a handler for each type. If no appropriate handler
101 * exists, it will create one for the given type. There are actually quite a
102 * lot of handlers since Java is not very object oriented.
Stuart McCullochbb014372012-06-07 21:57:32 +0000103 * @param type
Stuart McCullochbb014372012-06-07 21:57:32 +0000104 * @return
Stuart McCullochbb014372012-06-07 21:57:32 +0000105 * @throws Exception
106 */
107 Handler getHandler(Type type) throws Exception {
108
109 // First the static hard coded handlers for the common types.
110
111 if (type == String.class)
112 return sh;
113
114 if (type == Boolean.class || type == boolean.class)
115 return bh;
116
117 if (type == byte[].class)
118 return byteh;
119
120 if (Character.class == type || char.class == type)
121 return ch;
122
123 if (Pattern.class == type)
124 return sph;
125
126 if (Date.class == type)
127 return sdh;
128
129 if (File.class == type)
130 return fh;
131
132 Handler h;
133 synchronized (handlers) {
134 h = handlers.get(type);
135 }
136
137 if (h != null)
138 return h;
139
140 if (type instanceof Class) {
141
Stuart McCulloch2286f232012-06-15 13:27:53 +0000142 Class< ? > clazz = (Class< ? >) type;
Stuart McCullochbb014372012-06-07 21:57:32 +0000143
144 if (Enum.class.isAssignableFrom(clazz))
145 h = new EnumHandler(clazz);
Stuart McCulloch285034f2012-06-12 12:41:16 +0000146 else if (Iterable.class.isAssignableFrom(clazz)) // A Non Generic
Stuart McCullochbb014372012-06-07 21:57:32 +0000147 // collection
148
149 h = dch;
150 else if (clazz.isArray()) // Non generic array
151 h = new ArrayHandler(clazz, clazz.getComponentType());
152 else if (Map.class.isAssignableFrom(clazz)) // A Non Generic map
153 h = new MapHandler(clazz, Object.class, Object.class);
154 else if (Number.class.isAssignableFrom(clazz) || clazz.isPrimitive())
155 h = new NumberHandler(clazz);
156 else {
157 Method valueOf = null;
Stuart McCulloch2286f232012-06-15 13:27:53 +0000158 Constructor< ? > constructor = null;
Stuart McCullochbb014372012-06-07 21:57:32 +0000159
160 try {
161 constructor = clazz.getConstructor(String.class);
Stuart McCulloch2286f232012-06-15 13:27:53 +0000162 }
163 catch (Exception e) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000164 // Ignore
165 }
166 try {
167 valueOf = clazz.getMethod("valueOf", String.class);
Stuart McCulloch2286f232012-06-15 13:27:53 +0000168 }
169 catch (Exception e) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000170 // Ignore
171 }
172 if (constructor != null || valueOf != null)
173 h = new SpecialHandler(clazz, constructor, valueOf);
174 else
175 h = new ObjectHandler(this, clazz); // Hmm, might not be a
176 // data class ...
177 }
178
179 } else {
180
181 // We have generic information available
182 // We only support generics on Collection, Map, and arrays
183
184 if (type instanceof ParameterizedType) {
185 ParameterizedType pt = (ParameterizedType) type;
186 Type rawType = pt.getRawType();
187 if (rawType instanceof Class) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000188 Class< ? > rawClass = (Class< ? >) rawType;
Stuart McCulloch285034f2012-06-12 12:41:16 +0000189 if (Iterable.class.isAssignableFrom(rawClass))
Stuart McCullochbb014372012-06-07 21:57:32 +0000190 h = new CollectionHandler(rawClass, pt.getActualTypeArguments()[0]);
191 else if (Map.class.isAssignableFrom(rawClass))
Stuart McCulloch2286f232012-06-15 13:27:53 +0000192 h = new MapHandler(rawClass, pt.getActualTypeArguments()[0], pt.getActualTypeArguments()[1]);
Stuart McCullochbb014372012-06-07 21:57:32 +0000193 else
Stuart McCulloch2286f232012-06-15 13:27:53 +0000194 throw new IllegalArgumentException("Found a parameterized type that is not a map or collection");
Stuart McCullochbb014372012-06-07 21:57:32 +0000195 }
196 } else if (type instanceof GenericArrayType) {
197 GenericArrayType gat = (GenericArrayType) type;
198 h = new ArrayHandler(getRawClass(type), gat.getGenericComponentType());
199 } else
Stuart McCulloch2286f232012-06-15 13:27:53 +0000200 throw new IllegalArgumentException("Found a parameterized type that is not a map or collection");
Stuart McCullochbb014372012-06-07 21:57:32 +0000201 }
202 synchronized (handlers) {
203 // We might actually have duplicates
204 // but who cares? They should be identical
205 handlers.put(type, h);
206 }
207 return h;
208 }
209
210 Object decode(Type type, Decoder isr) throws Exception {
211 int c = isr.skipWs();
212 Handler h;
213
214 if (type == null || type == Object.class) {
215
216 // Establish default behavior when we run without
217 // type information
218
219 switch (c) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000220 case '{' :
221 type = LinkedHashMap.class;
222 break;
Stuart McCullochbb014372012-06-07 21:57:32 +0000223
Stuart McCulloch2286f232012-06-15 13:27:53 +0000224 case '[' :
225 type = ArrayList.class;
226 break;
Stuart McCullochbb014372012-06-07 21:57:32 +0000227
Stuart McCulloch2286f232012-06-15 13:27:53 +0000228 case '"' :
229 return parseString(isr);
Stuart McCullochbb014372012-06-07 21:57:32 +0000230
Stuart McCulloch2286f232012-06-15 13:27:53 +0000231 case 'n' :
232 isr.expect("ull");
233 return null;
Stuart McCullochbb014372012-06-07 21:57:32 +0000234
Stuart McCulloch2286f232012-06-15 13:27:53 +0000235 case 't' :
236 isr.expect("rue");
237 return true;
Stuart McCullochbb014372012-06-07 21:57:32 +0000238
Stuart McCulloch2286f232012-06-15 13:27:53 +0000239 case 'f' :
240 isr.expect("alse");
241 return false;
Stuart McCullochbb014372012-06-07 21:57:32 +0000242
Stuart McCulloch2286f232012-06-15 13:27:53 +0000243 case '0' :
244 case '1' :
245 case '2' :
246 case '3' :
247 case '4' :
248 case '5' :
249 case '6' :
250 case '7' :
251 case '8' :
252 case '9' :
253 case '-' :
254 return parseNumber(isr);
Stuart McCullochbb014372012-06-07 21:57:32 +0000255
Stuart McCulloch2286f232012-06-15 13:27:53 +0000256 default :
257 throw new IllegalArgumentException("Invalid character at begin of token: " + (char) c);
Stuart McCullochbb014372012-06-07 21:57:32 +0000258 }
259 }
260
261 h = getHandler(type);
262
263 switch (c) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000264 case '{' :
265 return h.decodeObject(isr);
Stuart McCullochbb014372012-06-07 21:57:32 +0000266
Stuart McCulloch2286f232012-06-15 13:27:53 +0000267 case '[' :
268 return h.decodeArray(isr);
Stuart McCullochbb014372012-06-07 21:57:32 +0000269
Stuart McCulloch2286f232012-06-15 13:27:53 +0000270 case '"' :
271 return h.decode(parseString(isr));
Stuart McCullochbb014372012-06-07 21:57:32 +0000272
Stuart McCulloch2286f232012-06-15 13:27:53 +0000273 case 'n' :
274 isr.expect("ull");
275 return h.decode();
Stuart McCullochbb014372012-06-07 21:57:32 +0000276
Stuart McCulloch2286f232012-06-15 13:27:53 +0000277 case 't' :
278 isr.expect("rue");
279 return h.decode(Boolean.TRUE);
Stuart McCullochbb014372012-06-07 21:57:32 +0000280
Stuart McCulloch2286f232012-06-15 13:27:53 +0000281 case 'f' :
282 isr.expect("alse");
283 return h.decode(Boolean.FALSE);
Stuart McCullochbb014372012-06-07 21:57:32 +0000284
Stuart McCulloch2286f232012-06-15 13:27:53 +0000285 case '0' :
286 case '1' :
287 case '2' :
288 case '3' :
289 case '4' :
290 case '5' :
291 case '6' :
292 case '7' :
293 case '8' :
294 case '9' :
295 case '-' :
296 return h.decode(parseNumber(isr));
Stuart McCullochbb014372012-06-07 21:57:32 +0000297
Stuart McCulloch2286f232012-06-15 13:27:53 +0000298 default :
299 throw new IllegalArgumentException("Unexpected character in input stream: " + (char) c);
Stuart McCullochbb014372012-06-07 21:57:32 +0000300 }
301 }
302
303 String parseString(Decoder r) throws Exception {
304 assert r.current() == '"';
305
306 int c = r.next(); // skip first "
307
308 StringBuilder sb = new StringBuilder();
309 while (c != '"') {
310 if (c < 0 || Character.isISOControl(c))
Stuart McCulloch2286f232012-06-15 13:27:53 +0000311 throw new IllegalArgumentException("JSON strings may not contain control characters: " + r.current());
Stuart McCullochbb014372012-06-07 21:57:32 +0000312
313 if (c == '\\') {
314 c = r.read();
315 switch (c) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000316 case '"' :
317 case '\\' :
318 case '/' :
319 sb.append((char) c);
320 break;
Stuart McCullochbb014372012-06-07 21:57:32 +0000321
Stuart McCulloch2286f232012-06-15 13:27:53 +0000322 case 'b' :
323 sb.append('\b');
324 break;
Stuart McCullochbb014372012-06-07 21:57:32 +0000325
Stuart McCulloch2286f232012-06-15 13:27:53 +0000326 case 'f' :
327 sb.append('\f');
328 break;
329 case 'n' :
330 sb.append('\n');
331 break;
332 case 'r' :
333 sb.append('\r');
334 break;
335 case 't' :
336 sb.append('\t');
337 break;
338 case 'u' :
339 int a3 = hexDigit(r.read()) << 12;
340 int a2 = hexDigit(r.read()) << 8;
341 int a1 = hexDigit(r.read()) << 4;
342 int a0 = hexDigit(r.read()) << 0;
343 c = a3 + a2 + a1 + a0;
344 sb.append((char) c);
345 break;
Stuart McCullochbb014372012-06-07 21:57:32 +0000346
Stuart McCulloch2286f232012-06-15 13:27:53 +0000347 default :
348 throw new IllegalArgumentException(
349 "The only characters after a backslash are \", \\, b, f, n, r, t, and u but got " + c);
Stuart McCullochbb014372012-06-07 21:57:32 +0000350 }
351 } else
352 sb.append((char) c);
353
354 c = r.read();
355 }
356 assert c == '"';
357 r.read(); // skip quote
358 return sb.toString();
359 }
360
361 private int hexDigit(int c) throws EOFException {
362 if (c >= '0' && c <= '9')
363 return c - '0';
364
365 if (c >= 'A' && c <= 'F')
366 return c - 'A' + 10;
367
368 if (c >= 'a' && c <= 'f')
369 return c - 'a' + 10;
370
371 throw new IllegalArgumentException("Invalid hex character: " + c);
372 }
373
374 private Number parseNumber(Decoder r) throws Exception {
375 StringBuilder sb = new StringBuilder();
376 boolean d = false;
377
378 if (r.current() == '-') {
379 sb.append('-');
380 r.read();
381 }
382
383 int c = r.current();
384 if (c == '0') {
385 sb.append('0');
386 c = r.read();
387 } else if (c >= '1' && c <= '9') {
388 sb.append((char) c);
389 c = r.read();
390
391 while (c >= '0' && c <= '9') {
392 sb.append((char) c);
393 c = r.read();
394 }
395 } else
396 throw new IllegalArgumentException("Expected digit");
397
398 if (c == '.') {
399 d = true;
400 sb.append('.');
401 c = r.read();
402 while (c >= '0' && c <= '9') {
403 sb.append((char) c);
404 c = r.read();
405 }
406 }
407 if (c == 'e' || c == 'E') {
408 d = true;
409 sb.append('e');
410 c = r.read();
411 if (c == '+') {
412 sb.append('+');
413 c = r.read();
414 } else if (c == '-') {
415 sb.append('-');
416 c = r.read();
417 }
418 while (c >= '0' && c <= '9') {
419 sb.append((char) c);
420 c = r.read();
421 }
422 }
423 if (d)
424 return Double.parseDouble(sb.toString());
425 long l = Long.parseLong(sb.toString());
426 if (l > Integer.MAX_VALUE || l < Integer.MIN_VALUE)
427 return l;
428 return (int) l;
429 }
430
431 void parseArray(Collection<Object> list, Type componentType, Decoder r) throws Exception {
432 assert r.current() == '[';
433 int c = r.next();
434 while (START_CHARACTERS.indexOf(c) >= 0) {
435 Object o = decode(componentType, r);
436 list.add(o);
437
438 c = r.skipWs();
439 if (c == ']')
440 break;
441
442 if (c == ',') {
443 c = r.next();
444 continue;
445 }
446
Stuart McCulloch2286f232012-06-15 13:27:53 +0000447 throw new IllegalArgumentException("Invalid character in parsing list, expected ] or , but found "
448 + (char) c);
Stuart McCullochbb014372012-06-07 21:57:32 +0000449 }
450 assert r.current() == ']';
451 r.read(); // skip closing
452 }
453
454 @SuppressWarnings("rawtypes")
Stuart McCulloch2286f232012-06-15 13:27:53 +0000455 Class< ? > getRawClass(Type type) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000456 if (type instanceof Class)
457 return (Class) type;
458
459 if (type instanceof ParameterizedType)
460 return getRawClass(((ParameterizedType) type).getRawType());
461
462 if (type instanceof GenericArrayType) {
463 Type subType = ((GenericArrayType) type).getGenericComponentType();
464 Class c = getRawClass(subType);
465 return Array.newInstance(c, 0).getClass();
466 }
467
468 throw new IllegalArgumentException(
Stuart McCulloch2286f232012-06-15 13:27:53 +0000469 "Does not support generics beyond Parameterized Type and GenericArrayType, got " + type);
Stuart McCullochbb014372012-06-07 21:57:32 +0000470 }
471
Stuart McCulloch285034f2012-06-12 12:41:16 +0000472 /**
473 * Ignore null values in output and input
Stuart McCulloch2286f232012-06-15 13:27:53 +0000474 *
Stuart McCulloch285034f2012-06-12 12:41:16 +0000475 * @param ignorenull
476 * @return
477 */
478 public JSONCodec setIgnorenull(boolean ignorenull) {
479 this.ignorenull = ignorenull;
480 return this;
481 }
482
483 public boolean isIgnorenull() {
484 return ignorenull;
485 }
486
Stuart McCullochbb014372012-06-07 21:57:32 +0000487}