Repository: tinkerpop Updated Branches: refs/heads/TINKERPOP-1791 [created] daa442cd7
TINKERPOP-1791 Added GremlinDsl.AnonymousMethod annotation Allows the ability to bypass the type inference system and provides for direct specification of the type parameters when generating anonymous methods. Project: http://git-wip-us.apache.org/repos/asf/tinkerpop/repo Commit: http://git-wip-us.apache.org/repos/asf/tinkerpop/commit/daa442cd Tree: http://git-wip-us.apache.org/repos/asf/tinkerpop/tree/daa442cd Diff: http://git-wip-us.apache.org/repos/asf/tinkerpop/diff/daa442cd Branch: refs/heads/TINKERPOP-1791 Commit: daa442cd7e100a7f48541cf17bc29b17c68cb5a5 Parents: 3648f80 Author: Stephen Mallette <sp...@genoprime.com> Authored: Fri Oct 20 08:58:52 2017 -0400 Committer: Stephen Mallette <sp...@genoprime.com> Committed: Fri Oct 20 08:58:52 2017 -0400 ---------------------------------------------------------------------- CHANGELOG.asciidoc | 1 + docs/src/reference/the-traversal.asciidoc | 5 +- .../upgrade/release-3.2.x-incubating.asciidoc | 56 ++++++++++++++++++- .../src/main/java/SocialTraversalDsl.java | 13 ++++- .../process/traversal/dsl/GremlinDsl.java | 20 +++++++ .../traversal/dsl/GremlinDslProcessor.java | 58 ++++++++++++++++---- .../traversal/dsl/SocialTraversalDsl.java | 19 ++++++- 7 files changed, 156 insertions(+), 16 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/daa442cd/CHANGELOG.asciidoc ---------------------------------------------------------------------- diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 07f1d69..dd7624a 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -31,6 +31,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima * Fixed a bug where bytecode containing lambdas would randomly select a traversal source from bindings. * Deprecated `GremlinScriptEngine.eval()` methods and replaced them with new overloads that include the specific `TraversalSource` to bind to. * Added `GraphHelper.cloneElements(Graph original, Graph clone)` to the `gremlin-test` module to quickly clone a graph. +* Added `GremlinDsl.AnonymousMethod` annotation to help provide explicit types for anonymous methods when the types are not easily inferred. * Bump to GMavenPlus 1.6. * Added better error message for illegal use of `repeat()`-step. * Fixed a bug in `RangeByIsCountStrategy` that led to unexpected behaviors when predicates were used with floating point numbers. http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/daa442cd/docs/src/reference/the-traversal.asciidoc ---------------------------------------------------------------------- diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc index 167073a..632ed09 100644 --- a/docs/src/reference/the-traversal.asciidoc +++ b/docs/src/reference/the-traversal.asciidoc @@ -3034,7 +3034,10 @@ public interface SocialTraversalDsl<S, E> extends GraphTraversal.Admin<S, E> { ---- IMPORTANT: Follow the TinkerPop convention of using `<S,E>` in naming generics as those conventions are taken into -account when generating the anonymous traversal class. +account when generating the anonymous traversal class. The processor attempts to infer the appropriate type parameters +when generating the anonymous traversal class. If it cannot do it correctly, it is possible to avoid the inference by +using the `GremlinDsl.AnonymousMethod` annotation on the DSL method. It allows explicit specification of the types to +use. The `@GremlinDsl` annotation is used by the link:https://docs.oracle.com/javase/8/docs/api/index.html?javax/annotation/processing/Processor.html[Java Annotation Processor] to generate the boilerplate class structure required to properly use the DSL within the TinkerPop framework. These http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/daa442cd/docs/src/upgrade/release-3.2.x-incubating.asciidoc ---------------------------------------------------------------------- diff --git a/docs/src/upgrade/release-3.2.x-incubating.asciidoc b/docs/src/upgrade/release-3.2.x-incubating.asciidoc index 732e29e..50226bb 100644 --- a/docs/src/upgrade/release-3.2.x-incubating.asciidoc +++ b/docs/src/upgrade/release-3.2.x-incubating.asciidoc @@ -29,7 +29,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima Please see the link:https://github.com/apache/tinkerpop/blob/3.2.7/CHANGELOG.asciidoc#release-3-2-7[changelog] for a complete list of all the modifications that are part of this release. -=== Embedded Remote Connection +==== Embedded Remote Connection As Gremlin Language Variants (GLVs) expand their usage and use of `withRemote()` becomes more common, the need to mock the "remote" in unit tests increases. To simplify mocking in Java, the new `EmbeddedRemoteConnection` provides a @@ -37,6 +37,60 @@ simple way to provide a "remote" that is actually local to the same JVM. See: link:https://issues.apache.org/jira/browse/TINKERPOP-1756[TINKERPOP-1756] +==== DSL Type Specification + +Prior to this version, the Java annotation processor for Gremlin DSLs has tried to infer the appropriate type +specifications when generating anonymous methods. It largely performed this inference on simple conventions in the +DSL method's template specification and there were times where it would fail. For example, a method like this: + +[source,java] +---- +public default GraphTraversal<S, E> person() { + return hasLabel("person"); +} +---- + +would generate an anonymous method like: + +[source,java] +---- +public static <S> SocialGraphTraversal<S, E> person() { + return hasLabel("person"); +} +---- + +and, of course, generate a compile error and `E` was not recognized as a symbol. The preferred generation would likely +be: + +[source,java] +---- +public static <S> SocialGraphTraversal<S, S> person() { + return hasLabel("person"); +} +---- + +To remedy this situation, a new annotation has been added which allows the user to control the type specifications +more directly providing a way to avoid/override the inference system: + +[source,java] +---- +@GremlinDsl.AnonymousMethod(returnTypeParameters = {"A", "A"}, methodTypeParameters = {"A"}) +public default GraphTraversal<S, E> person() { + return hasLabel("person"); +} +---- + +which will then generate: + +[source,java] +---- +public static <A> SocialGraphTraversal<A, A> person() { + return hasLabel("person"); +} +---- + +See: link:https://issues.apache.org/jira/browse/TINKERPOP-1791[TINKERPOP-1791] + ==== Specify a Cluster Object The `:remote connect` command can now take a pre-defined `Cluster` object as its argument as opposed to a YAML http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/daa442cd/gremlin-archetype/gremlin-archetype-dsl/src/main/resources/archetype-resources/src/main/java/SocialTraversalDsl.java ---------------------------------------------------------------------- diff --git a/gremlin-archetype/gremlin-archetype-dsl/src/main/resources/archetype-resources/src/main/java/SocialTraversalDsl.java b/gremlin-archetype/gremlin-archetype-dsl/src/main/resources/archetype-resources/src/main/java/SocialTraversalDsl.java index 7f83152..e4d48bc 100644 --- a/gremlin-archetype/gremlin-archetype-dsl/src/main/resources/archetype-resources/src/main/java/SocialTraversalDsl.java +++ b/gremlin-archetype/gremlin-archetype-dsl/src/main/resources/archetype-resources/src/main/java/SocialTraversalDsl.java @@ -19,6 +19,7 @@ package ${package}; import org.apache.tinkerpop.gremlin.process.traversal.dsl.GremlinDsl; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.GremlinDsl.AnonymousMethod; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.structure.Vertex; @@ -45,7 +46,7 @@ public interface SocialTraversalDsl<S, E> extends GraphTraversal.Admin<S, E> { * @param personName the name of the person to filter on */ public default GraphTraversal<S, Vertex> knows(String personName) { - return out("knows").hasLabel("person").has("name", personName); + return ((SocialTraversalDsl) out("knows")).person().has("name", personName); } /** @@ -53,7 +54,7 @@ public interface SocialTraversalDsl<S, E> extends GraphTraversal.Admin<S, E> { * those persons. */ public default <E2 extends Number> GraphTraversal<S, E2> youngestFriendsAge() { - return out("knows").hasLabel("person").values("age").min(); + return ((SocialTraversalDsl) out("knows")).person().values("age").min(); } /** @@ -64,4 +65,12 @@ public interface SocialTraversalDsl<S, E> extends GraphTraversal.Admin<S, E> { public default GraphTraversal<S, Long> createdAtLeast(int number) { return outE("created").count().is(P.gte(number)); } + + /** + * Filters objects by the "person" label. This step is designed to work with incoming vertices. + */ + @GremlinDsl.AnonymousMethod(returnTypeParameters = {"A", "A"}, methodTypeParameters = {"A"}) + public default GraphTraversal<S, E> person() { + return hasLabel("person"); + } } http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/daa442cd/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java ---------------------------------------------------------------------- diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java index d3a807f..700c8ee 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDsl.java @@ -74,4 +74,24 @@ public @interface GremlinDsl { * </ul> */ public boolean generateDefaultMethods() default true; + + /** + * Annotation that allows the user to directly override the type parameters on generated anonymous methods. If this + * annotation is not specified then the processor will attempt to infer the correct type parameters to use when + * generating the anonymous method representations of the DSL methods. + */ + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.CLASS) + public @interface AnonymousMethod { + + /** + * The type parameters to apply to the return type of the method applied in the order that they are specified. + */ + public String[] returnTypeParameters() default {}; + + /** + * The type parameters to apply to the method in the order that they are specified. + */ + public String[] methodTypeParameters() default {}; + } } http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/daa442cd/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDslProcessor.java ---------------------------------------------------------------------- diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDslProcessor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDslProcessor.java index 470dd4b..cdfad6a 100644 --- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDslProcessor.java +++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/dsl/GremlinDslProcessor.java @@ -61,9 +61,11 @@ import javax.tools.Diagnostic; import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * A custom Java annotation processor for the {@link GremlinDsl} annotation that helps to generate DSLs classes. @@ -135,24 +137,39 @@ public class GremlinDslProcessor extends AbstractProcessor { // process the methods of the GremlinDsl annotated class for (ExecutableElement templateMethod : findMethodsOfElement(ctx.annotatedDslType, null)) { + final Optional<GremlinDsl.AnonymousMethod> methodAnnotation = Optional.ofNullable(templateMethod.getAnnotation(GremlinDsl.AnonymousMethod.class)); + final String methodName = templateMethod.getSimpleName().toString(); - final TypeName returnType = getReturnTypeDefinition(ctx.traversalClassName, templateMethod); + // either use the direct return type of the DSL specification or override it with specification from + // GremlinDsl.AnonymousMethod + final TypeName returnType = methodAnnotation.isPresent() && methodAnnotation.get().returnTypeParameters().length > 0 ? + getOverridenReturnTypeDefinition(ctx.traversalClassName, methodAnnotation.get().returnTypeParameters()) : + getReturnTypeDefinition(ctx.traversalClassName, templateMethod); + final MethodSpec.Builder methodToAdd = MethodSpec.methodBuilder(methodName) .addModifiers(Modifier.STATIC, Modifier.PUBLIC) .addExceptions(templateMethod.getThrownTypes().stream().map(TypeName::get).collect(Collectors.toList())) .returns(returnType); - templateMethod.getTypeParameters().forEach(tp -> methodToAdd.addTypeVariable(TypeVariableName.get(tp))); - - // might have to deal with an "S" (in __ it's usually an "A") - how to make this less bound to that convention? - final List<? extends TypeMirror> returnTypeArguments = getTypeArguments(templateMethod); - returnTypeArguments.stream().filter(rtm -> rtm instanceof TypeVariable).forEach(rtm -> { - if (((TypeVariable) rtm).asElement().getSimpleName().contentEquals("S")) - methodToAdd.addTypeVariable(TypeVariableName.get(((TypeVariable) rtm).asElement().getSimpleName().toString())); - }); + // either use the method type parameter specified from the GremlinDsl.AnonymousMethod or just infer them + // from the DSL specification. "inferring" relies on convention and sometimes doesn't work for all cases. + final String startGeneric = methodAnnotation.isPresent() && methodAnnotation.get().methodTypeParameters().length > 0 ? + methodAnnotation.get().methodTypeParameters()[0] : "S"; + if (methodAnnotation.isPresent() && methodAnnotation.get().methodTypeParameters().length > 0) + Stream.of(methodAnnotation.get().methodTypeParameters()).map(TypeVariableName::get).forEach(methodToAdd::addTypeVariable); + else { + templateMethod.getTypeParameters().forEach(tp -> methodToAdd.addTypeVariable(TypeVariableName.get(tp))); + + // might have to deal with an "S" (in __ it's usually an "A") - how to make this less bound to that convention? + final List<? extends TypeMirror> returnTypeArguments = getTypeArguments(templateMethod); + returnTypeArguments.stream().filter(rtm -> rtm instanceof TypeVariable).forEach(rtm -> { + if (((TypeVariable) rtm).asElement().getSimpleName().contentEquals("S")) + methodToAdd.addTypeVariable(TypeVariableName.get(((TypeVariable) rtm).asElement().getSimpleName().toString())); + }); + } - addMethodBody(methodToAdd, templateMethod, "return __.<S>start().$L(", ")", methodName); + addMethodBody(methodToAdd, templateMethod, "return __.<" + startGeneric + ">start().$L(", ")", methodName); anonymousClass.addMethod(methodToAdd.build()); } @@ -425,6 +442,27 @@ public class GremlinDslProcessor extends AbstractProcessor { methodToAdd.addStatement(body, statementArgs); } + private TypeName getOverridenReturnTypeDefinition(final ClassName returnClazz, final String[] typeValues) { + return ParameterizedTypeName.get(returnClazz, Stream.of(typeValues).map(tv -> { + try { + return ClassName.get(Class.forName(tv)); + } catch (ClassNotFoundException cnfe) { + if (tv.contains("extends")) { + final String[] sides = tv.toString().split(" extends "); + final TypeVariableName name = TypeVariableName.get(sides[0]); + try { + name.withBounds(ClassName.get(Class.forName(sides[1]))); + } catch (Exception ex) { + name.withBounds(TypeVariableName.get(sides[1])); + } + return name; + } else { + return TypeVariableName.get(tv); + } + } + }).collect(Collectors.toList()).toArray(new TypeName[typeValues.length])); + } + private TypeName getReturnTypeDefinition(final ClassName returnClazz, final ExecutableElement templateMethod) { final List<? extends TypeMirror> returnTypeArguments = getTypeArguments(templateMethod); http://git-wip-us.apache.org/repos/asf/tinkerpop/blob/daa442cd/gremlin-core/src/test/resources/org/apache/tinkerpop/gremlin/process/traversal/dsl/SocialTraversalDsl.java ---------------------------------------------------------------------- diff --git a/gremlin-core/src/test/resources/org/apache/tinkerpop/gremlin/process/traversal/dsl/SocialTraversalDsl.java b/gremlin-core/src/test/resources/org/apache/tinkerpop/gremlin/process/traversal/dsl/SocialTraversalDsl.java index 4c31330..66db128 100644 --- a/gremlin-core/src/test/resources/org/apache/tinkerpop/gremlin/process/traversal/dsl/SocialTraversalDsl.java +++ b/gremlin-core/src/test/resources/org/apache/tinkerpop/gremlin/process/traversal/dsl/SocialTraversalDsl.java @@ -18,9 +18,9 @@ */ package org.apache.tinkerpop.gremlin.process.traversal.dsl; -import org.apache.tinkerpop.gremlin.process.traversal.dsl.GremlinDsl; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.process.traversal.P; /** * @author Stephen Mallette (http://stephen.genoprime.com) @@ -28,10 +28,25 @@ import org.apache.tinkerpop.gremlin.structure.Vertex; @GremlinDsl public interface SocialTraversalDsl<S, E> extends GraphTraversal.Admin<S, E> { public default GraphTraversal<S, Vertex> knows(final String personName) { - return out("knows").hasLabel("person").has("name", personName); + return ((SocialTraversalDsl) out("knows")).person().has("name", personName); } public default <E2 extends Number> GraphTraversal<S, E2> meanAgeOfFriends() { + return ((SocialTraversalDsl) out("knows")).person().values("age").mean(); + } + + @GremlinDsl.AnonymousMethod(returnTypeParameters = {"A", "A"}, methodTypeParameters = {"A"}) + public default GraphTraversal<S, E> person() { + return hasLabel("person"); + } + + @GremlinDsl.AnonymousMethod(returnTypeParameters = {"A", "org.apache.tinkerpop.gremlin.structure.Vertex"}, methodTypeParameters = {"A"}) + public default GraphTraversal<S, Vertex> knowsOverride(final String personName) { + return out("knows").hasLabel("person").has("name", personName); + } + + @GremlinDsl.AnonymousMethod(returnTypeParameters = {"A", "E2"}, methodTypeParameters = {"A", "E2 extends java.lang.Number"}) + public default <E2 extends Number> GraphTraversal<S, E2> meanAgeOfFriendsOverride() { return out("knows").hasLabel("person").values("age").mean(); } }