diff --git a/src/main/asciidoc/appendix/custom-queries.adoc b/src/main/asciidoc/appendix/custom-queries.adoc index 7422f3594..7a0a2f90b 100644 --- a/src/main/asciidoc/appendix/custom-queries.adoc +++ b/src/main/asciidoc/appendix/custom-queries.adoc @@ -420,11 +420,16 @@ public interface MyPersonRepository extends Neo4jRepository { <.> A `Pageable` has always the name `pageable` inside the SpEL context. <.> A `Sort` has always the name `sort` inside the SpEL context. +[[spel-extensions]] +=== Spring Expression Language extensions + +[[literal-extension]] +==== Literal extension + The `literal` extension can be used to make things like labels or relationship-types "dynamic" in custom queries. Neither labels nor relationship types can be parameterized in Cypher, so they must be given literal. [source,java] -[[literal-extension]] .literal-Extension ---- interface BaseClassRepository extends Neo4jRepository { @@ -440,6 +445,25 @@ If you pass in `SomeLabel` as a parameter to the method, `MATCH (n:``SomeLabel`` will be generated. Ticks have been added to correctly escape values. SDN won't do this for you as this is probably not what you want in all cases. +[[list-extensions]] +==== List extensions + +For more than one value there are `allOf` and `anyOf` in place that would render +either a `&` or `|` concatenated list of all values. + +[source,java] +.List extensions +---- +interface BaseClassRepository extends Neo4jRepository { + + @Query("MATCH (n:`:#{allOf(#label)}`) RETURN n") + List findByLabels(List labels); + + @Query("MATCH (n:`:#{anyOf(#label)}`) RETURN n") + List findByLabels(List labels); +} +---- + === Referring to Labels You already know how to map a Node to a domain object: diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java index 881e5a442..35b279d4b 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java @@ -15,6 +15,7 @@ */ package org.springframework.data.neo4j.repository.query; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; @@ -23,7 +24,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.apache.commons.logging.LogFactory; import org.apiguardian.api.API; +import org.springframework.core.log.LogAccessor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.core.mapping.CypherGenerator; @@ -49,8 +52,12 @@ import org.springframework.util.Assert; public final class Neo4jSpelSupport { public static String FUNCTION_LITERAL = "literal"; + public static String FUNCTION_ANY_OF = "anyOf"; + public static String FUNCTION_ALL_OF = "allOf"; public static String FUNCTION_ORDER_BY = "orderBy"; + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Neo4jSpelSupport.class)); + /** * Takes {@code arg} and tries to either extract a {@link Sort sort} from it or cast it to a sort. That sort is * than past to the {@link CypherGenerator} that renders a valid order by fragment which replaces the SpEL placeholder @@ -87,6 +94,34 @@ public final class Neo4jSpelSupport { return literalReplacement; } + public static LiteralReplacement anyOf(@Nullable Object arg) { + return labels(arg, "|"); + } + + public static LiteralReplacement allOf(@Nullable Object arg) { + return labels(arg, "&"); + } + + private static LiteralReplacement labels(@Nullable Object arg, String joinOn) { + return StringBasedLiteralReplacement + .withTargetAndValue(LiteralReplacement.Target.UNSPECIFIED, + arg == null ? "" : joinStrings(arg, joinOn) + ); + } + + private static String joinStrings(Object arg, String joinOn) { + if (arg instanceof Collection) { + return ((Collection) arg).stream().map(Object::toString).collect(Collectors.joining(joinOn)); + } + + // we are so kind and also accept plain strings instead of collection + if (arg instanceof String) { + return (String) arg; + } + + throw new IllegalArgumentException( + String.format("Cannot process argument %s. Please note that only Collection and String are supported types.", arg)); + } /** * A marker interface that indicates a literal replacement in a query instead of a parameter replacement. This * comes in handy in places where non-parameterizable things should be created dynamic, for example matching on diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java index 94f2d120d..5855b0f1b 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java @@ -45,12 +45,15 @@ public final class Neo4jEvaluationContextExtension implements EvaluationContextE @Override public Map getFunctions() { - Map functions = new HashMap<>(); functions.put(Neo4jSpelSupport.FUNCTION_ORDER_BY, new Function(ReflectionUtils .findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ORDER_BY, Object.class))); functions.put(Neo4jSpelSupport.FUNCTION_LITERAL, new Function(ReflectionUtils .findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_LITERAL, Object.class))); + functions.put(Neo4jSpelSupport.FUNCTION_ANY_OF, new Function(ReflectionUtils + .findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ANY_OF, Object.class))); + functions.put(Neo4jSpelSupport.FUNCTION_ALL_OF, new Function(ReflectionUtils + .findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ALL_OF, Object.class))); return functions; } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java index 76255b48a..ad926628a 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java @@ -3842,6 +3842,29 @@ class RepositoryIT { .first().isInstanceOf(Inheritance.ConcreteClassB.class) .extracting(Inheritance.BaseClass::getName) .isEqualTo("cc2"); + + List labels = new ArrayList<>(); + labels.add("ConcreteClassA"); + labels.add("ConcreteClassB"); + + assertThat(baseClassRepository.findByOrLabels(labels)).hasSize(2) + .hasOnlyElementsOfTypes(Inheritance.ConcreteClassA.class, Inheritance.ConcreteClassB.class) + .extracting(Inheritance.BaseClass::getName) + .containsExactlyInAnyOrder("cc1", "cc2"); + + assertThat(baseClassRepository.findByAndLabels(labels)).hasSize(0); + + String labelsString = "ConcreteClassA"; + assertThat(baseClassRepository.findByAndLabels(labelsString)).hasSize(1) + .first().isInstanceOf(Inheritance.ConcreteClassA.class) + .extracting(Inheritance.BaseClass::getName) + .isEqualTo("cc1"); + + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> baseClassRepository.findByAndLabels(1)) + .havingRootCause() + .isInstanceOf(IllegalArgumentException.class) + .withMessageContaining("Cannot process argument"); + } @Test @@ -4485,6 +4508,12 @@ class RepositoryIT { @Query("MATCH (n::#{literal(#label)}) RETURN n") List findByLabel(@Param("label") String label); + + @Query("MATCH (n::#{anyOf(#label)}) RETURN n") + List findByOrLabels(@Param("label") List labels); + + @Query("MATCH (n::#{allOf(#label)}) RETURN n") + List findByAndLabels(@Param("label") Object labels); } interface SuperBaseClassRepository extends Neo4jRepository {