Skip to content

Commit

Permalink
feat: support query data by use cypher language (#1866)
Browse files Browse the repository at this point in the history
implement #1748

Co-authored-by: jadepeng <[email protected]>
  • Loading branch information
jadepeng and jadepeng authored May 12, 2022
1 parent 9769406 commit cd09304
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 73 deletions.
8 changes: 7 additions & 1 deletion hugegraph-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-jersey3</artifactId>
</dependency>

<dependency>
<groupId>org.opencypher.gremlin</groupId>
<artifactId>translation</artifactId>
<version>1.0.4</version>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -153,7 +159,7 @@
</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<Implementation-Version>0.67.0.0</Implementation-Version>
<Implementation-Version>0.69.0.0</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> FORBIDDEN_REQUEST_EXCEPTIONS =
ImmutableSet.of("java.lang.SecurityException",
"jakarta.ws.rs.ForbiddenException");
private static final Set<String> BAD_REQUEST_EXCEPTIONS = ImmutableSet.of(
"java.lang.IllegalArgumentException",
"java.util.concurrent.TimeoutException",
"groovy.lang.",
"org.codehaus.",
"com.baidu.hugegraph."
);

@Context
private Provider<HugeConfig> 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
Expand Down Expand Up @@ -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<String, Object> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String> FORBIDDEN_REQUEST_EXCEPTIONS =
ImmutableSet.of("java.lang.SecurityException",
"jakarta.ws.rs.ForbiddenException");
private static final Set<String> BAD_REQUEST_EXCEPTIONS = ImmutableSet.of(
"java.lang.IllegalArgumentException",
"java.util.concurrent.TimeoutException",
"groovy.lang.",
"org.codehaus.",
"com.baidu.hugegraph."
);

@Context
private Provider<HugeConfig> 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<String, Object> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading

0 comments on commit cd09304

Please sign in to comment.