TINKERPOP-2040 Provide improved type handling for GroovyTranslator Refactored testing and added support for UUID, Date, Timestamp as well as a method to override standard type handling.
Project: http://git-wip-us.apache.org/repos/asf/tinkerpop/repo Commit: http://git-wip-us.apache.org/repos/asf/tinkerpop/commit/774371dc Tree: http://git-wip-us.apache.org/repos/asf/tinkerpop/tree/774371dc Diff: http://git-wip-us.apache.org/repos/asf/tinkerpop/diff/774371dc Branch: refs/heads/TINKERPOP-2041 Commit: 774371dc38d2d73be142eaa69c958226ac660892 Parents: 0403156 Author: Stephen Mallette <sp...@genoprime.com> Authored: Thu Sep 20 11:53:07 2018 -0400 Committer: Stephen Mallette <sp...@genoprime.com> Committed: Mon Oct 1 16:18:28 2018 -0400 ---------------------------------------------------------------------- .../gremlin/process/traversal/Translator.java | 34 ++++ .../groovy/jsr223/GroovyTranslatorTest.java | 162 +++++++++++++++---- .../gremlin/groovy/jsr223/GroovyTranslator.java | 32 +++- 3 files changed, 195 insertions(+), 33 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/774371dc/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Translator.java ---------------------------------------------------------------------- diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Translator.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Translator.java index 7e97fb3..0346092 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Translator.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Translator.java @@ -19,6 +19,8 @@ package org.apache.tinkerpop.gremlin.process.traversal; +import java.util.function.UnaryOperator; + /** * A Translator will translate {@link Bytecode} into another representation. That representation may be a * Java instance via {@link StepTranslator} or a String script in some language via {@link ScriptTranslator}. @@ -55,10 +57,42 @@ public interface Translator<S, T> { /// + /** + * Translates bytecode to a string representation. + */ public interface ScriptTranslator extends Translator<String, String> { + /** + * Provides a way to customize and override the standard translation process. A {@link ScriptTranslator} + * implementation can choose to expose a way to accept a {@code TypeTranslator} which will convert an incoming + * object to a different form which will then be normally processed or can return a {@link Handled} object + * with the already translated script. + */ + public interface TypeTranslator extends UnaryOperator<Object> { + public static TypeTranslator identity() { + return t -> t; + } + } + + /** + * Contains a completed type translation from the {@link TypeTranslator}. + */ + public class Handled { + private final String translation; + + public Handled(final String translation) { + this.translation = translation; + } + + public String getTranslation() { + return translation; + } + } } + /** + * Translates bytecode to actual steps. + */ public interface StepTranslator<S extends TraversalSource, T extends Traversal.Admin<?, ?>> extends Translator<S, T> { } http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/774371dc/gremlin-groovy-test/src/main/java/org/apache/tinkerpop/gremlin/groovy/jsr223/GroovyTranslatorTest.java ---------------------------------------------------------------------- diff --git a/gremlin-groovy-test/src/main/java/org/apache/tinkerpop/gremlin/groovy/jsr223/GroovyTranslatorTest.java b/gremlin-groovy-test/src/main/java/org/apache/tinkerpop/gremlin/groovy/jsr223/GroovyTranslatorTest.java index 942f1a5..686c7bd 100644 --- a/gremlin-groovy-test/src/main/java/org/apache/tinkerpop/gremlin/groovy/jsr223/GroovyTranslatorTest.java +++ b/gremlin-groovy-test/src/main/java/org/apache/tinkerpop/gremlin/groovy/jsr223/GroovyTranslatorTest.java @@ -22,6 +22,7 @@ package org.apache.tinkerpop.gremlin.groovy.jsr223; import org.apache.commons.configuration.MapConfiguration; import org.apache.tinkerpop.gremlin.AbstractGremlinTest; import org.apache.tinkerpop.gremlin.LoadGraphWith; +import org.apache.tinkerpop.gremlin.process.traversal.Translator; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.Traverser; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; @@ -32,47 +33,56 @@ import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.Transl import org.apache.tinkerpop.gremlin.process.traversal.strategy.verification.ReadOnlyStrategy; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.structure.util.ElementHelper; import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdge; import org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex; import org.apache.tinkerpop.gremlin.util.function.Lambda; import org.junit.Test; import javax.script.Bindings; +import javax.script.ScriptException; import javax.script.SimpleBindings; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; +import java.util.UUID; import java.util.function.Function; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; /** * @author Marko A. Rodriguez (http://markorodriguez.com) */ public class GroovyTranslatorTest extends AbstractGremlinTest { + private static final GremlinGroovyScriptEngine engine = new GremlinGroovyScriptEngine(); + @Test @LoadGraphWith(LoadGraphWith.GraphData.MODERN) public void shouldHandleStrategies() throws Exception { - GraphTraversalSource g = graph.traversal(); - g = g.withStrategies(SubgraphStrategy.create(new MapConfiguration(new HashMap<String, Object>() {{ + final GraphTraversalSource g = graph.traversal().withStrategies(SubgraphStrategy.create(new MapConfiguration(new HashMap<String, Object>() {{ put(SubgraphStrategy.VERTICES, __.has("name", "marko")); }}))); final Bindings bindings = new SimpleBindings(); bindings.put("g", g); - Traversal.Admin<Vertex, Object> traversal = new GremlinGroovyScriptEngine().eval(g.V().values("name").asAdmin().getBytecode(), bindings, "g"); + Traversal.Admin<Vertex, Object> traversal = engine.eval(g.V().values("name").asAdmin().getBytecode(), bindings, "g"); assertEquals("marko", traversal.next()); assertFalse(traversal.hasNext()); // - traversal = new GremlinGroovyScriptEngine().eval(g.withoutStrategies(SubgraphStrategy.class).V().count().asAdmin().getBytecode(), bindings, "g"); + traversal = engine.eval(g.withoutStrategies(SubgraphStrategy.class).V().count().asAdmin().getBytecode(), bindings, "g"); assertEquals(new Long(6), traversal.next()); assertFalse(traversal.hasNext()); // - traversal = new GremlinGroovyScriptEngine().eval(g.withStrategies(SubgraphStrategy.create(new MapConfiguration(new HashMap<String, Object>() {{ + traversal = engine.eval(g.withStrategies(SubgraphStrategy.create(new MapConfiguration(new HashMap<String, Object>() {{ put(SubgraphStrategy.VERTICES, __.has("name", "marko")); }})), ReadOnlyStrategy.instance()).V().values("name").asAdmin().getBytecode(), bindings, "g"); assertEquals("marko", traversal.next()); @@ -81,10 +91,10 @@ public class GroovyTranslatorTest extends AbstractGremlinTest { @Test @LoadGraphWith(LoadGraphWith.GraphData.MODERN) - public void shouldSupportStringSupplierLambdas() throws Exception { + public void shouldSupportStringSupplierLambdas() { GraphTraversalSource g = graph.traversal(); g = g.withStrategies(new TranslationStrategy(g, GroovyTranslator.of("g"))); - GraphTraversal.Admin<Vertex, Integer> t = g.withSideEffect("lengthSum", 0).withSack(1) + final GraphTraversal.Admin<Vertex, Integer> t = g.withSideEffect("lengthSum", 0).withSack(1) .V() .filter(Lambda.predicate("it.get().label().equals('person')")) .flatMap(Lambda.function("it.get().vertices(Direction.OUT)")) @@ -130,25 +140,77 @@ public class GroovyTranslatorTest extends AbstractGremlinTest { ".order().by({a,b -> a <=> b})" + ".sack({ a,b -> a + b })", script); + assertThatScriptOk(script, "g", g); } @Test @LoadGraphWith(LoadGraphWith.GraphData.MODERN) public void shouldHandleMaps() { - final GraphTraversalSource g = graph.traversal(); final String script = GroovyTranslator.of("g").translate(g.V().id().is(new LinkedHashMap<Object,Object>() {{ put(3, "32"); put(Arrays.asList(1, 2, 3.1d), 4); }}).asAdmin().getBytecode()); assertEquals("g.V().id().is([((int) 3):(\"32\"),([(int) 1, (int) 2, 3.1d]):((int) 4)])", script); + assertThatScriptOk(script, "g", g); } @Test public void shouldHandleEmptyMaps() { final Function identity = new Lambda.OneArgLambda("it.get()", "gremlin-groovy"); - final GraphTraversalSource g = graph.traversal(); final String script = GroovyTranslator.of("g").translate(g.inject(Collections.emptyMap()).map(identity).asAdmin().getBytecode()); assertEquals("g.inject([]).map({it.get()})", script); + assertThatScriptOk(script, "g", g); + } + + @Test + public void shouldHandleDate() { + final Calendar c = Calendar.getInstance(); + c.set(1975, Calendar.SEPTEMBER, 7); + final Date d = c.getTime(); + assertTranslation(String.format("new java.util.Date(%s)", d.getTime()), d); + } + + @Test + public void shouldHandleTimestamp() { + final Calendar c = Calendar.getInstance(); + c.set(1975, Calendar.SEPTEMBER, 7); + final Timestamp t = new Timestamp(c.getTime().getTime()); + assertTranslation(String.format("new java.sql.Timestamp(%s)", t.getTime()), t); + } + + @Test + public void shouldHandleUuid() { + final UUID uuid = UUID.fromString("ffffffff-fd49-1e4b-0000-00000d4b8a1d"); + assertTranslation(String.format("java.util.UUID.fromString('%s')", uuid), uuid); + } + + @Test + public void shouldOverrideDefaultTypeTranslationWithSomethingBonkers() { + final String thingToSuffixAllStringsWith = "-why-would-anyone-do-this"; + final String script = GroovyTranslator.of("g", x -> x instanceof String ? x + thingToSuffixAllStringsWith : x). + translate(g.inject("yyy", "xxx").asAdmin().getBytecode()); + assertEquals(String.format("g.inject(\"yyy%s\",\"xxx%s\")", thingToSuffixAllStringsWith, thingToSuffixAllStringsWith), script); + assertThatScriptOk(script, "g", g); + } + + @Test + public void shouldIncludeCustomTypeTranslationForSomethingSilly() { + final SillyClass notSillyEnough = SillyClass.from("not silly enough", 100); + final GraphTraversalSource g = graph.traversal(); + + // without type translation we get uglinesss + final String scriptBad = GroovyTranslator.of("g"). + translate(g.inject(notSillyEnough).asAdmin().getBytecode()); + assertEquals(String.format("g.inject(%s)", "not silly enough:100"), scriptBad); + + // with type translation we get valid gremlin + final String scriptGood = GroovyTranslator.of("g", + x -> x instanceof SillyClass ? + new Translator.ScriptTranslator.Handled(String.format("org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyTranslatorTest.SillyClass.from('%s', (int) %s)", + ((SillyClass) x).getX(), ((SillyClass) x).getY())) : x). + translate(g.inject(notSillyEnough).asAdmin().getBytecode()); + assertEquals(String.format("g.inject(org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyTranslatorTest.SillyClass.from('%s', (int) %s))", notSillyEnough.getX(), notSillyEnough.getY()), scriptGood); + assertThatScriptOk(scriptGood, "g", g); } @Test @@ -158,20 +220,8 @@ public class GroovyTranslatorTest extends AbstractGremlinTest { @Test public void shouldEscapeStrings() { - final GraphTraversalSource g = graph.traversal(); - final String script = GroovyTranslator.of("g").translate(g.addV("customer") - .property("customer_id", 501L) - .property("name", "Foo\u0020Bar") - .property("age", 25) - .property("special", "`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?") - .asAdmin().getBytecode()); - - assertEquals("g.addV(\"customer\")" + - ".property(\"customer_id\",501L)" + - ".property(\"name\",\"Foo Bar\")" + - ".property(\"age\",(int) 25)" + - ".property(\"special\",\"\"\"`~!@#\\$%^&*()-_=+[{]}\\\\|;:'\\\",<.>/?\"\"\")", - script); + assertTranslation("501L,\"Foo Bar\",(int) 25,\"\"\"`~!@#\\$%^&*()-_=+[{]}\\\\|;:'\\\",<.>/?\"\"\"", + 501L, "Foo\u0020Bar", 25, "`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?"); } @Test @@ -181,33 +231,87 @@ public class GroovyTranslatorTest extends AbstractGremlinTest { final Object id1 = "customer:10:foo\u0020bar\u0020\u0024100#90"; // customer:10:foo bar $100#90 final Vertex vertex1 = DetachedVertex.build().setLabel("customer").setId(id1) .create(); - final String script1 = GroovyTranslator.of("g").translate(g.V(vertex1).asAdmin().getBytecode()); - assertEquals("g.V(new org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex(" + + final String script1 = GroovyTranslator.of("g").translate(g.inject(vertex1).asAdmin().getBytecode()); + assertEquals("g.inject(new org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex(" + "\"customer:10:foo bar \\$100#90\"," + "\"customer\", Collections.emptyMap()))", script1); + assertThatScriptOk(script1, "g", g); final Object id2 = "user:20:foo\\u0020bar\\u005c\\u0022mr\\u005c\\u0022\\u00241000#50"; // user:20:foo\u0020bar\u005c\u0022mr\u005c\u0022\u00241000#50 final Vertex vertex2 = DetachedVertex.build().setLabel("user").setId(id2) .create(); - final String script2 = GroovyTranslator.of("g").translate(g.V(vertex2).asAdmin().getBytecode()); - assertEquals("g.V(new org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex(" + + final String script2 = GroovyTranslator.of("g").translate(g.inject(vertex2).asAdmin().getBytecode()); + assertEquals("g.inject(new org.apache.tinkerpop.gremlin.structure.util.detached.DetachedVertex(" + "\"user:20:foo\\\\u0020bar\\\\u005c\\\\u0022mr\\\\u005c\\\\u0022\\\\u00241000#50\"," + "\"user\", Collections.emptyMap()))", script2); + assertThatScriptOk(script2, "g", g); final Object id3 = "knows:30:foo\u0020bar\u0020\u0024100:\\u0020\\u0024500#70"; final Edge edge = DetachedEdge.build().setLabel("knows").setId(id3) .setOutV((DetachedVertex) vertex1) .setInV((DetachedVertex) vertex2) .create(); - final String script3 = GroovyTranslator.of("g").translate(g.E(edge).asAdmin().getBytecode()); - assertEquals("g.E(new org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdge(" + + final String script3 = GroovyTranslator.of("g").translate(g.inject(edge).asAdmin().getBytecode()); + assertEquals("g.inject(new org.apache.tinkerpop.gremlin.structure.util.detached.DetachedEdge(" + "\"knows:30:foo bar \\$100:\\\\u0020\\\\u0024500#70\"," + "\"knows\",Collections.emptyMap()," + "\"customer:10:foo bar \\$100#90\",\"customer\"," + "\"user:20:foo\\\\u0020bar\\\\u005c\\\\u0022mr\\\\u005c\\\\u0022\\\\u00241000#50\",\"user\"))", script3); + assertThatScriptOk(script3, "g", g); + } + + public static Object eval(final String s, final Object... args) throws ScriptException { + return engine.eval(s, new SimpleBindings(ElementHelper.asMap(args))); + } + + public static Object eval(final String s, final Bindings b) throws ScriptException { + return engine.eval(s, b); + } + + private void assertTranslation(final String expectedTranslation, final Object... objs) { + final String script = GroovyTranslator.of("g").translate(g.inject(objs).asAdmin().getBytecode()); + assertEquals(String.format("g.inject(%s)", expectedTranslation), script); + assertThatScriptOk(script, "g", g); + } + + private void assertThatScriptOk(final String s, final Object... args) { + try { + assertNotNull(eval(s, args)); + } catch (ScriptException se) { + se.printStackTrace(); + fail("Script should have eval'd"); + } + } + + public static class SillyClass { + + private final String x; + private final int y; + + private SillyClass(final String x, final int y) { + this.x = x; + this.y = y; + } + + public static SillyClass from(final String x, final int y) { + return new SillyClass(x, y); + } + + public String getX() { + return x; + } + + public int getY() { + return y; + } + + @Override + public String toString() { + return x + ":" + String.valueOf(y); + } } } http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/774371dc/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/jsr223/GroovyTranslator.java ---------------------------------------------------------------------- diff --git a/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/jsr223/GroovyTranslator.java b/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/jsr223/GroovyTranslator.java index 0452e0b..1d122c6 100644 --- a/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/jsr223/GroovyTranslator.java +++ b/gremlin-groovy/src/main/java/org/apache/tinkerpop/gremlin/groovy/jsr223/GroovyTranslator.java @@ -39,11 +39,16 @@ import org.apache.tinkerpop.gremlin.structure.VertexProperty; import org.apache.tinkerpop.gremlin.structure.util.StringFactory; import org.apache.tinkerpop.gremlin.util.function.Lambda; +import java.sql.Timestamp; import java.util.ArrayList; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.UUID; +import java.util.function.UnaryOperator; /** * @author Marko A. Rodriguez (http://markorodriguez.com) @@ -53,13 +58,19 @@ public final class GroovyTranslator implements Translator.ScriptTranslator { private static final boolean IS_TESTING = Boolean.valueOf(System.getProperty("is.testing", "false")); private final String traversalSource; + private final TypeTranslator typeTranslator; - private GroovyTranslator(final String traversalSource) { + private GroovyTranslator(final String traversalSource, final TypeTranslator typeTranslator) { this.traversalSource = traversalSource; + this.typeTranslator = typeTranslator; } public static final GroovyTranslator of(final String traversalSource) { - return new GroovyTranslator(traversalSource); + return of(traversalSource, TypeTranslator.identity()); + } + + public static final GroovyTranslator of(final String traversalSource, final TypeTranslator typeTranslator) { + return new GroovyTranslator(traversalSource, Optional.ofNullable(typeTranslator).orElse(TypeTranslator.identity())); } /////// @@ -108,8 +119,15 @@ public final class GroovyTranslator implements Translator.ScriptTranslator { return traversalScript.toString(); } - private String convertToString(final Object object) { - if (object instanceof Bytecode.Binding) + private String convertToString(final Object o) { + // a TypeTranslator that returns Handled means that the typetranslator figure out how to convert the + // object to a string and it should be used as-is, otherwise it gets passed down the line through the normal + // process + final Object object = typeTranslator.apply(o); + + if (object instanceof Handled) + return ((Handled) object).getTranslation(); + else if (object instanceof Bytecode.Binding) return ((Bytecode.Binding) object).variable(); else if (object instanceof Bytecode) return this.internalTranslate("__", (Bytecode) object); @@ -155,6 +173,12 @@ public final class GroovyTranslator implements Translator.ScriptTranslator { return "(int) " + object; else if (object instanceof Class) return ((Class) object).getCanonicalName(); + else if (object instanceof Timestamp) + return "new java.sql.Timestamp(" + ((Timestamp) object).getTime() + ")"; + else if (object instanceof Date) + return "new java.util.Date(" + ((Date) object).getTime() + ")"; + else if (object instanceof UUID) + return "java.util.UUID.fromString('" + object.toString() + "')"; else if (object instanceof P) return convertPToString((P) object, new StringBuilder()).toString(); else if (object instanceof SackFunctions.Barrier)