blob: 02601d4049c3c0644cf44bb496a77d364e8126e7 [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.
Stuart McCulloch2a0afd62012-09-06 18:28:06 +000037 * <p/>
38 * Will now use hex for encoding byte arrays
Stuart McCullochf3173222012-06-07 21:57:32 +000039 */
40public class JSONCodec {
41 final static String START_CHARACTERS = "[{\"-0123456789tfn";
42
43 // Handlers
Stuart McCulloch4482c702012-06-15 13:27:53 +000044 private final static WeakHashMap<Type,Handler> handlers = new WeakHashMap<Type,Handler>();
Stuart McCullochf3173222012-06-07 21:57:32 +000045 private static StringHandler sh = new StringHandler();
46 private static BooleanHandler bh = new BooleanHandler();
47 private static CharacterHandler ch = new CharacterHandler();
Stuart McCulloch4482c702012-06-15 13:27:53 +000048 private static CollectionHandler dch = new CollectionHandler(ArrayList.class,
Stuart McCullochf3173222012-06-07 21:57:32 +000049 Object.class);
Stuart McCulloch4482c702012-06-15 13:27:53 +000050 private static SpecialHandler sph = new SpecialHandler(Pattern.class, null, null);
Stuart McCullochf3173222012-06-07 21:57:32 +000051 private static DateHandler sdh = new DateHandler();
52 private static FileHandler fh = new FileHandler();
53 private static ByteArrayHandler byteh = new ByteArrayHandler();
54
Stuart McCulloch4482c702012-06-15 13:27:53 +000055 boolean ignorenull;
Stuart McCulloch0b639c62012-06-12 12:41:16 +000056
Stuart McCullochf3173222012-06-07 21:57:32 +000057 /**
58 * Create a new Encoder with the state and appropriate API.
59 *
60 * @return an Encoder
61 */
62 public Encoder enc() {
63 return new Encoder(this);
64 }
65
66 /**
67 * Create a new Decoder with the state and appropriate API.
68 *
69 * @return a Decoder
70 */
71 public Decoder dec() {
72 return new Decoder(this);
73 }
74
75 /*
76 * Work horse encode methods, all encoding ends up here.
77 */
Stuart McCulloch4482c702012-06-15 13:27:53 +000078 void encode(Encoder app, Object object, Type type, Map<Object,Type> visited) throws Exception {
Stuart McCullochf3173222012-06-07 21:57:32 +000079
80 // Get the null out of the way
81
82 if (object == null) {
83 app.append("null");
84 return;
85 }
86
87 // If we have no type or the type is Object.class
88 // we take the type of the object itself. Normally types
89 // come from declaration sites (returns, fields, methods, etc)
90 // and contain generic info.
91
92 if (type == null || type == Object.class)
93 type = object.getClass();
94
95 // Dispatch to the handler who knows how to handle the given type.
96 Handler h = getHandler(type);
97 h.encode(app, object, visited);
98 }
99
100 /*
101 * This method figures out which handler should handle the type specific
102 * stuff. It returns a handler for each type. If no appropriate handler
103 * exists, it will create one for the given type. There are actually quite a
104 * lot of handlers since Java is not very object oriented.
Stuart McCullochf3173222012-06-07 21:57:32 +0000105 * @param type
Stuart McCullochf3173222012-06-07 21:57:32 +0000106 * @return
Stuart McCullochf3173222012-06-07 21:57:32 +0000107 * @throws Exception
108 */
109 Handler getHandler(Type type) throws Exception {
110
111 // First the static hard coded handlers for the common types.
112
113 if (type == String.class)
114 return sh;
115
116 if (type == Boolean.class || type == boolean.class)
117 return bh;
118
119 if (type == byte[].class)
120 return byteh;
121
122 if (Character.class == type || char.class == type)
123 return ch;
124
125 if (Pattern.class == type)
126 return sph;
127
128 if (Date.class == type)
129 return sdh;
130
131 if (File.class == type)
132 return fh;
133
134 Handler h;
135 synchronized (handlers) {
136 h = handlers.get(type);
137 }
138
139 if (h != null)
140 return h;
141
142 if (type instanceof Class) {
143
Stuart McCulloch4482c702012-06-15 13:27:53 +0000144 Class< ? > clazz = (Class< ? >) type;
Stuart McCullochf3173222012-06-07 21:57:32 +0000145
146 if (Enum.class.isAssignableFrom(clazz))
147 h = new EnumHandler(clazz);
Stuart McCulloch0b639c62012-06-12 12:41:16 +0000148 else if (Iterable.class.isAssignableFrom(clazz)) // A Non Generic
Stuart McCullochf3173222012-06-07 21:57:32 +0000149 // collection
150
151 h = dch;
152 else if (clazz.isArray()) // Non generic array
153 h = new ArrayHandler(clazz, clazz.getComponentType());
154 else if (Map.class.isAssignableFrom(clazz)) // A Non Generic map
155 h = new MapHandler(clazz, Object.class, Object.class);
156 else if (Number.class.isAssignableFrom(clazz) || clazz.isPrimitive())
157 h = new NumberHandler(clazz);
158 else {
159 Method valueOf = null;
Stuart McCulloch4482c702012-06-15 13:27:53 +0000160 Constructor< ? > constructor = null;
Stuart McCullochf3173222012-06-07 21:57:32 +0000161
162 try {
163 constructor = clazz.getConstructor(String.class);
Stuart McCulloch4482c702012-06-15 13:27:53 +0000164 }
165 catch (Exception e) {
Stuart McCullochf3173222012-06-07 21:57:32 +0000166 // Ignore
167 }
168 try {
169 valueOf = clazz.getMethod("valueOf", String.class);
Stuart McCulloch4482c702012-06-15 13:27:53 +0000170 }
171 catch (Exception e) {
Stuart McCullochf3173222012-06-07 21:57:32 +0000172 // 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) {
Stuart McCulloch4482c702012-06-15 13:27:53 +0000190 Class< ? > rawClass = (Class< ? >) rawType;
Stuart McCulloch0b639c62012-06-12 12:41:16 +0000191 if (Iterable.class.isAssignableFrom(rawClass))
Stuart McCullochf3173222012-06-07 21:57:32 +0000192 h = new CollectionHandler(rawClass, pt.getActualTypeArguments()[0]);
193 else if (Map.class.isAssignableFrom(rawClass))
Stuart McCulloch4482c702012-06-15 13:27:53 +0000194 h = new MapHandler(rawClass, pt.getActualTypeArguments()[0], pt.getActualTypeArguments()[1]);
Stuart McCulloch1a890552012-06-29 19:23:09 +0000195 else if (Dictionary.class.isAssignableFrom(rawClass))
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000196 h = new MapHandler(Hashtable.class, pt.getActualTypeArguments()[0],
197 pt.getActualTypeArguments()[1]);
Stuart McCullochf3173222012-06-07 21:57:32 +0000198 else
Stuart McCulloch4482c702012-06-15 13:27:53 +0000199 throw new IllegalArgumentException("Found a parameterized type that is not a map or collection");
Stuart McCullochf3173222012-06-07 21:57:32 +0000200 }
201 } else if (type instanceof GenericArrayType) {
202 GenericArrayType gat = (GenericArrayType) type;
203 h = new ArrayHandler(getRawClass(type), gat.getGenericComponentType());
204 } else
Stuart McCulloch4482c702012-06-15 13:27:53 +0000205 throw new IllegalArgumentException("Found a parameterized type that is not a map or collection");
Stuart McCullochf3173222012-06-07 21:57:32 +0000206 }
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) {
Stuart McCulloch4482c702012-06-15 13:27:53 +0000225 case '{' :
226 type = LinkedHashMap.class;
227 break;
Stuart McCullochf3173222012-06-07 21:57:32 +0000228
Stuart McCulloch4482c702012-06-15 13:27:53 +0000229 case '[' :
230 type = ArrayList.class;
231 break;
Stuart McCullochf3173222012-06-07 21:57:32 +0000232
Stuart McCulloch4482c702012-06-15 13:27:53 +0000233 case '"' :
234 return parseString(isr);
Stuart McCullochf3173222012-06-07 21:57:32 +0000235
Stuart McCulloch4482c702012-06-15 13:27:53 +0000236 case 'n' :
237 isr.expect("ull");
238 return null;
Stuart McCullochf3173222012-06-07 21:57:32 +0000239
Stuart McCulloch4482c702012-06-15 13:27:53 +0000240 case 't' :
241 isr.expect("rue");
242 return true;
Stuart McCullochf3173222012-06-07 21:57:32 +0000243
Stuart McCulloch4482c702012-06-15 13:27:53 +0000244 case 'f' :
245 isr.expect("alse");
246 return false;
Stuart McCullochf3173222012-06-07 21:57:32 +0000247
Stuart McCulloch4482c702012-06-15 13:27:53 +0000248 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);
Stuart McCullochf3173222012-06-07 21:57:32 +0000260
Stuart McCulloch4482c702012-06-15 13:27:53 +0000261 default :
262 throw new IllegalArgumentException("Invalid character at begin of token: " + (char) c);
Stuart McCullochf3173222012-06-07 21:57:32 +0000263 }
264 }
265
266 h = getHandler(type);
267
268 switch (c) {
Stuart McCulloch4482c702012-06-15 13:27:53 +0000269 case '{' :
270 return h.decodeObject(isr);
Stuart McCullochf3173222012-06-07 21:57:32 +0000271
Stuart McCulloch4482c702012-06-15 13:27:53 +0000272 case '[' :
273 return h.decodeArray(isr);
Stuart McCullochf3173222012-06-07 21:57:32 +0000274
Stuart McCulloch4482c702012-06-15 13:27:53 +0000275 case '"' :
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000276 return h.decode(isr, parseString(isr));
Stuart McCullochf3173222012-06-07 21:57:32 +0000277
Stuart McCulloch4482c702012-06-15 13:27:53 +0000278 case 'n' :
279 isr.expect("ull");
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000280 return h.decode(isr);
Stuart McCullochf3173222012-06-07 21:57:32 +0000281
Stuart McCulloch4482c702012-06-15 13:27:53 +0000282 case 't' :
283 isr.expect("rue");
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000284 return h.decode(isr,Boolean.TRUE);
Stuart McCullochf3173222012-06-07 21:57:32 +0000285
Stuart McCulloch4482c702012-06-15 13:27:53 +0000286 case 'f' :
287 isr.expect("alse");
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000288 return h.decode(isr,Boolean.FALSE);
Stuart McCullochf3173222012-06-07 21:57:32 +0000289
Stuart McCulloch4482c702012-06-15 13:27:53 +0000290 case '0' :
291 case '1' :
292 case '2' :
293 case '3' :
294 case '4' :
295 case '5' :
296 case '6' :
297 case '7' :
298 case '8' :
299 case '9' :
300 case '-' :
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000301 return h.decode(isr,parseNumber(isr));
Stuart McCullochf3173222012-06-07 21:57:32 +0000302
Stuart McCulloch4482c702012-06-15 13:27:53 +0000303 default :
304 throw new IllegalArgumentException("Unexpected character in input stream: " + (char) c);
Stuart McCullochf3173222012-06-07 21:57:32 +0000305 }
306 }
307
308 String parseString(Decoder r) throws Exception {
309 assert r.current() == '"';
310
311 int c = r.next(); // skip first "
312
313 StringBuilder sb = new StringBuilder();
314 while (c != '"') {
315 if (c < 0 || Character.isISOControl(c))
Stuart McCulloch4482c702012-06-15 13:27:53 +0000316 throw new IllegalArgumentException("JSON strings may not contain control characters: " + r.current());
Stuart McCullochf3173222012-06-07 21:57:32 +0000317
318 if (c == '\\') {
319 c = r.read();
320 switch (c) {
Stuart McCulloch4482c702012-06-15 13:27:53 +0000321 case '"' :
322 case '\\' :
323 case '/' :
324 sb.append((char) c);
325 break;
Stuart McCullochf3173222012-06-07 21:57:32 +0000326
Stuart McCulloch4482c702012-06-15 13:27:53 +0000327 case 'b' :
328 sb.append('\b');
329 break;
Stuart McCullochf3173222012-06-07 21:57:32 +0000330
Stuart McCulloch4482c702012-06-15 13:27:53 +0000331 case 'f' :
332 sb.append('\f');
333 break;
334 case 'n' :
335 sb.append('\n');
336 break;
337 case 'r' :
338 sb.append('\r');
339 break;
340 case 't' :
341 sb.append('\t');
342 break;
343 case 'u' :
344 int a3 = hexDigit(r.read()) << 12;
345 int a2 = hexDigit(r.read()) << 8;
346 int a1 = hexDigit(r.read()) << 4;
347 int a0 = hexDigit(r.read()) << 0;
348 c = a3 + a2 + a1 + a0;
349 sb.append((char) c);
350 break;
Stuart McCullochf3173222012-06-07 21:57:32 +0000351
Stuart McCulloch4482c702012-06-15 13:27:53 +0000352 default :
353 throw new IllegalArgumentException(
354 "The only characters after a backslash are \", \\, b, f, n, r, t, and u but got " + c);
Stuart McCullochf3173222012-06-07 21:57:32 +0000355 }
356 } else
357 sb.append((char) c);
358
359 c = r.read();
360 }
361 assert c == '"';
362 r.read(); // skip quote
363 return sb.toString();
364 }
365
366 private int hexDigit(int c) throws EOFException {
367 if (c >= '0' && c <= '9')
368 return c - '0';
369
370 if (c >= 'A' && c <= 'F')
371 return c - 'A' + 10;
372
373 if (c >= 'a' && c <= 'f')
374 return c - 'a' + 10;
375
376 throw new IllegalArgumentException("Invalid hex character: " + c);
377 }
378
379 private Number parseNumber(Decoder r) throws Exception {
380 StringBuilder sb = new StringBuilder();
381 boolean d = false;
382
383 if (r.current() == '-') {
384 sb.append('-');
385 r.read();
386 }
387
388 int c = r.current();
389 if (c == '0') {
390 sb.append('0');
391 c = r.read();
392 } else if (c >= '1' && c <= '9') {
393 sb.append((char) c);
394 c = r.read();
395
396 while (c >= '0' && c <= '9') {
397 sb.append((char) c);
398 c = r.read();
399 }
400 } else
401 throw new IllegalArgumentException("Expected digit");
402
403 if (c == '.') {
404 d = true;
405 sb.append('.');
406 c = r.read();
407 while (c >= '0' && c <= '9') {
408 sb.append((char) c);
409 c = r.read();
410 }
411 }
412 if (c == 'e' || c == 'E') {
413 d = true;
414 sb.append('e');
415 c = r.read();
416 if (c == '+') {
417 sb.append('+');
418 c = r.read();
419 } else if (c == '-') {
420 sb.append('-');
421 c = r.read();
422 }
423 while (c >= '0' && c <= '9') {
424 sb.append((char) c);
425 c = r.read();
426 }
427 }
428 if (d)
429 return Double.parseDouble(sb.toString());
430 long l = Long.parseLong(sb.toString());
431 if (l > Integer.MAX_VALUE || l < Integer.MIN_VALUE)
432 return l;
433 return (int) l;
434 }
435
436 void parseArray(Collection<Object> list, Type componentType, Decoder r) throws Exception {
437 assert r.current() == '[';
438 int c = r.next();
439 while (START_CHARACTERS.indexOf(c) >= 0) {
440 Object o = decode(componentType, r);
441 list.add(o);
442
443 c = r.skipWs();
444 if (c == ']')
445 break;
446
447 if (c == ',') {
448 c = r.next();
449 continue;
450 }
451
Stuart McCulloch4482c702012-06-15 13:27:53 +0000452 throw new IllegalArgumentException("Invalid character in parsing list, expected ] or , but found "
453 + (char) c);
Stuart McCullochf3173222012-06-07 21:57:32 +0000454 }
455 assert r.current() == ']';
456 r.read(); // skip closing
457 }
458
459 @SuppressWarnings("rawtypes")
Stuart McCulloch4482c702012-06-15 13:27:53 +0000460 Class< ? > getRawClass(Type type) {
Stuart McCullochf3173222012-06-07 21:57:32 +0000461 if (type instanceof Class)
462 return (Class) type;
463
464 if (type instanceof ParameterizedType)
465 return getRawClass(((ParameterizedType) type).getRawType());
466
467 if (type instanceof GenericArrayType) {
468 Type subType = ((GenericArrayType) type).getGenericComponentType();
469 Class c = getRawClass(subType);
470 return Array.newInstance(c, 0).getClass();
471 }
472
473 throw new IllegalArgumentException(
Stuart McCulloch4482c702012-06-15 13:27:53 +0000474 "Does not support generics beyond Parameterized Type and GenericArrayType, got " + type);
Stuart McCullochf3173222012-06-07 21:57:32 +0000475 }
476
Stuart McCulloch0b639c62012-06-12 12:41:16 +0000477 /**
478 * Ignore null values in output and input
Stuart McCulloch4482c702012-06-15 13:27:53 +0000479 *
Stuart McCulloch0b639c62012-06-12 12:41:16 +0000480 * @param ignorenull
481 * @return
482 */
483 public JSONCodec setIgnorenull(boolean ignorenull) {
484 this.ignorenull = ignorenull;
485 return this;
486 }
487
488 public boolean isIgnorenull() {
489 return ignorenull;
490 }
491
Stuart McCullochf3173222012-06-07 21:57:32 +0000492}