blob: 47ad4abd9b6e1cdb6dee785044388906560ae1ae [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
Stuart McCulloch4482c702012-06-15 13:27:53 +000042 private final static WeakHashMap<Type,Handler> handlers = new WeakHashMap<Type,Handler>();
Stuart McCullochf3173222012-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 McCulloch4482c702012-06-15 13:27:53 +000046 private static CollectionHandler dch = new CollectionHandler(ArrayList.class,
Stuart McCullochf3173222012-06-07 21:57:32 +000047 Object.class);
Stuart McCulloch4482c702012-06-15 13:27:53 +000048 private static SpecialHandler sph = new SpecialHandler(Pattern.class, null, null);
Stuart McCullochf3173222012-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 McCulloch4482c702012-06-15 13:27:53 +000053 boolean ignorenull;
Stuart McCulloch55fbda52012-08-02 13:26:25 +000054 boolean useHex;
Stuart McCulloch0b639c62012-06-12 12:41:16 +000055
Stuart McCullochf3173222012-06-07 21:57:32 +000056 /**
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 */
Stuart McCulloch4482c702012-06-15 13:27:53 +000077 void encode(Encoder app, Object object, Type type, Map<Object,Type> visited) throws Exception {
Stuart McCullochf3173222012-06-07 21:57:32 +000078
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.
Stuart McCullochf3173222012-06-07 21:57:32 +0000104 * @param type
Stuart McCullochf3173222012-06-07 21:57:32 +0000105 * @return
Stuart McCullochf3173222012-06-07 21:57:32 +0000106 * @throws Exception
107 */
108 Handler getHandler(Type type) throws Exception {
109
110 // First the static hard coded handlers for the common types.
111
112 if (type == String.class)
113 return sh;
114
115 if (type == Boolean.class || type == boolean.class)
116 return bh;
117
118 if (type == byte[].class)
119 return byteh;
120
121 if (Character.class == type || char.class == type)
122 return ch;
123
124 if (Pattern.class == type)
125 return sph;
126
127 if (Date.class == type)
128 return sdh;
129
130 if (File.class == type)
131 return fh;
132
133 Handler h;
134 synchronized (handlers) {
135 h = handlers.get(type);
136 }
137
138 if (h != null)
139 return h;
140
141 if (type instanceof Class) {
142
Stuart McCulloch4482c702012-06-15 13:27:53 +0000143 Class< ? > clazz = (Class< ? >) type;
Stuart McCullochf3173222012-06-07 21:57:32 +0000144
145 if (Enum.class.isAssignableFrom(clazz))
146 h = new EnumHandler(clazz);
Stuart McCulloch0b639c62012-06-12 12:41:16 +0000147 else if (Iterable.class.isAssignableFrom(clazz)) // A Non Generic
Stuart McCullochf3173222012-06-07 21:57:32 +0000148 // collection
149
150 h = dch;
151 else if (clazz.isArray()) // Non generic array
152 h = new ArrayHandler(clazz, clazz.getComponentType());
153 else if (Map.class.isAssignableFrom(clazz)) // A Non Generic map
154 h = new MapHandler(clazz, Object.class, Object.class);
155 else if (Number.class.isAssignableFrom(clazz) || clazz.isPrimitive())
156 h = new NumberHandler(clazz);
157 else {
158 Method valueOf = null;
Stuart McCulloch4482c702012-06-15 13:27:53 +0000159 Constructor< ? > constructor = null;
Stuart McCullochf3173222012-06-07 21:57:32 +0000160
161 try {
162 constructor = clazz.getConstructor(String.class);
Stuart McCulloch4482c702012-06-15 13:27:53 +0000163 }
164 catch (Exception e) {
Stuart McCullochf3173222012-06-07 21:57:32 +0000165 // Ignore
166 }
167 try {
168 valueOf = clazz.getMethod("valueOf", String.class);
Stuart McCulloch4482c702012-06-15 13:27:53 +0000169 }
170 catch (Exception e) {
Stuart McCullochf3173222012-06-07 21:57:32 +0000171 // Ignore
172 }
173 if (constructor != null || valueOf != null)
174 h = new SpecialHandler(clazz, constructor, valueOf);
175 else
176 h = new ObjectHandler(this, clazz); // Hmm, might not be a
177 // data class ...
178 }
179
180 } else {
181
182 // We have generic information available
183 // We only support generics on Collection, Map, and arrays
184
185 if (type instanceof ParameterizedType) {
186 ParameterizedType pt = (ParameterizedType) type;
187 Type rawType = pt.getRawType();
188 if (rawType instanceof Class) {
Stuart McCulloch4482c702012-06-15 13:27:53 +0000189 Class< ? > rawClass = (Class< ? >) rawType;
Stuart McCulloch0b639c62012-06-12 12:41:16 +0000190 if (Iterable.class.isAssignableFrom(rawClass))
Stuart McCullochf3173222012-06-07 21:57:32 +0000191 h = new CollectionHandler(rawClass, pt.getActualTypeArguments()[0]);
192 else if (Map.class.isAssignableFrom(rawClass))
Stuart McCulloch4482c702012-06-15 13:27:53 +0000193 h = new MapHandler(rawClass, pt.getActualTypeArguments()[0], pt.getActualTypeArguments()[1]);
Stuart McCulloch1a890552012-06-29 19:23:09 +0000194 else if (Dictionary.class.isAssignableFrom(rawClass))
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000195 h = new MapHandler(Hashtable.class, pt.getActualTypeArguments()[0],
196 pt.getActualTypeArguments()[1]);
Stuart McCullochf3173222012-06-07 21:57:32 +0000197 else
Stuart McCulloch4482c702012-06-15 13:27:53 +0000198 throw new IllegalArgumentException("Found a parameterized type that is not a map or collection");
Stuart McCullochf3173222012-06-07 21:57:32 +0000199 }
200 } else if (type instanceof GenericArrayType) {
201 GenericArrayType gat = (GenericArrayType) type;
202 h = new ArrayHandler(getRawClass(type), gat.getGenericComponentType());
203 } else
Stuart McCulloch4482c702012-06-15 13:27:53 +0000204 throw new IllegalArgumentException("Found a parameterized type that is not a map or collection");
Stuart McCullochf3173222012-06-07 21:57:32 +0000205 }
206 synchronized (handlers) {
207 // We might actually have duplicates
208 // but who cares? They should be identical
209 handlers.put(type, h);
210 }
211 return h;
212 }
213
214 Object decode(Type type, Decoder isr) throws Exception {
215 int c = isr.skipWs();
216 Handler h;
217
218 if (type == null || type == Object.class) {
219
220 // Establish default behavior when we run without
221 // type information
222
223 switch (c) {
Stuart McCulloch4482c702012-06-15 13:27:53 +0000224 case '{' :
225 type = LinkedHashMap.class;
226 break;
Stuart McCullochf3173222012-06-07 21:57:32 +0000227
Stuart McCulloch4482c702012-06-15 13:27:53 +0000228 case '[' :
229 type = ArrayList.class;
230 break;
Stuart McCullochf3173222012-06-07 21:57:32 +0000231
Stuart McCulloch4482c702012-06-15 13:27:53 +0000232 case '"' :
233 return parseString(isr);
Stuart McCullochf3173222012-06-07 21:57:32 +0000234
Stuart McCulloch4482c702012-06-15 13:27:53 +0000235 case 'n' :
236 isr.expect("ull");
237 return null;
Stuart McCullochf3173222012-06-07 21:57:32 +0000238
Stuart McCulloch4482c702012-06-15 13:27:53 +0000239 case 't' :
240 isr.expect("rue");
241 return true;
Stuart McCullochf3173222012-06-07 21:57:32 +0000242
Stuart McCulloch4482c702012-06-15 13:27:53 +0000243 case 'f' :
244 isr.expect("alse");
245 return false;
Stuart McCullochf3173222012-06-07 21:57:32 +0000246
Stuart McCulloch4482c702012-06-15 13:27:53 +0000247 case '0' :
248 case '1' :
249 case '2' :
250 case '3' :
251 case '4' :
252 case '5' :
253 case '6' :
254 case '7' :
255 case '8' :
256 case '9' :
257 case '-' :
258 return parseNumber(isr);
Stuart McCullochf3173222012-06-07 21:57:32 +0000259
Stuart McCulloch4482c702012-06-15 13:27:53 +0000260 default :
261 throw new IllegalArgumentException("Invalid character at begin of token: " + (char) c);
Stuart McCullochf3173222012-06-07 21:57:32 +0000262 }
263 }
264
265 h = getHandler(type);
266
267 switch (c) {
Stuart McCulloch4482c702012-06-15 13:27:53 +0000268 case '{' :
269 return h.decodeObject(isr);
Stuart McCullochf3173222012-06-07 21:57:32 +0000270
Stuart McCulloch4482c702012-06-15 13:27:53 +0000271 case '[' :
272 return h.decodeArray(isr);
Stuart McCullochf3173222012-06-07 21:57:32 +0000273
Stuart McCulloch4482c702012-06-15 13:27:53 +0000274 case '"' :
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000275 return h.decode(isr, parseString(isr));
Stuart McCullochf3173222012-06-07 21:57:32 +0000276
Stuart McCulloch4482c702012-06-15 13:27:53 +0000277 case 'n' :
278 isr.expect("ull");
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000279 return h.decode(isr);
Stuart McCullochf3173222012-06-07 21:57:32 +0000280
Stuart McCulloch4482c702012-06-15 13:27:53 +0000281 case 't' :
282 isr.expect("rue");
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000283 return h.decode(isr,Boolean.TRUE);
Stuart McCullochf3173222012-06-07 21:57:32 +0000284
Stuart McCulloch4482c702012-06-15 13:27:53 +0000285 case 'f' :
286 isr.expect("alse");
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000287 return h.decode(isr,Boolean.FALSE);
Stuart McCullochf3173222012-06-07 21:57:32 +0000288
Stuart McCulloch4482c702012-06-15 13:27:53 +0000289 case '0' :
290 case '1' :
291 case '2' :
292 case '3' :
293 case '4' :
294 case '5' :
295 case '6' :
296 case '7' :
297 case '8' :
298 case '9' :
299 case '-' :
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000300 return h.decode(isr,parseNumber(isr));
Stuart McCullochf3173222012-06-07 21:57:32 +0000301
Stuart McCulloch4482c702012-06-15 13:27:53 +0000302 default :
303 throw new IllegalArgumentException("Unexpected character in input stream: " + (char) c);
Stuart McCullochf3173222012-06-07 21:57:32 +0000304 }
305 }
306
307 String parseString(Decoder r) throws Exception {
308 assert r.current() == '"';
309
310 int c = r.next(); // skip first "
311
312 StringBuilder sb = new StringBuilder();
313 while (c != '"') {
314 if (c < 0 || Character.isISOControl(c))
Stuart McCulloch4482c702012-06-15 13:27:53 +0000315 throw new IllegalArgumentException("JSON strings may not contain control characters: " + r.current());
Stuart McCullochf3173222012-06-07 21:57:32 +0000316
317 if (c == '\\') {
318 c = r.read();
319 switch (c) {
Stuart McCulloch4482c702012-06-15 13:27:53 +0000320 case '"' :
321 case '\\' :
322 case '/' :
323 sb.append((char) c);
324 break;
Stuart McCullochf3173222012-06-07 21:57:32 +0000325
Stuart McCulloch4482c702012-06-15 13:27:53 +0000326 case 'b' :
327 sb.append('\b');
328 break;
Stuart McCullochf3173222012-06-07 21:57:32 +0000329
Stuart McCulloch4482c702012-06-15 13:27:53 +0000330 case 'f' :
331 sb.append('\f');
332 break;
333 case 'n' :
334 sb.append('\n');
335 break;
336 case 'r' :
337 sb.append('\r');
338 break;
339 case 't' :
340 sb.append('\t');
341 break;
342 case 'u' :
343 int a3 = hexDigit(r.read()) << 12;
344 int a2 = hexDigit(r.read()) << 8;
345 int a1 = hexDigit(r.read()) << 4;
346 int a0 = hexDigit(r.read()) << 0;
347 c = a3 + a2 + a1 + a0;
348 sb.append((char) c);
349 break;
Stuart McCullochf3173222012-06-07 21:57:32 +0000350
Stuart McCulloch4482c702012-06-15 13:27:53 +0000351 default :
352 throw new IllegalArgumentException(
353 "The only characters after a backslash are \", \\, b, f, n, r, t, and u but got " + c);
Stuart McCullochf3173222012-06-07 21:57:32 +0000354 }
355 } else
356 sb.append((char) c);
357
358 c = r.read();
359 }
360 assert c == '"';
361 r.read(); // skip quote
362 return sb.toString();
363 }
364
365 private int hexDigit(int c) throws EOFException {
366 if (c >= '0' && c <= '9')
367 return c - '0';
368
369 if (c >= 'A' && c <= 'F')
370 return c - 'A' + 10;
371
372 if (c >= 'a' && c <= 'f')
373 return c - 'a' + 10;
374
375 throw new IllegalArgumentException("Invalid hex character: " + c);
376 }
377
378 private Number parseNumber(Decoder r) throws Exception {
379 StringBuilder sb = new StringBuilder();
380 boolean d = false;
381
382 if (r.current() == '-') {
383 sb.append('-');
384 r.read();
385 }
386
387 int c = r.current();
388 if (c == '0') {
389 sb.append('0');
390 c = r.read();
391 } else if (c >= '1' && c <= '9') {
392 sb.append((char) c);
393 c = r.read();
394
395 while (c >= '0' && c <= '9') {
396 sb.append((char) c);
397 c = r.read();
398 }
399 } else
400 throw new IllegalArgumentException("Expected digit");
401
402 if (c == '.') {
403 d = true;
404 sb.append('.');
405 c = r.read();
406 while (c >= '0' && c <= '9') {
407 sb.append((char) c);
408 c = r.read();
409 }
410 }
411 if (c == 'e' || c == 'E') {
412 d = true;
413 sb.append('e');
414 c = r.read();
415 if (c == '+') {
416 sb.append('+');
417 c = r.read();
418 } else if (c == '-') {
419 sb.append('-');
420 c = r.read();
421 }
422 while (c >= '0' && c <= '9') {
423 sb.append((char) c);
424 c = r.read();
425 }
426 }
427 if (d)
428 return Double.parseDouble(sb.toString());
429 long l = Long.parseLong(sb.toString());
430 if (l > Integer.MAX_VALUE || l < Integer.MIN_VALUE)
431 return l;
432 return (int) l;
433 }
434
435 void parseArray(Collection<Object> list, Type componentType, Decoder r) throws Exception {
436 assert r.current() == '[';
437 int c = r.next();
438 while (START_CHARACTERS.indexOf(c) >= 0) {
439 Object o = decode(componentType, r);
440 list.add(o);
441
442 c = r.skipWs();
443 if (c == ']')
444 break;
445
446 if (c == ',') {
447 c = r.next();
448 continue;
449 }
450
Stuart McCulloch4482c702012-06-15 13:27:53 +0000451 throw new IllegalArgumentException("Invalid character in parsing list, expected ] or , but found "
452 + (char) c);
Stuart McCullochf3173222012-06-07 21:57:32 +0000453 }
454 assert r.current() == ']';
455 r.read(); // skip closing
456 }
457
458 @SuppressWarnings("rawtypes")
Stuart McCulloch4482c702012-06-15 13:27:53 +0000459 Class< ? > getRawClass(Type type) {
Stuart McCullochf3173222012-06-07 21:57:32 +0000460 if (type instanceof Class)
461 return (Class) type;
462
463 if (type instanceof ParameterizedType)
464 return getRawClass(((ParameterizedType) type).getRawType());
465
466 if (type instanceof GenericArrayType) {
467 Type subType = ((GenericArrayType) type).getGenericComponentType();
468 Class c = getRawClass(subType);
469 return Array.newInstance(c, 0).getClass();
470 }
471
472 throw new IllegalArgumentException(
Stuart McCulloch4482c702012-06-15 13:27:53 +0000473 "Does not support generics beyond Parameterized Type and GenericArrayType, got " + type);
Stuart McCullochf3173222012-06-07 21:57:32 +0000474 }
475
Stuart McCulloch0b639c62012-06-12 12:41:16 +0000476 /**
477 * Ignore null values in output and input
Stuart McCulloch4482c702012-06-15 13:27:53 +0000478 *
Stuart McCulloch0b639c62012-06-12 12:41:16 +0000479 * @param ignorenull
480 * @return
481 */
482 public JSONCodec setIgnorenull(boolean ignorenull) {
483 this.ignorenull = ignorenull;
484 return this;
485 }
486
487 public boolean isIgnorenull() {
488 return ignorenull;
489 }
490
Stuart McCulloch55fbda52012-08-02 13:26:25 +0000491 /**
492 * Use hex instead of default base 64 encoding
493 *
494 * @param useHex
495 * @return
496 */
497 public JSONCodec setHex(boolean useHex) {
498 this.useHex = useHex;
499 return this;
500 }
501
502 public boolean isHex() {
503 return useHex;
504 }
505
Stuart McCullochf3173222012-06-07 21:57:32 +0000506}