This is an automated email from the ASF dual-hosted git repository.
Cole-Greer pushed a commit to branch 3.7-dev
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/3.7-dev by this push:
new d557266e40 Restrict Class value deserialization in GraphBinary,
GraphSON, and Gryo
d557266e40 is described below
commit d557266e4046672cbb5ed98fab56135ab87c38d2
Author: Guian Gumpac <[email protected]>
AuthorDate: Fri Aug 28 16:07:47 2026 -0700
Restrict Class value deserialization in GraphBinary, GraphSON, and Gryo
Added class registry that builds on top of the TraversalStrategy registry
Assisted-by: Claude Opus 5
---
CHANGELOG.asciidoc | 1 +
docs/src/reference/gremlin-applications.asciidoc | 21 ++
docs/src/reference/the-traversal.asciidoc | 5 +-
docs/src/upgrade/release-3.7.x.asciidoc | 16 +
.../gremlin/structure/io/ClassRegistry.java | 119 +++++++
.../structure/io/binary/types/ClassSerializer.java | 10 +-
.../io/graphson/ClassJacksonDeserializer.java | 54 +++
.../structure/io/graphson/GraphSONModule.java | 6 +
.../gremlin/structure/io/gryo/UtilSerializers.java | 16 +-
.../gremlin/process/TraversalStrategiesTest.java | 2 +-
.../gremlin/structure/io/ClassRegistryTest.java | 246 ++++++++++++++
.../io/graphson/ClassJacksonDeserializerTest.java | 219 +++++++++++++
.../structure/io/gryo/ClassSerializerTest.java | 250 ++++++++++++++
.../gremlin/structure/io/gryo/GryoMapperTest.java | 3 +-
gremlin-go/driver/connection_test.go | 2 +-
.../tinkerpop/gremlin/structure/io/Model.java | 4 +-
.../graphbinary/GraphBinaryCompatibilityTest.java | 22 ++
.../gremlin/util/ser/AbstractRoundTripTest.java | 2 +-
.../util/ser/binary/types/ClassSerializerTest.java | 361 +++++++++++++++++++++
.../types/TraversalStrategySerializerTest.java | 29 ++
.../{class-v1.gbin => class-unregistered-v1.gbin} | Bin
.../gremlin/structure/io/graphbinary/class-v1.gbin | Bin 18 -> 89 bytes
.../gremlin/structure/io/graphson/class-v2.json | 2 +-
.../gremlin/structure/io/graphson/class-v3.json | 2 +-
24 files changed, 1367 insertions(+), 25 deletions(-)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index b6865fa065..21474400d4 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -51,6 +51,7 @@
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* Deprecated `ProductiveByStrategy` which was introduced as a temporary way to
mimic pre-3.5.0 null processing behavior.
* Backported `TraversalStrategy` registration mechanism in
`TraversalStrategies` from the 3.8.x line.
* Restricted GraphBinary `TraversalStrategy` deserialization to registered
strategies that have not been denied with `denyStrategy()`.
+* Restricted GraphBinary, GraphSON and Gryo `Class` deserialization to classes
registered with `ClassRegistry.register()`, `registerStrategy()` or
`registerStrategies()`.
* Fixed `gremlin-python` GraphBinary serialization of
`BigInteger`/`BigDecimal` negative boundary values (e.g. `-129`) that raised
`OverflowError`.
* Fixed `gremlin-go` GraphBinary serialization of zero
`BigInteger`/`BigDecimal` values, which were encoded with zero length and
rejected by Java servers.
diff --git a/docs/src/reference/gremlin-applications.asciidoc
b/docs/src/reference/gremlin-applications.asciidoc
index cc80ae32e9..bdf8263b4d 100644
--- a/docs/src/reference/gremlin-applications.asciidoc
+++ b/docs/src/reference/gremlin-applications.asciidoc
@@ -1294,6 +1294,27 @@ construct the strategy from its configuration, so all
strategies should be regis
As described above, there are multiple ways in which to register serializers
for GraphBinary-based serialization. Note
that the `ioRegistries` setting is applied first, followed by the `custom`
setting.
+[[class-registry]]
+===== Class Registry
+
+A `Class` value in GraphBinary, 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 52945808a8..18c53baa3e 100644
--- a/docs/src/reference/the-traversal.asciidoc
+++ b/docs/src/reference/the-traversal.asciidoc
@@ -2301,6 +2301,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
@@ -2326,6 +2328,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)`]
@@ -6310,7 +6314,6 @@ Type ':help' or ':h' for help.
Display stack trace? [yN]
----
-
[[dsl]]
== Domain Specific Languages
diff --git a/docs/src/upgrade/release-3.7.x.asciidoc
b/docs/src/upgrade/release-3.7.x.asciidoc
index 93cd5fd283..f59e366613 100644
--- a/docs/src/upgrade/release-3.7.x.asciidoc
+++ b/docs/src/upgrade/release-3.7.x.asciidoc
@@ -225,6 +225,22 @@ Providers that send custom strategies over GraphBinary
must register every strat
using `registerStrategies()` or `registerStrategy()`, as described above. The
presence of a strategy on the application
class path is no longer sufficient.
+===== Class Values Must Be Registered
+
+For deserialization, a `Class` value must now name a registered class, in
GraphBinary, GraphSON and Gryo alike. An unregistered name is
+refused on read and the class is never loaded. 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 only required for in Java for deserialization, which typically
only impacts the server. Without
+registration, read attempts fail with a `Class not recognized` message naming
the class, raised as an
+`IOException` for GraphBinary, a `MismatchedInputException` for GraphSON and a
`RuntimeException` for Gryo.
+
== TinkerPop 3.7.6
*Release Date: April 1, 2026*
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..931c7e81ab
--- /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 GraphBinary, 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/binary/types/ClassSerializer.java
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ClassSerializer.java
index 7d2a516843..12b914cc1b 100644
---
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ClassSerializer.java
+++
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ClassSerializer.java
@@ -18,6 +18,7 @@
*/
package org.apache.tinkerpop.gremlin.structure.io.binary.types;
+import org.apache.tinkerpop.gremlin.structure.io.ClassRegistry;
import org.apache.tinkerpop.gremlin.structure.io.binary.DataType;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
@@ -26,18 +27,15 @@ import org.apache.tinkerpop.gremlin.structure.io.Buffer;
import java.io.IOException;
public class ClassSerializer extends SimpleTypeSerializer<Class> {
+
public ClassSerializer() {
super(DataType.CLASS);
}
-
@Override
protected Class readValue(final Buffer buffer, final GraphBinaryReader
context) throws IOException {
final String name = context.readValue(buffer, String.class, false);
- try {
- return Class.forName(name);
- } catch (Exception ex) {
- throw new RuntimeException(ex);
- }
+ return ClassRegistry.lookup(name).
+ orElseThrow(() -> new IOException("Class not recognized - " +
name));
}
@Override
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 37a79f5c1c..585ec65c2b 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
@@ -298,6 +298,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());
@@ -533,6 +536,9 @@ abstract class GraphSONModule extends
TinkerPopJacksonModule {
addDeserializer(TraversalMetrics.class, new
GraphSONSerializersV2.TraversalMetricsJacksonDeserializer());
addDeserializer(Tree.class, new
GraphSONSerializersV2.TreeJacksonDeserializer());
+ // 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/process/TraversalStrategiesTest.java
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/TraversalStrategiesTest.java
index 1311fef017..7fd3d0111f 100644
---
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/TraversalStrategiesTest.java
+++
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/TraversalStrategiesTest.java
@@ -76,7 +76,7 @@ public class TraversalStrategiesTest {
TraversalStrategies.GlobalCache.registerStrategies(TestGraphComputer.class,
TraversalStrategies.GlobalCache.getStrategies(GraphComputer.class).clone().addStrategies(new
StrategyC()));
}
-
+
@Test
public void shouldAllowUserManipulationOfGlobalCache() {
///////////
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..0933c7f1fa
--- /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 another 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 bd997f29c8..aabe7d34b0 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.Bytecode;
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;
@@ -757,7 +758,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));
}
diff --git a/gremlin-go/driver/connection_test.go
b/gremlin-go/driver/connection_test.go
index a04cddcbfc..5412515bb6 100644
--- a/gremlin-go/driver/connection_test.go
+++ b/gremlin-go/driver/connection_test.go
@@ -874,7 +874,7 @@ func TestConnection(t *testing.T) {
g := initializeGraph(t, testNoAuthUrl, testNoAuthAuthInfo,
testNoAuthTlsConfig)
defer g.remoteConnection.Close()
- prop := &GremlinType{"java.lang.Object"}
+ prop :=
&GremlinType{"org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategy"}
i := g.AddV("type_test").Property("data", prop).Iterate()
err := <-i
assert.Nil(t, err)
diff --git
a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/structure/io/Model.java
b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/structure/io/Model.java
index d4788bd5aa..b18d953051 100644
---
a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/structure/io/Model.java
+++
b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/structure/io/Model.java
@@ -31,6 +31,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Scope;
import org.apache.tinkerpop.gremlin.process.traversal.TextP;
import
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.BulkSet;
+import
org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategy;
import
org.apache.tinkerpop.gremlin.process.traversal.util.DefaultTraversalMetrics;
import org.apache.tinkerpop.gremlin.process.traversal.util.MutableMetrics;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalMetrics;
@@ -44,7 +45,6 @@ import
org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
import org.apache.tinkerpop.gremlin.util.message.ResponseMessage;
-import java.io.File;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
@@ -98,7 +98,7 @@ public class Model {
// IMPORTANT - the "title" or name of the Entry needs to be unique
- addCoreEntry(File.class, "Class", "");
+ addCoreEntry(SubgraphStrategy.class, "Class", "");
addCoreEntry(new Date(1481750076295L), "Date");
addCoreEntry(100.00d, "Double");
addCoreEntry(100.00f, "Float", "");
diff --git
a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphbinary/GraphBinaryCompatibilityTest.java
b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphbinary/GraphBinaryCompatibilityTest.java
index b12b3f4b5f..9c73f9c9c3 100644
---
a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphbinary/GraphBinaryCompatibilityTest.java
+++
b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphbinary/GraphBinaryCompatibilityTest.java
@@ -27,6 +27,7 @@ import org.apache.tinkerpop.gremlin.structure.io.Buffer;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
import org.apache.tinkerpop.gremlin.util.ser.NettyBufferFactory;
+import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -35,6 +36,10 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.fail;
+
/**
* @author Stephen Mallette (http://stephen.genoprime.com)
*/
@@ -112,4 +117,21 @@ public class GraphBinaryCompatibilityTest extends
AbstractTypedCompatibilityTest
public void shouldReadWriteAuthenticationChallenge() throws Exception {
super.shouldReadWriteAuthenticationChallenge();
}
+
+ /**
+ * The {@code class-unregistered} fixture is the {@code class} fixture,
when a {@code Class} value naming any
+ * class on the class path decoded. It names {@code java.io.File}, which
is not a registered
+ * {@link
org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy}, so it no
longer decodes.
+ * It is kept to record exactly which values stopped being readable.
+ */
+ @Test
+ public void shouldNotReadClassThatIsNotRegistered() throws Exception {
+ final byte[] bytes = readFromResource("class-unregistered");
+ try {
+ read(bytes, Class.class);
+ fail("A Class value naming a class that is not a registered
strategy must not deserialize");
+ } catch (IOException ex) {
+ assertThat(ex.getMessage(), containsString("Class not recognized -
java.io.File"));
+ }
+ }
}
diff --git
a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/AbstractRoundTripTest.java
b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/AbstractRoundTripTest.java
index 4ba4cede70..259dacf0ff 100644
---
a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/AbstractRoundTripTest.java
+++
b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/AbstractRoundTripTest.java
@@ -203,7 +203,7 @@ public abstract class AbstractRoundTripTest {
new Object[] {"Bytecode", bytecode, null},
new Object[] {"Binding", new Bytecode.Binding<>("x", 123),
null},
new Object[] {"Traverser", new
DefaultRemoteTraverser<>("marko", 100), null},
- new Object[] {"Class", Bytecode.class, null},
+ new Object[] {"Class", SubgraphStrategy.class, null},
new Object[] {"ByteBuffer", ByteBuffer.wrap(new byte[]{ 1, 2,
3 }), null},
new Object[] {"InetAddressV4",
InetAddress.getByName("127.0.0.1"), null},
new Object[] {"InetAddressV6", InetAddress.getByName("::1"),
null},
diff --git
a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/ClassSerializerTest.java
b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/ClassSerializerTest.java
new file mode 100644
index 0000000000..169899ea6f
--- /dev/null
+++
b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/ClassSerializerTest.java
@@ -0,0 +1,361 @@
+/*
+ * 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.util.ser.binary.types;
+
+import io.netty.buffer.ByteBufAllocator;
+import org.apache.tinkerpop.gremlin.process.traversal.Bytecode;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
+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.Buffer;
+import org.apache.tinkerpop.gremlin.structure.io.ClassRegistry;
+import org.apache.tinkerpop.gremlin.structure.io.binary.DataType;
+import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
+import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
+import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry;
+import
org.apache.tinkerpop.gremlin.structure.io.binary.types.SimpleTypeSerializer;
+import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph;
+import org.apache.tinkerpop.gremlin.util.ser.NettyBufferFactory;
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+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.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * A {@code Class} value carries a class name off the wire, and that name is
resolved through {@link ClassRegistry}
+ * rather than through a class loader, so only a class that registry holds, or
a {@link TraversalStrategy} registered
+ * with {@code TraversalStrategies.GlobalCache}, can be named. The write path
is deliberately left ungated because the
+ * writer is handed a class the server already holds.
+ */
+public class ClassSerializerTest {
+
+ private static final NettyBufferFactory bufferFactory = new
NettyBufferFactory();
+
+ private final ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
+
+ @After
+ public void unregisterFixtures() {
+
TraversalStrategies.GlobalCache.unregisterStrategy(NestedStrategy.class);
+ ClassRegistry.unregister(ProviderType.class);
+ }
+
+ @Test
+ public void shouldReadClassRegisteredAsABuiltIn() throws Exception {
+ assertEquals(SubgraphStrategy.class,
readClass(SubgraphStrategy.class.getName()));
+ }
+
+ /**
+ * The generic {@code Class} value exists for {@code withoutStrategies()},
so the decode is also pinned against the
+ * bytecode a client produces rather than only against a name written on
its own.
+ */
+ @Test
+ public void shouldDecodeRegisteredStrategyFromWithoutStrategiesBytecode()
throws Exception {
+ final Bytecode bytecode = EmptyGraph.instance().traversal().
+
withoutStrategies(SubgraphStrategy.class).V().asAdmin().getBytecode();
+
+ final Buffer buffer = bufferFactory.create(allocator.buffer());
+ new GraphBinaryWriter().write(bytecode, buffer);
+
+ assertEquals(bytecode, new GraphBinaryReader().read(buffer));
+ }
+
+ @Test
+ public void shouldRejectClassThatIsNotRegistered() throws Exception {
+ try {
+ readClass(File.class.getName());
+ fail("A class that is not a registered strategy must not
deserialize");
+ } catch (IOException ex) {
+ assertThat(ex.getMessage(), containsString("Class not recognized -
java.io.File"));
+ }
+ }
+
+ /**
+ * A refusal must not reveal whether the named class is on the classpath.
Present-but-unregistered and absent
+ * entirely get the same checked {@link IOException}, same message apart
from the name, no cause - that
+ * indistinguishability is what closing the oracle means. It holds by
construction: the registry lookup never
+ * consults a class loader. Pinned because a change that resolved the name
before refusing would still pass every
+ * other refusal test here.
+ */
+ @Test
+ public void shouldRefusePresentAndAbsentClassesIndistinguishably() throws
Exception {
+ final String present = File.class.getName();
+ final String absent =
"org.apache.tinkerpop.gremlin.util.ser.binary.types.NotOnTheClasspath";
+
+ // the test only means something while one name resolves and the other
does not, so both are pinned first
+ Class.forName(present);
+ try {
+ Class.forName(absent);
+ fail("the absent name must not be on the classpath for this test
to mean anything");
+ } catch (ClassNotFoundException expected) {
+ // the fixture holds
+ }
+
+ final IOException presentRefusal = refusalOf(present);
+ final IOException absentRefusal = refusalOf(absent);
+
+ assertEquals(presentRefusal.getClass(), absentRefusal.getClass());
+ assertEquals("Class not recognized - " + present,
presentRefusal.getMessage());
+ assertEquals("Class not recognized - " + absent,
absentRefusal.getMessage());
+
+ // the messages are the same once the name each one echoes is taken
out of it
+ assertEquals(presentRefusal.getMessage().replace(present, "?"),
+ absentRefusal.getMessage().replace(absent, "?"));
+
+ // nor does a cause answer what the message does not, such as a
ClassNotFoundException on one side only
+ assertNull(presentRefusal.getCause());
+ assertNull(absentRefusal.getCause());
+ }
+
+ @Test
+ public void shouldRejectEmptyName() throws Exception {
+ try {
+ readClass("");
+ fail("An empty class name must not deserialize");
+ } catch (IOException ex) {
+ assertEquals("Class not recognized - ", ex.getMessage());
+ }
+ }
+
+ /**
+ * A {@code String} value is not nullable at this position, so a null name
cannot arrive from a well-formed
+ * message. It is still worth pinning that a null reaches the same refusal
rather than a {@code NullPointerException}
+ * that would escape the request handlers and leave a client with no
response at all.
+ */
+ @Test
+ public void shouldRejectNullName() throws Exception {
+ final GraphBinaryReader reader = new
GraphBinaryReader(TypeSerializerRegistry.build().
+ add(String.class, new NullStringSerializer()).create());
+ try {
+ reader.readValue(bufferFactory.create(allocator.buffer()),
Class.class, false);
+ fail("A null class name must not deserialize");
+ } catch (IOException ex) {
+ assertEquals("Class not recognized - null", ex.getMessage());
+ }
+ }
+
+ @Test
+ public void shouldRejectArrayDescriptor() throws Exception {
+ try {
+ readClass("[Ljava.lang.String;");
+ fail("An array descriptor must not deserialize");
+ } catch (IOException ex) {
+ assertEquals("Class not recognized - [Ljava.lang.String;",
ex.getMessage());
+ }
+ }
+
+ @Test
+ public void shouldReadRegisteredNestedClass() throws Exception {
+ TraversalStrategies.GlobalCache.registerStrategy(NestedStrategy.class);
+
+ assertEquals(NestedStrategy.class,
readClass(NestedStrategy.class.getName()));
+ }
+
+ /**
+ * 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,
readClass(ProviderType.class.getName()));
+ }
+
+ /**
+ * Every other refusal test here 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";
+ final IOException refusal = refusalOf(fqcn);
+ assertEquals("Class not recognized - " + fqcn, refusal.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);
+ }
+
+ /**
+ * 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 Buffer buffer = bufferFactory.create(allocator.buffer());
+ new GraphBinaryWriter().write(Arrays.asList(SubgraphStrategy.class,
ProviderType.class), buffer);
+
+ final List<?> read = (List<?>) new GraphBinaryReader().read(buffer);
+
+ assertEquals(2, read.size());
+ assertSame(SubgraphStrategy.class, read.get(0));
+ assertSame(ProviderType.class, read.get(1));
+ }
+
+ /**
+ * A {@code Class} held inside a container is read by the same serializer
as one read on its own, because a
+ * collection reads each element as a fully qualified value and so
dispatches on the element's own type code. 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 Buffer buffer = bufferFactory.create(allocator.buffer());
+ new GraphBinaryWriter().write(Arrays.asList(SubgraphStrategy.class,
File.class), buffer);
+
+ try {
+ new GraphBinaryReader().read(buffer);
+ fail("A list holding a class that is not a registered strategy
must not deserialize");
+ } catch (IOException ex) {
+ assertThat(ex.getMessage(), containsString("Class not recognized -
" + File.class.getName()));
+ }
+ }
+
+ /**
+ * The writer is handed a class the server itself holds rather than a name
off the wire, so it is not gated. The
+ * consequence, recorded here, is that a server can write a {@code Class}
value that it would refuse to read.
+ */
+ @Test
+ public void shouldWriteClassThatIsNotRegistered() throws Exception {
+ final Buffer buffer = bufferFactory.create(allocator.buffer());
+ new GraphBinaryWriter().writeValue(File.class, buffer, false);
+
+ assertEquals(File.class.getName(), new
GraphBinaryReader().readValue(buffer, String.class, false));
+ }
+
+ /**
+ * The value of a {@code Class} is the class name written as a
non-nullable {@code String} value, so writing the
+ * name that way produces exactly what a client sends for a {@code Class}.
+ */
+ private Class readClass(final String fqcn) throws IOException {
+ final Buffer buffer = bufferFactory.create(allocator.buffer());
+ new GraphBinaryWriter().writeValue(fqcn, buffer, false);
+
+ return new GraphBinaryReader().readValue(buffer, Class.class, false);
+ }
+
+ /**
+ * Reads a {@code Class} value naming {@code fqcn} and hands back the
refusal it produced, failing the calling test
+ * if the name deserialized instead.
+ */
+ private IOException refusalOf(final String fqcn) throws IOException {
+ try {
+ readClass(fqcn);
+ } catch (IOException ex) {
+ return ex;
+ }
+
+ fail("A class that is not a registered strategy must not deserialize -
" + fqcn);
+ return null;
+ }
+
+ private static final class NestedStrategy
+ extends
AbstractTraversalStrategy<TraversalStrategy.DecorationStrategy>
+ implements TraversalStrategy.DecorationStrategy {
+
+ @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 unregisterFixtures}. 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 {
+ }
+
+ /**
+ * 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 -
+ * {@code shouldNotLoadClassNamedByUnregisteredStrategy} names it as a
string and watches its static initialiser.
+ */
+ 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 the {@code String} serializer to show that a null name is
refused the same way, which a
+ * well-formed message cannot produce at a non-nullable position.
+ */
+ private static class NullStringSerializer extends
SimpleTypeSerializer<String> {
+
+ NullStringSerializer() {
+ super(DataType.STRING);
+ }
+
+ @Override
+ protected String readValue(final Buffer buffer, final
GraphBinaryReader context) throws IOException {
+ return null;
+ }
+
+ @Override
+ protected void writeValue(final String value, final Buffer buffer,
+ final GraphBinaryWriter context) throws
IOException {
+ throw new IOException("the String serializer must not be asked to
write");
+ }
+ }
+}
diff --git
a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/TraversalStrategySerializerTest.java
b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/TraversalStrategySerializerTest.java
index 1c70494000..3ee763f06c 100644
---
a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/TraversalStrategySerializerTest.java
+++
b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/types/TraversalStrategySerializerTest.java
@@ -27,6 +27,7 @@ import
org.apache.tinkerpop.gremlin.process.traversal.strategy.TraversalStrategy
import
org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategy;
import
org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ReadOnlyStrategy;
import org.apache.tinkerpop.gremlin.structure.io.Buffer;
+import org.apache.tinkerpop.gremlin.structure.io.ClassRegistry;
import org.apache.tinkerpop.gremlin.structure.io.binary.DataType;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
@@ -67,6 +68,27 @@ public class TraversalStrategySerializerTest {
}
}
+ /**
+ * The other half of the one-way containment between the two registries,
seen at the wire. A name
+ * {@link ClassRegistry} holds decodes as a {@code Class} value, as
+ * {@code
ClassSerializerTest.shouldReadClassRegisteredThroughClassRegistry} shows, and
must still be refused here,
+ * because this position reflectively constructs what it resolves and only
{@code GlobalCache} admits a class to it.
+ */
+ @Test
+ public void shouldRejectStrategyNamedByAClassRegistryEntry() throws
Exception {
+ final String fqcn = ProviderType.class.getName();
+ ClassRegistry.register(ProviderType.class);
+
+ try {
+ readStrategy(reader(), fqcn);
+ fail("A class that is only nameable must not deserialize at the
strategy selector");
+ } catch (IOException ex) {
+ assertThat(ex.getMessage(), containsString("TraversalStrategy not
recognized - " + fqcn));
+ } finally {
+ ClassRegistry.unregister(ProviderType.class);
+ }
+ }
+
@Test
public void shouldRejectStrategyThatIsNotRegisteredWithoutInitializingIt()
throws Exception {
// a class literal does not initialize the class, so naming it this
way keeps the assertion below meaningful
@@ -178,6 +200,13 @@ public class TraversalStrategySerializerTest {
}
}
+ /**
+ * Stands in for a class a provider registers with {@link ClassRegistry},
which refuses a {@link TraversalStrategy},
+ * so a name that registry holds is never one the strategy selector may
construct.
+ */
+ private static final class ProviderType {
+ }
+
/**
* Stands in for the {@code ClassSerializer} to show that nothing consults
it while a strategy is read.
*/
diff --git
a/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary/class-v1.gbin
b/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary/class-unregistered-v1.gbin
similarity index 100%
copy from
gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary/class-v1.gbin
copy to
gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary/class-unregistered-v1.gbin
diff --git
a/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary/class-v1.gbin
b/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary/class-v1.gbin
index 6be272dc42..37e651f7f2 100644
Binary files
a/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary/class-v1.gbin
and
b/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary/class-v1.gbin
differ
diff --git
a/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphson/class-v2.json
b/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphson/class-v2.json
index 80f15a26ed..033a6efe65 100644
---
a/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphson/class-v2.json
+++
b/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphson/class-v2.json
@@ -1,4 +1,4 @@
{
"@type" : "g:Class",
- "@value" : "java.io.File"
+ "@value" :
"org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategy"
}
\ No newline at end of file
diff --git
a/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphson/class-v3.json
b/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphson/class-v3.json
index 80f15a26ed..033a6efe65 100644
---
a/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphson/class-v3.json
+++
b/gremlin-util/src/test/resources/org/apache/tinkerpop/gremlin/structure/io/graphson/class-v3.json
@@ -1,4 +1,4 @@
{
"@type" : "g:Class",
- "@value" : "java.io.File"
+ "@value" :
"org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategy"
}
\ No newline at end of file