Skip to content

Commit 4089155

Browse files
GH-2279 - Provide a SpEL-Expression to refer to the root entity in string based queries.
`#{#staticLabels}` will now resolve to the list of all static labels of a maped root entity in repository methods annotated with `@Query`. This closes #2279.
1 parent 7b06ea0 commit 4089155

File tree

6 files changed

+143
-4
lines changed

6 files changed

+143
-4
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.springframework.data.repository.query.ResultProcessor;
4646
import org.springframework.data.repository.query.ReturnedType;
4747
import org.springframework.data.util.ClassTypeInformation;
48+
import org.springframework.expression.spel.standard.SpelExpressionParser;
4849
import org.springframework.lang.Nullable;
4950
import org.springframework.util.Assert;
5051

@@ -58,6 +59,8 @@
5859
*/
5960
abstract class Neo4jQuerySupport {
6061

62+
protected static final SpelExpressionParser SPEL_EXPRESSION_PARSER = new SpelExpressionParser();
63+
6164
protected final Neo4jMappingContext mappingContext;
6265
protected final Neo4jQueryMethod queryMethod;
6366
/**

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

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,29 @@
1616
package org.springframework.data.neo4j.repository.query;
1717

1818
import java.util.LinkedHashMap;
19+
import java.util.Locale;
1920
import java.util.Map;
21+
import java.util.regex.Matcher;
22+
import java.util.regex.Pattern;
23+
import java.util.stream.Collectors;
2024

2125
import org.apiguardian.api.API;
2226
import org.springframework.data.domain.Pageable;
2327
import org.springframework.data.domain.Sort;
2428
import org.springframework.data.neo4j.core.mapping.CypherGenerator;
29+
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
30+
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
31+
import org.springframework.data.repository.core.EntityMetadata;
32+
import org.springframework.expression.Expression;
33+
import org.springframework.expression.ParserContext;
34+
import org.springframework.expression.spel.standard.SpelExpressionParser;
35+
import org.springframework.expression.spel.support.StandardEvaluationContext;
2536
import org.springframework.lang.Nullable;
37+
import org.springframework.util.Assert;
2638

2739
/**
28-
* This class provides a couple of extensions to the Spring Data Neo4j SpEL support and is registered by
29-
* the appropriate repository factories as a root bean.
40+
* This class provides a couple of extensions to the Spring Data Neo4j SpEL support. It's static functions are registered
41+
* inside an {@link org.springframework.data.spel.spi.EvaluationContextExtension} that in turn will be provided as a root bean.
3042
*
3143
* @author Michael J. Simons
3244
* @soundtrack Red Hot Chili Peppers - Californication
@@ -138,4 +150,68 @@ public Target getTarget() {
138150
}
139151
}
140152

153+
private static final Pattern LABEL_AND_TYPE_QUOTATION = Pattern.compile("`");
154+
private static final String EXPRESSION_PARAMETER = "$1#{";
155+
private static final String QUOTED_EXPRESSION_PARAMETER = "$1__HASH__{";
156+
157+
private static final String ENTITY_NAME = "staticLabels";
158+
private static final String ENTITY_NAME_VARIABLE = "#" + ENTITY_NAME;
159+
private static final String ENTITY_NAME_VARIABLE_EXPRESSION = "#{" + ENTITY_NAME_VARIABLE + "}";
160+
161+
private static final Pattern EXPRESSION_PARAMETER_QUOTING = Pattern.compile("([:?])#\\{(?!" + ENTITY_NAME_VARIABLE + ")");
162+
private static final Pattern EXPRESSION_PARAMETER_UNQUOTING = Pattern.compile("([:?])__HASH__\\{");
163+
164+
/**
165+
* @param query the query expression potentially containing a SpEL expression. Must not be {@literal null}.
166+
* @param metadata the {@link Neo4jPersistentEntity} for the given entity. Must not be {@literal null}.
167+
* @param parser Must not be {@literal null}.
168+
* @return A query in which some SpEL expression have been replaced with the result of evaluating the expression
169+
*/
170+
public static String renderQueryIfExpressionOrReturnQuery(String query, Neo4jMappingContext mappingContext, EntityMetadata<?> metadata,
171+
SpelExpressionParser parser) {
172+
173+
Assert.notNull(query, "query must not be null!");
174+
Assert.notNull(metadata, "metadata must not be null!");
175+
Assert.notNull(parser, "parser must not be null!");
176+
177+
if (!containsExpression(query)) {
178+
return query;
179+
}
180+
181+
StandardEvaluationContext evalContext = new StandardEvaluationContext();
182+
Neo4jPersistentEntity<?> requiredPersistentEntity = mappingContext
183+
.getRequiredPersistentEntity(metadata.getJavaType());
184+
evalContext.setVariable(ENTITY_NAME, requiredPersistentEntity.getStaticLabels()
185+
.stream()
186+
.map(l -> {
187+
Matcher matcher = LABEL_AND_TYPE_QUOTATION.matcher(l);
188+
return String.format(Locale.ENGLISH, "`%s`", matcher.replaceAll("``"));
189+
})
190+
.collect(Collectors.joining(":")));
191+
192+
query = potentiallyQuoteExpressionsParameter(query);
193+
194+
Expression expr = parser.parseExpression(query, ParserContext.TEMPLATE_EXPRESSION);
195+
196+
String result = expr.getValue(evalContext, String.class);
197+
198+
if (result == null) {
199+
return query;
200+
}
201+
202+
return potentiallyUnquoteParameterExpressions(result);
203+
}
204+
205+
static String potentiallyUnquoteParameterExpressions(String result) {
206+
return EXPRESSION_PARAMETER_UNQUOTING.matcher(result).replaceAll(EXPRESSION_PARAMETER);
207+
}
208+
209+
static String potentiallyQuoteExpressionsParameter(String query) {
210+
return EXPRESSION_PARAMETER_QUOTING.matcher(query).replaceAll(QUOTED_EXPRESSION_PARAMETER);
211+
}
212+
213+
214+
private static boolean containsExpression(String query) {
215+
return query.contains(ENTITY_NAME_VARIABLE_EXPRESSION);
216+
}
141217
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ private ReactiveStringBasedNeo4jQuery(ReactiveNeo4jOperations neo4jOperations, N
117117

118118
super(neo4jOperations, mappingContext, queryMethod, queryType);
119119

120+
cypherTemplate = Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(cypherTemplate, mappingContext, queryMethod.getEntityInformation(), SPEL_EXPRESSION_PARSER);
120121
SpelExtractor spelExtractor = SPEL_QUERY_CONTEXT.parse(cypherTemplate);
121122
this.spelEvaluator = new SpelEvaluator(evaluationContextProvider, queryMethod.getParameters(), spelExtractor);
122123
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,12 @@ private StringBasedNeo4jQuery(Neo4jOperations neo4jOperations, Neo4jMappingConte
161161
super(neo4jOperations, mappingContext, queryMethod, queryType);
162162

163163
Parameters<?, ?> methodParameters = queryMethod.getParameters();
164+
cypherTemplate = Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(cypherTemplate, mappingContext, queryMethod.getEntityInformation(), SPEL_EXPRESSION_PARSER);
164165
this.spelEvaluator = new SpelEvaluator(
165166
evaluationContextProvider, methodParameters, SPEL_QUERY_CONTEXT.parse(cypherTemplate));
166167
this.spelEvaluatorForCountQuery = queryMethod.getQueryAnnotation()
167168
.map(Query::countQuery)
169+
.map(q -> Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(q, mappingContext, queryMethod.getEntityInformation(), SPEL_EXPRESSION_PARSER))
168170
.map(countQuery -> new SpelEvaluator(evaluationContextProvider, methodParameters, SPEL_QUERY_CONTEXT.parse(countQuery)));
169171
}
170172

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3972,8 +3972,8 @@ interface PetRepository extends Neo4jRepository<Pet, Long> {
39723972
@Query(value = "MATCH (p:Pet) return p SKIP $skip LIMIT $limit", countQuery = "MATCH (p:Pet) return count(p)")
39733973
Slice<Pet> slicedPets(Pageable pageable);
39743974

3975-
@Query(value = "MATCH (p:Pet) where p.name=$petName return p SKIP $skip LIMIT $limit",
3976-
countQuery = "MATCH (p:Pet) return count(p)")
3975+
@Query(value = "MATCH (p:#{#staticLabels}) where p.name=$petName return p SKIP $skip LIMIT $limit",
3976+
countQuery = "MATCH (p:#{#staticLabels}) return count(p)")
39773977
Page<Pet> pagedPetsWithParameter(@Param("petName") String petName, Pageable pageable);
39783978

39793979
Pet findByFriendsName(String friendName);

src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTest.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,19 @@
1818
import static org.assertj.core.api.Assertions.assertThat;
1919
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
2020

21+
import java.util.Collections;
22+
2123
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.CsvSource;
2226
import org.springframework.data.domain.PageRequest;
2327
import org.springframework.data.domain.Sort;
28+
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
29+
import org.springframework.data.neo4j.core.schema.Id;
30+
import org.springframework.data.neo4j.core.schema.Node;
2431
import org.springframework.data.neo4j.repository.query.Neo4jSpelSupport.LiteralReplacement;
32+
import org.springframework.data.repository.core.EntityMetadata;
33+
import org.springframework.expression.spel.standard.SpelExpressionParser;
2534

2635
/**
2736
* @author Michael J. Simons
@@ -77,4 +86,52 @@ void cacheShouldWork() {
7786
LiteralReplacement literalReplacement2 = Neo4jSpelSupport.literal("x");
7887
assertThat(literalReplacement1).isSameAs(literalReplacement2);
7988
}
89+
90+
@ParameterizedTest // GH-2279
91+
@CsvSource({
92+
"MATCH (n:Something) WHERE n.name = ?#{#name}, MATCH (n:Something) WHERE n.name = ?__HASH__{#name}",
93+
"MATCH (n:Something) WHERE n.name = :#{#name}, MATCH (n:Something) WHERE n.name = :__HASH__{#name}"
94+
})
95+
void shouldQuoteParameterExpressionsCorrectly(String query, String expected) {
96+
97+
String quoted = Neo4jSpelSupport.potentiallyQuoteExpressionsParameter(query);
98+
assertThat(quoted).isEqualTo(expected);
99+
}
100+
101+
@ParameterizedTest // GH-2279
102+
@CsvSource({
103+
"MATCH (n:Something) WHERE n.name = ?__HASH__{#name}, MATCH (n:Something) WHERE n.name = ?#{#name}",
104+
"MATCH (n:Something) WHERE n.name = :__HASH__{#name}, MATCH (n:Something) WHERE n.name = :#{#name}"
105+
})
106+
void shouldUnquoteParameterExpressionsCorrectly(String quoted, String expected) {
107+
108+
String query = Neo4jSpelSupport.potentiallyUnquoteParameterExpressions(quoted);
109+
assertThat(query).isEqualTo(expected);
110+
}
111+
112+
@Test // GH-2279
113+
void shouldQuoteParameterExpressionsCorrectly() {
114+
115+
String quoted = Neo4jSpelSupport.potentiallyQuoteExpressionsParameter("MATCH (n:#{#staticLabels}) WHERE n.name = ?#{#name}");
116+
assertThat(quoted).isEqualTo("MATCH (n:#{#staticLabels}) WHERE n.name = ?__HASH__{#name}");
117+
}
118+
119+
@Test // GH-2279
120+
void shouldReplaceStaticLabels() {
121+
122+
Neo4jMappingContext schema = new Neo4jMappingContext();
123+
schema.setInitialEntitySet(Collections.singleton(BikeNode.class));
124+
125+
String query = Neo4jSpelSupport.renderQueryIfExpressionOrReturnQuery(
126+
"MATCH (n:#{#staticLabels}) WHERE n.name = ?#{#name} OR n.name = :?#{#name} RETURN n",
127+
new Neo4jMappingContext(), (EntityMetadata<BikeNode>) () -> BikeNode.class, new SpelExpressionParser());
128+
129+
assertThat(query).isEqualTo("MATCH (n:`Bike`:`Gravel`:`Easy Trail`) WHERE n.name = ?#{#name} OR n.name = :?#{#name} RETURN n");
130+
131+
}
132+
133+
@Node(primaryLabel = "Bike", labels = {"Gravel", "Easy Trail"})
134+
static class BikeNode {
135+
@Id String id;
136+
}
80137
}

0 commit comments

Comments
 (0)