diff --git a/hugegraph-api/pom.xml b/hugegraph-api/pom.xml
index 0bc3a36bb7..aab9d0292b 100644
--- a/hugegraph-api/pom.xml
+++ b/hugegraph-api/pom.xml
@@ -115,6 +115,12 @@
io.dropwizard.metrics
metrics-jersey3
+
+
+ org.opencypher.gremlin
+ translation
+ 1.0.4
+
@@ -153,7 +159,7 @@
- 0.67.0.0
+ 0.69.0.0
diff --git a/hugegraph-api/src/main/java/com/baidu/hugegraph/api/gremlin/CypherAPI.java b/hugegraph-api/src/main/java/com/baidu/hugegraph/api/gremlin/CypherAPI.java
new file mode 100644
index 0000000000..f85ae03785
--- /dev/null
+++ b/hugegraph-api/src/main/java/com/baidu/hugegraph/api/gremlin/CypherAPI.java
@@ -0,0 +1,93 @@
+package com.baidu.hugegraph.api.gremlin;
+
+import org.opencypher.gremlin.translation.TranslationFacade;
+import org.slf4j.Logger;
+
+import com.baidu.hugegraph.api.filter.CompressInterceptor;
+import com.baidu.hugegraph.util.E;
+import com.baidu.hugegraph.util.Log;
+import com.codahale.metrics.annotation.Timed;
+
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+
+@Path("graphs/{graph}/cypher")
+@Singleton
+public class CypherAPI extends GremlinQueryAPI {
+
+ private static final Logger LOG = Log.logger(CypherAPI.class);
+
+
+ @GET
+ @Timed
+ @CompressInterceptor.Compress(buffer = (1024 * 40))
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ public Response query(@PathParam("graph") String graph,
+ @Context HttpHeaders headers,
+ @QueryParam("cypher") String cypher) {
+ LOG.debug("Graph [{}] query by cypher: {}", graph, cypher);
+
+ return this.queryByCypher(graph, headers, cypher);
+ }
+
+ @POST
+ @Timed
+ @CompressInterceptor.Compress
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ public Response post(@PathParam("graph") String graph,
+ @Context HttpHeaders headers,
+ String cypher) {
+ LOG.debug("Graph [{}] query by cypher: {}", graph, cypher);
+ return this.queryByCypher(graph, headers, cypher);
+ }
+
+ private Response queryByCypher(String graph,
+ HttpHeaders headers,
+ String cypher) {
+ E.checkArgument(cypher != null && !cypher.isEmpty(),
+ "The cypher parameter can't be null or empty");
+
+ String gremlin = this.translateCpyher2Gremlin(graph, cypher);
+ LOG.debug("translated gremlin is {}", gremlin);
+
+ String auth = headers.getHeaderString(HttpHeaders.AUTHORIZATION);
+ String request = "{"
+ + "\"gremlin\":\"" + gremlin + "\","
+ + "\"bindings\":{},"
+ + "\"language\":\"gremlin-groovy\","
+ + "\"aliases\":{\"g\":\"__g_" + graph + "\"}}";
+
+ Response response = this.client().doPostRequest(auth, request);
+ return transformResponseIfNeeded(response);
+ }
+
+ private String translateCpyher2Gremlin(String graph, String cypher) {
+ TranslationFacade translator = new TranslationFacade();
+ String gremlin = translator.toGremlinGroovy(cypher);
+ gremlin = this.buildQueryableGremlin(graph, gremlin);
+ return gremlin;
+ }
+
+ private String buildQueryableGremlin(String graph, String gremlin) {
+ /*
+ * `CREATE (a:person { name : 'test', age: 20) return a`
+ * would be translated to:
+ * `g.addV('person').as('a').property(single, 'name', 'test') ...`,
+ * but hugegraph don't support `.property(single, k, v)`,
+ * so we replace it to `.property(k, v)` here
+ */
+ gremlin = gremlin.replace(".property(single,", ".property(");
+
+ return gremlin;
+ }
+}
diff --git a/hugegraph-api/src/main/java/com/baidu/hugegraph/api/gremlin/GremlinAPI.java b/hugegraph-api/src/main/java/com/baidu/hugegraph/api/gremlin/GremlinAPI.java
index 60fc5c9b19..78ce651138 100644
--- a/hugegraph-api/src/main/java/com/baidu/hugegraph/api/gremlin/GremlinAPI.java
+++ b/hugegraph-api/src/main/java/com/baidu/hugegraph/api/gremlin/GremlinAPI.java
@@ -49,40 +49,13 @@
@Path("gremlin")
@Singleton
-public class GremlinAPI extends API {
+public class GremlinAPI extends GremlinQueryAPI {
private static final Histogram GREMLIN_INPUT_HISTOGRAM =
MetricsUtil.registerHistogram(GremlinAPI.class, "gremlin-input");
private static final Histogram GREMLIN_OUTPUT_HISTOGRAM =
MetricsUtil.registerHistogram(GremlinAPI.class, "gremlin-output");
- private static final Set FORBIDDEN_REQUEST_EXCEPTIONS =
- ImmutableSet.of("java.lang.SecurityException",
- "jakarta.ws.rs.ForbiddenException");
- private static final Set BAD_REQUEST_EXCEPTIONS = ImmutableSet.of(
- "java.lang.IllegalArgumentException",
- "java.util.concurrent.TimeoutException",
- "groovy.lang.",
- "org.codehaus.",
- "com.baidu.hugegraph."
- );
-
- @Context
- private Provider configProvider;
-
- private GremlinClient client;
-
- public GremlinClient client() {
- if (this.client != null) {
- return this.client;
- }
- HugeConfig config = this.configProvider.get();
- String url = config.get(ServerOptions.GREMLIN_SERVER_URL);
- int timeout = config.get(ServerOptions.GREMLIN_SERVER_TIMEOUT) * 1000;
- int maxRoutes = config.get(ServerOptions.GREMLIN_SERVER_MAX_ROUTE);
- this.client = new GremlinClient(url, timeout, maxRoutes, maxRoutes);
- return this.client;
- }
@POST
@Timed
@@ -120,46 +93,4 @@ public Response get(@Context HugeConfig conf,
GREMLIN_OUTPUT_HISTOGRAM.update(response.getLength());
return transformResponseIfNeeded(response);
}
-
- private static Response transformResponseIfNeeded(Response response) {
- MediaType mediaType = response.getMediaType();
- if (mediaType != null) {
- // Append charset
- assert MediaType.APPLICATION_JSON_TYPE.equals(mediaType);
- response.getHeaders().putSingle(HttpHeaders.CONTENT_TYPE,
- mediaType.withCharset(CHARSET));
- }
-
- Response.StatusType status = response.getStatusInfo();
- if (status.getStatusCode() < 400) {
- // No need to transform if normal response without error
- return response;
- }
-
- if (mediaType == null || !JSON.equals(mediaType.getSubtype())) {
- String message = response.readEntity(String.class);
- throw new HugeGremlinException(status.getStatusCode(),
- ImmutableMap.of("message", message));
- }
-
- @SuppressWarnings("unchecked")
- Map map = response.readEntity(Map.class);
- String exClassName = (String) map.get("Exception-Class");
- if (FORBIDDEN_REQUEST_EXCEPTIONS.contains(exClassName)) {
- status = Response.Status.FORBIDDEN;
- } else if (matchBadRequestException(exClassName)) {
- status = Response.Status.BAD_REQUEST;
- }
- throw new HugeGremlinException(status.getStatusCode(), map);
- }
-
- private static boolean matchBadRequestException(String exClass) {
- if (exClass == null) {
- return false;
- }
- if (BAD_REQUEST_EXCEPTIONS.contains(exClass)) {
- return true;
- }
- return BAD_REQUEST_EXCEPTIONS.stream().anyMatch(exClass::startsWith);
- }
}
diff --git a/hugegraph-api/src/main/java/com/baidu/hugegraph/api/gremlin/GremlinQueryAPI.java b/hugegraph-api/src/main/java/com/baidu/hugegraph/api/gremlin/GremlinQueryAPI.java
new file mode 100644
index 0000000000..5a072e095a
--- /dev/null
+++ b/hugegraph-api/src/main/java/com/baidu/hugegraph/api/gremlin/GremlinQueryAPI.java
@@ -0,0 +1,90 @@
+package com.baidu.hugegraph.api.gremlin;
+
+import java.util.Map;
+import java.util.Set;
+
+import com.baidu.hugegraph.api.API;
+import com.baidu.hugegraph.config.HugeConfig;
+import com.baidu.hugegraph.config.ServerOptions;
+import com.baidu.hugegraph.exception.HugeGremlinException;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import jakarta.inject.Provider;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+public class GremlinQueryAPI extends API {
+
+ private static final Set FORBIDDEN_REQUEST_EXCEPTIONS =
+ ImmutableSet.of("java.lang.SecurityException",
+ "jakarta.ws.rs.ForbiddenException");
+ private static final Set BAD_REQUEST_EXCEPTIONS = ImmutableSet.of(
+ "java.lang.IllegalArgumentException",
+ "java.util.concurrent.TimeoutException",
+ "groovy.lang.",
+ "org.codehaus.",
+ "com.baidu.hugegraph."
+ );
+
+ @Context
+ private Provider configProvider;
+
+ private GremlinClient client;
+
+ public GremlinClient client() {
+ if (this.client != null) {
+ return this.client;
+ }
+ HugeConfig config = this.configProvider.get();
+ String url = config.get(ServerOptions.GREMLIN_SERVER_URL);
+ int timeout = config.get(ServerOptions.GREMLIN_SERVER_TIMEOUT) * 1000;
+ int maxRoutes = config.get(ServerOptions.GREMLIN_SERVER_MAX_ROUTE);
+ this.client = new GremlinClient(url, timeout, maxRoutes, maxRoutes);
+ return this.client;
+ }
+
+ protected static Response transformResponseIfNeeded(Response response) {
+ MediaType mediaType = response.getMediaType();
+ if (mediaType != null) {
+ // Append charset
+ assert MediaType.APPLICATION_JSON_TYPE.equals(mediaType);
+ response.getHeaders().putSingle(HttpHeaders.CONTENT_TYPE,
+ mediaType.withCharset(CHARSET));
+ }
+
+ Response.StatusType status = response.getStatusInfo();
+ if (status.getStatusCode() < 400) {
+ // No need to transform if normal response without error
+ return response;
+ }
+
+ if (mediaType == null || !JSON.equals(mediaType.getSubtype())) {
+ String message = response.readEntity(String.class);
+ throw new HugeGremlinException(status.getStatusCode(),
+ ImmutableMap.of("message", message));
+ }
+
+ @SuppressWarnings("unchecked")
+ Map map = response.readEntity(Map.class);
+ String exClassName = (String) map.get("Exception-Class");
+ if (FORBIDDEN_REQUEST_EXCEPTIONS.contains(exClassName)) {
+ status = Response.Status.FORBIDDEN;
+ } else if (matchBadRequestException(exClassName)) {
+ status = Response.Status.BAD_REQUEST;
+ }
+ throw new HugeGremlinException(status.getStatusCode(), map);
+ }
+
+ private static boolean matchBadRequestException(String exClass) {
+ if (exClass == null) {
+ return false;
+ }
+ if (BAD_REQUEST_EXCEPTIONS.contains(exClass)) {
+ return true;
+ }
+ return BAD_REQUEST_EXCEPTIONS.stream().anyMatch(exClass::startsWith);
+ }
+}
diff --git a/hugegraph-api/src/main/java/com/baidu/hugegraph/version/ApiVersion.java b/hugegraph-api/src/main/java/com/baidu/hugegraph/version/ApiVersion.java
index 14e2eba7d8..dc43e93b19 100644
--- a/hugegraph-api/src/main/java/com/baidu/hugegraph/version/ApiVersion.java
+++ b/hugegraph-api/src/main/java/com/baidu/hugegraph/version/ApiVersion.java
@@ -118,10 +118,11 @@ public final class ApiVersion {
* [0.66] Issue-1567: Support get schema RESTful API
* [0.67] Issue-1065: Support dynamically add/remove graph
* [0.68] Issue-1763: Support adamic-adar & resource-allocation API
+ * [0.69] Issue-1748: Support Cypher query RESTful API
*/
// The second parameter of Version.of() is for IDE running without JAR
- public static final Version VERSION = Version.of(ApiVersion.class, "0.68");
+ public static final Version VERSION = Version.of(ApiVersion.class, "0.69");
public static void check() {
// Check version of hugegraph-core. Firstly do check from version 0.3
diff --git a/hugegraph-test/src/main/java/com/baidu/hugegraph/api/BaseApiTest.java b/hugegraph-test/src/main/java/com/baidu/hugegraph/api/BaseApiTest.java
index ffe8a08524..b1b2d1296e 100644
--- a/hugegraph-test/src/main/java/com/baidu/hugegraph/api/BaseApiTest.java
+++ b/hugegraph-test/src/main/java/com/baidu/hugegraph/api/BaseApiTest.java
@@ -61,7 +61,7 @@ public class BaseApiTest {
private static final String USERNAME = "admin";
private static final String PASSWORD = "pa";
- private static final String URL_PREFIX = "graphs/" + GRAPH;
+ protected static final String URL_PREFIX = "graphs/" + GRAPH;
private static final String SCHEMA_PKS = "/schema/propertykeys";
private static final String SCHEMA_VLS = "/schema/vertexlabels";
private static final String SCHEMA_ELS = "/schema/edgelabels";
diff --git a/hugegraph-test/src/main/java/com/baidu/hugegraph/api/CypherApiTest.java b/hugegraph-test/src/main/java/com/baidu/hugegraph/api/CypherApiTest.java
new file mode 100644
index 0000000000..c2841e9bbb
--- /dev/null
+++ b/hugegraph-test/src/main/java/com/baidu/hugegraph/api/CypherApiTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017 HugeGraph Authors
+ *
+ * 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 com.baidu.hugegraph.api;
+
+import static com.baidu.hugegraph.testutil.Assert.assertContains;
+
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.baidu.hugegraph.testutil.Assert;
+import com.google.common.collect.ImmutableMap;
+
+import jakarta.ws.rs.core.Response;
+
+public class CypherApiTest extends BaseApiTest {
+
+ private static final String path = URL_PREFIX + "/cypher";
+ private static final String query = "MATCH (n:person) where n.city ='Beijing' return n";
+ private static final String queryResult = "Beijing";
+
+ @Before
+ public void prepareSchema() {
+ BaseApiTest.initPropertyKey();
+ BaseApiTest.initVertexLabel();
+ BaseApiTest.initEdgeLabel();
+ BaseApiTest.initIndexLabel();
+ BaseApiTest.initVertex();
+ BaseApiTest.initEdge();
+ }
+
+ @Test
+ public void testGet() {
+ Map params = ImmutableMap.of("cypher", query);
+ Response r = client().get(path, params);
+
+ this.validStatusAndTextContains(queryResult, r);
+ }
+
+ @Test
+ public void testPost() {
+ this.testCypherQueryAndContains(query, queryResult);
+ }
+
+ @Test
+ public void testCreate() {
+ this.testCypherQueryAndContains("CREATE (n:person { name : 'test', age: 20, city: 'Hefei' }) return n",
+ "Hefei");
+ }
+
+ @Test
+ public void testRelationQuery() {
+ String cypher = "MATCH (n:person)-[r:knows]->(friend:person)\n" +
+ "WHERE n.name = 'marko'\n" +
+ "RETURN n, friend.name AS friend";
+ this.testCypherQueryAndContains(cypher, "friend");
+ }
+
+ private void testCypherQueryAndContains(String cypher, String containsText) {
+ Response r = client().post(path, cypher);
+ this.validStatusAndTextContains(containsText, r);
+ }
+
+ private void validStatusAndTextContains(String value, Response r) {
+ String content = assertResponseStatus(200, r);
+ assertContains(value, content);
+ }
+}