blob: 47ad4abd9b6e1cdb6dee785044388906560ae1ae [file] [log] [blame]
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;
}
}