diff --git a/src/main/java/org/springframework/data/repository/query/parser/SpelEvaluator.java b/src/main/java/org/springframework/data/repository/query/SpelEvaluator.java similarity index 60% rename from src/main/java/org/springframework/data/repository/query/parser/SpelEvaluator.java rename to src/main/java/org/springframework/data/repository/query/SpelEvaluator.java index f01b8215c..626ab35db 100644 --- a/src/main/java/org/springframework/data/repository/query/parser/SpelEvaluator.java +++ b/src/main/java/org/springframework/data/repository/query/SpelEvaluator.java @@ -13,40 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.repository.query.parser; +package org.springframework.data.repository.query; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; -import org.springframework.data.repository.query.EvaluationContextProvider; -import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * Evaluates SpEL expressions as extracted by the SpelExtractor based on parameter information from a method and + * Evaluates SpEL expressions as extracted by the {@link SpelExtractor} based on parameter information from a method and * parameter values from a method call. * * @author Jens Schauder * @author Gerrit Meier + * @author Oliver Gierke + * @since 2.1 + * @see SpelQueryContext#parse(String, Parameters) */ @RequiredArgsConstructor public class SpelEvaluator { private final static SpelExpressionParser PARSER = new SpelExpressionParser(); - @NonNull private final EvaluationContextProvider evaluationContextProvider; - @NonNull private final Parameters parameters; - - /** - * A map from parameter name to SpEL expression as returned by {@link SpelQueryContext.SpelExtractor#parameterNameToSpelMap()}. - */ - @NonNull private final Map parameterNameToSpelMap; + private final @NonNull QueryMethodEvaluationContextProvider evaluationContextProvider; + private final @NonNull Parameters parameters; + private final @NonNull SpelExtractor extractor; /** * Evaluate all the SpEL expressions in {@link #parameterNameToSpelMap} based on values provided as an argument. @@ -58,19 +56,25 @@ public class SpelEvaluator { Assert.notNull(values, "Values must not be null."); - HashMap spelExpressionResults = new HashMap<>(); EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(parameters, values); - for (Map.Entry parameterNameToSpel : parameterNameToSpelMap.entrySet()) { - Object spElValue = getSpElValue(evaluationContext, parameterNameToSpel.getValue()); - spelExpressionResults.put(parameterNameToSpel.getKey(), spElValue); - } + return extractor.getParameters().collect(Collectors.toMap(// + it -> it.getKey(), // + it -> getSpElValue(evaluationContext, it.getValue()) // + )); + } - return spelExpressionResults; + /** + * Returns the query string produced by the intermediate SpEL expression collection step. + * + * @return + */ + public String getQueryString() { + return extractor.getQueryString(); } @Nullable - private Object getSpElValue(EvaluationContext evaluationContext, String expression) { - return PARSER.parseExpression(expression).getValue(evaluationContext, Object.class); + private static Object getSpElValue(EvaluationContext evaluationContext, String expression) { + return PARSER.parseExpression(expression).getValue(evaluationContext); } } diff --git a/src/main/java/org/springframework/data/repository/query/SpelQueryContext.java b/src/main/java/org/springframework/data/repository/query/SpelQueryContext.java new file mode 100644 index 000000000..9cac3ceb9 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/query/SpelQueryContext.java @@ -0,0 +1,309 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.query; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.BiFunction; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Range.Bound; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Source of {@link SpelExtractor} encapsulating configuration often common for all queries. + * + * @author Jens Schauder + * @author Gerrit Meier + * @since 2.1 + */ +@RequiredArgsConstructor(staticName = "of") +public class SpelQueryContext { + + private final static String SPEL_PATTERN_STRING = "([:?])#\\{([^}]+)}"; + private final static Pattern SPEL_PATTERN = Pattern.compile(SPEL_PATTERN_STRING); + + /** + * A function from the index of a SpEL expression in a query and the actual SpEL expression to the parameter name to + * be used in place of the SpEL expression. A typical implementation is expected to look like + * (index, spel) -> "__some_placeholder_" + index + */ + private final @NonNull BiFunction parameterNameSource; + + /** + * A function from a prefix used to demarcate a SpEL expression in a query and a parameter name as returned from + * {@link #parameterNameSource} to a {@literal String} to be used as a replacement of the SpEL in the query. The + * returned value should normally be interpretable as a bind parameter by the underlying persistence mechanism. A + * typical implementation is expected to look like (prefix, name) -> prefix + name or + * (prefix, name) -> "{" + name + "}" + */ + private final @NonNull BiFunction replacementSource; + + /** + * Parses the query for SpEL expressions using the pattern: + * + *
+	 * <prefix>#{<spel>}
+	 * 
+ * + * with prefix being the character ':' or '?'. Parsing honors quoted {@literal String}s enclosed in single or double + * quotation marks. + * + * @param query a query containing SpEL expressions in the format described above. Must not be {@literal null}. + * @return A {@link SpelExtractor} which makes the query with SpEL expressions replaced by bind parameters and a map + * from bind parameter to SpEL expression available. Guaranteed to be not {@literal null}. + */ + public SpelExtractor parse(String query) { + return new SpelExtractor(query); + } + + /** + * Createsa {@link EvaluatingSpelQueryContext} from the current one and the given + * {@link QueryMethodEvaluationContextProvider}. + * + * @param provider must not be {@literal null}. + * @return + */ + public EvaluatingSpelQueryContext withEvaluationContextProvider(QueryMethodEvaluationContextProvider provider) { + + Assert.notNull(provider, "QueryMethodEvaluationContextProvider must not be null!"); + + return new EvaluatingSpelQueryContext(provider, parameterNameSource, replacementSource); + } + + /** + * An extension of {@link SpelQueryContext} that can create {@link SpelEvaluator} instances as it also knows about a + * {@link QueryMethodEvaluationContextProvider}. + * + * @author Oliver Gierke + * @since 2.1 + */ + public static class EvaluatingSpelQueryContext extends SpelQueryContext { + + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + + /** + * Creates a new {@link EvaluatingSpelQueryContext} for the given {@link QueryMethodEvaluationContextProvider}, + * parameter name source and replacement source. + * + * @param evaluationContextProvider must not be {@literal null}. + * @param parameterNameSource must not be {@literal null}. + * @param replacementSource must not be {@literal null}. + */ + private EvaluatingSpelQueryContext(QueryMethodEvaluationContextProvider evaluationContextProvider, + BiFunction parameterNameSource, BiFunction replacementSource) { + + super(parameterNameSource, replacementSource); + + this.evaluationContextProvider = evaluationContextProvider; + } + + /** + * Parses the query for SpEL expressions using the pattern: + * + *
+		 * <prefix>#{<spel>}
+		 * 
+ * + * with prefix being the character ':' or '?'. Parsing honors quoted {@literal String}s enclosed in single or double + * quotation marks. + * + * @param query a query containing SpEL expressions in the format described above. Must not be {@literal null}. + * @param parameters a {@link Parameters} instance describing query method parameters + * @return A {@link SpelEvaluator} which allows to evaluate the SpEL expressions. Will never be {@literal null}. + */ + public SpelEvaluator parse(String query, Parameters parameters) { + return new SpelEvaluator(evaluationContextProvider, parameters, parse(query)); + } + } + + /** + * Parses a query string, identifies the contained SpEL expressions, replaces them with bind parameters and offers a + * {@link Map} from those bind parameters to the spel expression. + *

+ * The parser detects quoted parts of the query string and does not detect SpEL expressions inside such quoted parts + * of the query. + * + * @author Jens Schauder + * @author Oliver Gierke + * @since 2.1 + */ + public class SpelExtractor { + + private static final int PREFIX_GROUP_INDEX = 1; + private static final int EXPRESSION_GROUP_INDEX = 2; + + private final String query; + private final Map expressions; + private final QuotationMap quotations; + + /** + * Creates a SpelExtractor from a query String. + * + * @param query must not be {@literal null}. + */ + SpelExtractor(String query) { + + Assert.notNull(query, "Query must not be null"); + + Map expressions = new HashMap<>(); + Matcher matcher = SPEL_PATTERN.matcher(query); + StringBuilder resultQuery = new StringBuilder(); + QuotationMap quotedAreas = new QuotationMap(query); + + int expressionCounter = 0; + int matchedUntil = 0; + + while (matcher.find()) { + + if (quotedAreas.isQuoted(matcher.start())) { + + resultQuery.append(query.substring(matchedUntil, matcher.end())); + + } else { + + String spelExpression = matcher.group(EXPRESSION_GROUP_INDEX); + String prefix = matcher.group(PREFIX_GROUP_INDEX); + + String parameterName = parameterNameSource.apply(expressionCounter, spelExpression); + String replacement = replacementSource.apply(prefix, parameterName); + + resultQuery.append(query.substring(matchedUntil, matcher.start())); + resultQuery.append(replacement); + + expressions.put(parameterName, spelExpression); + expressionCounter++; + } + + matchedUntil = matcher.end(); + } + + resultQuery.append(query.substring(matchedUntil)); + + this.expressions = Collections.unmodifiableMap(expressions); + this.query = resultQuery.toString(); + this.quotations = quotedAreas; + } + + /** + * The query with all the SpEL expressions replaced with bind parameters. + * + * @return Guaranteed to be not {@literal null}. + */ + public String getQueryString() { + return query; + } + + public boolean isQuoted(int index) { + return quotations.isQuoted(index); + } + + public String getParameter(String name) { + return expressions.get(name); + } + + /** + * A {@literal Map} from parameter name to SpEL expression. + * + * @return Guaranteed to be not {@literal null}. + */ + Map getParameterMap() { + return expressions; + } + + Stream> getParameters() { + return expressions.entrySet().stream(); + } + } + + /** + * Value object to analyze a {@link String} to determine the parts of the {@link String} that are quoted and offers an + * API to query that information. + * + * @author Jens Schauder + * @author Oliver Gierke + * @since 2.1 + */ + static class QuotationMap { + + private static final Collection QUOTING_CHARACTERS = Arrays.asList('"', '\''); + + private final List> quotedRanges = new ArrayList<>(); + + /** + * Creates a new {@link QuotationMap} for the query. + * + * @param query can be {@literal null}. + */ + public QuotationMap(@Nullable String query) { + + if (query == null) { + return; + } + + Character inQuotation = null; + int start = 0; + + for (int i = 0; i < query.length(); i++) { + + char currentChar = query.charAt(i); + + if (QUOTING_CHARACTERS.contains(currentChar)) { + + if (inQuotation == null) { + + inQuotation = currentChar; + start = i; + + } else if (currentChar == inQuotation) { + + inQuotation = null; + + quotedRanges.add(Range.from(Bound.inclusive(start)).to(Bound.inclusive(i))); + } + } + } + + if (inQuotation != null) { + throw new IllegalArgumentException( + String.format("The string <%s> starts a quoted range at %d, but never ends it.", query, start)); + } + } + + /** + * Checks if a given index is within a quoted range. + * + * @param index to check if it is part of a quoted range. + * @return whether the query contains a quoted range at {@literal index}. + */ + public boolean isQuoted(int index) { + return quotedRanges.stream().anyMatch(r -> r.contains(index)); + } + } +} diff --git a/src/main/java/org/springframework/data/repository/query/parser/QuotationMap.java b/src/main/java/org/springframework/data/repository/query/parser/QuotationMap.java deleted file mode 100644 index c91507ce5..000000000 --- a/src/main/java/org/springframework/data/repository/query/parser/QuotationMap.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.repository.query.parser; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.springframework.data.domain.Range; -import org.springframework.lang.Nullable; - -/** - * Value object to analyze a String to determine the parts of the String that are quoted and offers an API to query that - * information. - * - * @author Jens Schauder - * @since 2.0.3 - */ -public class QuotationMap { - - private static final Set QUOTING_CHARACTERS = new HashSet<>(Arrays.asList('"', '\'')); - - private final List> quotedRanges = new ArrayList<>(); - - public QuotationMap(@Nullable String query) { - - if (query == null) { - return; - } - - Character inQuotation = null; - int start = 0; - - for (int i = 0; i < query.length(); i++) { - - char currentChar = query.charAt(i); - - if (QUOTING_CHARACTERS.contains(currentChar)) { - - if (inQuotation == null) { - - inQuotation = currentChar; - start = i; - - } else if (currentChar == inQuotation) { - - inQuotation = null; - quotedRanges.add(Range.of(Range.Bound.inclusive(start), Range.Bound.inclusive(i))); - } - } - } - - if (inQuotation != null) { - throw new IllegalArgumentException( - String.format("The string <%s> starts a quoted range at %d, but never ends it.", query, start)); - } - } - - /** - * Checks if a given index is within a quoted range. - * - * @param index to check if it is part of a quoted range. - * @return whether the query contains a quoted range at {@literal index}. - */ - public boolean isQuoted(int index) { - return quotedRanges.stream().anyMatch(r -> r.contains(index)); - } -} diff --git a/src/main/java/org/springframework/data/repository/query/parser/SpelQueryContext.java b/src/main/java/org/springframework/data/repository/query/parser/SpelQueryContext.java deleted file mode 100644 index 0980545b0..000000000 --- a/src/main/java/org/springframework/data/repository/query/parser/SpelQueryContext.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright 2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.repository.query.parser; - -import lombok.NonNull; -import lombok.RequiredArgsConstructor; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.function.BiFunction; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.springframework.util.Assert; - -/** - * Source of {@link SpelExtractor} encapsulating configuration often common for all queries. - * - * @author Jens Schauder - * @author Gerrit Meier - */ -@RequiredArgsConstructor -public class SpelQueryContext { - - private final static String SPEL_PATTERN_STRING = "([:?])#\\{([^}]+)}"; - private final static Pattern SPEL_PATTERN = Pattern.compile(SPEL_PATTERN_STRING); - - /** - * A function from the index of a SpEL expression in a query and the actual SpEL expression to the parameter name to - * be used in place of the SpEL expression. A typical implementation is expected to look like - * (index, spel) -> "__some_placeholder_" + index - */ - @NonNull private final BiFunction parameterNameSource; - - /** - * A function from a prefix used to demarcate a SpEL expression in a query and a parameter name as returned from - * {@link #parameterNameSource} to a {@literal String} to be used as a replacement of the SpEL in the query. The - * returned value should normally be interpretable as a bind parameter by the underlying persistence mechanism. A - * typical implementation is expected to look like (prefix, name) -> prefix + name or - * (prefix, name) -> "{" + name + "}" - */ - @NonNull private final BiFunction replacementSource; - - /** - * Parses the query for SpEL expressions using the pattern - * - *

-	 * <prefix>#{<spel>}
-	 * 
- * - * with prefix being the character ':' or '?'. Parsing honors quoted {@literal String}s enclosed in single or double - * quotation marks. - * - * @param query a query containing SpEL expressions in the format described above. Must not be {@literal null}. - * @return A {@link SpelExtractor} which makes the query with SpEL expressions replaced by bind parameters and a map - * from bind parameter to SpEL expression available. Guaranteed to be not {@literal null}. - */ - public SpelExtractor parse(String query) { - return new SpelExtractor(query); - } - - /** - * Parses a query string, identifies the contained SpEL expressions, replaces them with bind parameters and offers a - * {@link Map} from those bind parameters to the spel expression. - *

- * The parser detects quoted parts of the query string and does not detect SpEL expressions inside such quoted parts - * of the query. - * - * @author Jens Schauder - */ - public class SpelExtractor { - - private static final int PREFIX_GROUP_INDEX = 1; - private static final int EXPRESSION_GROUP_INDEX = 2; - - private final String query; - private final Map expressions; - - /** - * Creates a SpelExtractor from a query String. - * - * @param query Must not be {@literal null}. - */ - private SpelExtractor(String query) { - - Assert.notNull(query, "Query must not be null"); - - HashMap expressions = new HashMap<>(); - - Matcher matcher = SPEL_PATTERN.matcher(query); - - StringBuilder resultQuery = new StringBuilder(); - - QuotationMap quotedAreas = new QuotationMap(query); - - int expressionCounter = 0; - int matchedUntil = 0; - - while (matcher.find()) { - - if (quotedAreas.isQuoted(matcher.start())) { - - resultQuery.append(query.substring(matchedUntil, matcher.end())); - - } else { - - String spelExpression = matcher.group(EXPRESSION_GROUP_INDEX); - String prefix = matcher.group(PREFIX_GROUP_INDEX); - - String parameterName = parameterNameSource.apply(expressionCounter, spelExpression); - String replacement = replacementSource.apply(prefix, parameterName); - - resultQuery.append(query.substring(matchedUntil, matcher.start())); - resultQuery.append(replacement); - - expressions.put(parameterName, spelExpression); - expressionCounter++; - } - - matchedUntil = matcher.end(); - } - - resultQuery.append(query.substring(matchedUntil)); - - this.expressions = Collections.unmodifiableMap(expressions); - this.query = resultQuery.toString(); - } - - /** - * The query with all the SpEL expressions replaced with bind parameters. - * - * @return Guaranteed to be not {@literal null}. - */ - public String query() { - return query; - } - - /** - * A {@literal Map} from parameter name to SpEL expression. - * - * @return Guaranteed to be not {@literal null}. - */ - public Map parameterNameToSpelMap() { - return expressions; - } - } -} diff --git a/src/test/java/org/springframework/data/repository/query/parser/QuotationMapUnitTests.java b/src/test/java/org/springframework/data/repository/query/QuotationMapUnitTests.java similarity index 90% rename from src/test/java/org/springframework/data/repository/query/parser/QuotationMapUnitTests.java rename to src/test/java/org/springframework/data/repository/query/QuotationMapUnitTests.java index 8dddacced..5ddcf2d3e 100644 --- a/src/test/java/org/springframework/data/repository/query/parser/QuotationMapUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/QuotationMapUnitTests.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.repository.query.parser; +package org.springframework.data.repository.query; import static org.assertj.core.api.Assertions.*; import org.assertj.core.api.SoftAssertions; import org.junit.Test; +import org.springframework.data.repository.query.SpelQueryContext.QuotationMap; /** * Unit tests for {@link QuotationMap}. @@ -41,7 +42,9 @@ public class QuotationMapUnitTests { @Test // DATAJPA-1235 public void simpleStringDoesNotContainQuotes() { + String query = "something"; + isNotQuoted(query, "simple String", -1, 0, query.length() - 1, query.length(), query.length() + 1); } @@ -49,6 +52,7 @@ public class QuotationMapUnitTests { public void fullySingleQuotedStringDoesContainQuotes() { String query = "'something'"; + isNotQuoted(query, "quoted String", -1, query.length()); isQuoted(query, "quoted String", 0, 1, 5, query.length() - 1); } @@ -57,6 +61,7 @@ public class QuotationMapUnitTests { public void fullyDoubleQuotedStringDoesContainQuotes() { String query = "\"something\""; + isNotQuoted(query, "double quoted String", -1, query.length()); isQuoted(query, "double quoted String", 0, 1, 5, query.length() - 1); } @@ -65,6 +70,7 @@ public class QuotationMapUnitTests { public void stringWithEmptyQuotes() { String query = "abc''def"; + isNotQuoted(query, "zero length quote", -1, 0, 1, 2, 5, 6, 7); isQuoted(query, "zero length quote", 3, 4); } @@ -73,6 +79,7 @@ public class QuotationMapUnitTests { public void doubleInSingleQuotes() { String query = "abc'\"'def"; + isNotQuoted(query, "double inside single quote", -1, 0, 1, 2, 6, 7, 8); isQuoted(query, "double inside single quote", 3, 4, 5); } @@ -81,6 +88,7 @@ public class QuotationMapUnitTests { public void singleQuotesInDoubleQuotes() { String query = "abc\"'\"def"; + isNotQuoted(query, "single inside double quote", -1, 0, 1, 2, 6, 7, 8); isQuoted(query, "single inside double quote", 3, 4, 5); } @@ -95,11 +103,10 @@ public class QuotationMapUnitTests { @Test // DATAJPA-1235 public void openEndedQuoteThrowsException() { - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new QuotationMap("a'b")); } - private void isNotQuoted(String query, Object label, int... indexes) { + private static void isNotQuoted(String query, Object label, int... indexes) { QuotationMap quotationMap = new QuotationMap(query); @@ -111,14 +118,15 @@ public class QuotationMapUnitTests { } } - private void isQuoted(String query, Object label, int... indexes) { + private static void isQuoted(String query, Object label, int... indexes) { QuotationMap quotationMap = new QuotationMap(query); for (int index : indexes) { assertThat(quotationMap.isQuoted(index)) - .describedAs(String.format("(%s) %s does contain a quote at %s", label, query, index)).isTrue(); + .describedAs(String.format("(%s) %s does contain a quote at %s", label, query, index)) // + .isTrue(); } } } diff --git a/src/test/java/org/springframework/data/repository/query/SpelExtractorUnitTests.java b/src/test/java/org/springframework/data/repository/query/SpelExtractorUnitTests.java new file mode 100644 index 000000000..3f18c5426 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/query/SpelExtractorUnitTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.query; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +import org.assertj.core.api.SoftAssertions; +import org.assertj.core.groups.Tuple; +import org.junit.Test; +import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor; + +/** + * Unit tests for {@link SpelExtractor}. + * + * @author Jens Schauder + * @author Oliver Gierke + */ +public class SpelExtractorUnitTests { + + static final BiFunction PARAMETER_NAME_SOURCE = (index, spel) -> "EPP" + index; + static final BiFunction REPLACEMENT_SOURCE = (prefix, name) -> prefix + name; + + final SoftAssertions softly = new SoftAssertions(); + + @Test // DATACMNS-1258 + public void nullQueryThrowsException() { + + SpelQueryContext context = SpelQueryContext.of(PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); + + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> context.parse(null)); + } + + @Test // DATACMNS-1258 + public void emptyStringGetsParsedCorrectly() { + + SpelQueryContext context = SpelQueryContext.of(PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); + SpelExtractor extractor = context.parse(""); + + softly.assertThat(extractor.getQueryString()).isEqualTo(""); + softly.assertThat(extractor.getParameterMap()).isEmpty(); + + softly.assertAll(); + } + + @Test // DATACMNS-1258 + public void findsAndReplacesExpressions() { + + SpelQueryContext context = SpelQueryContext.of(PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); + SpelExtractor extractor = context.parse(":#{one} ?#{two}"); + + softly.assertThat(extractor.getQueryString()).isEqualTo(":EPP0 ?EPP1"); + softly.assertThat(extractor.getParameterMap().entrySet()) // + .extracting(Map.Entry::getKey, Map.Entry::getValue) // + .containsExactlyInAnyOrder( // + Tuple.tuple("EPP0", "one"), // + Tuple.tuple("EPP1", "two") // + ); + + softly.assertAll(); + } + + @Test // DATACMNS-1258 + public void keepsStringWhenNoMatchIsFound() { + + SpelQueryContext context = SpelQueryContext.of(PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); + SpelExtractor extractor = context.parse("abcdef"); + + softly.assertThat(extractor.getQueryString()).isEqualTo("abcdef"); + softly.assertThat(extractor.getParameterMap()).isEmpty(); + + softly.assertAll(); + } + + @Test // DATACMNS-1258 + public void spelsInQuotesGetIgnored() { + + List queries = Arrays.asList(// + "a'b:#{one}cd'ef", // + "a'b:#{o'ne}cdef", // + "ab':#{one}'cdef", // + "ab:'#{one}cd'ef", // + "ab:#'{one}cd'ef", // + "a'b:#{o'ne}cdef"); + + queries.forEach(this::checkNoSpelIsFound); + + softly.assertAll(); + } + + private void checkNoSpelIsFound(String query) { + + SpelQueryContext context = SpelQueryContext.of(PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); + SpelExtractor extractor = context.parse(query); + + softly.assertThat(extractor.getQueryString()).describedAs(query).isEqualTo(query); + softly.assertThat(extractor.getParameterMap()).describedAs(query).isEmpty(); + } +} diff --git a/src/test/java/org/springframework/data/repository/query/SpelQueryContextUnitTests.java b/src/test/java/org/springframework/data/repository/query/SpelQueryContextUnitTests.java new file mode 100644 index 000000000..de045f543 --- /dev/null +++ b/src/test/java/org/springframework/data/repository/query/SpelQueryContextUnitTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.repository.query; + +import static org.assertj.core.api.Assertions.*; + +import java.util.function.BiFunction; + +import org.junit.Test; + +/** + * Unit tests for {@link SpelQueryContext}. + * + * @author Oliver Gierke + * @author Jens Schauder + */ +public class SpelQueryContextUnitTests { + + static final QueryMethodEvaluationContextProvider EVALUATION_CONTEXT_PROVIDER = QueryMethodEvaluationContextProvider.DEFAULT; + static final BiFunction PARAMETER_NAME_SOURCE = (index, spel) -> "EPP" + index; + static final BiFunction REPLACEMENT_SOURCE = (prefix, name) -> prefix + name; + + @Test // DATACMNS-1258 + public void nullParameterNameSourceThrowsException() { + + assertThatExceptionOfType(IllegalArgumentException.class) // + .isThrownBy(() -> SpelQueryContext.of(null, REPLACEMENT_SOURCE)); + } + + @Test // DATACMNS-1258 + public void nullReplacementSourceThrowsException() { + + assertThatExceptionOfType(IllegalArgumentException.class) // + .isThrownBy(() -> SpelQueryContext.of(PARAMETER_NAME_SOURCE, null)); + } + + @Test // DATACMNS-1258 + public void rejectsNullEvaluationContextProvider() { + + SpelQueryContext context = SpelQueryContext.of(PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); + + assertThatExceptionOfType(IllegalArgumentException.class) // + .isThrownBy(() -> context.withEvaluationContextProvider(null)); + } + + @Test // DATACMNS-1258 + public void createsEvaluatingContextUsingProvider() { + + SpelQueryContext context = SpelQueryContext.of(PARAMETER_NAME_SOURCE, REPLACEMENT_SOURCE); + + assertThat(context.withEvaluationContextProvider(EVALUATION_CONTEXT_PROVIDER)).isNotNull(); + } +} diff --git a/src/test/java/org/springframework/data/repository/query/parser/SpelExtractorUnitTests.java b/src/test/java/org/springframework/data/repository/query/parser/SpelExtractorUnitTests.java deleted file mode 100644 index 02db2d980..000000000 --- a/src/test/java/org/springframework/data/repository/query/parser/SpelExtractorUnitTests.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.repository.query.parser; - -import static org.assertj.core.api.Assertions.*; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.function.BiFunction; - -import org.assertj.core.api.SoftAssertions; -import org.assertj.core.groups.Tuple; -import org.junit.Test; - -/** - * Unit tests for {@link SpelQueryContext} and - * {@link org.springframework.data.repository.query.parser.SpelQueryContext.SpelExtractor} - * - * @author Jens Schauder - */ -public class SpelExtractorUnitTests { - - static final String EXPRESSION_PARAMETER_PREFIX = "EPP"; - static final BiFunction PARAMETER_NAME_SOURCE = (index, spel) -> EXPRESSION_PARAMETER_PREFIX + index; - static final BiFunction REPLACEMENT_SOURCE = (prefix, name) -> prefix + name; - final SoftAssertions softly = new SoftAssertions(); - - @Test // DATACMNS-1258 - public void nullQueryThrowsException() { - - assertThatExceptionOfType(IllegalArgumentException.class) // - .isThrownBy(() -> new SpelQueryContext( // - PARAMETER_NAME_SOURCE, // - REPLACEMENT_SOURCE) // - .parse(null) // - ); - } - - @Test // DATACMNS-1258 - public void nullParameterNameSourceThrowsException() { - - assertThatExceptionOfType(IllegalArgumentException.class) // - .isThrownBy(() -> new SpelQueryContext( // - null, // - REPLACEMENT_SOURCE) // - ); - } - - @Test // DATACMNS-1258 - public void nullReplacementSourceThrowsException() { - - assertThatExceptionOfType(IllegalArgumentException.class) // - .isThrownBy( // - () -> new SpelQueryContext( // - PARAMETER_NAME_SOURCE, // - null) // - ); - } - - @Test // DATACMNS-1258 - public void emptyStringGetsParsedCorrectly() { - - SpelQueryContext.SpelExtractor extractor = new SpelQueryContext( // - PARAMETER_NAME_SOURCE, // - REPLACEMENT_SOURCE // - ).parse(""); - - softly.assertThat(extractor.query()).isEqualTo(""); - softly.assertThat(extractor.parameterNameToSpelMap()).isEmpty(); - - softly.assertAll(); - } - - @Test // DATACMNS-1258 - public void findsAndReplacesExpressions() { - - SpelQueryContext.SpelExtractor extractor = new SpelQueryContext( // - PARAMETER_NAME_SOURCE, // - REPLACEMENT_SOURCE // - ).parse(":#{one} ?#{two}"); - - softly.assertThat(extractor.query()).isEqualTo(":EPP0 ?EPP1"); - softly.assertThat(extractor.parameterNameToSpelMap().entrySet()) // - .extracting(Map.Entry::getKey, Map.Entry::getValue) // - .containsExactlyInAnyOrder( // - Tuple.tuple("EPP0", "one"), // - Tuple.tuple("EPP1", "two") // - ); - - softly.assertAll(); - } - - @Test // DATACMNS-1258 - public void keepsStringWhenNoMatchIsFound() { - - SpelQueryContext.SpelExtractor extractor = new SpelQueryContext( // - PARAMETER_NAME_SOURCE, // - REPLACEMENT_SOURCE // - ).parse("abcdef"); - - softly.assertThat(extractor.query()).isEqualTo("abcdef"); - softly.assertThat(extractor.parameterNameToSpelMap()).isEmpty(); - - softly.assertAll(); - } - - @Test // DATACMNS-1258 - public void spelsInQuotesGetIgnored() { - - List queries = Arrays.asList("a'b:#{one}cd'ef", "a'b:#{o'ne}cdef", "ab':#{one}'cdef", "ab:'#{one}cd'ef", - "ab:#'{one}cd'ef", "a'b:#{o'ne}cdef"); - - for (String query : queries) { - checkNoSpelIsFound(query); - } - - softly.assertAll(); - } - - private void checkNoSpelIsFound(String query) { - - SpelQueryContext.SpelExtractor extractor = new SpelQueryContext( // - PARAMETER_NAME_SOURCE, // - REPLACEMENT_SOURCE // - ).parse(query); - - softly.assertThat(extractor.query()).describedAs(query).isEqualTo(query); - softly.assertThat(extractor.parameterNameToSpelMap()).describedAs(query).isEmpty(); - } - -}