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()
