You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/10/06 14:27:51 UTC
(hugegraph) 03/14: fix(server): adapt GraphSON and Cypher
This is an automated email from the ASF dual-hosted git repository.
imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph.git
commit 421fbc6e547a5d286eb851bb2768a4d138f030e6
Author: contrueCT <co...@outlook.com>
AuthorDate: Tue Oct 6 01:17:45 2026 +0800
fix(server): adapt GraphSON and Cypher
- Adapt Cypher client and processor integration to the upgraded Driver APIs
- Update GraphSON modules and type serializer registration
- Retain Cypher and JSON serialization compatibility checks
---
.../apache/hugegraph/api/cypher/CypherClient.java | 102 +++++++++++++-
.../hugegraph/opencypher/CypherOpProcessor.java | 12 +-
.../apache/hugegraph/io/HugeGraphSONModule.java | 73 +++++++++-
.../io/HugeGraphTypeSerializerRegistryBuilder.java | 72 ++++++++++
.../java/org/apache/hugegraph/util/JsonUtil.java | 1 +
.../org/apache/hugegraph/api/CypherApiTest.java | 149 ++++++++++++++++++++-
.../hugegraph/api/cypher/CypherClientTest.java | 129 ++++++++++++++++++
.../unit/serializer/HugeGraphSONModuleTest.java | 148 ++++++++++++++++++++
.../apache/hugegraph/unit/util/JsonUtilTest.java | 29 ++++
9 files changed, 696 insertions(+), 19 deletions(-)
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java
index 92ae18c54..85d98a25c 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java
@@ -17,7 +17,10 @@
package org.apache.hugegraph.api.cypher;
+import java.lang.reflect.Array;
+import java.util.IdentityHashMap;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -29,20 +32,23 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import org.apache.commons.configuration2.Configuration;
+import org.apache.hugegraph.backend.id.Id;
import org.apache.hugegraph.util.E;
import org.apache.hugegraph.util.Log;
import org.apache.tinkerpop.gremlin.driver.Client;
import org.apache.tinkerpop.gremlin.driver.Cluster;
import org.apache.tinkerpop.gremlin.driver.Result;
import org.apache.tinkerpop.gremlin.driver.ResultSet;
-import org.apache.tinkerpop.gremlin.driver.Tokens;
-import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
+import org.apache.tinkerpop.gremlin.util.Tokens;
+import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
import org.slf4j.Logger;
@ThreadSafe
public final class CypherClient {
private static final Logger LOG = Log.logger(CypherClient.class);
+ private static final int NORMALIZE_MAX_DEPTH = 32;
private final Supplier<Configuration> configurationSupplier;
private String userName;
private String password;
@@ -105,12 +111,102 @@ public final class CypherClient {
while (iter.hasNext()) {
Result data = iter.next();
- list.add(data.getObject());
+ list.add(normalize(data.getObject()));
}
return list;
}
+ static Object normalize(Object value) {
+ return normalize(value, 0, new IdentityHashMap<>());
+ }
+
+ private static Object normalize(Object value, int depth,
+ IdentityHashMap<Object, Boolean> seen) {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof Id) {
+ return ((Id) value).asObject();
+ }
+ boolean composite = value instanceof Map || value instanceof Path ||
+ value instanceof Iterable ||
+ value.getClass().isArray();
+ if (!composite) {
+ return value;
+ }
+ if (depth >= NORMALIZE_MAX_DEPTH) {
+ throw new IllegalArgumentException(
+ "Exceeded max normalization depth 32");
+ }
+ if (value instanceof Map) {
+ if (seen.put(value, Boolean.TRUE) != null) {
+ throw new IllegalArgumentException(
+ "Detected cyclic Cypher result");
+ }
+ Map<Object, Object> normalized = new LinkedHashMap<>();
+ try {
+ for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
+ normalized.put(normalize(entry.getKey(), depth + 1, seen),
+ normalize(entry.getValue(), depth + 1, seen));
+ }
+ } finally {
+ seen.remove(value);
+ }
+ return normalized;
+ }
+ if (value instanceof Path) {
+ if (seen.put(value, Boolean.TRUE) != null) {
+ throw new IllegalArgumentException(
+ "Detected cyclic Cypher result");
+ }
+ Map<String, Object> normalized = new LinkedHashMap<>();
+ try {
+ Path path = (Path) value;
+ normalized.put("labels",
+ normalize(path.labels(), depth + 1, seen));
+ normalized.put("objects",
+ normalize(path.objects(), depth + 1, seen));
+ } finally {
+ seen.remove(value);
+ }
+ return normalized;
+ }
+ if (value instanceof Iterable) {
+ if (seen.put(value, Boolean.TRUE) != null) {
+ throw new IllegalArgumentException(
+ "Detected cyclic Cypher result");
+ }
+ List<Object> normalized = new LinkedList<>();
+ try {
+ for (Object item : (Iterable<?>) value) {
+ normalized.add(normalize(item, depth + 1, seen));
+ }
+ } finally {
+ seen.remove(value);
+ }
+ return normalized;
+ }
+ if (value.getClass().isArray()) {
+ if (seen.put(value, Boolean.TRUE) != null) {
+ throw new IllegalArgumentException(
+ "Detected cyclic Cypher result");
+ }
+ List<Object> normalized = new LinkedList<>();
+ try {
+ int length = Array.getLength(value);
+ for (int i = 0; i < length; i++) {
+ normalized.add(normalize(Array.get(value, i), depth + 1,
+ seen));
+ }
+ } finally {
+ seen.remove(value);
+ }
+ return normalized;
+ }
+ return value;
+ }
+
/**
* As Sasl does not support a token, which is a coded string to indicate a legal user,
* we had to use a trick to fix it. When the token is set, the password will be set to
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java
index dfad9b959..e3f0a6036 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java
@@ -19,7 +19,7 @@ package org.apache.hugegraph.opencypher;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static java.util.Optional.empty;
-import static org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode.SERVER_ERROR;
+import static org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode.SERVER_ERROR;
import static org.opencypher.gremlin.translation.StatementOption.EXPLAIN;
import static org.slf4j.LoggerFactory.getLogger;
@@ -33,10 +33,6 @@ import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
-import org.apache.tinkerpop.gremlin.driver.Tokens;
-import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
-import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage;
-import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal;
@@ -49,7 +45,11 @@ import org.apache.tinkerpop.gremlin.server.OpProcessor;
import org.apache.tinkerpop.gremlin.server.op.AbstractEvalOpProcessor;
import org.apache.tinkerpop.gremlin.server.op.OpProcessorException;
import org.apache.tinkerpop.gremlin.structure.Graph;
+import org.apache.tinkerpop.gremlin.util.Tokens;
import org.apache.tinkerpop.gremlin.util.function.ThrowingConsumer;
+import org.apache.tinkerpop.gremlin.util.message.RequestMessage;
+import org.apache.tinkerpop.gremlin.util.message.ResponseMessage;
+import org.apache.tinkerpop.gremlin.util.message.ResponseStatusCode;
import org.opencypher.gremlin.translation.CypherAst;
import org.opencypher.gremlin.translation.groovy.GroovyPredicate;
import org.opencypher.gremlin.translation.ir.TranslationWriter;
@@ -66,7 +66,7 @@ import scala.collection.Seq;
/**
* Description of the modifications:
* <p>
- * 1) Changed the method signature to adopt the gremlin-server 3.5.1.
+ * 1) Changed the method signature to adopt the gremlin-server Context API.
* <pre>
* public Optional<ThrowingConsumer<Context>> selectOther(RequestMessage requestMessage)
* -->
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java
index ddb7c1a98..f58404646 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphSONModule.java
@@ -57,7 +57,6 @@ import org.apache.hugegraph.util.Log;
import org.apache.hugegraph.util.SafeDateUtil;
import org.apache.tinkerpop.gremlin.process.traversal.Path;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
-import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONIo;
import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONTokens;
import org.apache.tinkerpop.gremlin.structure.io.graphson.TinkerPopJacksonModule;
@@ -103,12 +102,14 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
TYPE_DEFINITIONS = new ConcurrentHashMap<>();
TYPE_DEFINITIONS.put(Optional.class, "Optional");
+ TYPE_DEFINITIONS.put(File.class, "File");
TYPE_DEFINITIONS.put(Date.class, "Date");
TYPE_DEFINITIONS.put(UUID.class, "UUID");
// HugeGraph id serializer
TYPE_DEFINITIONS.put(StringId.class, "StringId");
TYPE_DEFINITIONS.put(LongId.class, "LongId");
+ TYPE_DEFINITIONS.put(UuidId.class, "UuidId");
TYPE_DEFINITIONS.put(EdgeId.class, "EdgeId");
// HugeGraph schema serializer
@@ -171,6 +172,7 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
module.addSerializer(Shard.class, new ShardSerializer());
module.addSerializer(File.class, new FileSerializer());
+ module.addDeserializer(File.class, new FileDeserializer());
boolean useTimestamp = false;
module.addSerializer(Date.class,
@@ -222,7 +224,9 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
*/
module.addSerializer(HugeVertex.class, new HugeVertexSerializer());
module.addSerializer(HugeEdge.class, new HugeEdgeSerializer());
+ }
+ public static void registerTraversalSerializers(SimpleModule module) {
module.addSerializer(Path.class, new PathSerializer());
module.addSerializer(Tree.class, new TreeSerializer());
}
@@ -641,8 +645,8 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
String idValue = ctxt.readValue(jsonParser, String.class);
return (T) IdGenerator.of(idValue);
} else if (clazz.equals(UuidId.class)) {
- UUID idValue = ctxt.readValue(jsonParser, UUID.class);
- return (T) IdGenerator.of(idValue);
+ String idValue = ctxt.readValue(jsonParser, String.class);
+ return (T) IdGenerator.of(UUID.fromString(idValue));
} else {
assert clazz.equals(EdgeId.class);
String idValue = ctxt.readValue(jsonParser, String.class);
@@ -883,9 +887,8 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
public void serialize(Tree tree, JsonGenerator jsonGenerator,
SerializerProvider provider) throws IOException {
jsonGenerator.writeStartArray();
- @SuppressWarnings("unchecked")
- Set<Map.Entry<Element, Tree>> set = tree.entrySet();
- for (Map.Entry<Element, Tree> entry : set) {
+ for (Object item : tree.entrySet()) {
+ Map.Entry<?, ?> entry = (Map.Entry<?, ?>) item;
jsonGenerator.writeStartObject();
jsonGenerator.writeObjectField(GraphSONTokens.KEY,
entry.getKey());
@@ -924,9 +927,65 @@ public class HugeGraphSONModule extends TinkerPopJacksonModule {
public void serialize(File file, JsonGenerator jsonGenerator,
SerializerProvider provider) throws IOException {
jsonGenerator.writeStartObject();
- jsonGenerator.writeStringField("file", file.getName());
+ this.writeFields(file, jsonGenerator);
jsonGenerator.writeEndObject();
}
+
+ @Override
+ public void serializeWithType(File file,
+ JsonGenerator jsonGenerator,
+ SerializerProvider provider,
+ TypeSerializer typeSer)
+ throws IOException {
+ WritableTypeId typeId = typeSer.typeId(
+ file, JsonToken.VALUE_EMBEDDED_OBJECT);
+ typeSer.writeTypePrefix(jsonGenerator, typeId);
+ this.serialize(file, jsonGenerator, provider);
+ typeSer.writeTypeSuffix(jsonGenerator, typeId);
+ }
+
+ private void writeFields(File file, JsonGenerator jsonGenerator)
+ throws IOException {
+ jsonGenerator.writeStringField("file", file.getName());
+ }
+ }
+
+ private static class FileDeserializer extends StdDeserializer<File> {
+
+ public FileDeserializer() {
+ super(File.class);
+ }
+
+ @Override
+ public File deserialize(JsonParser jsonParser,
+ DeserializationContext ctxt)
+ throws IOException {
+ JsonToken token = jsonParser.currentToken();
+ if (token == null) {
+ token = jsonParser.nextToken();
+ }
+ if (token == JsonToken.VALUE_STRING) {
+ return new File(jsonParser.getValueAsString());
+ }
+ if (token == JsonToken.START_OBJECT) {
+ String file = null;
+ while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
+ String field = jsonParser.currentName();
+ jsonParser.nextToken();
+ if ("file".equals(field)) {
+ file = jsonParser.getValueAsString();
+ } else {
+ jsonParser.skipChildren();
+ }
+ }
+ if (file == null) {
+ return (File) ctxt.handleUnexpectedToken(File.class,
+ jsonParser);
+ }
+ return new File(file);
+ }
+ return (File) ctxt.handleUnexpectedToken(File.class, jsonParser);
+ }
}
private static class BlobSerializer extends StdSerializer<Blob> {
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.java
new file mode 100644
index 000000000..f46e4ee39
--- /dev/null
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/io/HugeGraphTypeSerializerRegistryBuilder.java
@@ -0,0 +1,72 @@
+/*
+ * 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.hugegraph.io;
+
+import java.io.IOException;
+
+import org.apache.hugegraph.backend.id.Id;
+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.structure.io.binary.TypeSerializer;
+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.io.binary.types.TransformSerializer;
+
+public final class HugeGraphTypeSerializerRegistryBuilder
+ extends TypeSerializerRegistry.Builder {
+
+ private static final TypeSerializer<Id> ID_TRANSFORM_SERIALIZER =
+ new IdTransformSerializer();
+
+ public HugeGraphTypeSerializerRegistryBuilder() {
+ this.withFallbackResolver(type -> {
+ if (Id.class.isAssignableFrom(type)) {
+ return ID_TRANSFORM_SERIALIZER;
+ }
+ return null;
+ });
+ }
+
+ private static final class IdTransformSerializer
+ extends SimpleTypeSerializer<Id>
+ implements TransformSerializer<Id> {
+
+ private IdTransformSerializer() {
+ super(null);
+ }
+
+ @Override
+ protected Id readValue(Buffer buffer, GraphBinaryReader context)
+ throws IOException {
+ throw new IOException("HugeGraph Id is written as a wire primitive");
+ }
+
+ @Override
+ protected void writeValue(Id value, Buffer buffer,
+ GraphBinaryWriter context)
+ throws IOException {
+ throw new IOException("HugeGraph Id is written as a wire primitive");
+ }
+
+ @Override
+ public Object transform(Id value) {
+ return value.asObject();
+ }
+ }
+}
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java
index fd00816f6..b2fce8da5 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/util/JsonUtil.java
@@ -52,6 +52,7 @@ public final class JsonUtil {
HugeGraphSONModule.registerServiceSerializers(module);
HugeGraphSONModule.registerGraphSpaceSerializers(module);
HugeGraphSONModule.registerGraphSerializers(module);
+ HugeGraphSONModule.registerTraversalSerializers(module);
MAPPER.registerModule(module);
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java
index 3c3e3049f..3c111bae3 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java
@@ -19,8 +19,11 @@ package org.apache.hugegraph.api;
import static org.apache.hugegraph.testutil.Assert.assertContains;
+import java.util.List;
import java.util.Map;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.util.JsonUtil;
import org.junit.Before;
import org.junit.Test;
@@ -72,13 +75,153 @@ public class CypherApiTest extends BaseApiTest {
this.testCypherQueryAndContains(cypher, "friend");
}
- private void testCypherQueryAndContains(String cypher, String containsText) {
+ @Test
+ public void testReturnNodeIdAsPrimitiveValue() {
+ String cypher = "MATCH (n:person) WHERE n.name = 'marko' " +
+ "RETURN id(n) AS nodeId";
+
+ String content = this.testCypherQueryAndContains(cypher, "nodeId");
+ List<?> data = assertCypherSuccessData(content);
+ Map<?, ?> row = assertSingleMapRow(data);
+ Object nodeId = row.get("nodeId");
+
+ Assert.assertNotNull(nodeId);
+ assertPrimitiveValue(nodeId);
+ assertNoHugeGraphIdLeak(content);
+ }
+
+ @Test
+ public void testReturnNodeDoesNotLeakInternalIdTypes() {
+ String cypher = "MATCH (n:person) WHERE n.name = 'marko' RETURN n";
+
+ String content = this.testCypherQueryAndContains(cypher, "marko");
+ assertNoHugeGraphIdLeak(content);
+ }
+
+ @Test
+ public void testReturnNestedIdDoesNotLeakInternalIdTypes() {
+ String cypher = "MATCH (n:person) WHERE n.name = 'marko' " +
+ "RETURN {nodeId: id(n), values: [id(n), n.name]} " +
+ "AS payload";
+
+ String content = this.testCypherQueryAndContains(cypher, "payload");
+ List<?> data = assertCypherSuccessData(content);
+ Map<?, ?> row = assertSingleMapRow(data);
+ Map<?, ?> payload = assertMapValue(row, "payload");
+ List<?> values = assertListValue(payload, "values");
+ Object nodeId = payload.get("nodeId");
+
+ Assert.assertNotNull(nodeId);
+ assertPrimitiveValue(nodeId);
+ Assert.assertEquals(2, values.size());
+ Assert.assertEquals(nodeId, values.get(0));
+ Assert.assertEquals("marko", values.get(1));
+ assertNoHugeGraphIdLeak(content);
+ }
+
+ @Test
+ public void testReturnRelationIdDoesNotLeakInternalIdTypes() {
+ String cypher = "MATCH (n:person)-[r:knows]->(friend:person) " +
+ "WHERE n.name = 'marko' RETURN id(r) AS relationId";
+
+ String content = this.testCypherQueryAndContains(cypher, "relationId");
+ List<?> data = assertCypherSuccessData(content);
+ Map<?, ?> row = assertSingleMapRow(data);
+ Object relationId = row.get("relationId");
+
+ Assert.assertNotNull(relationId);
+ assertPrimitiveValue(relationId);
+ assertNoHugeGraphIdLeak(content);
+ }
+
+ @Test
+ public void testReturnPathShape() {
+ String cypher = "MATCH p=(n:person)-[r:knows]->(friend:person) " +
+ "WHERE n.name = 'marko' RETURN p AS path";
+
+ String content = this.testCypherQueryAndContains(cypher, "path");
+ List<?> data = assertCypherSuccessData(content);
+ Map<?, ?> row = assertSingleMapRow(data);
+ List<?> path = assertListValue(row, "path");
+
+ Assert.assertEquals(3, path.size());
+ Map<?, ?> source = assertMapValue(path, 0);
+ Map<?, ?> relation = assertMapValue(path, 1);
+ Map<?, ?> target = assertMapValue(path, 2);
+
+ Assert.assertEquals("node", source.get("_type"));
+ Assert.assertEquals("person", source.get("_label"));
+ Assert.assertEquals("marko", source.get("name"));
+ Assert.assertEquals("knows", relation.get("_label"));
+ Assert.assertEquals("node", target.get("_type"));
+ Assert.assertEquals("person", target.get("_label"));
+ Assert.assertEquals("peter", target.get("name"));
+ assertContains("marko", content);
+ assertContains("peter", content);
+ assertNoHugeGraphIdLeak(content);
+ }
+
+ private String testCypherQueryAndContains(String cypher,
+ String containsText) {
Response r = client().post(PATH, cypher);
- this.validStatusAndTextContains(containsText, r);
+ return this.validStatusAndTextContains(containsText, r);
}
- private void validStatusAndTextContains(String value, Response r) {
+ private String validStatusAndTextContains(String value, Response r) {
String content = assertResponseStatus(200, r);
assertContains(value, content);
+ return content;
+ }
+
+ private static void assertNoHugeGraphIdLeak(String content) {
+ Assert.assertFalse(content.contains("org.apache.hugegraph.backend.id"));
+ Assert.assertFalse(content.contains("StringId"));
+ Assert.assertFalse(content.contains("LongId"));
+ Assert.assertFalse(content.contains("UuidId"));
+ Assert.assertFalse(content.contains("EdgeId"));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<?> assertCypherSuccessData(String content) {
+ Map<?, ?> response = JsonUtil.fromJson(content, Map.class);
+ Assert.assertTrue(response.containsKey("requestId"));
+
+ Map<?, ?> status = assertMapValue(response, "status");
+ Assert.assertEquals(200, ((Number) status.get("code")).intValue());
+ Assert.assertEquals("", status.get("message"));
+
+ Map<?, ?> result = assertMapValue(response, "result");
+ Assert.assertInstanceOf(List.class, result.get("data"));
+ Assert.assertInstanceOf(Map.class, result.get("meta"));
+ return (List<?>) result.get("data");
+ }
+
+ private static Map<?, ?> assertSingleMapRow(List<?> data) {
+ Assert.assertEquals(1, data.size());
+ Assert.assertInstanceOf(Map.class, data.get(0));
+ return (Map<?, ?>) data.get(0);
+ }
+
+ private static Map<?, ?> assertMapValue(Map<?, ?> map, String key) {
+ Assert.assertTrue(map.containsKey(key));
+ Assert.assertInstanceOf(Map.class, map.get(key));
+ return (Map<?, ?>) map.get(key);
+ }
+
+ private static Map<?, ?> assertMapValue(List<?> list, int index) {
+ Assert.assertTrue(list.size() > index);
+ Assert.assertInstanceOf(Map.class, list.get(index));
+ return (Map<?, ?>) list.get(index);
+ }
+
+ private static List<?> assertListValue(Map<?, ?> map, String key) {
+ Assert.assertTrue(map.containsKey(key));
+ Assert.assertInstanceOf(List.class, map.get(key));
+ return (List<?>) map.get(key);
+ }
+
+ private static void assertPrimitiveValue(Object value) {
+ Assert.assertFalse(value instanceof Map);
+ Assert.assertFalse(value instanceof List);
}
}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java
new file mode 100644
index 000000000..d9d36638d
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/cypher/CypherClientTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.hugegraph.api.cypher;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.MutablePath;
+import org.junit.Test;
+
+public class CypherClientTest extends BaseUnitTest {
+
+ @Test
+ public void testNormalizeHandlesNullMapAndArrayValues() {
+ Map<Object, Object> value = new LinkedHashMap<>();
+ value.put(IdGenerator.of(1L),
+ new Object[]{IdGenerator.of("marko"), null});
+
+ Object normalized = CypherClient.normalize(value);
+
+ Assert.assertInstanceOf(Map.class, normalized);
+ Map<?, ?> map = (Map<?, ?>) normalized;
+ Assert.assertTrue(map.containsKey(1L));
+ Assert.assertInstanceOf(List.class, map.get(1L));
+
+ List<?> values = (List<?>) map.get(1L);
+ Assert.assertEquals("marko", values.get(0));
+ Assert.assertNull(values.get(1));
+ }
+
+ @Test
+ public void testNormalizeHandlesCyclicReferences() {
+ Map<String, Object> value = new LinkedHashMap<>();
+ value.put("private-value", value);
+
+ Assert.assertThrows(IllegalArgumentException.class,
+ () -> CypherClient.normalize(value), e -> {
+ Assert.assertContains("cyclic Cypher result", e.getMessage());
+ Assert.assertFalse(e.getMessage().contains("private-value"));
+ });
+ }
+
+ @Test
+ public void testNormalizePreservesThirtyTwoContainerLayers() {
+ Object value = "leaf";
+ for (int i = 0; i < 32; i++) {
+ value = new Object[]{value};
+ }
+
+ Object normalized = CypherClient.normalize(value);
+ Object current = normalized;
+ for (int i = 0; i < 32; i++) {
+ Assert.assertInstanceOf(List.class, current);
+ List<?> list = (List<?>) current;
+ Assert.assertEquals(1, list.size());
+ current = list.get(0);
+ }
+
+ Assert.assertEquals("leaf", current);
+ }
+
+ @Test
+ public void testNormalizeRejectsThirtyThirdContainerLayer() {
+ Object value = "leaf";
+ for (int i = 0; i < 33; i++) {
+ value = new Object[]{value};
+ }
+ Object deeplyNestedValue = value;
+
+ Assert.assertThrows(IllegalArgumentException.class,
+ () -> CypherClient.normalize(deeplyNestedValue),
+ e -> Assert.assertContains(
+ "max normalization depth 32",
+ e.getMessage()));
+ }
+
+ @Test
+ public void testNormalizePreservesPathLabelsAndObjects() {
+ Path path = MutablePath.make()
+ .extend(IdGenerator.of("marko"),
+ Set.of("a"))
+ .extend(IdGenerator.of("lop"),
+ Set.of("b", "software"));
+
+ Object normalized = CypherClient.normalize(path);
+
+ Assert.assertInstanceOf(Map.class, normalized);
+ Map<?, ?> map = (Map<?, ?>) normalized;
+ Assert.assertTrue(map.containsKey("labels"));
+ Assert.assertTrue(map.containsKey("objects"));
+
+ Assert.assertInstanceOf(List.class, map.get("labels"));
+ Assert.assertInstanceOf(List.class, map.get("objects"));
+
+ List<?> labels = (List<?>) map.get("labels");
+ List<?> objects = (List<?>) map.get("objects");
+ Assert.assertEquals(2, labels.size());
+ Assert.assertEquals(2, objects.size());
+
+ Assert.assertEquals("marko", objects.get(0));
+ Assert.assertEquals("lop", objects.get(1));
+ List<?> firstLabels = (List<?>) labels.get(0);
+ List<?> secondLabels = (List<?>) labels.get(1);
+ Assert.assertTrue(firstLabels.contains("a"));
+ Assert.assertTrue(secondLabels.contains("b"));
+ Assert.assertTrue(secondLabels.contains("software"));
+ }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.java
new file mode 100644
index 000000000..5b7b5fa4a
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/HugeGraphSONModuleTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.hugegraph.unit.serializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.hugegraph.backend.id.EdgeId;
+import org.apache.hugegraph.backend.id.IdGenerator;
+import org.apache.hugegraph.backend.id.IdGenerator.LongId;
+import org.apache.hugegraph.backend.id.IdGenerator.StringId;
+import org.apache.hugegraph.backend.id.IdGenerator.UuidId;
+import org.apache.hugegraph.io.HugeGraphIoRegistry;
+import org.apache.hugegraph.schema.PropertyKey;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.unit.BaseUnitTest;
+import org.apache.hugegraph.unit.FakeObjects;
+import org.apache.hugegraph.util.JsonUtil;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONMapper;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONReader;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONVersion;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONWriter;
+import org.apache.tinkerpop.gremlin.structure.io.graphson.TypeInfo;
+import org.junit.Test;
+
+public class HugeGraphSONModuleTest extends BaseUnitTest {
+
+ @Test
+ public void testSerializeFileWithGraphSONTypeInfo() throws IOException {
+ String json = writeTyped(new File("test.text"));
+ Map<?, ?> typedFile = JsonUtil.fromJson(json, Map.class);
+ Object value = typedFile.get("@value");
+
+ Assert.assertEquals("hugegraph:File", typedFile.get("@type"));
+ Assert.assertInstanceOf(Map.class, value);
+ Assert.assertEquals("test.text", ((Map<?, ?>) value).get("file"));
+ Assert.assertContains("hugegraph:File", json);
+ Assert.assertContains("\"file\"", json);
+ Assert.assertContains("test.text", json);
+
+ File file = readTyped(json, File.class);
+ Assert.assertEquals("test.text", file.getName());
+ }
+
+ @Test
+ public void testRoundTripIdWithGraphSONTypeInfo() throws IOException {
+ StringId expectedStringId = (StringId) IdGenerator.of("marko");
+ LongId expectedLongId = (LongId) IdGenerator.of(123L);
+ UuidId expectedUuidId = (UuidId) IdGenerator.of(
+ UUID.fromString("3cfcafc8-7906-4ab7-a207-4ded056f58de"));
+ EdgeId expectedEdgeId = EdgeId.parse("S1>2>3>4>L6");
+
+ String stringId = writeTyped(expectedStringId);
+ String longId = writeTyped(expectedLongId);
+ String uuidId = writeTyped(expectedUuidId);
+ String edgeId = writeTyped(expectedEdgeId);
+
+ Assert.assertContains("hugegraph:StringId", stringId);
+ Assert.assertContains("marko", stringId);
+ Assert.assertContains("hugegraph:LongId", longId);
+ Assert.assertContains("123", longId);
+ Assert.assertContains("hugegraph:UuidId", uuidId);
+ Assert.assertContains("3cfcafc8-7906-4ab7-a207-4ded056f58de",
+ uuidId);
+ Assert.assertContains("hugegraph:EdgeId", edgeId);
+ Assert.assertContains("S1>2>3>4>L6", edgeId);
+
+ Assert.assertEquals(expectedStringId,
+ readTyped(stringId, StringId.class));
+ Assert.assertEquals(expectedLongId,
+ readTyped(longId, LongId.class));
+ Assert.assertEquals(expectedUuidId,
+ readTyped(uuidId, UuidId.class));
+ Assert.assertEquals(expectedEdgeId,
+ readTyped(edgeId, EdgeId.class));
+ }
+
+ @Test
+ public void testSerializeSchemaWithUntypedGraphSONModule() throws IOException {
+ FakeObjects objects = new FakeObjects();
+ PropertyKey propertyKey = objects.newPropertyKey(IdGenerator.of(1L),
+ "name");
+
+ String json = writeUntyped(propertyKey);
+
+ Assert.assertContains("\"name\"", json);
+ }
+
+ private static String writeTyped(Object object) throws IOException {
+ GraphSONMapper mapper = mapper(TypeInfo.PARTIAL_TYPES);
+ GraphSONWriter writer = GraphSONWriter.build().mapper(mapper).create();
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+ writer.writeObject(output, object);
+
+ return output.toString(StandardCharsets.UTF_8.name());
+ }
+
+ private static <T> T readTyped(String json, Class<T> clazz)
+ throws IOException {
+ GraphSONMapper mapper = mapper(TypeInfo.PARTIAL_TYPES);
+ GraphSONReader reader = GraphSONReader.build().mapper(mapper).create();
+ ByteArrayInputStream input = new ByteArrayInputStream(
+ json.getBytes(StandardCharsets.UTF_8));
+
+ return reader.readObject(input, clazz);
+ }
+
+ private static String writeUntyped(Object object) throws IOException {
+ GraphSONMapper mapper = mapper(TypeInfo.NO_TYPES);
+ GraphSONWriter writer = GraphSONWriter.build().mapper(mapper).create();
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+ writer.writeObject(output, object);
+
+ return output.toString(StandardCharsets.UTF_8.name());
+ }
+
+ private static GraphSONMapper mapper(TypeInfo typeInfo) {
+ GraphSONMapper mapper = GraphSONMapper.build()
+ .version(GraphSONVersion.V3_0)
+ .typeInfo(typeInfo)
+ .addRegistry(HugeGraphIoRegistry.instance())
+ .create();
+
+ return mapper;
+ }
+}
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java
index c76d536ff..722f747ed 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/JsonUtilTest.java
@@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.UUID;
import org.apache.commons.lang3.tuple.Pair;
@@ -51,6 +52,9 @@ import org.apache.hugegraph.unit.BaseUnitTest;
import org.apache.hugegraph.unit.FakeObjects;
import org.apache.hugegraph.util.JsonUtil;
import org.apache.hugegraph.util.collection.CollectionFactory;
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.MutablePath;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
import org.apache.tinkerpop.shaded.jackson.core.type.TypeReference;
import org.eclipse.collections.api.map.primitive.MutableIntObjectMap;
import org.junit.Test;
@@ -308,6 +312,31 @@ public class JsonUtilTest extends BaseUnitTest {
"\"weight\":0.8}}", json);
}
+ @Test
+ public void testSerializePath() {
+ Path path = MutablePath.make()
+ .extend("marko", Set.of("a"))
+ .extend(29, Set.of("b"));
+
+ String json = JsonUtil.toJson(path);
+
+ Assert.assertEquals("{\"labels\":[[\"a\"],[\"b\"]]," +
+ "\"objects\":[\"marko\",29]}", json);
+ }
+
+ @Test
+ public void testSerializeTree() {
+ Tree<String> child = new Tree<>();
+ child.put("lop", new Tree<>());
+ Tree<String> tree = new Tree<>();
+ tree.put("marko", child);
+
+ String json = JsonUtil.toJson(tree);
+
+ Assert.assertEquals("[{\"key\":\"marko\",\"value\":[{" +
+ "\"key\":\"lop\",\"value\":[]}]}]", json);
+ }
+
@Test
public void testDeserializeList() {
String json = "[\"1\", \"2\", \"3\"]";