| package aQute.lib.json; |
| |
| import java.io.*; |
| import java.lang.reflect.*; |
| import java.util.*; |
| import java.util.regex.*; |
| |
| /** |
| * This is a simple JSON Coder and Encoder that uses the Java type system to |
| * convert data objects to JSON and JSON to (type safe) Java objects. The |
| * conversion is very much driven by classes and their public fields. Generic |
| * information, when present is taken into account. </p> Usage patterns to |
| * encode: |
| * |
| * <pre> |
| * JSONCoder codec = new JSONCodec(); // |
| * assert "1".equals( codec.enc().to().put(1).toString()); |
| * assert "[1,2,3]".equals( codec.enc().to().put(Arrays.asList(1,2,3).toString()); |
| * |
| * Map m = new HashMap(); |
| * m.put("a", "A"); |
| * assert "{\"a\":\"A\"}".equals( codec.enc().to().put(m).toString()); |
| * |
| * static class D { public int a; } |
| * D d = new D(); |
| * d.a = 41; |
| * assert "{\"a\":41}".equals( codec.enc().to().put(d).toString()); |
| * </pre> |
| * |
| * It is possible to redirect the encoder to another output (default is a |
| * string). See {@link Encoder#to()},{@link Encoder#to(File))}, |
| * {@link Encoder#to(OutputStream)}, {@link Encoder#to(Appendable))}. To reset |
| * the string output call {@link Encoder#to()}. |
| * <p/> |
| * This Codec class can be used in a concurrent environment. The Decoders and |
| * Encoders, however, must only be used in a single thread. |
| */ |
| public class JSONCodec { |
| final static String START_CHARACTERS = "[{\"-0123456789tfn"; |
| |
| // Handlers |
| private final static WeakHashMap<Type,Handler> handlers = new WeakHashMap<Type,Handler>(); |
| private static StringHandler sh = new StringHandler(); |
| private static BooleanHandler bh = new BooleanHandler(); |
| private static CharacterHandler ch = new CharacterHandler(); |
| private static CollectionHandler dch = new CollectionHandler(ArrayList.class, |
| Object.class); |
| private static SpecialHandler sph = new SpecialHandler(Pattern.class, null, null); |
| private static DateHandler sdh = new DateHandler(); |
| private static FileHandler fh = new FileHandler(); |
| private static ByteArrayHandler byteh = new ByteArrayHandler(); |
| |
| boolean ignorenull; |
| boolean useHex; |
| |
| /** |
| * Create a new Encoder with the state and appropriate API. |
| * |
| * @return an Encoder |
| */ |
| public Encoder enc() { |
| return new Encoder(this); |
| } |
| |
| /** |
| * Create a new Decoder with the state and appropriate API. |
| * |
| * @return a Decoder |
| */ |
| public Decoder dec() { |
| return new Decoder(this); |
| } |
| |
| /* |
| * Work horse encode methods, all encoding ends up here. |
| */ |
| void encode(Encoder app, Object object, Type type, Map<Object,Type> visited) throws Exception { |
| |
| // Get the null out of the way |
| |
| if (object == null) { |
| app.append("null"); |
| return; |
| } |
| |
| // If we have no type or the type is Object.class |
| // we take the type of the object itself. Normally types |
| // come from declaration sites (returns, fields, methods, etc) |
| // and contain generic info. |
| |
| if (type == null || type == Object.class) |
| type = object.getClass(); |
| |
| // Dispatch to the handler who knows how to handle the given type. |
| Handler h = getHandler(type); |
| h.encode(app, object, visited); |
| } |
| |
| /* |
| * This method figures out which handler should handle the type specific |
| * stuff. It returns a handler for each type. If no appropriate handler |
| * exists, it will create one for the given type. There are actually quite a |
| * lot of handlers since Java is not very object oriented. |
| * @param type |
| * @return |
| * @throws Exception |
| */ |
| Handler getHandler(Type type) throws Exception { |
| |
| // First the static hard coded handlers for the common types. |
| |
| if (type == String.class) |
| return sh; |
| |
| if (type == Boolean.class || type == boolean.class) |
| return bh; |
| |
| if (type == byte[].class) |
| return byteh; |
| |
| if (Character.class == type || char.class == type) |
| return ch; |
| |
| if (Pattern.class == type) |
| return sph; |
| |
| if (Date.class == type) |
| return sdh; |
| |
| if (File.class == type) |
| return fh; |
| |
| Handler h; |
| synchronized (handlers) { |
| h = handlers.get(type); |
| } |
| |
| if (h != null) |
| return h; |
| |
| if (type instanceof Class) { |
| |
| Class< ? > clazz = (Class< ? >) type; |
| |
| if (Enum.class.isAssignableFrom(clazz)) |
| h = new EnumHandler(clazz); |
| else if (Iterable.class.isAssignableFrom(clazz)) // A Non Generic |
| // collection |
| |
| h = dch; |
| else if (clazz.isArray()) // Non generic array |
| h = new ArrayHandler(clazz, clazz.getComponentType()); |
| else if (Map.class.isAssignableFrom(clazz)) // A Non Generic map |
| h = new MapHandler(clazz, Object.class, Object.class); |
| else if (Number.class.isAssignableFrom(clazz) || clazz.isPrimitive()) |
| h = new NumberHandler(clazz); |
| else { |
| Method valueOf = null; |
| Constructor< ? > constructor = null; |
| |
| try { |
| constructor = clazz.getConstructor(String.class); |
| } |
| catch (Exception e) { |
| // Ignore |
| } |
| try { |
| valueOf = clazz.getMethod("valueOf", String.class); |
| } |
| catch (Exception e) { |
| // Ignore |
| } |
| if (constructor != null || valueOf != null) |
| h = new SpecialHandler(clazz, constructor, valueOf); |
| else |
| h = new ObjectHandler(this, clazz); // Hmm, might not be a |
| // data class ... |
| } |
| |
| } else { |
| |
| // We have generic information available |
| // We only support generics on Collection, Map, and arrays |
| |
| if (type instanceof ParameterizedType) { |
| ParameterizedType pt = (ParameterizedType) type; |
| Type rawType = pt.getRawType(); |
| if (rawType instanceof Class) { |
| Class< ? > rawClass = (Class< ? >) rawType; |
| if (Iterable.class.isAssignableFrom(rawClass)) |
| h = new CollectionHandler(rawClass, pt.getActualTypeArguments()[0]); |
| else if (Map.class.isAssignableFrom(rawClass)) |
| h = new MapHandler(rawClass, pt.getActualTypeArguments()[0], pt.getActualTypeArguments()[1]); |
| else if (Dictionary.class.isAssignableFrom(rawClass)) |
| h = new MapHandler(Hashtable.class, pt.getActualTypeArguments()[0], |
| pt.getActualTypeArguments()[1]); |
| else |
| throw new IllegalArgumentException("Found a parameterized type that is not a map or collection"); |
| } |
| } else if (type instanceof GenericArrayType) { |
| GenericArrayType gat = (GenericArrayType) type; |
| h = new ArrayHandler(getRawClass(type), gat.getGenericComponentType()); |
| } else |
| throw new IllegalArgumentException("Found a parameterized type that is not a map or collection"); |
| } |
| synchronized (handlers) { |
| // We might actually have duplicates |
| // but who cares? They should be identical |
| handlers.put(type, h); |
| } |
| return h; |
| } |
| |
| Object decode(Type type, Decoder isr) throws Exception { |
| int c = isr.skipWs(); |
| Handler h; |
| |
| if (type == null || type == Object.class) { |
| |
| // Establish default behavior when we run without |
| // type information |
| |
| switch (c) { |
| case '{' : |
| type = LinkedHashMap.class; |
| break; |
| |
| case '[' : |
| type = ArrayList.class; |
| break; |
| |
| case '"' : |
| return parseString(isr); |
| |
| case 'n' : |
| isr.expect("ull"); |
| return null; |
| |
| case 't' : |
| isr.expect("rue"); |
| return true; |
| |
| case 'f' : |
| isr.expect("alse"); |
| return false; |
| |
| case '0' : |
| case '1' : |
| case '2' : |
| case '3' : |
| case '4' : |
| case '5' : |
| case '6' : |
| case '7' : |
| case '8' : |
| case '9' : |
| case '-' : |
| return parseNumber(isr); |
| |
| default : |
| throw new IllegalArgumentException("Invalid character at begin of token: " + (char) c); |
| } |
| } |
| |
| h = getHandler(type); |
| |
| switch (c) { |
| case '{' : |
| return h.decodeObject(isr); |
| |
| case '[' : |
| return h.decodeArray(isr); |
| |
| case '"' : |
| return h.decode(isr, parseString(isr)); |
| |
| case 'n' : |
| isr.expect("ull"); |
| return h.decode(isr); |
| |
| case 't' : |
| isr.expect("rue"); |
| return h.decode(isr,Boolean.TRUE); |
| |
| case 'f' : |
| isr.expect("alse"); |
| return h.decode(isr,Boolean.FALSE); |
| |
| case '0' : |
| case '1' : |
| case '2' : |
| case '3' : |
| case '4' : |
| case '5' : |
| case '6' : |
| case '7' : |
| case '8' : |
| case '9' : |
| case '-' : |
| return h.decode(isr,parseNumber(isr)); |
| |
| default : |
| throw new IllegalArgumentException("Unexpected character in input stream: " + (char) c); |
| } |
| } |
| |
| String parseString(Decoder r) throws Exception { |
| assert r.current() == '"'; |
| |
| int c = r.next(); // skip first " |
| |
| StringBuilder sb = new StringBuilder(); |
| while (c != '"') { |
| if (c < 0 || Character.isISOControl(c)) |
| throw new IllegalArgumentException("JSON strings may not contain control characters: " + r.current()); |
| |
| if (c == '\\') { |
| c = r.read(); |
| switch (c) { |
| case '"' : |
| case '\\' : |
| case '/' : |
| sb.append((char) c); |
| break; |
| |
| case 'b' : |
| sb.append('\b'); |
| break; |
| |
| case 'f' : |
| sb.append('\f'); |
| break; |
| case 'n' : |
| sb.append('\n'); |
| break; |
| case 'r' : |
| sb.append('\r'); |
| break; |
| case 't' : |
| sb.append('\t'); |
| break; |
| case 'u' : |
| int a3 = hexDigit(r.read()) << 12; |
| int a2 = hexDigit(r.read()) << 8; |
| int a1 = hexDigit(r.read()) << 4; |
| int a0 = hexDigit(r.read()) << 0; |
| c = a3 + a2 + a1 + a0; |
| sb.append((char) c); |
| break; |
| |
| default : |
| throw new IllegalArgumentException( |
| "The only characters after a backslash are \", \\, b, f, n, r, t, and u but got " + c); |
| } |
| } else |
| sb.append((char) c); |
| |
| c = r.read(); |
| } |
| assert c == '"'; |
| r.read(); // skip quote |
| return sb.toString(); |
| } |
| |
| private int hexDigit(int c) throws EOFException { |
| if (c >= '0' && c <= '9') |
| return c - '0'; |
| |
| if (c >= 'A' && c <= 'F') |
| return c - 'A' + 10; |
| |
| if (c >= 'a' && c <= 'f') |
| return c - 'a' + 10; |
| |
| throw new IllegalArgumentException("Invalid hex character: " + c); |
| } |
| |
| private Number parseNumber(Decoder r) throws Exception { |
| StringBuilder sb = new StringBuilder(); |
| boolean d = false; |
| |
| if (r.current() == '-') { |
| sb.append('-'); |
| r.read(); |
| } |
| |
| int c = r.current(); |
| if (c == '0') { |
| sb.append('0'); |
| c = r.read(); |
| } else if (c >= '1' && c <= '9') { |
| sb.append((char) c); |
| c = r.read(); |
| |
| while (c >= '0' && c <= '9') { |
| sb.append((char) c); |
| c = r.read(); |
| } |
| } else |
| throw new IllegalArgumentException("Expected digit"); |
| |
| if (c == '.') { |
| d = true; |
| sb.append('.'); |
| c = r.read(); |
| while (c >= '0' && c <= '9') { |
| sb.append((char) c); |
| c = r.read(); |
| } |
| } |
| if (c == 'e' || c == 'E') { |
| d = true; |
| sb.append('e'); |
| c = r.read(); |
| if (c == '+') { |
| sb.append('+'); |
| c = r.read(); |
| } else if (c == '-') { |
| sb.append('-'); |
| c = r.read(); |
| } |
| while (c >= '0' && c <= '9') { |
| sb.append((char) c); |
| c = r.read(); |
| } |
| } |
| if (d) |
| return Double.parseDouble(sb.toString()); |
| long l = Long.parseLong(sb.toString()); |
| if (l > Integer.MAX_VALUE || l < Integer.MIN_VALUE) |
| return l; |
| return (int) l; |
| } |
| |
| void parseArray(Collection<Object> list, Type componentType, Decoder r) throws Exception { |
| assert r.current() == '['; |
| int c = r.next(); |
| while (START_CHARACTERS.indexOf(c) >= 0) { |
| Object o = decode(componentType, r); |
| list.add(o); |
| |
| c = r.skipWs(); |
| if (c == ']') |
| break; |
| |
| if (c == ',') { |
| c = r.next(); |
| continue; |
| } |
| |
| throw new IllegalArgumentException("Invalid character in parsing list, expected ] or , but found " |
| + (char) c); |
| } |
| assert r.current() == ']'; |
| r.read(); // skip closing |
| } |
| |
| @SuppressWarnings("rawtypes") |
| Class< ? > getRawClass(Type type) { |
| if (type instanceof Class) |
| return (Class) type; |
| |
| if (type instanceof ParameterizedType) |
| return getRawClass(((ParameterizedType) type).getRawType()); |
| |
| if (type instanceof GenericArrayType) { |
| Type subType = ((GenericArrayType) type).getGenericComponentType(); |
| Class c = getRawClass(subType); |
| return Array.newInstance(c, 0).getClass(); |
| } |
| |
| throw new IllegalArgumentException( |
| "Does not support generics beyond Parameterized Type and GenericArrayType, got " + type); |
| } |
| |
| /** |
| * Ignore null values in output and input |
| * |
| * @param ignorenull |
| * @return |
| */ |
| public JSONCodec setIgnorenull(boolean ignorenull) { |
| this.ignorenull = ignorenull; |
| return this; |
| } |
| |
| public boolean isIgnorenull() { |
| return ignorenull; |
| } |
| |
| /** |
| * Use hex instead of default base 64 encoding |
| * |
| * @param useHex |
| * @return |
| */ |
| public JSONCodec setHex(boolean useHex) { |
| this.useHex = useHex; |
| return this; |
| } |
| |
| public boolean isHex() { |
| return useHex; |
| } |
| |
| } |