org.mortbay.util.ajax.JSON Maven / Gradle / Ivy
//========================================================================
//Copyright 2004-2008 Mort Bay Consulting Pty. Ltd.
//------------------------------------------------------------------------
//Licensed under the Apache License, Version 2.0 (the "License");
//you may not use this file except in compliance with the License.
//You may obtain a copy of the License at
//http://www.apache.org/licenses/LICENSE-2.0
//Unless required by applicable law or agreed to in writing, software
//distributed under the License is distributed on an "AS IS" BASIS,
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//See the License for the specific language governing permissions and
//limitations under the License.
//========================================================================
package org.mortbay.util.ajax;
import java.io.Externalizable;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.mortbay.log.Log;
import org.mortbay.util.IO;
import org.mortbay.util.Loader;
import org.mortbay.util.QuotedStringTokenizer;
import org.mortbay.util.TypeUtil;
/** JSON Parser and Generator.
*
* This class provides some static methods to convert POJOs to and from JSON
* notation. The mapping from JSON to java is:
* object ==> Map
* array ==> Object[]
* number ==> Double or Long
* string ==> String
* null ==> null
* bool ==> Boolean
*
*
* The java to JSON mapping is:
* String --> string
* Number --> number
* Map --> object
* List --> array
* Array --> array
* null --> null
* Boolean--> boolean
* Object --> string (dubious!)
*
*
* The interface {@link JSON.Convertible} may be implemented by classes that wish to externalize and
* initialize specific fields to and from JSON objects. Only directed acyclic graphs of objects are supported.
*
*
* The interface {@link JSON.Generator} may be implemented by classes that know how to render themselves as JSON and
* the {@link #toString(Object)} method will use {@link JSON.Generator#addJSON(StringBuffer)} to generate the JSON.
* The class {@link JSON.Literal} may be used to hold pre-gnerated JSON object.
*
* The interface {@link Convertor} may be implemented to provide static convertors for objects that may be registered
* with {@link #registerConvertor(Class, org.mortbay.util.ajax.JSON.Convertor)}. These convertors are looked up by class, interface and
* super class by {@link #getConvertor(Class)}.
*
* @author gregw
*
*/
public class JSON
{
private static JSON __default = new JSON();
private Map _convertors=Collections.synchronizedMap(new HashMap());
private int _stringBufferSize=256;
public JSON()
{
}
/* ------------------------------------------------------------ */
/**
* @return the initial stringBuffer size to use when creating JSON strings (default 256)
*/
public int getStringBufferSize()
{
return _stringBufferSize;
}
/* ------------------------------------------------------------ */
/**
* @param stringBufferSize the initial stringBuffer size to use when creating JSON strings (default 256)
*/
public void setStringBufferSize(int stringBufferSize)
{
_stringBufferSize=stringBufferSize;
}
/**
* Register a {@link Convertor} for a class or interface.
* @param forClass The class or interface that the convertor applies to
* @param convertor the convertor
*/
public static void registerConvertor(Class forClass, Convertor convertor)
{
__default.addConvertor(forClass,convertor);
}
public static JSON getDefault()
{
return __default;
}
public static void setDefault(JSON json)
{
__default=json;
}
public static String toString(Object object)
{
StringBuffer buffer=new StringBuffer(__default.getStringBufferSize());
synchronized (buffer)
{
__default.append(buffer,object);
return buffer.toString();
}
}
public static String toString(Map object)
{
StringBuffer buffer=new StringBuffer(__default.getStringBufferSize());
synchronized (buffer)
{
__default.appendMap(buffer,object);
return buffer.toString();
}
}
public static String toString(Object[] array)
{
StringBuffer buffer=new StringBuffer(__default.getStringBufferSize());
synchronized (buffer)
{
__default.appendArray(buffer,array);
return buffer.toString();
}
}
/**
* @param s String containing JSON object or array.
* @return A Map, Object array or primitive array parsed from the JSON.
*/
public static Object parse(String s)
{
return __default.parse(new StringSource(s),false);
}
/**
* @param s String containing JSON object or array.
* @param stripOuterComment If true, an outer comment around the JSON is ignored.
* @return A Map, Object array or primitive array parsed from the JSON.
*/
public static Object parse(String s, boolean stripOuterComment)
{
return __default.parse(new StringSource(s),stripOuterComment);
}
/**
* @param in Reader containing JSON object or array.
* @return A Map, Object array or primitive array parsed from the JSON.
*/
public static Object parse(Reader in) throws IOException
{
return __default.parse(new ReaderSource(in),false);
}
/**
* @param s Stream containing JSON object or array.
* @param stripOuterComment If true, an outer comment around the JSON is ignored.
* @return A Map, Object array or primitive array parsed from the JSON.
*/
public static Object parse(Reader in, boolean stripOuterComment) throws IOException
{
return __default.parse(new ReaderSource(in),stripOuterComment);
}
/**
* @deprecated use {@link #parse(Reader)}
* @param in Reader containing JSON object or array.
* @return A Map, Object array or primitive array parsed from the JSON.
*/
public static Object parse(InputStream in) throws IOException
{
return __default.parse(new StringSource(IO.toString(in)),false);
}
/**
* @deprecated use {@link #parse(Reader, boolean)}
* @param s Stream containing JSON object or array.
* @param stripOuterComment If true, an outer comment around the JSON is ignored.
* @return A Map, Object array or primitive array parsed from the JSON.
*/
public static Object parse(InputStream in, boolean stripOuterComment) throws IOException
{
return __default.parse(new StringSource(IO.toString(in)),stripOuterComment);
}
/* ------------------------------------------------------------ */
/** Convert Object to JSON
* @param object The object to convert
* @return The JSON String
*/
public String toJSON(Object object)
{
StringBuffer buffer=new StringBuffer(getStringBufferSize());
synchronized (buffer)
{
append(buffer,object);
return buffer.toString();
}
}
/* ------------------------------------------------------------ */
/** Convert JSON to Object
* @param json The json to convert
* @return The object
*/
public Object fromJSON(String json)
{
Source source = new StringSource(json);
return parse(source);
}
/**
* Append object as JSON to string buffer.
* @param buffer
* @param object
*/
public void append(StringBuffer buffer, Object object)
{
if (object==null)
buffer.append("null");
else if (object instanceof Convertible)
appendJSON(buffer,(Convertible)object);
else if (object instanceof Generator)
appendJSON(buffer,(Generator)object);
else if (object instanceof Map)
appendMap(buffer,(Map)object);
else if (object instanceof Collection)
appendArray(buffer,(Collection)object);
else if (object.getClass().isArray())
appendArray(buffer,object);
else if (object instanceof Number)
appendNumber(buffer,(Number)object);
else if (object instanceof Boolean)
appendBoolean(buffer,(Boolean)object);
else if (object instanceof String)
appendString(buffer,(String)object);
else
{
Convertor convertor=getConvertor(object.getClass());
if (convertor!=null)
appendJSON(buffer,convertor,object);
else
appendString(buffer,object.toString());
}
}
public void appendNull(StringBuffer buffer)
{
buffer.append("null");
}
public void appendJSON(final StringBuffer buffer, final Convertor convertor, final Object object)
{
appendJSON(buffer,new Convertible()
{
public void fromJSON(Map object)
{
}
public void toJSON(Output out)
{
convertor.toJSON(object,out);
}
});
}
public void appendJSON(final StringBuffer buffer, Convertible converter)
{
final char[] c=
{ '{' };
converter.toJSON(new Output()
{
public void add(Object obj)
{
if (c[0]==0)
throw new IllegalStateException();
append(buffer,obj);
c[0]=0;
}
public void addClass(Class type)
{
if (c[0]==0)
throw new IllegalStateException();
buffer.append(c);
buffer.append("\"class\":");
append(buffer,type.getName());
c[0]=',';
}
public void add(String name, Object value)
{
if (c[0]==0)
throw new IllegalStateException();
buffer.append(c);
QuotedStringTokenizer.quote(buffer,name);
buffer.append(':');
append(buffer,value);
c[0]=',';
}
public void add(String name, double value)
{
if (c[0]==0)
throw new IllegalStateException();
buffer.append(c);
QuotedStringTokenizer.quote(buffer,name);
buffer.append(':');
appendNumber(buffer,new Double(value));
c[0]=',';
}
public void add(String name, long value)
{
if (c[0]==0)
throw new IllegalStateException();
buffer.append(c);
QuotedStringTokenizer.quote(buffer,name);
buffer.append(':');
appendNumber(buffer,TypeUtil.newLong(value));
c[0]=',';
}
public void add(String name, boolean value)
{
if (c[0]==0)
throw new IllegalStateException();
buffer.append(c);
QuotedStringTokenizer.quote(buffer,name);
buffer.append(':');
appendBoolean(buffer,value?Boolean.TRUE:Boolean.FALSE);
c[0]=',';
}
});
if (c[0]=='{')
buffer.append("{}");
else if (c[0]!=0)
buffer.append("}");
}
public void appendJSON(StringBuffer buffer, Generator generator)
{
generator.addJSON(buffer);
}
public void appendMap(StringBuffer buffer, Map object)
{
if (object==null)
{
appendNull(buffer);
return;
}
buffer.append('{');
Iterator iter=object.entrySet().iterator();
while (iter.hasNext())
{
Map.Entry entry=(Map.Entry)iter.next();
QuotedStringTokenizer.quote(buffer,entry.getKey().toString());
buffer.append(':');
append(buffer,entry.getValue());
if (iter.hasNext())
buffer.append(',');
}
buffer.append('}');
}
public void appendArray(StringBuffer buffer, Collection collection)
{
if (collection==null)
{
appendNull(buffer);
return;
}
buffer.append('[');
Iterator iter=collection.iterator();
boolean first=true;
while (iter.hasNext())
{
if (!first)
buffer.append(',');
first=false;
append(buffer,iter.next());
}
buffer.append(']');
}
public void appendArray(StringBuffer buffer, Object array)
{
if (array==null)
{
appendNull(buffer);
return;
}
buffer.append('[');
int length=Array.getLength(array);
for (int i=0; i
* If no match is found for the class, then the interfaces for the class are tried. If still no
* match is found, then the super class and it's interfaces are tried recursively.
* @param forClass The class
* @return a {@link Convertor} or null if none were found.
*/
protected Convertor getConvertor(Class forClass)
{
Class cls=forClass;
Convertor convertor=(Convertor)_convertors.get(cls.getName());
if (convertor==null && this!=__default)
convertor=__default.getConvertor(cls);
while (convertor==null&&cls!=null&&cls!=Object.class)
{
Class[] ifs=cls.getInterfaces();
int i=0;
while (convertor==null&&ifs!=null&&i1)
{
switch (c)
{
case '*':
comment_state=3;
break;
case '/':
if (comment_state==3)
{
comment_state=0;
if (strip_state==2)
return o;
}
else
comment_state=2;
break;
default:
comment_state=2;
}
}
// handle // comment
else if (comment_state<0)
{
switch (c)
{
case '\r':
case '\n':
comment_state=0;
default:
break;
}
}
// handle unknown
else
{
if (!Character.isWhitespace(c))
{
if (c=='/')
comment_state=1;
else if (c=='*')
comment_state=3;
else if (o==null)
{
o=parse(source);
continue;
}
}
}
source.next();
}
return o;
}
public Object parse(Source source)
{
int comment_state=0; // 0=no comment, 1="/", 2="/*", 3="/* *" -1="//"
while (source.hasNext())
{
char c=source.peek();
// handle // or /* comment
if (comment_state==1)
{
switch (c)
{
case '/':
comment_state=-1;
break;
case '*':
comment_state=2;
}
}
// handle /* */ comment
else if (comment_state>1)
{
switch (c)
{
case '*':
comment_state=3;
break;
case '/':
if (comment_state==3)
comment_state=0;
else
comment_state=2;
break;
default:
comment_state=2;
}
}
// handle // comment
else if (comment_state<0)
{
switch (c)
{
case '\r':
case '\n':
comment_state=0;
break;
default:
break;
}
}
// handle unknown
else
{
switch (c)
{
case '{':
return parseObject(source);
case '[':
return parseArray(source);
case '"':
return parseString(source);
case '-':
return parseNumber(source);
case 'n':
complete("null",source);
return null;
case 't':
complete("true",source);
return Boolean.TRUE;
case 'f':
complete("false",source);
return Boolean.FALSE;
case 'u':
complete("undefined",source);
return null;
case '/':
comment_state=1;
break;
default:
if (Character.isDigit(c))
return parseNumber(source);
else if (Character.isWhitespace(c))
break;
return handleUnknown(source, c);
}
}
source.next();
}
return null;
}
protected Object handleUnknown(Source source, char c)
{
throw new IllegalStateException("unknown char '"+c+"'("+(int)c+") in "+source);
}
protected Object parseObject(Source source)
{
if (source.next()!='{')
throw new IllegalStateException();
Map map=newMap();
char next=seekTo("\"}",source);
while (source.hasNext())
{
if (next=='}')
{
source.next();
break;
}
String name=parseString(source);
seekTo(':',source);
source.next();
Object value=contextFor(name).parse(source);
map.put(name,value);
seekTo(",}",source);
next=source.next();
if (next=='}')
break;
else
next=seekTo("\"}",source);
}
String classname=(String)map.get("class");
if (classname!=null)
{
try
{
Class c=Loader.loadClass(JSON.class,classname);
return convertTo(c,map);
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
}
return map;
}
protected Object parseArray(Source source)
{
if (source.next()!='[')
throw new IllegalStateException();
int size=0;
ArrayList list=null;
Object item=null;
boolean coma=true;
while (source.hasNext())
{
char c=source.peek();
switch (c)
{
case ']':
source.next();
switch(size)
{
case 0:
return newArray(0);
case 1:
Object array = newArray(1);
Array.set(array,0,item);
return array;
default:
return list.toArray(newArray(list.size()));
}
case ',':
if (coma)
throw new IllegalStateException();
coma=true;
source.next();
break;
default:
if (Character.isWhitespace(c))
source.next();
else
{
coma=false;
if (size++==0)
item=contextForArray().parse(source);
else if (list==null)
{
list=new ArrayList();
list.add(item);
item=contextForArray().parse(source);
list.add(item);
item=null;
}
else
{
item=contextForArray().parse(source);
list.add(item);
item=null;
}
}
}
}
throw new IllegalStateException("unexpected end of array");
}
protected String parseString(Source source)
{
if (source.next()!='"')
throw new IllegalStateException();
boolean escape=false;
StringBuffer b=null;
final char[] scratch=source.scratchBuffer();
if (scratch!=null)
{
int i=0;
while (source.hasNext())
{
if(i>=scratch.length)
{
// we have filled the scratch buffer, so we must
// use the StringBuffer for a large string
b=new StringBuffer(scratch.length*2);
b.append(scratch,0,i);
break;
}
char c=source.next();
if (escape)
{
escape=false;
switch (c)
{
case '"':
scratch[i++]='"';
break;
case '\\':
scratch[i++]='\\';
break;
case '/':
scratch[i++]='/';
break;
case 'b':
scratch[i++]='\b';
break;
case 'f':
scratch[i++]='\f';
break;
case 'n':
scratch[i++]='\n';
break;
case 'r':
scratch[i++]='\r';
break;
case 't':
scratch[i++]='\t';
break;
case 'u':
char uc=(char)((TypeUtil.convertHexDigit((byte)source.next())<<12)+
(TypeUtil.convertHexDigit((byte)source.next())<<8)+
(TypeUtil.convertHexDigit((byte)source.next())<<4)+
(TypeUtil.convertHexDigit((byte)source.next())));
scratch[i++]=uc;
break;
default:
scratch[i++]=c;
}
}
else if (c=='\\')
{
escape=true;
continue;
}
else if (c=='\"')
{
// Return string that fits within scratch buffer
return toString(scratch,0,i);
}
else
scratch[i++]=c;
}
// Missing end quote, but return string anyway ?
if (b==null)
return toString(scratch,0,i);
}
else
b=new StringBuffer(getStringBufferSize());
// parse large string into string buffer
synchronized (b)
{
while (source.hasNext())
{
char c=source.next();
if (escape)
{
escape=false;
switch (c)
{
case '"':
b.append('"');
break;
case '\\':
b.append('\\');
break;
case '/':
b.append('/');
break;
case 'b':
b.append('\b');
break;
case 'f':
b.append('\f');
break;
case 'n':
b.append('\n');
break;
case 'r':
b.append('\r');
break;
case 't':
b.append('\t');
break;
case 'u':
char uc=(char)((TypeUtil.convertHexDigit((byte)source.next())<<12)+
(TypeUtil.convertHexDigit((byte)source.next())<<8)+
(TypeUtil.convertHexDigit((byte)source.next())<<4)+
(TypeUtil.convertHexDigit((byte)source.next())));
b.append(uc);
break;
default:
b.append(c);
}
}
else if (c=='\\')
{
escape=true;
continue;
}
else if (c=='\"')
break;
else
b.append(c);
}
return b.toString();
}
}
public Number parseNumber(Source source)
{
boolean minus=false;
long number=0;
StringBuffer buffer=null;
longLoop: while (source.hasNext())
{
char c=source.peek();
switch (c)
{
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
number=number*10+(c-'0');
source.next();
break;
case '-':
case '+':
if (number!=0)
throw new IllegalStateException("bad number");
minus=true;
source.next();
break;
case '.':
case 'e':
case 'E':
buffer=new StringBuffer(16);
if(minus)
buffer.append('-');
buffer.append(number);
buffer.append(c);
source.next();
break longLoop;
default:
break longLoop;
}
}
if (buffer==null)
return TypeUtil.newLong(minus?-1*number:number);
synchronized (buffer)
{
doubleLoop: while (source.hasNext())
{
char c=source.peek();
switch (c)
{
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '-':
case '.':
case '+':
case 'e':
case 'E':
buffer.append(c);
source.next();
break;
default:
break doubleLoop;
}
}
return new Double(buffer.toString());
}
}
protected void seekTo(char seek, Source source)
{
while (source.hasNext())
{
char c=source.peek();
if (c==seek)
return;
if (!Character.isWhitespace(c))
throw new IllegalStateException("Unexpected '"+c+" while seeking '"+seek+"'");
source.next();
}
throw new IllegalStateException("Expected '"+seek+"'");
}
protected char seekTo(String seek, Source source)
{
while (source.hasNext())
{
char c=source.peek();
if (seek.indexOf(c)>=0)
{
return c;
}
if (!Character.isWhitespace(c))
throw new IllegalStateException("Unexpected '"+c+"' while seeking one of '"+seek+"'");
source.next();
}
throw new IllegalStateException("Expected one of '"+seek+"'");
}
protected static void complete(String seek, Source source)
{
int i=0;
while (source.hasNext()&&i
* A JSON.Convertible object may be written to a JSONObject
* or initialized from a Map of field names to values.
*
* If the JSON is to be convertible back to an Object, then
* the method {@link Output#addClass(Class)} must be called from within toJSON()
*
*/
public interface Convertible
{
public void toJSON(Output out);
public void fromJSON(Map object);
}
/* ------------------------------------------------------------ */
/** Static JSON Convertor.
*
* may be implemented to provide static convertors for objects that may be registered
* with {@link JSON#registerConvertor(Class, org.mortbay.util.ajax.JSON.Convertor).
* These convertors are looked up by class, interface and
* super class by {@link JSON#getConvertor(Class)}. Convertors should be used when the
* classes to be converted cannot implement {@link Convertible} or {@link Generator}.
*/
public interface Convertor
{
public void toJSON(Object obj, Output out);
public Object fromJSON(Map object);
}
/* ------------------------------------------------------------ */
/** JSON Generator.
* A class that can add it's JSON representation directly to a StringBuffer.
* This is useful for object instances that are frequently converted and wish to
* avoid multiple Conversions
*/
public interface Generator
{
public void addJSON(StringBuffer buffer);
}
/* ------------------------------------------------------------ */
/** A Literal JSON generator
* A utility instance of {@link JSON.Generator} that holds a pre-generated string on JSON text.
*/
public static class Literal implements Generator
{
private String _json;
/* ------------------------------------------------------------ */
/** Construct a literal JSON instance for use by {@link JSON#toString(Object)}.
* If {@link Log#isDebugEnabled()} is true, the JSON will be parsed to check validity
* @param json A literal JSON string.
*/
public Literal(String json)
{
if (Log.isDebugEnabled())
parse(json);
_json=json;
}
public String toString()
{
return _json;
}
public void addJSON(StringBuffer buffer)
{
buffer.append(_json);
}
}
}