JsonUtils.java

package com.github.dakusui.json;

import com.google.gson.*;

import java.util.*;
import java.util.Map.Entry;

import static com.github.dakusui.json.JsonSummarizer.*;
import static com.github.dakusui.valid8j.Requires.require;
import static com.github.dakusui.valid8j_pcond.forms.Predicates.callp;
import static java.util.Objects.requireNonNull;

public class JsonUtils {

  static final ThreadLocal<JsonParser> JSON_PARSER;

  static {
    JSON_PARSER = new ThreadLocal<>();
  }

  public static String summarizeJsonElement(JsonElement jsonElement) {
    if (jsonElement == null || jsonElement.isJsonNull()) {
      return "null";
    }
    JsonElement compact = JsonSummarizer.compactJsonElement(jsonElement, 3, 3);

    if (jsonElement.isJsonPrimitive()) {
      return compact + " (primitive)";
    }
    if (jsonElement.isJsonArray()) {
      return focusedArray((JsonArray) compact) + " (array: size=" + jsonElement.getAsJsonArray().size() + ")";
    }
    if (jsonElement.isJsonObject()) {
      return focusedObject((JsonObject) compact) + " (object: " + jsonElement.getAsJsonObject().entrySet().size() + " entries)";
    }
    return compact + " (unknown)";
  }

  /**
   * Returns a string representation of a path to a JSON element {@code target} in {@code root} JSON object node.
   *
   * @param target A JSON element whose path in {@code root} is searched for.
   * @param root   A root JSON object node where {@code target}'s path is searched.
   * @return A string representation of path to {@code target} in {@code root}.
   */
  public static String findPathStringOf(JsonElement target, JsonObject root) {
    return jsonpathToString(findPathOf(target, root));
  }

  public static List<Object> findPathOf(JsonElement target, JsonObject root) {
    return buildPathInfo(root).get(target);
  }

  @SafeVarargs
  public static JsonObject createSummaryJsonObjectFromPaths(JsonObject rootJsonObject, List<Object>... paths) {
    JsonObject ret = new JsonObject();
    for (List<Object> eachPath : paths) {
      ret = merge(requireJsonObject(createSummaryJsonElementFromPath(rootJsonObject, eachPath)), ret);
    }
    return ret;
  }

  private static JsonElement createSummaryJsonElementFromPath(JsonObject rootJsonObject, List<Object> path) {
    return path.isEmpty() ?
        focusedElement(compactJsonObject(rootJsonObject, 3, 3)) :
        path.size() == 1 ?
            summaryTopLevelElement(rootJsonObject, path.getFirst()) :
            summaryObject(rootJsonObject, path.subList(0, path.size() - 1), path.getLast());
  }

  private static JsonElement summaryTopLevelElement(JsonObject rootJsonObject, Object topLevelAttribute) {
    assert topLevelAttribute instanceof String;
    JsonObject ret = new JsonObject();
    String topLevelAttributeName = (String) topLevelAttribute;
    ret.add(topLevelAttributeName, focusedElement(asJsonElement(rootJsonObject, topLevelAttribute)));
    return ret;
  }

  private static JsonObject requireJsonObject(JsonElement jsonElement) {
    return require(jsonElement, callp("isJsonObject")).getAsJsonObject();
  }

  enum JsonTypes {
    OBJECT {
      @Override
      JsonObject _validate(JsonElement value) throws JsonTypeMismatchException {
        if (!value.isJsonObject()) {
          throw new JsonTypeMismatchException(value, this);
        }
        return value.getAsJsonObject();
      }
    },
    ARRAY {
      @Override
      JsonArray _validate(JsonElement value) throws JsonTypeMismatchException {
        if (!value.isJsonArray()) {
          throw new JsonTypeMismatchException(value, this);
        }
        return value.getAsJsonArray();
      }

    },
    PRIMITIVE {
      @Override
      JsonPrimitive _validate(JsonElement value)
          throws JsonTypeMismatchException {
        if (!value.isJsonPrimitive()) {
          throw new JsonTypeMismatchException(value, this);
        }
        return value.getAsJsonPrimitive();
      }
    },
    NULL {
      @Override
      JsonNull _validate(JsonElement value) throws JsonTypeMismatchException {
        if (!value.isJsonNull()) {
          throw new JsonTypeMismatchException(value, this);
        }
        return value.getAsJsonNull();
      }
    };

    abstract JsonElement _validate(JsonElement value)
        throws JsonTypeMismatchException;

    JsonElement validate(JsonElement value) throws JsonTypeMismatchException {
      if (value == null) {
        return null;
      }
      return _validate(value);
    }
  }

