blob: 29ddc579f5cc65256e9038bb52d57bade7fe87e0 [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.gryo;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalExplanation;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.io.IoX;
import org.apache.tinkerpop.gremlin.structure.io.IoXIoRegistry;
import org.apache.tinkerpop.gremlin.structure.io.IoY;
import org.apache.tinkerpop.gremlin.structure.io.IoYIoRegistry;
import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONTokens;
import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex;
import org.apache.tinkerpop.shaded.kryo.ClassResolver;
import org.apache.tinkerpop.shaded.kryo.Kryo;
import org.apache.tinkerpop.shaded.kryo.Registration;
import org.apache.tinkerpop.shaded.kryo.Serializer;
import org.apache.tinkerpop.shaded.kryo.io.Input;
import org.apache.tinkerpop.shaded.kryo.io.Output;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Period;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.__;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.fail;
/**
* @author Stephen Mallette (http://stephen.genoprime.com)
*/
public class GryoMapperTest {
private final GryoMapper sharedMapper = GryoMapper.build().create();
private final Kryo sharedKryo = sharedMapper.createMapper();
@Test
public void shouldMakeNewInstance() {
final GryoMapper.Builder b = GryoMapper.build();
assertNotSame(b, GryoMapper.build());
}
@Test
public void shouldSerializeDeserialize() throws Exception {
final GryoMapper mapper = GryoMapper.build().create();
final Kryo kryo = mapper.createMapper();
try (final OutputStream stream = new ByteArrayOutputStream()) {
final Output out = new Output(stream);
final Map<String,Object> props = new HashMap<>();
final List<Map<String, Object>> propertyNames = new ArrayList<>(1);
final Map<String,Object> propertyName = new HashMap<>();
propertyName.put(GraphSONTokens.ID, "x");
propertyName.put(GraphSONTokens.KEY, "x");
propertyName.put(GraphSONTokens.VALUE, "no-way-this-will-ever-work");
propertyNames.add(propertyName);
props.put("x", propertyNames);
final DetachedVertex v = new DetachedVertex(100, Vertex.DEFAULT_LABEL, props);
kryo.writeClassAndObject(out, v);
try (final InputStream inputStream = new ByteArrayInputStream(out.toBytes())) {
final Input input = new Input(inputStream);
final DetachedVertex readX = (DetachedVertex) kryo.readClassAndObject(input);
assertEquals("no-way-this-will-ever-work", readX.value("x"));
}
}
}
@Test
public void shouldSerializeWithCustomClassResolverToDetachedVertex() throws Exception {
final Supplier<ClassResolver> classResolver = new CustomClassResolverSupplier();
final GryoMapper mapper = GryoMapper.build().classResolver(classResolver).create();
final Kryo kryo = mapper.createMapper();
try (final OutputStream stream = new ByteArrayOutputStream()) {
final Output out = new Output(stream);
final IoX x = new IoX("no-way-this-will-ever-work");
kryo.writeClassAndObject(out, x);
final GryoMapper mapperWithoutKnowledgeOfIox = GryoMapper.build().create();
final Kryo kryoWithoutKnowledgeOfIox = mapperWithoutKnowledgeOfIox.createMapper();
try (final InputStream inputStream = new ByteArrayInputStream(out.toBytes())) {
final Input input = new Input(inputStream);
final DetachedVertex readX = (DetachedVertex) kryoWithoutKnowledgeOfIox.readClassAndObject(input);
assertEquals("no-way-this-will-ever-work", readX.value("x"));
}
}
}
@Test
public void shouldSerializeWithCustomClassResolverToHashMap() throws Exception {
final Supplier<ClassResolver> classResolver = new CustomClassResolverSupplier();
final GryoMapper mapper = GryoMapper.build().classResolver(classResolver).create();
final Kryo kryo = mapper.createMapper();
try (final OutputStream stream = new ByteArrayOutputStream()) {
final Output out = new Output(stream);
final IoY y = new IoY(100, 200);
kryo.writeClassAndObject(out, y);
final GryoMapper mapperWithoutKnowledgeOfIoy = GryoMapper.build().create();
final Kryo kryoWithoutKnowledgeOfIox = mapperWithoutKnowledgeOfIoy.createMapper();
try (final InputStream inputStream = new ByteArrayInputStream(out.toBytes())) {
final Input input = new Input(inputStream);
final Map readY = (HashMap) kryoWithoutKnowledgeOfIox.readClassAndObject(input);
assertEquals("100-200", readY.get("y"));
}
}
}
@Test
public void shouldSerializeWithoutRegistration() throws Exception {
final GryoMapper mapper = GryoMapper.build().registrationRequired(false).create();
final Kryo kryo = mapper.createMapper();
try (final OutputStream stream = new ByteArrayOutputStream()) {
final Output out = new Output(stream);
final IoX x = new IoX("x");
final IoY y = new IoY(100, 200);
kryo.writeClassAndObject(out, x);
kryo.writeClassAndObject(out, y);
try (final InputStream inputStream = new ByteArrayInputStream(out.toBytes())) {
final Input input = new Input(inputStream);
final IoX readX = (IoX) kryo.readClassAndObject(input);
final IoY readY = (IoY) kryo.readClassAndObject(input);
assertEquals(x, readX);
assertEquals(y, readY);
}
}
}
@Test
public void shouldRegisterMultipleIoRegistryToSerialize() throws Exception {
final GryoMapper mapper = GryoMapper.build()
.addRegistry(IoXIoRegistry.InstanceBased.getInstance())
.addRegistry(IoYIoRegistry.InstanceBased.getInstance()).create();
final Kryo kryo = mapper.createMapper();
try (final OutputStream stream = new ByteArrayOutputStream()) {
final Output out = new Output(stream);
final IoX x = new IoX("x");
final IoY y = new IoY(100, 200);
kryo.writeClassAndObject(out, x);
kryo.writeClassAndObject(out, y);
try (final InputStream inputStream = new ByteArrayInputStream(out.toBytes())) {
final Input input = new Input(inputStream);
final IoX readX = (IoX) kryo.readClassAndObject(input);
final IoY readY = (IoY) kryo.readClassAndObject(input);
assertEquals(x, readX);
assertEquals(y, readY);
}
}
}
@Test
public void shouldExpectReadFailureAsIoRegistryOrderIsNotRespected() throws Exception {
final GryoMapper mapperWrite = GryoMapper.build()
.addRegistry(IoXIoRegistry.InstanceBased.getInstance())
.addRegistry(IoYIoRegistry.InstanceBased.getInstance()).create();
final GryoMapper mapperRead = GryoMapper.build()
.addRegistry(IoYIoRegistry.InstanceBased.getInstance())
.addRegistry(IoXIoRegistry.InstanceBased.getInstance()).create();
final Kryo kryoWriter = mapperWrite.createMapper();
final Kryo kryoReader = mapperRead.createMapper();
try (final OutputStream stream = new ByteArrayOutputStream()) {
final Output out = new Output(stream);
final IoX x = new IoX("x");
final IoY y = new IoY(100, 200);
kryoWriter.writeClassAndObject(out, x);
kryoWriter.writeClassAndObject(out, y);
try (final InputStream inputStream = new ByteArrayInputStream(out.toBytes())) {
final Input input = new Input(inputStream);
// kryo will read a IoY instance as we've reversed the registries. it is neither an X or a Y
// so assert that both are incorrect
final IoY readY = (IoY) kryoReader.readClassAndObject(input);
assertNotEquals(y, readY);
assertNotEquals(x, readY);
}
}
}
@Test
public void shouldOverrideExistingSerializer() throws Exception {
final GryoMapper mapper = GryoMapper.build()
.addCustom(Duration.class, new OverrideDurationSerializer()).create();
try (final OutputStream stream = new ByteArrayOutputStream()) {
final Output out = new Output(stream);
mapper.createMapper().writeObject(out, Duration.ZERO);
fail("The OverrideDurationSerializer throws exceptions so this should not have worked");
} catch (Exception ex) {
assertThat(ex, instanceOf(UnsupportedOperationException.class));
assertEquals("I don't do anything", ex.getMessage());
}
}
@Test
public void shouldHandleDuration() throws Exception {
final Duration o = Duration.ZERO;
assertEquals(o, serializeDeserialize(o, Duration.class));
}
@Test
public void shouldHandleInstant() throws Exception {
final Instant o = Instant.ofEpochMilli(System.currentTimeMillis());
assertEquals(o, serializeDeserialize(o, Instant.class));
}
@Test
public void shouldHandleLocalDate() throws Exception {
final LocalDate o = LocalDate.now();
assertEquals(o, serializeDeserialize(o, LocalDate.class));
}
@Test
public void shouldHandleLocalDateTime() throws Exception {
final LocalDateTime o = LocalDateTime.now();
assertEquals(o, serializeDeserialize(o, LocalDateTime.class));
}
@Test
public void shouldHandleLocalTime() throws Exception {
final LocalTime o = LocalTime.now();
assertEquals(o, serializeDeserialize(o, LocalTime.class));
}
@Test
public void shouldHandleMonthDay() throws Exception {
final MonthDay o = MonthDay.now();
assertEquals(o, serializeDeserialize(o, MonthDay.class));
}
@Test
public void shouldHandleOffsetDateTime() throws Exception {
final OffsetDateTime o = OffsetDateTime.now();
assertEquals(o, serializeDeserialize(o, OffsetDateTime.class));
}
@Test
public void shouldHandleOffsetTime() throws Exception {
final OffsetTime o = OffsetTime.now();
assertEquals(o, serializeDeserialize(o, OffsetTime.class));
}
@Test
public void shouldHandlePeriod() throws Exception {
final Period o = Period.ofDays(3);
assertEquals(o, serializeDeserialize(o, Period.class));
}
@Test
public void shouldHandleYear() throws Exception {
final Year o = Year.now();
assertEquals(o, serializeDeserialize(o, Year.class));
}
@Test
public void shouldHandleYearMonth() throws Exception {
final YearMonth o = YearMonth.now();
assertEquals(o, serializeDeserialize(o, YearMonth.class));
}
@Test
public void shouldHandleZonedDateTime() throws Exception {
final ZonedDateTime o = ZonedDateTime.now();
assertEquals(o, serializeDeserialize(o, ZonedDateTime.class));
}
@Test
public void shouldHandleZonedOffset() throws Exception {
final ZoneOffset o = ZonedDateTime.now().getOffset();
assertEquals(o, serializeDeserialize(o, ZoneOffset.class));
}
@Test
public void shouldHandleTraversalExplanation() throws Exception {
final TraversalExplanation te = __().out().outV().outE().explain();
assertEquals(te.toString(), serializeDeserialize(te, TraversalExplanation.class).toString());
}
public <T> T serializeDeserialize(final Object o, final Class<T> clazz) throws Exception {
try (final ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
final Output out = new Output(stream);
sharedKryo.writeObject(out, o);
out.flush();
try (final InputStream inputStream = new ByteArrayInputStream(stream.toByteArray())) {
final Input input = new Input(inputStream);
return sharedKryo.readObject(input, clazz);
}
}
}
/**
* Creates new {@link CustomClassResolver} when requested.
*/
public static class CustomClassResolverSupplier implements Supplier<ClassResolver> {
@Override
public ClassResolver get() {
return new CustomClassResolver();
}
}
/**
* A custom {@code ClassResolver} that alters the {@code Registration} returned to Kryo when an {@link IoX} class
* is requested, coercing it to a totally different class (a {@link DetachedVertex}). This coercion demonstrates
* how a TinkerPop provider might take a custom internal class and serialize it into something core to
* TinkerPop which then removes the requirement for providers to expose serializers on the client side for user
* consumption.
*/
public static class CustomClassResolver extends GryoClassResolver {
private IoXIoRegistry.IoXToVertexSerializer ioXToVertexSerializer = new IoXIoRegistry.IoXToVertexSerializer();
private IoYIoRegistry.IoYToHashMapSerializer ioYToHashMapSerializer = new IoYIoRegistry.IoYToHashMapSerializer();
public Registration getRegistration(final Class clazz) {
if (IoX.class.isAssignableFrom(clazz)) {
final Registration registration = super.getRegistration(DetachedVertex.class);
return new Registration(registration.getType(), ioXToVertexSerializer, registration.getId());
} else if (IoY.class.isAssignableFrom(clazz)) {
final Registration registration = super.getRegistration(HashMap.class);
return new Registration(registration.getType(), ioYToHashMapSerializer, registration.getId());
} else {
return super.getRegistration(clazz);
}
}
}
final static class OverrideDurationSerializer extends Serializer<Duration>
{
@Override
public void write(final Kryo kryo, final Output output, final Duration duration)
{
throw new UnsupportedOperationException("I don't do anything");
}
@Override
public Duration read(final Kryo kryo, final Input input, final Class<Duration> durationClass)
{
throw new UnsupportedOperationException("I don't do anything");
}
}
}