diff --git a/build.gradle b/build.gradle index 941b9d7..0dc48d6 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ plugins { id 'signing' } -def jarVersion = "2.21.0" +def jarVersion = "3.0.0" group = 'io.nats' def isMerge = System.getenv("BUILD_EVENT") == "push" diff --git a/src/main/java/io/nats/json/ArrayBuilder.java b/src/main/java/io/nats/json/ArrayBuilder.java new file mode 100644 index 0000000..17b5c33 --- /dev/null +++ b/src/main/java/io/nats/json/ArrayBuilder.java @@ -0,0 +1,52 @@ +// Copyright 2025 The NATS Authors +// 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: +// +// 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 io.nats.json; + +import java.util.ArrayList; +import java.util.Collection; + +public class ArrayBuilder implements JsonSerializable { + + public final JsonValue jv = new JsonValue(new ArrayList<>()); + + public static ArrayBuilder instance() { + return new ArrayBuilder(); + } + + public ArrayBuilder add(Object o) { + //noinspection DataFlowIssue // NO ISSUE, WE KNOW jv.array is NOT NULL + jv.array.add(JsonValue.instance(o)); + return this; + } + + public ArrayBuilder addItems(Collection c) { + if (c != null) { + for (Object o : c) { + //noinspection DataFlowIssue // NO ISSUE, WE KNOW jv.array is NOT NULL + jv.array.add(JsonValue.instance(o)); + } + } + return this; + } + + @Override + public String toJson() { + return jv.toJson(); + } + + @Override + public JsonValue toJsonValue() { + return jv; + } +} diff --git a/src/main/java/io/nats/json/DateTimeUtils.java b/src/main/java/io/nats/json/DateTimeUtils.java index 5efc416..91e8e3f 100644 --- a/src/main/java/io/nats/json/DateTimeUtils.java +++ b/src/main/java/io/nats/json/DateTimeUtils.java @@ -1,4 +1,4 @@ -// Copyright 2020-2024 The NATS Authors +// Copyright 2020-2025 The NATS Authors // 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: diff --git a/src/main/java/io/nats/json/Encoding.java b/src/main/java/io/nats/json/Encoding.java index 3288a06..2b21b17 100644 --- a/src/main/java/io/nats/json/Encoding.java +++ b/src/main/java/io/nats/json/Encoding.java @@ -1,4 +1,4 @@ -// Copyright 2020-2024 The NATS Authors +// Copyright 2020-2025 The NATS Authors // 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: @@ -13,11 +13,12 @@ package io.nats.json; +import org.apache.commons.codec.binary.Base64; + import java.net.URLDecoder; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import org.apache.commons.codec.binary.Base64; - /** * Utilities for encoding, i.e., Base64, URI and JSON */ @@ -52,6 +53,16 @@ public static String base64BasicEncodeToString(String input) { return Base64.encodeBase64String(input.getBytes(StandardCharsets.UTF_8)); } + /** + * base64 url encode a byte array to a byte array + * @param input the input byte array to encode + * @param charset the charset of the input string + * @return the encoded byte array + */ + public static String base64BasicEncodeToString(String input, Charset charset) { + return Base64.encodeBase64String(input.getBytes(charset)); + } + /** * base64 url encode a byte array to a byte array * @param input the input byte array to encode @@ -79,6 +90,16 @@ public static String base64UrlEncodeToString(String input) { return Base64.encodeBase64URLSafeString(input.getBytes(StandardCharsets.UTF_8)); } + /** + * base64 url encode a byte array to a byte array + * @param input the input byte array to encode + * @param charset the charset of the input string + * @return the encoded byte array + */ + public static String base64UrlEncodeToString(String input, Charset charset) { + return Base64.encodeBase64URLSafeString(input.getBytes(charset)); + } + /** * base64 decode a byte array * @param input the input byte array to decode @@ -103,7 +124,17 @@ public static byte[] base64BasicDecode(String input) { * @return the decoded string */ public static String base64BasicDecodeToString(String input) { - return new String(Base64.decodeBase64(input)); + return new String(Base64.decodeBase64(input), StandardCharsets.UTF_8); + } + + /** + * base64 decode a base64 encoded string + * @param input the input string to decode + * @param charset the charset to use when decoding the decoded bytes to string + * @return the decoded string + */ + public static String base64BasicDecodeToString(String input, Charset charset) { + return new String(Base64.decodeBase64(input), charset); } /** @@ -115,6 +146,15 @@ public static byte[] base64UrlDecode(byte[] input) { return Base64.decodeBase64(input); } + /** + * base64 url decode a byte array + * @param input the input byte array to decode + * @return the decoded byte array + */ + public static String base64UrlDecodeToString(byte[] input) { + return new String(Base64.decodeBase64(input)); + } + /** * base64 url decode a base64 url encoded string * @param input the input string to decode diff --git a/src/main/java/io/nats/json/JsonParser.java b/src/main/java/io/nats/json/JsonParser.java index 628a185..82c441e 100644 --- a/src/main/java/io/nats/json/JsonParser.java +++ b/src/main/java/io/nats/json/JsonParser.java @@ -1,4 +1,4 @@ -// Copyright 2023-2024 The NATS Authors +// Copyright 2023-2025 The NATS Authors // 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: @@ -135,14 +135,7 @@ public JsonParser(char[] json, Option... options) { public JsonParser(char[] json, int startIndex, Option... options) { this.json = json; - boolean kn = false; - for (Option o : options) { - if (o == Option.KEEP_NULLS) { - kn = true; - break; // b/c only option currently - } - } - keepNulls = kn; + keepNulls = options != null && options.length > 0; // KEEP_NULLS is currently the only option len = json == null ? 0 : json.length; idx = startIndex; @@ -311,7 +304,7 @@ private char peekToken() { return next; } - // next string assumes you have already seen the starting quote + // nextString() assumes you have already seen the starting quote private String nextString() throws JsonParseException { StringBuilder sb = new StringBuilder(); while (true) { diff --git a/src/main/java/io/nats/json/JsonSerializable.java b/src/main/java/io/nats/json/JsonSerializable.java index bedf0aa..cf39122 100644 --- a/src/main/java/io/nats/json/JsonSerializable.java +++ b/src/main/java/io/nats/json/JsonSerializable.java @@ -1,4 +1,4 @@ -// Copyright 2021-2024 The NATS Authors +// Copyright 2021-2025 The NATS Authors // 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: @@ -15,13 +15,32 @@ import java.nio.charset.StandardCharsets; +/** + * Interface for objects that can automatically render as JSON + */ public interface JsonSerializable { + + /** + * Get the String version of the JSON object + * @return the string + */ String toJson(); + /** + * Get the byte[] version of the JSON object + * The built-in default implementation uses the toJson() and converts it to a string. + * @return the byte array + */ default byte[] serialize() { return toJson().getBytes(StandardCharsets.UTF_8); } + /** + * Get the JsonValue version of the JSON object + * The built-in default implementation uses the toJson() and parse it to a JsonValue. + * It assumes that you have valid JSON + * @return the JsonValue + */ default JsonValue toJsonValue() { return JsonParser.parseUnchecked(toJson()); } diff --git a/src/main/java/io/nats/json/JsonValue.java b/src/main/java/io/nats/json/JsonValue.java index 04a4f52..7061cd1 100644 --- a/src/main/java/io/nats/json/JsonValue.java +++ b/src/main/java/io/nats/json/JsonValue.java @@ -1,4 +1,4 @@ -// Copyright 2023-2024 The NATS Authors +// Copyright 2023-2025 The NATS Authors // 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: @@ -13,21 +13,19 @@ package io.nats.json; -import static io.nats.json.Encoding.jsonEncode; -import static io.nats.json.JsonWriteUtils.addField; - import java.math.BigDecimal; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.time.Duration; +import java.util.*; + +import static io.nats.json.Encoding.jsonEncode; +import static io.nats.json.JsonWriteUtils.addField; public class JsonValue implements JsonSerializable { + /** + * Possible types of the underlying value + */ public enum Type { STRING, BOOL, INTEGER, LONG, DOUBLE, FLOAT, BIG_DECIMAL, BIG_INTEGER, MAP, ARRAY, NULL; } @@ -36,11 +34,11 @@ public enum Type { private static final char COMMA = ','; private static final String NULL_STR = "null"; - public static final JsonValue NULL = new JsonValue(); + public static final JsonValue NULL = new JsonValue(Type.NULL); public static final JsonValue TRUE = new JsonValue(true); public static final JsonValue FALSE = new JsonValue(false); - public static final JsonValue EMPTY_MAP = new JsonValue(Collections.unmodifiableMap(new HashMap<>())); - public static final JsonValue EMPTY_ARRAY = new JsonValue(Collections.unmodifiableList(new ArrayList<>())); + public static final JsonValue EMPTY_MAP = new JsonValue(Type.MAP); + public static final JsonValue EMPTY_ARRAY = new JsonValue(Type.ARRAY); /** * The underlying string @@ -62,8 +60,40 @@ public enum Type { public final List mapOrder; - public JsonValue() { - this(null, null, null, null, null, null, null, null, null, null); + public static JsonValue instance(Object o) { + return switch (o) { + case null -> JsonValue.NULL; + case String string -> new JsonValue(string); + case JsonValue jsonValue -> jsonValue; + case JsonSerializable jsonSerializable -> jsonSerializable.toJsonValue(); + case Boolean b -> new JsonValue(b); + case Integer i -> new JsonValue(i); + case Long l -> new JsonValue(l); + case Double d -> new JsonValue(d); + case Float v -> new JsonValue(v); + case BigDecimal bigDecimal -> new JsonValue(bigDecimal); + case BigInteger bigInteger -> new JsonValue(bigInteger); + case Collection list -> _instance(list); + case Map map -> _instance(map); + case Duration dur -> new JsonValue(dur.toNanos()); + default -> new JsonValue(o.toString()); + }; + } + + private static JsonValue _instance(Collection list) { + List jv = new ArrayList<>(); + for (Object o : list) { + jv.add(JsonValue.instance(o)); + } + return new JsonValue(jv); + } + + private static JsonValue _instance(Map map) { + Map jv = new HashMap<>(); + for(Map.Entry entry : map.entrySet()) { + jv.put(entry.getKey().toString(), JsonValue.instance(entry.getValue())); + } + return new JsonValue(jv); } public JsonValue(String string) { @@ -106,7 +136,7 @@ public JsonValue(Map map) { this(null, null, null, null, null, null, null, null, map, null); } - public JsonValue(List list) { + public JsonValue(Collection list) { this(null, null, null, null, null, null, null, null, null, list); } @@ -114,10 +144,17 @@ public JsonValue(JsonValue[] values) { this(null, null, null, null, null, null, null, null, null, values == null ? null : Arrays.asList(values)); } - private JsonValue(String string, Boolean bool, Integer i, Long l, Double d, Float f, BigDecimal bd, BigInteger bi, Map map, List array) { + private JsonValue(String string, Boolean bool, Integer i, Long l, Double d, Float f, BigDecimal bd, BigInteger bi, + Map map, Collection array) + { this.map = map; mapOrder = new ArrayList<>(); - this.array = array; + if (array == null) { + this.array = null; + } + else { + this.array = new ArrayList<>(array); + } this.string = string; this.bool = bool; this.i = i; @@ -181,6 +218,41 @@ else if (array != null) { } } + /** + * Special internal constructor for empty and null + * @param type the type; + */ + private JsonValue(Type type) { + this.type = type; + + string = null; + bool = null; + i = null; + l = null; + d = null; + f = null; + bd = null; + bi = null; + number = null; + mapOrder = new ArrayList<>(); + + if (type == Type.MAP) { + map = Collections.unmodifiableMap(new HashMap<>()); + array = null; + object = map; + } + else if (type == Type.ARRAY) { + map = null; + array = Collections.unmodifiableList(new ArrayList<>()); + object = array; + } + else { // Type.NULL + map = null; + array = null; + object = null; + } + } + public String toString(Class c) { return toString(c.getSimpleName()); } @@ -201,19 +273,19 @@ public JsonValue toJsonValue() { @Override public String toJson() { - switch (type) { - case STRING: return valueString(string); - case BOOL: return valueString(bool); - case MAP: return valueString(map); - case ARRAY: return valueString(array); - case INTEGER: return i.toString(); - case LONG: return l.toString(); - case DOUBLE: return d.toString(); - case FLOAT: return f.toString(); - case BIG_DECIMAL: return bd.toString(); - case BIG_INTEGER: return bi.toString(); - default: return NULL_STR; - } + return switch (type) { + case STRING -> valueString(string); + case BOOL -> valueString(bool); + case MAP -> valueString(map); + case ARRAY -> valueString(array); + case INTEGER -> i.toString(); + case LONG -> l.toString(); + case DOUBLE -> d.toString(); + case FLOAT -> f.toString(); + case BIG_DECIMAL -> bd.toString(); + case BIG_INTEGER -> bi.toString(); + default -> NULL_STR; + }; } private String valueString(String s) { diff --git a/src/main/java/io/nats/json/JsonValueUtils.java b/src/main/java/io/nats/json/JsonValueUtils.java index 93660c9..35ec51e 100644 --- a/src/main/java/io/nats/json/JsonValueUtils.java +++ b/src/main/java/io/nats/json/JsonValueUtils.java @@ -1,4 +1,4 @@ -// Copyright 2023-2024 The NATS Authors +// Copyright 2023-2025 The NATS Authors // 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: @@ -13,86 +13,98 @@ package io.nats.json; -import static io.nats.json.JsonValue.EMPTY_ARRAY; -import static io.nats.json.JsonValue.EMPTY_MAP; -import static io.nats.json.JsonValue.Type; - -import java.math.BigDecimal; -import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.Function; +import static io.nats.json.JsonValue.*; + /** - * Internal json value helpers. + * Utilities around JsonValue. */ public abstract class JsonValueUtils { private JsonValueUtils() {} /* ensures cannot be constructed */ + /** + * Interface allowing the ability to generically extract a value + * @param the output type + */ public interface JsonValueSupplier { T get(JsonValue v); } - public static T read(JsonValue jsonValue, String key, JsonValueSupplier valueSupplier) { - JsonValue v = jsonValue == null || jsonValue.map == null ? null : jsonValue.map.get(key); - return valueSupplier.get(v); - } - - public static JsonValue readValue(JsonValue jsonValue, String key) { - return read(jsonValue, key, v -> v); - } - - public static JsonValue readObject(JsonValue jsonValue, String key) { - return read(jsonValue, key, v -> v == null ? EMPTY_MAP : v); - } - - public static List readArray(JsonValue jsonValue, String key) { - return read(jsonValue, key, v -> v == null ? EMPTY_ARRAY.array : v.array); - } - - public static Map readStringStringMap(JsonValue jv, String key) { - JsonValue o = readObject(jv, key); - if (o.type == Type.MAP && !o.map.isEmpty()) { - Map temp = new HashMap<>(); - for (String k : o.map.keySet()) { - String value = readString(o, k); - if (value != null) { - temp.put(k, value); - } - } - return temp.isEmpty() ? null : temp; - } - return null; - } - - public static String readString(JsonValue jsonValue, String key) { - return read(jsonValue, key, v -> v == null ? null : v.string); - } - - public static String readString(JsonValue jsonValue, String key, String dflt) { - return read(jsonValue, key, v -> v == null ? dflt : v.string); - } - - public static ZonedDateTime readDate(JsonValue jsonValue, String key) { - return read(jsonValue, key, - v -> v == null || v.string == null ? null : DateTimeUtils.parseDateTimeThrowParseError(v.string)); - } - - public static Integer readInteger(JsonValue jsonValue, String key) { - return read(jsonValue, key, v -> v == null ? null : getInteger(v)); - } - - public static int readInteger(JsonValue jsonValue, String key, int dflt) { - return read(jsonValue, key, v -> { + /** + * Read a value generically + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @param valueSupplier the generic supplier that converts the object to the output type + * @return the value represented by the key + * @param the output type + */ + public static T read(JsonValue jv, String key, JsonValueSupplier valueSupplier) { + JsonValue jvv = jv == null || jv.type != Type.MAP ? null : jv.map.get(key); + return valueSupplier.get(jvv); + } + + /** + * Read a key's value, without assuming it's type + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return The JsonValue or null if the key is not found + */ + public static JsonValue readValue(JsonValue jv, String key) { + return read(jv, key, v -> v); + } + + /** + * Read a key's string value expecting the value to be of type JsonValue.Type.STRING, + * If the key is not found or the type is not STRING, null is returned + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return the string or null + */ + public static String readString(JsonValue jv, String key) { + return read(jv, key, v -> v == null ? null : v.string); + } + + /** + * Read a key's string value expecting the value to be of type JsonValue.Type.STRING, + * If the key is not found or the type is not STRING, the supplied default is returned + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @param dflt the default value + * @return the string or the default + */ + public static String readString(JsonValue jv, String key, String dflt) { + return read(jv, key, v -> v == null || v.type != Type.STRING ? dflt : v.string); + } + + /** + * Read a key's int value expecting the value to be of type JsonValue.Type.INTEGER, + * or of type JsonValue.Type.LONG with a value in the range of integer. + * If the key is not found or the value is not an integer, null is returned + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return the integer or null + */ + public static Integer readInteger(JsonValue jv, String key) { + return read(jv, key, v -> v == null ? null : getInteger(v)); + } + + /** + * Read a key's int value expecting the value to be of type JsonValue.Type.INTEGER, + * or of type JsonValue.Type.LONG with a value in the range of integer. + * If the key is not found or the value is not an integer, the supplied default is returned + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @param dflt the default value + * @return the integer or the default + */ + public static int readInteger(JsonValue jv, String key, int dflt) { + return read(jv, key, v -> { if (v != null) { Integer i = getInteger(v); if (i != null) { @@ -103,12 +115,29 @@ public static int readInteger(JsonValue jsonValue, String key, int dflt) { }); } - public static Long readLong(JsonValue jsonValue, String key) { - return read(jsonValue, key, v -> v == null ? null : getLong(v)); - } - - public static long readLong(JsonValue jsonValue, String key, long dflt) { - return read(jsonValue, key, v -> { + /** + * Read a key's int value expecting the value to be of type JsonValue.Type.INTEGER, + * or of type JsonValue.Type.LONG. + * If the key is not found or the value is not an integer or long, null is returned + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return the long or null + */ + public static Long readLong(JsonValue jv, String key) { + return read(jv, key, v -> v == null ? null : getLong(v)); + } + + /** + * Read a key's int value expecting the value to be of type JsonValue.Type.INTEGER, + * or of type JsonValue.Type.LONG. + * If the key is not found or the value is not an integer or long, the default is returned + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @param dflt the default value + * @return the long or the default + */ + public static long readLong(JsonValue jv, String key, long dflt) { + return read(jv, key, v -> { if (v != null) { Long l = getLong(v); if (l != null) { @@ -119,261 +148,380 @@ public static long readLong(JsonValue jsonValue, String key, long dflt) { }); } - public static boolean readBoolean(JsonValue jsonValue, String key) { - return readBoolean(jsonValue, key, false); - } - - public static Boolean readBoolean(JsonValue jsonValue, String key, Boolean dflt) { - return read(jsonValue, key, - v -> v == null || v.bool == null ? dflt : v.bool); - } - - public static Duration readNanos(JsonValue jsonValue, String key) { - Long l = readLong(jsonValue, key); + /** + * Read a key's string value expecting the value to be of type JsonValue.Type.BOOL + *