  public static JsonElement asJsonElementWithDefault(JsonElement base, JsonElement defaultValue, int from, Object[] path)
      throws JsonInvalidPathException {
    JsonElement ret = _asJsonElement(base, defaultValue, from, path);
    // //
    // TODO: To workaround test failure. Need to come up with more consistent
    // policy to handle JsonNull.INSTANCE.
    if (ret == null || ret == JsonNull.INSTANCE) {
      ret = defaultValue;
    }
    return ret;
  }

  public static JsonElement asJsonElement(JsonElement base, int from, Object[] path) throws JsonInvalidPathException {
    JsonElement ret = _asJsonElement(base, null, from, path);
    if (ret == null) {
      throw new JsonPathNotFoundException(base, path, from);
    }
    return ret;
  }

  private static JsonElement _asJsonElement(JsonElement base,
                                            JsonElement defaultValue, int from, Object[] path)
      throws JsonInvalidPathException {
    if (path.length == from || base == null) {
      return base;
    }
    JsonElement newbase;
    if (path[from] == null) {
      throw new JsonInvalidPathException(base, path, from); // invalid path;
    }
    if (base.isJsonObject()) {
      newbase = base.getAsJsonObject().get(path[from].toString());
    } else {
      if (base.isJsonArray()) {
        Integer index;
        if ((path[from] instanceof Integer) || (path[from] instanceof Long)
            || (path[from] instanceof Short)) {
          index = ((Number) path[from]).intValue();
        } else {
          if ((index = parseInt(path[from])) == null) {
            throw new JsonInvalidPathException(base, path, from);
          }
        }
        if (index < 0 || index >= base.getAsJsonArray().size()) {
          throw new JsonIndexOutOfBoundsException(base, path, from);
        }
        newbase = base.getAsJsonArray().get(index);
      } else if (base.isJsonPrimitive()) {
        return null;
      } else {
        // JsonNull
        return null;
      }
    }

    JsonElement ret = _asJsonElement(newbase, defaultValue, from + 1, path);

    if (ret == null) {
      if (defaultValue != null) {
        return defaultValue;
      }
    }
    return ret;
  }

  private static Integer parseInt(Object object) {
    Integer ret = null;
    try {
      String str = object.toString();
      ret = Integer.parseInt(str);
    } catch (NumberFormatException ignored) {
    }

    return ret;
  }

  public static boolean hasPath(JsonElement base, Object... path) {
    try {
      return _asJsonElement(base, null, 0, path) != null;
    } catch (JsonInvalidPathException e) {
      return false;
    }
  }

  public static JsonObject asJsonObjectWithDefault(JsonObject base,
                                                   JsonObject defaultValue, Object... path)
      throws JsonTypeMismatchException,
      JsonInvalidPathException {
    return (JsonObject) JsonTypes.OBJECT.validate(asJsonElementWithDefault(
        base, defaultValue, path));
  }

  public static JsonObject asJsonObject(JsonObject base, Object... path)
      throws JsonTypeMismatchException,
      JsonInvalidPathException {
    return asJsonObjectWithDefault(base, null, path);
  }

  public static JsonObject asJsonObjectWithPromotion(JsonElement base,
                                                     String[] prioritizedKeys, Object... path)
      throws JsonInvalidPathException,
      JsonTypeMismatchException {
    JsonObject ret;
    JsonElement elem = asJsonElement(base, path);
    if (elem.isJsonObject()) {
      ret = elem.getAsJsonObject();
    } else {
      JsonArray arr = asJsonArrayWithPromotion(base, path);
      ret = new JsonObject();
      int i = 0;
      for (JsonElement item : arr) {
        if (i >= prioritizedKeys.length) {
          if (prioritizedKeys.length == 0) {
            throw new JsonTypeMismatchException(elem,
                "An object or an empty array are acceptable");
          } else {
            throw new JsonTypeMismatchException(
                elem,
                String
                    .format(
                        "A primitive, an array whose length is less than or equal to %d, or an object are acceptable",
                        prioritizedKeys.length));
          }
        }
        String key = prioritizedKeys[i];
        ret.add(key, item);
        i++;
      }
    }
    return ret;
  }

  public static JsonArray asJsonArrayWithPromotion(JsonElement base,
                                                   Object... path) throws
      JsonInvalidPathException, JsonTypeMismatchException {
    JsonArray ret;
    JsonElement elem = asJsonElement(base, path);
    if (elem.isJsonObject()) {
      throw new JsonTypeMismatchException(elem, JsonTypes.ARRAY,
          JsonTypes.NULL, JsonTypes.PRIMITIVE);
    }
    if (elem.isJsonArray()) {
      ret = elem.getAsJsonArray();
    } else {
      ret = new JsonArray();
      ret.add(elem);
    }
    return ret;
  }

