You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by je...@apache.org on 2023/03/15 14:36:20 UTC
[incubator-hugegraph] branch master updated: feat: use an enhanced CypherAPI to refactor it (#2143)
This is an automated email from the ASF dual-hosted git repository.
jermy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-hugegraph.git
The following commit(s) were added to refs/heads/master by this push:
new 4b74ab6ce feat: use an enhanced CypherAPI to refactor it (#2143)
4b74ab6ce is described below
commit 4b74ab6cec4a8357dd6a8feef4e3c9ea616fa52f
Author: Bond <ly...@hotmail.com>
AuthorDate: Wed Mar 15 22:36:13 2023 +0800
feat: use an enhanced CypherAPI to refactor it (#2143)
Co-authored-by: lynn.bond <li...@baidu.com>
Co-authored-by: imbajin <ji...@apache.org>
---
.licenserc.yaml | 2 +
LICENSE | 2 +
.../org/apache/hugegraph/api/cypher/CypherAPI.java | 154 +++++++++++
.../apache/hugegraph/api/cypher/CypherClient.java | 153 ++++++++++
.../apache/hugegraph/api/cypher/CypherManager.java | 101 +++++++
.../apache/hugegraph/api/cypher/CypherModel.java | 63 +++++
.../apache/hugegraph/api/gremlin/CypherAPI.java | 111 --------
.../hugegraph/auth/StandardAuthenticator.java | 82 +++++-
.../hugegraph/opencypher/CypherOpProcessor.java | 308 +++++++++++++++++++++
.../apache/hugegraph/opencypher/CypherPlugin.java | 80 ++++++
...g.apache.tinkerpop.gremlin.jsr223.GremlinPlugin | 1 +
...org.apache.tinkerpop.gremlin.server.OpProcessor | 1 +
.../main/java/org/apache/hugegraph/HugeGraph.java | 5 +-
.../apache/hugegraph/auth/StandardAuthManager.java | 7 +-
.../traversal/optimize/HugePrimaryKeyStrategy.java | 107 +++++++
hugegraph-dist/release-docs/LICENSE | 2 +
.../src/assembly/static/conf/gremlin-server.yaml | 14 +-
.../src/assembly/static/conf/remote-objects.yaml | 7 +-
.../org/apache/hugegraph/api/ApiTestSuite.java | 3 +-
19 files changed, 1084 insertions(+), 119 deletions(-)
diff --git a/.licenserc.yaml b/.licenserc.yaml
index ea56bb6e1..334d89b51 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -94,6 +94,8 @@ header: # `header` section is configurations for source codes license header.
- '**/type/Nameable.java'
- '**/define/Cardinality.java'
- '**/util/StringEncoding.java'
+ - 'hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java'
+ - 'hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherPlugin.java'
comment: on-failure # on what condition license-eye will comment on the pull request, `on-failure`, `always`, `never`.
# license-location-threshold specifies the index threshold where the license header can be located,
diff --git a/LICENSE b/LICENSE
index 84199011b..ad08080e3 100644
--- a/LICENSE
+++ b/LICENSE
@@ -214,3 +214,5 @@ hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeScriptT
hugegraph-core/src/main/java/org/apache/hugegraph/type/Nameable.java from https://github.com/JanusGraph/janusgraph
hugegraph-core/src/main/java/org/apache/hugegraph/type/define/Cardinality.java from https://github.com/JanusGraph/janusgraph
hugegraph-core/src/main/java/org/apache/hugegraph/util/StringEncoding.java from https://github.com/JanusGraph/janusgraph
+hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java from https://github.com/opencypher/cypher-for-gremlin
+hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherPlugin.java from https://github.com/opencypher/cypher-for-gremlin
diff --git a/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java b/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java
new file mode 100644
index 000000000..bf43d6af4
--- /dev/null
+++ b/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherAPI.java
@@ -0,0 +1,154 @@
+/*
+ * 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.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hugegraph.api.API;
+import org.apache.hugegraph.api.filter.CompressInterceptor;
+import org.apache.hugegraph.util.E;
+import org.apache.hugegraph.util.Log;
+import org.slf4j.Logger;
+
+import com.codahale.metrics.annotation.Timed;
+
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.NotAuthorizedException;
+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;
+
+@Path("graphs/{graph}/cypher")
+@Singleton
+public class CypherAPI extends API {
+
+ private static final Logger LOG = Log.logger(CypherAPI.class);
+ private static final Charset UTF8 = StandardCharsets.UTF_8;
+ private static final String CLIENT_CONF = "conf/remote-objects.yaml";
+ private final Base64.Decoder decoder = Base64.getUrlDecoder();
+ private final String basic = "Basic ";
+ private final String bearer = "Bearer ";
+
+ private CypherManager cypherManager;
+
+ private CypherManager cypherManager() {
+ if (this.cypherManager == null) {
+ this.cypherManager = CypherManager.configOf(CLIENT_CONF);
+ }
+ return this.cypherManager;
+ }
+
+ @GET
+ @Timed
+ @CompressInterceptor.Compress(buffer = (1024 * 40))
+ @Produces(APPLICATION_JSON_WITH_CHARSET)
+ public CypherModel 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 CypherModel 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 CypherModel queryByCypher(String graph, HttpHeaders headers, String cypher) {
+ E.checkArgument(graph != null && !graph.isEmpty(),
+ "The graph parameter can't be null or empty");
+ E.checkArgument(cypher != null && !cypher.isEmpty(),
+ "The cypher parameter can't be null or empty");
+
+ Map<String, String> aliases = new HashMap<>(1, 1);
+ aliases.put("g", "__g_" + graph);
+
+ return this.client(headers).submitQuery(cypher, aliases);
+ }
+
+ private CypherClient client(HttpHeaders headers) {
+ String auth = headers.getHeaderString(HttpHeaders.AUTHORIZATION);
+
+ if (auth != null && !auth.isEmpty()) {
+ auth = auth.split(",")[0];
+ }
+
+ if (auth != null) {
+ if (auth.startsWith(basic)) {
+ return this.clientViaBasic(auth);
+ } else if (auth.startsWith(bearer)) {
+ return this.clientViaToken(auth);
+ }
+ }
+
+ throw new NotAuthorizedException("The Cypher-API called without any authorization.");
+ }
+
+ private CypherClient clientViaBasic(String auth) {
+ Pair<String, String> userPass = this.toUserPass(auth);
+ E.checkNotNull(userPass, "user-password-pair");
+
+ return this.cypherManager().getClient(userPass.getLeft(), userPass.getRight());
+ }
+
+ private CypherClient clientViaToken(String auth) {
+ return this.cypherManager().getClient(auth.substring(bearer.length()));
+ }
+
+ private Pair<String, String> toUserPass(String auth) {
+ if (auth == null || auth.isEmpty()) {
+ return null;
+ }
+ if (!auth.startsWith(basic)) {
+ return null;
+ }
+
+ String[] split;
+ try {
+ String encoded = auth.substring(basic.length());
+ byte[] userPass = this.decoder.decode(encoded);
+ String authorization = new String(userPass, UTF8);
+ split = authorization.split(":");
+ } catch (Exception e) {
+ LOG.error("Failed convert auth to credential.", e);
+ return null;
+ }
+
+ if (split.length != 2) {
+ return null;
+ }
+ return ImmutablePair.of(split[0], split[1]);
+ }
+}
diff --git a/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java b/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java
new file mode 100644
index 000000000..10e92f2c7
--- /dev/null
+++ b/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java
@@ -0,0 +1,153 @@
+/*
+ * 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 org.apache.hugegraph.util.E;
+import org.apache.hugegraph.util.Log;
+import org.apache.commons.configuration2.Configuration;
+import org.apache.tinkerpop.gremlin.driver.*;
+import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
+import org.slf4j.Logger;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Supplier;
+
+@ThreadSafe
+public final class CypherClient {
+
+ private static final Logger LOG = Log.logger(CypherClient.class);
+ private final Supplier<Configuration> configurationSupplier;
+ private String userName;
+ private String password;
+ private String token;
+
+ CypherClient(String userName, String password,
+ Supplier<Configuration> configurationSupplier) {
+ this.userName = userName;
+ this.password = password;
+ this.configurationSupplier = configurationSupplier;
+ }
+
+ CypherClient(String token, Supplier<Configuration> configurationSupplier) {
+ this.token = token;
+ this.configurationSupplier = configurationSupplier;
+ }
+
+ public CypherModel submitQuery(String cypherQuery, @Nullable Map<String, String> aliases) {
+ E.checkArgument(cypherQuery != null && !cypherQuery.isEmpty(),
+ "The cypher-query parameter can't be null or empty");
+
+ Cluster cluster = Cluster.open(getConfig());
+ Client client = cluster.connect();
+
+ if (aliases != null && !aliases.isEmpty()) {
+ client = client.alias(aliases);
+ }
+
+ RequestMessage request = createRequest(cypherQuery);
+ CypherModel res;
+
+ try {
+ List<Object> list = this.doQueryList(client, request);
+ res = CypherModel.dataOf(request.getRequestId().toString(), list);
+ } catch (Exception e) {
+ LOG.error(String.format("Failed to submit cypher-query: [ %s ], caused by:",
+ cypherQuery), e);
+ res = CypherModel.failOf(request.getRequestId().toString(), e.getMessage());
+ } finally {
+ client.close();
+ cluster.close();
+ }
+
+ return res;
+ }
+
+ private RequestMessage createRequest(String cypherQuery) {
+ return RequestMessage.build(Tokens.OPS_EVAL)
+ .processor("cypher")
+ .add(Tokens.ARGS_GREMLIN, cypherQuery)
+ .create();
+ }
+
+ private List<Object> doQueryList(Client client, RequestMessage request)
+ throws ExecutionException, InterruptedException {
+ ResultSet results = client.submitAsync(request).get();
+
+ Iterator<Result> iter = results.iterator();
+ List<Object> list = new LinkedList<>();
+
+ while (iter.hasNext()) {
+ Result data = iter.next();
+ list.add(data.getObject());
+ }
+
+ return list;
+ }
+
+ /**
+ * 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
+ * an empty string, which is an uncommon value under normal conditions.
+ * The token will then be transferred through the userName-property.
+ * To see org.apache.hugegraph.auth.StandardAuthenticator.TokenSaslAuthenticator
+ */
+ private Configuration getConfig() {
+ Configuration conf = this.configurationSupplier.get();
+ conf.addProperty("username", this.token == null ? this.userName : this.token);
+ conf.addProperty("password", this.token == null ? this.password : "");
+
+ return conf;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ CypherClient that = (CypherClient) o;
+
+ return Objects.equals(userName, that.userName) &&
+ Objects.equals(password, that.password) &&
+ Objects.equals(token, that.token);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(userName, password, token);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("CypherClient{");
+ builder.append("userName='").append(userName).append('\'')
+ .append(", token='").append(token).append('\'').append('}');
+
+ return builder.toString();
+ }
+}
diff --git a/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherManager.java b/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherManager.java
new file mode 100644
index 000000000..519ca66d9
--- /dev/null
+++ b/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherManager.java
@@ -0,0 +1,101 @@
+/*
+ * 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.io.File;
+import java.io.FileReader;
+import java.io.Reader;
+import java.net.URL;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.configuration2.YAMLConfiguration;
+import org.apache.hugegraph.util.E;
+
+@ThreadSafe
+public final class CypherManager {
+
+ private final String configurationFile;
+ private YAMLConfiguration configuration;
+
+ public static CypherManager configOf(String configurationFile) {
+ E.checkArgument(configurationFile != null && !configurationFile.isEmpty(),
+ "The configurationFile parameter can't be null or empty");
+ return new CypherManager(configurationFile);
+ }
+
+ private CypherManager(String configurationFile) {
+ this.configurationFile = configurationFile;
+ }
+
+ public CypherClient getClient(String userName, String password) {
+ E.checkArgument(userName != null && !userName.isEmpty(),
+ "The userName parameter can't be null or empty");
+ E.checkArgument(password != null && !password.isEmpty(),
+ "The password parameter can't be null or empty");
+
+ // TODO: Need to cache the client and make it hold the connection.
+ return new CypherClient(userName, password, this::cloneConfig);
+ }
+
+ public CypherClient getClient(String token) {
+ E.checkArgument(token != null && !token.isEmpty(),
+ "The token parameter can't be null or empty");
+
+ // TODO: Need to cache the client and make it hold the connection.
+ return new CypherClient(token, this::cloneConfig);
+ }
+
+ private Configuration cloneConfig() {
+ if (this.configuration == null) {
+ this.configuration = loadYaml(this.configurationFile);
+ }
+ return (Configuration) this.configuration.clone();
+ }
+
+ private static YAMLConfiguration loadYaml(String configurationFile) {
+ File yamlFile = getConfigFile(configurationFile);
+ YAMLConfiguration yaml;
+ try {
+ Reader reader = new FileReader(yamlFile);
+ yaml = new YAMLConfiguration();
+ yaml.read(reader);
+ } catch (Exception e) {
+ throw new RuntimeException(String.format("Failed to load configuration file," +
+ " the file at '%s'.", configurationFile), e);
+ }
+ return yaml;
+ }
+
+ private static File getConfigFile(String configurationFile) {
+ File systemFile = new File(configurationFile);
+ if (!systemFile.exists()) {
+ ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
+ URL resource = currentClassLoader.getResource(configurationFile);
+ assert resource != null;
+ File resourceFile = new File(resource.getFile());
+ if (!resourceFile.exists()) {
+ throw new IllegalArgumentException(String.format("Configuration file at '%s' does" +
+ " not exist", configurationFile));
+ }
+ return resourceFile;
+ }
+ return systemFile;
+ }
+}
diff --git a/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java b/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java
new file mode 100644
index 000000000..03cbdd603
--- /dev/null
+++ b/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java
@@ -0,0 +1,63 @@
+/*
+ * 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.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * As same as response of GremlinAPI
+ */
+public class CypherModel {
+
+ public String requestId;
+ public Status status = new Status();
+ public Result result = new Result();
+
+ public static CypherModel dataOf(String requestId, List<Object> data) {
+ CypherModel res = new CypherModel();
+ res.requestId = requestId;
+ res.status.code = 200;
+ res.result.data = data;
+ return res;
+ }
+
+ public static CypherModel failOf(String requestId, String message) {
+ CypherModel res = new CypherModel();
+ res.requestId = requestId;
+ res.status.code = 400;
+ res.status.message = message;
+ return res;
+ }
+
+ private CypherModel() {
+ }
+
+ public static class Status {
+ public String message = "";
+ public int code;
+ public Map<String, Object> attributes = Collections.EMPTY_MAP;
+ }
+
+ private static class Result {
+ public List<Object> data;
+ public Map<String, Object> meta = Collections.EMPTY_MAP;
+ }
+
+}
diff --git a/hugegraph-api/src/main/java/org/apache/hugegraph/api/gremlin/CypherAPI.java b/hugegraph-api/src/main/java/org/apache/hugegraph/api/gremlin/CypherAPI.java
deleted file mode 100644
index 24c39a69f..000000000
--- a/hugegraph-api/src/main/java/org/apache/hugegraph/api/gremlin/CypherAPI.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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.gremlin;
-
-import org.opencypher.gremlin.translation.TranslationFacade;
-import org.slf4j.Logger;
-
-import org.apache.hugegraph.api.filter.CompressInterceptor.Compress;
-import org.apache.hugegraph.util.E;
-import org.apache.hugegraph.util.Log;
-import com.codahale.metrics.annotation.Timed;
-
-import io.swagger.v3.oas.annotations.tags.Tag;
-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
-@Tag(name = "CypherAPI")
-public class CypherAPI extends GremlinQueryAPI {
-
- private static final Logger LOG = Log.logger(CypherAPI.class);
-
- @GET
- @Timed
- @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
- @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/org/apache/hugegraph/auth/StandardAuthenticator.java b/hugegraph-api/src/main/java/org/apache/hugegraph/auth/StandardAuthenticator.java
index 244bfd848..48e74225f 100644
--- a/hugegraph-api/src/main/java/org/apache/hugegraph/auth/StandardAuthenticator.java
+++ b/hugegraph-api/src/main/java/org/apache/hugegraph/auth/StandardAuthenticator.java
@@ -19,10 +19,12 @@ package org.apache.hugegraph.auth;
import java.io.Console;
import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
-import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang.StringUtils;
import org.apache.hugegraph.HugeGraph;
import org.apache.hugegraph.config.CoreOptions;
@@ -33,6 +35,8 @@ import org.apache.hugegraph.rpc.RpcClientProviderWithAuth;
import org.apache.hugegraph.util.ConfigUtil;
import org.apache.hugegraph.util.E;
import org.apache.hugegraph.util.StringEncoding;
+import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser;
+import org.apache.tinkerpop.gremlin.server.auth.AuthenticationException;
import org.apache.tinkerpop.gremlin.structure.util.GraphFactory;
public class StandardAuthenticator implements HugeAuthenticator {
@@ -194,7 +198,7 @@ public class StandardAuthenticator implements HugeAuthenticator {
@Override
public SaslNegotiator newSaslNegotiator(InetAddress remoteAddress) {
- throw new NotImplementedException("SaslNegotiator is unsupported");
+ return new TokenSaslAuthenticator();
}
public static void initAdminUserIfNeeded(String confFile) throws Exception {
@@ -210,4 +214,78 @@ public class StandardAuthenticator implements HugeAuthenticator {
auth.initAdminUser();
}
}
+
+ private class TokenSaslAuthenticator implements SaslNegotiator {
+
+ private static final byte NUL = 0;
+ private String username;
+ private String password;
+ private String token;
+
+ @Override
+ public byte[] evaluateResponse(final byte[] clientResponse) throws AuthenticationException {
+ decode(clientResponse);
+ return null;
+ }
+
+ @Override
+ public boolean isComplete() {
+ return this.username != null;
+ }
+
+ @Override
+ public AuthenticatedUser getAuthenticatedUser() throws AuthenticationException {
+ if (!this.isComplete()) {
+ throw new AuthenticationException(
+ "The SASL negotiation has not yet been completed.");
+ }
+
+ final Map<String, String> credentials = new HashMap<>(6, 1);
+ credentials.put(KEY_USERNAME, username);
+ credentials.put(KEY_PASSWORD, password);
+ credentials.put(KEY_TOKEN, token);
+
+ return authenticate(credentials);
+ }
+
+ /**
+ * SASL PLAIN mechanism specifies that credentials are encoded in a
+ * sequence of UTF-8 bytes, delimited by 0 (US-ASCII NUL).
+ * The form is : {code}authzId<NUL>authnId<NUL>password<NUL>{code}.
+ *
+ * @param bytes encoded credentials string sent by the client
+ */
+ private void decode(byte[] bytes) throws AuthenticationException {
+ this.username = null;
+ this.password = null;
+
+ int end = bytes.length;
+
+ for (int i = bytes.length - 1; i >= 0; i--) {
+ if (bytes[i] != NUL) {
+ continue;
+ }
+ if (this.password == null) {
+ password = new String(Arrays.copyOfRange(bytes, i + 1, end),
+ StandardCharsets.UTF_8);
+ } else if (this.username == null) {
+ username = new String(Arrays.copyOfRange(bytes, i + 1, end),
+ StandardCharsets.UTF_8);
+ }
+ end = i;
+ }
+
+ if (this.username == null) {
+ throw new AuthenticationException("SASL authentication ID must not be null.");
+ }
+ if (this.password == null) {
+ throw new AuthenticationException("SASL password must not be null.");
+ }
+
+ /* The trick is here. >_*/
+ if (password.isEmpty()) {
+ token = username;
+ }
+ }
+ }
}
diff --git a/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java b/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java
new file mode 100644
index 000000000..a4dfff60a
--- /dev/null
+++ b/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (c) 2018-2019 "Neo4j, Inc." [https://neo4j.com]
+ *
+ * Licensed 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.opencypher;
+
+import io.netty.channel.ChannelHandlerContext;
+
+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;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalInterruptedException;
+import org.apache.tinkerpop.gremlin.server.Context;
+import org.apache.tinkerpop.gremlin.server.GraphManager;
+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.function.ThrowingConsumer;
+import org.opencypher.gremlin.translation.CypherAst;
+import org.opencypher.gremlin.translation.groovy.GroovyPredicate;
+import org.opencypher.gremlin.translation.ir.TranslationWriter;
+import org.opencypher.gremlin.translation.ir.model.GremlinStep;
+import org.opencypher.gremlin.translation.translator.Translator;
+import org.opencypher.gremlin.traversal.ParameterNormalizer;
+import org.opencypher.gremlin.traversal.ProcedureContext;
+import org.opencypher.gremlin.traversal.ReturnNormalizer;
+import org.slf4j.Logger;
+
+import scala.collection.Seq;
+
+import java.util.*;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+
+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.opencypher.gremlin.translation.StatementOption.EXPLAIN;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Description of the modifications:
+ * <p>
+ * 1) Changed the method signature to adopt the gremlin-server 3.5.1.
+ * <pre>
+ * public Optional<ThrowingConsumer<Context>> selectOther(RequestMessage requestMessage)
+ * -->
+ * public Optional<ThrowingConsumer<Context>> selectOther(Context ctx)
+ * </pre>
+ * </p>
+ * <p>
+ * 2) Changed the package name.
+ * <pre>
+ * org.opencypher.gremlin.server.op.cypher
+ * -->
+ * org.apache.hugegraph.opencypher
+ * </pre>
+ * </p>
+ * <p>
+ * 3) Set the logger level from info to trace
+ * </p>
+ *
+ * {@link OpProcessor} implementation for processing Cypher {@link RequestMessage}s:
+ * <pre>
+ * {
+ * "requestId": "<some UUID>",
+ * "op": "eval",
+ * "processor": "cypher",
+ * "args": { "gremlin": "<CYPHER QUERY>" }
+ * }
+ * </pre>
+ */
+public class CypherOpProcessor extends AbstractEvalOpProcessor {
+
+ private static final String DEFAULT_TRANSLATOR_DEFINITION =
+ "gremlin+cfog_server_extensions+inline_parameters";
+
+ private static final Logger logger = getLogger(CypherOpProcessor.class);
+
+ public CypherOpProcessor() {
+ super(true);
+ }
+
+ @Override
+ public String getName() {
+ return "cypher";
+ }
+
+ @Override
+ public ThrowingConsumer<Context> getEvalOp() {
+ return this::evalCypher;
+ }
+
+ @Override
+ public Optional<ThrowingConsumer<Context>> selectOther(Context ctx) {
+ return empty();
+ }
+
+ private void evalCypher(Context context) throws OpProcessorException {
+ Map<String, Object> args = context.getRequestMessage().getArgs();
+ String cypher = (String) args.get(Tokens.ARGS_GREMLIN);
+ logger.trace("Cypher: {}", cypher.replaceAll("\n", " "));
+
+ GraphTraversalSource gts = traversal(context);
+ DefaultGraphTraversal g = new DefaultGraphTraversal(gts.clone());
+ Map<String, Object> parameters = ParameterNormalizer.normalize(getParameters(args));
+ ProcedureContext procedureContext = ProcedureContext.global();
+ CypherAst ast = CypherAst.parse(cypher, parameters, procedureContext.getSignatures());
+
+ String translatorDefinition = getTranslatorDefinition(context);
+
+ Translator<String, GroovyPredicate> strTranslator = Translator.builder()
+ .gremlinGroovy()
+ .build(translatorDefinition);
+
+ Translator<GraphTraversal, P> traversalTranslator = Translator.builder()
+ .traversal(g)
+ .build(translatorDefinition);
+
+ Seq<GremlinStep> ir = ast.translate(strTranslator.flavor(),
+ strTranslator.features(), procedureContext);
+
+ String gremlin = TranslationWriter.write(ir, strTranslator, parameters);
+ logger.trace("Gremlin: {}", gremlin);
+
+ if (ast.getOptions().contains(EXPLAIN)) {
+ explainQuery(context, ast, gremlin);
+ return;
+ }
+
+ GraphTraversal<?, ?> traversal = TranslationWriter.write(ir, traversalTranslator,
+ parameters);
+ ReturnNormalizer returnNormalizer = ReturnNormalizer.create(ast.getReturnTypes());
+ Iterator normalizedTraversal = returnNormalizer.normalize(traversal);
+ inTransaction(gts, () -> handleIterator(context, normalizedTraversal));
+ }
+
+ private void inTransaction(GraphTraversalSource gts, Runnable runnable) {
+ Graph graph = gts.getGraph();
+ boolean supportsTransactions = graph.features().graph().supportsTransactions();
+ if (!supportsTransactions) {
+ runnable.run();
+ return;
+ }
+
+ try {
+ graph.tx().open();
+ runnable.run();
+ graph.tx().commit();
+ } catch (Exception e) {
+ if (graph.tx().isOpen()) {
+ graph.tx().rollback();
+ }
+ }
+ }
+
+ private GraphTraversalSource traversal(Context context) throws OpProcessorException {
+ RequestMessage msg = context.getRequestMessage();
+ GraphManager graphManager = context.getGraphManager();
+
+ Optional<Map<String, String>> aliasesOptional = msg.optionalArgs(Tokens.ARGS_ALIASES);
+ String gAlias = aliasesOptional.map(alias -> alias.get(Tokens.VAL_TRAVERSAL_SOURCE_ALIAS))
+ .orElse(null);
+
+ if (gAlias == null) {
+ return graphManager.getGraphNames().stream()
+ .sorted()
+ .findFirst()
+ .map(graphManager::getGraph)
+ .map(Graph::traversal)
+ .orElseThrow(() -> opProcessorException(msg, "No graphs found on " +
+ "the server"));
+ }
+
+ Graph graph = graphManager.getGraph(gAlias);
+ if (graph != null) {
+ return graph.traversal();
+ }
+
+ TraversalSource traversalSource = graphManager.getTraversalSource(gAlias);
+ if (traversalSource instanceof GraphTraversalSource) {
+ return (GraphTraversalSource) traversalSource;
+ }
+
+ throw opProcessorException(msg, "Traversable alias '" + gAlias + "' not found");
+ }
+
+ private OpProcessorException opProcessorException(RequestMessage msg, String errorMessage) {
+ return new OpProcessorException(errorMessage, ResponseMessage.build(msg)
+ .code(SERVER_ERROR)
+ .statusMessage(errorMessage)
+ .create());
+ }
+
+ @Override
+ protected void handleIterator(Context context, Iterator traversal) {
+ RequestMessage msg = context.getRequestMessage();
+ final long timeout = msg.getArgs().containsKey(Tokens.ARGS_EVAL_TIMEOUT)
+ ? ((Number) msg.getArgs().get(Tokens.ARGS_EVAL_TIMEOUT)).longValue()
+ : context.getSettings().evaluationTimeout;
+
+ FutureTask<Void> evalFuture = new FutureTask<>(() -> {
+ try {
+ super.handleIterator(context, traversal);
+ } catch (Exception ex) {
+ String errorMessage = getErrorMessage(msg, ex);
+
+ logger.error("Error during traversal iteration", ex);
+ ChannelHandlerContext ctx = context.getChannelHandlerContext();
+ ctx.writeAndFlush(ResponseMessage.build(msg)
+ .code(SERVER_ERROR)
+ .statusMessage(errorMessage)
+ .statusAttributeException(ex)
+ .create());
+ }
+ return null;
+ }
+ );
+
+ final Future<?> executionFuture = context.getGremlinExecutor()
+ .getExecutorService().submit(evalFuture);
+ if (timeout > 0) {
+ context.getScheduledExecutorService().schedule(
+ () -> executionFuture.cancel(true)
+ , timeout, TimeUnit.MILLISECONDS);
+ }
+
+ }
+
+ private String getErrorMessage(RequestMessage msg, Exception ex) {
+ if (ex instanceof InterruptedException || ex instanceof TraversalInterruptedException) {
+ return String.format("A timeout occurred during traversal evaluation of [%s] " +
+ "- consider increasing the limit given to scriptEvaluationTimeout",
+ msg);
+ } else {
+ return ex.getMessage();
+ }
+ }
+
+ private void explainQuery(Context context, CypherAst ast, String gremlin) {
+ Map<String, Object> explanation = new LinkedHashMap<>();
+ explanation.put("translation", gremlin);
+ explanation.put("options", ast.getOptions().toString());
+
+ ResponseMessage explainMsg = ResponseMessage.build(context.getRequestMessage())
+ .code(ResponseStatusCode.SUCCESS)
+ .statusMessage("OK")
+ .result(singletonList(explanation))
+ .create();
+
+ ChannelHandlerContext ctx = context.getChannelHandlerContext();
+ ctx.writeAndFlush(explainMsg);
+ }
+
+ @Override
+ public void close() {
+ // do nothing = no resources to release
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map<String, Object> getParameters(Map<String, Object> args) {
+ if (args.containsKey(Tokens.ARGS_BINDINGS)) {
+ return (Map<String, Object>) args.get(Tokens.ARGS_BINDINGS);
+ } else {
+ return new HashMap<>();
+ }
+ }
+
+ private String getTranslatorDefinition(Context context) {
+ Map<String, Object> config = context.getSettings()
+ .optionalProcessor(CypherOpProcessor.class)
+ .map(p -> p.config)
+ .orElse(emptyMap());
+
+ HashSet<String> properties = new HashSet<>(config.keySet());
+ properties.remove("translatorDefinition");
+ properties.remove("translatorFeatures");
+ if (!properties.isEmpty()) {
+ throw new IllegalStateException("Unknown configuration parameters " +
+ "found for CypherOpProcessor: " + properties);
+ }
+
+ return config.getOrDefault("translatorDefinition", DEFAULT_TRANSLATOR_DEFINITION)
+ + "+" + config.getOrDefault("translatorFeatures", "");
+ }
+
+}
diff --git a/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherPlugin.java b/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherPlugin.java
new file mode 100644
index 000000000..98cf98eee
--- /dev/null
+++ b/hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherPlugin.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2018-2019 "Neo4j, Inc." [https://neo4j.com]
+ *
+ * Licensed 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.
+ */
+
+/**
+ * Description of the modifications:
+ * <p>
+ * 1) Changed the package name.
+ * <pre>
+ * org.opencypher.gremlin.server.jsr223
+ * -->
+ * org.apache.hugegraph.opencypher
+ * </pre>
+ * </p>
+ */
+
+package org.apache.hugegraph.opencypher;
+
+import org.apache.tinkerpop.gremlin.jsr223.Customizer;
+import org.apache.tinkerpop.gremlin.jsr223.DefaultImportCustomizer;
+import org.apache.tinkerpop.gremlin.jsr223.GremlinPlugin;
+import org.apache.tinkerpop.gremlin.jsr223.ImportCustomizer;
+import org.opencypher.gremlin.traversal.CustomFunctions;
+import org.opencypher.gremlin.traversal.CustomPredicate;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class CypherPlugin implements GremlinPlugin {
+
+ private static final ImportCustomizer IMPORTS =
+ DefaultImportCustomizer.build()
+ .addClassImports(CustomPredicate.class)
+ .addMethodImports(getDeclaredPublicMethods(CustomPredicate.class))
+ .addClassImports(CustomFunctions.class)
+ .addMethodImports(getDeclaredPublicMethods(CustomFunctions.class))
+ .create();
+
+ private static List<Method> getDeclaredPublicMethods(Class<?> klass) {
+ Method[] declaredMethods = klass.getDeclaredMethods();
+ return Stream.of(declaredMethods)
+ .filter(method -> Modifier.isPublic(method.getModifiers()))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public String getName() {
+ return "cypher.extra";
+ }
+
+ public static GremlinPlugin instance() {
+ return new CypherPlugin();
+ }
+
+ @Override
+ public boolean requireRestart() {
+ return true;
+ }
+
+ @Override
+ public Optional<Customizer[]> getCustomizers(String scriptEngineName) {
+ return Optional.of(new Customizer[]{IMPORTS});
+ }
+}
diff --git a/hugegraph-api/src/main/resources/META-INF/services/org.apache.tinkerpop.gremlin.jsr223.GremlinPlugin b/hugegraph-api/src/main/resources/META-INF/services/org.apache.tinkerpop.gremlin.jsr223.GremlinPlugin
new file mode 100644
index 000000000..4173c7e06
--- /dev/null
+++ b/hugegraph-api/src/main/resources/META-INF/services/org.apache.tinkerpop.gremlin.jsr223.GremlinPlugin
@@ -0,0 +1 @@
+org.apache.hugegraph.opencypher.CypherPlugin
diff --git a/hugegraph-api/src/main/resources/META-INF/services/org.apache.tinkerpop.gremlin.server.OpProcessor b/hugegraph-api/src/main/resources/META-INF/services/org.apache.tinkerpop.gremlin.server.OpProcessor
new file mode 100644
index 000000000..84b1028e9
--- /dev/null
+++ b/hugegraph-api/src/main/resources/META-INF/services/org.apache.tinkerpop.gremlin.server.OpProcessor
@@ -0,0 +1 @@
+org.apache.hugegraph.opencypher.CypherOpProcessor
diff --git a/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java b/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java
index 4a6270864..cd287c47b 100644
--- a/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java
+++ b/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java
@@ -45,6 +45,7 @@ import org.apache.hugegraph.task.TaskScheduler;
import org.apache.hugegraph.traversal.optimize.HugeCountStepStrategy;
import org.apache.hugegraph.traversal.optimize.HugeGraphStepStrategy;
import org.apache.hugegraph.traversal.optimize.HugeVertexStepStrategy;
+import org.apache.hugegraph.traversal.optimize.HugePrimaryKeyStrategy;
import org.apache.hugegraph.type.HugeType;
import org.apache.hugegraph.type.define.GraphMode;
import org.apache.hugegraph.type.define.GraphReadMode;
@@ -319,7 +320,9 @@ public interface HugeGraph extends Graph {
.clone();
strategies.addStrategies(HugeVertexStepStrategy.instance(),
HugeGraphStepStrategy.instance(),
- HugeCountStepStrategy.instance());
+ HugeCountStepStrategy.instance(),
+ HugePrimaryKeyStrategy.instance());
+
TraversalStrategies.GlobalCache.registerStrategies(clazz, strategies);
}
}
diff --git a/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java b/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java
index 7f3e12d35..910f19cdc 100644
--- a/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java
+++ b/hugegraph-core/src/main/java/org/apache/hugegraph/auth/StandardAuthManager.java
@@ -665,7 +665,12 @@ public class StandardAuthManager implements AuthManager {
Claims payload = null;
boolean needBuildCache = false;
if (username == null) {
- payload = this.tokenGenerator.verify(token);
+ try{
+ payload = this.tokenGenerator.verify(token);
+ }catch (Throwable t){
+ LOG.error(String.format("Failed to verify token:[ %s ], cause:",token),t);
+ return new UserWithRole("");
+ }
username = (String) payload.get(AuthConstant.TOKEN_USER_NAME);
needBuildCache = true;
}
diff --git a/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugePrimaryKeyStrategy.java b/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugePrimaryKeyStrategy.java
new file mode 100644
index 000000000..e00e4caf8
--- /dev/null
+++ b/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugePrimaryKeyStrategy.java
@@ -0,0 +1,107 @@
+/*
+ * 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.traversal.optimize;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy.ProviderOptimizationStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.step.Mutating;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexStartStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AddPropertyStep;
+import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
+import org.apache.tinkerpop.gremlin.structure.T;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.AddVertexStep;
+import org.apache.tinkerpop.gremlin.structure.VertexProperty.Cardinality;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class HugePrimaryKeyStrategy
+ extends AbstractTraversalStrategy<TraversalStrategy.ProviderOptimizationStrategy>
+ implements ProviderOptimizationStrategy {
+
+ private static final long serialVersionUID = 6307847098226016416L;
+ private static final HugePrimaryKeyStrategy INSTANCE = new HugePrimaryKeyStrategy();
+
+ public static HugePrimaryKeyStrategy instance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public void apply(Traversal.Admin<?, ?> traversal) {
+
+ List<Step> removeSteps = new LinkedList<>();
+ Mutating curAddStep = null;
+ List<Step> stepList = traversal.getSteps();
+
+ for (int i = 0, s = stepList.size(); i < s; i++) {
+ Step step = stepList.get(i);
+
+ if (i == 0 && AddVertexStartStep.class.isInstance(step)) {
+ curAddStep = (Mutating) step;
+ continue;
+ } else if (curAddStep == null && AddVertexStep.class.isInstance((step))) {
+ curAddStep = (Mutating) step;
+ continue;
+ }
+
+ if (curAddStep == null) continue;
+
+ if (!AddPropertyStep.class.isInstance(step)) {
+ curAddStep = null;
+ continue;
+ }
+
+ AddPropertyStep propertyStep = (AddPropertyStep) step;
+
+ if (propertyStep.getCardinality() == Cardinality.single
+ || propertyStep.getCardinality() == null) {
+
+ Object[] kvs = new Object[2];
+ List<Object> kvList = new LinkedList<>();
+
+ propertyStep.getParameters().getRaw().forEach((k, v) -> {
+ if (T.key.equals(k)) {
+ kvs[0] = v.get(0);
+ } else if (T.value.equals(k)) {
+ kvs[1] = v.get(0);
+ } else {
+ kvList.add(k.toString());
+ kvList.add(v.get(0));
+ }
+ });
+
+ curAddStep.configure(kvs);
+
+ if (kvList.size() > 0) {
+ curAddStep.configure(kvList.toArray(new Object[kvList.size()]));
+ }
+
+ removeSteps.add(step);
+ } else {
+ curAddStep = null;
+ }
+
+ }
+
+ for (Step index : removeSteps) {
+ traversal.removeStep(index);
+ }
+ }
+}
diff --git a/hugegraph-dist/release-docs/LICENSE b/hugegraph-dist/release-docs/LICENSE
index d9e5fb9fd..2c2f5f90a 100644
--- a/hugegraph-dist/release-docs/LICENSE
+++ b/hugegraph-dist/release-docs/LICENSE
@@ -224,6 +224,8 @@ hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeScriptT
hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/ProcessBasicSuite.java from https://github.com/apache/tinkerpop
hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/StructureBasicSuite.java from https://github.com/apache/tinkerpop
hugegraph-core/src/main/java/org/apache/hugegraph/backend/id/SnowflakeIdGenerator.java from https://github.com/twitter-archive/snowflake
+hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherOpProcessor.java from https://github.com/opencypher/cypher-for-gremlin
+hugegraph-api/src/main/java/org/apache/hugegraph/opencypher/CypherPlugin.java from https://github.com/opencypher/cypher-for-gremlin
========================================================================
diff --git a/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml b/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml
index 61c3517e3..dff43cb04 100644
--- a/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml
+++ b/hugegraph-dist/src/assembly/static/conf/gremlin-server.yaml
@@ -27,6 +27,10 @@ graphs: {
}
scriptEngines: {
gremlin-groovy: {
+ staticImports: [
+ org.opencypher.gremlin.process.traversal.CustomPredicates.*',
+ org.opencypher.gremlin.traversal.CustomFunctions.*
+ ],
plugins: {
org.apache.hugegraph.plugin.HugeGraphGremlinPlugin: {},
org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {},
@@ -60,9 +64,15 @@ scriptEngines: {
org.apache.hugegraph.traversal.optimize.ConditionP,
org.apache.hugegraph.traversal.optimize.Text,
org.apache.hugegraph.traversal.optimize.TraversalUtil,
- org.apache.hugegraph.util.DateUtil
+ org.apache.hugegraph.util.DateUtil,
+ org.opencypher.gremlin.traversal.CustomFunctions,
+ org.opencypher.gremlin.traversal.CustomPredicate
],
- methodImports: [java.lang.Math#*]
+ methodImports: [
+ java.lang.Math#*,
+ org.opencypher.gremlin.traversal.CustomPredicate#*,
+ org.opencypher.gremlin.traversal.CustomFunctions#*
+ ]
},
org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {
files: [scripts/empty-sample.groovy]
diff --git a/hugegraph-dist/src/assembly/static/conf/remote-objects.yaml b/hugegraph-dist/src/assembly/static/conf/remote-objects.yaml
index 55f38ab97..8ba24d00a 100644
--- a/hugegraph-dist/src/assembly/static/conf/remote-objects.yaml
+++ b/hugegraph-dist/src/assembly/static/conf/remote-objects.yaml
@@ -20,6 +20,11 @@ serializer: {
className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV1d0,
config: {
serializeResultToString: false,
- ioRegistries: [org.apache.hugegraph.io.HugeGraphIoRegistry]
+ # The duplication of HugeGraphIoRegistry is meant to fix a bug in the
+ # 'org.apache.tinkerpop.gremlin.driver.Settings:from(Configuration)' method.
+ ioRegistries: [
+ org.apache.hugegraph.io.HugeGraphIoRegistry,
+ org.apache.hugegraph.io.HugeGraphIoRegistry
+ ]
}
}
diff --git a/hugegraph-test/src/main/java/org/apache/hugegraph/api/ApiTestSuite.java b/hugegraph-test/src/main/java/org/apache/hugegraph/api/ApiTestSuite.java
index f3e037822..a5830a433 100644
--- a/hugegraph-test/src/main/java/org/apache/hugegraph/api/ApiTestSuite.java
+++ b/hugegraph-test/src/main/java/org/apache/hugegraph/api/ApiTestSuite.java
@@ -39,7 +39,8 @@ import org.apache.hugegraph.dist.RegisterUtil;
UserApiTest.class,
LoginApiTest.class,
ProjectApiTest.class,
- TraversersApiTestSuite.class
+ TraversersApiTestSuite.class,
+ CypherApiTest.class
})
public class ApiTestSuite {