// Copyright (c) 2014, SAS Institute Inc., Cary, NC, USA, All Rights Reserved package com.sas.unravl.util; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.BigIntegerNode; import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.DecimalNode; import com.fasterxml.jackson.databind.node.DoubleNode; import com.fasterxml.jackson.databind.node.FloatNode; import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.LongNode; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ShortNode; import com.fasterxml.jackson.databind.node.TextNode; import com.google.common.base.Function; import com.sas.unravl.UnRAVL; import com.sas.unravl.UnRAVLException; import com.sas.unravl.generators.Text; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.log4j.Logger; /** * JSON utility methods. * * @author David.Biesack@sas.com */ public class Json { private static ObjectMapper mapper = new ObjectMapper(); private static final Logger logger = Logger.getLogger(Json.class); /** * Convenience method for parsing a string as JSON. * * @param json * text JSON; this must be valid * @return the root JsonNode * @throws UnRAVLException * if the json is not valid. */ public static JsonNode parse(String json) throws UnRAVLException { try { return mapper.readTree(json); } catch (JsonProcessingException e) { logger.error(e); throw new UnRAVLException(e.getMessage(), e); } catch (IOException e) { logger.error(e); throw new UnRAVLException(e.getMessage(), e); } } /** * Process a JsonNode and its subtree and perform environment expansion on * all text. * * @param actual * an input Json * @param script * the Unravl script * @return a new JsonNode with the text mapped */ public static JsonNode expand(JsonNode actual, final UnRAVL script) { final JsonNodeFactory jnf = jsonNodeFactory(); /** * A recursive JsonNode transformation mapping function which replaces * each string with its environment expansion, That is, replace * {varName} with the current binding for "varName" in the script's * environment. * * @param node * the input JSON * @return the node or a replacement which the text expanded. */ Function<JsonNode, JsonNode> expandText = new Function<JsonNode, JsonNode>() { @Override public JsonNode apply(JsonNode node) { if (node.isTextual()) { return new TextNode(script.expand(node.textValue())); } else if (node.isArray()) { ArrayNode from = (ArrayNode) node; ArrayNode to = new ArrayNode(jnf); for (int i = 0; i < from.size(); i++) { to.add(this.apply(from.get(i))); } return to; } else if (node.isObject()) { ObjectNode from = (ObjectNode) node; ObjectNode to = new ObjectNode(jnf); for (Map.Entry<String, JsonNode> f : Json.fields(from)) { to.set(script.expand(f.getKey()), this.apply(f.getValue())); } return to; } else return node; } }; return map(actual, expandText); } /** * Transform a JsonNode tree by applying a mapping function to the nodes in * it. * * @param node * the input JSON * @param mappingFunction * a Function that maps a node to a node * @return the transformed map which may be the input JsonNode or a new * node. */ public static JsonNode map(JsonNode node, Function<JsonNode, JsonNode> mappingFunction) { return mappingFunction.apply(node); } /** * Return the input node as an ArrayNode when an UnRAVL script expects an * array node. * * @param node * input node * @return the input node, cast as an ArrayNode * @throws UnRAVLException * if the node is not an ArrayNode. This is used instead of * ClassCastException because it indicates a user error (an * improperly formed UnRAVL JSON script), not an implementation * error in the UnRAVL execution. */ public static ArrayNode array(JsonNode node) throws UnRAVLException { if (node.isArray()) return (ArrayNode) node; else throw new UnRAVLException("expected array when found " + node); } /** * Return the input node as an ObjectNode when an UnRAVL script expects an * array node. * * @param node * input node * @return the input node, cast as an ObjectNode * @throws UnRAVLException * if the node is not an ObjectNode. This is used instead of * ClassCastException because it indicates a user error (an * improperly formed UnRAVL JSON script), not an implementation * error in the UnRAVL execution. */ public static ObjectNode object(JsonNode node) throws UnRAVLException { if (node.isObject()) return (ObjectNode) node; else throw new UnRAVLException("expected object when found " + node); } /** * Return the input node as an TextNode when an UnRAVL script expects an * text node. * * @param node * input node * @return the input node, cast as an TextNode * @throws UnRAVLException * if the node is not an TextNode. This is used instead of * ClassCastException because it indicates a user error (an * improperly formed UnRAVL JSON script), not an implementation * error in the UnRAVL execution. */ public static TextNode text(JsonNode node) throws UnRAVLException { if (!node.isTextual()) throw new UnRAVLException("Text node required when " + node + " found."); TextNode text = (TextNode) node; return text; } /** * Access the first field of a JsonNode, which must be an ObjectNode * * @param node * a JsonNode which will be used as an ObjectNode * @return the first field in the ObjectNode as a Map.Entry * @throws UnRAVLException * if the node is not an ObjectNode or if the node is empty. */ public static Map.Entry<String, JsonNode> firstField(JsonNode node) throws UnRAVLException { ObjectNode o = object(node); Iterator<Map.Entry<String, JsonNode>> iter = o.fields(); if (iter.hasNext()) { Map.Entry<String, JsonNode> first = iter.next(); return first; } else throw new UnRAVLException("Non-empty object required when " + node + " found."); } /** * Convert an ArrayNode into an iterable list of JsonNode * * @param node * a JsonNode which will be used as an ObjectNode * @return a List containing the elements * @throws UnRAVLException * if the node is not an ArrayNode */ public static List<JsonNode> toArray(JsonNode node) throws UnRAVLException { ArrayNode a = array(node); List<JsonNode> list = new ArrayList<JsonNode>(a.size()); Iterator<JsonNode> iter = a.elements(); while (iter.hasNext()) { list.add(iter.next()); } return list; } /** * Return the name of the first field in a JsonNode * * @param node * a node which must be a non-empty ObjectNode * @return the name of the first field * @throws UnRAVLException * if the node is not an ObjectNode or if the node is empty. */ public static String firstFieldName(JsonNode node) throws UnRAVLException { return firstField(node).getKey(); } /** * Return the name of the first field in a JsonNode * * @param node * a node which must be a non-empty ObjectNode * @return the value of the first field * @throws UnRAVLException * if the node is not an ObjectNode or if the node is empty. */ public static JsonNode firstFieldValue(JsonNode node) throws UnRAVLException { return firstField(node).getValue(); } public static List<Map.Entry<String, JsonNode>> fields(JsonNode node) throws UnRAVLException { ObjectNode object = object(node); return fields(object); } /** * Convert an ObjectNode to a List of Map.Entry objects * * @param object * an JSON object * @return a List of Map.Entry objects. These are active and updating them * with setKey or setValue will update the input object. */ public static List<Map.Entry<String, JsonNode>> fields(ObjectNode object) { List<Map.Entry<String, JsonNode>> l = new ArrayList<Map.Entry<String, JsonNode>>(); Iterator<Map.Entry<String, JsonNode>> iter = object.fields(); while (iter.hasNext()) { Map.Entry<String, JsonNode> next = iter.next(); l.add(next); } return l; } /** * Write a JSON tree to a stream * * @param json * the JSON node * @param fileName * file name to write the JSON to * @throws UnRAVLException * if the file is not writable */ public static void extractToStream(JsonNode json, String fileName) throws UnRAVLException { try { boolean stdout = fileName.equals("-"); Writer w = stdout ? new PrintWriter(System.out) : new OutputStreamWriter(new FileOutputStream(fileName), Text.UTF_8); JsonFactory jf = jsonFactory(); JsonGenerator g = jf.createGenerator(w); g.setCodec(new ObjectMapper()); g.useDefaultPrettyPrinter(); g.writeTree(json); if (stdout) w.write("\n"); else w.close(); } catch (FileNotFoundException e) { throw new UnRAVLException(e.getMessage(), e); } catch (IOException e) { throw new UnRAVLException(e.getMessage(), e); } } public static JsonFactory jsonFactory() { return new JsonFactory(); } public static JsonNodeFactory jsonNodeFactory() { return JsonNodeFactory.instance; } public static ArrayNode wrapInArray(JsonNode node) { ArrayNode a = new ArrayNode(jsonNodeFactory()); a.add(node); return a; } /** * If the JSON node contains a field with the given name and it is text, * return that text, else return the default value * * @param node * a JSON node * @param fieldName * the name of a field to get from the object * @param defaultValue * the value to return if the named field is not found. * @return the string value of node.fieldname, if it exists, else * defaultValue */ public static String stringFieldOr(ObjectNode node, String fieldName, String defaultValue) { JsonNode value = node.get(fieldName); if (value == null || !value.isTextual()) return defaultValue; return value.textValue(); } public static Object unwrap(Object val) { // Can Jackson do this via // ObjectMapper.treeToValue()? The // spec is unclear Object result = val; ObjectMapper mapper = new ObjectMapper(); if (val instanceof ObjectNode) { result = mapper.convertValue((ObjectNode) val, Map.class); } else if (val instanceof ArrayNode) { result = mapper.convertValue((ObjectNode) val, List.class); } else if (val instanceof NullNode) { result = null; } else if (val instanceof BooleanNode) { result = ((BooleanNode) val).booleanValue(); } else if (val instanceof ShortNode) { result = ((ShortNode) val).shortValue(); } else if (val instanceof IntNode) { result = ((IntNode) val).intValue(); } else if (val instanceof LongNode) { result = ((LongNode) val).longValue(); } else if (val instanceof DoubleNode) { result = ((DoubleNode) val).doubleValue(); } else if (val instanceof FloatNode) { result = ((FloatNode) val).floatValue(); } else if (val instanceof BigIntegerNode) { result = ((BigIntegerNode) val).bigIntegerValue(); } else if (val instanceof DecimalNode) { result = ((DecimalNode) val).decimalValue(); } return result; } /** * Convert a Java Map to a JSON ObjectNode * * @param val * a Map object * @return a ObjectNode that corresponds to the Map */ public static ObjectNode wrap(@SuppressWarnings("rawtypes") Map val) { return mapper.valueToTree(val); } /** * Convert a Java List to a JSON ArrayNode * * @param val * a List object * @return a ArrayNode that corresponds to the Map */ public static ArrayNode wrap(@SuppressWarnings("rawtypes") List val) { return mapper.valueToTree(val); } /** * Convert a Java object to a JsonNode * * @param val * a List, Map, or other object * @return a JsonNode that corresponds to the val */ @SuppressWarnings("rawtypes") public static JsonNode wrap(Object val) { if (val == null) return NullNode.getInstance(); else if (val instanceof Map) return wrap((Map) val); else if (val instanceof List) return wrap((List) val); else { ArrayList<Object> o = new ArrayList<Object>(1); o.add(val); ArrayNode a = wrap(o); JsonNode n = a.get(0); return n; } } }