com.cloudmine.api.rest.JsonUtilities.java Source code

Java tutorial

Introduction

Here is the source code for com.cloudmine.api.rest.JsonUtilities.java

Source

package com.cloudmine.api.rest;

import com.cloudmine.api.*;
import com.cloudmine.api.exceptions.ConversionException;
import com.cloudmine.api.persistance.CMJacksonModule;
import com.cloudmine.api.persistance.CMUserConstructorMixIn;
import com.cloudmine.api.persistance.ClassNameRegistry;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.MapType;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.*;

/**
 * Simplify working with JSON by putting all the utility methods in one place. Mostly focused on converting
 * objects to and from JSON
 * <br>Copyright CloudMine LLC. All rights reserved<br> See LICENSE file included with SDK for details.
 */
public class JsonUtilities {

    private static final Logger LOG = LoggerFactory.getLogger(JsonUtilities.class);
    private static final ObjectMapper jsonMapper = new ObjectMapper();

    public static final String EMPTY_JSON = "{ }";

    static {
        SimpleModule customModule = new CMJacksonModule();

        jsonMapper.registerModule(customModule);
        jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    public static final String NULL_STRING = "\"\"";

    public static final String TAB = "  ";
    public static final String CLASS_KEY = "__class__";
    public static final String TYPE_KEY = "__type__"; //type is used to identify objects that have special properties in CloudMine
    public static final String OBJECT_ID_KEY = "__id__";
    public static final String DATE_CLASS = "datetime";
    public static final String TIME_KEY = "timestamp";
    public static final String ENCODING = "UTF-8";

    /**
     * Convert a {@link Date} to an unwrapped CloudMine date object. Unwrapped means it is not surrounded by { }
     * @param date the Date to convert. If null, an empty string "" is returned
     * @return the date as JSON, or "" if given null
     */
    public static String convertDateToUnwrappedJsonClass(Date date) {
        if (date == null) {
            return NULL_STRING;
        }
        long secondsTime = date.getTime() / 1000;

        return new StringBuilder(createJsonProperty(CLASS_KEY, DATE_CLASS)).append(",\n")
                .append(createJsonProperty(TIME_KEY, secondsTime)).toString();

    }

    public static void addCMUserMixinsTo(Class klass) {
        if (JavaCMUser.class.isAssignableFrom(klass)) {
            jsonMapper.addMixInAnnotations(klass, CMUserConstructorMixIn.class);
        }
    }

    /**
     * Convert a {@link Date} to a CloudMine date object
     * @param date the Date to convert. If null, a wrapped empty string {\n""\n} is returned
     * @return the date as a JSON object, or {""} if given null
     */
    public static String convertDateToJsonClass(Date date) {
        String unwrappedDate = convertDateToUnwrappedJsonClass(date);
        return "{\n" + unwrappedDate + "\n}";
    }

    /**
     * Convert a key and value to its JSON representation
     * @param key the JSON key
     * @param value the JSON value
     * @return "key":"value"
     */
    public static String createJsonProperty(String key, String value) {
        return new StringBuilder(addQuotes(key)).append(":").append(addQuotes(value)).toString();
    }

    public static String createJsonProperty(String key, boolean value) {
        return new StringBuilder(addQuotes(key)).append(":").append(value).toString();
    }

    public static String createJsonPropertyToJson(String key, String value) {
        return new StringBuilder(addQuotes(key)).append(":").append(value).toString();
    }

    /**
     * Convert a key and value to oits JSON representation
     * @param key the JSON key
     * @param value the JSON value
     * @return "key":value
     */
    public static String createJsonProperty(String key, Number value) {
        return new StringBuilder(addQuotes(key)).append(":").append(value).toString();
    }

    /**
     * Remove the first "{" and last "}" from a JSON string
     * @param json a valid JSON string
     * @return if json == null, an empty string. if json does not contain an opening and closing brace, the passed in string. Otherwise,
     *              the passed in JSON with the first and last { and } removed
     */
    public static String unwrap(String json) {
        if (json == null) {
            return "";
        }
        int openBraces = json.indexOf("{");
        int closeBraces = json.lastIndexOf("}");
        if (openBraces < 0 || closeBraces < 0) {
            LOG.error("Given json to unwrap that does not have braces: " + json);
            return json;
        }
        String preOpen = json.substring(0, openBraces);
        String betweenBraces = json.substring(openBraces + 1, closeBraces);
        String postClose = json.substring(closeBraces + 1, json.length());
        String unwrappedJson = preOpen + //everything before the first open brace
                betweenBraces + //everything between the first place and the last closing brace
                postClose; //everything after the last closing brace
        return unwrappedJson;
    }

    /**
     * Add a leading and trailing "{" and "}" to the given string
     * @param json
     * @return
     */
    public static String wrap(String json) {
        if (json == null) {
            return EMPTY_JSON;
        }
        return new StringBuilder("{").append(json).append("}").toString();
    }

    /**
     * Quote a string
     * @param toQuote the value to quote
     * @return "toQuote"
     */
    public static String addQuotes(String toQuote) {
        if (toQuote == null) {
            return NULL_STRING;
        }
        return "\"" + toQuote + "\"";
    }

    public static String getIdentifierBody(String email, String userName) {
        StringBuilder jsonBuilder = new StringBuilder("{");
        String separator = "";
        if (Strings.isNotEmpty(email)) {
            jsonBuilder.append("\"email\": \"").append(email).append("\"");
            separator = ", ";
        }
        if (Strings.isNotEmpty(userName)) {
            jsonBuilder.append(separator).append("\"username\": \"").append(userName).append("\"");
        }
        return jsonBuilder.append("}").toString();
    }

    public static Transportable keyedJsonCollection(Collection<CMObject> cmObjects) {
        if (cmObjects == null || cmObjects.isEmpty())
            return new TransportableString(EMPTY_JSON);
        String[] unwrapped = new String[cmObjects.size()];
        int i = 0;
        for (CMObject object : cmObjects) {
            unwrapped[i] = object.asKeyedObject();
            i++;
        }
        return jsonCollection(unwrapped);
    }

    /**
     * Enclose all the passed in jsonEntities in a JSON collection
     * @param jsonEntities to put into the collection
     * @return { jsonEntities[0].transportableRepresentation, jsonEntities[1].transportableRepresentation, ...}
     */
    public static Transportable jsonCollection(Collection<? extends Transportable> jsonEntities) {
        return jsonCollection(jsonEntities.toArray(new Transportable[jsonEntities.size()]));
    }

    public static Transportable jsonCollection(List<String> jsonEntities) {
        return jsonCollection(jsonEntities.toArray(new String[jsonEntities.size()]));
    }

    /**
     * Enclose all the passed in strings in a JSON collection
     * @param jsonEntities JSON strings to put in the collection
     * @return { jsonEntities[0], jsonEntities[1], ...}
     */
    public static Transportable jsonStringsCollection(Collection<String> jsonEntities) {
        return jsonCollection(jsonEntities.toArray(new String[jsonEntities.size()]));
    }

    /**
     * Enclose all the passed in transportableEntities in a JSON collection
     * @param transportableEntities to put into the collection
     * @return { transportableEntities[0].transportableRepresentation, transportableEntities[1].transportableRepresentation, ...}
     */
    public static Transportable jsonCollection(Transportable... transportableEntities) {
        String[] jsonStrings = new String[transportableEntities.length];
        for (int i = 0; i < transportableEntities.length; i++) {
            jsonStrings[i] = transportableEntities[i].transportableRepresentation();
        }
        return jsonCollection(jsonStrings);
    }

    /**
     * Enclose all the passed in strings in a JSON collection
     * @param jsonEntities JSON strings to put in the collection
     * @return { jsonEntities[0], jsonEntities[1], ...}
     */
    public static Transportable jsonCollection(String... jsonEntities) {
        StringBuilder json = new StringBuilder("{\n");
        String comma = "";
        for (String jsonEntity : jsonEntities) {
            json.append(comma).append(TAB).append(jsonEntity);
            comma = ",\n";
        }
        json.append("\n}");
        return new TransportableString(json.toString());
    }

    /**
     * Convert a Map to its representation as a JSON string.
     * @param map will be converted to its JSON representation
     * @return valid JSON that represents the passed in map. It should be true that map.equals(jsonToMap(mapToJson(map)))
     * @throws ConversionException if unable to convert this Map to json. This should never happen
     */
    public static String mapToJson(Map<String, ? extends Object> map) throws ConversionException {
        if (map == null) {
            return EMPTY_JSON;
        }
        StringWriter writer = new StringWriter();
        try {
            jsonMapper.writeValue(writer, map);
            return writer.toString();
        } catch (IOException e) {
            LOG.error("Trouble writing json", e);
            throw new ConversionException(e);
        } finally {
            try {
                writer.close();
            } catch (IOException e) {
                //nope don't care
            }
        }
    }

    public static <CMOBJECT extends CMObject> String cmobjectsToJson(Collection<CMOBJECT> objects)
            throws ConversionException {
        if (objects == null || objects.size() == 0) {
            return EMPTY_JSON;
        }
        return cmobjectsToJson(objects.toArray(new CMObject[objects.size()]));
    }

    /**
     * Convert a CMObject to its JSON representation
     * @param objects the objects to convert
     * @return valid JSON that represents the passed in objects as a collection of JSON
     * @throws ConversionException if unable to convert this CMObject to json
     */
    public static String cmobjectsToJson(CMObject... objects) throws ConversionException {
        if (objects == null) {
            LOG.debug("Received null objects, returning empty json");
            return EMPTY_JSON;
        }
        Map<String, CMObject> objectMap = new HashMap<String, CMObject>();
        for (CMObject object : objects) {
            objectMap.put(object.getObjectId(), object);
        }

        StringWriter writer = new StringWriter();
        try {

            jsonMapper.writeValue(writer, objectMap);
            return writer.toString();
        } catch (IOException e) {
            LOG.error("Trouble writing json", e);
            throw new ConversionException(e);
        } finally {
            try {
                writer.close();
            } catch (IOException e) {
                //nope don't care
            }
        }
    }

    public static String cmObjectToJson(Object object) {
        try {
            return wrap(jsonMapper.writeValueAsString(object));
        } catch (JsonProcessingException e) {
            throw new ConversionException(e);
        }
    }

    public static String objectToJson(Object object) throws ConversionException {
        StringWriter writer = new StringWriter();
        try {
            jsonMapper.writeValue(writer, object);
            return writer.toString();
        } catch (IOException e) {
            LOG.error("Exception thrown", e);
            throw new ConversionException(e);
        }
    }

    public static void writeObjectToJson(Object object, OutputStream stream) {
        try {
            jsonMapper.writeValue(stream, object);
        } catch (IOException e) {
            LOG.error("Exception thrown", e);
            throw new ConversionException(e);
        }
    }

    /**
     * Convert the given JSON to the given klass. If unable to convert, throws ConversionException
     * @param json JSON representing
     * @param klass
     * @param <CMO>
     * @return
     * @throws ConversionException
     */
    public static <CMO> CMO jsonToClass(String json, Class<CMO> klass) throws ConversionException {
        try {
            CMO object = jsonMapper.readValue(json, klass);
            return object;
        } catch (IOException e) {
            LOG.error("Trouble reading json: \n" + json, e);
            throw new ConversionException("JSON: " + json, e);
        }
    }

    public static CMObject jsonToClass(String json) throws ConversionException {
        if (Strings.isEmpty(json)) {
            throw new ConversionException("Can't convert an empty or null json string");
        }
        Map<String, Object> jsonMap = jsonToMap(json); //this is a slow but easy way to get the klass name, might have to be replaced in the future
        Object klassString = jsonMap.get(CLASS_KEY);
        CMType type = CMType.getTypeById(Strings.asString(jsonMap.get(TYPE_KEY)));

        boolean isTyped = type != null && !CMType.NONE.equals(type) && klassString == null; //if we have a class string, use that instead of the specified type
        if (isTyped) {
            return jsonToClass(json, type.getTypeClass());
        }

        boolean isUnknownClass = klassString == null
                || ClassNameRegistry.isRegistered(klassString.toString()) == false;
        if (isUnknownClass) {
            return new SimpleCMObject(new TransportableString(json));
        }

        Class<? extends CMObject> klass = ClassNameRegistry.forName(klassString.toString());
        return jsonToClass(json, klass);
    }

    /**
     * Convert a Transportable entity to a Map representation
     * @param transportable valid JSON
     * @return If transportable is null, returns an empty Map. Otherwise, a Map whose keys are JSON Strings and whose values are JSON values
     * @throws ConversionException if unable to convert the given transportable to a map. Will happen if the transportableRepresentation call fails or if unable to represent the transportable as a map
     */
    public static Map<String, Object> jsonToMap(Transportable transportable) throws ConversionException {
        if (transportable == null)
            return new HashMap<String, Object>();
        return jsonToMap(transportable.transportableRepresentation());
    }

    /**
     * Convert a JSON string to a Map representation
     * @param json valid JSON
     * @return If json is null, returns an empty Map. Otherwise, a Map whose keys are JSON Strings and whose values are JSON values
     * @throws ConversionException if unable to convert the given json to a map. Will happen if the transportableRepresentation call fails or if unable to represent the json as a map
     */
    public static Map<String, Object> jsonToMap(String json) throws ConversionException {
        Map<String, Object> jsonMap = jsonToClassMap(json, Object.class);
        convertDateClassesToDates(jsonMap);
        return jsonMap;
    }

    /**
     * Convert a JSON collection in the form { "key":{values...}, "anotherKey":{moreValues} } to a Map of key's to
     * objects, of the given klass.
     * @param json the JSON to convert
     * @param klass the
     * @param <CMO>
     * @return
     * @throws ConversionException
     */
    public static <CMO> Map<String, CMO> jsonToClassMap(String json, Class<CMO> klass) throws ConversionException {
        try {
            MapType mapType = jsonMapper.getTypeFactory().constructMapType(Map.class, String.class, klass);
            Map<String, CMO> jsonMap = jsonMapper.readValue(json, mapType);
            return jsonMap;
        } catch (IOException e) {
            LOG.error("Trouble reading json", e);
            throw new ConversionException("JSON: " + json, e);
        }
    }

    public static Map<String, CMObject> jsonToClassMap(String json) {
        Map<String, String> simpleMap = jsonMapToKeyMap(json);
        Map<String, CMObject> objectMap = new LinkedHashMap<String, CMObject>();
        for (Map.Entry<String, String> entry : simpleMap.entrySet()) {
            String objectId = entry.getKey();
            CMObject cmObject = jsonToClass(entry.getValue());
            cmObject.setObjectId(objectId);
            objectMap.put(objectId, cmObject);
        }
        return objectMap;
    }

    public static <CMO extends CMObject> Map<String, CMO> jsonToCMObjectMap(String json, Class<CMO> klass) {

        Map<String, String> simpleMap = jsonMapToKeyMap(json);
        Map<String, CMO> objectMap = new LinkedHashMap<String, CMO>();
        for (Map.Entry<String, String> entry : simpleMap.entrySet()) {
            objectMap.put(entry.getKey(), jsonToClass(entry.getValue(), klass));
        }
        return objectMap;
    }

    /**
     * This method only works if all the values are objects, otherwise it breaks
     * @param json
     * @return
     */
    public static Map<String, String> jsonMapToKeyMap(String json) {
        //TODO this method is big and kinda gross
        //TODO Also its kind of broken on input that has top level keys to none json objects
        if (Strings.isEmpty(json)) {
            return new HashMap<String, String>();
        }
        try {
            StringReader reader = new StringReader(json);
            int readInt;
            int open = 0;
            boolean inString = false;
            boolean escapeNext = false;
            Map<String, String> jsonMap = new LinkedHashMap<String, String>();
            StringBuilder keyBuilder = new StringBuilder();
            StringBuilder contentsBuilder = new StringBuilder();

            while ((readInt = reader.read()) != -1) {
                char read = (char) readInt;
                switch (read) {
                case '{':
                    if (!inString)
                        open++;
                    break;
                case '}':
                    if (!inString) {
                        open--;
                        if (open == 1) { //we closed a full block
                            //finish off the recording
                            contentsBuilder.append(read);
                            //get the key
                            String key = keyBuilder.toString();
                            String[] splitKey = key.split("\"");
                            if (splitKey.length < 1) {
                                throw new ConversionException("Missing key at: " + key);
                            }
                            String parsedKey = splitKey[1];
                            //get the contents
                            String contents = contentsBuilder.toString();
                            jsonMap.put(parsedKey, contents);
                            //reset
                            keyBuilder = new StringBuilder();
                            contentsBuilder = new StringBuilder();
                            continue;
                        }
                    }
                    break;
                case '\"':
                    if (escapeNext) {
                        escapeNext = false;
                    } else {
                        inString = !inString;
                    }
                    break;
                case '\\':
                    escapeNext = true;
                    break;
                default:
                    escapeNext = false; //only escape the next character

                }
                if (open == 1) {
                    keyBuilder.append(read);
                } else if (open > 1) {
                    contentsBuilder.append(read);
                }
            }
            return jsonMap;
        } catch (IOException e) {
            LOG.error("Exception thrown", e);
            throw new ConversionException("Trouble reading JSON: " + e);
        }
    }

    public static void mergeJsonUpdates(CMObject objectToUpdate, String json) throws ConversionException {
        try {
            jsonMapper.readerForUpdating(objectToUpdate).readValue(json);
        } catch (IOException e) {
            LOG.error("Exception thrown while merging json update: " + json, e);
            throw new ConversionException(e);
        }
    }

    /**
     * Replaces any json datetime objects with dates. Modifies the passed in map
     * @param jsonMap
     * @return
     * @throws ConversionException
     */
    private static Object convertDateClassesToDates(Map<String, Object> jsonMap) throws ConversionException {
        if (jsonMap == null)
            return null;
        boolean isDateClass = jsonMap.containsKey(CLASS_KEY) && DATE_CLASS.equals(jsonMap.get(CLASS_KEY));
        if (isDateClass) {
            Object time = jsonMap.get(TIME_KEY);
            if (time instanceof Number) {
                return CMDateFormat.fromNumber((Number) time); //replace Number with Date
            } else {
                throw new ConversionException("Received non number time");
            }
        }

        for (Map.Entry<String, Object> jsonEntry : new HashMap<String, Object>(jsonMap).entrySet()) {
            if (jsonEntry.getValue() instanceof Map) {
                jsonMap.put(jsonEntry.getKey(),
                        convertDateClassesToDates((Map<String, Object>) jsonEntry.getValue()));
            }
        }
        return jsonMap;
    }

    /**
     * Convert an InputStream containg JSON to a Map representation
     * @param inputJson a stream of valid JSON
     * @return If json is null, returns an empty Map. Otherwise, a Map whose keys are JSON Strings and whose values are JSON values
     * @throws ConversionException if unable to convert the given json to a map. Will happen if the transportableRepresentation call fails or if unable to represent the json as a map
     */
    public static Map<String, Object> jsonToMap(InputStream inputJson) throws ConversionException {
        if (inputJson == null) {
            return new HashMap<String, Object>();
        }
        StringWriter writer = new StringWriter();
        try {
            IOUtils.copy(inputJson, writer, ENCODING);
            return jsonToMap(writer.toString());
        } catch (IOException e) {
            throw new ConversionException("Couldn't read inputJson", e);
        }
    }

    /**
     * Tests whether two json strings are equivalent; ignores formating and order. Expensive operation
     * as the strings are parsed to JsonNodes, which are compared.
     * @param first
     * @param second
     * @return true if first and second are equivalent JSON objects
     * @throws ConversionException if unable to convert Transportable to a JsonNode or if first or second cannot be converted to a JSON string
     */
    public static boolean isJsonEquivalent(Transportable first, Transportable second) throws ConversionException {
        return isJsonEquivalent(first.transportableRepresentation(), second.transportableRepresentation());
    }

    /**
     * Tests whether two json strings are equivalent; ignores formating and order. Expensive operation
     * as the strings are parsed to JsonNodes, which are compared.
     * @param first
     * @param second
     * @return true if first and second are equivalent JSON objects
     * @throws ConversionException if unable to convert first or second to JsonNodes
     */
    public static boolean isJsonEquivalent(String first, String second) throws ConversionException {
        if (first == null)
            return second == null;
        if (second == null)
            return false;
        try {
            JsonNode firstNode = jsonMapper.readTree(first);
            try {
                JsonNode secondNode = jsonMapper.readTree(second);
                return firstNode.equals(secondNode);
            } catch (IOException e) {
                throw new ConversionException("Couldn't convert second string to json: " + second, e);
            }
        } catch (IOException e) {
            throw new ConversionException("Couldn't convert first string to json: " + first, e);
        }
    }
}