If the key is not found or the type is not BOOL, false is returned.

+ * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return the value or false + */ + public static boolean readBoolean(JsonValue jv, String key) { + return readBoolean(jv, key, false); + } + + /** + * Read a key's string value expecting the value to be of type JsonValue.Type.BOOL + *

If the key is not found or the type is not BOOL, the default returned.

+ * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @param dflt the default value + * @return the value or the default + */ + public static Boolean readBoolean(JsonValue jv, String key, Boolean dflt) { + return read(jv, key, + v -> v != null && v.type == Type.BOOL ? v.bool : dflt); + } + + /** + * Read a key's string value expecting the value to be of type JsonValue.Type.STRING, + * and then parses that string to a ZonedDateTime. + *

If the key is not found or the type is not STRING, null is returned.

+ *

If the string is found but is not parseable by ZonedDateTime, a DateTimeParseException is thrown

+ * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return the string or null + */ + public static ZonedDateTime readDate(JsonValue jv, String key) { + String s = readString(jv, key); + return s == null ? null : DateTimeUtils.parseDateTimeThrowParseError(s); + } + + /** + * Read a key's string value expecting the value to be of type JsonValue.Type.INTEGER + * or JsonValue.Type.LONG, and then converts that to a Duration assuming the number + * represents nanoseconds + *

If the key is not found or the type is not INTEGER or LONG, null is returned.

+ * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return the Duration or null + */ + public static Duration readNanosAsDuration(JsonValue jv, String key) { + Long l = readLong(jv, key); return l == null ? null : Duration.ofNanos(l); } - public static Duration readNanos(JsonValue jsonValue, String key, Duration dflt) { - Long l = readLong(jsonValue, key); + /** + * Read a key's string value expecting the value to be of type JsonValue.Type.INTEGER + * or JsonValue.Type.LONG, and then converts that to a Duration assuming the number + * represents nanoseconds + *

If the key is not found or the type is not INTEGER or LONG, null is returned.

+ * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @param dflt the default value + * @return the Duration or the default + */ + public static Duration readNanosAsDuration(JsonValue jv, String key, Duration dflt) { + Long l = readLong(jv, key); return l == null ? dflt : Duration.ofNanos(l); } - public static List listOf(JsonValue v, Function provider) { - List list = new ArrayList<>(); - if (v != null && v.array != null) { - for (JsonValue jv : v.array) { - T t = provider.apply(jv); - if (t != null) { - list.add(t); - } - } - } - return list; - } - - public static List optionalListOf(JsonValue v, Function provider) { - List list = listOf(v, provider); - return list.isEmpty() ? null : list; - } - - public static List readStringList(JsonValue jsonValue, String key) { - return read(jsonValue, key, v -> listOf(v, jv -> jv.string)); + /** + * + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return + */ + public static byte[] readBytes(JsonValue jv, String key) { + String s = readString(jv, key); + return s == null ? null : s.getBytes(StandardCharsets.US_ASCII); } - public static List readStringListIgnoreEmpty(JsonValue jsonValue, String key) { - return read(jsonValue, key, v -> listOf(v, jv -> { - if (jv.string != null) { - String s = jv.string.trim(); - if (!s.isEmpty()) { - return s; - } + /** + * + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return + */ + public static byte[] readBase64(JsonValue jv, String key) { + String b64 = readString(jv, key); + return b64 == null ? null : Encoding.base64BasicDecode(b64); + } + + /** + * Read a key's map value as a generic JsonValue assuming the value is a JSON object (a map) + * If the key is not found or the value type is not MAP, null is returned + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return The JsonValue for the MAP object or null + */ + public static JsonValue readMapObjectOrNull(JsonValue jv, String key) { + return read(jv, key, v -> v == null || v.type != Type.MAP ? null : v); + } + + /** + * Read a key's map value as a generic JsonValue assuming the value is a JSON object (a map) + * If the key is not found or the value type is not MAP, EMPTY_MAP is returned + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return The JsonValue for the MAP object or EMPTY_MAP + */ + public static JsonValue readMapObjectOrEmpty(JsonValue jv, String key) { + return read(jv, key, v -> v == null || v.type != Type.MAP ? EMPTY_MAP : v); + } + + /** + * Read a key's map value as a Map of String to JsonValue + * If the key is not found or the value type is not MAP, null is returned + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return The JsonValue for the MAP object or null + */ + public static Map readMapMapOrNull(JsonValue jv, String key) { + JsonValue jvv = readMapObjectOrNull(jv, key); + return jvv == null ? null : jvv.map; + } + + /** + * Read a key's map value as a Map of String to JsonValue + * If the key is not found or the value type is not MAP, EMPTY_MAP is returned + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return The JsonValue for the MAP object or EMPTY_MAP + */ + public static Map readMapMapOrEmpty(JsonValue jv, String key) { + return readMapObjectOrEmpty(jv, key).map; + } + + /** + * Read a key's map value as a Map of String to String. This will + * assume that every value in the looked-up map is a string, + * otherwise the key will not be included in the returned map. + * If there is no map for the key, or the map is found but empty, + * the function returns null. + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return The resolved map or null + */ + public static Map readStringMapOrNull(JsonValue jv, String key) { + Map map = readMapMapOrNull(jv, key); + return map == null ? null : convertToStringMap(map); + } + + /** + * Read a key's map value as a Map of String to String. This will + * assume that every value in the looked-up map is a string, + * otherwise the key will not be included in the returned map. + * If there is no map for the key, or the map is found but empty, + * the function returns null. + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return The resolved map or null + */ + public static Map readStringMapOrEmpty(JsonValue jv, String key) { + return convertToStringMap(readMapMapOrEmpty(jv, key)); + } + + /** + * Converts a JsonValue's Map of String to JsonValue to a Map of String to String, + * filtering out any key whose value is not a string. + * @param map the input map + * @return the converted map. Map be empty. + */ + public static Map convertToStringMap(Map map) { + Map temp = new HashMap<>(); + for (String k : map.keySet()) { + JsonValue value = map.get(k); + if (value.type == Type.STRING) { + temp.put(k, value.string); } + } + return temp; + } + + /** + * Read a value expecting it to be of type JsonValue.Type.ARRAY. + * If the value is not found or the type is not ARRAY, an empty list is returned + * @param jv the jsonValue that is an array (type is JsonValue.Type.ARRAY) + * @param processor a function that processes each value in the array + * @return The List of JsonValues in the array or an empty list if the value was null or not an ARRAY + */ + public static List listOfOrNull(JsonValue jv, Function processor) { + if (jv == null || jv.type != Type.ARRAY) { return null; - })); - } - - public static List readOptionalStringList(JsonValue jsonValue, String key) { - return read(jsonValue, key, v -> optionalListOf(v, jv -> jv.string)); - } - - public static List readLongList(JsonValue jsonValue, String key) { - return read(jsonValue, key, v -> listOf(v, JsonValueUtils::getLong)); - } - - public static List readNanosList(JsonValue jsonValue, String key) { - return readNanosList(jsonValue, key, false); - } - - public static List readNanosList(JsonValue jsonValue, String key, boolean nullIfEmpty) { - List list = read(jsonValue, key, - v -> listOf(v, vv -> { - Long l = getLong(vv); - return l == null ? null : Duration.ofNanos(l); - }) - ); - return list.isEmpty() && nullIfEmpty ? null : list; - } - - public static byte[] readBytes(JsonValue jsonValue, String key) { - String s = readString(jsonValue, key); - return s == null ? null : s.getBytes(StandardCharsets.US_ASCII); - } - - public static byte[] readBase64(JsonValue jsonValue, String key) { - String b64 = readString(jsonValue, key); - return b64 == null ? null : Base64.getDecoder().decode(b64); - } - - public static Integer getInteger(JsonValue v) { - if (v.i != null) { - return v.i; } - // just in case the number was stored as a long, which is unlikely, but I want to handle it - if (v.l != null && v.l <= Integer.MAX_VALUE && v.l >= Integer.MIN_VALUE) { - return v.l.intValue(); + return fillList(jv, new ArrayList<>(), processor); + } + + /** + * Read a value expecting it to be of type JsonValue.Type.ARRAY. + * If the value is not found or the type is not ARRAY, null is returned + * @param jv the jsonValue that is an array (type is JsonValue.Type.ARRAY) + * @param processor a function that processes each value in the array + * @return The List of JsonValues in the array or null if the value was null or not an ARRAY + */ + public static List listOfOrEmpty(JsonValue jv, Function processor) { + if (jv == null || jv.type != Type.ARRAY) { + return Collections.emptyList(); } - return null; - } - - public static Long getLong(JsonValue v) { - return v.l != null ? v.l : (v.i != null ? (long)v.i : null); - } - - public static long getLong(JsonValue v, long dflt) { - return v.l != null ? v.l : (v.i != null ? (long)v.i : dflt); + return fillList(jv, new ArrayList<>(), processor); } - public static JsonValue instance(Duration d) { - return new JsonValue(d.toNanos()); - } - - @SuppressWarnings("rawtypes") - public static JsonValue instance(Collection list) { - JsonValue v = new JsonValue(new ArrayList<>()); - for (Object o : list) { - v.array.add(toJsonValue(o)); + private static List fillList(JsonValue jv, List target, Function provider) { + for (JsonValue jvv : jv.array) { + T t = provider.apply(jvv); + if (t != null) { + target.add(t); + } } - return v; - } - - @SuppressWarnings("rawtypes") - public static JsonValue instance(Map map) { - JsonValue v = new JsonValue(new HashMap<>()); - for (Object key : map.keySet()) { - v.map.put(key.toString(), toJsonValue(map.get(key))); + return target; + } + + /** + * Read a key's value expecting the value to be of type JsonValue.Type.ARRAY, + * If the key is not found or the type is not ARRAY, null is returned + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return The List of JsonValues in the null + */ + public static List readArrayOrNull(JsonValue jv, String key) { + return read(jv, key, v -> v == null || v.type != Type.ARRAY ? null : v.array); + } + + /** + * Read a key's value expecting the value to be of type JsonValue.Type.ARRAY, + * If the key is not found or the type is not ARRAY, EMPTY_ARRAY is returned + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return The List of JsonValues in the array + */ + public static List readArrayOrEmpty(JsonValue jv, String key) { + return read(jv, key, v -> v == null || v.type != Type.ARRAY ? EMPTY_ARRAY.array : v.array); + } + + /** + * + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return + */ + public static List readStringListOrNull(JsonValue jv, String key) { + return read(jv, key, v -> listOfOrNull(v, jvv -> jvv.string)); + } + + /** + * Read a key's value expecting the value to be of type JsonValue.Type.ARRAY, + * If the key is not found or the type is not ARRAY, an empty list is returned + * If the value is not a string it is not included in the returned list. + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return The list of String in the array + */ + public static List readStringListOrEmpty(JsonValue jv, String key) { + return read(jv, key, v -> listOfOrEmpty(v, jvv -> jvv.string)); + } + + /** + * + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return + */ + public static List readIntegerListOrNull(JsonValue jv, String key) { + return read(jv, key, v -> listOfOrNull(v, JsonValueUtils::getInteger)); + } + + /** + * + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return + */ + public static List readIntegerListOrEmpty(JsonValue jv, String key) { + return read(jv, key, v -> listOfOrEmpty(v, JsonValueUtils::getInteger)); + } + + /** + * + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return + */ + public static List readLongListOrNull(JsonValue jv, String key) { + return read(jv, key, v -> listOfOrNull(v, JsonValueUtils::getLong)); + } + + /** + * + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return + */ + public static List readLongListOrEmpty(JsonValue jv, String key) { + return read(jv, key, v -> listOfOrEmpty(v, JsonValueUtils::getLong)); + } + + /** + * + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return + */ + public static List readNanosAsDurationListOrNull(JsonValue jv, String key) { + List list = readLongListOrNull(jv, key); + return list == null ? null : _nanosToDuration(list); + } + + /** + * + * @param jv the jsonValue that is an object (type is JsonValue.Type.MAP) + * @param key the key to look up + * @return + */ + public static List readNanosAsDurationListOrEmpty(JsonValue jv, String key) { + return _nanosToDuration(readLongListOrEmpty(jv, key)); + } + + private static List _nanosToDuration(List list) { + List durations = new ArrayList<>(); + for (Long l : list) { + durations.add(Duration.ofNanos(l)); } - return v; + return durations; } - public static JsonValue toJsonValue(Object o) { - if (o == null) { - return JsonValue.NULL; - } - if (o instanceof JsonValue) { - return (JsonValue)o; - } - if (o instanceof JsonSerializable) { - return ((JsonSerializable)o).toJsonValue(); + /** + * Get the int value of the JsonValue + * @param jv the jsonValue that represents an Integer or a Long with a value in the range of valid Integer + * @return the Integer value, which may be null + */ + public static Integer getInteger(JsonValue jv) { + if (jv.i != null) { + return jv.i; } - if (o instanceof Map) { - //noinspection unchecked,rawtypes - return new JsonValue((Map)o); + // just in case the number was stored as long, which is unlikely, but I want to handle it + if (jv.l != null && jv.l <= Integer.MAX_VALUE && jv.l >= Integer.MIN_VALUE) { + return jv.l.intValue(); } - if (o instanceof List) { - //noinspection unchecked,rawtypes - return new JsonValue((List)o); - } - if (o instanceof Set) { - //noinspection unchecked,rawtypes - return new JsonValue(new ArrayList<>((Set)o)); - } - if (o instanceof String) { - String s = ((String)o).trim(); - return s.length() == 0 ? new JsonValue() : new JsonValue(s); - } - if (o instanceof Boolean) { - return new JsonValue((Boolean)o); - } - if (o instanceof Integer) { - return new JsonValue((Integer)o); - } - if (o instanceof Long) { - return new JsonValue((Long)o); - } - if (o instanceof Double) { - return new JsonValue((Double)o); - } - if (o instanceof Float) { - return new JsonValue((Float)o); - } - if (o instanceof BigDecimal) { - return new JsonValue((BigDecimal)o); - } - if (o instanceof BigInteger) { - return new JsonValue((BigInteger)o); - } - return new JsonValue(o.toString()); + return null; } - public static MapBuilder mapBuilder() { - return new MapBuilder(); + /** + * Get the int value of the JsonValue + * @param jv the jsonValue that represents an Integer + * @param dflt the default value to use if the object does not represent an Integer + * @return the int value or default + */ + public static int getInt(JsonValue jv, int dflt) { + Integer i = getInteger(jv); + return i == null ? dflt : i; } - public static class MapBuilder implements JsonSerializable { - public JsonValue jv; - - public MapBuilder() { - jv = new JsonValue(new HashMap<>()); - } - - public MapBuilder(JsonValue jv) { - this.jv = jv; - } - - public MapBuilder put(String s, Object o) { - if (o != null) { - JsonValue vv = JsonValueUtils.toJsonValue(o); - if (vv.type != JsonValue.Type.NULL) { - jv.map.put(s, vv); - jv.mapOrder.add(s); - } - } - return this; - } - - public MapBuilder put(String s, Map stringMap) { - if (stringMap != null) { - MapBuilder mb = new MapBuilder(); - for (String key : stringMap.keySet()) { - mb.put(key, stringMap.get(key)); - } - jv.map.put(s, mb.jv); - jv.mapOrder.add(s); - } - return this; - } - - @Override - public String toJson() { - return jv.toJson(); - } - - @Override - public JsonValue toJsonValue() { - return jv; - } + /** + * Get the Long value of the JsonValue + * @param jv the jsonValue that represents a Long or an Integer + * @return the Long value, which may be null + */ + public static Long getLong(JsonValue jv) { + return jv.l != null ? jv.l : (jv.i != null ? (long)jv.i : null); } - public static ArrayBuilder arrayBuilder() { - return new ArrayBuilder(); - } - - public static class ArrayBuilder implements JsonSerializable { - public JsonValue jv = new JsonValue(new ArrayList<>()); - public ArrayBuilder add(Object o) { - if (o != null) { - JsonValue vv = JsonValueUtils.toJsonValue(o); - if (vv.type != JsonValue.Type.NULL) { - jv.array.add(JsonValueUtils.toJsonValue(o)); - } - } - return this; - } - - @Override - public String toJson() { - return jv.toJson(); - } - - @Override - public JsonValue toJsonValue() { - return jv; - } - - @Deprecated - public JsonValue getJsonValue() { - return jv; - } + /** + * Get the long value of the JsonValue + * @param jv the jsonValue that represents a Long or an Integer + * @param dflt the default value to use if the object does not represent a Long or Integer + * @return the long value or default + */ + public static long getLong(JsonValue jv, long dflt) { + return jv.l != null ? jv.l : (jv.i != null ? (long)jv.i : dflt); } } - diff --git a/src/main/java/io/nats/json/JsonWriteUtils.java b/src/main/java/io/nats/json/JsonWriteUtils.java index 77cb04b..aba30ac 100644 --- a/src/main/java/io/nats/json/JsonWriteUtils.java +++ b/src/main/java/io/nats/json/JsonWriteUtils.java @@ -1,4 +1,4 @@ -// Copyright 2020-2024 The NATS Authors +// Copyright 2020-2025 The NATS Authors // 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: @@ -13,16 +13,16 @@ package io.nats.json; -import static io.nats.json.DateTimeUtils.DEFAULT_TIME; -import static io.nats.json.Encoding.jsonEncode; -import static io.nats.json.JsonValueUtils.instance; - import java.time.Duration; import java.time.ZonedDateTime; import java.util.Arrays; import java.util.List; import java.util.Map; +import static io.nats.json.DateTimeUtils.DEFAULT_TIME; +import static io.nats.json.Encoding.jsonEncode; +import static io.nats.json.JsonValue.instance; + public abstract class JsonWriteUtils { public static final String Q = "\""; public static final String QCOLONQ = "\":\""; @@ -79,10 +79,10 @@ public static String endFormattedJson(StringBuilder sb) { } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname - * @param json raw json + * @param json raw JSON */ public static void addRawJson(StringBuilder sb, String fname, String json) { if (json != null && !json.isEmpty()) { @@ -95,7 +95,7 @@ public static void addRawJson(StringBuilder sb, String fname, String json) { } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param value field value @@ -111,7 +111,7 @@ public static void addField(StringBuilder sb, String fname, String value) { } /** - * Appends a json field to a string builder. Empty and null string are added as value of empty string + * Appends a JSON field to a string builder. Empty and null string are added as value of empty string * @param sb string builder * @param fname fieldname * @param value field value @@ -128,7 +128,7 @@ public static void addFieldEvenEmpty(StringBuilder sb, String fname, String valu } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param value field value @@ -142,7 +142,7 @@ public static void addField(StringBuilder sb, String fname, Boolean value) { } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param value field value @@ -154,7 +154,7 @@ public static void addFldWhenTrue(StringBuilder sb, String fname, Boolean value) } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param value field value @@ -168,7 +168,7 @@ public static void addField(StringBuilder sb, String fname, Integer value) { } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param value field value @@ -182,7 +182,7 @@ public static void addFieldWhenGtZero(StringBuilder sb, String fname, Integer va } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param value field value @@ -196,7 +196,7 @@ public static void addField(StringBuilder sb, String fname, Long value) { } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param value field value @@ -210,7 +210,7 @@ public static void addFieldWhenGtZero(StringBuilder sb, String fname, Long value } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param value field value @@ -224,7 +224,7 @@ public static void addFieldWhenGteMinusOne(StringBuilder sb, String fname, Long } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param value field value @@ -239,7 +239,7 @@ public static void addFieldWhenGreaterThan(StringBuilder sb, String fname, Long } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param value duration value @@ -253,7 +253,7 @@ public static void addFieldAsNanos(StringBuilder sb, String fname, Duration valu } /** - * Appends a json object to a string builder. + * Appends a JSON object to a string builder. * @param sb string builder * @param fname fieldname * @param value JsonSerializable value @@ -284,7 +284,7 @@ public interface ListAdder { } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param the list type * @param sb string builder * @param fname fieldname @@ -305,7 +305,7 @@ public static void _addList(StringBuilder sb, String fname, List list, Li } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param strings field value @@ -317,7 +317,7 @@ public static void addStrings(StringBuilder sb, String fname, String[] strings) } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param strings field value @@ -337,7 +337,7 @@ private static void _addStrings(StringBuilder sb, String fname, List str } /** - * Appends a json field to a string builder. + * Appends a JSON field to a string builder. * @param sb string builder * @param fname fieldname * @param jsons field value @@ -349,7 +349,7 @@ public static void addJsons(StringBuilder sb, String fname, List c) { return "\"" + c.getSimpleName() + "\":"; } + private static final int INDENT_WIDTH = 4; private static final String INDENT = " "; private static String indent(int level) { - return level == 0 ? "" : INDENT.substring(0, level * 4); + return level == 0 ? "" : INDENT.substring(0, level * INDENT_WIDTH); } - /** - * This isn't perfect but good enough for debugging - * @param o the object - * @return the formatted string - */ public static String getFormatted(Object o) { + String s = o.toString(); + String newline = System.lineSeparator(); + StringBuilder sb = new StringBuilder(); - int level = 0; - int arrayLevel = 0; - boolean lastWasClose = false; - boolean indentNext = true; + boolean begin_quotes = false; + + boolean opened = false; + int indentLevel = 0; String indent = ""; - String s = o.toString(); for (int x = 0; x < s.length(); x++) { char c = s.charAt(x); - if (c == '{') { - if (arrayLevel > 0 && lastWasClose) { - sb.append(indent); + + if (c == '\"') { + if (opened) { + sb.append(newline).append(indent); + opened = false; } - sb.append(c).append('\n'); - indent = indent(++level); - indentNext = true; - lastWasClose = false; - } - else if (c == '}') { - indent = indent(--level); - sb.append('\n').append(indent).append(c); - lastWasClose = true; + sb.append(c); + begin_quotes = !begin_quotes; + continue; } - else if (c == ',') { - sb.append(",\n"); - indentNext = true; - } - else { - if (c == '[') { - arrayLevel++; - } - else if (c == ']') { - arrayLevel--; - } - if (indentNext) { - if (c != ' ') { - sb.append(indent).append(c); - indentNext = false; - } - } - else { - sb.append(c); + + if (!begin_quotes) { + switch (c) { + case '{': + case '[': + sb.append(c); + opened = true; + indent = indent(++indentLevel); + continue; + case '}': + case ']': + indent = indent(--indentLevel); + if (!opened) { + sb.append(newline).append(indent); + } + sb.append(c); + opened = false; + continue; + case ':': + sb.append(c).append(" "); + continue; + case ',': + sb.append(c).append(newline).append(indentLevel > 0 ? indent : ""); + continue; + default: + if (Character.isWhitespace(c)) continue; + if (opened) { + sb.append(newline).append(indent); + opened = false; + } } - lastWasClose = lastWasClose && Character.isWhitespace(c); } + + sb.append(c).append(c == '\\' ? "" + s.charAt(++x) : ""); } + return sb.toString(); } diff --git a/src/main/java/io/nats/json/MapBuilder.java b/src/main/java/io/nats/json/MapBuilder.java new file mode 100644 index 0000000..27f9ab0 --- /dev/null +++ b/src/main/java/io/nats/json/MapBuilder.java @@ -0,0 +1,58 @@ +// Copyright 2025 The NATS Authors +// 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: +// +// 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 io.nats.json; + +import java.util.HashMap; +import java.util.Map; + +public class MapBuilder implements JsonSerializable { + + public final JsonValue jv; + + public static MapBuilder instance() { + return new MapBuilder(); + } + + public MapBuilder() { + jv = new JsonValue(new HashMap<>()); + } + + public MapBuilder put(String s, Object o) { + //noinspection DataFlowIssue // NO ISSUE, WE KNOW jv.map is NOT NULL + jv.map.put(s, JsonValue.instance(o)); + jv.mapOrder.add(s); + return this; + } + + public MapBuilder putEntries(Map map) { + if (map != null) { + for (String key : map.keySet()) { + //noinspection DataFlowIssue // NO ISSUE, WE KNOW jv.map is NOT NULL + jv.map.put(key, JsonValue.instance(map.get(key))); + jv.mapOrder.add(key); + } + } + return this; + } + + @Override + public String toJson() { + return jv.toJson(); + } + + @Override + public JsonValue toJsonValue() { + return jv; + } +} diff --git a/src/test/java/io/nats/json/DateTimeUtilsTests.java b/src/test/java/io/nats/json/DateTimeUtilsTests.java index fd33309..ceb6af1 100644 --- a/src/test/java/io/nats/json/DateTimeUtilsTests.java +++ b/src/test/java/io/nats/json/DateTimeUtilsTests.java @@ -1,4 +1,4 @@ -// Copyright 2015-2024 The NATS Authors +// Copyright 2025 The NATS Authors // 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: @@ -13,16 +13,14 @@ package io.nats.json; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; public final class DateTimeUtilsTests { diff --git a/src/test/java/io/nats/json/EncodingTests.java b/src/test/java/io/nats/json/EncodingTests.java index 7ac3419..3a5080e 100644 --- a/src/test/java/io/nats/json/EncodingTests.java +++ b/src/test/java/io/nats/json/EncodingTests.java @@ -1,4 +1,4 @@ -// Copyright 2015-2024 The NATS Authors +// Copyright 2025 The NATS Authors // 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: @@ -13,25 +13,16 @@ package io.nats.json; -import static io.nats.json.Encoding.base64BasicDecode; -import static io.nats.json.Encoding.base64BasicDecodeToString; -import static io.nats.json.Encoding.base64BasicEncode; -import static io.nats.json.Encoding.base64BasicEncodeToString; -import static io.nats.json.Encoding.base64UrlDecode; -import static io.nats.json.Encoding.base64UrlEncode; -import static io.nats.json.Encoding.base64UrlEncodeToString; -import static io.nats.json.Encoding.jsonDecode; -import static io.nats.json.Encoding.jsonEncode; -import static io.nats.json.Encoding.uriDecode; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import io.ResourceUtils; +import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Objects; -import org.junit.jupiter.api.Test; - -import io.ResourceUtils; +import static io.nats.json.Encoding.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public final class EncodingTests { @Test @@ -60,6 +51,7 @@ public void testJsonEncodeDecode() { _testJsonEncodeDecode("b4\\xafter", "b4xafter", "b4xafter"); // unknown escape _testJsonEncodeDecode("b4\\", "b4\\", "b4\\\\"); // last char is \ _testJsonEncodeDecode("b4\\/after", "b4/after", null); + _testJsonEncodeDecode("not-valid-u\\uu", "not-valid-uuu", "not-valid-uuu"); List utfs = ResourceUtils.resourceAsLines("utf8-only-no-ws-test-strings.txt"); for (String u : utfs) { @@ -72,12 +64,7 @@ private void _testJsonEncodeDecode(String encodedInput, String targetDecode, Str String decoded = jsonDecode(encodedInput); assertEquals(targetDecode, decoded); String encoded = jsonEncode(new StringBuilder(), decoded).toString(); - if (targetEncode == null) { - assertEquals(encodedInput, encoded); - } - else { - assertEquals(targetEncode, encoded); - } + assertEquals(Objects.requireNonNullElse(targetEncode, encodedInput), encoded); } @Test @@ -92,9 +79,13 @@ public void testBase64BasicEncoding() { assertEquals("YmxhaGJsYWg=", encFromBytes); assertEquals("YmxhaGJsYWg=", encFromString); + encFromString = base64BasicEncodeToString(text, StandardCharsets.US_ASCII); + assertEquals("YmxhaGJsYWg=", encFromString); + assertArrayEquals(btxt, base64BasicDecode(encBytesFromBytes)); assertArrayEquals(btxt, base64BasicDecode(encFromBytes)); assertEquals(text, base64BasicDecodeToString(encFromBytes)); + assertEquals(text, base64BasicDecodeToString(encFromBytes, StandardCharsets.US_ASCII)); String data = ResourceUtils.resourceAsString("test_bytes_000100.txt"); String check = ResourceUtils.resourceAsString("basic_encoded_000100.txt"); @@ -134,8 +125,12 @@ public void testBase64UrlEncoding() { byte[] uencBytes = base64UrlEncode(btxt); assertEquals("YmxhaGJsYWg", base64UrlEncodeToString(text)); + assertEquals("YmxhaGJsYWg", base64UrlEncodeToString(text, StandardCharsets.US_ASCII)); assertEquals("YmxhaGJsYWg", new String(uencBytes)); + assertEquals(text, base64UrlDecodeToString(uencBytes)); assertArrayEquals(btxt, base64UrlDecode(uencBytes)); + assertEquals(text, base64UrlDecodeToString(new String(uencBytes))); + assertArrayEquals(btxt, base64UrlDecode(new String(uencBytes))); uencBytes = base64UrlEncode(burl); assertEquals("aHR0cHM6Ly9uYXRzLmlvLw", base64UrlEncodeToString(surl)); diff --git a/src/test/java/io/nats/json/JsonParsingTests.java b/src/test/java/io/nats/json/JsonParsingTests.java index 0d86ebc..4cb454d 100644 --- a/src/test/java/io/nats/json/JsonParsingTests.java +++ b/src/test/java/io/nats/json/JsonParsingTests.java @@ -1,4 +1,4 @@ -// Copyright 2023-2024 The NATS Authors +// Copyright 2025 The NATS Authors // 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: @@ -13,57 +13,27 @@ package io.nats.json; -import static io.nats.json.Encoding.jsonEncode; -import static io.nats.json.JsonParser.Option; -import static io.nats.json.JsonParser.Option.KEEP_NULLS; -import static io.nats.json.JsonParser.parse; -import static io.nats.json.JsonParser.parseUnchecked; -import static io.nats.json.JsonValueUtils.ArrayBuilder; -import static io.nats.json.JsonValueUtils.MapBuilder; -import static io.nats.json.JsonValueUtils.arrayBuilder; -import static io.nats.json.JsonValueUtils.getInteger; -import static io.nats.json.JsonValueUtils.getLong; -import static io.nats.json.JsonValueUtils.instance; -import static io.nats.json.JsonValueUtils.mapBuilder; -import static io.nats.json.JsonValueUtils.read; -import static io.nats.json.JsonValueUtils.readBoolean; -import static io.nats.json.JsonValueUtils.readDate; -import static io.nats.json.JsonValueUtils.readInteger; -import static io.nats.json.JsonValueUtils.readLong; -import static io.nats.json.JsonValueUtils.readNanos; -import static io.nats.json.JsonValueUtils.readNanosList; -import static io.nats.json.JsonValueUtils.readObject; -import static io.nats.json.JsonValueUtils.readOptionalStringList; -import static io.nats.json.JsonValueUtils.readString; -import static io.nats.json.JsonValueUtils.readStringList; -import static io.nats.json.JsonValueUtils.readStringListIgnoreEmpty; -import static io.nats.json.JsonValueUtils.readStringStringMap; -import static io.nats.json.JsonValueUtils.readValue; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import io.ResourceUtils; +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.math.BigInteger; -import java.time.DateTimeException; +import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; - -import org.junit.jupiter.api.Test; +import java.util.*; -import io.ResourceUtils; -import nl.jqno.equalsverifier.EqualsVerifier; -import nl.jqno.equalsverifier.Warning; +import static io.nats.json.Encoding.jsonEncode; +import static io.nats.json.JsonParser.*; +import static io.nats.json.JsonParser.Option.KEEP_NULLS; +import static io.nats.json.JsonValue.instance; +import static io.nats.json.JsonWriteUtils.printFormatted; +import static io.nats.json.JsonWriteUtils.toKey; +import static org.junit.jupiter.api.Assertions.*; public final class JsonParsingTests { + static List UTF_STRINGS = ResourceUtils.resourceAsLines("utf8-only-no-ws-test-strings.txt"); private String key(int i) { return "key" + i; @@ -89,8 +59,7 @@ public void testStringParsing() { addField(key(x++), "b4" + (char) 0 + "after", oMap, list, encodeds, decodeds); addField(key(x++), "b4" + (char) 1 + "after", oMap, list, encodeds, decodeds); - List utfs = ResourceUtils.resourceAsLines("utf8-only-no-ws-test-strings.txt"); - for (String u : utfs) { + for (String u : UTF_STRINGS) { String uu = "b4\b\f\n\r\t" + u + "after"; addField(key(x++), uu, oMap, list, encodeds, decodeds); } @@ -114,7 +83,6 @@ public void testStringParsing() { for (int i = 0; i < list.size(); i++) { JsonValue v = list.get(i); assertEquals(v, v.toJsonValue()); - assertEquals(v.toString("JsonParsingTests"), v.toString(this.getClass())); assertEquals(decodeds.get(i), v.string); assertEquals(v.toJson(), "\"" + encodeds.get(i) + "\""); } @@ -156,14 +124,6 @@ public void testJsonValuePrimitives() throws JsonParseException { oMap.put("bigIntegerKey1", new JsonValue(new BigInteger("9223372036854775807"))); oMap.put("bigIntegerKey2", new JsonValue(new BigInteger("-9223372036854775808"))); - // some coverage here - JsonValue vMap = new JsonValue(oMap); - assertEquals(vMap.toJson(), vMap.toString()); - String s = JsonValueUtils.readString(vMap, "stringKey"); - byte[] ba = JsonValueUtils.readBytes(vMap, "stringKey"); - assertNotNull(ba); - assertEquals(s, new String(ba)); - validateMapTypes(oMap, oMap, true); // don't keep nulls @@ -177,6 +137,15 @@ public void testJsonValuePrimitives() throws JsonParseException { assertNotNull(parsed.map); assertEquals(oMap.size(), parsed.map.size()); validateMapTypes(parsed.map, oMap, true); + + // just some coverage + assertEquals(parsed.toJson(), new String(parsed.serialize(), StandardCharsets.UTF_8)); + String ts = parsed.toString(getClass()); + assertTrue(ts.startsWith('"' + getClass().getSimpleName() + "\":{")); + assertTrue(ts.endsWith("}")); + ts = parsed.toString(getClass().getSimpleName()); + assertTrue(ts.startsWith('"' + getClass().getSimpleName() + "\":{")); + assertTrue(ts.endsWith("}")); } private static void validateMapTypes(Map map, Map oMap, boolean original) { @@ -287,14 +256,9 @@ public void testArray() throws JsonParseException { assertEquals(v.number, p.number); } - Map rootMap = new HashMap<>(); - rootMap.put("list", new JsonValue(list)); - rootMap.put("array", new JsonValue(list.toArray(new JsonValue[0]))); - root = new JsonValue(rootMap); - List mappedList = readValue(root, "list").array; - + List mappedList = new JsonValue(list).array; List mappedList2 = parse(new JsonValue(mappedList).toJson()).array; - List mappedArray = readValue(root, "array").array; + List mappedArray = new JsonValue(list.toArray(new JsonValue[0])).array; List mappedArray2 = parse(new JsonValue(list.toArray(new JsonValue[0])).toJson()).array; for (int i = 0; i < list.size(); i++) { JsonValue v = list.get(i); @@ -319,96 +283,11 @@ public void testArray() throws JsonParseException { } } - @Test - public void testListReading() { - List jvList = new ArrayList<>(); - jvList.add(new JsonValue("string1")); - jvList.add(new JsonValue("string2")); - jvList.add(new JsonValue("")); - jvList.add(new JsonValue(true)); - jvList.add(new JsonValue((String)null)); - jvList.add(JsonValue.NULL); - jvList.add(JsonValue.EMPTY_MAP); - jvList.add(JsonValue.EMPTY_ARRAY); - jvList.add(new JsonValue(Integer.MAX_VALUE)); - jvList.add(new JsonValue(Long.MAX_VALUE)); - Map jvMap = new HashMap<>(); - jvMap.put("list", new JsonValue(jvList)); - JsonValue root = new JsonValue(jvMap); - - List list = readStringList(root, "list"); - assertEquals(3, list.size()); - assertTrue(list.contains("string1")); - assertTrue(list.contains("string2")); - assertTrue(list.contains("")); - - list = readStringListIgnoreEmpty(root, "list"); - assertEquals(2, list.size()); - assertTrue(list.contains("string1")); - assertTrue(list.contains("string2")); - - jvList.remove(0); - jvList.remove(0); - jvList.remove(0); - list = readOptionalStringList(root, "list"); - assertNull(list); - - list = readOptionalStringList(root, "na"); - assertNull(list); - - jvList.clear(); - Duration d0 = Duration.ofNanos(10000000000L); - Duration d1 = Duration.ofNanos(20000000000L); - Duration d2 = Duration.ofNanos(30000000000L); - - jvList.add(instance(d0)); - jvList.add(instance(d1)); - jvList.add(instance(d2)); - jvList.add(new JsonValue("not duration nanos")); - - root = new JsonValue(jvMap); - - List dlist = readNanosList(root, "list"); - assertEquals(3, dlist.size()); - assertEquals(d0, dlist.get(0)); - assertEquals(d1, dlist.get(1)); - assertEquals(d2, dlist.get(2)); - } - - @Test - public void testGetIntLong() { - JsonValue i = new JsonValue(Integer.MAX_VALUE); - JsonValue li = new JsonValue((long)Integer.MAX_VALUE); - JsonValue lmax = new JsonValue(Long.MAX_VALUE); - JsonValue lmin = new JsonValue(Long.MIN_VALUE); - assertEquals(Integer.MAX_VALUE, getInteger(i)); - assertEquals(Integer.MAX_VALUE, getInteger(li)); - assertNull(getInteger(lmax)); - assertNull(getInteger(lmin)); - assertNull(getInteger(JsonValue.NULL)); - assertNull(getInteger(JsonValue.EMPTY_MAP)); - assertNull(getInteger(JsonValue.EMPTY_ARRAY)); - - assertEquals(Integer.MAX_VALUE, getLong(i)); - assertEquals(Integer.MAX_VALUE, getLong(li)); - assertEquals(Long.MAX_VALUE, getLong(lmax)); - assertEquals(Long.MIN_VALUE, getLong(lmin)); - assertNull(getLong(JsonValue.NULL)); - assertNull(getLong(JsonValue.EMPTY_MAP)); - assertNull(getLong(JsonValue.EMPTY_ARRAY)); - - assertEquals(Integer.MAX_VALUE, getLong(i, -1)); - assertEquals(Integer.MAX_VALUE, getLong(li, -1)); - assertEquals(Long.MAX_VALUE, getLong(lmax, -1)); - assertEquals(Long.MIN_VALUE, getLong(lmin, -1)); - assertEquals(-1, getLong(JsonValue.NULL, -1)); - assertEquals(-1, getLong(JsonValue.EMPTY_MAP, -1)); - assertEquals(-1, getLong(JsonValue.EMPTY_ARRAY, -1)); - } - @Test public void testConstantsAreReadOnly() { + //noinspection DataFlowIssue // NO ISSUE, WE KNOW jv.map is NOT NULL assertThrows(UnsupportedOperationException.class, () -> JsonValue.EMPTY_MAP.map.put("foo", null)); + //noinspection DataFlowIssue // NO ISSUE, WE KNOW jv.array is NOT NULL assertThrows(UnsupportedOperationException.class, () -> JsonValue.EMPTY_ARRAY.array.add(null)); } @@ -436,116 +315,6 @@ public void testNullJsonValue() { assertEquals(JsonValue.NULL, new JsonValue((BigInteger) null)); } - @Test - public void testGetMapped() { - ZonedDateTime zdt = DateTimeUtils.gmtNow(); - Duration dur = Duration.ofNanos(4273); - Duration dur2 = Duration.ofNanos(7342); - - JsonValue v = new JsonValue(new HashMap<>()); - v.map.put("bool", new JsonValue(Boolean.TRUE)); - v.map.put("string", new JsonValue("hello")); - v.map.put("int", new JsonValue(Integer.MAX_VALUE)); - v.map.put("long", new JsonValue(Long.MAX_VALUE)); - v.map.put("date", new JsonValue(DateTimeUtils.toRfc3339(zdt))); - v.map.put("dur", new JsonValue(dur.toNanos())); - v.map.put("strings", new JsonValue(new JsonValue[]{new JsonValue("s1"), new JsonValue("s2")})); - v.map.put("durs", new JsonValue(new JsonValue[]{new JsonValue(dur.toNanos()), new JsonValue(dur2.toNanos())})); - - assertNotNull(readValue(v, "string")); - assertNull(readValue(v, "na")); - assertEquals(JsonValue.EMPTY_MAP, readObject(v, "na")); - assertNull(read(null, "na", vv -> vv)); - assertNull(read(JsonValue.NULL, "na", vv -> vv)); - assertNull(read(JsonValue.EMPTY_MAP, "na", vv -> vv)); - - assertNull(readDate(null, "na")); - assertNull(readDate(JsonValue.NULL, "na")); - assertNull(readDate(JsonValue.EMPTY_MAP, "na")); - assertEquals(zdt, readDate(v, "date")); - assertNull(readDate(v, "int")); - - assertFalse(readBoolean(null, "na")); - assertFalse(readBoolean(null, "na", false)); - assertTrue(readBoolean(null, "na", true)); - assertFalse(readBoolean(JsonValue.NULL, "na")); - assertFalse(readBoolean(JsonValue.NULL, "na", false)); - assertTrue(readBoolean(JsonValue.NULL, "na", true)); - assertFalse(readBoolean(JsonValue.EMPTY_MAP, "na")); - assertFalse(readBoolean(JsonValue.EMPTY_MAP, "na", false)); - assertTrue(readBoolean(JsonValue.EMPTY_MAP, "na", true)); - assertFalse(readBoolean(v, "na")); - assertFalse(readBoolean(v, "na", false)); - assertTrue(readBoolean(v, "na", true)); - assertFalse(readBoolean(v, "int")); - assertFalse(readBoolean(v, "int", false)); - assertTrue(readBoolean(v, "int", true)); - - assertTrue(readBoolean(v, "bool")); - assertTrue(readBoolean(v, "bool", false)); - assertFalse(readBoolean(v, "na")); - assertFalse(readBoolean(v, "na", false)); - assertTrue(readBoolean(v, "na", true)); - - assertEquals("hello", readString(v, "string")); - assertEquals("hello", readString(v, "string", null)); - assertNull(readString(v, "na")); - assertNull(readString(v, "na", null)); - assertEquals("default", readString(v, "na", "default")); - assertNull(readString(JsonValue.NULL, "na")); - assertNull(readString(JsonValue.NULL, "na", null)); - assertEquals("default", readString(JsonValue.NULL, "na", "default")); - - assertEquals(zdt, readDate(v, "date")); - assertNull(readDate(v, "na")); - assertThrows(DateTimeException.class, () -> readDate(v, "string")); - - assertEquals(Integer.MAX_VALUE, readInteger(v, "int")); - assertEquals(Integer.MAX_VALUE, readInteger(v, "int", -1)); - assertNull(readInteger(v, "string")); - assertEquals(-1, readInteger(v, "string", -1)); - assertNull(readInteger(v, "na")); - assertEquals(-1, readInteger(v, "na", -1)); - - assertEquals(Long.MAX_VALUE, readLong(v, "long")); - assertEquals(Long.MAX_VALUE, readLong(v, "long", -1)); - assertNull(readLong(v, "string")); - assertEquals(-1, readLong(v, "string", -1)); - assertNull(readLong(v, "na")); - assertEquals(-1, readLong(v, "na", -1)); - - assertEquals(dur, readNanos(v, "dur")); - assertEquals(dur, readNanos(v, "dur", null)); - assertNull(readNanos(v, "string")); - assertNull(readNanos(v, "string", null)); - assertEquals(dur2, readNanos(v, "string", dur2)); - assertNull(readNanos(v, "na")); - assertNull(readNanos(v, "na", null)); - assertEquals(dur2, readNanos(v, "na", dur2)); - - // these aren't maps - JsonValue jvn = new JsonValue(1); - JsonValue jvs = new JsonValue("s"); - JsonValue jvb = new JsonValue(true); - JsonValue[] notMaps = new JsonValue[] {JsonValue.NULL, JsonValue.EMPTY_ARRAY, jvn, jvs, jvb}; - - for (JsonValue vv : notMaps) { - assertNull(readValue(vv, "na")); - assertEquals(JsonValue.EMPTY_MAP, readObject(vv, "na")); - assertNull(readDate(vv, "na")); - assertNull(readInteger(vv, "na")); - assertEquals(-1, readInteger(vv, "na", -1)); - assertNull(readLong(vv, "na")); - assertEquals(-2, readLong(vv, "na", -2)); - assertFalse(readBoolean(vv, "na")); - assertNull(readBoolean(vv, "na", null)); - assertTrue(readBoolean(vv, "na", true)); - assertFalse(readBoolean(vv, "na", false)); - assertNull(readNanos(vv, "na")); - assertEquals(Duration.ZERO, readNanos(vv, "na", Duration.ZERO)); - } - } - @Test public void equalsContract() { Map map1 = new HashMap<>(); @@ -616,18 +385,24 @@ public void testParsingCoverage() throws JsonParseException { assertEquals(JsonValue.NULL, v); v = parse("{\"foo\":1,}"); + assertNotNull(v); + assertNotNull(v.map); assertEquals(1, v.map.size()); assertTrue(v.map.containsKey("foo")); assertEquals(1, v.map.get("foo").i); v = parse("INFO{\"foo\":1,}", 4); + assertNotNull(v); + assertNotNull(v.map); assertEquals(1, v.map.size()); assertTrue(v.map.containsKey("foo")); assertEquals(1, v.map.get("foo").i); v = parse("[\"foo\",]"); // handles dangling commas fine + assertNotNull(v); + assertNotNull(v.array); assertEquals(1, v.array.size()); - assertEquals("foo", v.array.get(0).string); + assertEquals("foo", v.array.getFirst().string); String s = "foo \b \t \n \f \r \" \\ /"; String j = "\"" + jsonEncode(s) + "\""; @@ -659,6 +434,15 @@ public void testParsingCoverage() throws JsonParseException { parseUnchecked(json, 0, Option.KEEP_NULLS); parseUnchecked(json.getBytes()); parseUnchecked(json.getBytes(), Option.KEEP_NULLS); + + // misc + json = ResourceUtils.resourceAsString("stream-info.json"); + JsonParser.parseUnchecked(json); + printFormatted(json); + assertEquals("\"JsonParsingTests\":", toKey(this.getClass())); + JsonParser.parseUnchecked(json, JsonParser.Option.KEEP_NULLS); + JsonParser.parseUnchecked(json, (JsonParser.Option) null); + JsonParser.parseUnchecked(json, (JsonParser.Option[]) null); } private void validateThrows(String json, String errorText) { @@ -746,7 +530,7 @@ public void testNumberParsing() throws JsonParseException { @Test public void testValueUtilsInstanceDuration() { - JsonValue v = instance(Duration.ofSeconds(1)); + JsonValue v = JsonValue.instance(Duration.ofSeconds(1)); assertNotNull(v.l); assertEquals(1000000000L, v.l); } @@ -755,6 +539,7 @@ static class TestSerializableMap implements JsonSerializable { @Override public String toJson() { JsonValue v = new JsonValue(new HashMap<>()); + //noinspection DataFlowIssue // NO ISSUE, WE KNOW jv.map is NOT NULL v.map.put("a", new JsonValue("A")); v.map.put("b", new JsonValue("B")); v.map.put("c", new JsonValue("C")); @@ -766,6 +551,7 @@ static class TestSerializableList implements JsonSerializable { @Override public String toJson() { JsonValue v = new JsonValue(new ArrayList<>()); + //noinspection DataFlowIssue // NO ISSUE, WE KNOW jv.array is NOT NULL v.array.add(new JsonValue("X")); v.array.add(new JsonValue("Y")); v.array.add(new JsonValue("Z")); @@ -778,6 +564,7 @@ public void testValueUtilsInstanceList() { List list = new ArrayList<>(); list.add("Hello"); list.add(""); + list.add(" "); list.add('c'); list.add(1); list.add(1L); @@ -792,31 +579,34 @@ public void testValueUtilsInstanceList() { list.add(new TestSerializableMap()); list.add(new TestSerializableList()); list.add(null); - JsonValue v = instance(list); + JsonValue v = JsonValue.instance(list); assertNotNull(v.array); - assertEquals(16, v.array.size()); - assertEquals(JsonValue.Type.STRING, v.array.get(0).type); - assertEquals(JsonValue.Type.NULL, v.array.get(1).type); - assertEquals(JsonValue.Type.STRING, v.array.get(2).type); - assertEquals(JsonValue.Type.INTEGER, v.array.get(3).type); - assertEquals(JsonValue.Type.LONG, v.array.get(4).type); - assertEquals(JsonValue.Type.DOUBLE, v.array.get(5).type); - assertEquals(JsonValue.Type.FLOAT, v.array.get(6).type); - assertEquals(JsonValue.Type.BIG_DECIMAL, v.array.get(7).type); - assertEquals(JsonValue.Type.BIG_INTEGER, v.array.get(8).type); - assertEquals(JsonValue.Type.BOOL, v.array.get(9).type); - assertEquals(JsonValue.Type.MAP, v.array.get(10).type); - assertEquals(JsonValue.Type.ARRAY, v.array.get(11).type); - assertEquals(JsonValue.Type.ARRAY, v.array.get(12).type); - assertEquals(JsonValue.Type.MAP, v.array.get(13).type); - assertEquals(JsonValue.Type.ARRAY, v.array.get(14).type); - assertEquals(JsonValue.Type.NULL, v.array.get(15).type); + assertEquals(17, v.array.size()); + int ix = 0; + assertEquals(JsonValue.Type.STRING, v.array.get(ix).type); + assertEquals(JsonValue.Type.STRING, v.array.get(++ix).type); + assertEquals(JsonValue.Type.STRING, v.array.get(++ix).type); + assertEquals(JsonValue.Type.STRING, v.array.get(++ix).type); + assertEquals(JsonValue.Type.INTEGER, v.array.get(++ix).type); + assertEquals(JsonValue.Type.LONG, v.array.get(++ix).type); + assertEquals(JsonValue.Type.DOUBLE, v.array.get(++ix).type); + assertEquals(JsonValue.Type.FLOAT, v.array.get(++ix).type); + assertEquals(JsonValue.Type.BIG_DECIMAL, v.array.get(++ix).type); + assertEquals(JsonValue.Type.BIG_INTEGER, v.array.get(++ix).type); + assertEquals(JsonValue.Type.BOOL, v.array.get(++ix).type); + assertEquals(JsonValue.Type.MAP, v.array.get(++ix).type); + assertEquals(JsonValue.Type.ARRAY, v.array.get(++ix).type); + assertEquals(JsonValue.Type.ARRAY, v.array.get(++ix).type); + assertEquals(JsonValue.Type.MAP, v.array.get(++ix).type); + assertEquals(JsonValue.Type.ARRAY, v.array.get(++ix).type); } @Test public void testValueUtilsInstanceMap() { - Map map = new HashMap<>(); + Map map = new HashMap<>(); map.put("string", "Hello"); + map.put("empty_is_string", ""); + map.put("space_is_string", " "); map.put("char", 'c'); map.put("int", 1); map.put("long", Long.MAX_VALUE); @@ -833,14 +623,15 @@ public void testValueUtilsInstanceMap() { map.put("jv", JsonValue.EMPTY_MAP); map.put("null", null); map.put("jvNull", JsonValue.NULL); - map.put("empty_is_null", ""); - validateMap(true, false, instance(map)); + validateMap(instance(map), false, false); } @Test public void testValueUtilsMapBuilder() { - MapBuilder builder = mapBuilder() + MapBuilder builder = MapBuilder.instance() .put("string", "Hello") + .put("empty_is_string", "") + .put("space_is_string", " ") .put("char", 'c') .put("int", 1) .put("long", Long.MAX_VALUE) @@ -856,15 +647,33 @@ public void testValueUtilsMapBuilder() { .put("slist", new TestSerializableList()) .put("jv", JsonValue.EMPTY_MAP) .put("null", null) - .put("jvNull", JsonValue.NULL) - .put("empty_is_null", ""); - validateMap(false, false, builder.toJsonValue()); - validateMap(false, true, JsonParser.parseUnchecked(builder.toJson())); + .put("jvNull", JsonValue.NULL); + + builder.jv.toJson(); // COVERAGE + validateMap(builder.toJsonValue(), false, false); + validateMap(JsonParser.parseUnchecked(builder.toJson()), true, false); + validateMap(JsonParser.parseUnchecked(builder.toJson(), KEEP_NULLS), true, true); + + //noinspection DataFlowIssue // NO ISSUE, WE KNOW jv.map is NOT NULL + builder.jv.map.put("jvNull", JsonValue.NULL); // because the original map builder does not put nulls + MapBuilder builder2 = MapBuilder.instance() + .put("map", builder.jv.map); + //noinspection DataFlowIssue // NO ISSUE, WE KNOW jv.map is NOT NULL + validateMap(builder2.jv.map.get("map"), false, false); + + builder2 = MapBuilder.instance().putEntries(builder.jv.map); + validateMap(builder2.jv, false, false); + + builder2 = MapBuilder.instance().putEntries(null); + assertNotNull(builder2.jv.map); + assertTrue(builder2.jv.map.isEmpty()); } - private static void validateMap(boolean checkNull, boolean parsed, JsonValue v) { + private static void validateMap(JsonValue v, boolean parsed, boolean parsedKeepNulls) { assertNotNull(v.map); assertEquals(JsonValue.Type.STRING, v.map.get("string").type); + assertEquals(JsonValue.Type.STRING, v.map.get("empty_is_string").type); + assertEquals(JsonValue.Type.STRING, v.map.get("space_is_string").type); assertEquals(JsonValue.Type.STRING, v.map.get("char").type); assertEquals(JsonValue.Type.INTEGER, v.map.get("int").type); assertEquals(JsonValue.Type.LONG, v.map.get("long").type); @@ -877,6 +686,13 @@ private static void validateMap(boolean checkNull, boolean parsed, JsonValue v) assertEquals(JsonValue.Type.DOUBLE, v.map.get("double").type); assertEquals(JsonValue.Type.FLOAT, v.map.get("float").type); assertEquals(JsonValue.Type.BIG_INTEGER, v.map.get("bi").type); + assertEquals(JsonValue.Type.NULL, v.map.get("jvNull").type); + } + if (parsed && !parsedKeepNulls) { + assertEquals(17, v.map.size()); + } + else { + assertEquals(19, v.map.size()); } assertEquals(JsonValue.Type.BIG_DECIMAL, v.map.get("bd").type); assertEquals(JsonValue.Type.BOOL, v.map.get("bool").type); @@ -886,15 +702,6 @@ private static void validateMap(boolean checkNull, boolean parsed, JsonValue v) assertEquals(JsonValue.Type.MAP, v.map.get("smap").type); assertEquals(JsonValue.Type.ARRAY, v.map.get("slist").type); assertEquals(JsonValue.Type.MAP, v.map.get("jv").type); - if (checkNull) { - assertEquals(18, v.map.size()); - assertEquals(JsonValue.Type.NULL, v.map.get("null").type); - assertEquals(JsonValue.Type.NULL, v.map.get("jvNull").type); - assertEquals(JsonValue.Type.NULL, v.map.get("empty_is_null").type); - } - else { - assertEquals(15, v.map.size()); - } } @Test @@ -916,12 +723,12 @@ public void testValueUtilsInstanceArray() { list.add(JsonValue.EMPTY_MAP); list.add(null); list.add(JsonValue.NULL); - validateArray(true, false, instance(list)); + validateArray(false, JsonValue.instance(list)); } @Test public void testValueUtilsArrayBuilder() { - ArrayBuilder builder = arrayBuilder() + ArrayBuilder builder = ArrayBuilder.instance() .add("Hello") .add('c') .add(1) @@ -938,11 +745,18 @@ public void testValueUtilsArrayBuilder() { .add(JsonValue.EMPTY_MAP) .add(null) .add(JsonValue.NULL); - validateArray(false, false, builder.toJsonValue()); - validateArray(false, true, JsonParser.parseUnchecked(builder.toJson())); + validateArray(false, builder.toJsonValue()); + validateArray(true, JsonParser.parseUnchecked(builder.toJson())); + + ArrayBuilder builder2 = ArrayBuilder.instance().addItems(builder.jv.array); + validateArray(false, builder2.jv); + + builder2 = ArrayBuilder.instance().addItems(null); + assertNotNull(builder2.jv.array); + assertTrue(builder2.jv.array.isEmpty()); } - private static void validateArray(boolean checkNull, boolean parsed, JsonValue v) { + private static void validateArray(boolean parsed, JsonValue v) { assertNotNull(v.array); assertEquals(JsonValue.Type.STRING, v.array.get(0).type); assertEquals(JsonValue.Type.STRING, v.array.get(1).type); @@ -958,6 +772,7 @@ private static void validateArray(boolean checkNull, boolean parsed, JsonValue v assertEquals(JsonValue.Type.FLOAT, v.array.get(5).type); assertEquals(JsonValue.Type.BIG_INTEGER, v.array.get(7).type); } + assertEquals(16, v.array.size()); assertEquals(JsonValue.Type.BIG_DECIMAL, v.array.get(6).type); assertEquals(JsonValue.Type.BOOL, v.array.get(8).type); assertEquals(JsonValue.Type.MAP, v.array.get(9).type); @@ -965,30 +780,34 @@ private static void validateArray(boolean checkNull, boolean parsed, JsonValue v assertEquals(JsonValue.Type.MAP, v.array.get(11).type); assertEquals(JsonValue.Type.ARRAY, v.array.get(12).type); assertEquals(JsonValue.Type.MAP, v.array.get(13).type); - if (checkNull) { - assertEquals(16, v.array.size()); - assertEquals(JsonValue.Type.NULL, v.array.get(14).type); - assertEquals(JsonValue.Type.NULL, v.array.get(15).type); - } - else { - assertEquals(14, v.array.size()); - } } @Test - public void testReadStringStringMap() { - JsonValue jv = mapBuilder() - .put("stringString", mapBuilder().put("a", "A").put("b", "B").toJsonValue()) - .put("empty", new HashMap<>()) - .put("string", "string") - .toJsonValue(); - - assertNull(readStringStringMap(jv, "string")); - assertNull(readStringStringMap(jv, "empty")); - Map stringString = readStringStringMap(jv, "stringString"); - assertNotNull(stringString); - assertEquals(2, stringString.size()); - assertEquals("A", stringString.get("a")); - assertEquals("B", stringString.get("b")); + public void testJsonParseException() { + Exception cause = new Exception("cause"); + JsonParseException e1 = new JsonParseException("e1"); + JsonParseException e2 = new JsonParseException("e2", cause); + JsonParseException e3 = new JsonParseException(cause); + assertEquals("e1", e1.getMessage()); + assertNull(e1.getCause()); + assertEquals("e2", e2.getMessage()); + assertNotNull(e2.getCause()); + assertEquals("java.lang.Exception: cause", e3.getMessage()); + assertNotNull(e3.getCause()); + + } + + @Test + public void testCoverageAndEdges() { + ArrayBuilder arrayBuilder = ArrayBuilder.instance(); + for (String u : UTF_STRINGS) { + arrayBuilder.add(u); + } + arrayBuilder.add("hasU\0U"); + + String json = arrayBuilder.toJson(); + JsonValue jv = JsonParser.parseUnchecked(json); + String json2 = jv.toJson(); + assertEquals(json, json2); } } diff --git a/src/test/java/io/nats/json/JsonValueUtilsTests.java b/src/test/java/io/nats/json/JsonValueUtilsTests.java new file mode 100644 index 0000000..2b48657 --- /dev/null +++ b/src/test/java/io/nats/json/JsonValueUtilsTests.java @@ -0,0 +1,554 @@ +// Copyright 2025 The NATS Authors +// 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: +// +// 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 io.nats.json; + +import io.ResourceUtils; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static io.nats.json.JsonValue.*; +import static io.nats.json.JsonValueUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +public final class JsonValueUtilsTests { + private static final String TEST_JSON = ResourceUtils.resourceAsString("test.json"); + private static final JsonValue TEST_JV = JsonParser.parseUnchecked(TEST_JSON); + private static final String STRING_STRING = "Hello"; + private static final String BASE64_STRING = "AGFiY2RlZgECBAg="; + private static final String DATE_STRING = "2021-01-25T20:09:10.6225191Z"; + private static final byte[] BASE64_DECODED = new byte[] {0, 'a', 'b', 'c', 'd', 'e', 'f', 1, 2, 4, 8}; + private static final ZonedDateTime TEST_DATE = DateTimeUtils.parseDateTime(DATE_STRING); + + private static final String STRING = "string"; + private static final String INTEGER = "integer"; + private static final String LONG = "long"; + private static final String BIG_DECIMAL = "big_decimal"; + private static final String BOOL = "bool"; + private static final String DATE = "date"; + private static final String NANOS = "nanos"; + private static final String BASE_64 = "base_64"; + private static final String MAP = "map"; + private static final String ARRAY = "array"; + private static final String MMAP = "mmap"; + private static final String SMAP = "smap"; + private static final String SLIST = "slist"; + private static final String MLIST = "mlist"; + private static final String ILIST = "ilist"; + private static final String LLIST = "llist"; + private static final String NLIST = "nlist"; + private static final String NOT_A_KEY = "not-a-key"; + + @Test + public void testRead() { + assertNotNull(read(TEST_JV, STRING, v -> v)); + + // these JsonValues are not MAPS + assertNull(read(null, NOT_A_KEY, v -> v)); + assertNull(read(EMPTY_ARRAY, NOT_A_KEY, v -> v)); + assertNull(read(TRUE, NOT_A_KEY, v -> v)); + assertNull(read(FALSE, NOT_A_KEY, v -> v)); + assertNull(read(NULL, NOT_A_KEY, v -> v)); + } + + @Test + public void testReadStrings() { + String s = readString(TEST_JV, STRING); + assertEquals(STRING_STRING, s); + + byte[] b = readBytes(TEST_JV, STRING); + assertNotNull(b); + assertEquals(s, new String(b)); + + assertEquals(DATE_STRING, readString(TEST_JV, DATE)); + assertEquals(BASE64_STRING, readString(TEST_JV, BASE_64)); + + ZonedDateTime zdt = readDate(TEST_JV, DATE); + assertEquals(TEST_DATE, zdt); + + b = readBase64(TEST_JV, BASE_64); + assertArrayEquals(BASE64_DECODED, b); + assertNull(readBase64(TEST_JV, INTEGER)); + + assertNull(readString(TEST_JV, INTEGER)); + assertNull(readString(TEST_JV, LONG)); + assertNull(readString(TEST_JV, BOOL)); + assertNull(readString(TEST_JV, MAP)); + assertNull(readString(TEST_JV, ARRAY)); + assertNull(readString(TEST_JV, NOT_A_KEY)); + + String dflt = "dflt"; + assertEquals(dflt, readString(TEST_JV, INTEGER, dflt)); + assertEquals(dflt, readString(TEST_JV, LONG, dflt)); + assertEquals(dflt, readString(TEST_JV, BOOL, dflt)); + assertEquals(dflt, readString(TEST_JV, MAP, dflt)); + assertEquals(dflt, readString(TEST_JV, ARRAY, dflt)); + assertEquals(dflt, readString(TEST_JV, NOT_A_KEY, dflt)); + + assertNull(readString(TEST_JV, NOT_A_KEY)); + assertNull(readString(TEST_JV, INTEGER)); + + assertEquals(STRING_STRING, readString(TEST_JV, STRING, dflt)); + assertEquals(dflt, readString(TEST_JV, NOT_A_KEY, dflt)); + assertEquals(dflt, readString(TEST_JV, INTEGER, dflt)); + assertEquals(dflt, readString(null, NOT_A_KEY, dflt)); + } + + @Test + public void testReadInteger() { + Integer i = readInteger(TEST_JV, INTEGER); + assertEquals(42, i); + + assertNull(readInteger(TEST_JV, STRING)); + assertNull(readInteger(TEST_JV, BOOL)); + assertNull(readInteger(TEST_JV, MAP)); + assertNull(readInteger(TEST_JV, ARRAY)); + assertNull(readInteger(TEST_JV, NOT_A_KEY)); + + assertEquals(i, readInteger(TEST_JV, STRING, i)); + assertEquals(i, readInteger(TEST_JV, BOOL, i)); + assertEquals(i, readInteger(TEST_JV, MAP, i)); + assertEquals(i, readInteger(TEST_JV, ARRAY, i)); + assertEquals(i, readInteger(TEST_JV, NOT_A_KEY, i)); + + int dflt = 99; + assertEquals(42, readInteger(TEST_JV, INTEGER, dflt)); + assertEquals(dflt, readInteger(TEST_JV, STRING, dflt)); + assertEquals(dflt, readInteger(TEST_JV, BOOL, dflt)); + assertEquals(dflt, readInteger(TEST_JV, MAP, dflt)); + assertEquals(dflt, readInteger(TEST_JV, ARRAY, dflt)); + assertEquals(dflt, readInteger(TEST_JV, NOT_A_KEY, dflt)); + } + + @Test + public void testReadLong() { + assertEquals(42, readLong(TEST_JV, INTEGER)); + assertEquals(9223372036854775806L, readLong(TEST_JV, LONG)); + + assertNull(readLong(TEST_JV, STRING)); + assertNull(readLong(TEST_JV, BOOL)); + assertNull(readLong(TEST_JV, MAP)); + assertNull(readLong(TEST_JV, ARRAY)); + assertNull(readLong(TEST_JV, NOT_A_KEY)); + + long dflt = 99; + assertEquals(dflt, readLong(TEST_JV, STRING, dflt)); + assertEquals(dflt, readLong(TEST_JV, BOOL, dflt)); + assertEquals(dflt, readLong(TEST_JV, MAP, dflt)); + assertEquals(dflt, readLong(TEST_JV, ARRAY, dflt)); + assertEquals(dflt, readLong(TEST_JV, NOT_A_KEY, dflt)); + + assertEquals(42, readLong(TEST_JV, INTEGER, dflt)); + assertEquals(9223372036854775806L, readLong(TEST_JV, LONG, dflt)); + assertEquals(dflt, readLong(TEST_JV, STRING, dflt)); + assertEquals(dflt, readLong(TEST_JV, BOOL, dflt)); + assertEquals(dflt, readLong(TEST_JV, MAP, dflt)); + assertEquals(dflt, readLong(TEST_JV, ARRAY, dflt)); + assertEquals(dflt, readLong(TEST_JV, NOT_A_KEY, dflt)); + } + + @Test + public void testReadBoolean() { + assertTrue(readBoolean(TEST_JV, BOOL)); + + assertFalse(readBoolean(TEST_JV, STRING)); + assertFalse(readBoolean(TEST_JV, INTEGER)); + assertFalse(readBoolean(TEST_JV, LONG)); + assertFalse(readBoolean(TEST_JV, MAP)); + assertFalse(readBoolean(TEST_JV, ARRAY)); + assertFalse(readBoolean(TEST_JV, NOT_A_KEY)); + + assertTrue(readBoolean(TEST_JV, STRING, true)); + assertTrue(readBoolean(TEST_JV, INTEGER, true)); + assertTrue(readBoolean(TEST_JV, LONG, true)); + assertTrue(readBoolean(TEST_JV, MAP, true)); + assertTrue(readBoolean(TEST_JV, ARRAY, true)); + assertTrue(readBoolean(TEST_JV, NOT_A_KEY, true)); + + assertFalse(readBoolean(TEST_JV, STRING, false)); + assertFalse(readBoolean(TEST_JV, INTEGER, false)); + assertFalse(readBoolean(TEST_JV, LONG, false)); + assertFalse(readBoolean(TEST_JV, MAP, false)); + assertFalse(readBoolean(TEST_JV, ARRAY, false)); + assertFalse(readBoolean(TEST_JV, NOT_A_KEY, false)); + } + + @Test + public void testReadDate() { + ZonedDateTime t = readDate(TEST_JV, DATE); + assertEquals(TEST_DATE, t); + + assertThrows(DateTimeParseException.class, () -> readDate(TEST_JV, STRING)); + assertThrows(DateTimeParseException.class, () -> readDate(TEST_JV, BASE_64)); + + assertNull(readDate(TEST_JV, BOOL)); + assertNull(readDate(TEST_JV, MAP)); + assertNull(readDate(TEST_JV, ARRAY)); + assertNull(readDate(TEST_JV, NOT_A_KEY)); + } + + @Test + public void testReadNanosAsDuration() { + assertEquals(Duration.ofSeconds(1), readNanosAsDuration(TEST_JV, NANOS)); + + assertNull(readNanosAsDuration(TEST_JV, STRING)); + assertNull(readNanosAsDuration(TEST_JV, BOOL)); + assertNull(readNanosAsDuration(TEST_JV, MAP)); + assertNull(readNanosAsDuration(TEST_JV, ARRAY)); + assertNull(readNanosAsDuration(TEST_JV, NOT_A_KEY)); + + Duration dflt = Duration.ofSeconds(99); + assertEquals(Duration.ofSeconds(1), readNanosAsDuration(TEST_JV, NANOS, dflt)); + assertEquals(dflt, readNanosAsDuration(TEST_JV, STRING, dflt)); + assertEquals(dflt, readNanosAsDuration(TEST_JV, BOOL, dflt)); + assertEquals(dflt, readNanosAsDuration(TEST_JV, MAP, dflt)); + assertEquals(dflt, readNanosAsDuration(TEST_JV, ARRAY, dflt)); + assertEquals(dflt, readNanosAsDuration(TEST_JV, NOT_A_KEY, dflt)); + } + + @Test + public void testObjectAndMaps() { + // smap has all string values + List jvvs = new ArrayList<>(); + jvvs.add(readMapObjectOrNull(TEST_JV, SMAP)); + jvvs.add(readMapObjectOrEmpty(TEST_JV, SMAP)); + for (JsonValue jvv : jvvs) { + assertEquals("A", readString(jvv, "a")); + assertEquals("B", readString(jvv, "b")); + assertEquals("C", readString(jvv, "c")); + } + + List> maps = new ArrayList<>(); + maps.add(readMapMapOrNull(TEST_JV, SMAP)); + maps.add(readMapMapOrEmpty(TEST_JV, SMAP)); + for (Map map : maps) { + assertNotNull(map); + assertEquals(3, map.size()); + assertEquals("A", map.get("a").string); + assertEquals("B", map.get("b").string); + assertEquals("C", map.get("c").string); + } + + // mmap has different types of values + jvvs.clear(); + jvvs.add(readMapObjectOrNull(TEST_JV, MMAP)); + jvvs.add(readMapObjectOrEmpty(TEST_JV, MMAP)); + for (JsonValue jvv : jvvs) { + assertEquals("ss", readString(jvv, "s")); + assertEquals(73, readInteger(jvv, "i")); + } + + maps.clear(); + maps.add(readMapMapOrNull(TEST_JV, MMAP)); + maps.add(readMapMapOrEmpty(TEST_JV, MMAP)); + for (Map map : maps) { + assertNotNull(map); + assertEquals(2, map.size()); + assertEquals("ss", map.get("s").string); + assertEquals(73, map.get("i").i); + } + + List> strmaps = new ArrayList<>(); + strmaps.add(readStringMapOrNull(TEST_JV, MMAP)); + strmaps.add(readStringMapOrEmpty(TEST_JV, MMAP)); + for (Map strmap : strmaps) { + assertNotNull(strmap); + assertEquals(1, strmap.size()); + assertEquals("ss", strmap.get("s")); + assertNull(strmap.get("i")); + } + } + + @Test + public void testArrays() { + assertNull(listOfOrNull(null, jv -> jv)); + assertNull(listOfOrNull(readValue(TEST_JV, STRING), jv -> jv)); + assertTrue(listOfOrEmpty(null, jv -> jv).isEmpty()); + assertTrue(listOfOrEmpty(readValue(TEST_JV, STRING), jv -> jv).isEmpty()); + + assertNull(readArrayOrNull(TEST_JV, STRING)); + assertTrue(readArrayOrEmpty(TEST_JV, STRING).isEmpty()); + + // slist has just strings + List> arrays = new ArrayList<>(); + arrays.add(readArrayOrNull(TEST_JV, SLIST)); + arrays.add(readArrayOrEmpty(TEST_JV, SLIST)); + for (List array : arrays) { + assertNotNull(array); + assertEquals(3, array.size()); + assertTrue(array.contains(new JsonValue("X"))); + assertTrue(array.contains(new JsonValue("Y"))); + assertTrue(array.contains(new JsonValue("Z"))); + } + + // mlist has a mix of value types + arrays.clear(); + arrays.add(readArrayOrNull(TEST_JV, MLIST)); + arrays.add(readArrayOrEmpty(TEST_JV, MLIST)); + for (List array : arrays) { + assertNotNull(array); + assertEquals(4, array.size()); + assertTrue(array.contains(new JsonValue("Q"))); + assertTrue(array.contains(new JsonValue("R"))); + assertTrue(array.contains(new JsonValue(" "))); + assertTrue(array.contains(new JsonValue(98))); + } + + assertNull(readStringListOrNull(TEST_JV, STRING)); + assertTrue(readStringListOrEmpty(TEST_JV, STRING).isEmpty()); + + List> stringLists = new ArrayList<>(); + stringLists.add(readStringListOrNull(TEST_JV, SLIST)); + stringLists.add(readStringListOrEmpty(TEST_JV, SLIST)); + for (List aos : stringLists) { + assertNotNull(aos); + assertEquals(3, aos.size()); + assertTrue(aos.contains("X")); + assertTrue(aos.contains("Y")); + assertTrue(aos.contains("Z")); + } + + stringLists.clear(); + stringLists.add(readStringListOrNull(TEST_JV, MLIST)); + stringLists.add(readStringListOrEmpty(TEST_JV, MLIST)); + for (List aos : stringLists) { + assertNotNull(aos); + assertEquals(3, aos.size()); + assertTrue(aos.contains("Q")); + assertTrue(aos.contains("R")); + assertTrue(aos.contains(" ")); + } + + assertNull(readIntegerListOrNull(TEST_JV, STRING)); + assertTrue(readIntegerListOrEmpty(TEST_JV, STRING).isEmpty()); + + List> intLists = new ArrayList<>(); + intLists.add(readIntegerListOrNull(TEST_JV, SLIST)); + intLists.add(readIntegerListOrEmpty(TEST_JV, SLIST)); + intLists.add(readIntegerListOrNull(TEST_JV, LLIST)); + intLists.add(readIntegerListOrEmpty(TEST_JV, LLIST)); + for (List aoi : intLists) { + assertTrue(aoi.isEmpty()); + } + + intLists.clear(); + intLists.add(readIntegerListOrNull(TEST_JV, MLIST)); + intLists.add(readIntegerListOrEmpty(TEST_JV, MLIST)); + for (List aoi : intLists) { + assertNotNull(aoi); + assertEquals(1, aoi.size()); + assertTrue(aoi.contains(98)); + } + + intLists.clear(); + intLists.add(readIntegerListOrNull(TEST_JV, ILIST)); + intLists.add(readIntegerListOrEmpty(TEST_JV, ILIST)); + for (List aoi : intLists) { + assertNotNull(aoi); + assertEquals(3, aoi.size()); + assertTrue(aoi.contains(42)); + assertTrue(aoi.contains(73)); + assertTrue(aoi.contains(99)); + } + + assertNull(readLongListOrNull(TEST_JV, STRING)); + assertTrue(readLongListOrEmpty(TEST_JV, STRING).isEmpty()); + + List> longLists = new ArrayList<>(); + longLists.add(readLongListOrNull(TEST_JV, SLIST)); + longLists.add(readLongListOrEmpty(TEST_JV, SLIST)); + for (List aol : longLists) { + assertTrue(aol.isEmpty()); + } + + longLists.clear(); + longLists.add(readLongListOrNull(TEST_JV, MLIST)); + longLists.add(readLongListOrEmpty(TEST_JV, MLIST)); + for (List aol : longLists) { + assertNotNull(aol); + assertEquals(1, aol.size()); + assertTrue(aol.contains(98L)); + } + + longLists.clear(); + longLists.add(readLongListOrNull(TEST_JV, ILIST)); + longLists.add(readLongListOrEmpty(TEST_JV, ILIST)); + for (List aol : longLists) { + assertNotNull(aol); + assertEquals(3, aol.size()); + assertTrue(aol.contains(42L)); + assertTrue(aol.contains(73L)); + assertTrue(aol.contains(99L)); + } + + longLists.clear(); + longLists.add(readLongListOrNull(TEST_JV, LLIST)); + longLists.add(readLongListOrEmpty(TEST_JV, LLIST)); + for (List aol : longLists) { + assertNotNull(aol); + assertEquals(3, aol.size()); + assertTrue(aol.contains(9223372036854775801L)); + assertTrue(aol.contains(9223372036854775802L)); + assertTrue(aol.contains(9223372036854775803L)); + } + + assertNull(readNanosAsDurationListOrNull(TEST_JV, STRING)); + assertTrue(readNanosAsDurationListOrEmpty(TEST_JV, STRING).isEmpty()); + + List> durLists = new ArrayList<>(); + durLists.add(readNanosAsDurationListOrNull(TEST_JV, NLIST)); + durLists.add(readNanosAsDurationListOrEmpty(TEST_JV, NLIST)); + for (List aod : durLists) { + assertNotNull(aod); + assertEquals(3, aod.size()); + assertTrue(aod.contains(Duration.ofSeconds(2))); + assertTrue(aod.contains(Duration.ofSeconds(3))); + assertTrue(aod.contains(Duration.ofSeconds(4))); + } + } + + @Test + public void testNotFoundOrWrongType() { + validateNotFoundOrWrongType(STRING, true, false, true, true, true, true, true, true); + validateNotFoundOrWrongType(INTEGER, true, true, true, false, false, true, true, true); + validateNotFoundOrWrongType(LONG, true, true, true, true, false, true, true, true); + validateNotFoundOrWrongType(BOOL, true, true, true, true, true, false, true, true); + validateNotFoundOrWrongType(DATE, true, false, false, true, true, false, true, true); + validateNotFoundOrWrongType(BASE_64, true, false, true, true, true, true, true, true); + validateNotFoundOrWrongType(BIG_DECIMAL, true, true, true, true, true, true, true, true); + validateNotFoundOrWrongType(MAP, true, true, true, true, true, true, false, true); + validateNotFoundOrWrongType(ARRAY, true, true, true, true, true, true, true, false); + validateNotFoundOrWrongType(SMAP, true, true, true, true, true, true, false, true); + validateNotFoundOrWrongType(SLIST, true, true, true, true, true, true, true, false); + validateNotFoundOrWrongType(NOT_A_KEY, false, true, true, true, true, true, true, true); + } + + private static void validateNotFoundOrWrongType( + String key, + boolean isKey, + boolean notString, + boolean notDate, + boolean notInteger, + boolean notLong, + boolean notBoolean, + boolean notMap, + boolean notArray) + { + JsonValue jv = readValue(TEST_JV, key); + if (isKey) { + assertNotNull(jv); + } + else { + assertNull(jv); + } + if (notString) { + assertNull(readBytes(TEST_JV, key)); + assertNull(readDate(TEST_JV, key)); + if (jv != null) { + assertNotSame(Type.STRING, jv.type); + assertNull(jv.string); + } + } + if (notDate) { + if (jv == null || jv.string == null) { + assertNull(readDate(TEST_JV, key)); + } + else { + assertThrows(DateTimeParseException.class, () -> readDate(TEST_JV, key)); + } + } + if (notInteger) { + assertNull(readInteger(TEST_JV, key)); + assertEquals(-1, readInteger(TEST_JV, key, -1)); + } + if (notLong) { + assertNull(readLong(TEST_JV, key)); + assertEquals(-1, readLong(TEST_JV, key, -1)); + } + if (notBoolean) { + assertFalse(readBoolean(TEST_JV, key)); + assertFalse(readBoolean(TEST_JV, key, false)); + } + + if (notMap) { + assertNull(readMapObjectOrNull(TEST_JV, key)); + assertEquals(EMPTY_MAP, readMapObjectOrEmpty(TEST_JV, key)); + assertNull(readMapMapOrNull(TEST_JV, key)); + assertEquals(EMPTY_MAP.map, readMapMapOrEmpty(TEST_JV, key)); + assertNull(readStringMapOrNull(TEST_JV, key)); + assertEquals(new HashMap<>(), readStringMapOrEmpty(TEST_JV, key)); + if (jv != null) { + assertNotSame(Type.MAP, jv.type); + assertNull(jv.map); + } + } + if (notArray) { + assertNull(readArrayOrNull(TEST_JV, key)); + assertEquals(EMPTY_ARRAY.array, readArrayOrEmpty(TEST_JV, key)); + if (jv != null) { + assertNotSame(Type.ARRAY, jv.type); + assertNull(jv.array); + } + } + } + + @Test + public void testGetIntLong() { + JsonValue x = readValue(TEST_JV, STRING); + JsonValue i = new JsonValue(Integer.MAX_VALUE); + JsonValue li = new JsonValue((long)Integer.MAX_VALUE); + JsonValue lmax = new JsonValue(Long.MAX_VALUE); + JsonValue lmin = new JsonValue(Long.MIN_VALUE); + + assertEquals(Integer.MAX_VALUE, getInt(i, -1)); + assertEquals(Integer.MAX_VALUE, getInt(li, -1)); + assertEquals(-1, getInt(x, -1)); + assertEquals(-1, getInt(JsonValue.NULL, -1)); + assertEquals(-1, getInt(EMPTY_MAP, -1)); + assertEquals(-1, getInt(EMPTY_ARRAY, -1)); + + assertEquals(Integer.MAX_VALUE, getInteger(i)); + assertEquals(Integer.MAX_VALUE, getInteger(li)); + assertNull(getInteger(x)); + assertNull(getInteger(lmax)); + assertNull(getInteger(lmin)); + assertNull(getInteger(JsonValue.NULL)); + assertNull(getInteger(EMPTY_MAP)); + assertNull(getInteger(EMPTY_ARRAY)); + + assertEquals(Integer.MAX_VALUE, getLong(i)); + assertEquals(Integer.MAX_VALUE, getLong(li)); + assertEquals(Long.MAX_VALUE, getLong(lmax)); + assertEquals(Long.MIN_VALUE, getLong(lmin)); + assertNull(getLong(x)); + assertNull(getLong(JsonValue.NULL)); + assertNull(getLong(EMPTY_MAP)); + assertNull(getLong(EMPTY_ARRAY)); + + assertEquals(Integer.MAX_VALUE, getLong(i, -1)); + assertEquals(Integer.MAX_VALUE, getLong(li, -1)); + assertEquals(Long.MAX_VALUE, getLong(lmax, -1)); + assertEquals(Long.MIN_VALUE, getLong(lmin, -1)); + assertEquals(-1, getLong(x, -1)); + assertEquals(-1, getLong(JsonValue.NULL, -1)); + assertEquals(-1, getLong(EMPTY_MAP, -1)); + assertEquals(-1, getLong(EMPTY_ARRAY, -1)); + } +} diff --git a/src/test/java/io/nats/json/JsonWriteUtilsTests.java b/src/test/java/io/nats/json/JsonWriteUtilsTests.java index 70af1ef..ce85fae 100644 --- a/src/test/java/io/nats/json/JsonWriteUtilsTests.java +++ b/src/test/java/io/nats/json/JsonWriteUtilsTests.java @@ -1,4 +1,4 @@ -// Copyright 2015-2024 The NATS Authors +// Copyright 2025 The NATS Authors // 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: @@ -13,43 +13,15 @@ package io.nats.json; -import static io.nats.json.DateTimeUtils.DEFAULT_TIME; -import static io.nats.json.JsonWriteUtils._addList; -import static io.nats.json.JsonWriteUtils.addDurations; -import static io.nats.json.JsonWriteUtils.addEnumWhenNot; -import static io.nats.json.JsonWriteUtils.addField; -import static io.nats.json.JsonWriteUtils.addFieldAsNanos; -import static io.nats.json.JsonWriteUtils.addFieldEvenEmpty; -import static io.nats.json.JsonWriteUtils.addFieldWhenGreaterThan; -import static io.nats.json.JsonWriteUtils.addFieldWhenGtZero; -import static io.nats.json.JsonWriteUtils.addFieldWhenGteMinusOne; -import static io.nats.json.JsonWriteUtils.addFldWhenTrue; -import static io.nats.json.JsonWriteUtils.addJsons; -import static io.nats.json.JsonWriteUtils.addRawJson; -import static io.nats.json.JsonWriteUtils.addStrings; -import static io.nats.json.JsonWriteUtils.beginFormattedJson; -import static io.nats.json.JsonWriteUtils.beginJson; -import static io.nats.json.JsonWriteUtils.beginJsonPrefixed; -import static io.nats.json.JsonWriteUtils.endFormattedJson; -import static io.nats.json.JsonWriteUtils.endJson; -import static io.nats.json.JsonWriteUtils.printFormatted; -import static io.nats.json.JsonWriteUtils.safeParseLong; -import static io.nats.json.JsonWriteUtils.toKey; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; import java.time.Duration; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; -import org.junit.jupiter.api.Test; - -import io.ResourceUtils; +import static io.nats.json.DateTimeUtils.DEFAULT_TIME; +import static io.nats.json.JsonWriteUtils.*; +import static org.junit.jupiter.api.Assertions.*; public final class JsonWriteUtilsTests { @@ -161,7 +133,7 @@ public void testAddFields() { addFieldWhenGtZero(sb, "longnotgt0", 0L); assertEquals(87, sb.length()); - addFieldWhenGtZero(sb, "intgt0", 1L); + addFieldWhenGtZero(sb, "intgt0", 1); assertEquals(98, sb.length()); addFieldWhenGtZero(sb, "longgt0", 1L); @@ -290,8 +262,6 @@ public void testAddFields() { jlist.add(jv); addJsons(sb, "jsons", jlist); assertEquals(351, sb.length()); - System.out.println(sb); - System.out.println(sb.length()); } @Test @@ -311,14 +281,6 @@ public void testParseLong() { assertEquals(1, safeParseLong("1")); } - @Test - public void testMiscCoverage() { - String json = ResourceUtils.resourceAsString("StreamInfo.json"); - printFormatted(JsonParser.parseUnchecked(json)); - - assertEquals("\"JsonWriteUtilsTests\":", toKey(this.getClass())); - } - @Test public void testMapEquals() { Map map1 = new HashMap<>(); diff --git a/src/test/resources/StreamInfo.json b/src/test/resources/stream-info.json similarity index 92% rename from src/test/resources/StreamInfo.json rename to src/test/resources/stream-info.json index 3834d53..cb3afcb 100644 --- a/src/test/resources/StreamInfo.json +++ b/src/test/resources/stream-info.json @@ -112,5 +112,17 @@ {"src":"18_st_src1","dest":"18_st_dest1"} ] } + ], + "alternates": [ + { + "name": "alt19", + "domain": "domain19", + "cluster": "cluster19" + }, + { + "name": "alt20", + "domain": "domain20", + "cluster": "cluster20" + } ] } diff --git a/src/test/resources/test.json b/src/test/resources/test.json new file mode 100644 index 0000000..b142fbd --- /dev/null +++ b/src/test/resources/test.json @@ -0,0 +1,70 @@ +{ + "string": "Hello", + "integer": 42, + "long": 9223372036854775806, + "big_decimal": 123.0, + "bool": true, + "date": "2021-01-25T20:09:10.6225191Z", + "nanos": 1000000000, + "base_64": "AGFiY2RlZgECBAg=", + "map": {}, + "array": [], + "smap": { + "a": "A", + "b": "B", + "c": "C" + }, + "mmap": { + "s": "ss", + "i": 73 + }, + "slist": [ + "X", + "Y", + "Z" + ], + "mlist": [ + "Q", + "R", + " ", + 98 + ], + "ilist": [ + 42, + 73, + 99 + ], + "llist": [ + 9223372036854775801, + 9223372036854775802, + 9223372036854775803 + ], + "nlist": [ + 2000000000, + 3000000000, + 4000000000 + ], + "levels1": { + "string": "Hello", + "integer": 42, + "long": 9223372036854775806, + "big_decimal": 123.0, + "bool": true, + "date": "2021-01-25T20:09:10.6225191Z", + "nanos": 1000000000, + "base_64": "AGFiY2RlZgECBAg=", + "map": {}, + "levels2": [ + true, + "2021-01-25T20:09:10.6225191Z", + 1000000000, + "AGFiY2RlZgECBAg=", + [ + true, + "2021-01-25T20:09:10.6225191Z", + 1000000000, + "AGFiY2RlZgECBAg=" + ] + ] + } +}