package net.varkhan.base.conversion.formats;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.*;
/**
* <b>JSON syntax parser and converter</b>.
* <p/>
* This class provides several static utilities, and helper objects, to serialize and deserialize
* basic Java objects (boolean, number, String, arrays, List and Map) to and from their
* representation as JSON text.
* <p/>
*
* @author varkhan
* @date 3/18/12
* @time 2:38 PM
*/
public class Json {
protected Json() { }
protected static final String LITERAL_TRUE = "true";
protected static final String LITERAL_FALSE = "false";
protected static final String LITERAL_NULL = "null";
/*********************************************************************************
** JSON writing
**/
/**
* Writes an object as a String.
*
* @param val the object to write
* @return a JSON string representation of thr object
*/
public static String toJson(Object val) {
StringBuilder buf = new StringBuilder();
try { writeObject(buf,val); }
catch (IOException e) { /* ignore: StringBuilder doesn't throw this */ }
return buf.toString();
}
/**
* <b>An interface that can be implemented by objects that can be written as Json.</b>
* <p/>
* This interface defines a #writeJson(Appendable) method, that handles the
* serialization of the object's data as Json to a character stream.
* <p/>
* It is the responsibility of the implementor to ensure that <em>valid Json</em>
* is produced.
*/
public static interface Writable {
public void writeJson(Appendable out) throws IOException;
}
/**
* Writes an object to an {@link Appendable}.
*
* @param out the output Appendable
* @param obj the object to write
* @param <A> the Appendable type
*
* @return the output Appendable (to facilitate chaining)
*
* @throws IOException if the output Appendable generated an exception
*/
@SuppressWarnings("unchecked")
public static <A extends Appendable> A writeObject(A out, Object obj) throws IOException {
if(obj==null) {
writeNull(out);
}
else if(obj instanceof Boolean) {
writeBoolean(out, (Boolean) obj);
}
else if(obj instanceof Number) {
writeNumber(out, (Number) obj);
}
else if(obj instanceof CharSequence) {
out.append('"');
writeString(out, (CharSequence) obj);
out.append('"');
}
else if(obj.getClass().isArray()) {
out.append('[');
writeArray(out, (Object[]) obj);
out.append(']');
}
else if(obj instanceof Map) {
out.append('{');
writeMap(out, (Map<CharSequence,Object>) obj);
out.append('}');
}
else if(obj instanceof List) {
out.append('[');
writeList(out, (List<Object>) obj);
out.append(']');
}
else if(obj instanceof Writable) {
((Writable) obj).writeJson(out);
}
else throw new IllegalArgumentException("Cannot serialize object to JSON: unknown class "+obj.getClass().getCanonicalName());
return out;
}
/**
* Writes a map to an {@link Appendable}.
*
* @param out the output Appendable
* @param map the map to write
* @param <A> the Appendable type
*
* @return the output Appendable (to facilitate chaining)
*
* @throws IOException if the output Appendable generated an exception
*/
public static <A extends Appendable> A writeMap(A out, Map<? extends CharSequence,?> map) throws IOException {
@SuppressWarnings("unchecked")
Iterator<Map.Entry<CharSequence, Object>> it = ((Map<CharSequence,Object>) map).entrySet().iterator();
while(it.hasNext()) {
Map.Entry<CharSequence, ?> x = it.next();
writeField(out, x.getKey().toString(), x.getValue());
if(it.hasNext()) out.append(',');
}
return out;
}
/**
* Writes a list to an {@link Appendable}.
*
* @param out the output Appendable
* @param lst the list to write
* @param <A> the Appendable type
*
* @return the output Appendable (to facilitate chaining)
*
* @throws IOException if the output Appendable generated an exception
*/
public static <A extends Appendable> A writeList(A out, List<?> lst) throws IOException {
Iterator<?> it = lst.iterator();
while(it.hasNext()) {
writeObject(out, it.next());
if(it.hasNext()) out.append(',');
}
return out;
}
/**
* Writes an array to an {@link Appendable}.
*
* @param out the output Appendable
* @param lst the variadic array to write
* @param <A> the Appendable type
*
* @return the output Appendable (to facilitate chaining)
*
* @throws IOException if the output Appendable generated an exception
*/
public static <A extends Appendable> A writeArray(A out, Object... lst) throws IOException {
final int len = lst.length;
for(int i=0;i<len;i++) {
if(i>0) out.append(',');
writeObject(out, lst[i]);
}
return out;
}
public static <A extends Appendable> A writeField(A out, CharSequence key, Object val) throws IOException {
out.append('"');
writeName(out, key);
out.append('"').append(':');
writeObject(out, val);
return out;
}
public static <A extends Appendable> A writeName(A out, CharSequence str) throws IOException {
final int ls = str.length();
for(int i=0;i<ls;i++) {
char c = str.charAt(i);
switch (c) {
case '\\': out.append("\\\\"); break;
case '\"': out.append("\\\""); break;
default:
if (c >= ' ' && c < 127) out.append(c);
else throw new IOException("Invalid JSON field name: \""+str+"\"");
break;
}
}
return out;
}
public static <A extends Appendable> A writeString(A out, CharSequence str) throws IOException {
final int ls = str.length();
for(int i=0;i<ls;i++) {
char c= str.charAt(i);
switch (c) {
case '\\': out.append("\\\\"); break;
case '\"': out.append("\\\""); break;
case '\b': out.append("\\b"); break;
case '\f': out.append("\\f"); break;
case '\n': out.append("\\n"); break;
case '\r': out.append("\\r"); break;
case '\t': out.append("\\t"); break;
default:
if (c >= ' ' && c < 127) out.append(c);
else out.append(String.format("\\u%04x" ,(int)c));
break;
}
}
return out;
}
public static <A extends Appendable> A writeNumber(A out, Number n) throws IOException {
if(n.longValue()==n.doubleValue()) {
out.append(Long.toString(n.longValue()));
}
else {
out.append(Double.toString(n.doubleValue()));
}
return out;
}
public static <A extends Appendable> A writeBoolean(A out, boolean b) throws IOException {
if(b) out.append(LITERAL_TRUE);
else out.append(LITERAL_FALSE);
return out;
}
public static <A extends Appendable> A writeNull(A out) throws IOException {
out.append(LITERAL_NULL);
return out;
}
/*********************************************************************************
** JSON reading
**/
/**
* State-aware wrapper for a reader and current char, needed to be able to read JSON types that do not have delimiters
*/
protected static class Parser {
private final Reader in;
private int st = ' ';
private int ln = 0;
private int cn = 0;
public Parser(Reader in) {
this.in=in;
}
/**
* The last character read.
* @return the last character read, or -1 if EOS has been reached
*/
public int last() { return st; }
/**
* Reads one character from the stream.
* @return the character read from the stream, or -1 if EOS has been reached
* @throws IOException if an I/O error occurred while reading from the stream
*/
public int next() throws IOException {
st = in.read();
if(st=='\n') { ln++; cn=0; }
else if(st>=0) cn ++;
return st;
}
/**
* Reads and discards all whitespace characters until a non-whitespace character is reached
* @return the last (non-whitespace) character read from the stream, or -1 if EOS has been reached
* @throws IOException if an I/O error occurred while reading from the stream
*/
public int skipWhitespace() throws IOException {
while(st>=0 && isWhiteSpace(st)) { next(); }
return st;
}
/**
* Create a new format exception
*
* @return a FormatException indicating line and column numbers
*/
public FormatException exception(String msg) {
return new FormatException(msg + " at ln:"+ln+",cn:"+cn+" near "+st, ln, cn, null);
}
/**
* Create a new format exception
*
* @return a FormatException indicating line and column numbers
*/
public FormatException exception(String msg, CharSequence ctx) {
return new FormatException(msg + " at ln:"+ln+",cn:"+cn+" near "+st+": "+ctx, ln, cn, ctx.toString());
}
/**
* Create a new format exception
*
* @return a FormatException indicating line and column numbers
*/
public FormatException exception(String msg, CharSequence ctx, Throwable exc) {
return new FormatException(msg + " at ln:"+ln+",cn:"+cn+" near "+st+": "+ctx, ln, cn, ctx.toString(), exc);
}
}
public static Object asJson(CharSequence str) throws FormatException {
if(str==null) return null;
try { return readObject(new StringReader(str.toString())); }
catch(IOException e) { return null; }
}
public static Object readObject(Reader in) throws IOException, FormatException {
Parser p = new Parser(in);
p.skipWhitespace();
return readObject(p);
}
protected static Object readObject(Parser p) throws IOException, FormatException {
int c=p.last();
switch(c) {
case '{': {
p.next();
Map<CharSequence,Object> obj=readMap(p, '"', ':', ',', '}');
c=p.last();
if(c!='}') throw p.exception("Unterminated map");
p.next();
return obj;
}
case '[': {
p.next();
List<Object> obj=readList(p, ',', ']');
c=p.last();
if(c!=']') throw p.exception("Unterminated list");
p.next();
return obj;
}
case '"': {
p.next();
String obj=readString(p, c);
c=p.last();
if(c!='"') throw p.exception("Unterminated string");
p.next();
return obj;
}
default:
return readLiteral(p);
}
}
public static Number readNumber(Reader in) throws IOException, FormatException {
Parser p = new Parser(in);
p.skipWhitespace();
return readNumber(p);
}
protected static Number readNumber(Parser p) throws IOException, FormatException {
StringBuilder buf = new StringBuilder();
boolean isInteger = true;
boolean isFloat = true;
int c = p.last();
while(c>=0 && !isWhiteSpace(c)) {
isInteger &= isValidIntegerChar(c);
isFloat &= isValidNumberChar(c);
buf.append((char)c);
c = p.next();
}
if(isInteger) try {
return Long.parseLong(buf.toString());
}
catch(NumberFormatException e) {
throw p.exception("Invalid number format",buf,e);
}
if(isFloat) try {
return Double.parseDouble(buf.toString());
}
catch(NumberFormatException e) {
throw p.exception("Invalid number format",buf,e);
}
throw p.exception("Invalid number",buf);
}
public static boolean readBoolean(Reader in) throws IOException, FormatException {
Parser p = new Parser(in);
p.skipWhitespace();
return readBoolean(p);
}
protected static boolean readBoolean(Parser p) throws IOException, FormatException {
StringBuilder buf = new StringBuilder();
int c = p.last();
while(c>=0 && c>='a' && c<='z') {
buf.append((char)c);
c = p.next();
}
if(isEqualSequence(buf, LITERAL_FALSE)) return false;
if(isEqualSequence(buf, LITERAL_TRUE)) return true;
throw p.exception("Invalid boolean",buf);
}
protected static Object readLiteral(Parser p) throws IOException, FormatException {
StringBuilder buf = new StringBuilder();
boolean isInteger = true;
boolean isFloat = true;
int c = p.last();
while(c>=0 && !isWhiteSpace(c) && !isReservedChar(c)) {
isInteger &= isValidIntegerChar(c);
isFloat &= isValidNumberChar(c);
buf.append((char)c);
c = p.next();
}
if(isEqualSequence(buf, LITERAL_NULL)) return null;
if(isEqualSequence(buf, LITERAL_FALSE)) return false;
if(isEqualSequence(buf, LITERAL_TRUE)) return true;
if(isInteger) try {
return Long.parseLong(buf.toString());
}
catch(NumberFormatException e) {
throw p.exception("Invalid number format",buf,e);
}
if(isFloat) try {
return Double.parseDouble(buf.toString());
}
catch(NumberFormatException e) {
throw p.exception("Invalid number format",buf,e);
}
throw p.exception("Invalid literal",buf);
}
public static String readString(Reader in) throws IOException, FormatException {
Parser p = new Parser(in);
int c = p.skipWhitespace();
char t='"';
if(c!=t) throw p.exception("Invalid character sequence format");
// Skip leading "
p.next();
String obj=readString(p, t);
c=p.last();
if(c!=t) throw p.exception("Unterminated string");
p.next();
return obj;
}
protected static String readString(Parser p, int t) throws IOException, FormatException {
StringBuilder buf = new StringBuilder();
int c = p.last();
// Read all characters until an unescaped terminator is found
while(c>=0 && c!=t) {
// Decode escape sequences
if(c=='\\') {
c = p.next();
if(c<=0) throw new IOException("Unterminated character escape");
switch(c) {
case '\\': buf.append('\\'); break;
case '\"': buf.append('\"'); break;
case '\'': buf.append('\''); break;
case 'b': buf.append('\b'); break;
case 'f': buf.append('\f'); break;
case 'n': buf.append('\n'); break;
case 'r': buf.append('\r'); break;
case 't': buf.append('\t'); break;
case 'u':
// Decode unicode escapes
int x = 0;
c = p.next();
if(c<0) throw p.exception("Unterminated unicode escape");
int d = asHexDigit(c);
if(d<0) throw p.exception("Invalid unicode escape character "+(char)c);
x |= d<<12;
c = p.next();
if(c<0) throw p.exception("Unterminated unicode escape");
d = asHexDigit(c);
if(d<0) throw p.exception("Invalid unicode escape character "+(char)c);
x |= d<<8;
c = p.next();
if(c<0) throw p.exception("Unterminated unicode escape");
d = asHexDigit(c);
if(d<0) throw p.exception("Invalid unicode escape character "+(char)c);
x |= d<<4;
c = p.next();
if(c<0) throw p.exception("Unterminated unicode escape");
d = asHexDigit(c);
if(d<0) throw p.exception("Invalid unicode escape character "+(char)c);
x |= d;
buf.append((char)x);
break;
default: buf.append((char)c); break;
}
}
else buf.append((char)c);
c = p.next();
}
return buf.toString();
}
public static List<Object> readList(Reader in) throws IOException, FormatException {
Parser p = new Parser(in);
int c = p.skipWhitespace();
if(c!='[') throw p.exception("Invalid list format");
// Skip leading [
p.next();
List<Object> obj=readList(p, ',', ']');
c=p.last();
if(c!=']') throw p.exception("Unterminated list");
// Skip trailing ]
p.next();
return obj;
}
protected static List<Object> readList(Parser p, char r, char t) throws IOException, FormatException {
List<Object> lst = new ArrayList<Object>();
int c = p.last();
// Read all objects until ] is found or the end of the stream is reached
while(c>=0) {
// Skip leading whitespace
c = p.skipWhitespace();
if(c==t|| c<0) break;
Object val = readObject(p);
lst.add(val);
// Skip trailing whitespace
c = p.skipWhitespace();
// Return on terminator
if(c==t || c<0) break;
// Validate and skip separator
else if(c==r) c = p.next();
else throw p.exception("Unexpected bare object in list");
}
return lst;
}
public static Map<CharSequence,Object> readMap(Reader in) throws IOException, FormatException {
Parser p = new Parser(in);
int c = p.skipWhitespace();
if(c!='{') throw p.exception("Invalid map format");
// Skip leading {
p.next();
Map<CharSequence,Object> obj=readMap(p, '"', ':', ',', '}');
c=p.last();
if(c!='}') throw p.exception("Unterminated map");
// Skip trailing
p.next();
return obj;
}
protected static Map<CharSequence,Object> readMap(Parser p, char f, char k, char r, char t) throws IOException, FormatException {
Map<CharSequence,Object> map = new LinkedHashMap<CharSequence,Object>();
int c = p.last();
// Read all objects until } is found or the end of the stream is reached
while(c>=0) {
// Skip leading whitespace
c = p.skipWhitespace();
// Return on terminator
if(c==t|| c<0) break;
String key;
if(c==f) {
// Skip first quote
p.next();
key = readString(p, f);
c=p.last();
if(c!=f) throw p.exception("Unterminated field");
// Skip last quote
p.next();
}
else {
// Parse in raw field: sequence of non-whitespace, non-reserved chars
StringBuilder buf = new StringBuilder();
while(c>=0 && !isWhiteSpace(c) && !isReservedChar(c)) { buf.append((char)c); c = p.next(); }
key = buf.toString();
}
// Skip intermediary whitespace
c = p.skipWhitespace();
if(c!=k) throw p.exception("Unexpected bare object in map");
else if(c<0) throw p.exception("Unterminated map");
// Skip key separator
p.next();
// Skip intermediary whitespace
p.skipWhitespace();
Object val = readObject(p);
map.put(key, val);
// Skip trailing whitespace
c = p.skipWhitespace();
// Return on terminator
if(c==t|| c<0) break;
// Validate and skip separator
else if(c==r) c = p.next();
else throw p.exception("Unexpected bare object in map");
}
return map;
}
/**
* Checks whether a character is white-space
*
* @param c the character to check
* @return {@literal true} if the character is whitespace
*/
public static boolean isWhiteSpace(int c) {
return c==' '||c=='\n'||c=='\r'||c=='\t';
}
/**
* Converts a decimal digit to its corresponding numeric value.
*
* @param c the character to convert
* @return the decimal value of the character, or {@literal -1} if it is not a valid decimal digit
*/
protected static int asDecDigit(int c) {
if(c>='0' && c<='9') return c-'0';
return -1;
}
/**
* Converts an hexadecimal digit to its corresponding numeric value.
*
* @param c the character to convert
* @return the hexadecimal value of the character, or {@literal -1} if it is not a valid hexadecimal digit
*/
protected static int asHexDigit(int c) {
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;
return -1;
}
/**
* Checks whether a character is legal in the character representation of an integer number.
*
* @param c the character to check
* @return {@literal true} if the character is legal in an integer number
*/
public static boolean isValidIntegerChar(int c) {
return (c>='0' && c<='9') || c=='-' || c=='+';
}
/**
* Checks whether a character is legal in the character representation of a floating point number.
*
* @param c the character to check
* @return {@literal true} if the character is legal in a floating point number
*/
public static boolean isValidNumberChar(int c) {
return (c>='0' && c<='9') || c=='-' || c=='+' || c=='.' || c=='e' || c=='E';
}
/**
* Checks whether a character is a reserved JSON character.
*
* @param c the character to check
* @return {@literal true} if the character is one of ,:[]{}'"
*/
public static boolean isReservedChar(int c) {
return c==',' || c==':' || c=='[' || c==']' || c=='{' || c=='}' || c=='\'' || c=='"';
}
/**
* Checks whether two character sequences are identical.
*
* @param o1 the first sequence
* @param o2 the second sequence
* @return {@literal true} iff the two sequences are both {@literal null},
* or have the same length and identical characters at each respective index
*/
protected static boolean isEqualSequence(CharSequence o1, CharSequence o2) {
if(o1==o2) return true;
if(o1==null || o2==null) return false;
int l1=o1.length();
if(l1!=o2.length()) return false;
for(int i=0; i<l1; i++) if(o1.charAt(i)!=o2.charAt(i)) return false;
return true;
}
}