/* # Licensed Materials - Property of IBM # Copyright IBM Corp. 2015 */ package com.ibm.streamsx.topology.json; import java.io.IOException; import java.util.ArrayList; import java.util.List; import com.ibm.json.java.JSON; import com.ibm.json.java.JSONArray; import com.ibm.json.java.JSONArtifact; import com.ibm.json.java.JSONObject; import com.ibm.streams.operator.OutputTuple; import com.ibm.streamsx.topology.TStream; import com.ibm.streamsx.topology.function.BiFunction; import com.ibm.streamsx.topology.function.Function; import com.ibm.streamsx.topology.spl.SPLStream; import com.ibm.streamsx.topology.spl.SPLStreams; import com.ibm.streamsx.topology.tuple.JSONAble; /** * Utilities for JSON streams. * * A JSON stream is a stream of JSON objects represented * by the class {@code com.ibm.json.java.JSONObject}. * When a JSON value that is an array or value (not an object) * needs to be present on the stream, the approach is to * represent it as a object with the key {@link #PAYLOAD payload} * containing the array or value. * <BR> * A JSON stream can be {@link TStream#publish(String) published} * so that IBM Streams applications implemented in different languages * can subscribe to it. * * @see <a href="http://www.json.org/">http://www.json.org - JSON (JavaScript Object Notation) is a lightweight data-interchange format.</a> */ public class JSONStreams { /** * JSON key for arrays and values. * When JSON is an array or a value (not an object) * it is represented as a {@code JSONObject} tuple * with an attribute (key) {@value} * containing the array or value. */ public static final String PAYLOAD = "payload"; /** * Function to deserialize a String to a JSONObject. * If the serialized JSON is an array, * then a JSON object is created, with * a single key {@code payload} containing the deserialized * value. */ public static final class DeserializeJSON implements Function<String, JSONObject> { private static final long serialVersionUID = 1L; @Override public JSONObject apply(String tuple) { try { JSONArtifact artifact = JSON.parse(tuple); if (artifact instanceof JSONObject) return (JSONObject) artifact; JSONObject wrapper = new JSONObject(); wrapper.put(PAYLOAD, artifact); return wrapper; } catch (IOException e) { throw new RuntimeException(e); } } } /** * Function to serialize a JSONObject to a String. */ public static final class SerializeJSON implements Function<JSONObject, String> { private static final long serialVersionUID = 1L; @Override public String apply(JSONObject v1) { try { return v1.serialize(); } catch (IOException e) { return null; } } } /** * Function to convert a {@link JSONAble} * tuple to a {@code JSONObject}, using {@link JSONAble#toJSON()}. */ public static final class ToJSON<T extends JSONAble> implements Function<T, JSONObject> { private static final long serialVersionUID = 1L; @Override public JSONObject apply(JSONAble v1) { return v1.toJSON(); } } /** * Convert a JSON stream to an SPLStream. * @param stream JSON stream to be converted. * @return SPLStream with schema {@link JSONSchemas#JSON}. */ public static SPLStream toSPL(TStream<JSONObject> stream) { return SPLStreams.convertStream(stream, new BiFunction<JSONObject, OutputTuple, OutputTuple>() { private static final long serialVersionUID = 1L; @Override public OutputTuple apply(JSONObject v1, OutputTuple v2) { try { v2.setString(0, v1.serialize()); return v2; } catch (IOException e) { return null; } } }, JSONSchemas.JSON); } /** * Create a stream of serialized JSON objects as String tuples. * * @param stream * Stream containing the JSON objects. * @return Stream that will contain the serialized JSON values. */ public static TStream<String> serialize(TStream<JSONObject> stream) { return stream.transform(new SerializeJSON()); } /** * Declare a stream of JSON objects from a stream of serialized JSON tuples. * If the serialized JSON is a simple value or an array, * then a JSON object is created, with * a single attribute {@code payload} containing the deserialized * value. * @param stream * Stream containing the JSON serialized values. * @return Stream that will contain the JSON objects. */ public static TStream<JSONObject> deserialize(TStream<String> stream) { return stream.transform(new DeserializeJSON()); } /** * Declare a stream of JSON objects from a stream * of Java objects that implement {@link JSONAble}. * @param stream Stream containing {@code JSONAble} tuples. * @return Stream that will contain the JSON objects. */ public static <T extends JSONAble> TStream<JSONObject> toJSON(TStream<T> stream) { return stream.transform(new ToJSON<T>()); } /** * Declare a stream that flattens an array present in the input tuple. * For each tuple on {@code stream} the key {@code arrayKey} and its * value are extracted and if it is an array then each element in * the array will be present on the returned stream as an individual tuple. * <BR> * If an array element is a JSON object it will be placed on returned stream, * otherwise a JSON object will be placed on the returned stream with * the key {@link #PAYLOAD payload} containing the element's value. * <BR> * If {@code arrayKey} is not present, is not an array or is an empty array * then no tuples will result on the returned stream. * <P> * Any additional keys ({@code additionalKeys}) that are specified are * copied (with their value) into each JSON object on the returned stream * from the input tuple, unless the flattened tuple already contains a value for the key. * <BR> * If an addition key is not in the input tuple, then it is not copied into * the flattened tuples. * </P> * <P> * For example, with a JSON object input tuple of: * <pre> * <code> * {"ts":"13:28:07", "sensors": * [ * {"temperature": 34.2}, * {"rainfall": 12.96} * ] * } * </code> * </pre> * and a call of {@code flattenArray(stream, "sensors", "ts")} would result in two tuples: * <pre> * <code> * {"temperature": 34.2, "ts": "13:28:07"} * {"rainfall": 12.96, "ts": "13:28:07"} * </code> * </pre> * </P> * <P> * With a JSON input tuple containing an array of simple values: * <pre> * <code> * {"ts":"13:43:09", "unit": "C", "readings": * [ * 33.9, * 33.8, * 34.1 * ] * } * </code> * </pre> * and a call of {@code flattenArray(stream, "readings", "ts", "unit")} * would result in three tuples: * <pre> * <code> * {"payload": 33.9, "ts": "13:43:09", "unit":"C"} * {"payload": 33.8, "ts": "13:43:09", "unit":"C"} * {"payload": 34.1, "ts": "13:43:09", "unit":"C"} * </code> * </pre> * </P> * </P> * @param stream Steam containing tuples with an array to be flattened. * @param arrayKey Key of the array in each input tuple. * @param additionalKeys Additional keys that copied from the input tuple into each resultant tuple from the array * @return Stream containing tuples flattened from input tuple. */ public static TStream<JSONObject> flattenArray(TStream<JSONObject> stream, final String arrayKey, final String... additionalKeys) { return stream.multiTransform( new Function<JSONObject, Iterable<JSONObject>>() { private static final long serialVersionUID = 1L; @Override public Iterable<JSONObject> apply(JSONObject tuple) { Object oa = tuple.get(arrayKey); if (!(oa instanceof JSONArray)) return null; JSONArray ja = (JSONArray) oa; if (ja.isEmpty()) return null; JSONObject additional = null; if (additionalKeys.length != 0) { additional = new JSONObject(); for (String addKey : additionalKeys) { Object akv = tuple.get(addKey); if (akv != null) additional.put(addKey, akv); } if (additional.isEmpty()) additional = null; } List<JSONObject> tuples = new ArrayList<>(ja.size()); for (Object av : ja) { JSONObject flattened; if (av instanceof JSONObject) { flattened = (JSONObject) av; } else { flattened = new JSONObject(); flattened.put(PAYLOAD, av); } if (additional != null) { for (Object addKey : additional.keySet()) { if (!flattened.containsKey(addKey)) flattened.put(addKey, additional.get(addKey)); } } tuples.add(flattened); } return tuples; } }); } }