From 521e2e6d263ef610b5dddb25a634ca6e5863a31c Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Mon, 17 Mar 2025 08:22:28 +0100 Subject: [PATCH] feat(json): Add parsing support --- .../main/java/datadog/json/JsonMapper.java | 49 ++ .../main/java/datadog/json/JsonReader.java | 515 ++++++++++++++++++ .../groovy/datadog/json/JsonMapperTest.groovy | 81 +++ .../java/datadog/json/JsonReaderTest.java | 351 ++++++++++++ .../java/datadog/json/JsonWriterTest.java | 1 - .../json/src/test/resources/lorem-ipsum.json | 7 + 6 files changed, 1003 insertions(+), 1 deletion(-) create mode 100644 components/json/src/main/java/datadog/json/JsonReader.java create mode 100644 components/json/src/test/java/datadog/json/JsonReaderTest.java create mode 100644 components/json/src/test/resources/lorem-ipsum.json diff --git a/components/json/src/main/java/datadog/json/JsonMapper.java b/components/json/src/main/java/datadog/json/JsonMapper.java index 83b8c7f59b8..4a69b7d7d93 100644 --- a/components/json/src/main/java/datadog/json/JsonMapper.java +++ b/components/json/src/main/java/datadog/json/JsonMapper.java @@ -1,6 +1,12 @@ package datadog.json; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; + +import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Map; /** Utility class for simple Java structure mapping into JSON strings. */ @@ -103,4 +109,47 @@ public static String toJson(String[] items) { return writer.toString(); } } + + /** + * Parses a JSON string into a {@link Map}. + * + * @param json The JSON string to parse. + * @return A {@link Map} containing the parsed JSON object's key-value pairs. + * @throws IOException If the JSON is invalid or a reader error occurs. + */ + @SuppressWarnings("unchecked") + public static Map fromJsonToMap(String json) throws IOException { + if (json == null || json.isEmpty() || "{}".equals(json) || "null".equals(json)) { + return emptyMap(); + } + try (JsonReader reader = new JsonReader(json)) { + Object value = reader.nextValue(); + if (!(value instanceof Map)) { + throw new IOException("Expected JSON object but was " + value.getClass().getSimpleName()); + } + return (Map) value; + } + } + + /** + * Parses a JSON string array into a {@link List}. + * + * @param json The JSON string array to parse. + * @return A {@link List} containing the parsed JSON strings. + * @throws IOException If the JSON is invalid or a reader error occurs. + */ + public static List fromJsonToList(String json) throws IOException { + if (json == null || json.isEmpty() || "[]".equals(json) || "null".equals(json)) { + return emptyList(); + } + try (JsonReader reader = new JsonReader(json)) { + List list = new ArrayList<>(); + reader.beginArray(); + while (reader.hasNext()) { + list.add(reader.nextString()); + } + reader.endArray(); + return list; + } + } } diff --git a/components/json/src/main/java/datadog/json/JsonReader.java b/components/json/src/main/java/datadog/json/JsonReader.java new file mode 100644 index 00000000000..771617cf0a2 --- /dev/null +++ b/components/json/src/main/java/datadog/json/JsonReader.java @@ -0,0 +1,515 @@ +package datadog.json; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A lightweight JSON reader without dependencies. It performs minimal JSON type and structure + * checks unless using the lenient mode. + */ +public class JsonReader implements AutoCloseable { + private static final int BUFFER_SIZE = 1024; + private final Reader reader; + private final char[] buffer = new char[BUFFER_SIZE]; + private final JsonStructure structure; + private int position = 0; + private int limit = 0; + private int lineNumber = 1; + private int linePosition = 0; + + /** Creates a reader with structure check. */ + public JsonReader(String json) { + this(new StringReader(json), true); + } + + /** Creates a reader with structure check. */ + public JsonReader(Reader reader) { + this(reader, true); + } + + /** + * Creates a reader. + * + * @param reader The source reader. + * @param safe {@code true} to use safe structure check, {@code false} for lenient mode. + */ + public JsonReader(Reader reader, boolean safe) { + if (reader == null) { + throw new IllegalArgumentException("reader cannot be null"); + } + this.reader = reader; + this.structure = safe ? new SafeJsonStructure() : new LenientJsonStructure(); + } + + /** + * Begins reading a JSON object. + * + * @return This reader instance. + * @throws IOException If an I/O error occurs. + */ + public JsonReader beginObject() throws IOException { + char c = advanceUpToNextValueChar(); + if (c != '{') { + throw unexpectedSyntaxError("'{'", c); + } + advance(); + this.structure.beginObject(); + return this; + } + + /** + * Ends reading a JSON object. + * + * @return This reader instance. + * @throws IOException If an I/O error occurs. + */ + public JsonReader endObject() throws IOException { + consumeWhitespace(); + char c = peek(); + if (c != '}') { + throw unexpectedSyntaxError("'}'", c); + } + advance(); + this.structure.endObject(); + return this; + } + + /** + * Begins reading a JSON array. + * + * @return This reader instance. + * @throws IOException If an I/O error occurs. + */ + public JsonReader beginArray() throws IOException { + char c = advanceUpToNextValueChar(); + if (c != '[') { + throw unexpectedSyntaxError("'['", c); + } + advance(); + this.structure.beginArray(); + return this; + } + + /** + * Ends reading a JSON array. + * + * @return This reader instance. + * @throws IOException If an I/O error occurs. + */ + public JsonReader endArray() throws IOException { + consumeWhitespace(); + char c = peek(); + if (c != ']') { + throw unexpectedSyntaxError("']'", c); + } + advance(); + this.structure.endArray(); + return this; + } + + /** + * Reads the next property name in an object. + * + * @return The property name or null if the object is empty or ended. + * @throws IOException If an I/O error occurs. + */ + public String nextName() throws IOException { + char c = advanceUpToNextValueChar(); + if (c == '}') { + return null; + } + if (c != '"') { + throw unexpectedSyntaxError("'\"'", c); + } + String name = readString(); + consumeWhitespace(); + c = peek(); + if (c != ':') { + throw unexpectedSyntaxError("':'", c); + } + advance(); + this.structure.addName(); + return name; + } + + /** + * Checks if the current value is null. + * + * @return true if the value is null. + * @throws IOException If an I/O error occurs. + */ + public boolean isNull() throws IOException { + return advanceUpToNextValueChar() == 'n'; + } + + /** + * Reads a boolean value. + * + * @return The boolean value. + * @throws IOException If an I/O error occurs. + */ + public boolean nextBoolean() throws IOException { + char c = advanceUpToNextValueChar(); + String value; + if (c == 't') { + value = readLiteral(4); + if (!"true".equals(value)) { + throw unexpectedSyntaxError("'true'", value); + } + return true; + } else if (c == 'f') { + value = readLiteral(5); + if (!"false".equals(value)) { + throw unexpectedSyntaxError("'false'", value); + } + return false; + } + throw unexpectedSyntaxError("'true' or 'false", c); + } + + /** + * Reads a string value. + * + * @return The string value. + * @throws IOException If an I/O error occurs. + */ + public String nextString() throws IOException { + char c = advanceUpToNextValueChar(); + if (c != '"') { + throw unexpectedSyntaxError("'\"'", c); + } + return readString(); + } + + /** + * Reads a number as an int value. + * + * @return The int value. + * @throws IOException If the value is not an int, or a reader error occurs. + */ + public int nextInt() throws IOException { + String number = readNumber(); + try { + return Integer.parseInt(number); + } catch (NumberFormatException e) { + throw unexpectedSyntaxError("an integer", number); + } + } + + /** + * Reads a number as a long value. + * + * @return The long value. + * @throws IOException If the value is not a long, or a reader error occurs. + */ + public long nextLong() throws IOException { + String number = readNumber(); + try { + return Long.parseLong(number); + } catch (NumberFormatException e) { + throw unexpectedSyntaxError("a long", number); + } + } + + /** + * Reads a number as a double value. + * + * @return The double value. + * @throws IOException If an I/O error occurs. + */ + public double nextDouble() throws IOException { + String number = readNumber(); + try { + return Double.parseDouble(number); + } catch (NumberFormatException e) { + throw unexpectedSyntaxError("a double", number); + } + } + + /** + * Checks if there are more elements in the current array or object. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + * @throws IOException If an I/O error occurs. + */ + public boolean hasNext() throws IOException { + consumeWhitespace(); + char c = peek(); + return c != ']' && c != '}'; + } + + /** + * Reads the next value and automatically detects its type. + * + *

Supported types are: + * + *

    + *
  • {@link String} for quoted values + *
  • {@link Boolean} for {@code true}/{@code false} + *
  • {@link Integer}/{@link Long}/{@link Double} for numbers + *
  • {@link Map} for objects + *
  • {@link List} for arrays + *
+ * + * @return The next value as its related Java type. + * @throws IOException If the JSON is invalid, or a reader error occurs. + */ + public Object nextValue() throws IOException { + char c = advanceUpToNextValueChar(); + switch (c) { + case '"': + return nextString(); + case '{': + Map map = new HashMap<>(); + beginObject(); + String name; + while ((name = nextName()) != null) { + map.put(name, nextValue()); + } + endObject(); + return map; + case '[': + List list = new ArrayList<>(); + beginArray(); + while (hasNext()) { + list.add(nextValue()); + } + endArray(); + return list; + case 't': + case 'f': + return nextBoolean(); + case 'n': + readLiteral(4); // Skip "null" + return null; + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + String number = readNumber(); + // Try parsing as integer first, then long, then fall back to double + if (!number.contains(".") && !number.contains("e") && !number.contains("E")) { + try { + return Integer.parseInt(number); + } catch (NumberFormatException e) { + try { + return Long.parseLong(number); + } catch (NumberFormatException e2) { + // Fall through to double + } + } + } + return Double.parseDouble(number); + default: + throw syntaxError("Unexpected character: " + c); + } + } + + private char advanceUpToNextValueChar() throws IOException { + consumeWhitespace(); + char c = peek(); + // Handle comma between values + if (c == ',') { + advance(); // Skip comma + consumeWhitespace(); + c = peek(); + } + return c; + } + + private void consumeWhitespace() throws IOException { + while (true) { + char c = peek(); + if (c == ' ' || c == '\n' || c == '\r' || c == '\t') { + advance(); + if (c == '\n') { + this.lineNumber++; + this.linePosition = 0; + } + } else { + break; + } + } + } + + private char peek() throws IOException { + if (this.position >= this.limit) { + fillBuffer(); + } + return this.position < this.limit ? this.buffer[this.position] : '\0'; + } + + private void advance() throws IOException { + if (this.position >= this.limit) { + fillBuffer(); + } + if (this.position < this.limit) { + this.position++; + this.linePosition++; + } + } + + private void fillBuffer() throws IOException { + this.limit = this.reader.read(this.buffer, 0, this.buffer.length); + this.position = 0; + if (this.limit == -1) { + this.limit = 0; + } + } + + private String readString() throws IOException { + StringBuilder sb = new StringBuilder(); + advance(); // Skip opening quote + while (true) { + char c = peek(); + if (c == '"') { + advance(); + break; + } + if (c == '\\') { + advance(); + c = peek(); + switch (c) { + case '"': + case '\\': + case '/': + sb.append(c); + break; + case 'b': + sb.append('\b'); + break; + case 'f': + sb.append('\f'); + break; + case 'n': + sb.append('\n'); + break; + case 'r': + sb.append('\r'); + break; + case 't': + sb.append('\t'); + break; + case 'u': + sb.append(readUnicodeEscape()); + continue; + default: + throw syntaxError("Invalid escape sequence: \\" + c); + } + } else if (c < ' ') { + throw syntaxError("Unterminated string"); + } else { + sb.append(c); + } + advance(); + } + return sb.toString(); + } + + private char readUnicodeEscape() throws IOException { + advance(); // Skip 'u' + StringBuilder hex = new StringBuilder(4); + for (int i = 0; i < 4; i++) { + hex.append(peek()); + advance(); + } + try { + return (char) Integer.parseInt(hex.toString(), 16); + } catch (NumberFormatException e) { + throw syntaxError("Invalid unicode escape sequence: \\u" + hex); + } + } + + private String readNumber() throws IOException { + StringBuilder sb = new StringBuilder(); + char c = peek(); + // Optional minus sign + if (c == '-') { + sb.append(c); + advance(); + } + // Integer part + readDigits(sb); + // Fractional part + if (peek() == '.') { + sb.append('.'); + advance(); + readDigits(sb); + } + // Exponent part + c = peek(); + if (c == 'e' || c == 'E') { + sb.append(c); + advance(); + c = peek(); + if (c == '+' || c == '-') { + sb.append(c); + advance(); + } + readDigits(sb); + } + return sb.toString(); + } + + private void readDigits(StringBuilder sb) throws IOException { + char c = peek(); + if (c < '0' || c > '9') { + throw unexpectedSyntaxError("digit", c); + } + while (true) { + c = peek(); + if (c < '0' || c > '9') { + break; + } + sb.append(c); + advance(); + } + } + + private String readLiteral(int length) throws IOException { + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append(peek()); + advance(); + } + return sb.toString(); + } + + private IOException unexpectedSyntaxError(String expected, String found) { + return new IOException( + String.format( + "Syntax error at line %d, position %d: expected %s but found '%s'", + this.lineNumber, this.linePosition, expected, found)); + } + + private IOException unexpectedSyntaxError(String expected, char found) { + return new IOException( + String.format( + "Syntax error at line %d, position %d: expected %s but found '%s'", + this.lineNumber, this.linePosition, expected, found)); + } + + private IOException syntaxError(String message) { + return new IOException( + String.format( + "Syntax error at line %d, position %d: %s", + this.lineNumber, this.linePosition, message)); + } + + @Override + public void close() throws IOException { + this.reader.close(); + } +} diff --git a/components/json/src/test/groovy/datadog/json/JsonMapperTest.groovy b/components/json/src/test/groovy/datadog/json/JsonMapperTest.groovy index 2c17cd87e53..43dd4dcb0aa 100644 --- a/components/json/src/test/groovy/datadog/json/JsonMapperTest.groovy +++ b/components/json/src/test/groovy/datadog/json/JsonMapperTest.groovy @@ -3,16 +3,52 @@ package datadog.json import spock.lang.Specification import static java.lang.Math.PI +import static java.util.Collections.emptyMap class JsonMapperTest extends Specification { def "test mapping to JSON object: #input"() { + setup: + def parsedExpected = input == null ? emptyMap() : input.clone() + parsedExpected.collect { + it -> { + if (it.value instanceof UnsupportedType) { + it.value = it.value.toString() + } else if (it.value instanceof Float) { + it.value = new Double(it.value) + } + + it + } + } + when: String json = JsonMapper.toJson((Map) input) then: json == expected + when: + def parsed = JsonMapper.fromJsonToMap(json) + + then: + if (input == null) { + parsed == [:] + } else { + parsed.size() == input.size() + input.each { + assert parsed.containsKey(it.key) + if (it.value instanceof UnsupportedType) { + assert parsed.get(it.key) == it.value.toString() + } else if (it.value instanceof Float) { + assert parsed.get(it.key) instanceof Double + assert (parsed.get(it.key) - it.value) < 0.001 + } else { + assert parsed.get(it.key) == it.value + } + } + } + where: input | expected null | '{}' @@ -30,6 +66,28 @@ class JsonMapperTest extends Specification { } } + def "test mapping to Map from empty JSON object"() { + when: + def parsed = JsonMapper.fromJsonToMap(json) + + then: + parsed == [:] + + where: + json << [null, 'null', '', '{}'] + } + + def "test mapping to Map from non-object JSON"() { + when: + JsonMapper.fromJsonToMap(json) + + then: + thrown(IOException) + + where: + json << ['1', '[1, 2]'] + } + def "test mapping iterable to JSON array: #input"() { when: String json = JsonMapper.toJson(input as Collection) @@ -37,6 +95,12 @@ class JsonMapperTest extends Specification { then: json == expected + when: + def parsed = JsonMapper.fromJsonToList(json) + + then: + parsed == (input?:[]) + where: input | expected null | "[]" @@ -53,6 +117,12 @@ class JsonMapperTest extends Specification { then: json == expected + when: + def parsed = JsonMapper.fromJsonToList(json).toArray(new String[0]) + + then: + parsed == (String[]) (input?:[]) + where: input | expected null | "[]" @@ -62,6 +132,17 @@ class JsonMapperTest extends Specification { ['va"lu"e1', 'value2'] | "[\"va\\\"lu\\\"e1\",\"value2\"]" } + def "test mapping to List from empty JSON object"() { + when: + def parsed = JsonMapper.fromJsonToList(json) + + then: + parsed == [] + + where: + json << [null, 'null', '', '[]'] + } + def "test mapping to JSON string: input"() { when: String escaped = JsonMapper.toJson((String) string) diff --git a/components/json/src/test/java/datadog/json/JsonReaderTest.java b/components/json/src/test/java/datadog/json/JsonReaderTest.java new file mode 100644 index 00000000000..e716a625103 --- /dev/null +++ b/components/json/src/test/java/datadog/json/JsonReaderTest.java @@ -0,0 +1,351 @@ +package datadog.json; + +import static java.util.Objects.requireNonNull; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +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 static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class JsonReaderTest { + @Test + void testReadObject() { + String json = + "{\"string\":\"bar\",\"int\":3,\"long\":3456789123,\"float\":3.142,\"double\":3.141592653589793,\"true\":true,\"false\":false,\"null\":null}"; + try (JsonReader reader = new JsonReader(json)) { + reader.beginObject(); + assertEquals("string", reader.nextName()); + assertEquals("bar", reader.nextString()); + assertEquals("int", reader.nextName()); + assertEquals(3, reader.nextInt()); + assertEquals("long", reader.nextName()); + assertEquals(3456789123L, reader.nextLong()); + assertEquals("float", reader.nextName()); + assertEquals(3.142, reader.nextDouble(), 0.001); + assertEquals("double", reader.nextName()); + assertEquals(3.141592653589793, reader.nextDouble(), 0.000000000000001); + assertEquals("true", reader.nextName()); + assertTrue(reader.nextBoolean()); + assertEquals("false", reader.nextName()); + assertFalse(reader.nextBoolean()); + assertEquals("null", reader.nextName()); + assertTrue(reader.isNull()); + assertNull(reader.nextValue()); + assertFalse(reader.hasNext()); + reader.endObject(); + } catch (IOException e) { + fail("Failed to read JSON object", e); + } + } + + @Test + void testReadEmptyObject() { + String json = "{}"; + try (JsonReader reader = new JsonReader(json)) { + reader.beginObject(); + reader.endObject(); + } catch (IOException e) { + fail("Failed to read JSON empty object", e); + } + } + + @ParameterizedTest + @ValueSource(strings = {"", "null", "1", "[]", "true", "false"}) + void testInvalidObjectStart(String json) { + assertThrows( + IOException.class, + () -> { + try (JsonReader reader = new JsonReader(json)) { + reader.beginObject(); + } + }); + } + + @ParameterizedTest + @ValueSource(strings = {"{", "{\"key\":\"value\"}", "{null}", "{]"}) + void testInvalidObjectEnd(String json) { + assertThrows( + IOException.class, + () -> { + try (JsonReader reader = new JsonReader(json)) { + reader.beginObject(); + reader.endObject(); + } + }); + } + + @ParameterizedTest + @ValueSource(strings = {"{\"key\"}", "{\"key\"\"value\"}", "{key:\"value\"}"}) + void testInvalidObjectNames(String json) { + assertThrows( + IOException.class, + () -> { + try (JsonReader reader = new JsonReader(json)) { + reader.beginObject(); + reader.nextName(); + } + }); + } + + @ParameterizedTest + @ValueSource(strings = {"{\"key\":value}"}) + void testInvalidObjectValue(String json) { + assertThrows( + IOException.class, + () -> { + try (JsonReader reader = new JsonReader(json)) { + reader.beginObject(); + reader.nextName(); + reader.nextValue(); + } + }); + } + + @Test + void testReadArray() { + String json = "[\"foo\",\"baz\",\"bar\",\"quux\"]"; + try (JsonReader reader = new JsonReader(json)) { + reader.beginArray(); + assertEquals("foo", reader.nextString()); + assertEquals("baz", reader.nextString()); + assertEquals("bar", reader.nextString()); + assertEquals("quux", reader.nextString()); + assertFalse(reader.hasNext()); + reader.endArray(); + } catch (IOException e) { + fail("Failed to read JSON array", e); + } + } + + @ParameterizedTest + @ValueSource(strings = {"", "null", "1", "{}", "true", "false"}) + void testInvalidArrayStart(String json) { + assertThrows( + IOException.class, + () -> { + try (JsonReader reader = new JsonReader(json)) { + reader.beginArray(); + } + }, + "Failed to detect invalid array start"); + } + + @ParameterizedTest + @ValueSource(strings = {"[", "[\"value\"]", "[null", "[}"}) + void testInvalidArrayEnd(String json) { + assertThrows( + IOException.class, + () -> { + try (JsonReader reader = new JsonReader(json)) { + reader.beginArray(); + reader.endArray(); + } + }, + "Failed to detect invalid array end"); + } + + @Test + void testIsNull() { + try (JsonReader reader = new JsonReader("null")) { + assertTrue(reader.isNull()); + } catch (IOException e) { + fail("Failed to read null value JSON", e); + } + } + + @ParameterizedTest + @ValueSource(strings = {"\"bar\"", "3", "3456789123", "3.142", "true", "false", "{}", "[]"}) + void testIsNotNull(String json) { + try (JsonReader reader = new JsonReader(json)) { + assertFalse(reader.isNull()); + } catch (IOException e) { + fail("Failed to read non-null value JSON", e); + } + } + + @Test + void testReadBoolean() { + assertDoesNotThrow( + () -> { + assertTrue(readBoolean("true")); + assertFalse(readBoolean("false")); + }, + "Failed to read boolean value"); + } + + @ParameterizedTest + @ValueSource( + strings = { + "ttrue", + "ffalse", + "TRUE", + "FALSE", + "null", + "\"bar\"", + "3", + "3456789123", + "3.142", + "{}", + "[]" + }) + void testInvalidBoolean(String json) { + assertThrows( + IOException.class, () -> readBoolean(json), "Failed to detect invalid boolean value"); + } + + @Test + void testStringEscaping() { + String json = "[\"\\\"\",\"\\\\\",\"\\/\",\"\\b\",\"\\f\",\"\\n\",\"\\r\",\"\\t\",\"\\u00C9\"]"; + try (JsonReader reader = new JsonReader(json)) { + reader.beginArray(); + assertEquals("\"", reader.nextString()); + assertEquals("\\", reader.nextString()); + assertEquals("/", reader.nextString()); + assertEquals("\b", reader.nextString()); + assertEquals("\f", reader.nextString()); + assertEquals("\n", reader.nextString()); + assertEquals("\r", reader.nextString()); + assertEquals("\t", reader.nextString()); + assertEquals("É", reader.nextString()); + reader.endArray(); + } catch (IOException e) { + fail("Failed to read escaped JSON strings", e); + } + } + + @ParameterizedTest + @ValueSource( + strings = { + "bar", + "true", + "false", + "null", + "3", + "3456789123", + "3.142", + "{}", + "[]", + "\"\\uGHIJ\"" + }) + void testInvalidString(String json) { + assertThrows( + IOException.class, + () -> { + try (JsonReader reader = new JsonReader(json)) { + reader.nextString(); + } + }, + "Failed to detect invalid string value"); + } + + @Test + void testReadInt() { + assertDoesNotThrow( + () -> { + assertEquals(1, readInt("1")); + assertEquals(0, readInt("0")); + assertEquals(-1, readInt("-1")); + assertEquals(Integer.MAX_VALUE, readInt("2147483647")); + assertEquals(Integer.MIN_VALUE, readInt("-2147483648")); + }, + "Failed to read int value"); + } + + @ParameterizedTest + @ValueSource( + strings = {"\"bar\"", "true", "false", "null", "3456789123", "3.142", "1e100", "{}", "[]"}) + void testInvalidInt(String json) { + assertThrows(IOException.class, () -> readInt(json), "Failed to detect invalid int value"); + } + + @Test + void testReadLong() { + assertDoesNotThrow( + () -> { + assertEquals(1L, readLong("1")); + assertEquals(0L, readLong("0")); + assertEquals(-1L, readLong("-1")); + assertEquals(Long.MAX_VALUE, readLong("9223372036854775807")); + assertEquals(Long.MIN_VALUE, readLong("-9223372036854775808")); + }, + "Failed to read long value"); + } + + @ParameterizedTest + @ValueSource(strings = {"\"bar\"", "true", "false", "null", "3.142", "1e100", "{}", "[]"}) + void testInvalidLong(String json) { + assertThrows(IOException.class, () -> readLong(json), "Failed to detect invalid long value"); + } + + @Test + void testReadDouble() { + assertDoesNotThrow( + () -> { + assertEquals(3.14, readDouble("3.14"), 0.01); + assertEquals(-3.14, readDouble("-3.14"), 0.01); + assertEquals(-3.14, readDouble("-3.14e0"), 0.01); + assertEquals(314, readDouble("3.14e2"), 1); + assertEquals(0.0314, readDouble("3.14e-2"), 0.0001); + }, + "Failed to read double value"); + } + + @ParameterizedTest + @ValueSource(strings = {"\"bar\"", "true", "false", "null", "1ee1", "1e", "{}", "[]"}) + void testInvalidDouble(String json) { + assertThrows( + IOException.class, () -> readDouble(json), "Failed to detect invalid double value"); + } + + @Test + void testReaderOnHugePayload() { + try (InputStream stream = JsonReader.class.getResourceAsStream("/lorem-ipsum.json"); + JsonReader reader = new JsonReader(new InputStreamReader(requireNonNull(stream)))) { + reader.beginArray(); + while (reader.hasNext()) { + reader.nextString(); + } + reader.endArray(); + } catch (IOException e) { + fail("Failed to read JSON stream", e); + } + } + + @Test + void testConstructor() { + assertThrows(IllegalArgumentException.class, () -> new JsonReader(null, false).close()); + } + + private boolean readBoolean(String json) throws IOException { + try (JsonReader reader = new JsonReader(json)) { + return reader.nextBoolean(); + } + } + + private int readInt(String json) throws IOException { + try (JsonReader reader = new JsonReader(json)) { + return reader.nextInt(); + } + } + + private long readLong(String json) throws IOException { + try (JsonReader reader = new JsonReader(json)) { + return reader.nextLong(); + } + } + + private double readDouble(String json) throws IOException { + try (JsonReader reader = new JsonReader(json)) { + return reader.nextDouble(); + } + } +} diff --git a/components/json/src/test/java/datadog/json/JsonWriterTest.java b/components/json/src/test/java/datadog/json/JsonWriterTest.java index 9a4b913e36d..ac22095d801 100644 --- a/components/json/src/test/java/datadog/json/JsonWriterTest.java +++ b/components/json/src/test/java/datadog/json/JsonWriterTest.java @@ -74,7 +74,6 @@ void testNaNValues() { void testArray() { try (JsonWriter writer = new JsonWriter()) { writer.beginArray().value("foo").value("baz").value("bar").value("quux").endArray(); - assertEquals("[\"foo\",\"baz\",\"bar\",\"quux\"]", writer.toString(), "Check array writer"); } } diff --git a/components/json/src/test/resources/lorem-ipsum.json b/components/json/src/test/resources/lorem-ipsum.json new file mode 100644 index 00000000000..9bc28d3f20d --- /dev/null +++ b/components/json/src/test/resources/lorem-ipsum.json @@ -0,0 +1,7 @@ +[ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus viverra neque vitae pretium aliquam. Nullam viverra facilisis mattis. In dui libero, tempor vel condimentum at, scelerisque et dui. Aliquam erat volutpat. Aliquam egestas massa a lectus pulvinar, sed finibus urna gravida. Praesent a velit porta tortor interdum facilisis eget quis metus. Integer ac varius lacus. Sed scelerisque ipsum ut lorem suscipit molestie. Quisque ligula orci, bibendum quis vestibulum id, interdum eget enim. Phasellus congue massa eget quam accumsan, et luctus neque vestibulum. Suspendisse placerat, purus auctor semper congue, felis ex aliquet quam, et auctor urna nisi vitae neque. Sed porta commodo quam, ac lobortis ex. Fusce faucibus eget dui at congue.", + "Integer sodales magna nec neque volutpat faucibus. Fusce ac commodo sapien. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla venenatis, nibh at sodales luctus, purus mi ultricies lorem, id tincidunt purus tortor vitae mi. Phasellus imperdiet mauris et nunc rhoncus vulputate. Morbi in sapien ut purus varius efficitur ac vel diam. Nullam egestas nec nibh quis porta. Fusce interdum ullamcorper dolor.", + "Praesent faucibus tristique turpis ac scelerisque. Vivamus ipsum massa, finibus et auctor ut, dignissim ac urna. Cras quis fermentum sem. Donec vulputate, lacus eu venenatis consequat, nulla dui iaculis lorem, at viverra nulla neque id velit. Curabitur accumsan, libero nec feugiat vestibulum, magna leo ultrices justo, a ultrices nunc tortor sit amet mi. Donec accumsan urna vel lacus elementum, ac sagittis lacus eleifend. Quisque diam leo, rutrum ut sem sed, malesuada porttitor odio. Aenean vitae mattis neque, suscipit ultrices nisi. Morbi vulputate velit tortor, vel posuere orci accumsan condimentum.", + "Aenean feugiat, magna nec pharetra imperdiet, nulla tellus molestie mauris, vel pulvinar neque tortor id nunc. Curabitur ultrices efficitur dapibus. Aliquam non convallis odio. Curabitur molestie, nibh id lacinia sollicitudin, massa dolor mattis velit, vel ornare nunc erat vitae nisi. Duis rutrum, quam in efficitur venenatis, nisl mi viverra libero, in convallis sapien neque ac mi. Vivamus faucibus nibh mauris, vel cursus arcu feugiat aliquam. Mauris luctus libero nec porttitor gravida. In faucibus facilisis nibh ac posuere.", + "Quisque iaculis blandit ipsum, tincidunt blandit mi eleifend sit amet. Mauris dapibus convallis eros sit amet aliquet. Duis nisi diam, lobortis non maximus sit amet, sollicitudin vel diam. Nunc ultrices cursus scelerisque. In finibus felis leo, ac facilisis orci semper vitae. Morbi quis ornare velit. Nullam tincidunt aliquet volutpat." +]