/* * Copyright (c) 2009, Metaweb Technologies, Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * THIS SOFTWARE IS PROVIDED BY METAWEB TECHNOLOGIES AND CONTRIBUTORS * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL METAWEB * TECHNOLOGIES OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ package com.freebase.json; import java.io.IOException; import java.io.Reader; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; /** * This class is a telescopic (meaning, jQuery-like) wrapper for all * the various classes that can make up a JSON object. * <p/> * While using JSON in Javascript or Python comes natural, dealing with it * in Java is a pain, mostly because of casting issues. Having a single * wrapper object that encapsulates all those casting issues allows us * to cascade the method calls. So something that in javascript would be * <p/> * <pre> var a = b.c[1].d[3];</pre> * <p/> * can be translated in Java as: * <p/> * <pre> String a = b.get("c").get(1).get("d").get(3).string();</pre> * <p/> * which is a lot more verbose but still better than having to * deal with all the casting between Map and List and String by hand. */ public class JSON { public enum Type { OBJECT, ARRAY, STRING, NUMBER, BOOLEAN } private Type type; private JSONObject obj; private JSONArray array; private String string; private Number number; private boolean bool; // -------------------------------------------------- public static JSON parse(String s) throws ParseException { JSONParser parser = new JSONParser(); return new JSON(parser.parse(s)); } public static JSON parse(Reader r) throws IOException, ParseException { JSONParser parser = new JSONParser(); return new JSON(parser.parse(r)); } // -------------------------------------------------- public JSON(Object o) { if (o == null) { throw new RuntimeException("can't wrap a null object"); } else if (o instanceof JSONObject) { this.obj = (JSONObject) o; this.type = Type.OBJECT; } else if (o instanceof JSONArray) { this.array = (JSONArray) o; this.type = Type.ARRAY; } else if (o instanceof String) { this.string = (String) o; this.type = Type.STRING; } else if (o instanceof Number) { this.number = (Number) o; this.type = Type.NUMBER; } else { throw new RuntimeException("don't how how to deal with this type of object: " + o); } } public JSON(boolean bool) { this.bool = bool; this.type = Type.BOOLEAN; } // -------------------------------------------------- public JSON get(String key) { if (key == null) throw new RuntimeException("Can't ask for a null key"); switch (this.type) { case BOOLEAN: case STRING: case NUMBER: throw new RuntimeException("Only objects or arrays contain other values"); case ARRAY: int index = Integer.parseInt(key); return new JSON(this.array.get(index)); case OBJECT: return new JSON(this.obj.get(key)); default: // this should never happen but just in case throw new RuntimeException("Don't recognize this object type: " + this.type); } } public JSON get(int index) { switch (this.type) { case BOOLEAN: case STRING: case NUMBER: throw new RuntimeException("Only objects or arrays contain other values"); case ARRAY: return new JSON(this.array.get(index)); case OBJECT: return new JSON(this.obj.get(Integer.toString(index))); default: // this should never happen but just in case throw new RuntimeException("Don't recognize this object type: " + this.type); } } public boolean has(String key) { switch (this.type) { case BOOLEAN: case STRING: case NUMBER: return false; case ARRAY: int index = Integer.parseInt(key); return (index > 0 && index < this.array.size()); case OBJECT: return this.obj.containsKey(key); default: // this should never happen but just in case throw new RuntimeException("Don't recognize this object type: " + this.type); } } // NOTE: added by Mark Watson 3/3/2010: public int length() { switch (this.type) { case BOOLEAN: case STRING: case NUMBER: return 0; case ARRAY: return this.array.size(); case OBJECT: return 0; default: // this should never happen but just in case throw new RuntimeException("Don't recognize this object type: " + this.type); } } public boolean has(int index) { switch (this.type) { case BOOLEAN: case STRING: case NUMBER: return false; case ARRAY: return (index > 0 && index < this.array.size()); case OBJECT: return this.obj.containsKey(Integer.toString(index)); default: // this should never happen but just in case throw new RuntimeException("Don't recognize this object type: " + this.type); } } public String stringify() { return toString(); } public String toString() { switch (this.type) { case BOOLEAN: return Boolean.toString(this.bool); case STRING: return this.string; case NUMBER: return this.number.toString(); case ARRAY: return this.array.toJSONString(); case OBJECT: return this.obj.toJSONString(); default: // this should never happen but just in case throw new RuntimeException("Don't recognize this object type: " + this.type); } } public Object value() { switch (this.type) { case BOOLEAN: return this.bool; case STRING: return this.string; case NUMBER: return this.number; case ARRAY: return this.array; case OBJECT: return this.obj; default: // this should never happen but just in case throw new RuntimeException("Don't recognize this object type: " + this.type); } } public String string() { if (this.type != Type.STRING) { throw new RuntimeException("This is not a String, it's a " + this.type); } return this.string; } public Number number() { if (this.type != Type.NUMBER) { throw new RuntimeException("This is not a Number, it's a " + this.type); } return this.number; } public boolean bool() { if (this.type != Type.BOOLEAN) { throw new RuntimeException("This is not a Boolean, it's a " + this.type); } return this.bool; } public boolean isArray() { return (this.type == Type.ARRAY); } public boolean isObject() { return (this.type == Type.OBJECT); } public boolean isContainer() { return (this.type == Type.OBJECT || this.type == Type.ARRAY); } public Type type() { return this.type; } // ----------------------------------------------------------------------------------- public static JSON o() { return new JSON(new JSONObject()); } @SuppressWarnings("unchecked") public static JSON o(Object... objs) { JSONObject obj = new JSONObject(); Object k = null; for (Object o : objs) { if (k == null) { k = o; } else { if (o instanceof JSON) { o = ((JSON) o).value(); } obj.put(k.toString(), o); k = null; } } if (k != null) { throw new RuntimeException("Odd number of arguments, make sure you didn't forget something in your key/value pairs"); } return new JSON(obj); } public static JSON a() { return new JSON(new JSONArray()); } @SuppressWarnings("unchecked") public static JSON a(Object... objs) { JSONArray a = new JSONArray(); for (Object o : objs) { if (o instanceof JSON) { o = ((JSON) o).value(); } a.add(o); } return new JSON(a); } public JSON put(Object k, Object v) { return _(k, v); } @SuppressWarnings("unchecked") public JSON _(Object k, Object v) { if (this.type != Type.OBJECT) { throw new RuntimeException("can't add key/value pairs to non-objects types"); } if (v instanceof JSON) { v = ((JSON) v).value(); } this.obj.put(k.toString(), v); return this; } public JSON put(Object o) { return _(o); } @SuppressWarnings("unchecked") public JSON _(Object o) { if (this.type != Type.ARRAY) { throw new RuntimeException("can't add a single value an object type"); } if (o instanceof JSON) { o = ((JSON) o).value(); } this.array.add(o); return this; } public JSON del(Object o) { if (o instanceof JSON) { o = ((JSON) o).value(); } if (this.type == Type.OBJECT) { this.obj.remove(o); } else if (this.type == Type.ARRAY) { this.array.remove(o); } else { throw new RuntimeException("you can remove stuff only from an object or an array"); } return this; } }