All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.smithy.model.node.NodeMapper Maven / Gradle / Ivy

/*
 * Copyright 2020 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.smithy.model.node;

import static java.lang.String.format;

import java.io.File;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.utils.SmithyBuilder;

/**
 * Serializes and deserializes Smithy {@code Node} values to/from objects.
 *
 * 

This class does not serialize a {@code Node} value as a JSON * string. It converts Java Object values to and from {@code Node} values. * Use {@link Node#printJson(Node)} to serialize JSON strings from a * {@code Node} value. * *

When stable, we may add the ability to add custom serializers and * deserializers. Until then, there is no way to customize the serialization * and deserialization rules. */ public final class NodeMapper { /** * Specifies the behavior of the mapper when attempting to deserialize an unknown property. */ public enum WhenMissing { /** * Throws an exception when attempting to deserialize an unknown property. */ FAIL { public void handle(Type into, String pointer, String property, Node value) { String message = createMessage(property, pointer, into, value); throw new NodeDeserializationException(message, value.getSourceLocation()); } }, /** * Warns when attempting to deserialize an unknown property. */ WARN { public void handle(Type into, String pointer, String property, Node value) { LOGGER.warning(createMessage(property, pointer, into, value)); } }, /** * Ignores unknown properties. */ IGNORE { public void handle(Type into, String pointer, String property, Node value) { } }; /** * Invoked when an object property cannot be deserialized. * * @param into The value type being created. * @param pointer The JSON pointer to the type from the original node. * @param property The property that was unknown to the type. * @param value The Node being deserialized. */ public abstract void handle(Type into, String pointer, String property, Node value); private static String createMessage(String property, String pointer, Type into, Node node) { String location = node.getSourceLocation() == SourceLocation.NONE ? "" : " " + node.getSourceLocation().toString().trim(); return format("Deserialization error at %s%s: unable to find setter method for `%s` on %s", getNormalizedPointer(pointer), location, property, into.getTypeName()); } } /** * Converts an object of type {@code T} into a {@code Node}. * *

This API is currently package-private until we're confident in the * implementation. * * @param Type to convert into a {@code Node} value. */ interface Serializer { /** * Gets the type that this serializer handles. * * @return Returns the serialization type. */ Class getType(); /** * Converts an object of type {@code T} into a {@code Node}. * *

Return {@code null} to allow other serializers to attempt to * serialize a type. * * @param value Value to convert into a {@code Node}. * @param serializedObjects Identity set used to track recursion. * @param mapper Mapper used to recursively serialize types. * @return Returns the serialized {@code Node} value. */ Node serialize(T value, Set serializedObjects, NodeMapper mapper); } /** * Creates objects from Node values for a specific type. */ @FunctionalInterface interface ObjectCreator { /** * Creates an Object from the given {@code Node} into the given {@code target}. * * @param node Node to convert into {@code target}. * @param type Type to create. * @param pointer The JSON pointer to the current serialization context. * @param mapper Mapper to invoke to recursively deserialize values. * @return Returns the created {@code target} instance. * @throws NodeDeserializationException when unable to deserialize a value. */ Object create(Node node, Type type, String pointer, NodeMapper mapper); } /** * Factory method used to create an {@link ObjectCreator} for a given * {@code NodeType} and target class. * *

This interface is introduced to allow for caching of the reflection * necessary to determine how to create the target type. */ @FunctionalInterface interface ObjectCreatorFactory { /** * Returns an {@code ObjectCreator} used to create {@code target} from a * {@code Node} of type {@code nodeType}. * * @param nodeType Node type being converted. * @param target The class to create from the Node. * @param nodeMapper The NodeMapper being used to call the ObjectCreator. * @return Returns the {@code ObjectCreator} or {@code null} if the factory cannot handle the given arguments. * @throws NodeDeserializationException when unable to create a factory. */ ObjectCreator getCreator(NodeType nodeType, Type target, NodeMapper nodeMapper); } interface ObjectClassCreatorFactory { ObjectCreator getCreator(NodeType nodeType, Class target, NodeMapper nodeMapper); } private static final Logger LOGGER = Logger.getLogger(NodeMapper.class.getName()); private WhenMissing whenMissing = WhenMissing.WARN; private final Set disableToNode = new HashSet<>(); private final Set disableFromNode = new HashSet<>(); private boolean serializeNullValues = false; private boolean omitEmptyValues; private final List serializers = DefaultNodeSerializers.SERIALIZERS; private final ObjectCreatorFactory creatorFactory = DefaultNodeDeserializers.DEFAULT_CACHED_CREATOR; /** * Specifies if {@code null} values returned from getters are serialized. * * @param serializeNullValues Set to true to serialize {@code null} values. */ public void setSerializeNullValues(boolean serializeNullValues) { this.serializeNullValues = serializeNullValues; } /** * @return Gets whether or not {@code null} values are serialized. */ public boolean getSerializeNullValues() { return serializeNullValues; } /** * Sets the behavior of the deserializer when a setting is missing. * * @param whenMissing Behavior when a property is not matched to a setter. */ public void setWhenMissingSetter(WhenMissing whenMissing) { this.whenMissing = Objects.requireNonNull(whenMissing); } /** * @return Gets the behavior of the deserializer when a setting is missing. */ public WhenMissing getWhenMissingSetter() { return whenMissing; } /** * Disables the use of the {@code toNode} method for a specific class * when serializing the class as a POJO. * *

This method disables a specific concrete class and does not * disable subclasses or implementations of an interface. * *

This is useful when using the NodeMapper inside of a {@code toNode} * implementation. * * @param type Class to disable the {@code toNode} method serialization for. */ public void disableToNodeForClass(Type type) { disableToNode.add(type); } /** * Enables the use of the {@code toNode} method for a specific class * when serializing the class as a POJO. * * @param type Class to enable the {@code toNode} method serialization for. */ public void enableToNodeForClass(Type type) { disableToNode.remove(type); } /** * Gets the set of classes where {@code toNode} is disabled. * * @return Returns the disabled classes. */ public Set getDisableToNode() { return disableToNode; } /** * Disables the use of {@code fromNode} method for a specific class * when deserializing the class. * *

This method disables a specific concrete class and does not * disable subclasses or implementations of an interface. * *

This is useful when using the NodeMapper inside of a {@code fromNode} * implementation. * * @param type Class to disable the {@code fromNode} method deserialization for. */ public void disableFromNodeForClass(Type type) { disableFromNode.add(type); } /** * Enables the use of the {@code FromNode} method for a specific class * when deserializing the class. * * @param type Class to enable the {@code fromNode} method deserialization for. */ public void enableFromNodeForClass(Type type) { disableFromNode.remove(type); } /** * Gets the set of classes where {@code fromNode} is disabled. * * @return Returns the disabled classes. */ public Set getDisableFromNode() { return disableFromNode; } /** * Gets whether or not false, empty arrays, and empty objects are omitted from * serialized POJOs. * * @return Returns true if empty arrays and POJOs returned from POJO getters are omitted. */ public boolean getOmitEmptyValues() { return omitEmptyValues; } /** * Gets whether or not false, empty arrays, and empty objects are omitted from serialized POJOs. * * @param omitEmptyValues Set to true if false, empty arrays, and objects returned from POJO getters are omitted. */ public void setOmitEmptyValues(boolean omitEmptyValues) { this.omitEmptyValues = omitEmptyValues; } /** * Serializes the given {@code object} as a {@code Node}. * *

This method is able to serialize the following types in the * given evaluation order: * *

    *
  1. A {@code null} value is serialized as a {@link NullNode} if {@link #getSerializeNullValues()} * returns {@code true}. *
  2. *
  3. Instances of {@link ToNode} will return the result of calling {@link ToNode#toNode()}. *
  4. *
  5. Instances of {@link Optional} will serialize a {@link NullNode} when the Optional is empty, or * the result of serializing the value contained in the {@code Optional} when present. *
  6. *
  7. {@link String} value is serialized as a {@link StringNode}. *
  8. *
  9. {@link Boolean} value or {@code boolean} is serialized as a {@link BooleanNode}. *
  10. *
  11. Any instance of {@link Number} value is serialized as a {@link NumberNode}. *
  12. *
  13. The {@code toString} method is called when {@link URL}, {@link URI}, {@link Pattern}, and * {@link Path} are serialized. *
  14. *
  15. A {@link File} is serialized by serializing the string value of {@link File#toURI()}. *
  16. *
  17. {@link Enum} value is serialized as a {@link StringNode} by calling its {@code toString} method. *
  18. {@link ShapeId} is serialized as a {@link StringNode} that contains the absolute shape ID. *
  19. *
  20. Any instance of a {@link Map} is supported as long as the key and value of the map are both * supported types (note that Map keys must serialize as StringNode). A {@code Map} is converted to * an {@link ObjectNode}. *
  21. *
  22. Any instance of a {@link Iterable} is supported as long as the value contained in the * {@code Iterable} is a supported type. An {@code Iterable} is converted to an {@link ArrayNode}. * An {@code Iterable} broadly covers many Java types, including {@link Collection}. *
  23. *
  24. Primitive arrays are converted to an {@code ArrayNode} if and only if the values contained in the * array are one of the supported types supported by the serializer. *
  25. *
  26. Finally, an object is serialized using Bean style semantics; any public getter * (get* or is* method with no arguments) is invoked and it's return value is put in the {@link ObjectNode}. * Each property of the Bean recursively invokes the serializer and must be one of the supported types. * Properties associated with a getter that are marked as {@code transient} are not serialized (where an * "association" is defined as a class field with the same lowercase name as the suffix of the getter * method). For example, given a method "getFoo", both "foo" and "Foo" are checked as associated * property names. *
  27. *
* * @param object Object to serialize. * @return Returns the serialized {@code Node}. * @throws NodeSerializationException on error. */ public Node serialize(Object object) { return serialize(object, Collections.newSetFromMap(new IdentityHashMap<>())); } /** * Serializes the given {@code object} as a {@code Node}. * *

This method is used when serializing values recursively from another serializer. * * @param object Object to serialize. * @param serializedObject An identity set of objects that have already been serialized. * This prevents infinite recursion on a self-referencing value. * @return Returns the serialized {@code Node}. * @throws NodeSerializationException on error. */ @SuppressWarnings("unchecked") Node serialize(Object object, Set serializedObject) { if (object == null) { return Node.nullNode(); } // Iterate over the serializers in the correct order. for (Serializer serializer : serializers) { if (serializer.getType().isInstance(object)) { Node result = serializer.serialize(object, serializedObject, this); if (result != null) { return result; } } } // Finally, attempt to serialize using bean conventions. return DefaultNodeSerializers.FROM_BEAN.serialize(object, serializedObject, this); } /** * Deserialize a Node {@code value} into an instance of {@code T}. * *

This method can deserialize various kinds of values depending on the given * node type and the target type: * *

    *
  1. {@code null}
  2. *
  3. String
  4. *
  5. Primitive and boxed booleans
  6. *
  7. Primitive and boxed {@link Number} types
  8. *
  9. Lists and Sets of any support value
  10. *
  11. Maps with String keys and values of any supported type
  12. *
  13. Direct {@link Node} to {@code Node} conversions.
  14. *
  15. Any object that has a public static {@code fromNode} method that accepts a * {@link Node} and returns an instance of the object.
  16. *
  17. Strings are deserialized to enums by finding the first enum value that has a {@code toString} * method that matches the string value.
  18. *
  19. Built-in support for URI, URL, Pattern, Path, and File
  20. *
  21. When deserializing an object, any target object that provides a public static method named * {@code builder} that returns an instance of {@link SmithyBuilder} is invoked, and the builder is then * mutated using bean like setters (with an optional "set") prefix, until finally, the build method is * called and its result is returned.
  22. *
  23. When deserializing an object, the last thing tried is to find a public, zero-arg constructor, * and then the object is mutated using bean-style setter conventions for each key-value pair.
  24. *
  25. NodeMapper does not support non-static inner classes, classes with generic parameters, or * generic arrays. Support for these may be added in the future.
  26. *
* *

Objects with a public method named {@code sourceLocation} or {@code setSourceLocation} * are invoked and provided the source location of the deserialized {@code value}. * * @param value Value to deserialize. * @param into Class to create. * @param Type of value to create. * @return Returns the created value. * @throws NodeDeserializationException on error. * @see #deserializeCollection(Node, Class, Class) * @see #deserializeMap(Node, Class, Class) */ public T deserialize(Node value, Class into) { return deserializeNext(value, "", into, this); } /** * Invokes setters on the given {@code objectToMutate} from the provided * {@code Node}. * * @param value Value to deserialize. * @param objectToMutate Object to mutate and populate from the node. * @param The value to mutate using Bean style setters. * @return Returns the passed in value. * @throws NodeDeserializationException on error. */ public T deserializeInto(Node value, T objectToMutate) { try { DefaultNodeDeserializers.BeanMapper.apply(objectToMutate, value, objectToMutate.getClass(), "", this); return objectToMutate; } catch (ReflectiveOperationException e) { // Wrap unexpected exceptions. throw createError(objectToMutate.getClass(), "/", value, e.getMessage(), e); } } /** * Deserialize a Node {@code value} into a {@link Collection} {@code T} of {@code U} members. * *

This method is necessary because of Java's runtime type erasure. * * @param value Value to deserialize. * @param into Collection class to create. * @param members The collection's parametric type. * @param Type of collection value to create. * @param Type contained within the collection. * @param Returned collection type. * @return Returns the created collection. * @throws NodeDeserializationException on error. * @see #deserialize(Node, Class) */ public , U, V extends Collection> V deserializeCollection( Node value, Class into, Class members ) { ParameterizedType type = new ParameterizedType() { @Override public Type[] getActualTypeArguments() { return new Type[]{members}; } @Override public Type getRawType() { return into; } @Override public Type getOwnerType() { return null; } }; return deserializeNext(value, "", type, this); } /** * Deserialize a Node {@code value} into a {@link Map} {@code T} * with String keys and {@code U} values. * *

This method is necessary because of Java's runtime type erasure. * * @param value Value to deserialize. * @param into Map class to create. * @param members The maps's parametric type. * @param Type of map value to create. * @param Type contained within the map values. * @param Returned map type. * @return Returns the created map. * @throws NodeDeserializationException on error. * @see #deserialize(Node, Class) */ public , U, V extends Map> V deserializeMap( Node value, Class into, Class members ) { ParameterizedType type = new ParameterizedType() { @Override public Type[] getActualTypeArguments() { return new Type[]{String.class, members}; } @Override public Type getRawType() { return into; } @Override public Type getOwnerType() { return null; } }; return deserializeNext(value, "", type, this); } /** * Performs the inner deserialization of a type. * *

This method is invoked by instances of {@link ObjectCreator} to * recursively deserialize a type while also tracking the updated JSON * pointer context for error reporting. * * @param value Node value to deserialize. * @param pointer The JSON Pointer to the location of the value being deserialized. * @param into The type being created. * @param mapper The {@code Mapper} that can be invoked to recursively deserialize. * @param The type of value to create. * @return Returns the created value. */ @SuppressWarnings("unchecked") T deserializeNext(Node value, String pointer, Type into, NodeMapper mapper) { Objects.requireNonNull(value, "Deserialization value cannot be null"); Objects.requireNonNull(pointer, "Deserialization pointer cannot be null"); Objects.requireNonNull(into, "Deserialization into cannot be null"); Objects.requireNonNull(mapper, "Deserialization mapper cannot be null"); try { ObjectCreator creator = creatorFactory.getCreator(value.getType(), into, this); if (creator == null) { throw createError(into, pointer, value, null, null); } return (T) creator.create(value, into, pointer, mapper); } catch (NodeDeserializationException e) { // Rethrow already formatted exceptions. throw e; } catch (Exception e) { // Wrap unexpected exceptions. throw createError(into, pointer, value, e.getMessage(), e); } } private static NodeDeserializationException createError( Type into, String pointer, Node node, String message, Throwable cause ) { String errorMessage = createErrorMessage(into, pointer, node, message); return new NodeDeserializationException(errorMessage, node.getSourceLocation(), cause); } static String createErrorMessage(Type into, String pointer, Node node, String message) { String formatted = String.format( "Deserialization error at %s: unable to create %s from %s", getNormalizedPointer(pointer), into.getTypeName(), Node.printJson(node)); if (message != null) { formatted += ": " + message; } return formatted; } private static String getNormalizedPointer(String pointer) { return "(" + (pointer.equals(" ") || pointer.isEmpty() ? "/" : pointer) + ")"; } }