  public static JsonArray asJsonArray(JsonElement base, Object... path)
      throws JsonException {
    return asJsonArrayWithDefault(base, null, path);
  }

  public static JsonArray asJsonArrayWithDefault(JsonElement base,
                                                 JsonArray defaultValue, Object... path) throws JsonTypeMismatchException,
      JsonInvalidPathException {
    return (JsonArray) JsonTypes.ARRAY.validate(asJsonElementWithDefault(base,
        defaultValue, path));
  }

  public static JsonElement asJsonElementWithDefault(JsonElement base,
                                                     JsonElement defaultValue, Object... path)
      throws JsonInvalidPathException {
    return asJsonElementWithDefault(base, defaultValue, 0, path);
  }

  public static JsonElement asJsonElement(JsonElement base, Object... path)
      throws JsonInvalidPathException {
    return asJsonElement(base, 0, path);
  }

  public static String asString(JsonElement base, Object... path)
      throws JsonTypeMismatchException,
      JsonInvalidPathException {
    JsonPrimitive prim = (JsonPrimitive) JsonTypes.PRIMITIVE
        .validate(asJsonElement(base, path));
    if (prim == null) {
      return null;
    }
    return prim.getAsString();
  }

  public static String asStringWithDefault(JsonElement base,
                                           String defaultValue, Object... path) throws JsonTypeMismatchException,
      JsonInvalidPathException {
    JsonElement dv = null;
    if (defaultValue != null) {
      dv = new JsonPrimitive(defaultValue);
    }
    JsonPrimitive prim = (JsonPrimitive) JsonTypes.PRIMITIVE
        .validate(asJsonElementWithDefault(base, dv, path));
    if (prim == null) {
      return null;
    }
    return prim.getAsString();
  }

  public static int asInt(JsonElement base, Object... path)
      throws JsonTypeMismatchException,
      JsonFormatException, JsonInvalidPathException {
    try {
      return Integer.parseInt(requireNonNull(asString(base, path)));
    } catch (NumberFormatException e) {
      throw new JsonFormatException(asJsonElement(base, path));
    }
  }

  public static int asIntWithDefault(JsonElement base, int defaultValue,
                                     Object... path) throws JsonTypeMismatchException,
      JsonFormatException, JsonInvalidPathException {
    try {
      return Integer.parseInt(requireNonNull(asStringWithDefault(base, Integer.toString(defaultValue), path)));
    } catch (NumberFormatException e) {
      throw new JsonFormatException(asJsonElement(base, path));
    }
  }

  public static long asLong(JsonElement base, Object... path)
      throws JsonTypeMismatchException,
      JsonFormatException, JsonInvalidPathException {
    try {
      return Long.parseLong(requireNonNull(asString(base, path)));
    } catch (NumberFormatException e) {
      throw new JsonFormatException(asJsonElement(base, path));
    }
  }

  @SuppressWarnings("UnusedDeclaration")
  public static long asLongWithDefault(JsonElement base, long defaultValue,
                                       Object... path) throws JsonTypeMismatchException,
      JsonFormatException, JsonInvalidPathException {
    try {
      return Long.parseLong(requireNonNull(asStringWithDefault(base, Long.toString(defaultValue), path)));
    } catch (NumberFormatException e) {
      throw new JsonFormatException(asJsonElement(base, path));
    }
  }

  @SuppressWarnings("UnusedDeclaration")
  public static float asFloat(JsonElement base, Object... path)
      throws JsonTypeMismatchException,
      JsonFormatException, JsonInvalidPathException {
    try {
      return Float.parseFloat(requireNonNull(asString(base, path)));
    } catch (NumberFormatException e) {
      throw new JsonFormatException(asJsonElement(base, path));
    }
  }

  @SuppressWarnings("UnusedDeclaration")
  public static float asFloatWithDefault(JsonElement base, float defaultValue,
                                         Object... path) throws JsonTypeMismatchException,
      JsonFormatException, JsonInvalidPathException {
    try {
      return Float.parseFloat(requireNonNull(asStringWithDefault(base, Float.toString(defaultValue), path)));
    } catch (NumberFormatException e) {
      throw new JsonFormatException(asJsonElement(base, path));
    }
  }

  public static double asDouble(JsonElement base, Object... path)
      throws JsonException {
    try {
      return Double.parseDouble(requireNonNull(asString(base, path)));
    } catch (NumberFormatException e) {
      throw new JsonFormatException(asJsonElement(base, path));
    }
  }

