/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License 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 org.apache.tinkerpop.gremlin.structure.io.graphson; import org.apache.tinkerpop.gremlin.structure.Graph; import org.apache.tinkerpop.gremlin.structure.io.IoRegistry; import org.apache.tinkerpop.gremlin.structure.io.Mapper; import org.apache.tinkerpop.shaded.jackson.annotation.JsonTypeInfo; import org.apache.tinkerpop.shaded.jackson.core.JsonGenerator; import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper; import org.apache.tinkerpop.shaded.jackson.databind.SerializationFeature; import org.apache.tinkerpop.shaded.jackson.databind.jsontype.TypeResolverBuilder; import org.apache.tinkerpop.shaded.jackson.databind.jsontype.impl.StdTypeResolverBuilder; import org.apache.tinkerpop.shaded.jackson.databind.module.SimpleModule; import org.apache.tinkerpop.shaded.jackson.databind.ser.DefaultSerializerProvider; import org.javatuples.Pair; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.util.UUID; /** * An extension to the standard Jackson {@code ObjectMapper} which automatically registers the standard * {@link GraphSONModule} for serializing {@link Graph} elements. This class * can be used for generalized JSON serialization tasks that require meeting GraphSON standards. * <p/> * {@link Graph} implementations providing an {@link IoRegistry} should register their {@code SimpleModule} * implementations to it as follows: * <pre> * {@code * public class MyGraphIoRegistry extends AbstractIoRegistry { * public MyGraphIoRegistry() { * register(GraphSONIo.class, null, new MyGraphSimpleModule()); * } * } * } * </pre> * * @author Stephen Mallette (http://stephen.genoprime.com) */ public class GraphSONMapper implements Mapper<ObjectMapper> { private final List<SimpleModule> customModules; private final boolean loadCustomSerializers; private final boolean normalize; private final boolean embedTypes; private final GraphSONVersion version; private final TypeInfo typeInfo; private GraphSONMapper(final Builder builder) { this.customModules = builder.customModules; this.loadCustomSerializers = builder.loadCustomModules; this.normalize = builder.normalize; this.embedTypes = builder.embedTypes; this.version = builder.version; this.typeInfo = builder.typeInfo; } @Override public ObjectMapper createMapper() { final ObjectMapper om = new ObjectMapper(); om.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); final GraphSONModule graphSONModule = version.getBuilder().create(normalize); om.registerModule(graphSONModule); customModules.forEach(om::registerModule); // plugin external serialization modules if (loadCustomSerializers) om.findAndRegisterModules(); // graphson 3.0 only allows type - there is no option to remove embedded types if (version == GraphSONVersion.V3_0 || (version == GraphSONVersion.V2_0 && typeInfo != TypeInfo.NO_TYPES)) { final GraphSONTypeIdResolver graphSONTypeIdResolver = new GraphSONTypeIdResolver(); final TypeResolverBuilder typer = new GraphSONTypeResolverBuilder() .typesEmbedding(getTypeInfo()) .valuePropertyName(GraphSONTokens.VALUEPROP) .init(JsonTypeInfo.Id.CUSTOM, graphSONTypeIdResolver) .typeProperty(GraphSONTokens.VALUETYPE); // Registers native Java types that are supported by Jackson registerJavaBaseTypes(graphSONTypeIdResolver); // Registers the GraphSON Module's types graphSONModule.getTypeDefinitions().forEach( (targetClass, typeId) -> graphSONTypeIdResolver.addCustomType( String.format("%s:%s", graphSONModule.getTypeNamespace(), typeId), targetClass)); // Register types to typeResolver for the Custom modules customModules.forEach(e -> { if (e instanceof TinkerPopJacksonModule) { final TinkerPopJacksonModule mod = (TinkerPopJacksonModule) e; final Map<Class, String> moduleTypeDefinitions = mod.getTypeDefinitions(); if (moduleTypeDefinitions != null) { if (mod.getTypeNamespace() == null || mod.getTypeNamespace().isEmpty()) throw new IllegalStateException("Cannot specify a module for GraphSON 2.0 with type definitions but without a type Domain. " + "If no specific type domain is required, use Gremlin's default domain, \"gremlin\" but there may be collisions."); moduleTypeDefinitions.forEach((targetClass, typeId) -> graphSONTypeIdResolver.addCustomType( String.format("%s:%s", mod.getTypeNamespace(), typeId), targetClass)); } } }); om.setDefaultTyping(typer); } else if (version == GraphSONVersion.V1_0 || version == GraphSONVersion.V2_0) { if (embedTypes) { final TypeResolverBuilder<?> typer = new StdTypeResolverBuilder() .init(JsonTypeInfo.Id.CLASS, null) .inclusion(JsonTypeInfo.As.PROPERTY) .typeProperty(GraphSONTokens.CLASS); om.setDefaultTyping(typer); } } else { throw new IllegalStateException("Unknown GraphSONVersion : " + version); } // this provider toStrings all unknown classes and converts keys in Map objects that are Object to String. final DefaultSerializerProvider provider = new GraphSONSerializerProvider(version); om.setSerializerProvider(provider); if (normalize) om.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS); // keep streams open to accept multiple values (e.g. multiple vertices) om.getFactory().disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); return om; } public GraphSONVersion getVersion() { return this.version; } public static Builder build() { return new Builder(); } public TypeInfo getTypeInfo() { return this.typeInfo; } private void registerJavaBaseTypes(final GraphSONTypeIdResolver graphSONTypeIdResolver) { Arrays.asList( UUID.class, Class.class, Calendar.class, Date.class, TimeZone.class, Timestamp.class ).forEach(e -> graphSONTypeIdResolver.addCustomType(String.format("%s:%s", GraphSONTokens.GREMLIN_TYPE_NAMESPACE, e.getSimpleName()), e)); } public static class Builder implements Mapper.Builder<Builder> { private List<SimpleModule> customModules = new ArrayList<>(); private boolean loadCustomModules = false; private boolean normalize = false; private boolean embedTypes = false; private List<IoRegistry> registries = new ArrayList<>(); private GraphSONVersion version = GraphSONVersion.V2_0; // GraphSON 2.0 should have types activated by default, otherwise use there's no point in using it instead of 1.0. private TypeInfo typeInfo = TypeInfo.PARTIAL_TYPES; private Builder() { } /** * {@inheritDoc} */ @Override public Builder addRegistry(final IoRegistry registry) { registries.add(registry); return this; } /** * Set the version of GraphSON to use. The default is {@link GraphSONVersion#V2_0}. */ public Builder version(final GraphSONVersion version) { this.version = version; return this; } /** * Set the version of GraphSON to use. */ public Builder version(final String version) { this.version = GraphSONVersion.valueOf(version); return this; } /** * Supply a mapper module for serialization/deserialization. */ public Builder addCustomModule(final SimpleModule custom) { this.customModules.add(custom); return this; } /** * Try to load {@code SimpleModule} instances from the current classpath. These are loaded in addition to * the one supplied to the {@link #addCustomModule(SimpleModule)}; */ public Builder loadCustomModules(final boolean loadCustomModules) { this.loadCustomModules = loadCustomModules; return this; } /** * Forces keys to be sorted. */ public Builder normalize(final boolean normalize) { this.normalize = normalize; return this; } /** * Embeds Java types into generated JSON to clarify their origins. Setting this value will override the value * of {@link #typeInfo(TypeInfo)} where true will set it to {@link TypeInfo#PARTIAL_TYPES} and false will set * it to {@link TypeInfo#NO_TYPES}. * * @deprecated As of release 3.2.1, replaced by {@link #typeInfo(TypeInfo)}. */ @Deprecated public Builder embedTypes(final boolean embedTypes) { this.embedTypes = embedTypes; this.typeInfo = embedTypes ? TypeInfo.PARTIAL_TYPES : TypeInfo.NO_TYPES; return this; } /** * Specify if the values are going to be typed or not, and at which level. Setting this value will override * the value of {@link #embedTypes(boolean)} where {@link TypeInfo#PARTIAL_TYPES} will set it to true and * {@link TypeInfo#NO_TYPES} will set it to false. * * The level can be {@link TypeInfo#NO_TYPES} or {@link TypeInfo#PARTIAL_TYPES}, and could be extended in the * future. */ public Builder typeInfo(final TypeInfo typeInfo) { this.typeInfo = typeInfo; if (typeInfo.equals(TypeInfo.PARTIAL_TYPES)) this.embedTypes = true; else if (typeInfo.equals(TypeInfo.NO_TYPES)) this.embedTypes = false; else throw new IllegalArgumentException("This value can only be set to PARTIAL_TYPES and NO_TYPES"); return this; } public GraphSONMapper create() { registries.forEach(registry -> { final List<Pair<Class, SimpleModule>> simpleModules = registry.find(GraphSONIo.class, SimpleModule.class); simpleModules.stream().map(Pair::getValue1).forEach(this.customModules::add); }); return new GraphSONMapper(this); } } }