diff options
author | HÃ¥kon Hallingstad <hakon.hallingstad@gmail.com> | 2022-10-27 17:07:38 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-27 17:07:38 +0200 |
commit | d92a557986032c9fdf98f0ee76263b6baa5d7635 (patch) | |
tree | cea23c41ba61dfff45ca1ac3e0a91512421ddc8c | |
parent | 5436fcb54be30fc9b2eb2457332307fb1d631042 (diff) | |
parent | 170f6d98a806b77827538697171d70a180278972 (diff) |
Merge pull request #24615 from vespa-engine/hakonhall/coresapihandler
Add some String utils in SlimeUtils
6 files changed, 262 insertions, 54 deletions
diff --git a/testutil/src/main/java/com/yahoo/test/json/JsonBuilder.java b/testutil/src/main/java/com/yahoo/test/json/JsonBuilder.java new file mode 100644 index 00000000000..bbb8586a290 --- /dev/null +++ b/testutil/src/main/java/com/yahoo/test/json/JsonBuilder.java @@ -0,0 +1,75 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.test.json; + +import com.fasterxml.jackson.core.io.JsonStringEncoder; + +/** + * String buffer for building a formatted JSON. + * + * @author hakonhall + */ +class JsonBuilder { + private final JsonStringEncoder jsonStringEncoder = JsonStringEncoder.getInstance(); + private final StringBuilder builder = new StringBuilder(); + private final String indentation; + private final boolean multiLine; + private final String colon; + + private boolean bol = true; + private int level = 0; + + static JsonBuilder forCompactJson() { return new JsonBuilder(0, true); } + static JsonBuilder forMultiLineJson(int spacesPerIndent) { return new JsonBuilder(spacesPerIndent, false); } + + JsonBuilder(int spacesPerIndent, boolean compact) { + this.indentation = compact ? "" : " ".repeat(spacesPerIndent); + this.multiLine = !compact; + this.colon = compact ? ":" : ": "; + } + + void appendLineAndIndent(String text) { appendLineAndIndent(text, 0); } + + void newLineIndentAndAppend(int levelShift, String text) { + appendNewLine(); + indent(levelShift); + append(text); + } + + void appendLineAndIndent(String text, int levelShift) { + appendLine(text); + indent(levelShift); + append(""); + } + + void appendColon() { builder.append(colon); } + + void appendStringValue(String rawString) { + builder.append('"'); + jsonStringEncoder.quoteAsString(rawString, builder); + builder.append('"'); + } + + void append(String textWithoutNewline) { + if (bol) { + builder.append(indentation.repeat(level)); + bol = false; + } + + builder.append(textWithoutNewline); + } + + private void indent(int levelShift) { level += levelShift; } + + private void appendLine(String text) { + append(text); + appendNewLine(); + } + + private void appendNewLine() { + if (multiLine) builder.append('\n'); + bol = true; + } + + @Override + public String toString() { return builder.toString(); } +} diff --git a/testutil/src/main/java/com/yahoo/test/json/JsonNodeFormatter.java b/testutil/src/main/java/com/yahoo/test/json/JsonNodeFormatter.java new file mode 100644 index 00000000000..d0e07e66e0b --- /dev/null +++ b/testutil/src/main/java/com/yahoo/test/json/JsonNodeFormatter.java @@ -0,0 +1,81 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.test.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; + +/** + * Formats a {@link JsonNode} to a normalized JSON string, see {@link JsonTestHelper}. + * + * @author hakonhall + */ +class JsonNodeFormatter { + private final JsonBuilder builder; + + /** See {@link JsonTestHelper}. */ + static String toNormalizedJson(JsonNode jsonNode, boolean compact) { + JsonNodeFormatter formatter = new JsonNodeFormatter(compact); + formatter.appendValue(jsonNode); + return formatter.toString(); + } + + private JsonNodeFormatter(boolean compact) { + builder = compact ? JsonBuilder.forCompactJson() : JsonBuilder.forMultiLineJson(2); + } + + private void appendValue(JsonNode jsonNode) { + switch (jsonNode.getNodeType()) { + case OBJECT -> { + ObjectNode objectNode = (ObjectNode) jsonNode; + ArrayList<String> fieldNames = new ArrayList<>(); + objectNode.fieldNames().forEachRemaining(fieldNames::add); + Collections.sort(fieldNames); + if (fieldNames.isEmpty()) { + builder.append("{}"); + } else { + boolean firstIteration = true; + for (var fieldName : fieldNames) { + if (firstIteration) { + builder.appendLineAndIndent("{", +1); + firstIteration = false; + } else { + builder.appendLineAndIndent(","); + } + + builder.appendStringValue(fieldName); + builder.appendColon(); + appendValue(objectNode.get(fieldName)); + } + + builder.newLineIndentAndAppend(-1, "}"); + } + } + case ARRAY -> { + Iterator<JsonNode> elements = jsonNode.elements(); + if (elements.hasNext()) { + builder.appendLineAndIndent("[", +1); + appendValue(elements.next()); + + while (elements.hasNext()) { + builder.appendLineAndIndent(","); + appendValue(elements.next()); + } + + builder.newLineIndentAndAppend(-1, "]"); + } else { + builder.append("[]"); + } + } + case BOOLEAN, NUMBER, NULL -> builder.append(jsonNode.asText()); + case STRING -> builder.appendStringValue(jsonNode.asText()); + case BINARY, MISSING, POJO -> throw new IllegalStateException(jsonNode.getNodeType().toString()); + } + } + + @Override + public String toString() { return builder.toString(); } +} diff --git a/testutil/src/main/java/com/yahoo/test/json/JsonTestHelper.java b/testutil/src/main/java/com/yahoo/test/json/JsonTestHelper.java index f7112ee9379..589cc3ab4ef 100644 --- a/testutil/src/main/java/com/yahoo/test/json/JsonTestHelper.java +++ b/testutil/src/main/java/com/yahoo/test/json/JsonTestHelper.java @@ -17,6 +17,31 @@ public class JsonTestHelper { private static final ObjectMapper mapper = new ObjectMapper(); /** + * Returns a normalized JSON String. + * + * <ol> + * <li>A JSON string with each object's names in sorted order.</li> + * <li>Two JSONs are equal iff their normalized JSON strings are equal.*</li> + * <li>The normalized JSON is (by default) an indented multi-line string to facilitate + * readability and line-based diff tools.</li> + * <li>The normalized string does not end with a newline (\n).</li> + * </ol> + * + * <p>*) No effort is done to normalize decimals and may cause false non-equality, + * e.g. 1.2e1 is not equal to 12. This may be fixed at a later time if needed.</p> + */ + public static String normalize(String json) { + JsonNode jsonNode; + try { + jsonNode = mapper.readTree(json); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid JSON", e); + } + + return JsonNodeFormatter.toNormalizedJson(jsonNode, false); + } + + /** * Convenience method to input JSON without escaping double quotes and newlines * Each parameter represents a line of JSON encoded data * The lines are joined with newline and single quotes are replaced with double quotes diff --git a/testutil/src/test/java/com/yahoo/test/json/JsonTestHelperTest.java b/testutil/src/test/java/com/yahoo/test/json/JsonTestHelperTest.java new file mode 100644 index 00000000000..d0798284da1 --- /dev/null +++ b/testutil/src/test/java/com/yahoo/test/json/JsonTestHelperTest.java @@ -0,0 +1,45 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.test.json; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author hakonhall + */ +public class JsonTestHelperTest { + @Test + public void normalize() { + verifyNormalization(""" + {"a": 1, "c": 2, + "b": [ {"n": 3, "m": 4}, 5 ] + } + """, + """ + { + "a": 1, + "b": [ + { + "m": 4, + "n": 3 + }, + 5 + ], + "c": 2 + }"""); + + verifyNormalization("[1,2]", """ + [ + 1, + 2 + ]"""); + + verifyNormalization("null", "null"); + verifyNormalization("{ \n}", "{}"); + } + + private static void verifyNormalization(String json, String normalizedJson) { + assertEquals(normalizedJson, JsonTestHelper.normalize(json)); + } +}
\ No newline at end of file diff --git a/vespajlib/src/main/java/com/yahoo/io/IOUtils.java b/vespajlib/src/main/java/com/yahoo/io/IOUtils.java index 81e1306b29e..699ac22d278 100644 --- a/vespajlib/src/main/java/com/yahoo/io/IOUtils.java +++ b/vespajlib/src/main/java/com/yahoo/io/IOUtils.java @@ -413,7 +413,7 @@ public abstract class IOUtils { /** Read an input stream completely into a string */ public static String readAll(InputStream stream, Charset charset) throws IOException { - return readAll(new InputStreamReader(stream, charset)); + return new String(stream.readAllBytes(), charset); } /** Convenience method for closing a list of readers. Does nothing if the given reader list is null. */ diff --git a/vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java b/vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java index c36b001d056..c2e11be34be 100644 --- a/vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java +++ b/vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java @@ -3,6 +3,7 @@ package com.yahoo.slime; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; @@ -16,6 +17,8 @@ import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; +import static com.yahoo.yolean.Exceptions.uncheck; + /** * Extra utilities/operations on slime trees. * @@ -31,34 +34,16 @@ public class SlimeUtils { } - private static void setObjectEntry(Inspector from, String name, Cursor to) { + public static void setObjectEntry(Inspector from, String name, Cursor to) { switch (from.type()) { - case NIX: - to.setNix(name); - break; - case BOOL: - to.setBool(name, from.asBool()); - break; - case LONG: - to.setLong(name, from.asLong()); - break; - case DOUBLE: - to.setDouble(name, from.asDouble()); - break; - case STRING: - to.setString(name, from.asString()); - break; - case DATA: - to.setData(name, from.asData()); - break; - case ARRAY: - Cursor array = to.setArray(name); - copyArray(from, array); - break; - case OBJECT: - Cursor object = to.setObject(name); - copyObject(from, object); - break; + case NIX -> to.setNix(name); + case BOOL -> to.setBool(name, from.asBool()); + case LONG -> to.setLong(name, from.asLong()); + case DOUBLE -> to.setDouble(name, from.asDouble()); + case STRING -> to.setString(name, from.asString()); + case DATA -> to.setData(name, from.asData()); + case ARRAY -> copyArray(from, to.setArray(name)); + case OBJECT -> copyObject(from, to.setObject(name)); } } @@ -71,32 +56,14 @@ public class SlimeUtils { private static void addValue(Inspector from, Cursor to) { switch (from.type()) { - case NIX: - to.addNix(); - break; - case BOOL: - to.addBool(from.asBool()); - break; - case LONG: - to.addLong(from.asLong()); - break; - case DOUBLE: - to.addDouble(from.asDouble()); - break; - case STRING: - to.addString(from.asString()); - break; - case DATA: - to.addData(from.asData()); - break; - case ARRAY: - Cursor array = to.addArray(); - copyArray(from, array); - break; - case OBJECT: - Cursor object = to.addObject(); - copyObject(from, object); - break; + case NIX -> to.addNix(); + case BOOL -> to.addBool(from.asBool()); + case LONG -> to.addLong(from.asLong()); + case DOUBLE -> to.addDouble(from.asDouble()); + case STRING -> to.addString(from.asString()); + case DATA -> to.addData(from.asData()); + case ARRAY -> copyArray(from, to.addArray()); + case OBJECT -> copyObject(from, to.addObject()); } } @@ -115,6 +82,21 @@ public class SlimeUtils { return baos.toByteArray(); } + public static String toJson(Slime slime) { + return toJson(slime.get()); + } + + public static String toJson(Inspector inspector) { + return toJson(inspector, true); + } + + public static String toJson(Inspector inspector, boolean compact) { + var outputStream = new ByteArrayOutputStream(); + var jsonFormat = new JsonFormat(compact ? 0 : 2); + uncheck(() -> jsonFormat.encode(outputStream, inspector)); + return outputStream.toString(StandardCharsets.UTF_8); + } + public static Slime jsonToSlime(byte[] json) { Slime slime = new Slime(); new JsonDecoder().decode(slime, json); |