blob: 8e608f85099917edb2d558e14959e564506049b2 [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 McCulloch81d48de2012-06-29 19:23:09 +0000193 else if (Dictionary.class.isAssignableFrom(rawClass))
194 h = new MapHandler(Hashtable.class, pt.getActualTypeArguments()[0], pt.getActualTypeArguments()[1]);
Stuart McCullochbb014372012-06-07 21:57:32 +0000195 else
Stuart McCulloch2286f232012-06-15 13:27:53 +0000196 throw new IllegalArgumentException("Found a parameterized type that is not a map or collection");
Stuart McCullochbb014372012-06-07 21:57:32 +0000197 }
198 } else if (type instanceof GenericArrayType) {
199 GenericArrayType gat = (GenericArrayType) type;
200 h = new ArrayHandler(getRawClass(type), gat.getGenericComponentType());
201 } else
Stuart McCulloch2286f232012-06-15 13:27:53 +0000202 throw new IllegalArgumentException("Found a parameterized type that is not a map or collection");
Stuart McCullochbb014372012-06-07 21:57:32 +0000203 }
204 synchronized (handlers) {
205 // We might actually have duplicates
206 // but who cares? They should be identical
207 handlers.put(type, h);
208 }
209 return h;
210 }
211
212 Object decode(Type type, Decoder isr) throws Exception {
213 int c = isr.skipWs();
214 Handler h;
215
216 if (type == null || type == Object.class) {
217
218 // Establish default behavior when we run without
219 // type information
220
221 switch (c) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000222 case '{' :
223 type = LinkedHashMap.class;
224 break;
Stuart McCullochbb014372012-06-07 21:57:32 +0000225
Stuart McCulloch2286f232012-06-15 13:27:53 +0000226 case '[' :
227 type = ArrayList.class;
228 break;
Stuart McCullochbb014372012-06-07 21:57:32 +0000229
Stuart McCulloch2286f232012-06-15 13:27:53 +0000230 case '"' :
231 return parseString(isr);
Stuart McCullochbb014372012-06-07 21:57:32 +0000232
Stuart McCulloch2286f232012-06-15 13:27:53 +0000233 case 'n' :
234 isr.expect("ull");
235 return null;
Stuart McCullochbb014372012-06-07 21:57:32 +0000236
Stuart McCulloch2286f232012-06-15 13:27:53 +0000237 case 't' :
238 isr.expect("rue");
239 return true;
Stuart McCullochbb014372012-06-07 21:57:32 +0000240
Stuart McCulloch2286f232012-06-15 13:27:53 +0000241 case 'f' :
242 isr.expect("alse");
243 return false;
Stuart McCullochbb014372012-06-07 21:57:32 +0000244
Stuart McCulloch2286f232012-06-15 13:27:53 +0000245 case '0' :
246 case '1' :
247 case '2' :
248 case '3' :
249 case '4' :
250 case '5' :
251 case '6' :
252 case '7' :
253 case '8' :
254 case '9' :
255 case '-' :
256 return parseNumber(isr);
Stuart McCullochbb014372012-06-07 21:57:32 +0000257
Stuart McCulloch2286f232012-06-15 13:27:53 +0000258 default :
259 throw new IllegalArgumentException("Invalid character at begin of token: " + (char) c);
Stuart McCullochbb014372012-06-07 21:57:32 +0000260 }
261 }
262
263 h = getHandler(type);
264
265 switch (c) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000266 case '{' :
267 return h.decodeObject(isr);
Stuart McCullochbb014372012-06-07 21:57:32 +0000268
Stuart McCulloch2286f232012-06-15 13:27:53 +0000269 case '[' :
270 return h.decodeArray(isr);
Stuart McCullochbb014372012-06-07 21:57:32 +0000271
Stuart McCulloch2286f232012-06-15 13:27:53 +0000272 case '"' :
273 return h.decode(parseString(isr));
Stuart McCullochbb014372012-06-07 21:57:32 +0000274
Stuart McCulloch2286f232012-06-15 13:27:53 +0000275 case 'n' :
276 isr.expect("ull");
277 return h.decode();
Stuart McCullochbb014372012-06-07 21:57:32 +0000278
Stuart McCulloch2286f232012-06-15 13:27:53 +0000279 case 't' :
280 isr.expect("rue");
281 return h.decode(Boolean.TRUE);
Stuart McCullochbb014372012-06-07 21:57:32 +0000282
Stuart McCulloch2286f232012-06-15 13:27:53 +0000283 case 'f' :
284 isr.expect("alse");
285 return h.decode(Boolean.FALSE);
Stuart McCullochbb014372012-06-07 21:57:32 +0000286
Stuart McCulloch2286f232012-06-15 13:27:53 +0000287 case '0' :
288 case '1' :
289 case '2' :
290 case '3' :
291 case '4' :
292 case '5' :
293 case '6' :
294 case '7' :
295 case '8' :
296 case '9' :
297 case '-' :
298 return h.decode(parseNumber(isr));
Stuart McCullochbb014372012-06-07 21:57:32 +0000299
Stuart McCulloch2286f232012-06-15 13:27:53 +0000300 default :
301 throw new IllegalArgumentException("Unexpected character in input stream: " + (char) c);
Stuart McCullochbb014372012-06-07 21:57:32 +0000302 }
303 }
304
305 String parseString(Decoder r) throws Exception {
306 assert r.current() == '"';
307
308 int c = r.next(); // skip first "
309
310 StringBuilder sb = new StringBuilder();
311 while (c != '"') {
312 if (c < 0 || Character.isISOControl(c))
Stuart McCulloch2286f232012-06-15 13:27:53 +0000313 throw new IllegalArgumentException("JSON strings may not contain control characters: " + r.current());
Stuart McCullochbb014372012-06-07 21:57:32 +0000314
315 if (c == '\\') {
316 c = r.read();
317 switch (c) {
Stuart McCulloch2286f232012-06-15 13:27:53 +0000318 case '"' :
319 case '\\' :
320 case '/' :
321 sb.append((char) c);
322 break;
Stuart McCullochbb014372012-06-07 21:57:32 +0000323
Stuart McCulloch2286f232012-06-15 13:27:53 +0000324 case 'b' :
325 sb.append('\b');
326 break;
Stuart McCullochbb014372012-06-07 21:57:32 +0000327
Stuart McCulloch2286f232012-06-15 13:27:53 +0000328 case 'f' :
329 sb.append('\f');
330 break;
331 case 'n' :
332 sb.append('\n');
333 break;
334 case 'r' :
335 sb.append('\r');
336 break;
337 case 't' :
338 sb.append('\t');
339 break;
340 case 'u' :
341 int a3 = hexDigit(r.read()) << 12;
342 int a2 = hexDigit(r.read()) << 8;
343 int a1 = hexDigit(r.read()) << 4;
344 int a0 = hexDigit(r.read()) << 0;
345 c = a3 + a2 + a1 + a0;
346 sb.append((char) c);
347 break;
Stuart McCullochbb014372012-06-07 21:57:32 +0000348
Stuart McCulloch2286f232012-06-15 13:27:53 +0000349 default :
350 throw new IllegalArgumentException(
351 "The only characters after a backslash are \", \\, b, f, n, r, t, and u but got " + c);
Stuart McCullochbb014372012-06-07 21:57:32 +0000352 }
353 } else
354 sb.append((char) c);
355
356 c = r.read();
357 }
358 assert c == '"';
359 r.read(); // skip quote
360 return sb.toString();
361 }
362
363 private int hexDigit(int c) throws EOFException {
364 if (c >= '0' && c <= '9')
365 return c - '0';
366
367 if (c >= 'A' && c <= 'F')
368 return c - 'A' + 10;
369
370 if (c >= 'a' && c <= 'f')
371 return c - 'a' + 10;
372
373 throw new IllegalArgumentException("Invalid hex character: " + c);
374 }
375
376 private Number parseNumber(Decoder r) throws Exception {
377 StringBuilder sb = new StringBuilder();
378 boolean d = false;
379
380 if (r.current() == '-') {
381 sb.append('-');
382 r.read();
383 }
384
385 int c = r.current();
386 if (c == '0') {
387 sb.append('0');
388 c = r.read();
389 } else if (c >= '1' && c <= '9') {
390 sb.append((char) c);
391 c = r.read();
392
393 while (c >= '0' && c <= '9') {
394 sb.append((char) c);
395 c = r.read();
396 }
397 } else
398 throw new IllegalArgumentException("Expected digit");
399
400 if (c == '.') {
401 d = true;
402 sb.append('.');
403 c = r.read();
404 while (c >= '0' && c <= '9') {
405 sb.append((char) c);
406 c = r.read();
407 }
408 }
409 if (c == 'e' || c == 'E') {
410 d = true;
411 sb.append('e');
412 c = r.read();
413 if (c == '+') {
414 sb.append('+');
415 c = r.read();
416 } else if (c == '-') {
417 sb.append('-');
418 c = r.read();
419 }
420 while (c >= '0' && c <= '9') {
421 sb.append((char) c);
422 c = r.read();
423 }
424 }
425 if (d)
426 return Double.parseDouble(sb.toString());
427 long l = Long.parseLong(sb.toString());
428 if (l > Integer.MAX_VALUE || l < Integer.MIN_VALUE)
429 return l;
430 return (int) l;
431 }
432
433 void parseArray(Collection<Object> list, Type componentType, Decoder r) throws Exception {
434 assert r.current() == '[';
435 int c = r.next();
436 while (START_CHARACTERS.indexOf(c) >= 0) {
437 Object o = decode(componentType, r);
438 list.add(o);
439
440 c = r.skipWs();
441 if (c == ']')
442 break;
443
444 if (c == ',') {
445 c = r.next();
446 continue;
447 }
448
Stuart McCulloch2286f232012-06-15 13:27:53 +0000449 throw new IllegalArgumentException("Invalid character in parsing list, expected ] or , but found "
450 + (char) c);
Stuart McCullochbb014372012-06-07 21:57:32 +0000451 }
452 assert r.current() == ']';
453 r.read(); // skip closing
454 }
455
456 @SuppressWarnings("rawtypes")
Stuart McCulloch2286f232012-06-15 13:27:53 +0000457 Class< ? > getRawClass(Type type) {
Stuart McCullochbb014372012-06-07 21:57:32 +0000458 if (type instanceof Class)
459 return (Class) type;
460
461 if (type instanceof ParameterizedType)
462 return getRawClass(((ParameterizedType) type).getRawType());
463
464 if (type instanceof GenericArrayType) {
465 Type subType = ((GenericArrayType) type).getGenericComponentType();
466 Class c = getRawClass(subType);
467 return Array.newInstance(c, 0).getClass();
468 }
469
470 throw new IllegalArgumentException(
Stuart McCulloch2286f232012-06-15 13:27:53 +0000471 "Does not support generics beyond Parameterized Type and GenericArrayType, got " + type);
Stuart McCullochbb014372012-06-07 21:57:32 +0000472 }
473
Stuart McCulloch285034f2012-06-12 12:41:16 +0000474 /**
475 * Ignore null values in output and input
Stuart McCulloch2286f232012-06-15 13:27:53 +0000476 *
Stuart McCulloch285034f2012-06-12 12:41:16 +0000477 * @param ignorenull
478 * @return
479 */
480 public JSONCodec setIgnorenull(boolean ignorenull) {
481 this.ignorenull = ignorenull;
482 return this;
483 }
484
485 public boolean isIgnorenull() {
486 return ignorenull;
487 }
488
Stuart McCullochbb014372012-06-07 21:57:32 +0000489}