Skip to content

Commit 3667fb5

Browse files
committed
GH-2681 - Add anyOf / allOf to SpEL.
Currently we only have the literals clause to render an arbitrary string into a custom Cypher statement. Although one could prepare the string upfront manually and joining multiple strings on | or & and pass this to the query method, it would be more convenient to add one or two functions directly. Namely those functions will be anyOf : renders input ["a","b", ..] to A|B|.. allOf : renders input ["a","b", ..] to A&B&.. The usage would be MATCH (n::#{allOf(#labels)}) RETURN n, for example. Closes #2681
1 parent 7b30ecf commit 3667fb5

File tree

4 files changed

+93
-2
lines changed

4 files changed

+93
-2
lines changed

src/main/asciidoc/appendix/custom-queries.adoc

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,11 +420,16 @@ public interface MyPersonRepository extends Neo4jRepository<Person, Long> {
420420
<.> A `Pageable` has always the name `pageable` inside the SpEL context.
421421
<.> A `Sort` has always the name `sort` inside the SpEL context.
422422

423+
[[spel-extensions]]
424+
=== Spring Expression Language extensions
425+
426+
[[literal-extension]]
427+
==== Literal extension
428+
423429
The `literal` extension can be used to make things like labels or relationship-types "dynamic" in custom queries.
424430
Neither labels nor relationship types can be parameterized in Cypher, so they must be given literal.
425431

426432
[source,java]
427-
[[literal-extension]]
428433
.literal-Extension
429434
----
430435
interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {
@@ -440,6 +445,25 @@ If you pass in `SomeLabel` as a parameter to the method, `MATCH (n:``SomeLabel``
440445
will be generated. Ticks have been added to correctly escape values. SDN won't do this
441446
for you as this is probably not what you want in all cases.
442447

448+
[[list-extensions]]
449+
==== List extensions
450+
451+
For more than one value there are `allOf` and `anyOf` in place that would render
452+
either a `&` or `|` concatenated list of all values.
453+
454+
[source,java]
455+
.List extensions
456+
----
457+
interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Long> {
458+
459+
@Query("MATCH (n:`:#{allOf(#label)}`) RETURN n")
460+
List<Inheritance.BaseClass> findByLabels(List<String> labels);
461+
462+
@Query("MATCH (n:`:#{anyOf(#label)}`) RETURN n")
463+
List<Inheritance.BaseClass> findByLabels(List<String> labels);
464+
}
465+
----
466+
443467
=== Referring to Labels
444468

445469
You already know how to map a Node to a domain object:

src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.neo4j.repository.query;
1717

18+
import java.util.Collection;
1819
import java.util.LinkedHashMap;
1920
import java.util.Locale;
2021
import java.util.Map;
@@ -23,7 +24,9 @@
2324
import java.util.regex.Pattern;
2425
import java.util.stream.Collectors;
2526

27+
import org.apache.commons.logging.LogFactory;
2628
import org.apiguardian.api.API;
29+
import org.springframework.core.log.LogAccessor;
2730
import org.springframework.data.domain.Pageable;
2831
import org.springframework.data.domain.Sort;
2932
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
@@ -49,8 +52,12 @@
4952
public final class Neo4jSpelSupport {
5053

5154
public static String FUNCTION_LITERAL = "literal";
55+
public static String FUNCTION_ANY_OF = "anyOf";
56+
public static String FUNCTION_ALL_OF = "allOf";
5257
public static String FUNCTION_ORDER_BY = "orderBy";
5358

59+
private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(Neo4jSpelSupport.class));
60+
5461
/**
5562
* Takes {@code arg} and tries to either extract a {@link Sort sort} from it or cast it to a sort. That sort is
5663
* than past to the {@link CypherGenerator} that renders a valid order by fragment which replaces the SpEL placeholder
@@ -87,6 +94,34 @@ public static LiteralReplacement literal(@Nullable Object arg) {
8794
return literalReplacement;
8895
}
8996

97+
public static LiteralReplacement anyOf(@Nullable Object arg) {
98+
return labels(arg, "|");
99+
}
100+
101+
public static LiteralReplacement allOf(@Nullable Object arg) {
102+
return labels(arg, "&");
103+
}
104+
105+
private static LiteralReplacement labels(@Nullable Object arg, String joinOn) {
106+
return StringBasedLiteralReplacement
107+
.withTargetAndValue(LiteralReplacement.Target.UNSPECIFIED,
108+
arg == null ? "" : joinStrings(arg, joinOn)
109+
);
110+
}
111+
112+
private static String joinStrings(Object arg, String joinOn) {
113+
if (arg instanceof Collection) {
114+
return ((Collection<?>) arg).stream().map(Object::toString).collect(Collectors.joining(joinOn));
115+
}
116+
117+
// we are so kind and also accept plain strings instead of collection<string>
118+
if (arg instanceof String) {
119+
return (String) arg;
120+
}
121+
122+
throw new IllegalArgumentException(
123+
String.format("Cannot process argument %s. Please note that only Collection<String> and String are supported types.", arg));
124+
}
90125
/**
91126
* A marker interface that indicates a literal replacement in a query instead of a parameter replacement. This
92127
* comes in handy in places where non-parameterizable things should be created dynamic, for example matching on

src/main/java/org/springframework/data/neo4j/repository/support/Neo4jEvaluationContextExtension.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,15 @@ public String getExtensionId() {
4545

4646
@Override
4747
public Map<String, Function> getFunctions() {
48-
4948
Map<String, Function> functions = new HashMap<>();
5049
functions.put(Neo4jSpelSupport.FUNCTION_ORDER_BY, new Function(ReflectionUtils
5150
.findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ORDER_BY, Object.class)));
5251
functions.put(Neo4jSpelSupport.FUNCTION_LITERAL, new Function(ReflectionUtils
5352
.findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_LITERAL, Object.class)));
53+
functions.put(Neo4jSpelSupport.FUNCTION_ANY_OF, new Function(ReflectionUtils
54+
.findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ANY_OF, Object.class)));
55+
functions.put(Neo4jSpelSupport.FUNCTION_ALL_OF, new Function(ReflectionUtils
56+
.findRequiredMethod(Neo4jSpelSupport.class, Neo4jSpelSupport.FUNCTION_ALL_OF, Object.class)));
5457

5558
return functions;
5659
}

src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3842,6 +3842,29 @@ void findByDynamicLabel(@Autowired BaseClassRepository baseClassRepository) {
38423842
.first().isInstanceOf(Inheritance.ConcreteClassB.class)
38433843
.extracting(Inheritance.BaseClass::getName)
38443844
.isEqualTo("cc2");
3845+
3846+
List<String> labels = new ArrayList<>();
3847+
labels.add("ConcreteClassA");
3848+
labels.add("ConcreteClassB");
3849+
3850+
assertThat(baseClassRepository.findByOrLabels(labels)).hasSize(2)
3851+
.hasOnlyElementsOfTypes(Inheritance.ConcreteClassA.class, Inheritance.ConcreteClassB.class)
3852+
.extracting(Inheritance.BaseClass::getName)
3853+
.containsExactlyInAnyOrder("cc1", "cc2");
3854+
3855+
assertThat(baseClassRepository.findByAndLabels(labels)).hasSize(0);
3856+
3857+
String labelsString = "ConcreteClassA";
3858+
assertThat(baseClassRepository.findByAndLabels(labelsString)).hasSize(1)
3859+
.first().isInstanceOf(Inheritance.ConcreteClassA.class)
3860+
.extracting(Inheritance.BaseClass::getName)
3861+
.isEqualTo("cc1");
3862+
3863+
assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> baseClassRepository.findByAndLabels(1))
3864+
.havingRootCause()
3865+
.isInstanceOf(IllegalArgumentException.class)
3866+
.withMessageContaining("Cannot process argument");
3867+
38453868
}
38463869

38473870
@Test
@@ -4485,6 +4508,12 @@ interface BaseClassRepository extends Neo4jRepository<Inheritance.BaseClass, Lon
44854508

44864509
@Query("MATCH (n::#{literal(#label)}) RETURN n")
44874510
List<Inheritance.BaseClass> findByLabel(@Param("label") String label);
4511+
4512+
@Query("MATCH (n::#{anyOf(#label)}) RETURN n")
4513+
List<Inheritance.BaseClass> findByOrLabels(@Param("label") List<String> labels);
4514+
4515+
@Query("MATCH (n::#{allOf(#label)}) RETURN n")
4516+
List<Inheritance.BaseClass> findByAndLabels(@Param("label") Object labels);
44884517
}
44894518

44904519
interface SuperBaseClassRepository extends Neo4jRepository<Inheritance.SuperBaseClass, Long> {

0 commit comments

Comments
 (0)