| // |
| // ======================================================================== |
| // Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.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.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| |
| import org.eclipse.jetty.util.IO; |
| import org.eclipse.jetty.util.Loader; |
| import org.eclipse.jetty.util.QuotedStringTokenizer; |
| import org.eclipse.jetty.util.TypeUtil; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| |
| /** |
| * JSON Parser and Generator. |
| * <p /> |
| * This class provides some static methods to convert POJOs to and from JSON |
| * notation. The mapping from JSON to java is: |
| * |
| * <pre> |
| * object ==> Map |
| * array ==> Object[] |
| * number ==> Double or Long |
| * string ==> String |
| * null ==> null |
| * bool ==> Boolean |
| * </pre> |
| |
| * The java to JSON mapping is: |
| * |
| * <pre> |
| * String --> string |
| * Number --> number |
| * Map --> object |
| * List --> array |
| * Array --> array |
| * null --> null |
| * Boolean--> boolean |
| * Object --> string (dubious!) |
| * </pre> |
| * |
| * 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. |
| * <p /> |
| * 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(Appendable)} to generate the JSON. |
| * The class {@link JSON.Literal} may be used to hold pre-generated JSON object. |
| * <p /> |
| * The interface {@link JSON.Convertor} may be implemented to provide static |
| * converters for objects that may be registered with |
| * {@link #registerConvertor(Class, Convertor)}. |
| * These converters are looked up by class, interface and super class by |
| * {@link #getConvertor(Class)}. |
| * <p /> |
| * If a JSON object has a "class" field, then a java class for that name is |
| * loaded and the method {@link #convertTo(Class,Map)} is used to find a |
| * {@link JSON.Convertor} for that class. |
| * <p /> |
| * If a JSON object has a "x-class" field then a direct lookup for a |
| * {@link JSON.Convertor} for that class name is done (without loading the class). |
| */ |
| public class JSON |
| { |
| static final Logger LOG = Log.getLogger(JSON.class); |
| public final static JSON DEFAULT = new JSON(); |
| |
| private Map<String, Convertor> _convertors = new ConcurrentHashMap<String, Convertor>(); |
| private int _stringBufferSize = 1024; |
| |
| public JSON() |
| { |
| } |
| |
| /** |
| * @return the initial stringBuffer size to use when creating JSON strings |
| * (default 1024) |
| */ |
| public int getStringBufferSize() |
| { |
| return _stringBufferSize; |
| } |
| |
| /** |
| * @param stringBufferSize |
| * the initial stringBuffer size to use when creating JSON |
| * strings (default 1024) |
| */ |
| 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; |
| } |
| |
| @Deprecated |
| public static void setDefault(JSON json) |
| { |
| } |
| |
| public static String toString(Object object) |
| { |
| StringBuilder buffer = new StringBuilder(DEFAULT.getStringBufferSize()); |
| DEFAULT.append(buffer,object); |
| return buffer.toString(); |
| } |
| |
| public static String toString(Map object) |
| { |
| StringBuilder buffer = new StringBuilder(DEFAULT.getStringBufferSize()); |
| DEFAULT.appendMap(buffer,object); |
| return buffer.toString(); |
| } |
| |
| public static String toString(Object[] array) |
| { |
| StringBuilder buffer = new StringBuilder(DEFAULT.getStringBufferSize()); |
| 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 in |
| * Reader 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. |
| */ |
| @Deprecated |
| public static Object parse(InputStream in) throws IOException |
| { |
| return DEFAULT.parse(new StringSource(IO.toString(in)),false); |
| } |
| |
| /** |
| * @deprecated use {@link #parse(Reader, boolean)} |
| * @param in |
| * 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. |
| */ |
| @Deprecated |
| 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) |
| { |
| StringBuilder buffer = new StringBuilder(getStringBufferSize()); |
| 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); |
| } |
| |
| @Deprecated |
| public void append(StringBuffer buffer, Object object) |
| { |
| append((Appendable)buffer,object); |
| } |
| |
| /** |
| * Append object as JSON to string buffer. |
| * |
| * @param buffer |
| * the buffer to append to |
| * @param object |
| * the object to append |
| */ |
| public void append(Appendable buffer, Object object) |
| { |
| try |
| { |
| if (object == null) |
| { |
| buffer.append("null"); |
| } |
| // Most likely first |
| else if (object instanceof Map) |
| { |
| appendMap(buffer,(Map)object); |
| } |
| else if (object instanceof String) |
| { |
| appendString(buffer,(String)object); |
| } |
| else if (object instanceof Number) |
| { |
| appendNumber(buffer,(Number)object); |
| } |
| else if (object instanceof Boolean) |
| { |
| appendBoolean(buffer,(Boolean)object); |
| } |
| else if (object.getClass().isArray()) |
| { |
| appendArray(buffer,object); |
| } |
| else if (object instanceof Character) |
| { |
| appendString(buffer,object.toString()); |
| } |
| else if (object instanceof Convertible) |
| { |
| appendJSON(buffer,(Convertible)object); |
| } |
| else if (object instanceof Generator) |
| { |
| appendJSON(buffer,(Generator)object); |
| } |
| else |
| { |
| // Check Convertor before Collection to support JSONCollectionConvertor |
| Convertor convertor = getConvertor(object.getClass()); |
| if (convertor != null) |
| { |
| appendJSON(buffer,convertor,object); |
| } |
| else if (object instanceof Collection) |
| { |
| appendArray(buffer,(Collection)object); |
| } |
| else |
| { |
| appendString(buffer,object.toString()); |
| } |
| } |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Deprecated |
| public void appendNull(StringBuffer buffer) |
| { |
| appendNull((Appendable)buffer); |
| } |
| |
| public void appendNull(Appendable buffer) |
| { |
| try |
| { |
| buffer.append("null"); |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Deprecated |
| public void appendJSON(final StringBuffer buffer, final Convertor convertor, final Object object) |
| { |
| appendJSON((Appendable)buffer,convertor,object); |
| } |
| |
| public void appendJSON(final Appendable 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); |
| } |
| }); |
| } |
| |
| @Deprecated |
| public void appendJSON(final StringBuffer buffer, Convertible converter) |
| { |
| appendJSON((Appendable)buffer,converter); |
| } |
| |
| public void appendJSON(final Appendable buffer, Convertible converter) |
| { |
| ConvertableOutput out=new ConvertableOutput(buffer); |
| converter.toJSON(out); |
| out.complete(); |
| } |
| |
| @Deprecated |
| public void appendJSON(StringBuffer buffer, Generator generator) |
| { |
| generator.addJSON(buffer); |
| } |
| |
| public void appendJSON(Appendable buffer, Generator generator) |
| { |
| generator.addJSON(buffer); |
| } |
| |
| @Deprecated |
| public void appendMap(StringBuffer buffer, Map<?,?> map) |
| { |
| appendMap((Appendable)buffer,map); |
| } |
| |
| public void appendMap(Appendable buffer, Map<?,?> map) |
| { |
| try |
| { |
| if (map == null) |
| { |
| appendNull(buffer); |
| return; |
| } |
| |
| buffer.append('{'); |
| Iterator<?> iter = map.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('}'); |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Deprecated |
| public void appendArray(StringBuffer buffer, Collection collection) |
| { |
| appendArray((Appendable)buffer,collection); |
| } |
| |
| public void appendArray(Appendable buffer, Collection collection) |
| { |
| try |
| { |
| 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(']'); |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Deprecated |
| public void appendArray(StringBuffer buffer, Object array) |
| { |
| appendArray((Appendable)buffer,array); |
| } |
| |
| public void appendArray(Appendable buffer, Object array) |
| { |
| try |
| { |
| if (array == null) |
| { |
| appendNull(buffer); |
| return; |
| } |
| |
| buffer.append('['); |
| int length = Array.getLength(array); |
| |
| for (int i = 0; i < length; i++) |
| { |
| if (i != 0) |
| buffer.append(','); |
| append(buffer,Array.get(array,i)); |
| } |
| |
| buffer.append(']'); |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Deprecated |
| public void appendBoolean(StringBuffer buffer, Boolean b) |
| { |
| appendBoolean((Appendable)buffer,b); |
| } |
| |
| public void appendBoolean(Appendable buffer, Boolean b) |
| { |
| try |
| { |
| if (b == null) |
| { |
| appendNull(buffer); |
| return; |
| } |
| buffer.append(b?"true":"false"); |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Deprecated |
| public void appendNumber(StringBuffer buffer, Number number) |
| { |
| appendNumber((Appendable)buffer,number); |
| } |
| |
| public void appendNumber(Appendable buffer, Number number) |
| { |
| try |
| { |
| if (number == null) |
| { |
| appendNull(buffer); |
| return; |
| } |
| buffer.append(String.valueOf(number)); |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| @Deprecated |
| public void appendString(StringBuffer buffer, String string) |
| { |
| appendString((Appendable)buffer,string); |
| } |
| |
| public void appendString(Appendable buffer, String string) |
| { |
| if (string == null) |
| { |
| appendNull(buffer); |
| return; |
| } |
| |
| QuotedStringTokenizer.quote(buffer,string); |
| } |
| |
| // Parsing utilities |
| |
| protected String toString(char[] buffer, int offset, int length) |
| { |
| return new String(buffer,offset,length); |
| } |
| |
| protected Map<String, Object> newMap() |
| { |
| return new HashMap<String, Object>(); |
| } |
| |
| protected Object[] newArray(int size) |
| { |
| return new Object[size]; |
| } |
| |
| protected JSON contextForArray() |
| { |
| return this; |
| } |
| |
| protected JSON contextFor(String field) |
| { |
| return this; |
| } |
| |
| protected Object convertTo(Class type, Map map) |
| { |
| if (type != null && Convertible.class.isAssignableFrom(type)) |
| { |
| try |
| { |
| Convertible conv = (Convertible)type.newInstance(); |
| conv.fromJSON(map); |
| return conv; |
| } |
| catch (Exception e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| Convertor convertor = getConvertor(type); |
| if (convertor != null) |
| { |
| return convertor.fromJSON(map); |
| } |
| return map; |
| } |
| |
| /** |
| * 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 void addConvertor(Class forClass, Convertor convertor) |
| { |
| _convertors.put(forClass.getName(),convertor); |
| } |
| |
| /** |
| * Lookup a convertor for a class. |
| * <p> |
| * 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 JSON.Convertor} or null if none were found. |
| */ |
| protected Convertor getConvertor(Class forClass) |
| { |
| Class cls = forClass; |
| Convertor convertor = _convertors.get(cls.getName()); |
| if (convertor == null && this != DEFAULT) |
| convertor = DEFAULT.getConvertor(cls); |
| |
| while (convertor == null && cls != Object.class) |
| { |
| Class[] ifs = cls.getInterfaces(); |
| int i = 0; |
| while (convertor == null && ifs != null && i < ifs.length) |
| convertor = _convertors.get(ifs[i++].getName()); |
| if (convertor == null) |
| { |
| cls = cls.getSuperclass(); |
| convertor = _convertors.get(cls.getName()); |
| } |
| } |
| return convertor; |
| } |
| |
| /** |
| * Register a {@link JSON.Convertor} for a named class or interface. |
| * |
| * @param name |
| * name of a class or an interface that the convertor applies to |
| * @param convertor |
| * the convertor |
| */ |
| public void addConvertorFor(String name, Convertor convertor) |
| { |
| _convertors.put(name,convertor); |
| } |
| |
| /** |
| * Lookup a convertor for a named class. |
| * |
| * @param name |
| * name of the class |
| * @return a {@link JSON.Convertor} or null if none were found. |
| */ |
| public Convertor getConvertorFor(String name) |
| { |
| Convertor convertor = _convertors.get(name); |
| if (convertor == null && this != DEFAULT) |
| convertor = DEFAULT.getConvertorFor(name); |
| return convertor; |
| } |
| |
| public Object parse(Source source, boolean stripOuterComment) |
| { |
| int comment_state = 0; // 0=no comment, 1="/", 2="/*", 3="/* *" -1="//" |
| if (!stripOuterComment) |
| return parse(source); |
| |
| int strip_state = 1; // 0=no strip, 1=wait for /*, 2= wait for */ |
| |
| Object o = null; |
| 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; |
| if (strip_state == 1) |
| { |
| comment_state = 0; |
| strip_state = 2; |
| } |
| } |
| } |
| // handle /* */ comment |
| else if (comment_state > 1) |
| { |
| 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 'N': |
| complete("NaN",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<String, Object> 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 xclassname = (String)map.get("x-class"); |
| if (xclassname != null) |
| { |
| Convertor c = getConvertorFor(xclassname); |
| if (c != null) |
| return c.fromJSON(map); |
| LOG.warn("No Convertor for x-class '{}'", xclassname); |
| } |
| |
| String classname = (String)map.get("class"); |
| if (classname != null) |
| { |
| try |
| { |
| Class c = Loader.loadClass(JSON.class,classname); |
| return convertTo(c,map); |
| } |
| catch (ClassNotFoundException e) |
| { |
| LOG.warn("No Class for '{}'", classname); |
| } |
| } |
| |
| 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; |
| |
| StringBuilder 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 StringBuilder(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; |
| } |
| 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 StringBuilder(getStringBufferSize()); |
| |
| // parse large string into string buffer |
| final StringBuilder builder=b; |
| while (source.hasNext()) |
| { |
| char c = source.next(); |
| |
| if (escape) |
| { |
| escape = false; |
| switch (c) |
| { |
| case '"': |
| builder.append('"'); |
| break; |
| case '\\': |
| builder.append('\\'); |
| break; |
| case '/': |
| builder.append('/'); |
| break; |
| case 'b': |
| builder.append('\b'); |
| break; |
| case 'f': |
| builder.append('\f'); |
| break; |
| case 'n': |
| builder.append('\n'); |
| break; |
| case 'r': |
| builder.append('\r'); |
| break; |
| case 't': |
| builder.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()))); |
| builder.append(uc); |
| break; |
| default: |
| builder.append(c); |
| } |
| } |
| else if (c == '\\') |
| { |
| escape = true; |
| } |
| else if (c == '\"') |
| { |
| break; |
| } |
| else |
| { |
| builder.append(c); |
| } |
| } |
| return builder.toString(); |
| } |
| |
| public Number parseNumber(Source source) |
| { |
| boolean minus = false; |
| long number = 0; |
| StringBuilder 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 StringBuilder(16); |
| if (minus) |
| buffer.append('-'); |
| buffer.append(number); |
| buffer.append(c); |
| source.next(); |
| break longLoop; |
| |
| default: |
| break longLoop; |
| } |
| } |
| |
| if (buffer == null) |
| return minus ? -1 * number : number; |
| |
| 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 < seek.length()) |
| { |
| char c = source.next(); |
| if (c != seek.charAt(i++)) |
| throw new IllegalStateException("Unexpected '" + c + " while seeking \"" + seek + "\""); |
| } |
| |
| if (i < seek.length()) |
| throw new IllegalStateException("Expected \"" + seek + "\""); |
| } |
| |
| private final class ConvertableOutput implements Output |
| { |
| private final Appendable _buffer; |
| char c = '{'; |
| |
| private ConvertableOutput(Appendable buffer) |
| { |
| _buffer = buffer; |
| } |
| |
| public void complete() |
| { |
| try |
| { |
| if (c == '{') |
| _buffer.append("{}"); |
| else if (c != 0) |
| _buffer.append("}"); |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| public void add(Object obj) |
| { |
| if (c == 0) |
| throw new IllegalStateException(); |
| append(_buffer,obj); |
| c = 0; |
| } |
| |
| public void addClass(Class type) |
| { |
| try |
| { |
| if (c == 0) |
| throw new IllegalStateException(); |
| _buffer.append(c); |
| _buffer.append("\"class\":"); |
| append(_buffer,type.getName()); |
| c = ','; |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| public void add(String name, Object value) |
| { |
| try |
| { |
| if (c == 0) |
| throw new IllegalStateException(); |
| _buffer.append(c); |
| QuotedStringTokenizer.quote(_buffer,name); |
| _buffer.append(':'); |
| append(_buffer,value); |
| c = ','; |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| public void add(String name, double value) |
| { |
| try |
| { |
| if (c == 0) |
| throw new IllegalStateException(); |
| _buffer.append(c); |
| QuotedStringTokenizer.quote(_buffer,name); |
| _buffer.append(':'); |
| appendNumber(_buffer, value); |
| c = ','; |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| public void add(String name, long value) |
| { |
| try |
| { |
| if (c == 0) |
| throw new IllegalStateException(); |
| _buffer.append(c); |
| QuotedStringTokenizer.quote(_buffer,name); |
| _buffer.append(':'); |
| appendNumber(_buffer, value); |
| c = ','; |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| public void add(String name, boolean value) |
| { |
| try |
| { |
| if (c == 0) |
| throw new IllegalStateException(); |
| _buffer.append(c); |
| QuotedStringTokenizer.quote(_buffer,name); |
| _buffer.append(':'); |
| appendBoolean(_buffer,value?Boolean.TRUE:Boolean.FALSE); |
| c = ','; |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| |
| public interface Source |
| { |
| boolean hasNext(); |
| |
| char next(); |
| |
| char peek(); |
| |
| char[] scratchBuffer(); |
| } |
| |
| public static class StringSource implements Source |
| { |
| private final String string; |
| private int index; |
| private char[] scratch; |
| |
| public StringSource(String s) |
| { |
| string = s; |
| } |
| |
| public boolean hasNext() |
| { |
| if (index < string.length()) |
| return true; |
| scratch = null; |
| return false; |
| } |
| |
| public char next() |
| { |
| return string.charAt(index++); |
| } |
| |
| public char peek() |
| { |
| return string.charAt(index); |
| } |
| |
| @Override |
| public String toString() |
| { |
| return string.substring(0,index) + "|||" + string.substring(index); |
| } |
| |
| public char[] scratchBuffer() |
| { |
| if (scratch == null) |
| scratch = new char[string.length()]; |
| return scratch; |
| } |
| } |
| |
| public static class ReaderSource implements Source |
| { |
| private Reader _reader; |
| private int _next = -1; |
| private char[] scratch; |
| |
| public ReaderSource(Reader r) |
| { |
| _reader = r; |
| } |
| |
| public void setReader(Reader reader) |
| { |
| _reader = reader; |
| _next = -1; |
| } |
| |
| public boolean hasNext() |
| { |
| getNext(); |
| if (_next < 0) |
| { |
| scratch = null; |
| return false; |
| } |
| return true; |
| } |
| |
| public char next() |
| { |
| getNext(); |
| char c = (char)_next; |
| _next = -1; |
| return c; |
| } |
| |
| public char peek() |
| { |
| getNext(); |
| return (char)_next; |
| } |
| |
| private void getNext() |
| { |
| if (_next < 0) |
| { |
| try |
| { |
| _next = _reader.read(); |
| } |
| catch (IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| |
| public char[] scratchBuffer() |
| { |
| if (scratch == null) |
| scratch = new char[1024]; |
| return scratch; |
| } |
| |
| } |
| |
| /** |
| * JSON Output class for use by {@link Convertible}. |
| */ |
| public interface Output |
| { |
| public void addClass(Class c); |
| |
| public void add(Object obj); |
| |
| public void add(String name, Object value); |
| |
| public void add(String name, double value); |
| |
| public void add(String name, long value); |
| |
| public void add(String name, boolean value); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** |
| * JSON Convertible object. Object can implement this interface in a similar |
| * way to the {@link Externalizable} interface is used to allow classes to |
| * provide their own serialization mechanism. |
| * <p> |
| * A JSON.Convertible object may be written to a JSONObject or initialized |
| * from a Map of field names to values. |
| * <p> |
| * 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. |
| * <p> |
| * may be implemented to provide static convertors for objects that may be |
| * registered with |
| * {@link JSON#registerConvertor(Class, org.eclipse.jetty.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(Appendable 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()) // TODO: Make this a configurable option on JSON instead! |
| parse(json); |
| _json = json; |
| } |
| |
| @Override |
| public String toString() |
| { |
| return _json; |
| } |
| |
| public void addJSON(Appendable buffer) |
| { |
| try |
| { |
| buffer.append(_json); |
| } |
| catch(IOException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| } |