/*
* Copyright (c) 2009-2010, 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 java.util.List;
import java.util.Map;
import org.json.simple.JSONArray;
import org.json.simple.JSONAware;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
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.
*
* 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
*
* <pre> var a = b.c[1].d[3];</pre>
*
* can be translated in Java as:
*
* <pre> String a = b.get("c").get(1).get("d").get(3).string();</pre>
*
* 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 implements JSONAware {
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) JSONize((JSONObject) o);
this.type = Type.OBJECT;
} else if (o instanceof JSONArray) {
this.array = (JSONArray) JSONize((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 if (o instanceof Boolean) {
this.bool = (Boolean) o;
this.type = Type.BOOLEAN;
} else {
throw new RuntimeException("don't how how to deal with this type of object: " + o);
}
}
// --------------------------------------------------
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);
Object ao = this.array.get(index);
return (JSON) ((ao instanceof JSON || ao == null) ? ao : new JSON(ao));
case OBJECT:
Object oo = this.obj.get(key);
return (JSON) ((oo instanceof JSON || oo == null) ? oo : new JSON(oo));
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:
Object ao = this.array.get(index);
return (JSON) ((ao instanceof JSON || ao == null) ? ao : new JSON(ao));
case OBJECT:
Object oo = this.obj.get(Integer.toString(index));
return (JSON) ((oo instanceof JSON || oo == null) ? oo : new JSON(oo));
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) {
if (key == null) throw new RuntimeException("Can't ask for a null 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);
}
}
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 JSONValue.toJSONString(this.bool);
case STRING:
return JSONValue.toJSONString(this.string);
case NUMBER:
return JSONValue.toJSONString(this.number);
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 String toJSONString() {
return toString();
}
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;
}
@SuppressWarnings("unchecked")
public Map object() {
if (this.type != Type.OBJECT) {
throw new RuntimeException("This is not an object, it's a " + this.type);
}
return this.obj;
}
@SuppressWarnings("unchecked")
public List array() {
if (this.type != Type.ARRAY) {
throw new RuntimeException("This is not an array, it's a " + this.type);
}
return this.array;
}
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 boolean equals(Object o) {
if (o instanceof JSON) {
o = ((JSON) o).value();
}
return this.value().equals(o);
}
// -----------------------------------------------------------------------------------
@SuppressWarnings("unchecked")
private Object JSONize(Object obj) {
if (obj instanceof JSONObject) {
JSONObject o = (JSONObject) obj;
for (Object key : o.keySet()) {
Object value = o.get(key);
if (value instanceof JSONObject || value instanceof JSONArray) {
o.put(key,new JSON(value));
}
}
} else if (obj instanceof JSONArray) {
JSONArray a = (JSONArray) obj;
for (int i = 0; i < a.size(); i++) {
Object value = a.get(i);
if (value instanceof JSONObject || value instanceof JSONArray) {
a.set(i, new JSON(value));
}
}
}
return obj;
}
// -----------------------------------------------------------------------------------
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 != null && !(o instanceof JSON)) {
o = new JSON(o);
}
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 != null && !(o instanceof JSON)) {
o = new JSON(o);
}
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 != null && !(v instanceof JSON)) {
v = new JSON(v);
}
this.obj.put(k.toString(), v);
return this;
}
public JSON put(Object o) {
return _(o);
}
public JSON add(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 = new JSON(o);
}
this.array.add(o);
return this;
}
public JSON del(Object o) {
if (this.type == Type.OBJECT) {
this.obj.remove(o);
} else if (this.type == Type.ARRAY) {
this.array.remove(new JSON(o));
} else {
throw new RuntimeException("you can remove stuff only from an object or an array");
}
return this;
}
}