Skip to content

Commit

Permalink
make sure cypher types can be used correctly
Browse files Browse the repository at this point in the history
  • Loading branch information
tb06904 committed Jul 25, 2024
1 parent ff84d77 commit 70580e2
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 4 deletions.
2 changes: 0 additions & 2 deletions library/tinkerpop/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@
<properties>
<google-guice.version>4.2.3</google-guice.version>
<cucumber.version>7.15.0</cucumber.version>
<!-- Note later versions require scala version 2.12.x -->
<cypher-gremlin.version>1.0.0</cypher-gremlin.version>

<!-- Don't run any of the standard tinkerpop tests by default-->
<test>!GafferPopGraphStructureStandardTest,!GafferPopFeatureTest,!GafferPopGraphProcessStandardTest</test>
Expand Down
2 changes: 2 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
<mockserver.version>5.15.0</mockserver.version>
<opentelemetry.version>1.36.0</opentelemetry.version>
<tinkerpop.version>3.7.1</tinkerpop.version>
<!-- Note later versions require scala version 2.12.x -->
<cypher-gremlin.version>1.0.0</cypher-gremlin.version>

<!-- Maven plugins -->
<checkstyle.plugin.version>2.17</checkstyle.plugin.version>
Expand Down
5 changes: 5 additions & 0 deletions rest-api/spring-rest/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@
<artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.opencypher.gremlin</groupId>
<artifactId>cypher-gremlin-server-plugin</artifactId>
<version>${cypher-gremlin.version}</version>
</dependency>

