org.assertj
assertj-core
@@ -105,7 +81,7 @@
- software.amazon.awssdk.protocols.ion
+ software.amazon.awssdk.protocols.jsoncore
diff --git a/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/JsonNode.java b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/JsonNode.java
new file mode 100644
index 000000000000..ef597c1b742a
--- /dev/null
+++ b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/JsonNode.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocols.jsoncore;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import software.amazon.awssdk.annotations.SdkProtectedApi;
+import software.amazon.awssdk.protocols.jsoncore.internal.ObjectJsonNode;
+import software.amazon.awssdk.thirdparty.jackson.core.JsonFactory;
+
+/**
+ * A node in a JSON document. Either a number, string, boolean, array, object or null. Also can be an embedded object,
+ * which is a non-standard type used in JSON extensions, like CBOR.
+ *
+ * Created from a JSON document via {@link #parser()} or {@link #parserBuilder()}.
+ *
+ *
The type of node can be determined using "is" methods like {@link #isNumber()} and {@link #isString()}.
+ * Once the type is determined, the value of the node can be extracted via the "as" methods, like {@link #asNumber()}
+ * and {@link #asString()}.
+ */
+@SdkProtectedApi
+public interface JsonNode {
+ /**
+ * Create a {@link JsonNodeParser} for generating a {@link JsonNode} from a JSON document.
+ */
+ static JsonNodeParser parser() {
+ return JsonNodeParser.create();
+ }
+
+ /**
+ * Create a {@link JsonNodeParser.Builder} for generating a {@link JsonNode} from a JSON document.
+ */
+ static JsonNodeParser.Builder parserBuilder() {
+ return JsonNodeParser.builder();
+ }
+
+ /**
+ * Return an empty object node.
+ */
+ static JsonNode emptyObjectNode() {
+ return new ObjectJsonNode(Collections.emptyMap());
+ }
+
+ /**
+ * Returns true if this node represents a JSON number: https://datatracker.ietf.org/doc/html/rfc8259#section-6
+ *
+ * @see #asNumber()
+ */
+ default boolean isNumber() {
+ return false;
+ }
+
+ /**
+ * Returns true if this node represents a JSON string: https://datatracker.ietf.org/doc/html/rfc8259#section-7
+ *
+ * @see #asString()
+ */
+ default boolean isString() {
+ return false;
+ }
+
+ /**
+ * Returns true if this node represents a JSON boolean: https://datatracker.ietf.org/doc/html/rfc8259#section-3
+ *
+ * @see #asBoolean()
+ */
+ default boolean isBoolean() {
+ return false;
+ }
+
+ /**
+ * Returns true if this node represents a JSON null: https://datatracker.ietf.org/doc/html/rfc8259#section-3
+ */
+ default boolean isNull() {
+ return false;
+ }
+
+ /**
+ * Returns true if this node represents a JSON array: https://datatracker.ietf.org/doc/html/rfc8259#section-5
+ *
+ * @see #asArray()
+ */
+ default boolean isArray() {
+ return false;
+ }
+
+ /**
+ * Returns true if this node represents a JSON object: https://datatracker.ietf.org/doc/html/rfc8259#section-4
+ *
+ * @see #asObject()
+ */
+ default boolean isObject() {
+ return false;
+ }
+
+ /**
+ * Returns true if this node represents a JSON "embedded object". This non-standard type is associated with JSON extensions,
+ * like CBOR or ION. It allows additional data types to be embedded in a JSON document, like a timestamp or a raw byte array.
+ *
+ *
Users who are only concerned with handling JSON can ignore this field. It will only be present when using a custom
+ * {@link JsonFactory} via {@link JsonNodeParser.Builder#jsonFactory(JsonFactory)}.
+ *
+ * @see #asEmbeddedObject()
+ */
+ default boolean isEmbeddedObject() {
+ return false;
+ }
+
+ /**
+ * When {@link #isNumber()} is true, this returns the number associated with this node. This will throw an exception if
+ * {@link #isNumber()} is false.
+ *
+ * @see #text()
+ */
+ String asNumber();
+
+ /**
+ * When {@link #isString()}, is true, this returns the string associated with this node. This will throw an exception if
+ * {@link #isString()} ()} is false.
+ */
+ String asString();
+
+ /**
+ * When {@link #isBoolean()} is true, this returns the boolean associated with this node. This will throw an exception if
+ * {@link #isBoolean()} is false.
+ */
+ boolean asBoolean();
+
+ /**
+ * When {@link #isArray()} is true, this returns the array associated with this node. This will throw an exception if
+ * {@link #isArray()} is false.
+ */
+ List asArray();
+
+ /**
+ * When {@link #isObject()} is true, this returns the object associated with this node. This will throw an exception if
+ * {@link #isObject()} is false.
+ */
+ Map asObject();
+
+ /**
+ * When {@link #isEmbeddedObject()} is true, this returns the embedded object associated with this node. This will throw
+ * an exception if {@link #isEmbeddedObject()} is false.
+ *
+ * @see #isEmbeddedObject()
+ */
+ Object asEmbeddedObject();
+
+ /**
+ * Visit this node using the provided visitor.
+ */
+ T visit(JsonNodeVisitor visitor);
+
+ /**
+ * When {@link #isString()}, {@link #isBoolean()}, or {@link #isNumber()} is true, this will return the value of this node
+ * as a textual string. If this is any other type, this will return null.
+ */
+ String text();
+
+ /**
+ * When {@link #isObject()} is true, this will return the result of {@code Optional.ofNullable(asObject().get(child))}. If
+ * this is any other type, this will return {@link Optional#empty()}.
+ */
+ default Optional field(String child) {
+ return Optional.empty();
+ }
+
+ /**
+ * When {@link #isArray()} is true, this will return the result of {@code asArray().get(child)} if child is within bounds. If
+ * this is any other type or the child is out of bounds, this will return {@link Optional#empty()}.
+ */
+ default Optional index(int child) {
+ return Optional.empty();
+ }
+}
diff --git a/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/JsonNodeParser.java b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/JsonNodeParser.java
new file mode 100644
index 000000000000..f87100eab1f5
--- /dev/null
+++ b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/JsonNodeParser.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocols.jsoncore;
+
+import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import software.amazon.awssdk.annotations.SdkProtectedApi;
+import software.amazon.awssdk.protocols.jsoncore.internal.ArrayJsonNode;
+import software.amazon.awssdk.protocols.jsoncore.internal.BooleanJsonNode;
+import software.amazon.awssdk.protocols.jsoncore.internal.EmbeddedObjectJsonNode;
+import software.amazon.awssdk.protocols.jsoncore.internal.NullJsonNode;
+import software.amazon.awssdk.protocols.jsoncore.internal.NumberJsonNode;
+import software.amazon.awssdk.protocols.jsoncore.internal.ObjectJsonNode;
+import software.amazon.awssdk.protocols.jsoncore.internal.StringJsonNode;
+import software.amazon.awssdk.thirdparty.jackson.core.JsonFactory;
+import software.amazon.awssdk.thirdparty.jackson.core.JsonParseException;
+import software.amazon.awssdk.thirdparty.jackson.core.JsonParser;
+import software.amazon.awssdk.thirdparty.jackson.core.JsonToken;
+import software.amazon.awssdk.thirdparty.jackson.core.json.JsonReadFeature;
+
+/**
+ * Parses an JSON document into a simple DOM-like structure, {@link JsonNode}.
+ *
+ * This is created using {@link #create()} or {@link #builder()}.
+ */
+@SdkProtectedApi
+public final class JsonNodeParser {
+ /**
+ * The default {@link JsonFactory} used for {@link #create()} or if a factory is not configured via
+ * {@link Builder#jsonFactory(JsonFactory)}.
+ */
+ public static final JsonFactory DEFAULT_JSON_FACTORY =
+ JsonFactory.builder()
+ .configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true)
+ .build();
+
+ private final boolean removeErrorLocations;
+ private final JsonFactory jsonFactory;
+
+ private JsonNodeParser(Builder builder) {
+ this.removeErrorLocations = builder.removeErrorLocations;
+ this.jsonFactory = builder.jsonFactory;
+ }
+
+ /**
+ * Create a parser using the default configuration.
+ */
+ public static JsonNodeParser create() {
+ return builder().build();
+ }
+
+ /**
+ * Create a parser using custom configuration.
+ */
+ public static JsonNodeParser.Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Parse the provided {@link InputStream} into a {@link JsonNode}.
+ */
+ public JsonNode parse(InputStream content) {
+ return invokeSafely(() -> {
+ try (JsonParser parser = jsonFactory.createParser(content)
+ .configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false)) {
+ return parse(parser);
+ }
+ });
+ }
+
+ /**
+ * Parse the provided {@code byte[]} into a {@link JsonNode}.
+ */
+ public JsonNode parse(byte[] content) {
+ return invokeSafely(() -> {
+ try (JsonParser parser = jsonFactory.createParser(content)
+ .configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false)) {
+ return parse(parser);
+ }
+ });
+ }
+
+ /**
+ * Parse the provided {@link String} into a {@link JsonNode}.
+ */
+ public JsonNode parse(String content) {
+ return invokeSafely(() -> {
+ try (JsonParser parser = jsonFactory.createParser(content)
+ .configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false)) {
+ return parse(parser);
+ }
+ });
+ }
+
+ private JsonNode parse(JsonParser parser) throws IOException {
+ try {
+ return parseToken(parser, parser.nextToken());
+ } catch (Exception e) {
+ removeErrorLocationsIfRequired(e);
+ throw e;
+ }
+ }
+
+ private void removeErrorLocationsIfRequired(Throwable exception) {
+ if (removeErrorLocations) {
+ removeErrorLocations(exception);
+ }
+ }
+
+ private void removeErrorLocations(Throwable exception) {
+ if (exception == null) {
+ return;
+ }
+
+ if (exception instanceof JsonParseException) {
+ ((JsonParseException) exception).clearLocation();
+ }
+
+ removeErrorLocations(exception.getCause());
+ }
+
+ private JsonNode parseToken(JsonParser parser, JsonToken token) throws IOException {
+ if (token == null) {
+ return null;
+ }
+ switch (token) {
+ case VALUE_STRING:
+ return new StringJsonNode(parser.getText());
+ case VALUE_FALSE:
+ return new BooleanJsonNode(false);
+ case VALUE_TRUE:
+ return new BooleanJsonNode(true);
+ case VALUE_NULL:
+ return NullJsonNode.instance();
+ case VALUE_NUMBER_FLOAT:
+ case VALUE_NUMBER_INT:
+ return new NumberJsonNode(parser.getText());
+ case START_OBJECT:
+ return parseObject(parser);
+ case START_ARRAY:
+ return parseArray(parser);
+ case VALUE_EMBEDDED_OBJECT:
+ return new EmbeddedObjectJsonNode(parser.getEmbeddedObject());
+ default:
+ throw new IllegalArgumentException("Unexpected JSON token - " + token);
+ }
+ }
+
+ private JsonNode parseObject(JsonParser parser) throws IOException {
+ JsonToken currentToken = parser.nextToken();
+ Map object = new LinkedHashMap<>();
+ while (currentToken != JsonToken.END_OBJECT) {
+ String fieldName = parser.getText();
+ object.put(fieldName, parseToken(parser, parser.nextToken()));
+ currentToken = parser.nextToken();
+ }
+ return new ObjectJsonNode(object);
+ }
+
+ private JsonNode parseArray(JsonParser parser) throws IOException {
+ JsonToken currentToken = parser.nextToken();
+ List array = new ArrayList<>();
+ while (currentToken != JsonToken.END_ARRAY) {
+ array.add(parseToken(parser, currentToken));
+ currentToken = parser.nextToken();
+ }
+ return new ArrayJsonNode(array);
+ }
+
+ /**
+ * A builder for configuring and creating {@link JsonNodeParser}. Created via {@link #builder()}.
+ */
+ public static final class Builder {
+ private JsonFactory jsonFactory = DEFAULT_JSON_FACTORY;
+ private boolean removeErrorLocations = false;
+
+ private Builder() {
+ }
+
+ /**
+ * Whether error locations should be removed if parsing fails. This prevents the content of the JSON from appearing in
+ * error messages. This is useful when the content of the JSON may be sensitive and not want to be logged.
+ *
+ * By default, this is false.
+ */
+ public Builder removeErrorLocations(boolean removeErrorLocations) {
+ this.removeErrorLocations = removeErrorLocations;
+ return this;
+ }
+
+ /**
+ * The {@link JsonFactory} implementation to be used when parsing the input. This allows JSON extensions like CBOR or
+ * Ion to be supported.
+ *
+ *
It's highly recommended us use a shared {@code JsonFactory} where possible, so they should be stored statically:
+ * http://wiki.fasterxml.com/JacksonBestPracticesPerformance
+ *
+ *
By default, this is {@link #DEFAULT_JSON_FACTORY}.
+ */
+ public Builder jsonFactory(JsonFactory jsonFactory) {
+ this.jsonFactory = jsonFactory;
+ return this;
+ }
+
+ /**
+ * Build a {@link JsonNodeParser} based on the current configuration of this builder.
+ */
+ public JsonNodeParser build() {
+ return new JsonNodeParser(this);
+ }
+ }
+}
diff --git a/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/JsonNodeVisitor.java b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/JsonNodeVisitor.java
new file mode 100644
index 000000000000..0b6efd8bb1f1
--- /dev/null
+++ b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/JsonNodeVisitor.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocols.jsoncore;
+
+import java.util.List;
+import java.util.Map;
+import software.amazon.awssdk.annotations.SdkProtectedApi;
+
+/**
+ * Converter from a {@link JsonNode} to a new type. This is usually invoked via {@link JsonNode#visit(JsonNodeVisitor)}.
+ */
+@SdkProtectedApi
+public interface JsonNodeVisitor {
+ /**
+ * Invoked if {@link JsonNode#visit(JsonNodeVisitor)} is invoked on a null JSON node.
+ */
+ T visitNull();
+
+ /**
+ * Invoked if {@link JsonNode#visit(JsonNodeVisitor)} is invoked on a boolean JSON node.
+ */
+ T visitBoolean(boolean bool);
+
+ /**
+ * Invoked if {@link JsonNode#visit(JsonNodeVisitor)} is invoked on a number JSON node.
+ */
+ T visitNumber(String number);
+
+ /**
+ * Invoked if {@link JsonNode#visit(JsonNodeVisitor)} is invoked on a string JSON node.
+ */
+ T visitString(String string);
+
+ /**
+ * Invoked if {@link JsonNode#visit(JsonNodeVisitor)} is invoked on an array JSON node.
+ */
+ T visitArray(List array);
+
+ /**
+ * Invoked if {@link JsonNode#visit(JsonNodeVisitor)} is invoked on an object JSON node.
+ */
+ T visitObject(Map object);
+
+ /**
+ * Invoked if {@link JsonNode#visit(JsonNodeVisitor)} is invoked on an embedded object JSON node.
+ */
+ T visitEmbeddedObject(Object embeddedObject);
+}
diff --git a/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/ArrayJsonNode.java b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/ArrayJsonNode.java
new file mode 100644
index 000000000000..ccf43ad940ef
--- /dev/null
+++ b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/ArrayJsonNode.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocols.jsoncore.internal;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.protocols.jsoncore.JsonNode;
+import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor;
+
+/**
+ * An array {@link JsonNode}.
+ */
+@SdkInternalApi
+public final class ArrayJsonNode implements JsonNode {
+ private final List value;
+
+ public ArrayJsonNode(List value) {
+ this.value = value;
+ }
+
+ @Override
+ public boolean isArray() {
+ return true;
+ }
+
+ @Override
+ public String asNumber() {
+ throw new UnsupportedOperationException("A JSON array cannot be converted to a number.");
+ }
+
+ @Override
+ public String asString() {
+ throw new UnsupportedOperationException("A JSON array cannot be converted to a string.");
+ }
+
+ @Override
+ public boolean asBoolean() {
+ throw new UnsupportedOperationException("A JSON array cannot be converted to a boolean.");
+ }
+
+ @Override
+ public List asArray() {
+ return value;
+ }
+
+ @Override
+ public Map asObject() {
+ throw new UnsupportedOperationException("A JSON array cannot be converted to an object.");
+ }
+
+ @Override
+ public Object asEmbeddedObject() {
+ throw new UnsupportedOperationException("A JSON array cannot be converted to an embedded object.");
+ }
+
+ @Override
+ public T visit(JsonNodeVisitor visitor) {
+ return visitor.visitArray(asArray());
+ }
+
+ @Override
+ public String text() {
+ return null;
+ }
+
+ @Override
+ public Optional index(int child) {
+ if (child < 0 || child >= value.size()) {
+ return Optional.empty();
+ }
+ return Optional.of(value.get(child));
+ }
+
+ @Override
+ public String toString() {
+ return value.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ArrayJsonNode that = (ArrayJsonNode) o;
+
+ return value.equals(that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+}
diff --git a/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/BooleanJsonNode.java b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/BooleanJsonNode.java
new file mode 100644
index 000000000000..a77f469ab937
--- /dev/null
+++ b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/BooleanJsonNode.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocols.jsoncore.internal;
+
+import java.util.List;
+import java.util.Map;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.protocols.jsoncore.JsonNode;
+import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor;
+
+/**
+ * A boolean {@link JsonNode}.
+ */
+@SdkInternalApi
+public final class BooleanJsonNode implements JsonNode {
+ private final boolean value;
+
+ public BooleanJsonNode(boolean value) {
+ this.value = value;
+ }
+
+ @Override
+ public boolean isBoolean() {
+ return true;
+ }
+
+ @Override
+ public String asNumber() {
+ throw new UnsupportedOperationException("A JSON boolean cannot be converted to a number.");
+ }
+
+ @Override
+ public String asString() {
+ throw new UnsupportedOperationException("A JSON boolean cannot be converted to a string.");
+ }
+
+ @Override
+ public boolean asBoolean() {
+ return value;
+ }
+
+ @Override
+ public List asArray() {
+ throw new UnsupportedOperationException("A JSON boolean cannot be converted to an array.");
+ }
+
+ @Override
+ public Map asObject() {
+ throw new UnsupportedOperationException("A JSON boolean cannot be converted to an object.");
+ }
+
+ @Override
+ public Object asEmbeddedObject() {
+ throw new UnsupportedOperationException("A JSON boolean cannot be converted to an embedded object.");
+ }
+
+ @Override
+ public T visit(JsonNodeVisitor visitor) {
+ return visitor.visitBoolean(asBoolean());
+ }
+
+ @Override
+ public String text() {
+ return Boolean.toString(value);
+ }
+
+ @Override
+ public String toString() {
+ return Boolean.toString(value);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ BooleanJsonNode that = (BooleanJsonNode) o;
+
+ return value == that.value;
+ }
+
+ @Override
+ public int hashCode() {
+ return (value ? 1 : 0);
+ }
+}
diff --git a/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/EmbeddedObjectJsonNode.java b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/EmbeddedObjectJsonNode.java
new file mode 100644
index 000000000000..197b68e688d4
--- /dev/null
+++ b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/EmbeddedObjectJsonNode.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocols.jsoncore.internal;
+
+import java.util.List;
+import java.util.Map;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.protocols.jsoncore.JsonNode;
+import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor;
+
+/**
+ * An embedded object {@link JsonNode}.
+ */
+@SdkInternalApi
+public final class EmbeddedObjectJsonNode implements JsonNode {
+ private final Object embeddedObject;
+
+ public EmbeddedObjectJsonNode(Object embeddedObject) {
+ this.embeddedObject = embeddedObject;
+ }
+
+ @Override
+ public boolean isEmbeddedObject() {
+ return true;
+ }
+
+ @Override
+ public String asNumber() {
+ throw new UnsupportedOperationException("A JSON embedded object cannot be converted to a number.");
+ }
+
+ @Override
+ public String asString() {
+ throw new UnsupportedOperationException("A JSON embedded object cannot be converted to a string.");
+ }
+
+ @Override
+ public boolean asBoolean() {
+ throw new UnsupportedOperationException("A JSON embedded object cannot be converted to a boolean.");
+ }
+
+ @Override
+ public List asArray() {
+ throw new UnsupportedOperationException("A JSON embedded object cannot be converted to an array.");
+ }
+
+ @Override
+ public Map asObject() {
+ throw new UnsupportedOperationException("A JSON embedded object cannot be converted to an object.");
+ }
+
+ @Override
+ public Object asEmbeddedObject() {
+ return embeddedObject;
+ }
+
+ @Override
+ public T visit(JsonNodeVisitor visitor) {
+ return visitor.visitEmbeddedObject(asEmbeddedObject());
+ }
+
+ @Override
+ public String text() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "<>";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ EmbeddedObjectJsonNode that = (EmbeddedObjectJsonNode) o;
+
+ return embeddedObject.equals(that.embeddedObject);
+ }
+
+ @Override
+ public int hashCode() {
+ return embeddedObject.hashCode();
+ }
+}
diff --git a/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/NullJsonNode.java b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/NullJsonNode.java
new file mode 100644
index 000000000000..36b68ae8ad61
--- /dev/null
+++ b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/NullJsonNode.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocols.jsoncore.internal;
+
+import java.util.List;
+import java.util.Map;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.protocols.jsoncore.JsonNode;
+import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor;
+
+/**
+ * A null {@link JsonNode}.
+ */
+@SdkInternalApi
+public final class NullJsonNode implements JsonNode {
+ private static final NullJsonNode INSTANCE = new NullJsonNode();
+
+ private NullJsonNode() {
+ }
+
+ public static NullJsonNode instance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public boolean isNull() {
+ return true;
+ }
+
+ @Override
+ public String asNumber() {
+ throw new UnsupportedOperationException("A JSON null cannot be converted to a number.");
+ }
+
+ @Override
+ public String asString() {
+ throw new UnsupportedOperationException("A JSON null cannot be converted to a string.");
+ }
+
+ @Override
+ public boolean asBoolean() {
+ throw new UnsupportedOperationException("A JSON null cannot be converted to a boolean.");
+ }
+
+ @Override
+ public List asArray() {
+ throw new UnsupportedOperationException("A JSON null cannot be converted to an array.");
+ }
+
+ @Override
+ public Map asObject() {
+ throw new UnsupportedOperationException("A JSON null cannot be converted to an object.");
+ }
+
+ @Override
+ public Object asEmbeddedObject() {
+ throw new UnsupportedOperationException("A JSON null cannot be converted to an embedded object.");
+ }
+
+ @Override
+ public T visit(JsonNodeVisitor visitor) {
+ return visitor.visitNull();
+ }
+
+ @Override
+ public String text() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "null";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return this == obj;
+ }
+
+ @Override
+ public int hashCode() {
+ return 0;
+ }
+}
diff --git a/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/NumberJsonNode.java b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/NumberJsonNode.java
new file mode 100644
index 000000000000..b2042095b333
--- /dev/null
+++ b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/NumberJsonNode.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocols.jsoncore.internal;
+
+import java.util.List;
+import java.util.Map;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.protocols.jsoncore.JsonNode;
+import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor;
+
+/**
+ * A numeric {@link JsonNode}.
+ */
+@SdkInternalApi
+public final class NumberJsonNode implements JsonNode {
+ private final String value;
+
+ public NumberJsonNode(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public boolean isNumber() {
+ return true;
+ }
+
+ @Override
+ public String asNumber() {
+ return value;
+ }
+
+ @Override
+ public String asString() {
+ throw new UnsupportedOperationException("A JSON number cannot be converted to a string.");
+ }
+
+ @Override
+ public boolean asBoolean() {
+ throw new UnsupportedOperationException("A JSON number cannot be converted to a boolean.");
+ }
+
+ @Override
+ public List asArray() {
+ throw new UnsupportedOperationException("A JSON number cannot be converted to an array.");
+ }
+
+ @Override
+ public Map asObject() {
+ throw new UnsupportedOperationException("A JSON number cannot be converted to an object.");
+ }
+
+ @Override
+ public Object asEmbeddedObject() {
+ throw new UnsupportedOperationException("A JSON number cannot be converted to an embedded object.");
+ }
+
+ @Override
+ public T visit(JsonNodeVisitor visitor) {
+ return visitor.visitNumber(asNumber());
+ }
+
+ @Override
+ public String text() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ NumberJsonNode that = (NumberJsonNode) o;
+
+ return value.equals(that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+}
diff --git a/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/ObjectJsonNode.java b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/ObjectJsonNode.java
new file mode 100644
index 000000000000..2a388421f4e2
--- /dev/null
+++ b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/ObjectJsonNode.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocols.jsoncore.internal;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.protocols.jsoncore.JsonNode;
+import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor;
+
+/**
+ * An object {@link JsonNode}.
+ */
+@SdkInternalApi
+public final class ObjectJsonNode implements JsonNode {
+ private final Map value;
+
+ public ObjectJsonNode(Map value) {
+ this.value = value;
+ }
+
+ @Override
+ public boolean isObject() {
+ return true;
+ }
+
+ @Override
+ public String asNumber() {
+ throw new UnsupportedOperationException("A JSON object cannot be converted to a number.");
+ }
+
+ @Override
+ public String asString() {
+ throw new UnsupportedOperationException("A JSON object cannot be converted to a string.");
+ }
+
+ @Override
+ public boolean asBoolean() {
+ throw new UnsupportedOperationException("A JSON object cannot be converted to a boolean.");
+ }
+
+ @Override
+ public List asArray() {
+ throw new UnsupportedOperationException("A JSON object cannot be converted to an array.");
+ }
+
+ @Override
+ public Map asObject() {
+ return value;
+ }
+
+ @Override
+ public T visit(JsonNodeVisitor visitor) {
+ return visitor.visitObject(asObject());
+ }
+
+ @Override
+ public Object asEmbeddedObject() {
+ throw new UnsupportedOperationException("A JSON object cannot be converted to an embedded object.");
+ }
+
+ @Override
+ public String text() {
+ return null;
+ }
+
+ @Override
+ public Optional field(String child) {
+ return Optional.ofNullable(value.get(child));
+ }
+
+ @Override
+ public String toString() {
+ if (value.isEmpty()) {
+ return "{}";
+ }
+
+ StringBuilder output = new StringBuilder();
+ output.append("{");
+ value.forEach((k, v) -> output.append("\"").append(k).append("\": ")
+ .append(v.toString()).append(","));
+ output.setCharAt(output.length() - 1, '}');
+ return output.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ObjectJsonNode that = (ObjectJsonNode) o;
+
+ return value.equals(that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+}
diff --git a/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/StringJsonNode.java b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/StringJsonNode.java
new file mode 100644
index 000000000000..2d74673c6c77
--- /dev/null
+++ b/core/json-utils/src/main/java/software/amazon/awssdk/protocols/jsoncore/internal/StringJsonNode.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.protocols.jsoncore.internal;
+
+import java.util.List;
+import java.util.Map;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.protocols.jsoncore.JsonNode;
+import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor;
+import software.amazon.awssdk.utils.Validate;
+
+/**
+ * A string {@link JsonNode}.
+ */
+@SdkInternalApi
+public final class StringJsonNode implements JsonNode {
+ private final String value;
+
+ public StringJsonNode(String value) {
+ Validate.paramNotNull(value, "value");
+ this.value = value;
+ }
+
+ @Override
+ public boolean isString() {
+ return true;
+ }
+
+ @Override
+ public String asNumber() {
+ throw new UnsupportedOperationException("A JSON string cannot be converted to a number.");
+ }
+
+ @Override
+ public String asString() {
+ return value;
+ }
+
+ @Override
+ public boolean asBoolean() {
+ throw new UnsupportedOperationException("A JSON string cannot be converted to a boolean.");
+ }
+
+ @Override
+ public List asArray() {
+ throw new UnsupportedOperationException("A JSON string cannot be converted to an array.");
+ }
+
+ @Override
+ public Map asObject() {
+ throw new UnsupportedOperationException("A JSON string cannot be converted to an object.");
+ }
+
+ @Override
+ public Object asEmbeddedObject() {
+ throw new UnsupportedOperationException("A JSON string cannot be converted to an embedded object.");
+ }
+
+ @Override
+ public T visit(JsonNodeVisitor visitor) {
+ return visitor.visitString(asString());
+ }
+
+ @Override
+ public String text() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ // Does not handle unicode control characters
+ return "\"" +
+ value.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ + "\"";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ StringJsonNode that = (StringJsonNode) o;
+
+ return value.equals(that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+}
\ No newline at end of file
diff --git a/core/json-utils/src/test/java/software/amazon/awssdk/protocols/jsoncore/JsonNodeTest.java b/core/json-utils/src/test/java/software/amazon/awssdk/protocols/jsoncore/JsonNodeTest.java
new file mode 100644
index 000000000000..756c891a9225
--- /dev/null
+++ b/core/json-utils/src/test/java/software/amazon/awssdk/protocols/jsoncore/JsonNodeTest.java
@@ -0,0 +1,269 @@
+package software.amazon.awssdk.protocols.jsoncore;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.Test;
+import software.amazon.awssdk.utils.StringInputStream;
+
+public class JsonNodeTest {
+ private static final JsonNodeParser PARSER = JsonNode.parser();
+
+ @Test
+ public void parseString_works() {
+ assertThat(PARSER.parse("{}").isObject()).isTrue();
+ }
+
+ @Test
+ public void parseInputStream_works() {
+ assertThat(PARSER.parse(new StringInputStream("{}")).isObject()).isTrue();
+ }
+
+ @Test
+ public void parseByteArray_works() {
+ assertThat(PARSER.parse("{}".getBytes(UTF_8)).isObject()).isTrue();
+ }
+
+ @Test
+ public void parseNull_givesCorrectType() {
+ JsonNode node = PARSER.parse("null");
+
+ assertThat(node.isNull()).isTrue();
+ assertThat(node.isBoolean()).isFalse();
+ assertThat(node.isNumber()).isFalse();
+ assertThat(node.isString()).isFalse();
+ assertThat(node.isArray()).isFalse();
+ assertThat(node.isObject()).isFalse();
+ assertThat(node.isEmbeddedObject()).isFalse();
+
+ assertThatThrownBy(node::asBoolean).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asNumber).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asString).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asArray).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asObject).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asEmbeddedObject).isInstanceOf(UnsupportedOperationException.class);
+ }
+
+ @Test
+ public void parseBoolean_givesCorrectType() {
+ String[] options = { "true", "false" };
+ for (String option : options) {
+ JsonNode node = PARSER.parse(option);
+
+
+ assertThat(node.isNull()).isFalse();
+ assertThat(node.isBoolean()).isTrue();
+ assertThat(node.isNumber()).isFalse();
+ assertThat(node.isString()).isFalse();
+ assertThat(node.isArray()).isFalse();
+ assertThat(node.isObject()).isFalse();
+ assertThat(node.isEmbeddedObject()).isFalse();
+
+ assertThatThrownBy(node::asNumber).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asString).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asArray).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asObject).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asEmbeddedObject).isInstanceOf(UnsupportedOperationException.class);
+ }
+ }
+
+ @Test
+ public void parseNumber_givesCorrectType() {
+ String[] options = { "-1e100", "-1", "0", "1", "1e100" };
+ for (String option : options) {
+ JsonNode node = PARSER.parse(option);
+
+ assertThat(node.isNull()).isFalse();
+ assertThat(node.isBoolean()).isFalse();
+ assertThat(node.isNumber()).isTrue();
+ assertThat(node.isString()).isFalse();
+ assertThat(node.isArray()).isFalse();
+ assertThat(node.isObject()).isFalse();
+ assertThat(node.isEmbeddedObject()).isFalse();
+
+ assertThatThrownBy(node::asBoolean).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asString).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asArray).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asObject).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asEmbeddedObject).isInstanceOf(UnsupportedOperationException.class);
+ }
+ }
+
+ @Test
+ public void parseString_givesCorrectType() {
+ String[] options = { "\"\"", "\"foo\"" };
+ for (String option : options) {
+ JsonNode node = PARSER.parse(option);
+
+ assertThat(node.isNull()).isFalse();
+ assertThat(node.isBoolean()).isFalse();
+ assertThat(node.isNumber()).isFalse();
+ assertThat(node.isString()).isTrue();
+ assertThat(node.isArray()).isFalse();
+ assertThat(node.isObject()).isFalse();
+ assertThat(node.isEmbeddedObject()).isFalse();
+
+ assertThatThrownBy(node::asBoolean).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asNumber).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asArray).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asObject).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asEmbeddedObject).isInstanceOf(UnsupportedOperationException.class);
+ }
+ }
+
+ @Test
+ public void parseArray_givesCorrectType() {
+ String[] options = { "[]", "[null]" };
+ for (String option : options) {
+ JsonNode node = PARSER.parse(option);
+
+ assertThat(node.isNull()).isFalse();
+ assertThat(node.isBoolean()).isFalse();
+ assertThat(node.isNumber()).isFalse();
+ assertThat(node.isString()).isFalse();
+ assertThat(node.isArray()).isTrue();
+ assertThat(node.isObject()).isFalse();
+ assertThat(node.isEmbeddedObject()).isFalse();
+
+ assertThatThrownBy(node::asBoolean).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asNumber).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asString).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asObject).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asEmbeddedObject).isInstanceOf(UnsupportedOperationException.class);
+ }
+ }
+
+ @Test
+ public void parseObject_givesCorrectType() {
+ String[] options = { "{}", "{ \"foo\": null }" };
+ for (String option : options) {
+ JsonNode node = PARSER.parse(option);
+
+ assertThat(node.isNull()).isFalse();
+ assertThat(node.isBoolean()).isFalse();
+ assertThat(node.isNumber()).isFalse();
+ assertThat(node.isString()).isFalse();
+ assertThat(node.isArray()).isFalse();
+ assertThat(node.isObject()).isTrue();
+ assertThat(node.isEmbeddedObject()).isFalse();
+
+ assertThatThrownBy(node::asBoolean).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asNumber).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asString).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asArray).isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(node::asEmbeddedObject).isInstanceOf(UnsupportedOperationException.class);
+ }
+ }
+
+ @Test
+ public void parseBoolean_givesCorrectValue() {
+ assertThat(PARSER.parse("true").asBoolean()).isTrue();
+ assertThat(PARSER.parse("false").asBoolean()).isFalse();
+ }
+
+ @Test
+ public void parseNumber_givesCorrectValue() {
+ assertThat(PARSER.parse("0").asNumber()).isEqualTo("0");
+ assertThat(PARSER.parse("-1").asNumber()).isEqualTo("-1");
+ assertThat(PARSER.parse("1").asNumber()).isEqualTo("1");
+ assertThat(PARSER.parse("1e10000").asNumber()).isEqualTo("1e10000");
+ assertThat(PARSER.parse("-1e10000").asNumber()).isEqualTo("-1e10000");
+ assertThat(PARSER.parse("1.23").asNumber()).isEqualTo("1.23");
+ assertThat(PARSER.parse("-1.23").asNumber()).isEqualTo("-1.23");
+ }
+
+ @Test
+ public void parseString_givesCorrectValue() {
+ assertThat(PARSER.parse("\"foo\"").asString()).isEqualTo("foo");
+ assertThat(PARSER.parse("\"\"").asString()).isEqualTo("");
+ assertThat(PARSER.parse("\" \"").asString()).isEqualTo(" ");
+ assertThat(PARSER.parse("\"%20\"").asString()).isEqualTo("%20");
+ assertThat(PARSER.parse("\"\\\"\"").asString()).isEqualTo("\"");
+ assertThat(PARSER.parse("\" \"").asString()).isEqualTo(" ");
+ }
+
+ @Test
+ public void parseArray_givesCorrectValue() {
+ assertThat(PARSER.parse("[]").asArray()).isEmpty();
+ assertThat(PARSER.parse("[null, 1]").asArray()).satisfies(list -> {
+ assertThat(list).hasSize(2);
+ assertThat(list.get(0).isNull()).isTrue();
+ assertThat(list.get(1).asNumber()).isEqualTo("1");
+ });
+ }
+
+ @Test
+ public void parseObject_givesCorrectValue() {
+ assertThat(PARSER.parse("{}").asObject()).isEmpty();
+ assertThat(PARSER.parse("{\"foo\": \"bar\", \"baz\": 0}").asObject()).satisfies(map -> {
+ assertThat(map).hasSize(2);
+ assertThat(map.get("foo").asString()).isEqualTo("bar");
+ assertThat(map.get("baz").asNumber()).isEqualTo("0");
+ });
+ }
+
+ @Test
+ public void text_returnsContent() {
+ assertThat(PARSER.parse("null").text()).isEqualTo(null);
+ assertThat(PARSER.parse("0").text()).isEqualTo("0");
+ assertThat(PARSER.parse("\"foo\"").text()).isEqualTo("foo");
+ assertThat(PARSER.parse("true").text()).isEqualTo("true");
+ assertThat(PARSER.parse("[]").text()).isEqualTo(null);
+ assertThat(PARSER.parse("{}").text()).isEqualTo(null);
+ }
+
+ @Test
+ public void getString_returnsContent() {
+ assertThat(PARSER.parse("null").field("")).isEmpty();
+ assertThat(PARSER.parse("0").field("")).isEmpty();
+ assertThat(PARSER.parse("\"foo\"").field("")).isEmpty();
+ assertThat(PARSER.parse("true").field("")).isEmpty();
+ assertThat(PARSER.parse("[]").field("")).isEmpty();
+ assertThat(PARSER.parse("{\"\":0}").field("")).map(JsonNode::asNumber).hasValue("0");
+ }
+
+ @Test
+ public void getArray_returnsContent() {
+ assertThat(PARSER.parse("null").index(0)).isEmpty();
+ assertThat(PARSER.parse("0").index(0)).isEmpty();
+ assertThat(PARSER.parse("\"foo\"").index(0)).isEmpty();
+ assertThat(PARSER.parse("true").index(0)).isEmpty();
+ assertThat(PARSER.parse("[]").index(0)).isEmpty();
+ assertThat(PARSER.parse("[null]").index(0)).map(JsonNode::isNull).hasValue(true);
+ assertThat(PARSER.parse("{}").field("")).isEmpty();
+ }
+
+ @Test
+ public void toStringIsCorrect() {
+ String input = "{"
+ + "\"1\": \"2\","
+ + "\"3\": 4,"
+ + "\"5\": null,"
+ + "\"6\": false,"
+ + "\"7\": [[{}]],"
+ + "\"8\": \"\\\\n\\\"\""
+ + "}";
+ assertThat(PARSER.parse(input).toString()).isEqualTo(input);
+ }
+
+ @Test
+ public void exceptionsIncludeErrorLocation() {
+ assertThatThrownBy(() -> PARSER.parse("{{foo}")).hasMessageContaining("foo");
+ }
+
+ @Test
+ public void removeErrorLocations_removesErrorLocations() {
+ assertThatThrownBy(() -> JsonNode.parserBuilder()
+ .removeErrorLocations(true)
+ .build()
+ .parse("{{foo}"))
+ .satisfies(exception -> {
+ Throwable cause = exception;
+ while (cause != null) {
+ assertThat(cause.getMessage()).doesNotContain("foo");
+ cause = cause.getCause();
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/core/metrics-spi/pom.xml b/core/metrics-spi/pom.xml
index be2324caecd9..b56ae4a00d77 100644
--- a/core/metrics-spi/pom.xml
+++ b/core/metrics-spi/pom.xml
@@ -5,7 +5,7 @@
core
software.amazon.awssdk
- 2.16.105-SNAPSHOT
+ 2.17.0-SNAPSHOT
4.0.0
diff --git a/core/pom.xml b/core/pom.xml
index 379ee414ec1b..590b1b0fd8e0 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -21,7 +21,7 @@
aws-sdk-java-pom
software.amazon.awssdk
- 2.16.105-SNAPSHOT
+ 2.17.0-SNAPSHOT
core
@@ -42,6 +42,7 @@
regions
protocols
metrics-spi
+ json-utils
diff --git a/core/profiles/pom.xml b/core/profiles/pom.xml
index ef561baa35e5..aa1aca566287 100644
--- a/core/profiles/pom.xml
+++ b/core/profiles/pom.xml
@@ -22,7 +22,7 @@
software.amazon.awssdk
core
- 2.16.105-SNAPSHOT
+ 2.17.0-SNAPSHOT
profiles
diff --git a/core/protocols/aws-cbor-protocol/pom.xml b/core/protocols/aws-cbor-protocol/pom.xml
index d687d2725750..64c39a83308c 100644
--- a/core/protocols/aws-cbor-protocol/pom.xml
+++ b/core/protocols/aws-cbor-protocol/pom.xml
@@ -20,7 +20,7 @@
protocols
software.amazon.awssdk
- 2.16.105-SNAPSHOT
+ 2.17.0-SNAPSHOT
4.0.0
@@ -47,12 +47,14 @@
${awsjavasdk.version}