This is an automated email from the ASF dual-hosted git repository.
Cole-Greer pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/master by this push:
new de8954ad41 Restrict Class value deserialization in GraphSON and Gryo
(master) (#3706)
de8954ad41 is described below
commit de8954ad41fe06327b4513b19ddebf60029e387d
Author: Iryna-Kahamlyk <[email protected]>
AuthorDate: Thu Oct 22 00:25:49 2026 -0700
Restrict Class value deserialization in GraphSON and Gryo (master) (#3706)
Ported the upstream fix
(https://github.com/apache/tinkerpop/commit/d557266e4046672cbb5ed98fab56135ab87c38d2)
to master, adding class deserialization allowlisting for GraphSON and Gryo to
prevent arbitrary class loading. GraphBinary changes were not needed on master.
---
CHANGELOG.asciidoc | 1 +
docs/src/reference/gremlin-applications.asciidoc | 21 ++
docs/src/reference/the-traversal.asciidoc | 4 +
docs/src/upgrade/release-4.x.x.asciidoc | 17 ++
.../gremlin/structure/io/ClassRegistry.java | 119 ++++++++++
.../io/graphson/ClassJacksonDeserializer.java | 54 +++++
.../structure/io/graphson/GraphSONModule.java | 6 +
.../gremlin/structure/io/gryo/UtilSerializers.java | 16 +-
.../gremlin/structure/io/ClassRegistryTest.java | 246 ++++++++++++++++++++
.../io/graphson/ClassJacksonDeserializerTest.java | 219 ++++++++++++++++++
.../structure/io/gryo/ClassSerializerTest.java | 250 +++++++++++++++++++++
.../gremlin/structure/io/gryo/GryoMapperTest.java | 3 +-
12 files changed, 945 insertions(+), 11 deletions(-)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 41e5a47ac8..4808c2e2f1 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -26,6 +26,7 @@
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
=== TinkerPop 4.0.0 (Release Date: NOT OFFICIALLY RELEASED YET)
* Added a `Char` type to `gremlin-go` to represent the Gremlin `Character`
type in `GremlinLang`.
+* Restricted GraphSON and Gryo `Class` deserialization to classes registered
with `ClassRegistry.register()`, `registerStrategy()` or `registerStrategies()`.
* Fixed `gremlin-go` to report a malformed or truncated GraphBinary response
as a deserialization error rather than a bare decoder message.
* Made `TinkerGraph` an interface and renamed the in-memory implementation to
`TinkerMemoryGraph`; `TinkerGraph.open()` and `gremlin.graph=...TinkerGraph`
behave as before.
* Renamed `TinkerTransactionGraph` to `TinkerStorageGraph`.
diff --git a/docs/src/reference/gremlin-applications.asciidoc
b/docs/src/reference/gremlin-applications.asciidoc
index d39caf8690..ed8092c43a 100644
--- a/docs/src/reference/gremlin-applications.asciidoc
+++ b/docs/src/reference/gremlin-applications.asciidoc
@@ -1136,6 +1136,27 @@ It has the MIME type of
`application/vnd.graphbinary-v4.0` and the following con
As described above, there are multiple ways in which to register serializers
for GraphBinary-based serialization.
+[[class-registry]]
+===== Class Registry
+
+A `Class` value in GraphSON or Gryo is resolved through `ClassRegistry` rather
than through a class loader, so it can
+only name a class registered in advance. A name the registry does not hold is
refused and the named class is never
+loaded.
+
+[source,java]
+----
+ClassRegistry.register(MyClass.class);
+ClassRegistry.unregister(MyClass.class);
+----
+
+Registration is required in Java on the deserialization path only.
`ClassRegistry` matches the fully qualified class
+name exactly, so registering a superclass or an interface does not admit its
subclasses or implementors. Each concrete
+class that is deserialized needs its own registration.
+
+A `TraversalStrategy` is not registered here. Strategies belong to
`TraversalStrategies.GlobalCache.GLOBAL_REGISTRY`,
+which `ClassRegistry.lookup` also reads, so a registered strategy is nameable
already and passing one to
+`ClassRegistry.register` or `unregister` throws `IllegalArgumentException`.
+
[[metrics]]
==== Metrics
diff --git a/docs/src/reference/the-traversal.asciidoc
b/docs/src/reference/the-traversal.asciidoc
index 0a97fdd136..abbc25675b 100644
--- a/docs/src/reference/the-traversal.asciidoc
+++ b/docs/src/reference/the-traversal.asciidoc
@@ -2905,6 +2905,8 @@ g.io("graph.json").write().iterate();
NOTE: Additional documentation for GraphSON can be found in the
link:https://tinkerpop.apache.org/docs/x.y.z/dev/io/#graphson[IO Reference].
+NOTE: A `Class` stored as a property value can only be read back if it is
registered, see <<class-registry,Class Registry>>.
+
anchor:gryo-reader-writer[]
[[gryo]]
==== Gryo
@@ -2930,6 +2932,8 @@ g.io("graph.kryo").read().iterate()
g.io("graph.kryo").write().iterate()
----
+NOTE: A `Class` stored as a property value can only be read back if it is
registered, see <<class-registry,Class Registry>>.
+
*Additional References*
link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversalSource.html#io(java.lang.String)++[`io(String)`]
diff --git a/docs/src/upgrade/release-4.x.x.asciidoc
b/docs/src/upgrade/release-4.x.x.asciidoc
index 16e98aa801..f495f36a53 100644
--- a/docs/src/upgrade/release-4.x.x.asciidoc
+++ b/docs/src/upgrade/release-4.x.x.asciidoc
@@ -178,6 +178,23 @@ Storage" above: the protected `graphLocation` and
`graphFormat` fields and the `
are removed, as is `requiresPersistence()` on the test provider
`TinkerMemoryGraphProvider`. Durable storage is now
the job of a `TinkerStorage` engine on `TinkerStorageGraph`.
+===== Class Values Must Be Registered
+
+For deserialization, a `Class` value must now name a registered class in
GraphSON and Gryo alike. An unregistered name
+is refused on read and the class is never loaded. GraphBinary has no generic
`Class` wire type and is unaffected.
+Strategies registered with `TraversalStrategies.GlobalCache.registerStrategy`
already resolve, so only an application
+or provider that serializes its own classes needs to act.
+
+[source,java]
+----
+ClassRegistry.register(MyClass.class);
+ClassRegistry.unregister(MyClass.class);
+----
+
+Registration is required in Java for deserialization only, which typically
only impacts the server. Without
+registration, read attempts fail with a `Class not recognized` message naming
the class, raised as a
+`MismatchedInputException` for GraphSON and a `RuntimeException` for Gryo.
+
==== Graph Driver Providers
Drivers should send the user agent only as the `User-Agent` request header, in
the format described in the provider
diff --git
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/ClassRegistry.java
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/ClassRegistry.java
new file mode 100644
index 0000000000..f4af05a728
--- /dev/null
+++
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/ClassRegistry.java
@@ -0,0 +1,119 @@
+/*
+ * 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;
+
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchStep;
+import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedFactory;
+import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceFactory;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Holds the classes a GraphSON or Gryo {@code Class} value may name, so such
a name is resolved from this registry
+ * rather than by a class loader. Registration is a Java API only and refuses
a {@link TraversalStrategy}, which
+ * belongs to {@link TraversalStrategies.GlobalCache}.
+ * <p/>
+ * {@link #lookup(String)} also falls back to {@code
GlobalCache.getRegisteredStrategyClassByFullName}, so a strategy
+ * the selector can construct is nameable without being registered here.
+ */
+public final class ClassRegistry {
+
+ private ClassRegistry() {
+ }
+
+ /**
+ * Fully qualified names ({@link Class#getName()}) of the classes a
serialized traversal may name as a generic
+ * {@code Class} value but not construct. {@link #lookup(String)} guards a
{@code null} name itself, because a
+ * {@code ConcurrentHashMap} refuses a {@code null} key.
+ */
+ private static final Map<String, Class<?>> REGISTRY = new
ConcurrentHashMap<>();
+
+ // this block writes REGISTRY through register(), so the field above has
to be declared first.
+ static {
+ // Gryo names these two out of MatchAlgorithmStrategy's
matchAlgorithmClass field, the count one being what that
+ // strategy's builder holds when no algorithm is named.
+ register(MatchStep.GreedyMatchAlgorithm.class);
+ register(MatchStep.CountMatchAlgorithm.class);
+
+ // Gryo names these two out of HaltedTraverserStrategy's
haltedTraverserFactory field, which holds one or the
+ // other and nothing else.
+ register(DetachedFactory.class);
+ register(ReferenceFactory.class);
+ }
+
+ /**
+ * Registers a class as nameable as a generic {@code Class} value, without
making it constructible from a serialized
+ * traversal, so it cannot widen what the selector can construct. A {@link
TraversalStrategy} is refused with an
+ * {@link IllegalArgumentException} and a {@code null} with a {@link
NullPointerException}.
+ */
+ public static void register(final Class<?> clazz) {
+ Objects.requireNonNull(clazz, "clazz can not be null");
+ rejectStrategy(clazz);
+
+ REGISTRY.put(clazz.getName(), clazz);
+ }
+
+ /**
+ * Removes a class from this registry, so it is no longer nameable as a
generic {@code Class} value. A
+ * {@link TraversalStrategy} is refused because strategies belong to
{@link TraversalStrategies.GlobalCache}, and a
+ * {@code null} is refused with a {@link NullPointerException}.
+ */
+ public static void unregister(final Class<?> clazz) {
+ Objects.requireNonNull(clazz, "clazz can not be null");
+ rejectStrategy(clazz);
+
+ REGISTRY.remove(clazz.getName());
+ }
+
+ /**
+ * Looks up a class nameable as a generic {@code Class} value, without
loading it. Matching is on the exact
+ * {@link Class#getName()}, never assignability, so registering a
superclass or an interface does not admit its
+ * subtypes. A {@code null} name yields an empty {@link Optional}, since a
{@code Class} value can name nothing.
+ */
+ public static Optional<Class<?>> lookup(final String className) {
+ // a null key would throw, and it can match nothing, so it leaves
before the fall-back and its initialization.
+ if (null == className)
+ return Optional.empty();
+
+ final Class<?> clazz = REGISTRY.get(className);
+
+ // this call also forces GlobalCache initialization, which registers
the strategies from registerStrategies().
+ // Leave it here: a cached copy of what it reads would not.
+ return null != clazz
+ ? Optional.of(clazz)
+ :
TraversalStrategies.GlobalCache.getRegisteredStrategyClassByFullName(className)
+ .map(c -> (Class<?>) c);
+ }
+
+ /**
+ * Refuses a {@link TraversalStrategy}, so that a caller who reaches for
this registry is sent to the one the
+ * strategy selector reads and {@link #lookup(String)} falls back to. Both
callers guard {@code null} before this
+ * runs, so a class is assumed here.
+ */
+ private static void rejectStrategy(final Class<?> clazz) {
+ if (TraversalStrategy.class.isAssignableFrom(clazz))
+ throw new IllegalArgumentException("TraversalStrategy classes are
registered with " +
+ "TraversalStrategies.GlobalCache, not ClassRegistry - " +
clazz.getName());
+ }
+}
diff --git
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/ClassJacksonDeserializer.java
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/ClassJacksonDeserializer.java
new file mode 100644
index 0000000000..d4583a47ea
--- /dev/null
+++
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/ClassJacksonDeserializer.java
@@ -0,0 +1,54 @@
+/*
+ * 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.io.ClassRegistry;
+import org.apache.tinkerpop.shaded.jackson.core.JsonParser;
+import org.apache.tinkerpop.shaded.jackson.core.JsonToken;
+import org.apache.tinkerpop.shaded.jackson.databind.DeserializationContext;
+import org.apache.tinkerpop.shaded.jackson.databind.deser.std.StdDeserializer;
+
+import java.io.IOException;
+
+/**
+ * Resolves the name a {@code Class} value carries through {@link
ClassRegistry} rather than through a class loader, so
+ * the named class is never loaded. Registering this keeps Jackson's own
{@code Class} deserializer from running, which
+ * would resolve the name with {@code Class.forName(name, true, loader)} and
initialize whatever it reached.
+ */
+final class ClassJacksonDeserializer extends StdDeserializer<Class> {
+
+ ClassJacksonDeserializer() {
+ super(Class.class);
+ }
+
+ @Override
+ public Class deserialize(final JsonParser jsonParser, final
DeserializationContext deserializationContext) throws IOException {
+ // the parser arrives positioned on the value, so the name is the
current token and not the next one.
+ // A value that is not a string names no class, so it becomes a null
name and is refused by the same lookup.
+ final String name = JsonToken.VALUE_STRING ==
jsonParser.currentToken() ? jsonParser.getText() : null;
+
+ return ClassRegistry.lookup(name).
+ orElseThrow(() -> new IOException("Class not recognized - " +
name));
+ }
+
+ @Override
+ public boolean isCachable() {
+ return true;
+ }
+}
diff --git
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONModule.java
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONModule.java
index 95acff7c07..afd7a79e26 100644
---
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONModule.java
+++
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONModule.java
@@ -437,6 +437,9 @@ abstract class GraphSONModule extends
TinkerPopJacksonModule {
addDeserializer(Set.class, new
JavaUtilSerializersV3.SetJacksonDeserializer());
}
+ // java.lang - a Class value names a registered strategy rather
than anything a class loader can reach
+ addDeserializer(Class.class, new ClassJacksonDeserializer());
+
// numbers
addDeserializer(Integer.class, new
GraphSONSerializersV3.IntegerJackonsDeserializer());
addDeserializer(Double.class, new
GraphSONSerializersV3.DoubleJacksonDeserializer());
@@ -670,6 +673,9 @@ abstract class GraphSONModule extends
TinkerPopJacksonModule {
addDeserializer(Tree.class, new
GraphSONSerializersV2.TreeJacksonDeserializer());
addDeserializer(TraversalStrategyProxy.class, new
TraversalSerializersV2.TraversalStrategyProxyJacksonDeserializer<>());
+ // java.lang - a Class value names a registered strategy rather
than anything a class loader can reach
+ addDeserializer(Class.class, new ClassJacksonDeserializer());
+
// numbers
addDeserializer(Integer.class, new
GraphSONSerializersV2.IntegerJacksonDeserializer());
addDeserializer(Double.class, new
GraphSONSerializersV2.DoubleJacksonDeserializer());
diff --git
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/UtilSerializers.java
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/UtilSerializers.java
index bd7bc086e2..87a4f25407 100644
---
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/UtilSerializers.java
+++
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/gryo/UtilSerializers.java
@@ -18,6 +18,7 @@
*/
package org.apache.tinkerpop.gremlin.structure.io.gryo;
+import org.apache.tinkerpop.gremlin.structure.io.ClassRegistry;
import org.apache.tinkerpop.gremlin.structure.io.gryo.kryoshim.InputShim;
import org.apache.tinkerpop.gremlin.structure.io.gryo.kryoshim.KryoShim;
import org.apache.tinkerpop.gremlin.structure.io.gryo.kryoshim.OutputShim;
@@ -96,11 +97,8 @@ final class UtilSerializers {
@Override
public <I extends InputShim> Class read(final KryoShim<I, ?> kryo,
final I input, final Class<Class> clazz) {
final String name = input.readString();
- try {
- return Class.forName(name);
- } catch (Exception ex) {
- throw new RuntimeException(ex);
- }
+ return ClassRegistry.lookup(name).
+ orElseThrow(() -> new RuntimeException("Class not
recognized - " + name));
}
}
@@ -118,11 +116,9 @@ final class UtilSerializers {
final int size = input.readInt();
final Class[] clazzes = new Class[size];
for (int i = 0; i < size; i++) {
- try {
- clazzes[i] = Class.forName(input.readString());
- } catch (Exception ex) {
- throw new RuntimeException(ex);
- }
+ final String name = input.readString();
+ clazzes[i] = ClassRegistry.lookup(name).
+ orElseThrow(() -> new RuntimeException("Class not
recognized - " + name));
}
return clazzes;
}
diff --git
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/ClassRegistryTest.java
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/ClassRegistryTest.java
new file mode 100644
index 0000000000..48bd5f308c
--- /dev/null
+++
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/ClassRegistryTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchStep;
+import
org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
+import
org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ReadOnlyStrategy;
+import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedFactory;
+import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceFactory;
+import org.junit.After;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+import static
org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies.GlobalCache.getRegisteredStrategyClassByFullName;
+import static
org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies.GlobalCache.unregisterStrategy;
+import static org.apache.tinkerpop.gremlin.structure.io.ClassRegistry.lookup;
+import static org.apache.tinkerpop.gremlin.structure.io.ClassRegistry.register;
+import static
org.apache.tinkerpop.gremlin.structure.io.ClassRegistry.unregister;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+/**
+ * Covers what {@link ClassRegistry} holds, what it refuses, and how a name
resolves against it. Also covers
+ * {@link ClassRegistry#lookup(String)} falling back into {@code
TraversalStrategies.GlobalCache}, which is how a
+ * strategy the selector can construct is nameable without being registered
here.
+ */
+public class ClassRegistryTest {
+
+ @After
+ public void unregisterProviderType() {
+ unregister(ProviderType.class);
+ }
+
+ /**
+ * The registry holds the classes that are not strategies, which is what
it is for, so a registered one resolves by
+ * its fully qualified name without being known to the strategy selector.
+ */
+ @Test
+ public void shouldResolveRegisteredClassAsNameable() {
+ register(ProviderType.class);
+
+ assertEquals(ProviderType.class,
lookup(ProviderType.class.getName()).get());
+ }
+
+ /**
+ * Gryo names the two algorithms out of {@code MatchAlgorithmStrategy}'s
{@code matchAlgorithmClass} field and the
+ * two factories out of {@code HaltedTraverserStrategy}'s {@code
haltedTraverserFactory} field, so the seeds are
+ * what keep those readable. Nothing else here covers the seeds, only Gryo
suites in this module.
+ */
+ @Test
+ public void shouldSeedTheClassesGryoNamesFromStrategyFields() {
+ for (final Class<?> clazz :
Arrays.asList(MatchStep.GreedyMatchAlgorithm.class,
+ MatchStep.CountMatchAlgorithm.class, DetachedFactory.class,
ReferenceFactory.class)) {
+ assertEquals("the seed for " + clazz.getName() + " is missing",
clazz,
+ lookup(clazz.getName()).orElse(null));
+ }
+ }
+
+ /**
+ * A {@link TraversalStrategy} is registered with {@code GlobalCache},
which is what makes it nameable, so this
+ * registry refuses one rather than holding a second place to register it.
Both a strategy the selector knows and
+ * one it does not are refused, since the class being a strategy is the
whole of the test.
+ */
+ @Test
+ public void shouldRefuseToRegisterAStrategy() {
+ for (final Class<? extends TraversalStrategy> clazz :
Arrays.asList(ReadOnlyStrategy.class,
+ AbsentStrategy.class)) {
+ try {
+ register(clazz);
+ fail("A TraversalStrategy must not be registered with
ClassRegistry - " + clazz.getName());
+ } catch (IllegalArgumentException ex) {
+ assertThat(ex.getMessage(), containsString(clazz.getName()));
+ assertThat(ex.getMessage(),
containsString("TraversalStrategies.GlobalCache"));
+ }
+ }
+ }
+
+ /**
+ * A caller unregistering a strategy here is making the same mistake as
one registering it, since
+ * {@code GlobalCache.unregisterStrategy} is what takes a strategy's
nameability away.
+ */
+ @Test
+ public void shouldRefuseToUnregisterAStrategy() {
+ try {
+ unregister(ReadOnlyStrategy.class);
+ fail("A TraversalStrategy must not be unregistered from
ClassRegistry");
+ } catch (IllegalArgumentException ex) {
+ assertThat(ex.getMessage(),
containsString(ReadOnlyStrategy.class.getName()));
+ assertThat(ex.getMessage(),
containsString("TraversalStrategies.GlobalCache"));
+ }
+
+ assertEquals(ReadOnlyStrategy.class,
lookup(ReadOnlyStrategy.class.getName()).get());
+ }
+
+ /**
+ * A {@code null} reaching registration is a mistake in a caller's Java
code rather than anything a serialized
+ * traversal can send, so it fails with a message naming the parameter
instead of the bare
+ * {@link NullPointerException} that {@code isAssignableFrom} throws,
which names nothing at all. Contrast
+ * {@link #shouldNotResolveNullAsNameable}, where a {@code null} arrives
on a read path and must not throw.
+ */
+ @Test
+ public void shouldRefuseToRegisterNull() {
+ try {
+ register(null);
+ fail("A null must not be registered with ClassRegistry");
+ } catch (NullPointerException ex) {
+ assertThat(ex.getMessage(), containsString("clazz"));
+ }
+ }
+
+ /**
+ * Unregistration guards a {@code null} the same way, since both entry
points reach the same field and a caller
+ * passing one has made the same mistake in either direction.
+ */
+ @Test
+ public void shouldRefuseToUnregisterNull() {
+ try {
+ unregister(null);
+ fail("A null must not be unregistered from ClassRegistry");
+ } catch (NullPointerException ex) {
+ assertThat(ex.getMessage(), containsString("clazz"));
+ }
+ }
+
+ /**
+ * The other direction of the containment: construction implies
nameability, so a strategy registered for the
+ * selector resolves through both accessors without being registered as
nameable.
+ */
+ @Test
+ public void shouldResolveConstructibleStrategyAsNameable() {
+ assertEquals(ReadOnlyStrategy.class,
+
getRegisteredStrategyClassByFullName(ReadOnlyStrategy.class.getName()).get());
+ assertEquals(ReadOnlyStrategy.class,
+ lookup(ReadOnlyStrategy.class.getName()).get());
+ }
+
+ @Test
+ public void shouldNotResolveUnregisteredStrategyAsNameable() {
+ unregisterStrategy(AbsentStrategy.class);
+ assertFalse(lookup(AbsentStrategy.class.getName()).isPresent());
+ }
+
+ /**
+ * A {@code null} must reach an empty {@code Optional} rather than a
{@link NullPointerException}, since an
+ * unchecked throw on a deserialization read path escapes the request
handlers and leaves the client with no
+ * response at all.
+ */
+ @Test
+ public void shouldNotResolveNullAsNameable() {
+ assertFalse(lookup(null).isPresent());
+ }
+
+ @Test
+ public void shouldNotResolveSimpleNameAsNameable() {
+ register(ProviderType.class);
+
+
assertFalse(lookup(ReadOnlyStrategy.class.getSimpleName()).isPresent());
+ assertFalse(lookup(ProviderType.class.getSimpleName()).isPresent());
+ }
+
+ /**
+ * {@link ClassRegistry#lookup(String)} matches the exact {@link
Class#getName()}, never assignability, so
+ * registering a superclass admits that class alone. {@code Object} is the
widest case there is: were the match on
+ * assignability, one registration would make every class on the classpath
nameable. Registered here rather than in
+ * {@code unregisterProviderType} because no other test wants it, so it is
undone in a {@code finally}.
+ */
+ @Test
+ public void shouldNotResolveSubclassOfRegisteredClassAsNameable() {
+ register(Object.class);
+ try {
+ assertEquals(Object.class,
lookup(Object.class.getName()).orElse(null));
+
+ assertFalse(lookup(ProviderType.class.getName()).isPresent());
+ assertFalse(lookup(String.class.getName()).isPresent());
+ } finally {
+ unregister(Object.class);
+ }
+ }
+
+ /**
+ * The same exact match applies to an interface, so registering one does
not admit the classes that implement it.
+ * {@code ProviderType} implements {@code ProviderInterface} for this
test, so the registration being no help to it
+ * is the point rather than an accident of the two being unrelated.
+ */
+ @Test
+ public void shouldNotResolveImplementorOfRegisteredInterfaceAsNameable() {
+ register(ProviderInterface.class);
+ try {
+ assertEquals(ProviderInterface.class,
lookup(ProviderInterface.class.getName()).orElse(null));
+
+ assertFalse(lookup(ProviderType.class.getName()).isPresent());
+ } finally {
+ unregister(ProviderInterface.class);
+ }
+ }
+
+ /**
+ * Stands in for an interface a provider registers as nameable. {@code
ProviderType} implements it so that
+ * registering the interface can be shown not to admit the implementor.
+ */
+ private interface ProviderInterface {
+ }
+
+ /**
+ * Stands in for a class a provider registers as nameable, which is
deliberately not a {@link TraversalStrategy}
+ * because the registry refuses one. It is unregistered again in {@code
unregisterProviderType}.
+ */
+ private static final class ProviderType implements ProviderInterface {
+ }
+
+ /**
+ * A {@link TraversalStrategy} that the selector does not know, so
refusing to register it tests the class being a
+ * strategy rather than the selector already holding it.
+ */
+ private static final class AbsentStrategy
+ extends
AbstractTraversalStrategy<TraversalStrategy.DecorationStrategy>
+ implements TraversalStrategy.DecorationStrategy {
+
+ @Override
+ public void apply(final Traversal.Admin<?, ?> traversal) {
+ // do nothing
+ }
+ }
+}
diff --git
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/ClassJacksonDeserializerTest.java
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/ClassJacksonDeserializerTest.java
new file mode 100644
index 0000000000..50fc9c4bd4
--- /dev/null
+++
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/ClassJacksonDeserializerTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
+import
org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
+import
org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategy;
+import org.apache.tinkerpop.gremlin.structure.io.ClassRegistry;
+import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
+import
org.apache.tinkerpop.shaded.jackson.databind.exc.MismatchedInputException;
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Each test reads through both the V2 and V3 mappers, which share one
deserializer. The file is not parameterized by
+ * version because the canary can report only one load, so a second run would
see a flag the first had already set.
+ */
+public class ClassJacksonDeserializerTest extends AbstractGraphSONTest {
+
+ private final ObjectMapper mapperV2 =
GraphSONMapper.build().version(GraphSONVersion.V2_0).
+ typeInfo(TypeInfo.PARTIAL_TYPES).create().createMapper();
+
+ private final ObjectMapper mapperV3 =
GraphSONMapper.build().version(GraphSONVersion.V3_0).
+ typeInfo(TypeInfo.PARTIAL_TYPES).create().createMapper();
+
+ @After
+ public void unregisterProviderType() {
+ ClassRegistry.unregister(ProviderType.class);
+ }
+
+ /**
+ * {@code SubgraphStrategy} is registered with {@code
TraversalStrategies.GlobalCache}, which the registry falls
+ * back to, so nothing has to register it here.
+ */
+ @Test
+ public void shouldReadClassOfRegisteredStrategy() throws Exception {
+ assertEquals(SubgraphStrategy.class, serializeDeserialize(mapperV2,
SubgraphStrategy.class, Class.class));
+ assertEquals(SubgraphStrategy.class, serializeDeserialize(mapperV3,
SubgraphStrategy.class, Class.class));
+ }
+
+ /**
+ * A provider makes a class of its own nameable by calling {@link
ClassRegistry#register(Class)}, so that call is
+ * pinned here through the deserializer rather than only through the
registry's fall-back to {@code GlobalCache}.
+ * {@code ProviderType} is registered rather than refused, so do not write
a refusal test against it.
+ */
+ @Test
+ public void shouldReadClassRegisteredThroughClassRegistry() throws
Exception {
+ ClassRegistry.register(ProviderType.class);
+
+ assertEquals(ProviderType.class, serializeDeserialize(mapperV2,
ProviderType.class, Class.class));
+ assertEquals(ProviderType.class, serializeDeserialize(mapperV3,
ProviderType.class, Class.class));
+ }
+
+ @Test
+ public void shouldRejectClassThatIsNotRegistered() throws Exception {
+ final String fqcn = File.class.getName();
+
+ assertThat(refusalOf(mapperV2, fqcn).getMessage(),
containsString(fqcn));
+ assertThat(refusalOf(mapperV3, fqcn).getMessage(),
containsString(fqcn));
+ }
+
+ /**
+ * The counterpart to the refusal below, without which a blanket refusal
of every list holding a {@code Class} would
+ * pass that test. Both names resolve, one through {@code GlobalCache} and
one through {@link ClassRegistry}, so both
+ * resolution paths are covered inside a container, and both elements are
pinned by identity and in order so a
+ * partly read or empty list cannot pass.
+ */
+ @Test
+ public void shouldReadListElementsThatAreRegistered() throws Exception {
+ ClassRegistry.register(ProviderType.class);
+
+ final String elements = classValue(SubgraphStrategy.class.getName()) +
"," +
+ classValue(ProviderType.class.getName());
+
+ // V2 writes a list as a bare JSON array while V3 wraps it in g:List,
as in the refusal below
+ final List<?> fromV2 = readList(mapperV2, "[" + elements + "]");
+ final List<?> fromV3 = readList(mapperV3,
"{\"@type\":\"g:List\",\"@value\":[" + elements + "]}");
+
+ for (final List<?> read : Arrays.asList(fromV2, fromV3)) {
+ assertEquals(2, read.size());
+ assertSame(SubgraphStrategy.class, read.get(0));
+ assertSame(ProviderType.class, read.get(1));
+ }
+ }
+
+ /**
+ * A {@code Class} held inside a container reaches the same deserializer
as one read on its own, because a list
+ * reads each element as an {@code Object} and so resolves the type the
element itself carries. The unregistered
+ * name is the second element, so the refusal covers every element rather
than only the first. V2 writes a list as a
+ * bare JSON array while V3 wraps it in {@code g:List}, so the two reads
are not handed the same document.
+ */
+ @Test
+ public void shouldRejectListElementThatIsNotRegistered() throws Exception {
+ final String refusal = "Class not recognized - " +
File.class.getName();
+ final String elements = classValue(SubgraphStrategy.class.getName()) +
"," +
+ classValue(File.class.getName());
+
+ assertThat(refusalOfJson(mapperV2, "[" + elements + "]").getMessage(),
containsString(refusal));
+ assertThat(refusalOfJson(mapperV3,
"{\"@type\":\"g:List\",\"@value\":[" + elements + "]}").getMessage(),
+ containsString(refusal));
+ }
+
+ @Test
+ public void shouldNotLoadClassNamedByUnregisteredStrategy() throws
Exception {
+ assertFalse("the canary was already initialised before the read, so
this fixture no longer proves anything",
+ CanaryFlag.initialised);
+
+ final String fqcn = ClassJacksonDeserializerTest.class.getName() +
"$CanaryStrategy";
+ assertThat(refusalOf(mapperV2, fqcn).getMessage(),
containsString(fqcn));
+ assertThat(refusalOf(mapperV3, fqcn).getMessage(),
containsString(fqcn));
+
+ assertFalse("reading the name of an unregistered strategy initialised
the class it named",
+ CanaryFlag.initialised);
+
+ Class.forName(fqcn);
+ assertTrue("the canary cannot report a load, so the assertions above
cannot fail", CanaryFlag.initialised);
+ }
+
+ /**
+ * Reads a {@code Class} value naming {@code fqcn} into an {@code Object},
as a bytecode argument arrives, and
+ * returns the refusal. The deserializer's {@code IOException} surfaces as
a {@link MismatchedInputException}.
+ */
+ private MismatchedInputException refusalOf(final ObjectMapper mapper,
final String fqcn) throws Exception {
+ return refusalOfJson(mapper, classValue(fqcn));
+ }
+
+ /**
+ * Reads {@code json} into an {@code Object} and hands back the refusal it
produced, failing the calling test if it
+ * deserialized instead.
+ */
+ private MismatchedInputException refusalOfJson(final ObjectMapper mapper,
final String json) throws Exception {
+ try {
+ mapper.readValue(json, Object.class);
+ } catch (MismatchedInputException ex) {
+ return ex;
+ }
+
+ fail("A class that is not a registered strategy must not deserialize -
" + json);
+ return null;
+ }
+
+ /**
+ * Reads {@code json} into an {@code Object}, as a bytecode argument
arrives, and hands back the list it produced.
+ */
+ private List<?> readList(final ObjectMapper mapper, final String json)
throws Exception {
+ return (List<?>) mapper.readValue(json, Object.class);
+ }
+
+ /**
+ * The {@code Class} value a client sends, as it appears both on its own
and as an element of a list.
+ */
+ private static String classValue(final String fqcn) {
+ return "{\"@type\":\"g:Class\",\"@value\":\"" + fqcn + "\"}";
+ }
+
+ /**
+ * The flag lives here rather than on {@code CanaryStrategy} because
reading a static field initialises the class
+ * declaring it, so a flag on {@code CanaryStrategy} could not be read
without loading what it reports on.
+ */
+ static final class CanaryFlag {
+ static boolean initialised;
+ }
+
+ /**
+ * A {@link TraversalStrategy} that is never registered, so refusing it
tests registry membership rather than the
+ * class not being a strategy. Never name it as a class literal and never
register it.
+ */
+ static final class CanaryStrategy
+ extends
AbstractTraversalStrategy<TraversalStrategy.DecorationStrategy>
+ implements TraversalStrategy.DecorationStrategy {
+
+ static {
+ CanaryFlag.initialised = true;
+ }
+
+ @Override
+ public void apply(final Traversal.Admin<?, ?> traversal) {
+ // do nothing
+ }
+ }
+
+ /**
+ * Stands in for a class a provider ships, registered by each test that
reads it and unregistered again in
+ * {@code unregisterProviderType}. It is deliberately not a {@link
TraversalStrategy}, which
+ * the registry refuses, and it is registered rather than refused, so it
is not a refusal fixture:
+ * {@code CanaryStrategy} is the class that is never registered.
+ */
+ private static final class ProviderType {
+ }
+}
diff --git
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/ClassSerializerTest.java
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/ClassSerializerTest.java
new file mode 100644
index 0000000000..98f1acf806
--- /dev/null
+++
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/ClassSerializerTest.java
@@ -0,0 +1,250 @@
+/*
+ * 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.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
+import
org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
+import
org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategy;
+import org.apache.tinkerpop.gremlin.structure.io.ClassRegistry;
+import
org.apache.tinkerpop.gremlin.structure.io.gryo.kryoshim.shaded.ShadedInputAdapter;
+import org.apache.tinkerpop.shaded.kryo.Kryo;
+import org.apache.tinkerpop.shaded.kryo.io.Input;
+import org.apache.tinkerpop.shaded.kryo.io.Output;
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Covers both serializers that read a class name, {@code ClassSerializer} for
a single value and
+ * {@code ClassArraySerializer} for the {@code withoutStrategies()} argument.
Each test reads through both Gryo
+ * versions, which share them. The file is not parameterized by version
because the canary can report only one load, so
+ * a second run would see a flag the first had already set.
+ */
+public class ClassSerializerTest {
+
+ private static final GryoVersion[] VERSIONS = new
GryoVersion[]{GryoVersion.V1_0, GryoVersion.V3_0};
+
+ @After
+ public void unregisterProviderType() {
+ ClassRegistry.unregister(ProviderType.class);
+ }
+
+ /**
+ * A provider makes a class of its own nameable by calling {@link
ClassRegistry#register(Class)}, so that call is
+ * pinned here through the serializer rather than only through the
registry's fall-back to {@code GlobalCache}.
+ * {@code ProviderType} is registered rather than refused, so do not write
a refusal test against it.
+ */
+ @Test
+ public void shouldReadClassRegisteredThroughClassRegistry() throws
Exception {
+ ClassRegistry.register(ProviderType.class);
+
+ for (final GryoVersion version : VERSIONS) {
+ assertEquals(ProviderType.class, roundTrip(version,
ProviderType.class, Class.class));
+ }
+ }
+
+ /**
+ * {@code SubgraphStrategy} is registered with {@code
TraversalStrategies.GlobalCache}, which the registry falls
+ * back to, so only {@code ProviderType} has to be registered here. Two
elements are written so that the read
+ * covers the loop rather than one name.
+ */
+ @Test
+ public void shouldReadClassArrayRegisteredThroughClassRegistry() throws
Exception {
+ ClassRegistry.register(ProviderType.class);
+
+ final Class[] classes = new Class[]{SubgraphStrategy.class,
ProviderType.class};
+ for (final GryoVersion version : VERSIONS) {
+ assertArrayEquals(classes, roundTrip(version, classes,
Class[].class));
+ }
+ }
+
+ @Test
+ public void shouldRejectClassThatIsNotRegistered() throws Exception {
+ for (final GryoVersion version : VERSIONS) {
+ assertEquals("Class not recognized - " + File.class.getName(),
+ refusalOf(version, File.class, Class.class).getMessage());
+ }
+ }
+
+ /**
+ * The unregistered name is the second element, so the refusal covers
every element rather than only the first.
+ * Refusing part way through leaves the stream partly consumed, which
costs nothing because the whole read fails.
+ */
+ @Test
+ public void shouldRejectClassArrayThatIsNotRegistered() throws Exception {
+ final Class[] classes = new Class[]{SubgraphStrategy.class,
File.class};
+ for (final GryoVersion version : VERSIONS) {
+ assertEquals("Class not recognized - " + File.class.getName(),
+ refusalOf(version, classes, Class[].class).getMessage());
+ }
+ }
+
+ /**
+ * The counterpart to the refusal below, without which a blanket refusal
of every list holding a {@code Class} would
+ * pass that test. Both names resolve, one through {@code GlobalCache} and
one through {@link ClassRegistry}, so both
+ * resolution paths are covered inside a container, and both elements are
pinned by identity and in order so a
+ * partly read or empty list cannot pass.
+ */
+ @Test
+ public void shouldReadListElementsThatAreRegistered() throws Exception {
+ ClassRegistry.register(ProviderType.class);
+
+ final ArrayList<Class> classes = new
ArrayList<>(Arrays.asList(SubgraphStrategy.class, ProviderType.class));
+ for (final GryoVersion version : VERSIONS) {
+ final ArrayList<?> read = roundTrip(version, classes,
ArrayList.class);
+
+ assertEquals(2, read.size());
+ assertSame(SubgraphStrategy.class, read.get(0));
+ assertSame(ProviderType.class, read.get(1));
+ }
+ }
+
+ /**
+ * A {@code Class} held inside a container reaches the same serializer as
one read on its own, because Kryo reads
+ * each element of a collection that carries no element type with the
serializer registered for the class the
+ * element itself names. The unregistered name is the second element, so
the refusal covers every element rather
+ * than only the first.
+ */
+ @Test
+ public void shouldRejectListElementThatIsNotRegistered() throws Exception {
+ final ArrayList<Class> classes = new
ArrayList<>(Arrays.asList(SubgraphStrategy.class, File.class));
+ for (final GryoVersion version : VERSIONS) {
+ assertEquals("Class not recognized - " + File.class.getName(),
+ refusalOf(version, classes, ArrayList.class).getMessage());
+ }
+ }
+
+ /**
+ * The refusal test above would pass against an implementation that
resolved the name through a class loader before
+ * refusing, so this is the only test that distinguishes a refusal from
never having loaded the class. It detects a
+ * return to the initialising {@code Class.forName(name)}. The
three-argument form loads without initialising and
+ * would not be detected.
+ */
+ @Test
+ public void shouldNotLoadClassNamedByUnregisteredStrategy() throws
Exception {
+ assertFalse("the canary was already initialised before the read, so
this fixture no longer proves anything",
+ CanaryFlag.initialised);
+
+ final String fqcn = ClassSerializerTest.class.getName() +
"$CanaryStrategy";
+ try {
+ readName(fqcn);
+ fail("A class that is not a registered strategy must not
deserialize - " + fqcn);
+ } catch (RuntimeException ex) {
+ assertEquals("Class not recognized - " + fqcn, ex.getMessage());
+ }
+
+ assertFalse("reading the name of an unregistered strategy initialised
the class it named",
+ CanaryFlag.initialised);
+
+ Class.forName(fqcn);
+ assertTrue("the canary cannot report a load, so the assertions above
cannot fail", CanaryFlag.initialised);
+ }
+
+ private <T> T roundTrip(final GryoVersion version, final Object value,
final Class<T> clazz) throws Exception {
+ final Kryo kryo =
GryoMapper.build().version(version).create().createMapper();
+ try (final ByteArrayOutputStream stream = new ByteArrayOutputStream())
{
+ final Output output = new Output(stream);
+ kryo.writeObject(output, value);
+ output.flush();
+
+ try (final InputStream inputStream = new
ByteArrayInputStream(stream.toByteArray())) {
+ return kryo.readObject(new Input(inputStream), clazz);
+ }
+ }
+ }
+
+ /**
+ * Round trips {@code value} and hands back the refusal the read produced,
failing the calling test if it
+ * deserialized instead.
+ */
+ private RuntimeException refusalOf(final GryoVersion version, final Object
value,
+ final Class<?> clazz) throws Exception {
+ try {
+ roundTrip(version, value, clazz);
+ } catch (RuntimeException ex) {
+ return ex;
+ }
+
+ fail("A class that is not a registered strategy must not deserialize -
" + value);
+ return null;
+ }
+
+ /**
+ * Hands a name to {@code ClassSerializer} written the way its own {@code
write} writes one. The writer takes a
+ * loaded {@code Class}, so a name that must stay unloaded cannot be
produced by round tripping a class literal.
+ */
+ private Class readName(final String fqcn) {
+ final Output output = new Output(64, -1);
+ output.writeString(fqcn);
+ output.flush();
+
+ return new UtilSerializers.ClassSerializer().read(null,
+ new ShadedInputAdapter(new Input(output.toBytes())),
Class.class);
+ }
+
+ /**
+ * Records that the static initialiser of {@code CanaryStrategy} ran. It
is held here, on a class of its own,
+ * rather than on {@code CanaryStrategy} itself, because reading a static
field initialises the class that declares
+ * it: a flag on {@code CanaryStrategy} could not be read without loading
the very class it reports on.
+ */
+ static final class CanaryFlag {
+ static boolean initialised;
+ }
+
+ /**
+ * A {@link TraversalStrategy} on the test classpath, never registered, so
refusing it tests registry membership
+ * rather than the class not being a strategy. Never name it as a class
literal and never register it.
+ */
+ static final class CanaryStrategy
+ extends
AbstractTraversalStrategy<TraversalStrategy.DecorationStrategy>
+ implements TraversalStrategy.DecorationStrategy {
+
+ static {
+ CanaryFlag.initialised = true;
+ }
+
+ @Override
+ public void apply(final Traversal.Admin<?, ?> traversal) {
+ // do nothing
+ }
+ }
+
+ /**
+ * Stands in for a class a provider ships, registered by each test that
reads it and unregistered again in
+ * {@code unregisterProviderType}. It is deliberately not a {@link
TraversalStrategy}, which the registry refuses,
+ * and it is registered rather than refused, so it is not a refusal
fixture: {@code CanaryStrategy} is the class
+ * that is never registered.
+ */
+ private static final class ProviderType {
+ }
+}
diff --git
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapperTest.java
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapperTest.java
index 06ec76009f..36a4f56367 100644
---
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapperTest.java
+++
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/gryo/GryoMapperTest.java
@@ -23,6 +23,7 @@ import
org.apache.tinkerpop.gremlin.process.traversal.GremlinLang;
import org.apache.tinkerpop.gremlin.process.traversal.Merge;
import org.apache.tinkerpop.gremlin.process.traversal.TextP;
import
org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy;
+import
org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategy;
import
org.apache.tinkerpop.gremlin.process.traversal.util.TraversalExplanation;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.io.Io;
@@ -752,7 +753,7 @@ public class GryoMapperTest {
@Test
public void shouldHandleClass() throws Exception {
- final Class<?> clazz = java.io.File.class;
+ final Class<?> clazz = SubgraphStrategy.class;
assertEquals(clazz, serializeDeserialize(clazz, Class.class));
}