<!-- Test dependencies -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph;
import org.json.JSONObject;
import org.opencypher.gremlin.server.jsr223.CypherPlugin;
import org.opencypher.gremlin.translation.CypherAst;
import org.opencypher.gremlin.translation.translator.Translator;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -43,7 +44,9 @@
import uk.gov.gchq.gaffer.tinkerpop.GafferPopGraphVariables;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;
Expand All @@ -61,12 +64,15 @@ public class GremlinController {
private final ConcurrentBindings bindings = new ConcurrentBindings();
private final AbstractUserFactory userFactory;
private final Graph graph;
private final Map<String, Map<String, Object>> plugins = new HashMap<>();

@Autowired
public GremlinController(final GraphTraversalSource g, final AbstractUserFactory userFactory) {
bindings.putIfAbsent("g", g);
graph = g.getGraph();
this.userFactory = userFactory;
// Add cypher plugin so cypher functions can be used in queries
plugins.put(CypherPlugin.class.getName(), new HashMap<>());
}

/**
Expand Down Expand Up @@ -103,6 +109,8 @@ public String cypherExplain(@RequestHeader final HttpHeaders httpHeaders, @Reque
// Translate the cypher to gremlin, always add a .toList() otherwise Gremlin wont execute it as its lazy
final String translation = ast.buildTranslation(Translator.builder().gremlinGroovy().enableCypherExtensions().build()) + ".toList()";

System.out.println(translation);

JSONObject response = runGremlinAndGetExplain(translation, httpHeaders);
response.put(EXPLAIN_GREMLIN_KEY, translation);
return response.toString();
Expand Down Expand Up @@ -163,13 +171,15 @@ private JSONObject runGremlinAndGetExplain(final String gremlinQuery, final Http
} else {
gafferPopGraph = (GafferPopGraph) graph;
}
gafferPopGraph.setDefaultVariables((GafferPopGraphVariables) gafferPopGraph.variables());
// Hooks for user auth
userFactory.setHttpHeaders(httpHeaders);
graph.variables().set(GafferPopGraphVariables.USER, userFactory.createUser());

JSONObject explain = new JSONObject();
try (GremlinExecutor gremlinExecutor = GremlinExecutor.build().globalBindings(bindings).create()) {
gafferPopGraph.setDefaultVariables((GafferPopGraphVariables) gafferPopGraph.variables());
try (GremlinExecutor gremlinExecutor = GremlinExecutor.build()
.addPlugins("gremlin-groovy", plugins)
.globalBindings(bindings).create()) {
// Execute the query note this will actually run the query which we need
// as Gremlin will skip steps if there is no input from the previous ones
gremlinExecutor.eval(gremlinQuery).join();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.apache.tinkerpop.gremlin.util.ser.MessageTextSerializer;
import org.apache.tinkerpop.gremlin.util.ser.SerTokens;
import org.json.JSONObject;
import org.opencypher.gremlin.server.jsr223.CypherPlugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.BinaryMessage;
Expand All @@ -57,6 +58,7 @@
import java.nio.charset.StandardCharsets;
import java.util.AbstractMap.SimpleEntry;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
Expand Down Expand Up @@ -88,6 +90,7 @@ public class GremlinWebSocketHandler extends BinaryWebSocketHandler {
private final ConcurrentBindings bindings = new ConcurrentBindings();
private final AbstractUserFactory userFactory;
private final Graph graph;
private final Map<String, Map<String, Object>> plugins = new HashMap<>();

/**
* Constructor
Expand All @@ -99,6 +102,8 @@ public GremlinWebSocketHandler(final GraphTraversalSource g, final AbstractUserF
bindings.putIfAbsent("g", g);
graph = g.getGraph();
this.userFactory = userFactory;
// Add cypher plugin so cypher functions can be used in queries
plugins.put(CypherPlugin.class.getName(), new HashMap<>());
}

@Override
Expand Down Expand Up @@ -143,6 +148,7 @@ private ResponseMessage handleGremlinRequest(final WebSocketSession session, fin
try (Scope scope = span.makeCurrent();
GremlinExecutor gremlinExecutor = GremlinExecutor.build()
.globalBindings(bindings)
.addPlugins("gremlin-groovy", plugins)
.executorService(executorService)
.create()) {
// Set current headers for potential authorisation then set the user
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import uk.gov.gchq.gaffer.operation.impl.Limit;
import uk.gov.gchq.gaffer.operation.impl.get.GetAllElements;
import uk.gov.gchq.gaffer.operation.impl.get.GetElements;
import uk.gov.gchq.gaffer.rest.factory.spring.AbstractUserFactory;
import uk.gov.gchq.gaffer.rest.factory.spring.UnknownUserFactory;
Expand Down Expand Up @@ -125,6 +127,7 @@ void shouldRejectMalformedGremlinQuery() throws Exception {
void shouldReturnExplainOfValidCypherQuery() throws Exception {
// Given
String cypherString = "MATCH (p:person) WHERE ID(p) = '" + MARKO.getId() + "' RETURN p";

List<String> expectedOperations = Arrays.asList(GetElements.class.getName());

// When
Expand Down Expand Up @@ -152,6 +155,38 @@ void shouldReturnExplainOfValidCypherQuery() throws Exception {
.containsExactlyElementsOf(expectedOperations);
}

@Test
void shouldReturnExplainOfCypherQueryWithExtensions() throws Exception {
// Given (uses the toInteger custom function)
String cypherString = "MATCH (p:person) WHERE p.age > toInteger(22) RETURN p";

List<String> expectedOperations = Arrays.asList(GetAllElements.class.getName(), Limit.class.getName());

// When
MvcResult result = mockMvc
.perform(MockMvcRequestBuilders
.post(CYPHER_EXPLAIN_ENDPOINT)
.content(cypherString)
.contentType(TEXT_PLAIN_VALUE))
.andReturn();

// Then
// Ensure OK response
assertThat(result.getResponse().getStatus()).isEqualTo(200);

// Get and check response
JSONObject jsonResponse = new JSONObject(result.getResponse().getContentAsString());
assertThat(jsonResponse.has(GremlinController.EXPLAIN_OVERVIEW_KEY)).isTrue();
assertThat(jsonResponse.has(GremlinController.EXPLAIN_OP_CHAIN_KEY)).isTrue();
assertThat(jsonResponse.has(GremlinController.EXPLAIN_GREMLIN_KEY)).isTrue();

// Check the operations that ran are as expected
JSONArray operations = jsonResponse.getJSONObject("chain").getJSONArray("operations");
assertThat(operations)
.map(json -> ((JSONObject) json).getString("class"))
.containsExactlyElementsOf(expectedOperations);
}

@Test
void shouldRejectMalformedCypherQuery() throws Exception {
// Given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,22 @@ void shouldAcceptQueryWithCypher() {
.containsExactly(MARKO.getName());
}

@Test
void shouldAcceptGremlinQueryUsingCustomCypherFunctions() {
// Given
String query = "g.V().hasLabel('person').values('age').map(cypherToString()).toList()";

// When
List<Result> results = client.submit(query).stream().collect(Collectors.toList());

// Then
assertThat(results)
.map(result -> result.getObject())
.containsExactlyInAnyOrder(
String.valueOf(MARKO.getAge()),
String.valueOf(VADAS.getAge()),
String.valueOf(PETER.getAge()),
String.valueOf(JOSH.getAge()));
}

}

0 comments on commit 70580e2

Please sign in to comment.