From 57390e8a3fc6a94cea29195416be0dcbd9db7f70 Mon Sep 17 00:00:00 2001 From: Tihomir Mateev Date: Wed, 28 Aug 2024 14:40:08 +0300 Subject: [PATCH] Handle null values --- .../lettuce/core/json/DefaultJsonParser.java | 44 +++++++------------ .../lettuce/core/json/DelegateJsonArray.java | 5 +++ .../lettuce/core/json/DelegateJsonObject.java | 5 +++ .../lettuce/core/json/DelegateJsonValue.java | 4 ++ .../java/io/lettuce/core/json/JsonValue.java | 5 +++ .../lettuce/core/json/RedisJsonException.java | 37 ++++++++++++++++ .../core/json/UnproccessedJsonValue.java | 6 +++ .../core/json/RedisJsonIntegrationTests.java | 20 ++++++++- src/test/resources/bike-inventory.json | 1 + 9 files changed, 98 insertions(+), 29 deletions(-) create mode 100644 src/main/java/io/lettuce/core/json/RedisJsonException.java diff --git a/src/main/java/io/lettuce/core/json/DefaultJsonParser.java b/src/main/java/io/lettuce/core/json/DefaultJsonParser.java index b1ddf76506..82f62539f6 100644 --- a/src/main/java/io/lettuce/core/json/DefaultJsonParser.java +++ b/src/main/java/io/lettuce/core/json/DefaultJsonParser.java @@ -3,23 +3,11 @@ * All rights reserved. * * Licensed under the MIT License. - * - * This file contains contributions from third-party contributors - * licensed 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 - * - * https://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 io.lettuce.core.json; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.lettuce.core.codec.RedisCodec; @@ -69,15 +57,10 @@ private JsonValue parse(String value) { ObjectMapper mapper = new ObjectMapper(); try { JsonNode root = mapper.readTree(value); - - if (root.isObject()) { - return new DelegateJsonObject<>(root, codec); - } else if (root.isArray()) { - return new DelegateJsonArray<>(root, codec); - } - return new DelegateJsonValue<>(root, codec); - } catch (IOException e) { - throw new RuntimeException(e); + return wrap(root); + } catch (JsonProcessingException e) { + throw new RedisJsonException( + "Failed to process the provided value as JSON: " + String.format("%.50s", value) + "...", e); } } @@ -89,15 +72,20 @@ public JsonValue parse(ByteBuffer byteBuffer) { byteBuffer.get(bytes); JsonNode root = mapper.readTree(bytes); - if (root.isObject()) { - return new DelegateJsonObject<>(root, codec); - } else if (root.isArray()) { - return new DelegateJsonArray<>(root, codec); - } - return new DelegateJsonValue<>(root, codec); + return wrap(root); } catch (IOException e) { throw new RuntimeException(e); } } + private JsonValue wrap(JsonNode root) { + if (root.isObject()) { + return new DelegateJsonObject<>(root, codec); + } else if (root.isArray()) { + return new DelegateJsonArray<>(root, codec); + } + + return new DelegateJsonValue<>(root, codec); + } + } diff --git a/src/main/java/io/lettuce/core/json/DelegateJsonArray.java b/src/main/java/io/lettuce/core/json/DelegateJsonArray.java index 38a97cb6e4..6229566f6e 100644 --- a/src/main/java/io/lettuce/core/json/DelegateJsonArray.java +++ b/src/main/java/io/lettuce/core/json/DelegateJsonArray.java @@ -145,4 +145,9 @@ public Number asNumber() { throw new UnsupportedOperationException("The JSON value is not a number"); } + @Override + public boolean isNull() { + return false; + } + } diff --git a/src/main/java/io/lettuce/core/json/DelegateJsonObject.java b/src/main/java/io/lettuce/core/json/DelegateJsonObject.java index 20efd669db..2d017cfb31 100644 --- a/src/main/java/io/lettuce/core/json/DelegateJsonObject.java +++ b/src/main/java/io/lettuce/core/json/DelegateJsonObject.java @@ -102,4 +102,9 @@ public Number asNumber() { throw new UnsupportedOperationException("The JSON value is not a number"); } + @Override + public boolean isNull() { + return false; + } + } diff --git a/src/main/java/io/lettuce/core/json/DelegateJsonValue.java b/src/main/java/io/lettuce/core/json/DelegateJsonValue.java index 3aaec40a82..9b878d5c6e 100644 --- a/src/main/java/io/lettuce/core/json/DelegateJsonValue.java +++ b/src/main/java/io/lettuce/core/json/DelegateJsonValue.java @@ -91,6 +91,10 @@ public boolean isNumber() { return node.isNumber(); } + public boolean isNull() { + return node.isNull(); + } + @Override public Number asNumber() { if (node.isInt()) { diff --git a/src/main/java/io/lettuce/core/json/JsonValue.java b/src/main/java/io/lettuce/core/json/JsonValue.java index f96ddc212b..4e0395e6b2 100644 --- a/src/main/java/io/lettuce/core/json/JsonValue.java +++ b/src/main/java/io/lettuce/core/json/JsonValue.java @@ -99,4 +99,9 @@ public interface JsonValue { */ Number asNumber(); + /** + * @return {@code true} if this {@link JsonValue} represents the value of null + */ + boolean isNull(); + } diff --git a/src/main/java/io/lettuce/core/json/RedisJsonException.java b/src/main/java/io/lettuce/core/json/RedisJsonException.java new file mode 100644 index 0000000000..57759f8bfc --- /dev/null +++ b/src/main/java/io/lettuce/core/json/RedisJsonException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + * + * This file contains contributions from third-party contributors + * licensed 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 + * + * https://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 io.lettuce.core.json; + +public class RedisJsonException extends RuntimeException { + + public RedisJsonException(String message) { + super(message); + } + + public RedisJsonException(String message, Throwable cause) { + super(message, cause); + } + + public RedisJsonException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java b/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java index ea100dce5f..67e33355f6 100644 --- a/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java +++ b/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java @@ -112,6 +112,12 @@ public Number asNumber() { return jsonValue.asNumber(); } + @Override + public boolean isNull() { + lazilyDeserialize(); + return jsonValue.isNull(); + } + private void lazilyDeserialize() { if (deserialized) { return; diff --git a/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java b/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java index a21ab3398b..118cf1a115 100644 --- a/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java +++ b/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java @@ -174,6 +174,7 @@ void jsonGet(String path) { assertThat(value.get(0).asJsonArray().asList().get(0).isString()).isTrue(); assertThat(value.get(0).asJsonArray().asList().get(0).asString()).isEqualTo("Phoebe"); assertThat(value.get(0).asJsonArray().asList().get(1).isString()).isTrue(); + assertThat(value.get(0).asJsonArray().asList().get(1).isNull()).isFalse(); assertThat(value.get(0).asJsonArray().asList().get(1).asString()).isEqualTo("Quaoar"); } else { assertThat(value.get(0).toValue()).isEqualTo("\"Phoebe\""); @@ -184,6 +185,23 @@ void jsonGet(String path) { } } + @Test + void jsonGetNull() { + JsonPath myPath = JsonPath.of("$..inventory.owner"); + + // Verify codec parsing + List> value = redis.jsonGet(BIKES_INVENTORY, JsonGetArgs.Builder.none(), myPath); + assertThat(value).hasSize(1); + + assertThat(value.get(0).toValue()).isEqualTo("[null]"); + + // Verify array parsing + assertThat(value.get(0).isJsonArray()).isTrue(); + assertThat(value.get(0).asJsonArray().size()).isEqualTo(1); + assertThat(value.get(0).asJsonArray().asList().get(0).toValue()).isEqualTo("null"); + assertThat(value.get(0).asJsonArray().asList().get(0).isNull()).isTrue(); + } + @ParameterizedTest(name = "With {0} as path") @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) void jsonMerge(String path) { @@ -355,7 +373,7 @@ void jsonSet(String path) { JsonObject bikeSpecs = parser.createEmptyJsonObject(); JsonArray bikeColors = parser.createEmptyJsonArray(); - bikeSpecs.put("material", parser.createJsonValue("\"composite\"")); + bikeSpecs.put("material", parser.createJsonValue("null")); bikeSpecs.put("weight", parser.createJsonValue("11")); bikeColors.add(parser.createJsonValue("\"yellow\"")); diff --git a/src/test/resources/bike-inventory.json b/src/test/resources/bike-inventory.json index c316c626dd..a69a073ee3 100644 --- a/src/test/resources/bike-inventory.json +++ b/src/test/resources/bike-inventory.json @@ -1,6 +1,7 @@ { "inventory": { "complete": false, + "owner": null, "mountain_bikes": [ { "id": "bike:1",