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