blob: bfa239b21267a49cdb66527166f2f37e53803a8d [file] [log] [blame]
/*
* 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 GraphSONVersion version;
private final TypeInfo typeInfo;
private GraphSONMapper(final Builder builder) {
this.customModules = builder.customModules;
this.loadCustomSerializers = builder.loadCustomModules;
this.normalize = builder.normalize;
this.version = builder.version;
if (null == builder.typeInfo)
this.typeInfo = builder.version == GraphSONVersion.V1_0 ? TypeInfo.NO_TYPES : TypeInfo.PARTIAL_TYPES;
else
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 && typeInfo == TypeInfo.NO_TYPES)
throw new IllegalStateException(String.format("GraphSON 3.0 does not support %s", TypeInfo.NO_TYPES));
if (version == GraphSONVersion.V3_0 || (version == GraphSONVersion.V2_0 && typeInfo != TypeInfo.NO_TYPES)) {
final GraphSONTypeIdResolver graphSONTypeIdResolver = new GraphSONTypeIdResolver();
final TypeResolverBuilder typer = new GraphSONTypeResolverBuilder(version)
.typesEmbedding(this.typeInfo)
.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 (typeInfo == TypeInfo.PARTIAL_TYPES) {
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();
}
/**
* Create a new Builder from a given {@link GraphSONMapper}.
*
* @return a new builder, with properties taken from the original mapper already applied.
*/
public static Builder build(final GraphSONMapper mapper) {
Builder builder = build();
builder.customModules = mapper.customModules;
builder.version = mapper.version;
builder.loadCustomModules = mapper.loadCustomSerializers;
builder.normalize = mapper.normalize;
builder.typeInfo = mapper.typeInfo;
return 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 List<IoRegistry> registries = new ArrayList<>();
private GraphSONVersion version = GraphSONVersion.V3_0;
/**
* GraphSON 2.0/3.0 should have types activated by default (3.0 does not have a typeless option), and 1.0
* should use no types by default.
*/
private TypeInfo typeInfo = null;
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#V3_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;
}
/**
* Specify if the values are going to be typed or not, and at which level.
*
* 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;
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);
}
}
}