diff --git a/.licenserc.yaml b/.licenserc.yaml
index ea56bb6e15..334d89b51e 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 84199011b5..ad08080e31 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 0000000000..bf43d6af44
--- /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 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 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 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 0000000000..10e92f2c78
--- /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 configurationSupplier;
+ private String userName;
+ private String password;
+ private String token;
+
+ CypherClient(String userName, String password,
+ Supplier configurationSupplier) {
+ this.userName = userName;
+ this.password = password;
+ this.configurationSupplier = configurationSupplier;
+ }
+
+ CypherClient(String token, Supplier configurationSupplier) {
+ this.token = token;
+ this.configurationSupplier = configurationSupplier;
+ }
+
+ public CypherModel submitQuery(String cypherQuery, @Nullable Map 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
+ *
+ * 2) Changed the package name.
+ *
+ * org.opencypher.gremlin.server.op.cypher
+ * -->
+ * org.apache.hugegraph.opencypher
+ *
+ *
+ *
+ * 3) Set the logger level from info to trace
+ *
+ *
+ * {@link OpProcessor} implementation for processing Cypher {@link RequestMessage}s:
+ *
+ * {
+ * "requestId": "<some UUID>",
+ * "op": "eval",
+ * "processor": "cypher",
+ * "args": { "gremlin": "<CYPHER QUERY>" }
+ * }
+ *
+ */
+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 getEvalOp() {
+ return this::evalCypher;
+ }
+
+ @Override
+ public Optional> selectOther(Context ctx) {
+ return empty();
+ }
+
+ private void evalCypher(Context context) throws OpProcessorException {
+ Map 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 parameters = ParameterNormalizer.normalize(getParameters(args));
+ ProcedureContext procedureContext = ProcedureContext.global();
+ CypherAst ast = CypherAst.parse(cypher, parameters, procedureContext.getSignatures());
+
+ String translatorDefinition = getTranslatorDefinition(context);
+
+ Translator strTranslator = Translator.builder()
+ .gremlinGroovy()
+ .build(translatorDefinition);
+
+ Translator traversalTranslator = Translator.builder()
+ .traversal(g)
+ .build(translatorDefinition);
+
+ Seq 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