diff --git a/src/main/java/com/microsoft/spring/data/gremlin/annotation/Query.java b/src/main/java/com/microsoft/spring/data/gremlin/annotation/Query.java new file mode 100644 index 00000000..af5dba29 --- /dev/null +++ b/src/main/java/com/microsoft/spring/data/gremlin/annotation/Query.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.spring.data.gremlin.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.annotation.QueryAnnotation; + +/** + * Annotation to declare Parameterized queries to be defined as String. + * Inspired from Spring Neo4j implementation + * + * @author Ganesh Guttikonda + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@QueryAnnotation +@Documented +public @interface Query { + + /** + * Defines the Gremlin query to be executed when the annotated method is called. + */ + String value() default ""; + +} diff --git a/src/main/java/com/microsoft/spring/data/gremlin/query/GremlinTemplate.java b/src/main/java/com/microsoft/spring/data/gremlin/query/GremlinTemplate.java index f84a554b..121bf91d 100644 --- a/src/main/java/com/microsoft/spring/data/gremlin/query/GremlinTemplate.java +++ b/src/main/java/com/microsoft/spring/data/gremlin/query/GremlinTemplate.java @@ -348,7 +348,7 @@ private T recoverDomain(@NonNull GremlinSource source, @NonNull List List recoverDomainList(@NonNull GremlinSource source, @NonNull List results) { + public List recoverDomainList(@NonNull GremlinSource source, @NonNull List results) { return results.stream().map(r -> recoverDomain(source, Collections.singletonList(r))).collect(toList()); } diff --git a/src/main/java/com/microsoft/spring/data/gremlin/query/query/GraphRepositoryGremlinQuery.java b/src/main/java/com/microsoft/spring/data/gremlin/query/query/GraphRepositoryGremlinQuery.java new file mode 100644 index 00000000..8f4452e8 --- /dev/null +++ b/src/main/java/com/microsoft/spring/data/gremlin/query/query/GraphRepositoryGremlinQuery.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.spring.data.gremlin.query.query; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import org.apache.tinkerpop.gremlin.driver.Client; +import org.apache.tinkerpop.gremlin.driver.Result; +import org.apache.tinkerpop.gremlin.driver.ResultSet; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.lang.NonNull; + +import com.microsoft.spring.data.gremlin.common.GremlinUtils; +import com.microsoft.spring.data.gremlin.conversion.source.GremlinSource; +import com.microsoft.spring.data.gremlin.query.GremlinOperations; +import com.microsoft.spring.data.gremlin.query.GremlinTemplate; +import com.microsoft.spring.data.gremlin.query.paramerter.GremlinParameterAccessor; +import com.microsoft.spring.data.gremlin.query.paramerter.GremlinParametersParameterAccessor; + +public class GraphRepositoryGremlinQuery extends AbstractGremlinQuery { + + private final GremlinQueryMethod method; + private final GremlinOperations operations; + private final Client gremlinClient; + + public GraphRepositoryGremlinQuery(@NonNull Client gremlinClient, @NonNull GremlinQueryMethod method, + @NonNull GremlinOperations operations) { + super(method, operations); + this.gremlinClient = gremlinClient; + this.method = method; + this.operations = operations; + } + + @Override + protected GremlinQuery createQuery(GremlinParameterAccessor accessor) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + @Override + public Object execute(@NonNull Object[] parameters) { + final String query = method.getQuery(); + final Map params = this.resolveParams(this.method.getParameters(), parameters); + final GremlinParameterAccessor accessor = new GremlinParametersParameterAccessor(this.method, parameters); + final ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); + final Class methodReturnType = processor.getReturnedType().getDomainType(); + final ResultSet rs = this.gremlinClient.submit(query, params); + + if (ResultSet.class.equals(methodReturnType)) { + return rs; + } + + final GremlinSource source = GremlinUtils.toGremlinSource(methodReturnType); + if (GremlinTemplate.class.equals(this.operations.getClass())) { + try { + final List gremlinResults = rs.all().get(); + final List results = ((GremlinTemplate) this.operations).recoverDomainList(source, gremlinResults); + if (results == null || results.isEmpty()) { + //return null for not found results + return null; + } + + if (results.size() == 1) { + // return pojo instead of list + return results.get(0); + } + return results; + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } catch (ExecutionException e) { + throw new IllegalStateException(e); + } + } + + throw new UnsupportedOperationException(methodReturnType + " is not handled by deserializer!"); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected Map resolveParams(Parameters methodParameters, Object[] parameters) { + + final Map resolvedParameters = new HashMap<>(); + + for (final Parameter parameter : methodParameters) { + final int parameterIndex = parameter.getIndex(); + final Object parameterValue = parameters[parameterIndex]; + // Convenience! Client can simply pass Map params, + // we automatically resolve them to individual parameters. + // this is to allow the pass through for GremlinClient + if (parameterValue instanceof Map) { + resolvedParameters.putAll((Map) parameterValue); + } + parameter.getName().ifPresent(parameterName -> resolvedParameters.put(parameterName, parameterValue)); + + } + return resolvedParameters; + } + + @Override + @NonNull + public GremlinQueryMethod getQueryMethod() { + return this.method; + } + +} diff --git a/src/main/java/com/microsoft/spring/data/gremlin/query/query/GremlinQueryMethod.java b/src/main/java/com/microsoft/spring/data/gremlin/query/query/GremlinQueryMethod.java index b38dc7b5..3c409512 100644 --- a/src/main/java/com/microsoft/spring/data/gremlin/query/query/GremlinQueryMethod.java +++ b/src/main/java/com/microsoft/spring/data/gremlin/query/query/GremlinQueryMethod.java @@ -5,21 +5,30 @@ */ package com.microsoft.spring.data.gremlin.query.query; +import com.microsoft.spring.data.gremlin.annotation.Query; import com.microsoft.spring.data.gremlin.query.GremlinEntityMetadata; import com.microsoft.spring.data.gremlin.query.SimpleGremlinEntityMetadata; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.EntityMetadata; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.util.StringUtils; import java.lang.reflect.Method; public class GremlinQueryMethod extends QueryMethod { private GremlinEntityMetadata metadata; + private final Query queryAnnotation; + private final Method method; public GremlinQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { super(method, metadata, factory); + this.queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); + this.method = method; } @Override @@ -30,4 +39,22 @@ public EntityMetadata getEntityInformation() { return this.metadata; } + + public String getQuery() { + return queryAnnotation.value(); + } + + public boolean hasAnnotatedQuery() { + return getAnnotatedQuery() != null; + } + + private String getAnnotatedQuery() { + + final String query = (String) AnnotationUtils.getValue(getQueryAnnotation()); + return StringUtils.hasText(query) ? query : null; + } + + private Query getQueryAnnotation() { + return AnnotatedElementUtils.findMergedAnnotation(method, Query.class); + } } diff --git a/src/main/java/com/microsoft/spring/data/gremlin/repository/support/GremlinRepositoryFactory.java b/src/main/java/com/microsoft/spring/data/gremlin/repository/support/GremlinRepositoryFactory.java index 67391a0d..1ae859ed 100644 --- a/src/main/java/com/microsoft/spring/data/gremlin/repository/support/GremlinRepositoryFactory.java +++ b/src/main/java/com/microsoft/spring/data/gremlin/repository/support/GremlinRepositoryFactory.java @@ -5,7 +5,9 @@ */ package com.microsoft.spring.data.gremlin.repository.support; +import com.microsoft.spring.data.gremlin.common.GremlinFactory; import com.microsoft.spring.data.gremlin.query.GremlinOperations; +import com.microsoft.spring.data.gremlin.query.query.GraphRepositoryGremlinQuery; import com.microsoft.spring.data.gremlin.query.query.GremlinQueryMethod; import com.microsoft.spring.data.gremlin.query.query.PartTreeGremlinQuery; import org.springframework.context.ApplicationContext; @@ -55,14 +57,16 @@ public EntityInformation getEntityInformation(Class domainClas @Override protected Optional getQueryLookupStrategy( QueryLookupStrategy.Key key, QueryMethodEvaluationContextProvider provider) { - return Optional.of(new GremlinQueryLookupStrategy(this.operations)); + return Optional.of(new GremlinQueryLookupStrategy(this.context, this.operations)); } private static class GremlinQueryLookupStrategy implements QueryLookupStrategy { private final GremlinOperations operations; + private final ApplicationContext context; - public GremlinQueryLookupStrategy(@NonNull GremlinOperations operations) { + public GremlinQueryLookupStrategy(@NonNull ApplicationContext context, @NonNull GremlinOperations operations) { + this.context = context; this.operations = operations; } @@ -73,7 +77,12 @@ public RepositoryQuery resolveQuery(@NonNull Method method, RepositoryMetadata m Assert.notNull(queryMethod, "queryMethod should not be null"); Assert.notNull(this.operations, "operations should not be null"); - + if (queryMethod.hasAnnotatedQuery()) { + final GremlinFactory gremlinFactory = context.getBean(GremlinFactory.class); + Assert.notNull(gremlinFactory, "gremlinFactory bean should not be null"); + return new GraphRepositoryGremlinQuery(gremlinFactory.getGremlinClient(), queryMethod, operations); + } + return new PartTreeGremlinQuery(queryMethod, this.operations); } } diff --git a/src/test/java/com/microsoft/spring/data/gremlin/common/repository/PersonRepository.java b/src/test/java/com/microsoft/spring/data/gremlin/common/repository/PersonRepository.java index c2e7e80b..1f7c54b0 100644 --- a/src/test/java/com/microsoft/spring/data/gremlin/common/repository/PersonRepository.java +++ b/src/test/java/com/microsoft/spring/data/gremlin/common/repository/PersonRepository.java @@ -5,10 +5,16 @@ */ package com.microsoft.spring.data.gremlin.common.repository; +import com.microsoft.spring.data.gremlin.annotation.Query; import com.microsoft.spring.data.gremlin.common.domain.Person; import com.microsoft.spring.data.gremlin.repository.GremlinRepository; + +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface PersonRepository extends GremlinRepository { + + @Query("g.V().has('name', name)") + public Person findPersonByName(@Param("name") String name); } diff --git a/src/test/java/com/microsoft/spring/data/gremlin/repository/PersonRepositoryIT.java b/src/test/java/com/microsoft/spring/data/gremlin/repository/PersonRepositoryIT.java index 4b00a9bd..1f63e1e5 100644 --- a/src/test/java/com/microsoft/spring/data/gremlin/repository/PersonRepositoryIT.java +++ b/src/test/java/com/microsoft/spring/data/gremlin/repository/PersonRepositoryIT.java @@ -32,6 +32,7 @@ public class PersonRepositoryIT { private final Person person = new Person(TestConstants.VERTEX_PERSON_ID, TestConstants.VERTEX_PERSON_NAME); private final Person person0 = new Person(TestConstants.VERTEX_PERSON_0_ID, TestConstants.VERTEX_PERSON_0_NAME); + private final Person person1 = new Person(TestConstants.VERTEX_PERSON_1_ID, TestConstants.VERTEX_PERSON_1_NAME); private final Project project = new Project(TestConstants.VERTEX_PROJECT_ID, TestConstants.VERTEX_PROJECT_NAME, TestConstants.VERTEX_PROJECT_URI); @@ -231,5 +232,17 @@ public void testFindAll() { this.repository.deleteAll(); Assert.assertFalse(this.repository.findAll().iterator().hasNext()); } + + + @Test + public void testQueryAnnotation() { + final Person result = this.repository.save(this.person1); + Assert.assertNotNull(result); + final Person foundPerson = this.repository.findPersonByName(this.person1.getName()); + + Assert.assertNotNull(foundPerson); + Assert.assertEquals(foundPerson.getId(), this.person1.getId()); + Assert.assertEquals(foundPerson.getName(), this.person1.getName()); + } }