Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
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() {}
}