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

software.amazon.smithy.model.node.DefaultNodeSerializers 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 software.amazon.smithy.model.node.NodeMapper.Serializer;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.StringUtils;

/**
 * The default implementations use to convert Objects to Node values in {@link NodeMapper}.
 */
final class DefaultNodeSerializers {

    private static final Logger LOGGER = Logger.getLogger(DefaultNodeSerializers.class.getName());

    // Serialize the result of calling the ToNode#toNode method of an object.
    private static final Serializer TO_NODE_SERIALIZER = new Serializer() {
        @Override
        public Class getType() {
            return ToNode.class;
        }

        @Override
        public Node serialize(ToNode value, Set serializedObjects, NodeMapper mapper) {
            // Handle cases where the toNode method is disabled for a specific type.
            // This allows other serializers to attempt to serialize the value.
            if (mapper.getDisableToNode().contains(value.getClass())) {
                return null;
            }

            // TODO: make sure every instance of `toNode` is setting this
            return value.toNode();
        }
    };

    // Serialize the value contained in an Optional if present, or a NullNode if not present.
    private static final Serializer OPTIONAL_SERIALIZER = new Serializer() {
        @Override
        public Class getType() {
            return Optional.class;
        }

        @Override
        @SuppressWarnings("unchecked")
        public Node serialize(Optional value, Set serializedObjects, NodeMapper mapper) {
            return (Node) value.map(v -> mapper.serialize(v, serializedObjects)).orElse(Node.nullNode());
        }
    };

    // Serialize a Number into a NumberNode.
    private static final Serializer NUMBER_SERIALIZER = new Serializer() {
        @Override
        public Class getType() {
            return Number.class;
        }

        @Override
        public Node serialize(Number value, Set serializedObjects, NodeMapper mapper) {
            return Node.from(value);
        }
    };

    // Serialize a String into a StringNode.
    private static final Serializer STRING_SERIALIZER = new Serializer() {
        @Override
        public Class getType() {
            return String.class;
        }

        @Override
        public Node serialize(String value, Set serializedObjects, NodeMapper mapper) {
            return Node.from(value);
        }
    };

    // Serializes File instances.
    private static final Serializer FILE_SERIALIZER = new Serializer() {
        @Override
        public Class getType() {
            return File.class;
        }

        @Override
        public Node serialize(File value, Set serializedObjects, NodeMapper mapper) {
            return Node.from(value.getAbsolutePath());
        }
    };

    // Serializes Path instances.
    private static final Serializer PATH_SERIALIZER = new Serializer() {
        @Override
        public Class getType() {
            return Path.class;
        }

        @Override
        public Node serialize(Path value, Set serializedObjects, NodeMapper mapper) {
            return Node.from(value.toUri().toString());
        }
    };

    private static final class ToStringSerializer implements Serializer {
        private Class type;

        ToStringSerializer(Class type) {
            this.type = type;
        }

        @Override
        public Class getType() {
            return type;
        }

        @Override
        public Node serialize(T value, Set serializedObjects, NodeMapper mapper) {
            return Node.from(value.toString());
        }
    }

    private static final Serializer SHAPE_ID_SERIALIZER = new ToStringSerializer<>(ShapeId.class);

    // Mirror's Jackson's behavior of WRITE_ENUMS_USING_TO_STRING
    // See https://github.com/FasterXML/jackson-databind/wiki/Serialization-features
    private static final Serializer ENUM_SERIALIZER = new ToStringSerializer<>(Enum.class);

    // Mirror's a subset of Jackson's behavior.
    // See https://github.com/FasterXML/jackson-databind/blob/62c9d3dfe4b512380fdb7cfb38f6f9a0204f0c1a/src/main/java/com/fasterxml/jackson/databind/ser/std/StringLikeSerializer.java
    private static final Serializer URL_SERIALIZER = new ToStringSerializer<>(URL.class);
    private static final Serializer URI_SERIALIZER = new ToStringSerializer<>(URI.class);
    private static final Serializer PATTERN_SERIALIZER = new ToStringSerializer<>(Pattern.class);

