
uk.gov.gchq.gaffer.rest.controller.GremlinController Maven / Gradle / Ivy
/*
* Copyright 2024 Crown Copyright
*
* 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 uk.gov.gchq.gaffer.rest.controller;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutor;
import org.apache.tinkerpop.gremlin.jsr223.ConcurrentBindings;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONMapper;
import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONVersion;
import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONWriter;
import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONXModuleV3;
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.opencypher.v9_0.util.SyntaxException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import uk.gov.gchq.gaffer.commonutil.otel.OtelUtil;
import uk.gov.gchq.gaffer.core.exception.GafferRuntimeException;
import uk.gov.gchq.gaffer.exception.SerialisationException;
import uk.gov.gchq.gaffer.jsonserialisation.JSONSerialiser;
import uk.gov.gchq.gaffer.operation.Operation;
import uk.gov.gchq.gaffer.operation.OperationChain;
import uk.gov.gchq.gaffer.rest.factory.spring.AbstractUserFactory;
import uk.gov.gchq.gaffer.tinkerpop.GafferPopGraph;
import uk.gov.gchq.gaffer.tinkerpop.GafferPopGraphVariables;
import uk.gov.gchq.gaffer.user.User;
import uk.gov.gchq.koryphe.tuple.n.Tuple2;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.MediaType.APPLICATION_NDJSON;
import static org.springframework.http.MediaType.APPLICATION_NDJSON_VALUE;
import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;
@RestController
@Tag(name = "gremlin")
@RequestMapping("/rest/gremlin")
public class GremlinController {
/**
* The mapper for converting to GraphSONv3.
*/
public static final GraphSONMapper GRAPHSON_V3_MAPPER = GraphSONMapper.build()
.version(GraphSONVersion.V3_0)
.addCustomModule(GraphSONXModuleV3.build()).create();
/**
* Writer for writing GraphSONv3 output to output streams.
*/
public static final GraphSONWriter GRAPHSON_V3_WRITER = GraphSONWriter.build()
.mapper(GRAPHSON_V3_MAPPER)
.wrapAdjacencyList(true).create();
// Keys for response explain JSON
public static final String EXPLAIN_OVERVIEW_KEY = "overview";
public static final String EXPLAIN_OP_CHAIN_KEY = "chain";
public static final String EXPLAIN_GREMLIN_KEY = "gremlin";
private static final Logger LOGGER = LoggerFactory.getLogger(GremlinController.class);
private static final String GENERAL_ERROR_MSG = "Failed to evaluate Gremlin query: ";
private final ConcurrentBindings bindings = new ConcurrentBindings();
private final ExecutorService executorService = Context.taskWrapping(Executors.newFixedThreadPool(4));
private final Long requestTimeout;
private final AbstractUserFactory userFactory;
private final Graph graph;
private final Map> plugins = new HashMap<>();
@Autowired
public GremlinController(final GraphTraversalSource g, final AbstractUserFactory userFactory, final Long requestTimeout) {
bindings.putIfAbsent("g", g);
graph = g.getGraph();
this.userFactory = userFactory;
this.requestTimeout = requestTimeout;
// Add cypher plugin so cypher functions can be used in queries
plugins.put(CypherPlugin.class.getName(), new HashMap<>());
}
/**
* Explains what Gaffer operations are run for a given gremlin query.
*
* @param httpHeaders The request headers.
* @param gremlinQuery The gremlin groovy query.
* @return JSON response with explanation in.
*/
@PostMapping(path = "/explain", consumes = TEXT_PLAIN_VALUE, produces = APPLICATION_JSON_VALUE)
@io.swagger.v3.oas.annotations.Operation(
summary = "Explain a Gremlin Query",
description = "Runs a Gremlin query and outputs an explanation of what Gaffer operations were executed on the graph")
public String explain(@RequestHeader final HttpHeaders httpHeaders, @RequestBody final String gremlinQuery) {
preExecuteSetUp(httpHeaders);
return runGremlinQuery(gremlinQuery).get1().toString();
}
/**
* Endpoint for running a gremlin groovy query, will respond with an output
* stream of GraphSONv3 JSON.
*
* @param httpHeaders The request headers.
* @param gremlinQuery The gremlin groovy query.
* @return A response output stream of GraphSONv3.
*
* @throws IOException If issue writing output.
*/
@PostMapping(path = "/execute", consumes = TEXT_PLAIN_VALUE, produces = APPLICATION_NDJSON_VALUE)
@io.swagger.v3.oas.annotations.Operation(
summary = "Run a Gremlin Query",
description = "Runs a Gremlin Groovy script and outputs the result as GraphSONv3 JSON")
public ResponseEntity execute(
@RequestHeader final HttpHeaders httpHeaders,
@RequestBody final String gremlinQuery) throws IOException {
// Set up
preExecuteSetUp(httpHeaders);
HttpStatus status = HttpStatus.OK;
StreamingResponseBody responseBody;
try {
Object result = runGremlinQuery(gremlinQuery).get0();
// Write to output stream for response
responseBody = os -> GRAPHSON_V3_WRITER.writeObject(os, result);
} catch (final GafferRuntimeException e) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
responseBody = os -> os.write(
new JSONObject().put("simpleMessage", e.getMessage()).toString().getBytes(StandardCharsets.UTF_8));
}
return ResponseEntity.status(status)
.contentType(APPLICATION_NDJSON)
.body(responseBody);
}
/**
* Explains what Gaffer operations are ran for a given cypher query,
* will translate to Gremlin using {@link CypherAst} before executing.
*
* @param httpHeaders The request headers.
* @param cypherQuery Opencypher query.
* @return JSON response with explanation in.
*/
@PostMapping(path = "/cypher/explain", consumes = TEXT_PLAIN_VALUE, produces = APPLICATION_JSON_VALUE)
@io.swagger.v3.oas.annotations.Operation(
summary = "Explain a Cypher Query Executed via Gremlin",
description = "Translates a Cypher query to Gremlin and outputs an explanation of what Gaffer operations" +
"were executed on the graph, note will always append a '.toList()' to the translation")
public String cypherExplain(@RequestHeader final HttpHeaders httpHeaders, @RequestBody final String cypherQuery) {
final CypherAst ast = CypherAst.parse(cypherQuery);
// 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()";
JSONObject response = runGremlinQuery(translation).get1();
response.put(EXPLAIN_GREMLIN_KEY, translation);
return response.toString();
}
/**
* Endpoint for running a cypher query through gremlin, will respond with an
* output stream of GraphSONv3 JSON.
*
* @param httpHeaders The request headers.
* @param cypherQuery The cypher query.
* @return The output stream of GraphSONv3.
*
* @throws IOException If issue writing output.
*/
@PostMapping(path = "/cypher/execute", consumes = TEXT_PLAIN_VALUE, produces = APPLICATION_NDJSON_VALUE)
@io.swagger.v3.oas.annotations.Operation(
summary = "Run a Cypher Query",
description = "Translates a Cypher query to Gremlin and executes it returning a GraphSONv3 JSON result." +
"Note will always append a '.toList()' to the translation")
public ResponseEntity cypherExecute(
@RequestHeader final HttpHeaders httpHeaders,
@RequestBody final String cypherQuery) throws IOException {
// Set up
preExecuteSetUp(httpHeaders);
HttpStatus status = HttpStatus.OK;
StreamingResponseBody responseBody;
try {
final CypherAst ast = CypherAst.parse(cypherQuery);
// 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()";
// Run Query
Object result = runGremlinQuery(translation).get0();
// Write to output stream for response
responseBody = os -> GRAPHSON_V3_WRITER.writeObject(os, result);
} catch (final GafferRuntimeException | SyntaxException e) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
responseBody = os -> os.write(
new JSONObject().put("simpleMessage", e.getMessage()).toString().getBytes(StandardCharsets.UTF_8));
}
return ResponseEntity.status(status)
.contentType(APPLICATION_NDJSON)
.body(responseBody);
}
/**
* Gets an explanation of the last chain of operations ran on a GafferPop graph.
* This essentially shows how a Gremlin query mapped to a Gaffer operation
* chain.
* Note due to how Gaffer maps to Tinkerpop some filtering steps in the Gremlin
* query may be absent from the operation chains in the explain as it may have
* been done in the Tinkerpop framework instead.
*
* @param graph The GafferPop graph
* @return A JSON payload with an overview and full JSON representation of the
* chain in.
*/
public static JSONObject getGafferPopExplanation(final GafferPopGraph graph) {
JSONObject result = new JSONObject();
// Get the last operation chain that ran
LinkedList operations = new LinkedList<>();
((GafferPopGraphVariables) graph.variables())
.getLastOperationChain()
.getOperations()
.forEach(op -> {
if (op instanceof OperationChain) {
operations.addAll(((OperationChain) op).flatten());
} else {
operations.add(op);
}
});
OperationChain> flattenedChain = new OperationChain<>(operations);
String overview = flattenedChain.toOverviewString();
result.put(EXPLAIN_OVERVIEW_KEY, overview);
try {
result.put(EXPLAIN_OP_CHAIN_KEY, new JSONObject(new String(JSONSerialiser.serialise(flattenedChain), StandardCharsets.UTF_8)));
} catch (final SerialisationException e) {
result.put(EXPLAIN_OP_CHAIN_KEY, "FAILED TO SERIALISE OPERATION CHAIN");
}
return result;
}
/**
* Do some basic pre execute set up so the graph is ready for the gremlin
* request to be executed.
*
* @param httpHeaders Headers for user auth
*/
private void preExecuteSetUp(final HttpHeaders httpHeaders) {
// Check we actually have a graph instance to use
GafferPopGraph gafferPopGraph;
if (graph instanceof EmptyGraph) {
throw new GafferRuntimeException("There is no GafferPop Graph configured");
} else {
gafferPopGraph = (GafferPopGraph) graph;
}
gafferPopGraph.setDefaultVariables((GafferPopGraphVariables) gafferPopGraph.variables());
// Hooks for user auth
userFactory.setHttpHeaders(httpHeaders);
graph.variables().set(GafferPopGraphVariables.USER, userFactory.createUser());
}
/**
* Executes a given Gremlin query and returns the result along with an explanation.
*
* @param gremlinQuery The Gremlin groovy query.
* @return A pair tuple with result and explain in.
*/
private Tuple2
© 2015 - 2025 Weber Informatics LLC | Privacy Policy