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 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 doQueryList(Client client, RequestMessage request) + throws ExecutionException, InterruptedException { + ResultSet results = client.submitAsync(request).get(); + + Iterator iter = results.iterator(); + List 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 0000000000..519ca66d9e --- /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 0000000000..03cbdd6036 --- /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 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 attributes = Collections.EMPTY_MAP; + } + + private static class Result { + public List data; + public Map 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 24c39a69fd..0000000000 --- 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 244bfd8485..48e74225f0 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 @@ 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.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 AuthManager authManager() { @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 static void initAdminUserIfNeeded(String confFile) throws Exception { 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 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}authzIdauthnIdpassword{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 0000000000..a4dfff60a4 --- /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: + *

+ * 1) Changed the method signature to adopt the gremlin-server 3.5.1. + *

+ * public Optional> selectOther(RequestMessage requestMessage)
+ * -->
+ * public Optional> selectOther(Context ctx)
+ * 
+ *

+ *

+ * 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> 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 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 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 getParameters(Map args) { + if (args.containsKey(Tokens.ARGS_BINDINGS)) { + return (Map) args.get(Tokens.ARGS_BINDINGS); + } else { + return new HashMap<>(); + } + } + + private String getTranslatorDefinition(Context context) { + Map config = context.getSettings() + .optionalProcessor(CypherOpProcessor.class) + .map(p -> p.config) + .orElse(emptyMap()); + + HashSet 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 0000000000..98cf98eeef --- /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: + *

+ * 1) Changed the package name. + *

+ * org.opencypher.gremlin.server.jsr223
+ * -->
+ * org.apache.hugegraph.opencypher
+ * 
+ *

+ */ + +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 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 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 0000000000..4173c7e065 --- /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 0000000000..84b1028e9b --- /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 4a62708646..cd287c47be 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.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 @@ static void registerTraversalStrategies(Class clazz) { .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 7f3e12d35e..910f19cdc5 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 UserWithRole validateUser(String token) { 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 0000000000..e00e4caf80 --- /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 + 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 removeSteps = new LinkedList<>(); + Mutating curAddStep = null; + List 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 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 d9e5fb9fd7..2c2f5f90a9 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 61c3517e39..dff43cb04b 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 55f38ab97d..8ba24d00a1 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 f3e0378224..a5830a4336 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 @@ UserApiTest.class, LoginApiTest.class, ProjectApiTest.class, - TraversersApiTestSuite.class + TraversersApiTestSuite.class, + CypherApiTest.class }) public class ApiTestSuite {