    // Serialize a Boolean/boolean into a BooleanNode.
    private static final Serializer BOOLEAN_SERIALIZER = new Serializer() {
        @Override
        public Class getType() {
            return Boolean.class;
        }

        @Override
        public Node serialize(Boolean value, Set serializedObjects, NodeMapper mapper) {
            return Node.from(value);
        }
    };

    // Serialize a Map into an ObjectNode.
    private static final Serializer MAP_SERIALIZER = new Serializer() {
        @Override
        public Class getType() {
            return Map.class;
        }

        @Override
        @SuppressWarnings("unchecked")
        public Node serialize(Map value, Set serializedObjects, NodeMapper mapper) {
            Map mappings = new LinkedHashMap<>();
            Set> entries = (Set>) value.entrySet();

            // Iterate over the map entries and populate map entries for an ObjectNode.
            for (Map.Entry entry : entries) {
                // Serialize the key and require that it is serialized as a StringNode.
                Node key = mapper.serialize(entry.getKey(), serializedObjects);
                if (key instanceof StringNode) {
                    mappings.put((StringNode) key, mapper.serialize(entry.getValue(), serializedObjects));
                } else {
                    throw new NodeSerializationException(
                            "Unable to write Map key because it was not serialized as a string: "
                            + entry.getKey() + " -> " + Node.printJson(key));
                }
            }

            return new ObjectNode(mappings, SourceLocation.NONE);
        }
    };

    // Serialize the elements of an Iterable into an ArrayNode.
    private static final Serializer ITERABLE_SERIALIZER = new Serializer() {
        @Override
        public Class getType() {
            return Iterable.class;
        }

        @Override
        public Node serialize(Iterable value, Set serializedObjects, NodeMapper mapper) {
            List nodes = new ArrayList<>();
            for (Object item : value) {
                nodes.add(mapper.serialize(item, serializedObjects));
            }
            return new ArrayNode(nodes, SourceLocation.NONE);
        }
    };

    // Serialize an array of values into an ArrayNode.
    private static final Serializer ARRAY_SERIALIZER = new Serializer() {
        @Override
        public Class getType() {
            return Object[].class;
        }

        @Override
        public Node serialize(Object[] value, Set serializedObjects, NodeMapper mapper) {
            List nodes = new ArrayList<>();
            for (Object item : value) {
                nodes.add(mapper.serialize(item, serializedObjects));
            }
            return new ArrayNode(nodes, SourceLocation.NONE);
        }
    };

