From 495b1f30bb75dc7d44e1b60b79f122b3b509383b Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Mon, 14 May 2018 16:46:50 +0200 Subject: [PATCH] DATACMNS-1258 - Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved newly introduced types into the ….data.repository.query package as the parser one contains types that parses queries from method names. Simplified the API surface by hiding SpelExtractor completely and making the API on SpelQueryContext more convenient to directly create a SpelEvaluator. --- .../query/{parser => }/SpelEvaluator.java | 44 +-- .../repository/query/SpelQueryContext.java | 309 ++++++++++++++++++ .../repository/query/parser/QuotationMap.java | 83 ----- .../query/parser/SpelQueryContext.java | 161 --------- .../{parser => }/QuotationMapUnitTests.java | 18 +- .../query/SpelExtractorUnitTests.java | 116 +++++++ .../query/SpelQueryContextUnitTests.java | 66 ++++ .../query/parser/SpelExtractorUnitTests.java | 145 -------- 8 files changed, 528 insertions(+), 414 deletions(-) rename src/main/java/org/springframework/data/repository/query/{parser => }/SpelEvaluator.java (60%) create mode 100644 src/main/java/org/springframework/data/repository/query/SpelQueryContext.java delete mode 100644 src/main/java/org/springframework/data/repository/query/parser/QuotationMap.java delete mode 100644 src/main/java/org/springframework/data/repository/query/parser/SpelQueryContext.java rename src/test/java/org/springframework/data/repository/query/{parser => }/QuotationMapUnitTests.java (90%) create mode 100644 src/test/java/org/springframework/data/repository/query/SpelExtractorUnitTests.java create mode 100644 src/test/java/org/springframework/data/repository/query/SpelQueryContextUnitTests.java delete mode 100644 src/test/java/org/springframework/data/repository/query/parser/SpelExtractorUnitTests.java 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(); - } - -}