This is an automated email from the ASF dual-hosted git repository.

kenhuuu pushed a commit to branch 3.8-dev
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 35d8f49b6be07c767d8c73d4dab5b3918905b6f6
Merge: 82850f7783 739d5c4a6f
Author: Ken Hu <[email protected]>
AuthorDate: Mon Aug 31 10:17:23 2026 -0700

    Merge branch '3.7-dev' into 3.8-dev

 CHANGELOG.asciidoc                                 |   1 +
 .../gremlin-server/gremlin-server-integration.yaml |   2 +-
 docs/src/dev/io/graphson.asciidoc                  |  10 +-
 docs/src/reference/gremlin-applications.asciidoc   |  24 +-
 docs/src/upgrade/release-3.7.x.asciidoc            |  39 ++
 .../structure/io/graphson/GraphSONMapper.java      | 237 +++++++-
 .../com/example/gadget/GraphSONTestGadgets.java    | 127 +++++
 .../io/graphson/GraphSONMapperBuilderTest.java     |  40 ++
 .../GraphSONMapperPartialEmbeddedTypeTest.java     | 600 +++++++++++++++++++++
 .../gremlin/driver/ClusterConfigTest.java          |  54 ++
 .../gremlin/server/GremlinDriverIntegrateTest.java |  16 +-
 .../tinkerpop/gremlin/server/SettingsTest.java     |  44 ++
 .../gremlin/server/gremlin-server-integration.yaml |   1 +
 .../gremlin/structure/io/IoCustomTest.java         |   5 +-
 .../tinkerpop/gremlin/structure/io/IoTest.java     |   6 +-
 .../ser/AbstractGraphSONMessageSerializerV1.java   |  11 +
 .../util/ser/AbstractMessageSerializer.java        |   8 +-
 .../util/ser/GraphSONMessageSerializerV1Test.java  |  67 +++
 18 files changed, 1271 insertions(+), 21 deletions(-)

diff --cc docs/src/reference/gremlin-applications.asciidoc
index 5f5d091314,81c157247f..538f9322b7
--- a/docs/src/reference/gremlin-applications.asciidoc
+++ b/docs/src/reference/gremlin-applications.asciidoc
@@@ -1216,23 -1218,24 +1217,39 @@@ Gremlin Server is configured by defaul
  |=========================================================
  |Key |Description |Default
  |ioRegistries |A list of `IoRegistry` implementations to be applied to the 
serializer. |_none_
+ |allowedTypeIdNames |A list of fully qualified class names that 
`GraphSONMessageSerializerV1` is allowed to resolve from `@class` type ids, in 
addition to its defaults. |_none_
  |=========================================================
  
 +The GraphSON 2.0 and 3.0 reference implementations only deserialize a 
`TraversalStrategy` when its class is registered
 +with `TraversalStrategies.GlobalCache`. TinkerPop's built-in strategies are 
registered by default. Providers must
 +register every custom strategy before GraphSON input is read, either as part 
of a graph or graph computer strategy set
 +with `registerStrategies()`, or individually with `registerStrategy()`:
 +
 +[source,java]
 +----
 +TraversalStrategies.GlobalCache.registerStrategies(MyGraph.class, 
traversalStrategies);
 +// or
 +TraversalStrategies.GlobalCache.registerStrategy(MyStrategy.class);
 +----
 +
 +The presence of a strategy on the application class path is not sufficient. 
Registration permits serialized data to
 +construct the strategy from its configuration, so all strategies should be 
registered.
 +
+ The `allowedTypeIdNames` option only affects typed GraphSON 1.0 reads. The 
equivalent mapper configuration uses
+ `GraphSONMapper.Builder.addAllowedTypeIdName(String...)`:
+ 
+ [source,java]
+ ----
+ 
GraphSONMapper.build().version(GraphSONVersion.V1_0).typeInfo(TypeInfo.PARTIAL_TYPES)
+         .addAllowedTypeIdName("com.example.MyType", 
"com.example.MyOtherType").create();
+ ----
+ 
+ Each configured name is matched exactly, so it does not allow subclasses or 
other classes in the same package. An
+ array is checked by its component class name, so allowing 
`com.example.MyType` also allows arrays of that class.
+ Parameterized type ids are rejected before class-name matching. Therefore, a 
type id such as
+ `java.util.EnumMap<...>` cannot be enabled by adding either 
`java.util.EnumMap` or the parameterized type id to the
+ configured names.
+ 
  It is worth noting that GraphSON 1.0 still has some appeal for some users as 
it can be configured to produce an untyped
  JSON format which is a bit easier to consume than its successors which embed 
data types into the output. This version
  of GraphSON tends to be the one that users like to utilize when 