    /**
     * Contains the getters of a class that are eligible to convert to a Node.
     *
     * 

Getters are public methods that take zero arguments and start with * "get" or "is". Getters that are associated with properties marked as * {@code transient} are not serialized. */ private static final class ClassInfo { // Cache previously evaluated objects. private static final IdentityClassCache CACHE = new IdentityClassCache<>(); // Methods aren't returned normally in any particular order, so give them an order. final Map getters = new TreeMap<>(); static ClassInfo fromClass(Class klass) { return CACHE.getForClass(klass, klass, () -> { ClassInfo info = new ClassInfo(); Set transientFields = getTransientFields(klass); // Determine which methods are getters that aren't backed by transient properties. for (Method method : klass.getMethods()) { // Ignore Object.class, getSourceLocation, etc. if (isIgnoredMethod(klass, method)) { continue; } int fieldPrefixChars = getGetterPrefixCharCount(method); // If the method starts with the parsed prefix characters, then check if it's transient. if (fieldPrefixChars > 0 && fieldPrefixChars != method.getName().length()) { // Always normalize as the lowercase name (i.e., "getFoo" -> "foo"). String lowerFieldName = StringUtils.uncapitalize(method.getName().substring(fieldPrefixChars)); if (!transientFields.contains(lowerFieldName)) { info.getters.put(lowerFieldName, method); } else { LOGGER.fine(klass.getName() + " getter " + method.getName() + " is transient"); } } } LOGGER.fine(() -> "Detected the following getters for " + klass.getName() + ": " + info.getters); return info; }); } private static boolean isIgnoredMethod(Class klass, Method method) { // Ignore Object.class methods. if (method.getDeclaringClass() == Object.class) { return true; } // Special casing for ignore getSourceLocation. // Does this need to be made more generic? if (FromSourceLocation.class.isAssignableFrom(klass) && method.getName().equals("getSourceLocation")) { return true; } return false; } private static Set getTransientFields(Class klass) { Set transientFields = new HashSet<>(); for (Field field : klass.getDeclaredFields()) { if (Modifier.isTransient(field.getModifiers())) { // Normalize field names to lowercase the first character. transientFields.add(StringUtils.uncapitalize(field.getName())); } } return transientFields; } private static int getGetterPrefixCharCount(Method method) { // Don't use static methods, or methods with arguments. if (!Modifier.isStatic(method.getModifiers()) && method.getParameterCount() == 0) { if (method.getName().startsWith("get")) { return 3; } else if (method.getName().startsWith("is") && method.getReturnType() == boolean.class) { return 2; } } return 0; } } static final Serializer FROM_BEAN = new Serializer() { @Override public Class getType() { return Object.class; } @Override public Node serialize(Object value, Set serializedObjects, NodeMapper mapper) { if (serializedObjects.contains(value)) { return Node.nullNode(); } // Add the current value to the set. serializedObjects.add(value); Map mappings = new TreeMap<>(Comparator.comparing(StringNode::getValue)); ClassInfo info = ClassInfo.fromClass(value.getClass()); for (Map.Entry entry : info.getters.entrySet()) { try { Object getterResult = entry.getValue().invoke(value); Node result = mapper.serialize(getterResult, serializedObjects); if (canSerialize(mapper, result)) { mappings.put(Node.from(entry.getKey()), result); } } catch (ReflectiveOperationException e) { // There's almost always a previous exception, so grab it's more useful message. // If this isn't done, I observed that the message of ReflectiveOperationException is null. String causeMessage = e.getCause() != null ? e.getCause().getMessage() : e.getMessage(); String message = String.format( "Error serializing `%s` field of %s using %s(): %s", entry.getKey(), value.getClass().getName(), entry.getValue().getName(), causeMessage); throw new NodeSerializationException(message, e); } } // Remove the current value from the set to ensure that it can be serialized // multiple times (like in a List). serializedObjects.remove(value); // Pass on the source location if it is present. SourceLocation sourceLocation = SourceLocation.NONE; if (value instanceof FromSourceLocation) { sourceLocation = ((FromSourceLocation) value).getSourceLocation(); } return new ObjectNode(mappings, sourceLocation); } private boolean canSerialize(NodeMapper mapper, Node value) { if (!mapper.getSerializeNullValues() && value.isNullNode()) { return false; } if (mapper.getOmitEmptyValues()) { if (value.isObjectNode() && value.expectObjectNode().isEmpty()) { return false; } else if (value.isArrayNode() && value.expectArrayNode().isEmpty()) { return false; } else if (value.isBooleanNode() && !value.expectBooleanNode().getValue()) { return false; } } return true; } }; // The priority ordered list of default serializers that NodeMapper uses. // // The priority is determined based on the specificity of each deserializer; // the most specific ones should appear at the start of the list, and the // most generic ones should appear at the end. For example, Iterable is // very broad, and many things implement it. It should be at or near the // in of the list in case that same object implements some other // serializer. // // If we ever open up the API, then we should consider making the priority // more explicit by adding it to the Serializer interface. static final List SERIALIZERS = ListUtils.of( TO_NODE_SERIALIZER, OPTIONAL_SERIALIZER, STRING_SERIALIZER, BOOLEAN_SERIALIZER, NUMBER_SERIALIZER, MAP_SERIALIZER, ARRAY_SERIALIZER, SHAPE_ID_SERIALIZER, ENUM_SERIALIZER, URL_SERIALIZER, URI_SERIALIZER, PATTERN_SERIALIZER, PATH_SERIALIZER, FILE_SERIALIZER, // Lots of things implement iterable that have specialized serialization. ITERABLE_SERIALIZER ); private DefaultNodeSerializers() {} }