All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.googlecode.fascinator.common.JsonSimple Maven / Gradle / Ivy

There is a newer version: 1.6
Show newest version
/*
 * The Fascinator - JSON Simple
 * Copyright (C) 2011 University of Southern Queensland
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package com.googlecode.fascinator.common;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrSubstitutor;
import org.json.simple.JSONArray;
import org.json.simple.parser.ContainerFactory;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

Introduction

*

* This class wraps objects and methods of the * JSON.simple library. *

* *

Purpose

* It provides the following functionality: *
    *
  • Basic retrieval methods for complicated paths / data structures.
  • *
  • Typed retrieval for String, Integer and Boolean data types.
  • *
  • Object retrieval from the JSON.simple API (JsonObject and JSONArray).
  • *
  • Static utility methods for common operations manipulating data structures.
  • *
  • The ability to use place holder string for runtime substitution with system properties.
  • *
* * @author Greg Pendlebury */ public class JsonSimple { /** Logging */ private static Logger log = LoggerFactory.getLogger(JsonSimple.class); /** Holds this object's JSON */ private JsonObject jsonObject; /** Hold the unmodified json object as parse(String) essentially throws away the JSONArray */ private JSONArray jsonArray; /** Flag for system property substitution */ private boolean substitueProperties; /** * Creates an empty JSON object * * @throws IOException if there was an error during creation */ public JsonSimple() { substitueProperties = true; jsonObject = new JsonObject(); } /** * Creates a JSON object from the specified file * * @param jsonFile a JSON file * @throws IOException if there was an error parsing or reading the file */ public JsonSimple(File jsonFile) throws IOException { substitueProperties = true; if (jsonFile == null) { jsonObject = new JsonObject(); } else { InputStream is = new FileInputStream(jsonFile); String json = IOUtils.toString(is, "UTF-8"); is.close(); parse(json); } } /** * Creates a JSON object from the specified input stream * * @param jsonIn a JSON stream * @throws IOException if there was an error parsing or reading the stream */ public JsonSimple(InputStream jsonIn) throws IOException { substitueProperties = true; if (jsonIn == null) { jsonObject = new JsonObject(); } else { // Stream the data into a string parse(IOUtils.toString(jsonIn, "UTF-8")); jsonIn.close(); } } /** * Creates a JSON object from the specified string * * @param jsonIn a JSON string * @throws IOException if there was an error parsing the string */ public JsonSimple(String jsonString) throws IOException { substitueProperties = true; if (jsonString == null) { jsonObject = new JsonObject(); } else { parse(jsonString); } } /** * Wrap a JsonObject in this class * * @param newJsonObject : The JsonObject to wrap */ public JsonSimple(JsonObject newJsonObject) { substitueProperties = true; if (newJsonObject == null) { newJsonObject = new JsonObject(); } jsonObject = newJsonObject; } /** * Parse the provided JSON * * @param jsonString a JSON string * @throws IOException if there was an error parsing the string */ private void parse(String jsonString) throws IOException { JSONParser parser = new JSONParser(); ContainerFactory containerFactory = new ContainerFactory() { @Override public List creatArrayContainer() { return new JSONArray(); } @Override public Map createObjectContainer() { return new JsonObject(); } }; // Parse the String Object object; try { object = parser.parse(jsonString, containerFactory); } catch(ParseException pe) { log.error("JSON Parse Error: ", pe); throw new IOException(pe); } // Take a look at what we have now if (object instanceof JsonObject) { jsonObject = (JsonObject) object; } else { if (object instanceof JSONArray) { // @TODO: the reasons as to why the original JSONArray is discarded has been lost in time - will need to determine this is by design jsonArray = (JSONArray) object; jsonObject = getFromArray((JSONArray) object); } else { log.error("Expected JsonObject or at least JSONArray, but" + " found neither. Please check JSON syntax: '{}'", jsonString); jsonObject = null; } } } /** *

* Set method for behaviour flag on substituting system properties during * string retrieval. *

* *

* If set to true (default) strings of the form "{$system.property}" * will be substituted for matching system properties during retrieval. *

* * @param newFlag : The new flag value to set for this behaviour */ public void setPropertySubstitution(boolean newFlag) { substitueProperties = newFlag; } /** * Find the first valid JsonObject in a JSONArray. * * @param array : The array to search * @return JsonObject : A JSON object * @throws IOException if there was an error */ private JsonObject getFromArray(JSONArray array) { if (array.isEmpty()) { log.warn("Found only empty array, starting new object"); return new JsonObject(); } // Grab the first element Object object = array.get(0); if (object == null) { log.warn("Null entry, starting new object"); return new JsonObject(); } // Nested array, go deeper if (object instanceof JSONArray) { return getFromArray((JSONArray) object); } return (JsonObject) object; } /** * Retrieve the given node from the provided object. * * @param path : An array of indeterminate length to use as the path * @return JsonObject : The JSON representation */ private Object getNode(Object object, Object path) { if (isArray(object)) { try { return ((JSONArray) object).get((Integer) path); } catch(ArrayIndexOutOfBoundsException ex) { return null; } } if (isObject(object)) { return ((JsonObject) object).get(path); } return null; } /** * Return the JsonObject holding this object's JSON representation * * @return JsonObject : The JSON representation */ public JsonObject getJsonObject() { return jsonObject; } /** * Walk down the JSON nodes specified by the path and retrieve the target * JSONArray. * * @param path : Variable length array of path segments * @return JSONArray : The target node, or NULL if path invalid or not an * array */ public JSONArray getArray(Object... path) { Object object = getPath(path); if (object instanceof JSONArray) { return (JSONArray) object; } return null; } /** * Walk down the JSON nodes specified by the path and retrieve the target * JsonObject. * * @param path : Variable length array of path segments * @return JsonObject : The target node, or NULL if path invalid or not an * object */ public JsonObject getObject(Object... path) { Object object = getPath(path); if (object instanceof JsonObject) { return (JsonObject) object; } return null; } /** * Walk down the JSON nodes specified by the path and retrieve the target. * * @param path : Variable length array of path segments * @return Object : The target node, or NULL if invalid */ public Object getPath(Object... path) { Object object = jsonObject; boolean valid = true; for (Object node : path) { if (isValidPath(object, node)) { object = getNode(object, node); } else { valid = false; break; } } if (valid) { return object; } return null; } /** * Retrieve the Boolean value on the given path. * * IMPORTANT: The default value only applies if the path is * not found. If a string on the path is found it will be considered * false unless the value is 'true' (ignoring case). This is the * default behaviour of the Boolean.parseBoolean() method. * * @param defaultValue : The fallback value to use if the path is * invalid or not found * @param path : An array of indeterminate length to use as the path * @return Boolean : The Boolean value found on the given path, or null if * no default provided */ public Boolean getBoolean(Boolean defaultValue, Object... path) { Object object = getPath(path); if (object == null) { return defaultValue; } if (isNumber(object)) { log.warn("getBoolean() : Integer value targeted. Expected Boolean"); return defaultValue; } if (object instanceof String) { return Boolean.parseBoolean((String) object); } if (object instanceof Boolean) { return (Boolean) object; } return null; } /** * Retrieve the Integer value on the given path. * * @param defaultValue : The fallback value to use if the path is * invalid or not found * @param path : An array of indeterminate length to use as the path * @return Integer : The Integer value found on the given path, or null if * no default provided */ public Integer getInteger(Integer defaultValue, Object... path) { Object object = getPath(path); if (object == null) { return defaultValue; } if (isNumber(object)) { return makeNumber(object); } if (object instanceof String) { try { return Integer.parseInt((String) object); } catch (NumberFormatException ex) { log.warn("getInteger() : String is not a parsable Integer '{}'", (String) object); return defaultValue; } } if (object instanceof Boolean) { log.warn("getInteger() : Boolean value targeted. Expected Integer"); return defaultValue; } return null; } /** * Retrieve the String value on the given path. * * @param defaultValue : The fallback value to use if the path is * invalid or not found * @param path : An array of indeterminate length to use as the path * @return String : The String value found on the given path, or null if * no default provided */ public String getString(String defaultValue, Object... path) { String response = null; Object object = getPath(path); if (object == null) { response = defaultValue; } else { } if (isNumber(object)) { response = Integer.toString(makeNumber(object)); } if (object instanceof String) { response = (String) object; } if (object instanceof Boolean) { response = Boolean.toString((Boolean) object); } if (object instanceof JSONArray) { jsonArray = (JSONArray)object; if (jsonArray.size() == 1) { Object value = jsonArray.get(0); if (value instanceof String) { response = (String)value; } } if (! (response instanceof String)) { log.warn("Unable to convert JSONArray: " + object + " to string."); } } // Are we substituting system properites? if (substitueProperties) { response = StrSubstitutor.replaceSystemProperties(response); } return response; } /** *

* Retrieve a list of Strings found on the given path. Note that this is a * utility function, and not designed for data traversal. It will * only retrieve Strings found on the provided node, and the node must be * a JSONArray. *

* * @param path : An array of indeterminate length to use as the path * @return List : A list of Strings, null if the node is not found */ public List getStringList(Object... path) { Object target = getPath(path); List response = new LinkedList(); if (isArray(target)) { if (substitueProperties) { List temp = JsonSimple.getStringList((JSONArray) target); for (String string : temp) { response.add(StrSubstitutor.replaceSystemProperties(string)); } return response; } else { return JsonSimple.getStringList((JSONArray) target); } } if (isString(target)) { // Are we substituting system properites? if (substitueProperties) { response.add(StrSubstitutor.replaceSystemProperties((String) target)); } else { response.add((String) target); } return response; } return null; } /** *

* Retrieve a list of JsonSimple objects found on the given path. Note that * this is a utility function, and not designed for data traversal. It * will only retrieve valid JsonObjects found on the provided node, * and wrap them in JsonSimple objects. *

* *

* Other objects found on that path will be ignored, and if the path itself * is not a JSONArray or not found, the function will return NULL. *

* * @param path : An array of indeterminate length to use as the path * @return List : A list of JSONSimple objects, or null */ public List getJsonSimpleList(Object... path) { JSONArray array = getArray(path); if (isArray(array)) { return JsonSimple.toJavaList(array); } return null; } /** *

* Retrieve a map of JsonSimple objects found on the given path. Note that * this is a utility function, and not designed for data traversal. It * will only retrieve valid JsonObjects found on the provided node, * and wrap them in JsonSimple objects. *

* *

* Other objects found on that path will be ignored, and if the path itself * is not a JsonObject or not found, the function will return NULL. *

* * @param path : An array of indeterminate length to use as the path * @return Map : A map of JSONSimple objects, or null */ public Map getJsonSimpleMap(Object... path) { JsonObject object = getObject(path); if (isObject(object)) { return JsonSimple.toJavaMap(object); } return null; } /** *

* Search through the JSON for any nodes (at any depth) matching the * requested name and return them. The returned List will be of type * Object and require type interrogation for detailed use, but will be * implemented as a LinkedList to preserve order. *

* * @param node : The node name we are looking for * @return List : A list of matching Objects from the data */ public List search(String node) { List response = new LinkedList(); for (Object key : jsonObject.keySet()) { Object value = jsonObject.get(key); // Check this node if (node.equals((String) key)) { response.add(value); } // Check Object children if (isObject(value)) { JsonSimple child = new JsonSimple((JsonObject) value); response.addAll(child.search(node)); } // Check Object grand-children if hidden in arrays if (isArray(value)) { List grandChildren = JsonSimple.toJavaList((JSONArray) value); for (JsonSimple grandChild : grandChildren) { response.addAll(grandChild.search(node)); } } } return response; } /** *

* Walk down the JSON nodes specified by the path and retrieve the target * array, writing each node that doesn't exist along the way. *

* *

* Note, when addressing path segments that are array indices you can use * '-1' to indicate that the next object should always be created fresh * and appended to the array. *

* *

* Array indices that are standard (ie. not -1) integers will be read as * normal, but return a null value from the method if they are not found. * This method will not attempt to write to a non-existing index of an * array. *

* * @param path : Variable length array of path segments * @return JSONArray : The target node, or NULL if path is invalid */ public JSONArray writeArray(Object... path) { Object response = writePath(new JSONArray(), path); if (isArray(response)) { return (JSONArray) response; } // The node already exists and it is not an array return null; } /** *

* Walk down the JSON nodes specified by the path and retrieve the target * object, writing each node that doesn't exist along the way. *

* *

* Note, when addressing path segments that are array indices you can use * '-1' to indicate that the next object should always be created fresh * and appended to the array. *

* *

* Array indices that are standard (ie. not -1) integers will be read as * normal, but return a null value from the method if they are not found. * This method will not attempt to write to a non-existing index of an * array. *

* * @param path : Variable length array of path segments * @return Object : The target node, or NULL if path is invalid */ public JsonObject writeObject(Object... path) { Object response = writePath(new JsonObject(), path); if (isObject(response)) { return (JsonObject) response; } // The node already exists and it is not a JsonObject return null; } /** * Walk down the JSON nodes specified by the path and retrieve the target, * writing each node that doesn't exist along the way. * * @param defaultNode : The Object to place at the end of the path if it * doesn't exist * @param path : Variable length array of path segments * @return Object : The target node, or NULL if path is invalid */ private Object writePath(Object defaultNode, Object... path) { Object object = jsonObject; Object child = null; for (int i = 0; i < path.length; i++) { Object node = path[i]; // Make sure the path provided is valid to address this node if (isValidPath(object, node)) { child = getNode(object, node); // What to do about a non-existant node if (child == null) { // Check the next node first int j = i + 1; if (j >= path.length) { // End of the path, use the default value from param child = writeNode(object, defaultNode, node); } else { // We aren't at the end of the path, what is // the next node expecting to find? Object nextNode = path[j]; if (isString(nextNode)) { // An object child = writeNode(object, new JsonObject(), node); } else { if (isNumber(nextNode)) { // An array child = writeNode( object, new JSONArray(), node); } else { // Neither... that path won't work return null; } } } } // Continue the loop object = child; } else { return null; } } return object; } /** * Retrieve the specified node for writing. Will create if necessary. * * @param parent : The object to get the node from (or write to) * @param newChild : The new node to write if none is found there * @param nodeName : The node name * @return Object : The node found at that path, or provided child. * Null if path is invalid */ private Object writeNode(Object parent, Object newChild, Object nodeName) { if (nodeName == null || parent == null) { return null; } if (!isValidPath(parent, nodeName)) { return null; } // Can we just return a valid node? Object oldChild = getNode(parent, nodeName); if (oldChild != null) { return oldChild; } // Are we writing into an array? if (isArray(parent)) { // We will only append to arrays if (-1 == (Integer) nodeName) { JSONArray array = (JSONArray) parent; array.add(newChild); return newChild; } } // Or an object? if (isObject(parent)) { JsonObject object = (JsonObject) parent; object.put(nodeName, newChild); return newChild; } // Something is wrong with the data types of the path if we got here return null; } /*=============================================== * * A series of small wrappers for type testing. * * ============================================== */ // JSONArray private boolean isArray(Object object) { return (object instanceof JSONArray); } // Integer or Long private boolean isNumber(Object object) { return (object instanceof Integer || object instanceof Long); } // JsonObject private boolean isObject(Object object) { return (object instanceof JsonObject); } // String private boolean isString(Object object) { return (object instanceof String); } // Test that path is valid for the object private boolean isValidPath(Object object, Object path) { if (isArray(object) && path instanceof Integer) { return true; } if (isObject(object) && path instanceof String) { return true; } return false; } // Integer or Long private Integer makeNumber(Object object) { if (object instanceof Integer) { return (Integer) object; } if (object instanceof Long) { return Integer.parseInt(Long.toString((Long) object)); } return null; } private String printNode(String label, Object data, String prefix) { // Prefix String tab = " "; // Label String labelString = ""; if (label != null) { labelString = "\"" + label + "\": "; } // Data - Arrays String dataString = null; if (data instanceof JSONArray) { List lines = new LinkedList(); JSONArray array = (JSONArray) data; for (Object entry : array) { lines.add(printNode(null, entry, prefix + tab)); } String output = StringUtils.join(lines, ",\n"); dataString = "[\n" + output + "\n" + prefix + "]"; } // Data - Objects if (data instanceof JsonObject) { List lines = new LinkedList(); JsonObject object = (JsonObject) data; for (Object key : object.keySet()) { lines.add(printNode( (String) key, object.get(key), prefix + tab)); } String output = StringUtils.join(lines, ",\n"); dataString = "{\n" + output + "\n" + prefix + "}"; } // Data - Boolean if (data instanceof Boolean) { dataString = ((Boolean) data).toString(); } // Data - Numbers if (data instanceof Long) { dataString = ((Long) data).toString(); } // Data - Everything else... Strings if (dataString == null) { if (data == null) { dataString = "null"; } else { // Escape quotes and slashes String value = data.toString(); value = value.replace("\\", "\\\\"); value = value.replace("\"", "\\\""); dataString = "\"" + value + "\""; } } return prefix + labelString + dataString; } /** * Return the String representation of this object's JSON * * @return String : The JSON String */ @Override public String toString() { return toString(false); } /** * Return the String representation of this object's JSON, optionally * formatted for human readability. * * @return String : The JSON String */ public String toString(boolean pretty) { // Simple, just a plain old string if (!pretty) { return jsonObject.toJSONString(); } // More complicated return printNode(null, getJsonObject(), ""); } /* =============================================== * Below can be found a selection of static * utilities for common JSON manipulation * =============================================== */ /** * Get a list of strings found on the specified node * * @param json object to retrieve from * @param field The field that has the list * @return List The resulting list */ public static List getStringList(JsonObject json, String field) { List response = new LinkedList(); Object object = json.get(field); if (object instanceof JSONArray) { return getStringList((JSONArray) object); } return response; } /** * Get a list of strings from the provided JSONArray * * @param json Array to retrieve strings from * @return List The resulting list */ public static List getStringList(JSONArray json) { List response = new LinkedList(); for (Object obj : json) { if (obj instanceof String) { response.add((String) obj); } } return response; } /** *

* Take all of the JsonObjects found in a JSONArray, wrap them in * JsonSimple objects, then add to a Java list and return. *

* * All entries found that are not JsonObjects are ignored. * * @return String : The JSON String */ public static List toJavaList(JSONArray array) { List response = new LinkedList(); if (array != null && !array.isEmpty()) { for (Object object : array) { if (object != null && object instanceof JsonObject) { response.add(new JsonSimple((JsonObject) object)); } } } return response; } /** *

* Take all of the JsonObjects found in a JsonObject, wrap them in * JsonSimple objects, then add to a Java Map and return. *

* * All entries found that are not JsonObjects are ignored. * * @return String : The JSON String */ public static Map toJavaMap(JsonObject object) { Map response = new LinkedHashMap(); if (object != null && !object.isEmpty()) { for (Object key : object.keySet()) { Object child = object.get(key); if (child != null && child instanceof JsonObject) { response.put((String) key, new JsonSimple((JsonObject) child)); } } } return response; } /** *

* Take all of the JsonSimple objects in the given Map and return a Map * having replace them all with their base JsonObjects. *

* * Useful in when combining stacks of JSON objects. * * @return String : The JSON String */ public static Map fromJavaMap( Map from) { Map response = new LinkedHashMap(); if (from != null && !from.isEmpty()) { for (String key : from.keySet()) { response.put(key, from.get(key).getJsonObject()); } } return response; } public JSONArray getJsonArray() { return jsonArray; } }