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": "&lt;some UUID&gt;",
+ *   "op": "eval",
+ *   "processor": "cypher",
+ *   "args": { "gremlin": "&lt;CYPHER QUERY&gt;" }
+ * }
+ * </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 {