You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by va...@apache.org on 2023/11/15 22:35:40 UTC
(tinkerpop) branch 3.6-dev updated: Bytecode support for HTTP requests (#2336)
This is an automated email from the ASF dual-hosted git repository.
valentyn pushed a commit to branch 3.6-dev
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/3.6-dev by this push:
new 8365421f25 Bytecode support for HTTP requests (#2336)
8365421f25 is described below
commit 8365421f2543335b043578096944e2b076e2b867
Author: Valentyn Kahamlyk <vk...@users.noreply.github.com>
AuthorDate: Wed Nov 15 14:35:35 2023 -0800
Bytecode support for HTTP requests (#2336)
Added Gremlin bytecode support for HTTP requests for Java and Python GLV's without translation to Gremlin script.
Added serialization for entire RequestMessage.
Payload type distinguished by Content-Type header .
---
.../driver/handler/HttpGremlinRequestEncoder.java | 44 ++----
.../driver/handler/HttpGremlinResponseDecoder.java | 8 +-
.../ser/GraphSONMessageSerializerV3d0Test.java | 48 +++++-
.../binary/GraphBinaryMessageSerializerV1Test.java | 27 +++-
.../gremlin/groovy/engine/GremlinExecutor.java | 86 +++++++++--
.../gremlin_python/driver/aiohttp/transport.py | 4 +-
.../main/python/gremlin_python/driver/protocol.py | 32 +---
.../driver/test_driver_remote_connection_http.py | 19 +--
.../conf/gremlin-server-rest-modern.yaml | 5 +-
.../server/handler/HttpGremlinEndpointHandler.java | 66 +++++----
.../gremlin/server/handler/HttpHandlerUtil.java | 55 ++++++-
.../server/util/TextPlainMessageSerializer.java | 7 +-
.../server/GremlinServerHttpIntegrateTest.java | 20 ++-
.../gremlin/server/HttpDriverIntegrateTest.java | 31 +++-
.../server/handler/HttpHandlerUtilTest.java | 165 +++++++++++++++++++++
15 files changed, 468 insertions(+), 149 deletions(-)
diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinRequestEncoder.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinRequestEncoder.java
index 2c271cede8..bdf0f544af 100644
--- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinRequestEncoder.java
+++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinRequestEncoder.java
@@ -28,15 +28,12 @@ import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import org.apache.tinkerpop.gremlin.driver.MessageSerializer;
-import org.apache.tinkerpop.gremlin.driver.Tokens;
import org.apache.tinkerpop.gremlin.driver.exception.ResponseException;
import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode;
+import org.apache.tinkerpop.gremlin.driver.ser.SerTokens;
import org.apache.tinkerpop.gremlin.process.traversal.Bytecode;
-import org.apache.tinkerpop.gremlin.process.traversal.translator.GroovyTranslator;
-import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
-import java.util.HashMap;
import java.util.List;
import java.util.function.UnaryOperator;
@@ -47,8 +44,6 @@ import java.util.function.UnaryOperator;
public final class HttpGremlinRequestEncoder extends MessageToMessageEncoder<RequestMessage> {
private final MessageSerializer<?> serializer;
- private static final ObjectMapper mapper = new ObjectMapper();
-
private final UnaryOperator<FullHttpRequest> interceptor;
public HttpGremlinRequestEncoder(final MessageSerializer<?> serializer, final UnaryOperator<FullHttpRequest> interceptor) {
@@ -58,29 +53,22 @@ public final class HttpGremlinRequestEncoder extends MessageToMessageEncoder<Req
@Override
protected void encode(final ChannelHandlerContext channelHandlerContext, final RequestMessage requestMessage, final List<Object> objects) throws Exception {
- try {
- final String gremlin;
- final Object gremlinStringOrBytecode = requestMessage.getArg(Tokens.ARGS_GREMLIN);
+ final String mimeType = serializer.mimeTypesSupported()[0];
+ // only GraphSON3 and GraphBinary recommended for serialization of Bytecode requests
+ if (requestMessage.getArg("gremlin") instanceof Bytecode &&
+ !mimeType.equals(SerTokens.MIME_GRAPHSON_V3D0) &&
+ !mimeType.equals(SerTokens.MIME_GRAPHBINARY_V1D0)) {
+ throw new ResponseException(ResponseStatusCode.REQUEST_ERROR_SERIALIZATION, String.format(
+ "An error occurred during serialization of this request [%s] - it could not be sent to the server - Reason: only GraphSON3 and GraphBinary recommended for serialization of Bytecode requests, but used %s",
+ requestMessage, serializer.getClass().getName()));
+ }
- // the gremlin key can contain a Gremlin script or bytecode. if it's bytecode we can't submit it over
- // http as such. it has to be converted to a script and we can do that with the Groovy translator.
- final boolean usesBytecode = gremlinStringOrBytecode instanceof Bytecode;
- if (usesBytecode) {
- gremlin = GroovyTranslator.of("g").translate((Bytecode) gremlinStringOrBytecode).getScript();
- } else {
- gremlin = gremlinStringOrBytecode.toString();
- }
- final byte[] payload = mapper.writeValueAsBytes(new HashMap<String,Object>() {{
- put(Tokens.ARGS_GREMLIN, gremlin);
- put(Tokens.REQUEST_ID, requestMessage.getRequestId());
- if (usesBytecode) put("op", Tokens.OPS_BYTECODE);
- }});
- final ByteBuf bb = channelHandlerContext.alloc().buffer(payload.length);
- bb.writeBytes(payload);
- final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/", bb);
- request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/json");
- request.headers().add(HttpHeaderNames.CONTENT_LENGTH, payload.length);
- request.headers().add(HttpHeaderNames.ACCEPT, serializer.mimeTypesSupported()[0]);
+ try {
+ final ByteBuf buffer = serializer.serializeRequestAsBinary(requestMessage, channelHandlerContext.alloc());
+ final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/", buffer);
+ request.headers().add(HttpHeaderNames.CONTENT_TYPE, mimeType);
+ request.headers().add(HttpHeaderNames.CONTENT_LENGTH, buffer.readableBytes());
+ request.headers().add(HttpHeaderNames.ACCEPT, mimeType);
objects.add(interceptor.apply(request));
} catch (Exception ex) {
diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinResponseDecoder.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinResponseDecoder.java
index 24ffc39de3..57a549f77f 100644
--- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinResponseDecoder.java
+++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinResponseDecoder.java
@@ -45,12 +45,6 @@ public final class HttpGremlinResponseDecoder extends MessageToMessageDecoder<Fu
@Override
protected void decode(final ChannelHandlerContext channelHandlerContext, final FullHttpResponse httpResponse, final List<Object> objects) throws Exception {
- final String content = httpResponse.content().toString(CharsetUtil.UTF_8);
-
- if (!(serializer instanceof MessageTextSerializer))
- throw new IllegalStateException(String.format("%s must be of type %s to decode responses from HTTP endpoints",
- serializer.getClass().getSimpleName(), HttpGremlinResponseDecoder.class.getSimpleName()));
-
- objects.add(((MessageTextSerializer) serializer).deserializeResponse(content));
+ objects.add(serializer.deserializeResponse(httpResponse.content()));
}
}
diff --git a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ser/GraphSONMessageSerializerV3d0Test.java b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ser/GraphSONMessageSerializerV3d0Test.java
index 701e2cdaa3..d6d0232639 100644
--- a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ser/GraphSONMessageSerializerV3d0Test.java
+++ b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ser/GraphSONMessageSerializerV3d0Test.java
@@ -21,7 +21,10 @@ package org.apache.tinkerpop.gremlin.driver.ser;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.UnpooledByteBufAllocator;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph;
import org.apache.tinkerpop.gremlin.driver.MessageSerializer;
+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;
@@ -38,9 +41,9 @@ import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory;
import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
import org.apache.tinkerpop.shaded.jackson.databind.JsonMappingException;
-import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
import org.junit.Test;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@@ -49,6 +52,9 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
+import static org.apache.tinkerpop.gremlin.driver.MockitoHamcrestMatcherAdapter.reflectionEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItemInArray;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -67,7 +73,7 @@ public class GraphSONMessageSerializerV3d0Test {
private final ResponseMessage.Builder responseMessageBuilder = ResponseMessage.build(requestId);
private final static ByteBufAllocator allocator = UnpooledByteBufAllocator.DEFAULT;
- public final MessageSerializer<ObjectMapper> serializer = new GraphSONMessageSerializerV3d0();
+ public final GraphSONMessageSerializerV3d0 serializer = new GraphSONMessageSerializerV3d0();
@Test
public void shouldSerializeIterable() throws Exception {
@@ -80,8 +86,8 @@ public class GraphSONMessageSerializerV3d0Test {
final List<Integer> deserializedFunList = (List<Integer>) response.getResult().getData();
assertEquals(2, deserializedFunList.size());
- assertEquals(new Integer(1), deserializedFunList.get(0));
- assertEquals(new Integer(100), deserializedFunList.get(1));
+ assertEquals(Integer.valueOf(1), deserializedFunList.get(0));
+ assertEquals(Integer.valueOf(100), deserializedFunList.get(1));
}
@Test
@@ -96,9 +102,9 @@ public class GraphSONMessageSerializerV3d0Test {
final List<Integer> deserializedFunList = (List<Integer>) response.getResult().getData();
assertEquals(3, deserializedFunList.size());
- assertEquals(new Integer(1), deserializedFunList.get(0));
+ assertEquals(Integer.valueOf(1), deserializedFunList.get(0));
assertNull(deserializedFunList.get(1));
- assertEquals(new Integer(100), deserializedFunList.get(2));
+ assertEquals(Integer.valueOf(100), deserializedFunList.get(2));
}
@Test
@@ -221,7 +227,7 @@ public class GraphSONMessageSerializerV3d0Test {
v.property(VertexProperty.Cardinality.single, "friends", friends);
- final List list = IteratorUtils.list(graph.vertices());
+ final List<Vertex> list = IteratorUtils.list(graph.vertices());
final ResponseMessage response = convert(list);
assertCommon(response);
@@ -263,7 +269,7 @@ public class GraphSONMessageSerializerV3d0Test {
final Map<Vertex, Integer> deserializedMap = (Map<Vertex, Integer>) response.getResult().getData();
assertEquals(1, deserializedMap.size());
- assertEquals(new Integer(1000), deserializedMap.get(v1));
+ assertEquals(Integer.valueOf(1000), deserializedMap.get(v1));
}
@Test
@@ -346,6 +352,32 @@ public class GraphSONMessageSerializerV3d0Test {
assertNotNull(m.getArgs());
}
+ @Test
+ public void shouldSerializeAndDeserializeRequest() throws Exception {
+ final GraphTraversalSource g = EmptyGraph.instance().traversal();
+ final Traversal.Admin t = g.V().hasLabel("person").out().asAdmin();
+
+ final Map<String, String> aliases = new HashMap<>();
+ aliases.put("g","g");
+
+ final RequestMessage request = RequestMessage.build(Tokens.OPS_BYTECODE)
+ .processor("traversal")
+ .overrideRequestId(UUID.randomUUID())
+ .addArg(Tokens.ARGS_GREMLIN, t.getBytecode())
+ .addArg(Tokens.ARGS_ALIASES, aliases)
+ .create();
+
+ final ByteBuf buffer = serializer.serializeRequestAsBinary(request, allocator);
+ final int mimeLen = buffer.readByte();
+ final byte[] bytes = new byte[mimeLen];
+ buffer.readBytes(bytes);
+ final String mimeType = new String(bytes, StandardCharsets.UTF_8);
+
+ final RequestMessage deserialized = serializer.deserializeRequest(buffer);
+ assertThat(request, reflectionEquals(deserialized));
+ assertThat(serializer.mimeTypesSupported(), hasItemInArray(mimeType));
+ }
+
@Test
public void shouldRegisterGremlinServerModuleAutomaticallyWithMapper() throws SerializationException {
GraphSONMapper.Builder builder = GraphSONMapper.build().addCustomModule(GraphSONXModuleV3d0.build().create(false));
diff --git a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ser/binary/GraphBinaryMessageSerializerV1Test.java b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ser/binary/GraphBinaryMessageSerializerV1Test.java
index 052bfce456..dd9af34979 100644
--- a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ser/binary/GraphBinaryMessageSerializerV1Test.java
+++ b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ser/binary/GraphBinaryMessageSerializerV1Test.java
@@ -20,14 +20,19 @@ package org.apache.tinkerpop.gremlin.driver.ser.binary;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
+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.driver.ser.GraphBinaryMessageSerializerV1;
import org.apache.tinkerpop.gremlin.driver.ser.SerializationException;
+import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.structure.io.binary.TypeSerializerRegistry;
+import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph;
import org.junit.Test;
+import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -35,8 +40,9 @@ import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static org.apache.tinkerpop.gremlin.driver.MockitoHamcrestMatcherAdapter.reflectionEquals;
-import static org.junit.Assert.assertEquals;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItemInArray;
+import static org.junit.Assert.assertEquals;
public class GraphBinaryMessageSerializerV1Test {
private final ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
@@ -44,17 +50,28 @@ public class GraphBinaryMessageSerializerV1Test {
@Test
public void shouldSerializeAndDeserializeRequest() throws SerializationException {
- final RequestMessage request = RequestMessage.build("op1")
- .processor("proc1")
+ final GraphTraversalSource g = EmptyGraph.instance().traversal();
+ final Traversal.Admin t = g.V().hasLabel("person").out().asAdmin();
+
+ final Map<String, String> aliases = new HashMap<>();
+ aliases.put("g","g");
+
+ final RequestMessage request = RequestMessage.build(Tokens.OPS_BYTECODE)
+ .processor("traversal")
.overrideRequestId(UUID.randomUUID())
- .addArg("arg1", "value1")
+ .addArg(Tokens.ARGS_GREMLIN, t.getBytecode())
+ .addArg(Tokens.ARGS_ALIASES, aliases)
.create();
final ByteBuf buffer = serializer.serializeRequestAsBinary(request, allocator);
final int mimeLen = buffer.readByte();
- buffer.readBytes(new byte[mimeLen]);
+ final byte[] bytes = new byte[mimeLen];
+ buffer.readBytes(bytes);
+ final String mimeType = new String(bytes, StandardCharsets.UTF_8);
+
final RequestMessage deserialized = serializer.deserializeRequest(buffer);
assertThat(request, reflectionEquals(deserialized));
+ assertThat(serializer.mimeTypesSupported(), hasItemInArray(mimeType));
}
@Test
diff --git a/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java b/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java
index 2884043aeb..9010578e53 100644
--- a/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java
+++ b/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/engine/GremlinExecutor.java
@@ -29,7 +29,9 @@ import org.apache.tinkerpop.gremlin.jsr223.GremlinScriptEngine;
import org.apache.tinkerpop.gremlin.jsr223.GremlinScriptEngineManager;
import org.apache.tinkerpop.gremlin.process.traversal.Bytecode;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
+import org.apache.tinkerpop.gremlin.process.traversal.util.BytecodeHelper;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalInterruptedException;
+import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -182,7 +184,7 @@ public class GremlinExecutor implements AutoCloseable {
* @param boundVars the bindings to evaluate in the context of the script
*/
public CompletableFuture<Object> eval(final String script, final String language, final Bindings boundVars) {
- return eval(script, language, boundVars, null, null);
+ return eval(script, language, boundVars, null, null, null);
}
/**
@@ -190,14 +192,30 @@ public class GremlinExecutor implements AutoCloseable {
* result after script evaluates but before transaction commit and before the returned {@link CompletableFuture}
* is completed.
*
- * @param script the script to evaluate
+ * @param gremlin the script or bytecode to evaluate
* @param language the language to evaluate it in
* @param boundVars the bindings to evaluate in the context of the script
* @param transformResult a {@link Function} that transforms the result - can be {@code null}
*/
- public CompletableFuture<Object> eval(final String script, final String language, final Map<String, Object> boundVars,
+ public CompletableFuture<Object> eval(final Object gremlin, final String language, final Map<String, Object> boundVars,
final Function<Object, Object> transformResult) {
- return eval(script, language, new SimpleBindings(boundVars), transformResult, null);
+ return eval(gremlin, language, new SimpleBindings(boundVars), transformResult, null);
+ }
+
+ /**
+ * Evaluate a script and allow for the submission of a transform {@link Function} that will transform the
+ * result after script evaluates but before transaction commit and before the returned {@link CompletableFuture}
+ * is completed.
+ *
+ * @param gremlin the script or bytecode to evaluate
+ * @param language the language to evaluate it in
+ * @param boundVars the bindings to evaluate in the context of the script
+ * @param timeOut optional override for evaluation timeout
+ * @param transformResult a {@link Function} that transforms the result - can be {@code null}
+ */
+ public CompletableFuture<Object> eval(final Object gremlin, final String language, final Map<String, Object> boundVars,
+ final Long timeOut, final Function<Object, Object> transformResult) {
+ return eval(gremlin, language, new SimpleBindings(boundVars), timeOut, transformResult, null);
}
/**
@@ -222,34 +240,54 @@ public class GremlinExecutor implements AutoCloseable {
* processing after the script evaluates and after the {@link CompletableFuture} is completed, but before the
* transaction is committed.
*
- * @param script the script to evaluate
+ * @param gremlin the script or bytecode to evaluate
* @param language the language to evaluate it in
* @param boundVars the bindings to evaluate in the context of the script
* @param transformResult a {@link Function} that transforms the result - can be {@code null}
* @param withResult a {@link Consumer} that accepts the result - can be {@code null}
*/
- public CompletableFuture<Object> eval(final String script, final String language, final Bindings boundVars,
+ public CompletableFuture<Object> eval(final Object gremlin, final String language, final Bindings boundVars,
+ final Function<Object, Object> transformResult, final Consumer<Object> withResult) {
+ return eval(gremlin, language, boundVars, null, transformResult, withResult);
+ }
+
+ /**
+ * Evaluate a script and allow for the submission of both a transform {@link Function} and {@link Consumer}.
+ * The {@link Function} will transform the result after script evaluates but before transaction commit and before
+ * the returned {@link CompletableFuture} is completed. The {@link Consumer} will take the result for additional
+ * processing after the script evaluates and after the {@link CompletableFuture} is completed, but before the
+ * transaction is committed.
+ *
+ * @param gremlin the script or bytecode to evaluate
+ * @param language the language to evaluate it in
+ * @param boundVars the bindings to evaluate in the context of the script
+ * @param timeOut optional override for evaluation timeout
+ * @param transformResult a {@link Function} that transforms the result - can be {@code null}
+ * @param withResult a {@link Consumer} that accepts the result - can be {@code null}
+ */
+ public CompletableFuture<Object> eval(final Object gremlin, final String language, final Bindings boundVars, final Long timeOut,
final Function<Object, Object> transformResult, final Consumer<Object> withResult) {
final LifeCycle lifeCycle = LifeCycle.build()
+ .evaluationTimeoutOverride(timeOut)
.transformResult(transformResult)
.withResult(withResult).create();
- return eval(script, language, boundVars, lifeCycle);
+ return eval(gremlin, language, boundVars, lifeCycle);
}
/**
* Evaluate a script and allow for the submission of alteration to the entire evaluation execution lifecycle.
*
- * @param script the script to evaluate
+ * @param gremlin the script or bytecode to evaluate
* @param language the language to evaluate it in
* @param boundVars the bindings to evaluate in the context of the script
* @param lifeCycle a set of functions that can be applied at various stages of the evaluation process
*/
- public CompletableFuture<Object> eval(final String script, final String language, final Bindings boundVars, final LifeCycle lifeCycle) {
+ public CompletableFuture<Object> eval(final Object gremlin, final String language, final Bindings boundVars, final LifeCycle lifeCycle) {
final String lang = Optional.ofNullable(language).orElse("gremlin-groovy");
if (logger.isDebugEnabled()) {
- logger.debug("Preparing to evaluate script - {} - in thread [{}]", script, Thread.currentThread().getName());
+ logger.debug("Preparing to evaluate script - {} - in thread [{}]", gremlin, Thread.currentThread().getName());
}
final Bindings bindings = new SimpleBindings();
@@ -258,7 +296,9 @@ public class GremlinExecutor implements AutoCloseable {
// override the timeout if the lifecycle has a value assigned. if the script contains with(timeout)
// options then allow that value to override what's provided on the lifecycle
- final Optional<Long> timeoutDefinedInScript = GremlinScriptChecker.parse(script).getTimeout();
+ final Optional<Long> timeoutDefinedInScript = gremlin instanceof String
+ ? GremlinScriptChecker.parse((String) gremlin).getTimeout()
+ : Optional.empty();
final long scriptEvalTimeOut = timeoutDefinedInScript.orElse(
lifeCycle.getEvaluationTimeoutOverride().orElse(evaluationTimeout));
@@ -267,9 +307,23 @@ public class GremlinExecutor implements AutoCloseable {
try {
lifeCycle.getBeforeEval().orElse(beforeEval).accept(bindings);
- logger.debug("Evaluating script - {} - in thread [{}]", script, Thread.currentThread().getName());
+ logger.debug("Evaluating script - {} - in thread [{}]", gremlin, Thread.currentThread().getName());
+
+ Object o;
+ if (gremlin instanceof String) {
+ o = gremlinScriptEngineManager.getEngineByName(lang).eval((String) gremlin, bindings);
+ }
+ else if (gremlin instanceof Bytecode) {
+ final Bytecode bytecode = (Bytecode) gremlin;
+ final Traversal.Admin<?, ?> traversal = eval(
+ bytecode, bindings, BytecodeHelper.getLambdaLanguage(bytecode).orElse("gremlin-groovy"), "g");
+
+ o = IteratorUtils.asList(traversal);
+ }
+ else {
+ throw new IllegalArgumentException("Invalid gremlin type.");
+ }
- final Object o = gremlinScriptEngineManager.getEngineByName(lang).eval(script, bindings);
// apply a transformation before sending back the result - useful when trying to force serialization
// in the same thread that the eval took place given ThreadLocal nature of graphs as well as some
@@ -299,7 +353,7 @@ public class GremlinExecutor implements AutoCloseable {
|| root instanceof InterruptedIOException) {
lifeCycle.getAfterTimeout().orElse(afterTimeout).accept(bindings, root);
evaluationFuture.completeExceptionally(new TimeoutException(
- String.format("Evaluation exceeded the configured 'evaluationTimeout' threshold of %s ms or evaluation was otherwise cancelled directly for request [%s]: %s", scriptEvalTimeOut, script, root.getMessage())));
+ String.format("Evaluation exceeded the configured 'evaluationTimeout' threshold of %s ms or evaluation was otherwise cancelled directly for request [%s]: %s", scriptEvalTimeOut, gremlin, root.getMessage())));
} else {
lifeCycle.getAfterFailure().orElse(afterFailure).accept(bindings, root);
evaluationFuture.completeExceptionally(root);
@@ -318,7 +372,7 @@ public class GremlinExecutor implements AutoCloseable {
final CompletableFuture<Object> ef = evaluationFutureRef.get();
if (ef != null) {
ef.completeExceptionally(new TimeoutException(
- String.format("Evaluation exceeded the configured 'evaluationTimeout' threshold of %s ms or evaluation was otherwise cancelled directly for request [%s]", scriptEvalTimeOut, script)));
+ String.format("Evaluation exceeded the configured 'evaluationTimeout' threshold of %s ms or evaluation was otherwise cancelled directly for request [%s]", scriptEvalTimeOut, gremlin)));
}
}
}, scriptEvalTimeOut, TimeUnit.MILLISECONDS);
@@ -326,7 +380,7 @@ public class GremlinExecutor implements AutoCloseable {
// Cancel the scheduled timeout if the eval future is complete or the script evaluation failed with exception
evaluationFuture.handleAsync((v, t) -> {
if (!sf.isDone()) {
- logger.debug("Killing scheduled timeout on script evaluation - {} - as the eval completed (possibly with exception).", script);
+ logger.debug("Killing scheduled timeout on script evaluation - {} - as the eval completed (possibly with exception).", gremlin);
sf.cancel(true);
}
diff --git a/gremlin-python/src/main/python/gremlin_python/driver/aiohttp/transport.py b/gremlin-python/src/main/python/gremlin_python/driver/aiohttp/transport.py
index c5facb9120..7366add5b2 100644
--- a/gremlin-python/src/main/python/gremlin_python/driver/aiohttp/transport.py
+++ b/gremlin-python/src/main/python/gremlin_python/driver/aiohttp/transport.py
@@ -187,7 +187,6 @@ class AiohttpHTTPTransport(AbstractBaseTransport):
else:
self._client_session = aiohttp.ClientSession(base_url=url, headers=headers, loop=self._loop)
-
# Execute the async connect synchronously.
self._loop.run_until_complete(async_connect())
@@ -212,8 +211,7 @@ class AiohttpHTTPTransport(AbstractBaseTransport):
# Inner function to perform async read.
async def async_read():
async with async_timeout.timeout(self._read_timeout):
- # using http request read()
- return await self._http_req_resp.text()
+ return await self._http_req_resp.read()
return self._loop.run_until_complete(async_read())
diff --git a/gremlin-python/src/main/python/gremlin_python/driver/protocol.py b/gremlin-python/src/main/python/gremlin_python/driver/protocol.py
index 6aaa9b8efe..6105ad9faa 100644
--- a/gremlin-python/src/main/python/gremlin_python/driver/protocol.py
+++ b/gremlin-python/src/main/python/gremlin_python/driver/protocol.py
@@ -206,35 +206,18 @@ class GremlinServerHTTPProtocol(AbstractBaseProtocol):
def connection_made(self, transport):
super(GremlinServerHTTPProtocol, self).connection_made(transport)
- # Transforms request message into string
def write(self, request_id, request_message):
- payload = {
- 'requestId': request_id
- }
-
- gremlinArgs = request_message.args['gremlin']
- useBytecode = isinstance(gremlinArgs, Bytecode)
-
- # translate bytecode into scripts
- if useBytecode:
- request_message.args['gremlin'] = Translator().of('g').translate(gremlinArgs)
- payload['op'] = 'bytecode'
-
- # add all args into the payload
- for k, v in request_message.args.items():
- payload[k] = v
-
- json_data = json.dumps(payload)
basic_auth = {}
if self._username and self._password:
basic_auth['username'] = self._username
basic_auth['password'] = self._password
+ content_type = str(self._message_serializer.version, encoding='utf-8')
message = {
- 'headers': {'CONTENT-TYPE': 'application/json',
- 'ACCEPT': str(self._message_serializer.version, encoding='utf-8')},
- 'payload': json_data,
+ 'headers': {'CONTENT-TYPE': content_type,
+ 'ACCEPT': content_type},
+ 'payload': self._message_serializer.serialize_message(request_id, request_message),
'auth': basic_auth
}
@@ -247,13 +230,6 @@ class GremlinServerHTTPProtocol(AbstractBaseProtocol):
raise GremlinServerError({'code': 500,
'message': 'Server disconnected - please try to reconnect', 'attributes': {}})
- # if a request query cannot be compiled by Gremlin Server, the HTTP handler will send back the exception in
- # json string without a requestId
- if 'message' in message and 'requestId' not in message:
- log.error("\r\nReceived error message from server '%s'", str(message))
- raise GremlinServerError({'code': 400,
- 'message': message, 'attributes': {}})
-
message = self._message_serializer.deserialize_message(message)
request_id = message['requestId']
result_set = results_dict[request_id] if request_id in results_dict else ResultSet(None, None)
diff --git a/gremlin-python/src/main/python/tests/driver/test_driver_remote_connection_http.py b/gremlin-python/src/main/python/tests/driver/test_driver_remote_connection_http.py
index 1c7f1aa8e7..10f58fd056 100644
--- a/gremlin-python/src/main/python/tests/driver/test_driver_remote_connection_http.py
+++ b/gremlin-python/src/main/python/tests/driver/test_driver_remote_connection_http.py
@@ -31,20 +31,10 @@ from gremlin_python.process.strategies import SubgraphStrategy, SeedStrategy
from gremlin_python.structure.io.util import HashableDict
from gremlin_python.driver.serializer import GraphSONSerializersV2d0
-"""
-Due to the limitation of relying on python translator for sending groovy scripts over HTTP, certain tests are omitted.
-
-Some known limitation with current HTTP via groovy script translator:
-- Any known limitation with groovy script will be inherited, such as injection of 2 items with the second null,
- e.g. g.inject(null, null), will lead to NPE
-- Any server side NPE will cause hanging with no response, need to check server HTTP handlers
-- HTTPS only works with HttpChannelizer, need to check how WsAndHttpChannelizer handles SSL
-- No transaction support
-"""
-
gremlin_server_url_http = os.environ.get('GREMLIN_SERVER_URL_HTTP', 'http://localhost:{}/')
test_no_auth_http_url = gremlin_server_url_http.format(45940)
+
class TestDriverRemoteConnectionHttp(object):
def test_traversals(self, remote_connection_http):
statics.load_statics(globals())
@@ -164,10 +154,12 @@ class TestDriverRemoteConnectionHttp(object):
g = traversal().withRemote(remote_connection_http)
assert 24.0 == g.withSack(1.0, lambda: ("x -> x + 1", "gremlin-groovy")).V().both().sack().sum_().next()
- assert 24.0 == g.withSack(lambda: ("{1.0d}", "gremlin-groovy"), lambda: ("x -> x + 1", "gremlin-groovy")).V().both().sack().sum_().next()
+ assert 24.0 == g.withSack(lambda: ("{1.0d}", "gremlin-groovy"),
+ lambda: ("x -> x + 1", "gremlin-groovy")).V().both().sack().sum_().next()
assert 48.0 == g.withSack(1.0, lambda: ("x, y -> x + y + 1", "gremlin-groovy")).V().both().sack().sum_().next()
- assert 48.0 == g.withSack(lambda: ("{1.0d}", "gremlin-groovy"), lambda: ("x, y -> x + y + 1", "gremlin-groovy")).V().both().sack().sum_().next()
+ assert 48.0 == g.withSack(lambda: ("{1.0d}", "gremlin-groovy"),
+ lambda: ("x, y -> x + y + 1", "gremlin-groovy")).V().both().sack().sum_().next()
def test_strategies(self, remote_connection_http):
statics.load_statics(globals())
@@ -220,6 +212,7 @@ class TestDriverRemoteConnectionHttp(object):
assert 5 == t.clone().limit(5).count().next()
assert 10 == t.clone().limit(10).count().next()
+
"""
# The WsAndHttpChannelizer somehow does not distinguish the ssl handlers so authenticated https remote connection
# only work with HttpChannelizer that is currently not in the testing set up, thus this is commented out for now
diff --git a/gremlin-server/conf/gremlin-server-rest-modern.yaml b/gremlin-server/conf/gremlin-server-rest-modern.yaml
index a34b14e453..66928acba0 100644
--- a/gremlin-server/conf/gremlin-server-rest-modern.yaml
+++ b/gremlin-server/conf/gremlin-server-rest-modern.yaml
@@ -28,7 +28,10 @@ scriptEngines: {
org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]},
org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-modern.groovy]}}}}
serializers:
- - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3d0] }} # application/json
+ - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3d0] }} # application/vnd.gremlin-v3.0+json
+ - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1 } # application/vnd.graphbinary-v1.0
+ - { className: org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1, config: { serializeResultToString: true }} # application/vnd.graphbinary-v1.0-stringd
+
metrics: {
slf4jReporter: {enabled: true, interval: 180000}}
strictTransactionManagement: false
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
index 4aaaa68fd7..59b179c84a 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpGremlinEndpointHandler.java
@@ -19,38 +19,43 @@
package org.apache.tinkerpop.gremlin.server.handler;
import com.codahale.metrics.Timer;
-import io.netty.handler.codec.http.*;
-import org.apache.tinkerpop.gremlin.driver.Tokens;
-import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
-import org.apache.tinkerpop.gremlin.driver.ser.SerializationException;
-import org.apache.tinkerpop.gremlin.process.remote.traversal.DefaultRemoteTraverser;
-import org.apache.tinkerpop.gremlin.server.util.TextPlainMessageSerializer;
-import org.javatuples.Pair;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.apache.tinkerpop.gremlin.util.ExceptionHelper;
-import org.apache.tinkerpop.gremlin.driver.MessageSerializer;
-import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage;
-import org.apache.tinkerpop.gremlin.driver.message.ResponseStatusCode;
-import org.apache.tinkerpop.gremlin.driver.ser.MessageTextSerializer;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import io.netty.handler.codec.TooLongFrameException;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpUtil;
+import io.netty.util.ReferenceCountUtil;
import org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutor;
+import org.apache.tinkerpop.gremlin.process.remote.traversal.DefaultRemoteTraverser;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource;
import org.apache.tinkerpop.gremlin.server.GraphManager;
import org.apache.tinkerpop.gremlin.server.GremlinServer;
import org.apache.tinkerpop.gremlin.server.Settings;
import org.apache.tinkerpop.gremlin.server.auth.AuthenticatedUser;
import org.apache.tinkerpop.gremlin.server.util.MetricManager;
+import org.apache.tinkerpop.gremlin.server.util.TextPlainMessageSerializer;
import org.apache.tinkerpop.gremlin.structure.Graph;
+import org.apache.tinkerpop.gremlin.util.ExceptionHelper;
+import org.apache.tinkerpop.gremlin.driver.MessageSerializer;
+import org.apache.tinkerpop.gremlin.driver.Tokens;
import org.apache.tinkerpop.gremlin.util.function.FunctionUtils;
import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
-import io.netty.buffer.ByteBuf;
-import io.netty.buffer.Unpooled;
-import io.netty.channel.ChannelHandler;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelInboundHandlerAdapter;
-import io.netty.channel.ChannelPromise;
-import io.netty.handler.codec.TooLongFrameException;
-import io.netty.util.ReferenceCountUtil;
+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.driver.ser.MessageTextSerializer;
+import org.apache.tinkerpop.gremlin.driver.ser.SerializationException;
+import org.javatuples.Pair;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javax.script.Bindings;
import javax.script.SimpleBindings;
@@ -71,12 +76,12 @@ import java.util.stream.Stream;
import static com.codahale.metrics.MetricRegistry.name;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpMethod.POST;
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
+import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
+import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
-import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
-import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
-import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
/**
@@ -142,9 +147,9 @@ public class HttpGremlinEndpointHandler extends ChannelInboundHandlerAdapter {
final RequestMessage requestMessage;
try {
- requestMessage = HttpHandlerUtil.getRequestMessageFromHttpRequest(req);
- } catch (IllegalArgumentException iae) {
- HttpHandlerUtil.sendError(ctx, BAD_REQUEST, iae.getMessage(), keepAlive);
+ requestMessage = HttpHandlerUtil.getRequestMessageFromHttpRequest(req, serializers);
+ } catch (IllegalArgumentException|SerializationException ex) {
+ HttpHandlerUtil.sendError(ctx, BAD_REQUEST, ex.getMessage(), keepAlive);
ReferenceCountUtil.release(msg);
return;
}
@@ -215,6 +220,7 @@ public class HttpGremlinEndpointHandler extends ChannelInboundHandlerAdapter {
// is ready to be written as a ByteBuf directly to the response. nothing should be blocking here.
final CompletableFuture<Object> evalFuture = gremlinExecutor.eval(
requestMessage.getArg(Tokens.ARGS_GREMLIN), requestMessage.getArg(Tokens.ARGS_LANGUAGE), bindings,
+ requestMessage.getArgOrDefault(Tokens.ARGS_EVAL_TIMEOUT, null),
FunctionUtils.wrapFunction(o -> {
// stopping the timer here is roughly equivalent to where the timer would have been stopped for
// this metric in other contexts. we just want to measure eval time not serialization time.
@@ -228,7 +234,7 @@ public class HttpGremlinEndpointHandler extends ChannelInboundHandlerAdapter {
// results to Traverser so that GLVs can handle the results. don't quite get the same
// benefit here because the bulk has to be 1 since we've already resolved the result,
// but at least http is compatible
- final List<Object> results = requestMessage.getProcessor().equals(Tokens.OPS_BYTECODE) ?
+ final List<Object> results = requestMessage.getOp().equals(Tokens.OPS_BYTECODE) ?
(List<Object>) IteratorUtils.asList(o).stream().map(r -> new DefaultRemoteTraverser<Object>(r, 1)).collect(Collectors.toList()) :
IteratorUtils.asList(o);
final ResponseMessage responseMessage = ResponseMessage.build(requestMessage.getRequestId())
@@ -242,7 +248,7 @@ public class HttpGremlinEndpointHandler extends ChannelInboundHandlerAdapter {
attemptCommit(requestMessage.getArg(Tokens.ARGS_ALIASES), graphManager, settings.strictTransactionManagement);
try {
- return Unpooled.wrappedBuffer(serializer.getValue1().serializeResponseAsString(responseMessage, ctx.alloc()).getBytes(UTF8));
+ return Unpooled.wrappedBuffer(serializer.getValue1().serializeResponseAsBinary(responseMessage, ctx.alloc()));
} catch (Exception ex) {
logger.warn(String.format("Error during serialization for %s", responseMessage), ex);
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpHandlerUtil.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpHandlerUtil.java
index f25080a5cc..748d9d39ae 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpHandlerUtil.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/handler/HttpHandlerUtil.java
@@ -19,6 +19,7 @@
package org.apache.tinkerpop.gremlin.server.handler;
import com.codahale.metrics.Meter;
+import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
@@ -26,11 +27,13 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.tinkerpop.gremlin.driver.MessageSerializer;
import org.apache.tinkerpop.gremlin.driver.Tokens;
import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
import org.apache.tinkerpop.gremlin.server.GremlinServer;
import org.apache.tinkerpop.gremlin.server.op.standard.StandardOpProcessor;
import org.apache.tinkerpop.gremlin.server.util.MetricManager;
+import org.apache.tinkerpop.gremlin.driver.ser.SerializationException;
import org.apache.tinkerpop.shaded.jackson.databind.JsonNode;
import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
import org.apache.tinkerpop.shaded.jackson.databind.node.ArrayNode;
@@ -39,7 +42,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -50,6 +55,7 @@ import java.util.UUID;
import static com.codahale.metrics.MetricRegistry.name;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpMethod.GET;
+import static io.netty.handler.codec.http.HttpMethod.POST;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
/**
@@ -70,10 +76,55 @@ public class HttpHandlerUtil {
/**
* Convert a http request into a {@link RequestMessage}.
+ * There are 2 payload types options here.
+ * 1.
+ * existing https://tinkerpop.apache.org/docs/current/reference/#connecting-via-http
+ * intended to use with curl, postman, etc. by users
+ * both GET and POST
+ * Content-Type header can be empty or application/json
+ * Accept header can be any, most useful can be application/json, text/plain, application/vnd.gremlin-v3.0+json and application/vnd.gremlin-v3.0+json;types=false
+ * Request body example: { "gremlin": "g.V()" }
+ * 2.
+ * experimental payload with serialized RequestMessage
+ * intended for drivers/GLV's. Support both gremlin and bytecode queries.
+ * only POST
+ * Content-Type is defined by used serializer, expected type GraphSON application/vnd.gremlin-v3.0+json or GraphBinary application/vnd.graphbinary-v1.0. Untyped GraphSON is not supported, it can't deserialize bytecode
+ * Accept header can be any.
+ * Request body contains serialized RequestMessage
*/
- public static RequestMessage getRequestMessageFromHttpRequest(final FullHttpRequest request) {
- final String contentType = Optional.ofNullable(request.headers().get(HttpHeaderNames.CONTENT_TYPE)).orElse("application/json");
+ public static RequestMessage getRequestMessageFromHttpRequest(final FullHttpRequest request,
+ Map<String, MessageSerializer<?>> serializers) throws SerializationException {
+ final String contentType = request.headers().get(HttpHeaderNames.CONTENT_TYPE);
+
+ if (request.method() == POST && contentType != null && !contentType.equals("application/json") && serializers.containsKey(contentType)) {
+ final MessageSerializer<?> serializer = serializers.get(contentType);
+
+ final ByteBuf buffer = request.content();
+
+ // additional validation for header
+ final int first = buffer.readByte();
+ // payload can be plain json or can start with additional header with content type.
+ // if first character is not "{" (0x7b) then need to verify is correct serializer selected.
+ if (first != 0x7b) {
+ final byte[] bytes = new byte[first];
+ buffer.readBytes(bytes);
+ final String mimeType = new String(bytes, StandardCharsets.UTF_8);
+
+ if (Arrays.stream(serializer.mimeTypesSupported()).noneMatch(t -> t.equals(mimeType)))
+ throw new IllegalArgumentException("Mime type mismatch. Value in content-type header is not equal payload header.");
+ } else
+ buffer.resetReaderIndex();
+
+ return serializer.deserializeRequest(buffer);
+ }
+ return getRequestMessageFromHttpRequest(request);
+ }
+
+ /**
+ * Convert a http request into a {@link RequestMessage}.
+ */
+ public static RequestMessage getRequestMessageFromHttpRequest(final FullHttpRequest request) {
// default is just the StandardOpProcessor which maintains compatibility with older versions which only
// processed scripts.
final RequestMessage.Builder msgBuilder = RequestMessage.build(StandardOpProcessor.OP_PROCESSOR_NAME);
diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/TextPlainMessageSerializer.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/TextPlainMessageSerializer.java
index 81b3662b96..ea0332de48 100644
--- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/TextPlainMessageSerializer.java
+++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/TextPlainMessageSerializer.java
@@ -20,6 +20,7 @@ package org.apache.tinkerpop.gremlin.server.util;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
+import io.netty.util.CharsetUtil;
import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage;
import org.apache.tinkerpop.gremlin.driver.ser.MessageTextSerializer;
@@ -42,7 +43,11 @@ public class TextPlainMessageSerializer implements MessageTextSerializer<Functio
@Override
public ByteBuf serializeResponseAsBinary(final ResponseMessage responseMessage, final ByteBufAllocator allocator) throws SerializationException {
- throw new UnsupportedOperationException("text/plain does not produce binary");
+ final String payload = serializeResponseAsString(responseMessage, allocator);
+ final ByteBuf encodedMessage = allocator.buffer(payload.length());
+ encodedMessage.writeCharSequence(payload, CharsetUtil.UTF_8);
+
+ return encodedMessage;
}
@Override
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
index b9c17d27cc..6d01dd45e6 100644
--- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerHttpIntegrateTest.java
@@ -18,7 +18,10 @@
*/
package org.apache.tinkerpop.gremlin.server;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.tinkerpop.gremlin.driver.message.ResponseMessage;
import org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1;
@@ -47,6 +50,7 @@ import org.apache.tinkerpop.shaded.jackson.databind.JsonNode;
import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
import org.junit.Test;
+import java.io.IOException;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
@@ -401,9 +405,9 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
assertEquals(200, response.getStatusLine().getStatusCode());
assertEquals(mime, response.getEntity().getContentType().getValue());
- final String base64 = EntityUtils.toString(response.getEntity());
+
final GraphBinaryMessageSerializerV1 serializer = new GraphBinaryMessageSerializerV1(TypeSerializerRegistry.INSTANCE);
- final ResponseMessage msg = serializer.deserializeResponse(base64);
+ final ResponseMessage msg = serializer.deserializeResponse(toByteBuf(response.getEntity()));
final List<Object> data = (List<Object>) msg.getResult().getData();
assertEquals(6, data.size());
for (Object o : data) {
@@ -422,9 +426,9 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
assertEquals(200, response.getStatusLine().getStatusCode());
assertEquals(mime, response.getEntity().getContentType().getValue());
- final String base64 = EntityUtils.toString(response.getEntity());
+
final GraphBinaryMessageSerializerV1 serializer = new GraphBinaryMessageSerializerV1(TypeSerializerRegistry.INSTANCE);
- final ResponseMessage msg = serializer.deserializeResponse(base64);
+ final ResponseMessage msg = serializer.deserializeResponse(toByteBuf(response.getEntity()));
final List<Object> data = (List<Object>) msg.getResult().getData();
assertEquals(6, data.size());
for (Object o : data) {
@@ -1070,4 +1074,12 @@ public class GremlinServerHttpIntegrateTest extends AbstractGremlinServerIntegra
threadPool.shutdown();
}
+
+ private static ByteBuf toByteBuf(final HttpEntity httpEntity) throws IOException {
+ final byte[] asArray = new byte[(int)httpEntity.getContentLength()];
+
+ httpEntity.getContent().read(asArray);
+
+ return Unpooled.wrappedBuffer(asArray);
+ }
}
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java
index 1d0fbc6fb5..528e325d8a 100644
--- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/HttpDriverIntegrateTest.java
@@ -18,18 +18,23 @@
*/
package org.apache.tinkerpop.gremlin.server;
+import io.netty.handler.codec.EncoderException;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.tinkerpop.gremlin.driver.Channelizer;
import org.apache.tinkerpop.gremlin.driver.Client;
import org.apache.tinkerpop.gremlin.driver.Cluster;
import org.apache.tinkerpop.gremlin.driver.remote.DriverRemoteConnection;
-import org.apache.tinkerpop.gremlin.driver.ser.Serializers;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer;
import org.apache.tinkerpop.gremlin.structure.Transaction;
+import org.apache.tinkerpop.gremlin.driver.ser.Serializers;
import org.junit.Test;
import static org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.core.AllOf.allOf;
+import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@@ -81,7 +86,8 @@ public class HttpDriverIntegrateTest extends AbstractGremlinServerIntegrationTes
.create();
try {
final GraphTraversalSource g = traversal().withRemote(DriverRemoteConnection.using(cluster));
- assertEquals("2", g.inject("2").toList().get(0));
+ final String result = g.inject("2").toList().get(0);
+ assertEquals("2", result);
} catch (Exception ex) {
throw ex;
} finally {
@@ -89,6 +95,24 @@ public class HttpDriverIntegrateTest extends AbstractGremlinServerIntegrationTes
}
}
+ @Test
+ public void shouldGetErrorForBytecodeWithUntypedGraphSON() throws Exception {
+ final Cluster cluster = TestClientFactory.build()
+ .channelizer(Channelizer.HttpChannelizer.class)
+ .serializer(Serializers.GRAPHSON_V2D0_UNTYPED)
+ .create();
+ try {
+ final GraphTraversalSource g = traversal().withRemote(DriverRemoteConnection.using(cluster));
+ g.inject("2").toList();
+ fail("Exception expected");
+ } catch (EncoderException ex) {
+ assertThat(ex.getMessage(), allOf(containsString("An error occurred during serialization of this request"),
+ containsString("it could not be sent to the server - Reason: only GraphSON3 and GraphBinary recommended for serialization of Bytecode requests, but used org.apache.tinkerpop.gremlin.")));
+ } finally {
+ cluster.close();
+ }
+ }
+
@Test
public void shouldSubmitBytecodeWithGraphBinary() throws Exception {
final Cluster cluster = TestClientFactory.build()
@@ -97,7 +121,8 @@ public class HttpDriverIntegrateTest extends AbstractGremlinServerIntegrationTes
.create();
try {
final GraphTraversalSource g = traversal().withRemote(DriverRemoteConnection.using(cluster));
- assertEquals("2", g.inject("2").toList().get(0));
+ final String result = g.inject("2").toList().get(0);
+ assertEquals("2", result);
} catch (Exception ex) {
throw ex;
} finally {
diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/handler/HttpHandlerUtilTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/handler/HttpHandlerUtilTest.java
new file mode 100644
index 0000000000..6b911d9b2e
--- /dev/null
+++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/handler/HttpHandlerUtilTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.tinkerpop.gremlin.server.handler;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.handler.codec.http.DefaultFullHttpRequest;
+import io.netty.handler.codec.http.DefaultHttpHeaders;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http.QueryStringEncoder;
+import io.netty.util.CharsetUtil;
+import org.apache.tinkerpop.gremlin.driver.MessageSerializer;
+import org.apache.tinkerpop.gremlin.driver.Tokens;
+import org.apache.tinkerpop.gremlin.driver.message.RequestMessage;
+import org.apache.tinkerpop.gremlin.driver.ser.GraphBinaryMessageSerializerV1;
+import org.apache.tinkerpop.gremlin.driver.ser.GraphSONMessageSerializerV3d0;
+import org.apache.tinkerpop.gremlin.driver.ser.SerTokens;
+import org.apache.tinkerpop.gremlin.driver.ser.SerializationException;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.samePropertyValuesAs;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class HttpHandlerUtilTest {
+
+ private final ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
+ private final GraphBinaryMessageSerializerV1 graphBinarySerializer = new GraphBinaryMessageSerializerV1();
+ public final GraphSONMessageSerializerV3d0 graphSONSerializer = new GraphSONMessageSerializerV3d0();
+
+ @Test
+ public void shouldFailWhenIncorrectSerializerUsed() throws SerializationException {
+ final RequestMessage request = RequestMessage.build(Tokens.OPS_BYTECODE)
+ .processor("traversal")
+ .overrideRequestId(UUID.randomUUID())
+ .addArg(Tokens.ARGS_GREMLIN, "g.V()")
+ .create();
+
+ final ByteBuf buffer = graphSONSerializer.serializeRequestAsBinary(request, allocator);
+
+ final HttpHeaders headers = new DefaultHttpHeaders();
+ headers.add(HttpHeaderNames.CONTENT_TYPE, SerTokens.MIME_GRAPHBINARY_V1D0);
+
+ final FullHttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "some uri",
+ buffer, headers, new DefaultHttpHeaders());
+
+ final Map<String, MessageSerializer<?>> serializers = new HashMap<>();
+ serializers.put(SerTokens.MIME_GRAPHBINARY_V1D0, graphBinarySerializer);
+
+ try {
+ HttpHandlerUtil.getRequestMessageFromHttpRequest(httpRequest, serializers);
+ fail("SerializationException expected");
+ } catch (IllegalArgumentException ex) {
+ assertEquals("Mime type mismatch. Value in content-type header is not equal payload header.", ex.getMessage());
+ }
+ }
+
+ @Test
+ public void shouldCorrectlyDeserializeRequestMessage() throws SerializationException {
+ final RequestMessage request = RequestMessage.build(Tokens.OPS_BYTECODE)
+ .processor("traversal")
+ .overrideRequestId(UUID.randomUUID())
+ .addArg(Tokens.ARGS_GREMLIN, "g.V()")
+ .create();
+
+ final ByteBuf buffer = graphBinarySerializer.serializeRequestAsBinary(request, allocator);
+
+ final HttpHeaders headers = new DefaultHttpHeaders();
+ headers.add(HttpHeaderNames.CONTENT_TYPE, SerTokens.MIME_GRAPHBINARY_V1D0);
+
+ final FullHttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "some uri",
+ buffer, headers, new DefaultHttpHeaders());
+
+ final Map<String, MessageSerializer<?>> serializers = new HashMap<>();
+ serializers.put(SerTokens.MIME_GRAPHBINARY_V1D0, graphBinarySerializer);
+
+ final RequestMessage deserialized = HttpHandlerUtil.getRequestMessageFromHttpRequest(httpRequest, serializers);
+ assertThat(request, samePropertyValuesAs(deserialized));
+ }
+
+ @Test
+ public void shouldCorrectlyDeserializeGremlinFromPostRequest() throws SerializationException {
+ final String gremlin = "g.V().hasLabel('person')";
+ final UUID requestId = UUID.randomUUID();
+ final ByteBuf buffer = allocator.buffer();
+ buffer.writeCharSequence("{ \"gremlin\": \"" + gremlin +
+ "\", \"requestId\": \"" + requestId +
+ "\", \"language\": \"gremlin-groovy\"}",
+ CharsetUtil.UTF_8);
+
+ final HttpHeaders headers = new DefaultHttpHeaders();
+ headers.add(HttpHeaderNames.CONTENT_TYPE, SerTokens.MIME_JSON);
+
+ final FullHttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "some uri",
+ buffer, headers, new DefaultHttpHeaders());
+
+ final Map<String, MessageSerializer<?>> serializers = new HashMap<>();
+ serializers.put(SerTokens.MIME_GRAPHBINARY_V1D0, graphBinarySerializer);
+
+ final RequestMessage deserialized = HttpHandlerUtil.getRequestMessageFromHttpRequest(httpRequest, serializers);
+ assertEquals(gremlin, deserialized.getArgs().get(Tokens.ARGS_GREMLIN));
+ assertEquals(requestId, deserialized.getRequestId());
+ assertEquals("gremlin-groovy", deserialized.getArg(Tokens.ARGS_LANGUAGE));
+ }
+
+ @Test
+ public void shouldCorrectlyDeserializeGremlinFromGetRequest() throws SerializationException {
+ final String gremlin = "g.V().hasLabel('person')";
+ final UUID requestId = UUID.randomUUID();
+ final ByteBuf buffer = allocator.buffer();
+
+ final List<String> headerOptions = Arrays.asList("", null, SerTokens.MIME_JSON, "some invalid value");
+
+ for (final String contentTypeValue : headerOptions) {
+ final HttpHeaders headers = new DefaultHttpHeaders();
+ if (contentTypeValue != null) {
+ headers.add(HttpHeaderNames.CONTENT_TYPE, contentTypeValue);
+ }
+
+ final QueryStringEncoder encoder = new QueryStringEncoder("/");
+ encoder.addParam("gremlin", gremlin);
+ encoder.addParam("requestId", requestId.toString());
+ encoder.addParam("language", "gremlin-groovy");
+
+ final FullHttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
+ encoder.toString(), buffer, headers, new DefaultHttpHeaders());
+
+ final Map<String, MessageSerializer<?>> serializers = new HashMap<>();
+ serializers.put(SerTokens.MIME_GRAPHBINARY_V1D0, graphBinarySerializer);
+
+ final RequestMessage deserialized = HttpHandlerUtil.getRequestMessageFromHttpRequest(httpRequest, serializers);
+ assertEquals(gremlin, deserialized.getArgs().get(Tokens.ARGS_GREMLIN));
+ assertEquals(requestId, deserialized.getRequestId());
+ assertEquals("gremlin-groovy", deserialized.getArg(Tokens.ARGS_LANGUAGE));
+ }
+ }
+}