  public static double asDoubleWithDefault(JsonElement base,
                                           double defaultValue, Object... path) throws JsonTypeMismatchException,
      JsonFormatException, JsonInvalidPathException {
    try {
      return Double.parseDouble(requireNonNull(asStringWithDefault(base, Double.toString(defaultValue), path)));
    } catch (NumberFormatException e) {
      throw new JsonFormatException(asJsonElement(base, path));
    }
  }

  public static Map<JsonElement, List<Object>> buildPathInfo(JsonObject root) {
    Map<JsonElement, List<Object>> ret = new HashMap<>();
    ret.put(root, List.of());
    List<Object> path = new LinkedList<>();
    buildPathInfo(ret, path, root);
    return ret;
  }

  private static void buildPathInfo(Map<JsonElement, List<Object>> map, List<Object> path, JsonElement elem) {
    if (!elem.isJsonNull()) {
      if (elem.isJsonArray()) {
        buildPathInfo(map, path, elem.getAsJsonArray());
      } else if (elem.isJsonObject()) {
        buildPathInfo(map, path, elem.getAsJsonObject());
      }
      map.put(elem, new ArrayList<>(path));
    }
  }

  private static void buildPathInfo(Map<JsonElement, List<Object>> map, List<Object> path, JsonArray arr) {
    int len = arr.size();
    for (int i = 0; i < len; i++) {
      path.add(i);
      buildPathInfo(map, path, arr.get(i));
      path.removeLast();
    }
  }

  private static void buildPathInfo(Map<JsonElement, List<Object>> map, List<Object> path, JsonObject obj) {
    for (Entry<String, JsonElement> ent : obj.entrySet()) {
      String k = ent.getKey();
      JsonElement elem = ent.getValue();
      path.add(k);
      buildPathInfo(map, path, elem);
      path.removeLast();
    }
  }

  private static String jsonpathToString(List<Object> path) {
    StringBuilder buf = new StringBuilder();
    for (Object each : path) {
      if (each instanceof Number) {
        buf.append("[").append(each).append("]");
      } else {
        buf.append(".").append(quoteIfNonAlphanumericalIsContained(each));
      }
    }
    if (buf.isEmpty())
      buf.append('.');
    return buf.toString();
  }

  private static String quoteIfNonAlphanumericalIsContained(Object obj) {
    if (obj instanceof String s) {
      return s.matches("[a-zA-Z_][a-zA-Z0-9_]+") ? s : '"' + escape(s) + '"';
    }
    return Objects.toString(obj);
  }


  /**
   * <a href="https://stackoverflow.com/a/61628600/820227">Answer by Dan in stackoverflow.com</a>
   * escape()
   * Escape a give String to make it safe to be printed or stored.
   *
   * @param s The input String.
   * @return The output String.
   **/
  private static String escape(String s) {
    return s.replace("\\", "\\\\")
        .replace("\t", "\\t")
        .replace("\b", "\\b")
        .replace("\n", "\\n")
        .replace("\r", "\\r")
        .replace("\f", "\\f")
        .replace("\'", "\\'")      // <== not necessary
        .replace("\"", "\\\"");
  }

  public static JsonElement toJson(String str) {
    return JsonParser.parseString(str);
  }

  public static Iterator<String> keyIterator(final JsonObject json) {
    return new Iterator<>() {
      final Iterator<Entry<String, JsonElement>> iEntries = json.entrySet()
          .iterator();

      public boolean hasNext() {
        return iEntries.hasNext();
      }

      public String next() {
        return iEntries.next().getKey();
      }

      public void remove() {
        iEntries.remove();
      }
    };
  }

  public static String formatPath(Object... relPath) {
    StringBuilder ret = new StringBuilder();
    for (Object obj : relPath) {
      if (!ret.isEmpty())
        ret.append("/");
      ret.append(obj);
    }
    return ret.toString();
  }

  public static JsonObject merge(JsonObject left, JsonObject right) throws JsonInvalidPathException {
    return merge(left, right, new LinkedList<>());
  }

  public static JsonObject merge(JsonObject left, JsonObject right, List<Object> relPath) throws JsonInvalidPathException {
    JsonObject ret = new JsonObject();
    for (Entry<String, JsonElement> each : left.entrySet()) {
      if (right.has(each.getKey())) {
        if (each.getValue().isJsonObject() && right.get(each.getKey()).isJsonObject()) {
          ret.add(
              each.getKey(),
              merge(each.getValue().getAsJsonObject(), right.get(each.getKey()).getAsJsonObject())
          );
        } else {
          throw new JsonInvalidPathException(right, relPath.toArray());
        }
      } else {
        ret.add(each.getKey(), each.getValue());
      }
    }
    for (Entry<String, JsonElement> each : right.entrySet()) {
      if (!ret.has(each.getKey())) {
        ret.add(each.getKey(), each.getValue());
      }
    }
    return ret;
  }
}