blob: b77633924f572791e67c5faaa47deea01f533516 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.calcite.runtime;
import org.apache.calcite.linq4j.function.Deterministic;
import org.apache.calcite.sql.SqlJsonConstructorNullClause;
import org.apache.calcite.sql.SqlJsonExistsErrorBehavior;
import org.apache.calcite.sql.SqlJsonQueryEmptyOrErrorBehavior;
import org.apache.calcite.sql.SqlJsonQueryWrapperBehavior;
import org.apache.calcite.sql.SqlJsonValueEmptyOrErrorBehavior;
import org.apache.calcite.util.Util;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.core.PrettyPrinter;
import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.InvalidPathException;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.apache.calcite.config.CalciteSystemProperty.FUNCTION_LEVEL_CACHE_MAX_SIZE;
import static org.apache.calcite.linq4j.Nullness.castNonNull;
import static org.apache.calcite.util.Static.RESOURCE;
import static java.util.Objects.requireNonNull;
/**
* A collection of functions used in JSON processing.
*/
public class JsonFunctions {
private static final Pattern JSON_PATH_BASE =
Pattern.compile("^\\s*(?<mode>strict|lax)\\s+(?<spec>.+)$",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
private static final JacksonJsonProvider JSON_PATH_JSON_PROVIDER =
new JacksonJsonProvider();
private static final MappingProvider JSON_PATH_MAPPING_PROVIDER =
new JacksonMappingProvider();
private static final PrettyPrinter JSON_PRETTY_PRINTER =
new DefaultPrettyPrinter().withObjectIndenter(
DefaultIndenter.SYSTEM_LINEFEED_INSTANCE.withLinefeed("\n"));
private static final String JSON_ROOT_PATH = "$";
private JsonFunctions() {
}
private static boolean isScalarObject(Object obj) {
if (obj instanceof Collection) {
return false;
}
if (obj instanceof Map) {
return false;
}
return true;
}
public static String jsonize(@Nullable Object input) {
return JSON_PATH_JSON_PROVIDER.toJson(input);
}
public static @Nullable Object dejsonize(String input) {
return JSON_PATH_JSON_PROVIDER.parse(input);
}
public static JsonValueContext jsonValueExpression(String input) {
try {
return JsonValueContext.withJavaObj(dejsonize(input));
} catch (Exception e) {
return JsonValueContext.withException(e);
}
}
public static JsonPathContext jsonApiCommonSyntax(String input) {
return jsonApiCommonSyntax(jsonValueExpression(input));
}
public static JsonPathContext jsonApiCommonSyntax(JsonValueContext input) {
return jsonApiCommonSyntax(input, "strict $");
}
public static JsonPathContext jsonApiCommonSyntax(String input, String pathSpec) {
return jsonApiCommonSyntax(jsonValueExpression(input), pathSpec);
}
public static JsonPathContext jsonApiCommonSyntax(JsonValueContext input, String pathSpec) {
PathMode mode;
String pathStr;
try {
Matcher matcher = JSON_PATH_BASE.matcher(pathSpec);
if (!matcher.matches()) {
mode = PathMode.STRICT;
pathStr = pathSpec;
} else {
mode = PathMode.valueOf(castNonNull(matcher.group(1)).toUpperCase(Locale.ROOT));
pathStr = castNonNull(matcher.group(2));
}
DocumentContext ctx;
switch (mode) {
case STRICT:
if (input.hasException()) {
return JsonPathContext.withStrictException(pathSpec, input.exc);
}
ctx =
JsonPath.parse(input.obj(),
Configuration.builder()
.jsonProvider(JSON_PATH_JSON_PROVIDER)
.mappingProvider(JSON_PATH_MAPPING_PROVIDER)
.build());
break;
case LAX:
if (input.hasException()) {
return JsonPathContext.withJavaObj(PathMode.LAX, null);
}
ctx =
JsonPath.parse(input.obj(),
Configuration.builder()
.options(Option.SUPPRESS_EXCEPTIONS)
.jsonProvider(JSON_PATH_JSON_PROVIDER)
.mappingProvider(JSON_PATH_MAPPING_PROVIDER)
.build());
break;
default:
throw RESOURCE.illegalJsonPathModeInPathSpec(mode.toString(), pathSpec).ex();
}
try {
return JsonPathContext.withJavaObj(mode, ctx.read(pathStr));
} catch (Exception e) {
return JsonPathContext.withStrictException(pathSpec, e);
}
} catch (Exception e) {
return JsonPathContext.withUnknownException(e);
}
}
/** State for {@code JSON_EXISTS}, {@code JSON_VALUE}, {@code JSON_QUERY}.
*
* <p>Marked deterministic so that the code generator instantiates one once
* per query, not once per row. */
@Deterministic
public static class StatefulFunction {
private final LoadingCache<String, JsonValueContext> cache =
CacheBuilder.newBuilder()
.maximumSize(FUNCTION_LEVEL_CACHE_MAX_SIZE.value())
.build(CacheLoader.from(JsonFunctions::jsonValueExpression));
public JsonPathContext jsonApiCommonSyntaxWithCache(String input,
String pathSpec) {
return jsonApiCommonSyntax(cache.getUnchecked(input), pathSpec);
}
public @Nullable Boolean jsonExists(String input, String pathSpec) {
return jsonExists(jsonApiCommonSyntaxWithCache(input, pathSpec));
}
public @Nullable Boolean jsonExists(String input, String pathSpec,
SqlJsonExistsErrorBehavior errorBehavior) {
return jsonExists(jsonApiCommonSyntaxWithCache(input, pathSpec),
errorBehavior);
}
public @Nullable Boolean jsonExists(JsonValueContext input,
String pathSpec) {
return jsonExists(jsonApiCommonSyntax(input, pathSpec));
}
public @Nullable Boolean jsonExists(JsonValueContext input, String pathSpec,
SqlJsonExistsErrorBehavior errorBehavior) {
return jsonExists(jsonApiCommonSyntax(input, pathSpec), errorBehavior);
}
public @Nullable Boolean jsonExists(JsonPathContext context) {
return jsonExists(context, SqlJsonExistsErrorBehavior.FALSE);
}
public @Nullable Boolean jsonExists(JsonPathContext context,
SqlJsonExistsErrorBehavior errorBehavior) {
if (context.hasException()) {
switch (errorBehavior) {
case TRUE:
return Boolean.TRUE;
case FALSE:
return Boolean.FALSE;
case ERROR:
throw toUnchecked(context.exc);
case UNKNOWN:
return null;
default:
throw RESOURCE.illegalErrorBehaviorInJsonExistsFunc(
errorBehavior.toString()).ex();
}
} else {
return context.obj != null;
}
}
public @Nullable Object jsonValue(String input,
String pathSpec,
SqlJsonValueEmptyOrErrorBehavior emptyBehavior,
Object defaultValueOnEmpty,
SqlJsonValueEmptyOrErrorBehavior errorBehavior,
Object defaultValueOnError) {
return jsonValue(
jsonApiCommonSyntaxWithCache(input, pathSpec),
emptyBehavior,
defaultValueOnEmpty,
errorBehavior,
defaultValueOnError);
}
public @Nullable Object jsonValue(JsonValueContext input,
String pathSpec,
SqlJsonValueEmptyOrErrorBehavior emptyBehavior,
Object defaultValueOnEmpty,
SqlJsonValueEmptyOrErrorBehavior errorBehavior,
Object defaultValueOnError) {
return jsonValue(
jsonApiCommonSyntax(input, pathSpec),
emptyBehavior,
defaultValueOnEmpty,
errorBehavior,
defaultValueOnError);
}
public @Nullable Object jsonValue(JsonPathContext context,
SqlJsonValueEmptyOrErrorBehavior emptyBehavior,
Object defaultValueOnEmpty,
SqlJsonValueEmptyOrErrorBehavior errorBehavior,
Object defaultValueOnError) {
final Exception exc;
if (context.hasException()) {
exc = context.exc;
} else {
Object value = context.obj;
if (value == null || context.mode == PathMode.LAX
&& !isScalarObject(value)) {
switch (emptyBehavior) {
case ERROR:
throw RESOURCE.emptyResultOfJsonValueFuncNotAllowed().ex();
case NULL:
return null;
case DEFAULT:
return defaultValueOnEmpty;
default:
throw RESOURCE.illegalEmptyBehaviorInJsonValueFunc(
emptyBehavior.toString()).ex();
}
} else if (context.mode == PathMode.STRICT
&& !isScalarObject(value)) {
exc =
RESOURCE.scalarValueRequiredInStrictModeOfJsonValueFunc(
value.toString()).ex();
} else {
return value;
}
}
switch (errorBehavior) {
case ERROR:
throw toUnchecked(exc);
case NULL:
return null;
case DEFAULT:
return defaultValueOnError;
default:
throw RESOURCE.illegalErrorBehaviorInJsonValueFunc(
errorBehavior.toString()).ex();
}
}
public @Nullable Object jsonQuery(
String input,
String pathSpec,
SqlJsonQueryWrapperBehavior wrapperBehavior,
SqlJsonQueryEmptyOrErrorBehavior emptyBehavior,
SqlJsonQueryEmptyOrErrorBehavior errorBehavior,
boolean jsonize) {
return jsonQuery(
jsonApiCommonSyntaxWithCache(input, pathSpec),
wrapperBehavior, emptyBehavior, errorBehavior, jsonize);
}
public @Nullable Object jsonQuery(JsonValueContext input,
String pathSpec,
SqlJsonQueryWrapperBehavior wrapperBehavior,
SqlJsonQueryEmptyOrErrorBehavior emptyBehavior,
SqlJsonQueryEmptyOrErrorBehavior errorBehavior,
boolean jsonize) {
return jsonQuery(
jsonApiCommonSyntax(input, pathSpec),
wrapperBehavior, emptyBehavior, errorBehavior, jsonize);
}
public @Nullable Object jsonQuery(
JsonPathContext context,
SqlJsonQueryWrapperBehavior wrapperBehavior,
SqlJsonQueryEmptyOrErrorBehavior emptyBehavior,
SqlJsonQueryEmptyOrErrorBehavior errorBehavior,
boolean jsonize) {
final Exception exc;
if (context.hasException()) {
exc = context.exc;
} else {
Object value;
if (context.obj == null) {
value = null;
} else {
switch (wrapperBehavior) {
case WITHOUT_ARRAY:
value = context.obj;
break;
case WITH_UNCONDITIONAL_ARRAY:
value = Collections.singletonList(context.obj);
break;
case WITH_CONDITIONAL_ARRAY:
if (context.obj instanceof Collection) {
value = context.obj;
} else {
value = Collections.singletonList(context.obj);
}
break;
default:
throw RESOURCE.illegalWrapperBehaviorInJsonQueryFunc(
wrapperBehavior.toString()).ex();
}
}
if (value == null || context.mode == PathMode.LAX
&& isScalarObject(value)) {
switch (emptyBehavior) {
case ERROR:
throw RESOURCE.emptyResultOfJsonQueryFuncNotAllowed().ex();
case NULL:
return null;
case EMPTY_ARRAY:
return jsonQueryEmptyArray(jsonize);
case EMPTY_OBJECT:
return jsonQueryEmptyObject(jsonize);
default:
throw RESOURCE.illegalEmptyBehaviorInJsonQueryFunc(
emptyBehavior.toString()).ex();
}
} else if (context.mode == PathMode.STRICT && isScalarObject(value)) {
exc =
RESOURCE.arrayOrObjectValueRequiredInStrictModeOfJsonQueryFunc(
value.toString()).ex();
} else {
if (jsonize) {
try {
return jsonize(value);
} catch (Exception e) {
exc = e;
}
} else {
return value;
}
}
}
switch (errorBehavior) {
case ERROR:
throw toUnchecked(exc);
case NULL:
return null;
case EMPTY_ARRAY:
return jsonQueryEmptyArray(jsonize);
case EMPTY_OBJECT:
return jsonQueryEmptyObject(jsonize);
default:
throw RESOURCE.illegalErrorBehaviorInJsonQueryFunc(
errorBehavior.toString()).ex();
}
}
private static Object jsonQueryEmptyArray(boolean jsonize) {
return jsonize ? "[]" : Collections.emptyList();
}
private static String jsonQueryEmptyObject(boolean jsonize) {
if (jsonize) {
return "{}";
} else {
throw RESOURCE.illegalEmptyObjectInJsonQueryFunc().ex();
}
}
}
public static String jsonObject(SqlJsonConstructorNullClause nullClause,
@Nullable Object... kvs) {
assert kvs.length % 2 == 0;
Map<String, @Nullable Object> map = new HashMap<>();
for (int i = 0; i < kvs.length; i += 2) {
String k = (String) kvs[i];
Object v = kvs[i + 1];
if (k == null) {
throw RESOURCE.nullKeyOfJsonObjectNotAllowed().ex();
}
if (v == null) {
if (nullClause == SqlJsonConstructorNullClause.NULL_ON_NULL) {
map.put(k, null);
}
} else {
map.put(k, v);
}
}
return jsonize(map);
}
public static void jsonObjectAggAdd(Map map, String k, @Nullable Object v,
SqlJsonConstructorNullClause nullClause) {
if (k == null) {
throw RESOURCE.nullKeyOfJsonObjectNotAllowed().ex();
}
if (v == null) {
if (nullClause == SqlJsonConstructorNullClause.NULL_ON_NULL) {
map.put(k, null);
}
} else {
map.put(k, v);
}
}
public static String jsonArray(SqlJsonConstructorNullClause nullClause,
@Nullable Object... elements) {
List<@Nullable Object> list = new ArrayList<>();
for (Object element : elements) {
if (element == null) {
if (nullClause == SqlJsonConstructorNullClause.NULL_ON_NULL) {
list.add(null);
}
} else {
list.add(element);
}
}
return jsonize(list);
}
public static void jsonArrayAggAdd(List list, @Nullable Object element,
SqlJsonConstructorNullClause nullClause) {
if (element == null) {
if (nullClause == SqlJsonConstructorNullClause.NULL_ON_NULL) {
list.add(null);
}
} else {
list.add(element);
}
}
public static String jsonPretty(String input) {
return jsonPretty(jsonValueExpression(input));
}
public static String jsonPretty(JsonValueContext input) {
try {
return JSON_PATH_JSON_PROVIDER.getObjectMapper().writer(JSON_PRETTY_PRINTER)
.writeValueAsString(input.obj);
} catch (Exception e) {
throw RESOURCE.exceptionWhileSerializingToJson(Objects.toString(input.obj)).ex(e);
}
}
public static String jsonType(String input) {
return jsonType(jsonValueExpression(input));
}
public static String jsonType(JsonValueContext input) {
final String result;
final Object val = input.obj;
try {
if (val instanceof Integer) {
result = "INTEGER";
} else if (val instanceof String) {
result = "STRING";
} else if (val instanceof Float) {
result = "FLOAT";
} else if (val instanceof Double) {
result = "DOUBLE";
} else if (val instanceof Long) {
result = "LONG";
} else if (val instanceof Boolean) {
result = "BOOLEAN";
} else if (val instanceof Date) {
result = "DATE";
} else if (val instanceof Map) {
result = "OBJECT";
} else if (val instanceof Collection) {
result = "ARRAY";
} else if (val == null) {
result = "NULL";
} else {
throw RESOURCE.invalidInputForJsonType(val.toString()).ex();
}
return result;
} catch (Exception ex) {
throw RESOURCE.invalidInputForJsonType(val.toString()).ex(ex);
}
}
public static @Nullable Integer jsonDepth(String input) {
return jsonDepth(jsonValueExpression(input));
}
public static @Nullable Integer jsonDepth(JsonValueContext input) {
final Integer result;
final Object o = input.obj;
try {
if (o == null) {
result = null;
} else {
result = calculateDepth(o);
}
return result;
} catch (Exception ex) {
throw RESOURCE.invalidInputForJsonDepth(o.toString()).ex(ex);
}
}
@SuppressWarnings("JdkObsolete")
private static Integer calculateDepth(Object o) {
if (isScalarObject(o)) {
return 1;
}
// Note: even even though LinkedList implements Queue, it supports null values
//
Queue<Object> q = new LinkedList<>();
int depth = 0;
q.add(o);
while (!q.isEmpty()) {
int size = q.size();
for (int i = 0; i < size; ++i) {
Object obj = q.poll();
if (obj instanceof Map) {
for (Object value : ((LinkedHashMap) obj).values()) {
q.add(value);
}
} else if (obj instanceof Collection) {
for (Object value : (Collection) obj) {
q.add(value);
}
}
}
++depth;
}
return depth;
}
public static @Nullable Integer jsonLength(String input) {
return jsonLength(jsonApiCommonSyntax(input));
}
public static @Nullable Integer jsonLength(JsonValueContext input) {
return jsonLength(jsonApiCommonSyntax(input));
}
public static @Nullable Integer jsonLength(String input, String pathSpec) {
return jsonLength(jsonApiCommonSyntax(input, pathSpec));
}
public static @Nullable Integer jsonLength(JsonValueContext input, String pathSpec) {
return jsonLength(jsonApiCommonSyntax(input, pathSpec));
}
public static @Nullable Integer jsonLength(JsonPathContext context) {
final Integer result;
final Object value;
try {
if (context.hasException()) {
throw toUnchecked(context.exc);
}
value = context.obj;
if (value == null) {
result = null;
} else {
if (value instanceof Collection) {
result = ((Collection) value).size();
} else if (value instanceof Map) {
result = ((LinkedHashMap) value).size();
} else if (isScalarObject(value)) {
result = 1;
} else {
result = 0;
}
}
} catch (Exception ex) {
throw RESOURCE.invalidInputForJsonLength(
context.toString()).ex(ex);
}
return result;
}
public static String jsonKeys(String input) {
return jsonKeys(jsonApiCommonSyntax(input));
}
public static String jsonKeys(JsonValueContext input) {
return jsonKeys(jsonApiCommonSyntax(input));
}
public static String jsonKeys(String input, String pathSpec) {
return jsonKeys(jsonApiCommonSyntax(input, pathSpec));
}
public static String jsonKeys(JsonValueContext input, String pathSpec) {
return jsonKeys(jsonApiCommonSyntax(input, pathSpec));
}
public static String jsonKeys(JsonPathContext context) {
List<String> list = new ArrayList<>();
final Object value;
try {
if (context.hasException()) {
throw toUnchecked(context.exc);
}
value = context.obj;
if ((value == null) || (value instanceof Collection)
|| isScalarObject(value)) {
list = null;
} else if (value instanceof Map) {
for (Object key : ((LinkedHashMap) value).keySet()) {
list.add(key.toString());
}
}
} catch (Exception ex) {
throw RESOURCE.invalidInputForJsonKeys(
context.toString()).ex(ex);
}
return jsonize(list);
}
public static String jsonRemove(String input, String... pathSpecs) {
return jsonRemove(jsonValueExpression(input), pathSpecs);
}
public static String jsonRemove(JsonValueContext input, String... pathSpecs) {
try {
DocumentContext ctx =
JsonPath.parse(input.obj(),
Configuration.builder()
.options(Option.SUPPRESS_EXCEPTIONS)
.jsonProvider(JSON_PATH_JSON_PROVIDER)
.mappingProvider(JSON_PATH_MAPPING_PROVIDER)
.build());
for (String pathSpec : pathSpecs) {
if ((pathSpec != null) && (ctx.read(pathSpec) != null)) {
ctx.delete(pathSpec);
}
}
return ctx.jsonString();
} catch (Exception ex) {
throw RESOURCE.invalidInputForJsonRemove(
input.toString(), Arrays.toString(pathSpecs)).ex(ex);
}
}
public static Integer jsonStorageSize(String input) {
return jsonStorageSize(jsonValueExpression(input));
}
public static Integer jsonStorageSize(JsonValueContext input) {
try {
return JSON_PATH_JSON_PROVIDER.getObjectMapper()
.writeValueAsBytes(input.obj).length;
} catch (Exception e) {
throw RESOURCE.invalidInputForJsonStorageSize(Objects.toString(input.obj)).ex(e);
}
}
private static String jsonModify(JsonValueContext jsonDoc, JsonModifyMode type, Object... kvs) {
int step = type == JsonModifyMode.REMOVE ? 1 : 2;
assert kvs.length % step == 0;
String result = null;
DocumentContext ctx =
JsonPath.parse(jsonDoc.obj(),
Configuration.builder()
.options(Option.SUPPRESS_EXCEPTIONS)
.jsonProvider(JSON_PATH_JSON_PROVIDER)
.mappingProvider(JSON_PATH_MAPPING_PROVIDER)
.build());
for (int i = 0; i < kvs.length; i += step) {
String k = (String) kvs[i];
Object v = kvs[i + step - 1];
if (!(JsonPath.isPathDefinite(k) && k.contains(JSON_ROOT_PATH))) {
throw RESOURCE.validationError(k).ex();
}
switch (type) {
case REPLACE:
if (k.equals(JSON_ROOT_PATH)) {
result = jsonize(v);
} else {
if (ctx.read(k) != null) {
ctx.set(k, v);
}
}
break;
case INSERT:
if (!k.equals(JSON_ROOT_PATH) && ctx.read(k) == null) {
insertToJson(ctx, k, v);
}
break;
case SET:
if (k.equals(JSON_ROOT_PATH)) {
result = jsonize(v);
} else {
if (ctx.read(k) != null) {
ctx.set(k, v);
} else {
insertToJson(ctx, k, v);
}
}
break;
case REMOVE:
if (ctx.read(k) != null) {
ctx.delete(k);
}
break;
}
}
return result == null || result.isEmpty() ? ctx.jsonString() : result;
}
private static void insertToJson(DocumentContext ctx, String path, Object value) {
final String parentPath;
final String key;
// The following paragraph of logic is mainly used to obtain the
// parent node path of path and the key value that should be
// inserted when the preant Path is map.
// eg: path is $.a ,parentPath is $
// eg: path is $.a[1] ,parentPath is $.a
Integer dotIndex = path.lastIndexOf(".");
Integer leftBracketIndex = path.lastIndexOf("[");
if (dotIndex.equals(-1) && leftBracketIndex.equals(-1)) {
parentPath = path;
key = path;
} else if (!dotIndex.equals(-1) && leftBracketIndex.equals(-1)) {
parentPath = path.substring(0, dotIndex);
key = path.substring(dotIndex + 1);
} else if (dotIndex.equals(-1) && !leftBracketIndex.equals(-1)) {
parentPath = path.substring(0, leftBracketIndex);
key = path.substring(leftBracketIndex + 1);
} else {
int position = dotIndex > leftBracketIndex ? dotIndex : leftBracketIndex;
parentPath = path.substring(0, position);
key = path.substring(position);
}
Object obj = ctx.read(parentPath);
if (obj instanceof Map) {
ctx.put(parentPath, key, value.toString());
} else if (obj instanceof Collection) {
ctx.add(parentPath, value);
}
}
public static String jsonReplace(String jsonDoc, Object... kvs) {
return jsonReplace(jsonValueExpression(jsonDoc), kvs);
}
public static String jsonReplace(JsonValueContext input, Object... kvs) {
try {
return jsonModify(input, JsonModifyMode.REPLACE, kvs);
} catch (Exception ex) {
throw RESOURCE.invalidInputForJsonReplace(input.toString(), jsonize(kvs)).ex();
}
}
public static String jsonInsert(String jsonDoc, Object... kvs) {
return jsonInsert(jsonValueExpression(jsonDoc), kvs);
}
public static String jsonInsert(JsonValueContext input, Object... kvs) {
try {
return jsonModify(input, JsonModifyMode.INSERT, kvs);
} catch (Exception ex) {
throw RESOURCE.invalidInputForJsonInsert(input.toString(), jsonize(kvs)).ex();
}
}
public static String jsonSet(String jsonDoc, Object... kvs) {
return jsonSet(jsonValueExpression(jsonDoc), kvs);
}
public static String jsonSet(JsonValueContext input, Object... kvs) {
try {
return jsonModify(input, JsonModifyMode.SET, kvs);
} catch (Exception ex) {
throw RESOURCE.invalidInputForJsonSet(input.toString(), jsonize(kvs)).ex();
}
}
public static boolean isJsonValue(String input) {
try {
dejsonize(input);
return true;
} catch (Exception e) {
return false;
}
}
public static boolean isJsonObject(String input) {
try {
Object o = dejsonize(input);
return o instanceof Map;
} catch (Exception e) {
return false;
}
}
public static boolean isJsonArray(String input) {
try {
Object o = dejsonize(input);
return o instanceof Collection;
} catch (Exception e) {
return false;
}
}
public static boolean isJsonScalar(String input) {
try {
Object o = dejsonize(input);
return !(o instanceof Map) && !(o instanceof Collection);
} catch (Exception e) {
return false;
}
}
private static RuntimeException toUnchecked(Exception e) {
return Util.toUnchecked(e);
}
/**
* Returned path context of JsonApiCommonSyntax, public for testing.
*/
public static class JsonPathContext {
public final PathMode mode;
public final @Nullable Object obj;
public final @Nullable Exception exc;
private JsonPathContext(@Nullable Object obj, @Nullable Exception exc) {
this(PathMode.NONE, obj, exc);
}
private JsonPathContext(PathMode mode, @Nullable Object obj, @Nullable Exception exc) {
assert obj == null || exc == null;
this.mode = mode;
this.obj = obj;
this.exc = exc;
}
@EnsuresNonNullIf(expression = "exc", result = true)
public boolean hasException() {
return exc != null;
}
public static JsonPathContext withUnknownException(Exception exc) {
return new JsonPathContext(PathMode.UNKNOWN, null, exc);
}
public static JsonPathContext withStrictException(Exception exc) {
return new JsonPathContext(PathMode.STRICT, null, exc);
}
public static JsonPathContext withStrictException(String pathSpec, Exception exc) {
if (exc.getClass() == InvalidPathException.class) {
exc = RESOURCE.illegalJsonPathSpec(pathSpec).ex();
}
return withStrictException(exc);
}
public static JsonPathContext withJavaObj(PathMode mode, @Nullable Object obj) {
if (mode == PathMode.UNKNOWN) {
throw RESOURCE.illegalJsonPathMode(mode.toString()).ex();
}
if (mode == PathMode.STRICT && obj == null) {
throw RESOURCE.strictPathModeRequiresNonEmptyValue().ex();
}
return new JsonPathContext(mode, obj, null);
}
@Override public String toString() {
return "JsonPathContext{"
+ "mode=" + mode
+ ", obj=" + obj
+ ", exc=" + exc
+ '}';
}
}
/**
* The Java output of {@link org.apache.calcite.sql.fun.SqlJsonValueExpressionOperator}.
*/
public static class JsonValueContext {
@JsonValue
public final @Nullable Object obj;
public final @Nullable Exception exc;
private JsonValueContext(@Nullable Object obj, @Nullable Exception exc) {
assert obj == null || exc == null;
this.obj = obj;
this.exc = exc;
}
public static JsonValueContext withJavaObj(@Nullable Object obj) {
return new JsonValueContext(obj, null);
}
public static JsonValueContext withException(Exception exc) {
return new JsonValueContext(null, exc);
}
Object obj() {
return requireNonNull(obj, "json object must not be null");
}
@EnsuresNonNullIf(expression = "exc", result = true)
public boolean hasException() {
return exc != null;
}
@Override public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
JsonValueContext jsonValueContext = (JsonValueContext) o;
return Objects.equals(obj, jsonValueContext.obj);
}
@Override public int hashCode() {
return Objects.hash(obj);
}
@Override public String toString() {
return jsonize(obj);
}
}
/**
* Path spec has two different modes: lax mode and strict mode.
* Lax mode suppresses any thrown exception and returns null,
* whereas strict mode throws exceptions.
*/
public enum PathMode {
LAX,
STRICT,
UNKNOWN,
NONE
}
/**
* Used in the JsonModify function.
*/
public enum JsonModifyMode {
REPLACE,
INSERT,
SET,
REMOVE
}
}