summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHÃ¥kon Hallingstad <hakon.hallingstad@gmail.com>2022-10-27 17:07:38 +0200
committerGitHub <noreply@github.com>2022-10-27 17:07:38 +0200
commitd92a557986032c9fdf98f0ee76263b6baa5d7635 (patch)
treecea23c41ba61dfff45ca1ac3e0a91512421ddc8c
parent5436fcb54be30fc9b2eb2457332307fb1d631042 (diff)
parent170f6d98a806b77827538697171d70a180278972 (diff)
Merge pull request #24615 from vespa-engine/hakonhall/coresapihandler
Add some String utils in SlimeUtils
-rw-r--r--testutil/src/main/java/com/yahoo/test/json/JsonBuilder.java75
-rw-r--r--testutil/src/main/java/com/yahoo/test/json/JsonNodeFormatter.java81
-rw-r--r--testutil/src/main/java/com/yahoo/test/json/JsonTestHelper.java25
-rw-r--r--testutil/src/test/java/com/yahoo/test/json/JsonTestHelperTest.java45
-rw-r--r--vespajlib/src/main/java/com/yahoo/io/IOUtils.java2
-rw-r--r--vespajlib/src/main/java/com/yahoo/slime/SlimeUtils.java88
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);