/* * The MIT License * * Copyright 2017 Intuit Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.intuit.karate; import com.intuit.karate.core.Feature; import com.intuit.karate.driver.DriverElement; import com.intuit.karate.driver.Element; import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.Option; import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.spi.json.JsonProvider; import com.jayway.jsonpath.spi.json.JsonSmartJsonProvider; import com.jayway.jsonpath.spi.mapper.JsonSmartMappingProvider; import com.jayway.jsonpath.spi.mapper.MappingProvider; import de.siegmar.fastcsv.reader.CsvContainer; import de.siegmar.fastcsv.reader.CsvReader; import de.siegmar.fastcsv.reader.CsvRow; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import jdk.nashorn.api.scripting.ScriptObjectMirror; import net.minidev.json.JSONStyle; import net.minidev.json.JSONValue; import net.minidev.json.parser.JSONParser; import net.minidev.json.parser.ParseException; import net.minidev.json.reader.JsonWriter; import net.minidev.json.reader.JsonWriterI; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; /** * * @author pthomas3 */ public class JsonUtils { private JsonUtils() { // only static methods } private static class NashornObjectJsonWriter implements JsonWriterI<ScriptObjectMirror> { @Override public <E extends ScriptObjectMirror> void writeJSONString(E value, Appendable out, JSONStyle compression) throws IOException { if (value.isArray()) { Object[] array = value.values().toArray(); JsonWriter.arrayWriter.writeJSONString(array, out, compression); } else if (value.isFunction()) { JsonWriter.toStringWriter.writeJSONString("\"#function\"", out, compression); } else { // JSON JsonWriter.JSONMapWriter.writeJSONString(value, out, compression); } } } private static class FeatureJsonWriter implements JsonWriterI<Feature> { @Override public <E extends Feature> void writeJSONString(E value, Appendable out, JSONStyle compression) throws IOException { JsonWriter.toStringWriter.writeJSONString("\"#feature\"", out, compression); } } private static class ElementJsonWriter implements JsonWriterI<Element> { @Override public <E extends Element> void writeJSONString(E value, Appendable out, JSONStyle compression) throws IOException { JsonWriter.toStringWriter.writeJSONString("\"" + value.getLocator() + "\"", out, compression); } } static { // prevent things like the karate script bridge getting serialized (especially in the javafx ui) JSONValue.registerWriter(ScriptObjectMirror.class, new NashornObjectJsonWriter()); JSONValue.registerWriter(Feature.class, new FeatureJsonWriter()); JSONValue.registerWriter(DriverElement.class, new ElementJsonWriter()); // ensure that even if jackson (databind?) is on the classpath, don't switch provider Configuration.setDefaults(new Configuration.Defaults() { private final JsonProvider jsonProvider = new JsonSmartJsonProvider(); private final MappingProvider mappingProvider = new JsonSmartMappingProvider(); @Override public JsonProvider jsonProvider() { return jsonProvider; } @Override public MappingProvider mappingProvider() { return mappingProvider; } @Override public Set<Option> options() { return EnumSet.noneOf(Option.class); } }); } public static DocumentContext toJsonDocStrict(String raw) { try { JSONParser parser = new JSONParser(JSONParser.MODE_RFC4627); Object o = parser.parse(raw.trim()); return JsonPath.parse(o); } catch (ParseException e) { throw new RuntimeException(e); } } public static DocumentContext toJsonDoc(String raw) { return JsonPath.parse(raw); } public static String toStrictJsonString(String raw) { DocumentContext dc = toJsonDoc(raw); return dc.jsonString(); } public static String toJson(Object o) { return JSONValue.toJSONString(o); } public static Map removeCyclicReferences(Map map) { Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap()); seen.add(map); map = new LinkedHashMap(map); // clone for safety recurseCyclic(0, map, seen); return map; } private static boolean recurseCyclic(int depth, Object o, Set<Object> seen) { // we use a depth check because for some reason // ScriptObjectMirror has some object equality problems for entries if (o instanceof Map) { if (depth > 10 || !seen.add(o)) { return true; } Map map = (Map) o; map.forEach((k, v) -> { if (recurseCyclic(depth + 1, v, seen)) { map.put(k, "#" + v.getClass().getName()); } }); } else if (o instanceof List) { if (depth > 10 || !seen.add(o)) { return true; } List list = (List) o; int count = list.size(); for (int i = 0; i < count; i++) { Object v = list.get(i); if (recurseCyclic(depth + 1, v, seen)) { list.set(i, "#" + v.getClass().getName()); } } } return false; } public static DocumentContext toJsonDoc(Object o) { return toJsonDoc(toJson(o)); } public static Object fromJson(String s, String className) { try { Class clazz = Class.forName(className); return JSONValue.parse(s, clazz); } catch (Exception e) { throw new RuntimeException(e); } } public static <T> T fromJson(String s, Class<T> clazz) { return (T) fromJson(s, clazz.getName()); } public static String toPrettyJsonString(DocumentContext doc) { Object o = doc.read("$"); StringBuilder sb = new StringBuilder(); // anti recursion / back-references Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap()); recursePretty(o, sb, 0, seen); sb.append('\n'); return sb.toString(); } private static void pad(StringBuilder sb, int depth) { for (int i = 0; i < depth; i++) { sb.append(' ').append(' '); } } private static void ref(StringBuilder sb, Object o) { sb.append("\"#ref:").append(o.getClass().getName()).append('"'); } public static String escapeValue(String raw) { return JSONValue.escape(raw, JSONStyle.LT_COMPRESS); } public static void removeKeysWithNullValues(Object o) { if (o instanceof Map) { Map<String, Object> map = (Map) o; List<String> toRemove = new ArrayList(); for (Map.Entry<String, Object> entry : map.entrySet()) { Object v = entry.getValue(); if (v == null) { toRemove.add(entry.getKey()); } else { removeKeysWithNullValues(v); } } toRemove.forEach(key -> map.remove(key)); } else if (o instanceof List) { List list = (List) o; for (Object v : list) { removeKeysWithNullValues(v); } } } private static void recursePretty(Object o, StringBuilder sb, int depth, Set<Object> seen) { if (o == null) { sb.append("null"); } else if (o instanceof Map) { if (seen.add(o)) { sb.append('{').append('\n'); Map<String, Object> map = (Map<String, Object>) o; Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Object> entry = iterator.next(); String key = entry.getKey(); pad(sb, depth + 1); sb.append('"').append(escapeValue(key)).append('"'); sb.append(':').append(' '); recursePretty(entry.getValue(), sb, depth + 1, seen); if (iterator.hasNext()) { sb.append(','); } sb.append('\n'); } pad(sb, depth); sb.append('}'); } else { ref(sb, o); } } else if (o instanceof List) { List list = (List) o; Iterator iterator = list.iterator(); if (seen.add(o)) { sb.append('[').append('\n'); while (iterator.hasNext()) { Object child = iterator.next(); pad(sb, depth + 1); recursePretty(child, sb, depth + 1, seen); if (iterator.hasNext()) { sb.append(','); } sb.append('\n'); } pad(sb, depth); sb.append(']'); } else { ref(sb, o); } } else if (o instanceof String) { String value = (String) o; sb.append('"').append(escapeValue(value)).append('"'); } else { sb.append(o); } } public static StringUtils.Pair getParentAndLeafPath(String path) { int pos = path.lastIndexOf('.'); int temp = path.lastIndexOf("['"); if (temp != -1 && temp > pos) { pos = temp - 1; } String right = path.substring(pos + 1); if (right.startsWith("[")) { pos = pos + 1; } String left = path.substring(0, pos == -1 ? 0 : pos); return StringUtils.pair(left, right); } public static void removeValueByPath(DocumentContext doc, String path) { setValueByPath(doc, path, null, true); } public static void setValueByPath(DocumentContext doc, String path, Object value) { setValueByPath(doc, path, value, false); } public static void setValueByPath(DocumentContext doc, String path, Object value, boolean remove) { if ("$".equals(path)) { throw new RuntimeException("cannot replace root path $"); } StringUtils.Pair pathLeaf = getParentAndLeafPath(path); String left = pathLeaf.left; String right = pathLeaf.right; if (right.endsWith("]") && !right.endsWith("']")) { // json array int indexPos = right.lastIndexOf('['); int index = -1; // append, for case 'foo[]' (no integer, empty brackets) if (right.length() != indexPos + 1) { try { index = Integer.valueOf(right.substring(indexPos + 1, right.length() - 1)); } catch (Exception e) { // index will be -1, default to append } } right = right.substring(0, indexPos); List list; String listPath; if (right.startsWith("[")) { listPath = left + right; } else { if ("".equals(left)) { // special case, root array listPath = right; } else { listPath = left + "." + right; } } try { list = doc.read(listPath); if (index == -1) { index = list.size(); } if (index < list.size()) { if (remove) { list.remove(index); } else { list.set(index, value); } } else if (!remove) { list.add(value); } } catch (Exception e) { // path does not exist or null if (!remove) { list = new ArrayList(); list.add(value); doc.put(left, right, list); } } } else { if (remove) { doc.delete(path); } else { if (right.startsWith("[")) { right = right.substring(2, right.length() - 2); } if (!pathExists(doc, left)) { createParents(doc, left); } doc.put(left, right, value); } } } private static void createParents(DocumentContext doc, String path) { StringUtils.Pair pathLeaf = getParentAndLeafPath(path); String left = pathLeaf.left; String right = pathLeaf.right; if ("".equals(left)) { // if root if (!"$".equals(right)) { // special case, root is array, typically "$[0]" doc.add("$", new LinkedHashMap()); // TODO we assume that second level is always object (not array of arrays) } return; } if (!pathExists(doc, left)) { createParents(doc, left); } Object empty; if (right.endsWith("]") && !right.endsWith("']")) { int pos = right.indexOf('['); right = right.substring(0, pos); List list = new ArrayList(); list.add(new LinkedHashMap()); empty = list; } else { empty = new LinkedHashMap(); } doc.put(left, right, empty); } public static boolean pathExists(DocumentContext doc, String path) { try { return doc.read(path) != null; } catch (PathNotFoundException pnfe) { return false; } } public static DocumentContext fromYaml(String raw) { Yaml yaml = new Yaml(new SafeConstructor()); Object o = yaml.load(raw); return JsonPath.parse(o); } public static DocumentContext fromCsv(String raw) { CsvReader reader = new CsvReader(); reader.setContainsHeader(true); try { CsvContainer csv = reader.read(new StringReader(raw)); List<Map> rows = new ArrayList(csv.getRowCount()); for (CsvRow row : csv.getRows()) { rows.add(row.getFieldMap()); } return toJsonDoc(rows); } catch (Exception e) { throw new RuntimeException(e); } } /** * use bracket notation if needed instead of dot notation */ public static String buildPath(String parentPath, String key) { boolean needsQuotes = key.indexOf('-') != -1 || key.indexOf(' ') != -1 || key.indexOf('.') != -1; return needsQuotes ? parentPath + "['" + key + "']" : parentPath + '.' + key; } public static DocumentContext emptyJsonObject() { return toJsonDoc(new LinkedHashMap()); } public static DocumentContext emptyJsonArray(int length) { List list = new ArrayList(length); for (int i = 0; i < length; i++) { list.add(new LinkedHashMap()); } return toJsonDoc(list); } }