<<connecting-via-http,connecting via HTTP>> and is still
diff --cc 
gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapperPartialEmbeddedTypeTest.java
index 523cdc95c8,34948e6f3d..ea1d216e36
--- 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapperPartialEmbeddedTypeTest.java
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/structure/io/graphson/GraphSONMapperPartialEmbeddedTypeTest.java
@@@ -248,6 -279,567 +279,575 @@@ public class GraphSONMapperPartialEmbed
          assertEquals(100L, read.get("test"));
      }
  
+     @Test
+     public void shouldRejectNetworkPackageTypeWithEmbedTypeSettingV1() {
+         // a name resolves only by being listed, so java.net.URL does not, 
even though the sibling
+         // java.net.InetAddress that GraphSON 2.0/3.0 register does
+         assertDeniedByTypeValidator(v1Typed(),
+                 
"{\"@class\":\"java.util.HashMap\",\"v\":{\"@class\":\"java.net.URL\",\"u\":\"http://example.com\"}}";);
+     }
+ 
+     @Test
+     public void shouldRoundTripArraysWithEmbedTypeSettingV1() throws 
Exception {
+         // an array type id is decided by its component name, and a nested 
array carries one per level. A primitive
+         // component ("[B") names no class, while an object component 
("[Ljava.lang.String;") names one.
+         final ObjectMapper mapper = v1Typed();
+         assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) 
roundTripInMap(mapper, new byte[]{1, 2, 3}));
+         assertArrayEquals(new Boolean[]{true, false},
+                 (Boolean[]) roundTripInMap(mapper, new Boolean[]{true, 
false}));
+         assertArrayEquals(new String[]{"a", "b"}, (String[]) 
roundTripInMap(mapper, new String[]{"a", "b"}));
+         assertThat(Arrays.deepEquals(new String[][]{{"a"}, {"b"}},
+                 (String[][]) roundTripInMap(mapper, new String[][]{{"a"}, 
{"b"}})), is(true));
+     }
+ 
+     @Test
+     public void shouldRoundTripSqlAndUtilValueTypesWithEmbedTypeSettingV1() 
throws Exception {
+         // java.sql.Time is written as its toString, which formats and parses 
back in the default time zone, so
+         // valueOf of a fixed literal round-trips anywhere
+         final ObjectMapper mapper = v1Typed();
+         assertRoundTripsInMap(mapper, Arrays.asList(
+                 new java.sql.Timestamp(0L),
+                 java.sql.Time.valueOf("12:34:56"),
+                 Locale.US,
+                 Currency.getInstance("USD")));
+ 
+         // java.util.ArrayDeque does not define value equality, so it is 
compared element-wise
+         final Object read = roundTripInMap(mapper, new 
ArrayDeque<>(Arrays.asList("a", "b")));
+         assertThat(read, instanceOf(ArrayDeque.class));
+         assertEquals(Arrays.asList("a", "b"), new ArrayList<>((ArrayDeque<?>) 
read));
+     }
+ 
+     @Test
+     public void 
shouldRejectArrayOfDisallowedComponentWithEmbedTypeSettingV1() {
+         // an array type id is decided by its component name, so an unlisted 
component does not resolve
+         assertDeniedByTypeValidator(v1Typed(),
+                 
"{\"@class\":\"java.util.HashMap\",\"v\":[\"[Ljava.io.File;\",[\"/tmp/x\"]]}");
+     }
+ 
+     @Test
+     public void shouldAllowConfiguredTypeIdNameWithEmbedTypeSettingV1() 
throws Exception {
+         final String json = 
"{\"@class\":\"java.util.HashMap\",\"p\":{\"@class\":\"com.example.gadget.GraphSONTestGadgets$SamplePojo\",\"x\":42}}";
+ 
+         // not among the allowed names by default
+         assertDeniedByTypeValidator(v1Typed(), json);
+ 
+         final ObjectMapper mapper = 
GraphSONMapper.build().version(GraphSONVersion.V1_0).typeInfo(TypeInfo.PARTIAL_TYPES)
+                 
.addAllowedTypeIdName("com.example.gadget.GraphSONTestGadgets$SamplePojo").create().createMapper();
+         final Map read = mapper.readValue(json, HashMap.class);
+         assertEquals(new SamplePojo(42), read.get("p"));
+     }
+ 
+     @Test
+     public void 
shouldAllowConfiguredTypeIdNameInsideCollectionWithEmbedTypeSettingV1() throws 
Exception {
+         final ObjectMapper mapper = 
GraphSONMapper.build().version(GraphSONVersion.V1_0)
+                 .typeInfo(TypeInfo.PARTIAL_TYPES)
+                 .addAllowedTypeIdName(SamplePojo.class.getName())
+                 .create().createMapper();
+ 
+         final Object read = roundTripInMap(mapper, 
Collections.singletonList(new SamplePojo(42)));
+         assertEquals(Collections.singletonList(new SamplePojo(42)), read);
+         assertEquals(SamplePojo.class, ((List<?>) read).get(0).getClass());
+     }
+ 
+     @Test
+     public void 
shouldAllowArrayOfConfiguredTypeIdNameButRejectSubclassWithEmbedTypeSettingV1() 
throws Exception {
+         final ObjectMapper mapper = 
GraphSONMapper.build().version(GraphSONVersion.V1_0)
+                 .typeInfo(TypeInfo.PARTIAL_TYPES)
+                 .addAllowedTypeIdName(SamplePojo.class.getName())
+                 .create().createMapper();
+ 
+         assertArrayEquals(new SamplePojo[]{new SamplePojo(42)},
+                 (SamplePojo[]) roundTripInMap(mapper, new SamplePojo[]{new 
SamplePojo(42)}));
+ 
+         final String subclassTypeId = SamplePojoSubclass.class.getName();
+         assertTypeIdDeniedByTypeValidator(mapper,
+                 "{\"@class\":\"java.util.HashMap\",\"p\":{\"@class\":\"" + 
subclassTypeId + "\",\"x\":42}}",
+                 subclassTypeId);
+     }
+ 
+     @Test
+     public void 
shouldRejectDisallowedCollectionElementWithoutInitializingClassV1() {
+         final String typeId = StaticInitCanaryElement.class.getName();
+         final String json = 
"{\"@class\":\"java.util.HashMap\",\"v\":[\"java.util.ArrayList\",["
+                 + "{\"@class\":\"" + typeId + "\",\"x\":1}]]}";
+ 
+         System.clearProperty(StaticInitCanaryElement.FIRED_PROPERTY);
+         assertTypeIdDeniedByTypeValidator(v1Typed(), json, typeId);
+         assertNull("type id resolution must not initialize the class named by 
a refused collection element",
+                 System.getProperty(StaticInitCanaryElement.FIRED_PROPERTY));
+     }
+ 
+     @Test
+     public void shouldRejectEnumTypeParameterAndNotLoadItV1() {
+         System.clearProperty(StaticInitCanaryEnum.FIRED_PROPERTY);
+         assertDeniedByTypeValidator(v1Typed(),
+                 
"{\"@class\":\"java.util.HashMap<com.example.gadget.GraphSONTestGadgets$StaticInitCanaryEnum,java.lang.String>\",\"A\":\"v\"}");
+         assertNull("type id resolution must not load the enum named as a type 
argument",
+                 System.getProperty(StaticInitCanaryEnum.FIRED_PROPERTY));
+     }
+ 
+     @Test
+     public void shouldRejectClassValueAndNotLoadItV1() {
+         // java.lang.Class is held out of the derived names by 
graphSON1dDerivedTypeNames()
+         System.clearProperty(StaticInitCanaryValue.FIRED_PROPERTY);
+         assertDeniedByTypeValidator(v1Typed(),
+                 
"{\"@class\":\"java.util.HashMap\",\"c\":[\"java.lang.Class\",\"com.example.gadget.GraphSONTestGadgets$StaticInitCanaryValue\"]}");
+         assertNull("type id resolution must not load the class named by a 
java.lang.Class value",
+                 System.getProperty(StaticInitCanaryValue.FIRED_PROPERTY));
+     }
+ 
+     @Test
+     public void shouldNotLoadDisallowedClassWhenRefusingV1() {
+         // a @class outside the allowed names is decided from the name alone
+         System.clearProperty(StaticInitCanary.FIRED_PROPERTY);
+         assertDeniedByTypeValidator(v1Typed(),
+                 
"{\"@class\":\"java.util.HashMap\",\"g\":{\"@class\":\"com.example.gadget.GraphSONTestGadgets$StaticInitCanary\",\"x\":1}}");
+         assertNull("type id resolution must not load the class named by a 
refused @class",
+                 System.getProperty(StaticInitCanary.FIRED_PROPERTY));
+     }
+ 
+     @Test
+     public void shouldRoundTripInetAddressWithEmbedTypeSettingV1() throws 
Exception {
+         // java.net.InetAddress is a derived name, since GraphSON 2.0/3.0 
register it
+         final ObjectMapper mapper = v1Typed();
+         final Map<String, Object> m = new HashMap<>();
+         m.put("a", java.net.InetAddress.getByAddress(new byte[]{127, 0, 0, 
1}));
+ 
+         final Map read = mapper.readValue(mapper.writeValueAsString(m), 
HashMap.class);
+         assertEquals(java.net.InetAddress.getByAddress(new byte[]{127, 0, 0, 
1}), read.get("a"));
+     }
+ 
+     @Test
+     public void shouldRoundTripUriWithEmbedTypeSettingV1() throws Exception {
+         // java.net.URI is listed rather than derived, as GraphSON 2.0/3.0 do 
not register it
+         final ObjectMapper mapper = v1Typed();
+         final Map<String, Object> m = new HashMap<>();
+         m.put("u", new java.net.URI("http://example.com/x";));
+ 
+         final Map read = mapper.readValue(mapper.writeValueAsString(m), 
HashMap.class);
+         assertEquals(new java.net.URI("http://example.com/x";), read.get("u"));
+     }
+ 
+     @Test
+     public void shouldRoundTripBoxedPrimitivesWithEmbedTypeSettingV1() throws 
Exception {
+         // String, Integer, Double and Boolean are written bare, so only the 
boxed types JSON cannot represent
+         // natively carry a type id
+         assertRoundTripsInMap(v1Typed(), Arrays.asList(
+                 Character.valueOf('c'),
+                 BigDecimal.ONE));
+     }
+ 
+     @Test
+     public void shouldRoundTripCollectionTypesWithEmbedTypeSettingV1() throws 
Exception {
+         // the concrete collection class names GraphSON 1.0 writes for a 
Map-nested value. Each case checks the type
+         // id in the text written as well as the class read back, since 
AbstractMap.equals and AbstractList.equals
+         // are structural and would keep passing with no type id written at 
all.
+         final ObjectMapper mapper = v1Typed();
+         final Map<String, Object> entry = Collections.singletonMap("a", "b");
+         final List<String> one = Collections.singletonList("a");
+ 
+         assertTypedRoundTripInMap(mapper, new LinkedHashMap<>(entry));
+         assertTypedRoundTripInMap(mapper, new LinkedHashSet<>(one));
+ 
+         // Jackson cannot rebuild either class named, so it reads the type id 
back through a stand-in it can
+         // construct: Arrays.asList comes back as a plain ArrayList, and 
either unmodifiable list name comes back as a
+         // wrapper around an ArrayList. unmodifiableList writes 
Collections$UnmodifiableList over a LinkedList but
+         // Collections$UnmodifiableRandomAccessList over an ArrayList, so the 
allowed names carry both.
+         assertTypedRoundTripInMap(mapper, Arrays.asList("a", "b"), 
ArrayList.class);
+         assertTypedRoundTripInMap(mapper, Collections.unmodifiableList(new 
LinkedList<>(one)),
+                 Collections.unmodifiableList(new 
ArrayList<>(one)).getClass());
+ 
+         // a Map in a List in a Map, so a type id is resolved at every depth
+         final Map<String, Object> nested = new HashMap<>();
+         nested.put("inner", new ArrayList<>(Collections.singletonList(new 
HashMap<String, Object>(entry))));
+         final String nestedJson = writeInMap(mapper, nested);
+         assertThat(nestedJson, 
containsString("\"inner\":[\"java.util.ArrayList\",["));
+ 
+         final Object readNested = readMapValue(mapper, nestedJson);
+         assertEquals(nested, readNested);
+         assertEquals(HashMap.class, readNested.getClass());
+         final Object readInner = ((Map<?, ?>) readNested).get("inner");
+         assertEquals(ArrayList.class, readInner.getClass());
+         assertEquals(HashMap.class, ((List<?>) readInner).get(0).getClass());
+     }
+ 
+     @Test
+     public void shouldRoundTripEnumTypesWithEmbedTypeSettingV1() throws 
Exception {
+         // T uses per-constant subclasses, whose type id is still the 
declaring enum. DayOfWeek is listed in
+         // GRAPHSON_1_0_ALLOWED_EXTRA_TYPE_NAMES rather than derived from the 
GraphSON 2.0/3.0 registry.
+         assertRoundTripsInMap(v1Typed(), Arrays.asList(
+                 Direction.OUT,
+                 T.id,
+                 DayOfWeek.MONDAY));
+     }
+ 
+     @Test
+     public void shouldReadAllowedBaseTypeIdsWithEmbedTypeSettingV1() throws 
Exception {
+         // allowed names GraphSON 1.0 does not write itself, since it writes 
the concrete runtime class instead. A
+         // document may still name them, and what comes back is the concrete 
type Jackson picks for the base type.
+         final ObjectMapper mapper = v1Typed();
+ 
+         assertEquals(Collections.singletonMap("a", "b"),
+                 readMapValue(mapper, 
"{\"@class\":\"java.util.HashMap\",\"v\":{\"@class\":\"java.util.Map\",\"a\":\"b\"}}"));
+         assertEquals(ByteBuffer.wrap(new byte[]{1, 2}),
+                 readMapValue(mapper, 
"{\"@class\":\"java.util.HashMap\",\"v\":[\"java.nio.ByteBuffer\",\"AQI=\"]}"));
+     }
+ 
+     @Test
+     public void shouldRefuseStarGraphBecauseV1WritesItsStarVertexAsAMapV1() 
throws Exception {
+         // GraphSON 1.0 writes a StarGraph as a bean whose "starVertex" 
property carries the type id
+         // java.util.HashMap rather than a StarVertex, so Jackson cannot 
rebuild a StarGraph from what GraphSON 1.0
+         // writes and the name is not among the allowed names.
+         final ObjectMapper mapper = v1Typed();
+         final String json;
+         try (final StarGraph starGraph = StarGraph.open()) {
+             starGraph.addVertex("label", "person");
+             json = writeInMap(mapper, starGraph);
+         }
+ 
+         assertThat(json, containsString("\"" + GraphSONTokens.CLASS + "\":\"" 
+ StarGraph.class.getName() + "\""));
+         assertThat(json, containsString("\"starVertex\":{\"" + 
GraphSONTokens.CLASS + "\":\"java.util.HashMap\""));
+         assertTypeIdDeniedByTypeValidator(mapper, json, 
StarGraph.class.getName());
+     }
+ 
+     @Test
+     public void shouldRefuseByteBufferBecauseV1WritesItsConcreteHeapClassV1() 
throws Exception {
+         // java.nio.ByteBuffer is a derived name and does resolve (see
+         // shouldReadAllowedBaseTypeIdsWithEmbedTypeSettingV1), but what 
GraphSON 1.0 writes for wrap() or allocate()
+         // is the concrete java.nio.HeapByteBuffer, which the allowed names 
do not carry, so V1 cannot read what it
+         // writes.
+         final ObjectMapper mapper = v1Typed();
+ 
+         final String json = writeInMap(mapper, ByteBuffer.wrap(new byte[]{1, 
2}));
+         assertThat(json, containsString("[\"java.nio.HeapByteBuffer\","));
+         assertTypeIdDeniedByTypeValidator(mapper, json, 
"java.nio.HeapByteBuffer");
+     }
+ 
+     @Test
+     public void shouldRefuseEnumMapBecauseV1WritesAParameterizedTypeIdV1() 
throws Exception {
+         // GraphSON 1.0 writes an EnumMap as the parameterized type id 
"java.util.EnumMap<...,...>", which
+         // GraphSON1dScreeningIdResolver refuses, so an EnumMap no longer 
reads back. That is a round-trip regression
+         // introduced by this change. An EnumSet is written as a 
parameterized type id too, but it did not read back
+         // beforehand either, as jackson-databind#4849 leaves the type id 
EnumSet writes unresolvable.
+         final ObjectMapper mapper = v1Typed();
+         final String typeId = "java.util.EnumMap<" + 
Direction.class.getName() + ",java.lang.Object>";
+ 
+         final EnumMap<Direction, String> value = new 
EnumMap<>(Direction.class);
+         value.put(Direction.OUT, "x");
+ 
+         final String json = writeInMap(mapper, value);
+         assertThat(json, containsString("\"" + GraphSONTokens.CLASS + "\":\"" 
+ typeId + "\""));
+         assertParameterizedTypeIdRefused(mapper, json, typeId);
+     }
+ 
+     @Test
+     public void 
shouldRejectUnresolvableHostnameWithoutLookupWithEmbedTypeSettingV1() {
+         // java.net.InetAddress is among the allowed names, so a document may 
name it, but a value that is not an IP
+         // address literal is refused on its syntax rather than looked up. 
The message is the evidence of that, as a
+         // lookup would report an UnknownHostException instead. It is 
Jackson's own wording, so an upgrade can move it.
+         final String json = "{\"@class\":\"java.util.HashMap\",\"v\":" +
+                 
"[\"java.net.InetAddress\",\"this-name-should-not-resolve.invalid\"]}";
+         try {
+             v1Typed().readValue(json, HashMap.class);
+             fail("an InetAddress value that is not an IP address literal must 
be refused");
+         } catch (Exception e) {
+             assertThat(e, instanceOf(InvalidFormatException.class));
+             assertThat(e.getMessage(), containsString("Not a valid IP address 
string literal"));
+         }
+     }
+ 
+     @Test
+     public void shouldAllowConfiguredClassValueWithEmbedTypeSettingV1() 
throws Exception {
+         // java.lang.Class is held out of the derived names, and no separate 
list holds it out permanently, so a
+         // caller that wants GraphSON 1.0 to read one names it like any other 
name
+         final String json = 
"{\"@class\":\"java.util.HashMap\",\"c\":[\"java.lang.Class\",\"java.lang.String\"]}";
+         assertDeniedByTypeValidator(v1Typed(), json);
+ 
+         final ObjectMapper mapper = 
GraphSONMapper.build().version(GraphSONVersion.V1_0)
+                 
.typeInfo(TypeInfo.PARTIAL_TYPES).addAllowedTypeIdName("java.lang.Class").create().createMapper();
+         assertEquals(String.class, mapper.readValue(json, 
HashMap.class).get("c"));
+     }
+ 
+     @Test
+     public void 
shouldAllowSeveralConfiguredTypeIdNamesWithEmbedTypeSettingV1() throws 
Exception {
+         // addAllowedTypeIdName takes several names at once, and successive 
calls add to earlier ones rather than
+         // replacing them
+         final String json = "{\"@class\":\"java.util.HashMap\","
+                 + 
"\"p\":{\"@class\":\"com.example.gadget.GraphSONTestGadgets$SamplePojo\",\"x\":42},"
+                 + "\"u\":[\"java.net.URL\",\"http://example.com/x\"]}";;
+ 
+         assertDeniedByTypeValidator(v1Typed(), json);
+ 
+         
assertConfiguredNamesResolve(GraphSONMapper.build().version(GraphSONVersion.V1_0)
+                 .typeInfo(TypeInfo.PARTIAL_TYPES)
+                 .addAllowedTypeIdName(SamplePojo.class.getName(), 
"java.net.URL")
+                 .create().createMapper(), json);
+ 
+         
assertConfiguredNamesResolve(GraphSONMapper.build().version(GraphSONVersion.V1_0)
+                 .typeInfo(TypeInfo.PARTIAL_TYPES)
+                 .addAllowedTypeIdName(SamplePojo.class.getName())
+                 .addAllowedTypeIdName("java.net.URL")
+                 .create().createMapper(), json);
+     }
+ 
+     @Test
+     public void 
shouldIgnoreDuplicateConfiguredTypeIdNamesWithEmbedTypeSettingV1() throws 
Exception {
+         final String json = "{\"@class\":\"java.util.HashMap\","
+                 + 
"\"p\":{\"@class\":\"com.example.gadget.GraphSONTestGadgets$SamplePojo\",\"x\":42}}";
+ 
+         final ObjectMapper mapper = 
GraphSONMapper.build().version(GraphSONVersion.V1_0)
+                 .typeInfo(TypeInfo.PARTIAL_TYPES)
+                 .addAllowedTypeIdName(SamplePojo.class.getName())
+                 .addAllowedTypeIdName(SamplePojo.class.getName())
+                 .create().createMapper();
+ 
+         assertEquals(new SamplePojo(42), mapper.readValue(json, 
HashMap.class).get("p"));
+     }
+ 
+     @Test
+     public void 
shouldDeriveTypeIdNamesFromTheRegisteredGraphSON2And3TypesV1() {
+         // a new put(...) in GraphSONModuleV2, GraphSONModuleV3, 
GraphSONXModuleV2 or GraphSONXModuleV3 widens what
+         // GraphSON 1.0 reads as a side effect, which pinning the derived 
names makes visible.
+         //
+         // The set is classpath dependent: 
GraphSONModule.tryLoadSparqlStrategy() contributes SparqlStrategy when
+         // sparql-gremlin is present, which it is not on the gremlin-core 
test classpath. A new name that is genuinely
+         // wanted belongs in the expected set below.
+         final Set<String> expected = new TreeSet<>(Arrays.asList(
+                 "java.lang.Byte",
+                 "java.lang.Character",
+                 "java.lang.Double",
+                 "java.lang.Float",
+                 "java.lang.Integer",
+                 "java.lang.Long",
+                 "java.lang.Short",
+                 "java.math.BigDecimal",
+                 "java.math.BigInteger",
+                 "java.net.InetAddress",
+                 "java.nio.ByteBuffer",
+                 "java.sql.Timestamp",
+                 "java.time.Duration",
+                 "java.time.Instant",
+                 "java.time.LocalDate",
+                 "java.time.LocalDateTime",
+                 "java.time.LocalTime",
+                 "java.time.MonthDay",
+                 "java.time.OffsetDateTime",
+                 "java.time.OffsetTime",
+                 "java.time.Period",
+                 "java.time.Year",
+                 "java.time.YearMonth",
+                 "java.time.ZoneOffset",
+                 "java.time.ZonedDateTime",
+                 "java.util.Calendar",
+                 "java.util.Date",
+                 "java.util.List",
+                 "java.util.Map",
+                 "java.util.Set",
+                 "java.util.TimeZone",
+                 "java.util.UUID",
+                 
"org.apache.tinkerpop.gremlin.process.computer.traversal.strategy.decoration.VertexProgramStrategy",
++                
"org.apache.tinkerpop.gremlin.process.computer.traversal.strategy.finalization.ComputerFinalizationStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.computer.traversal.strategy.optimization.GraphFilterStrategy",
++                
"org.apache.tinkerpop.gremlin.process.computer.traversal.strategy.optimization.MessagePassingReductionStrategy",
++                
"org.apache.tinkerpop.gremlin.process.computer.traversal.strategy.verification.VertexProgramRestrictionStrategy",
+                 "org.apache.tinkerpop.gremlin.process.traversal.Bytecode",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.Bytecode$Binding",
+                 "org.apache.tinkerpop.gremlin.process.traversal.DT",
++                "org.apache.tinkerpop.gremlin.process.traversal.GType",
+                 "org.apache.tinkerpop.gremlin.process.traversal.Merge",
++                "org.apache.tinkerpop.gremlin.process.traversal.NotP",
+                 "org.apache.tinkerpop.gremlin.process.traversal.Operator",
+                 "org.apache.tinkerpop.gremlin.process.traversal.Order",
+                 "org.apache.tinkerpop.gremlin.process.traversal.P",
+                 "org.apache.tinkerpop.gremlin.process.traversal.Path",
+                 "org.apache.tinkerpop.gremlin.process.traversal.Pick",
+                 "org.apache.tinkerpop.gremlin.process.traversal.Pop",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.SackFunctions$Barrier",
+                 "org.apache.tinkerpop.gremlin.process.traversal.Scope",
+                 "org.apache.tinkerpop.gremlin.process.traversal.TextP",
+                 "org.apache.tinkerpop.gremlin.process.traversal.Traverser",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.step.util.BulkSet",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree",
++                
"org.apache.tinkerpop.gremlin.process.traversal.strategy.TraversalStrategyProxy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ConnectiveStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.ElementIdStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.EventStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.HaltedTraverserStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.OptionsStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.PartitionStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SeedStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.finalization.MatchAlgorithmStrategy",
++                
"org.apache.tinkerpop.gremlin.process.traversal.strategy.finalization.ProfileStrategy",
++                
"org.apache.tinkerpop.gremlin.process.traversal.strategy.finalization.ReferenceElementStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.AdjacentToIncidentStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.ByModulatorOptimizationStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.CountStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.EarlyLimitStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.FilterRankingStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.IdentityRemovalStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.IncidentToAdjacentStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.InlineFilterStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.LazyBarrierStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.MatchPredicateStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.OrderLimitStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.PathProcessorStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.PathRetractionStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.ProductiveByStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.RepeatUnrollStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ComputerVerificationStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.EdgeLabelVerificationStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.LambdaRestrictionStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ReadOnlyStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ReservedKeysVerificationStrategy",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.StandardVerificationStrategy",
+                 "org.apache.tinkerpop.gremlin.process.traversal.util.AndP",
+                 "org.apache.tinkerpop.gremlin.process.traversal.util.Metrics",
+                 "org.apache.tinkerpop.gremlin.process.traversal.util.OrP",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.util.TraversalExplanation",
+                 
"org.apache.tinkerpop.gremlin.process.traversal.util.TraversalMetrics",
+                 "org.apache.tinkerpop.gremlin.structure.Column",
+                 "org.apache.tinkerpop.gremlin.structure.Direction",
+                 "org.apache.tinkerpop.gremlin.structure.Edge",
+                 "org.apache.tinkerpop.gremlin.structure.Property",
+                 "org.apache.tinkerpop.gremlin.structure.T",
+                 "org.apache.tinkerpop.gremlin.structure.Vertex",
+                 "org.apache.tinkerpop.gremlin.structure.VertexProperty",
+                 
"org.apache.tinkerpop.gremlin.structure.VertexProperty$Cardinality",
+                 "org.apache.tinkerpop.gremlin.util.function.Lambda"));
+ 
+         final Set<String> derived = new 
TreeSet<>(GraphSONMapper.graphSON1dDerivedTypeNames());
+         final Set<String> added = new TreeSet<>(derived);
+         added.removeAll(expected);
+         final Set<String> dropped = new TreeSet<>(expected);
+         dropped.removeAll(derived);
+         assertEquals("derived but not expected: " + added + "; expected but 
not derived: " + dropped,
+                 expected, derived);
+     }
+ 
+     private static ObjectMapper v1Typed() {
+         return 
GraphSONMapper.build().version(GraphSONVersion.V1_0).typeInfo(TypeInfo.PARTIAL_TYPES).create().createMapper();
+     }
+ 
+     /**
+      * Writes a value as a {@code Map} value and reads it back. That is the 
shape real payloads use, and it exercises
+      * the untyped {@code Object} value path rather than a declared concrete 
class.
+      */
+     private static Object roundTripInMap(final ObjectMapper mapper, final 
Object value) throws Exception {
+         return readMapValue(mapper, writeInMap(mapper, value));
+     }
+ 
+     /**
+      * Writes a value as a {@code Map} value and returns the text, so the 
type id GraphSON 1.0 emitted for it can be
+      * asserted on directly.
+      */
+     private static String writeInMap(final ObjectMapper mapper, final Object 
value) throws Exception {
+         final Map<String, Object> m = new HashMap<>();
+         m.put(MAP_VALUE_KEY, value);
+         return mapper.writeValueAsString(m);
+     }
+ 
+     private static void assertRoundTripsInMap(final ObjectMapper mapper, 
final List<Object> values) throws Exception {
+         for (final Object value : values) {
+             assertEquals(value.getClass().getName(), value, 
roundTripInMap(mapper, value));
+         }
+     }
+ 
+     private static void assertTypedRoundTripInMap(final ObjectMapper mapper, 
final Object value) throws Exception {
+         assertTypedRoundTripInMap(mapper, value, value.getClass());
+     }
+ 
+     /**
+      * Asserts that a {@code Map} or {@code Collection} value reads back 
equal and as {@code expectedClass}, and that
+      * the type id GraphSON 1.0 wrote for it is in the text written. Equality 
alone is no evidence of typing, as an
+      * equal value of any other concrete {@code Map} or {@code List} class 
satisfies it. {@code expectedClass} is what
+      * Jackson rebuilds for the type id written, which is not always the 
class that was written.
+      */
+     private static void assertTypedRoundTripInMap(final ObjectMapper mapper, 
final Object value,
+                                                   final Class<?> 
expectedClass) throws Exception {
+         final String name = value.getClass().getName();
+         final String json = writeInMap(mapper, value);
+         assertThat("no type id written for " + name + " in " + json, json, 
containsString(typeIdInMapOf(value)));
+ 
+         final Object read = readMapValue(mapper, json);
+         assertEquals(name, value, read);
+         assertEquals("concrete class read back for " + name, expectedClass, 
read.getClass());
+     }
+ 
+     /**
+      * The text GraphSON 1.0 writes for the type id of a {@code Map} or 
{@code Collection} held as a {@code Map}
+      * value: a {@code Map} carries it as an {@code "@class"} field of its 
own object, a {@code Collection} as the
+      * first element of a wrapper array. Both are anchored to the key the 
value sits under, so neither can be
+      * satisfied by the type id of the enclosing {@code Map}.
+      */
+     private static String typeIdInMapOf(final Object value) {
+         final String prefix = "\"" + MAP_VALUE_KEY + "\":";
+         final String name = value.getClass().getName();
+         return value instanceof Map
+                 ? prefix + "{\"" + GraphSONTokens.CLASS + "\":\"" + name + 
"\""
+                 : prefix + "[\"" + name + "\",";
+     }
+ 
+     private static Object readMapValue(final ObjectMapper mapper, final 
String json) throws Exception {
+         return mapper.readValue(json, HashMap.class).get(MAP_VALUE_KEY);
+     }
+ 
+     /**
+      * Asserts that both configured names resolved. A {@code java.net.URL} is 
compared as text, since
+      * {@code URL.equals} can consult the network.
+      */
+     private static void assertConfiguredNamesResolve(final ObjectMapper 
mapper, final String json) throws Exception {
+         final Map read = mapper.readValue(json, HashMap.class);
+         assertEquals(new SamplePojo(42), read.get("p"));
+         assertEquals("http://example.com/x";, read.get("u").toString());
+     }
+ 
+     private static void assertDeniedByTypeValidator(final ObjectMapper 
mapper, final String json) {
+         try {
+             mapper.readValue(json, HashMap.class);
+             fail("a @class outside the allowed names must not resolve");
+         } catch (InvalidTypeIdException expected) {
+         } catch (Exception other) {
+             throw new AssertionError("expected InvalidTypeIdException, got " 
+ other, other);
+         }
+     }
+ 
+     /**
+      * Asserts that {@code typeId} in particular is what the read was refused 
on, and that the allowed names are what
+      * refused it. A listed type id that names no loadable class is also an 
{@code InvalidTypeIdException}, reported as
+      * "no such class found" rather than as a denial. Both strings matched 
are Jackson's own wording, so a Jackson
+      * upgrade can move them.
+      */
+     private static void assertTypeIdDeniedByTypeValidator(final ObjectMapper 
mapper, final String json,
+                                                           final String 
typeId) {
+         try {
+             mapper.readValue(json, HashMap.class);
+             fail("resolution of the type id " + typeId + " must be refused");
+         } catch (InvalidTypeIdException expected) {
+             assertThat(expected.getMessage(), containsString("Could not 
resolve type id '" + typeId + "'"));
+             assertThat(expected.getMessage(), containsString("denied 
resolution"));
+         } catch (Exception other) {
+             throw new AssertionError("expected InvalidTypeIdException, got " 
+ other, other);
+         }
+     }
+ 
+     /**
+      * Asserts that {@code typeId} was refused for being parameterized, which 
{@code GraphSON1dScreeningIdResolver}
+      * does before the allowed names are consulted at all.
+      */
+     private static void assertParameterizedTypeIdRefused(final ObjectMapper 
mapper, final String json,
+                                                          final String typeId) 
{
+         try {
+             mapper.readValue(json, HashMap.class);
+             fail("a parameterized type id must be refused: " + typeId);
+         } catch (InvalidTypeIdException expected) {
+             assertThat(expected.getMessage(), containsString("Could not 
resolve type id '" + typeId + "'"));
+             assertThat(expected.getMessage(), containsString("GraphSON 1.0 
does not permit a parameterized type id"));
+         } catch (Exception other) {
+             throw new AssertionError("expected InvalidTypeIdException, got " 
+ other, other);
+         }
+     }
+ 
      @Test
      public void shouldNotHandleMapWithTypesUsingEmbedTypeSettingV1() throws 
Exception {
          final ObjectMapper mapper = GraphSONMapper.build()

Reply via email to