| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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.apache.ignite.springdata22.repository.query; |
| |
| import java.lang.reflect.Array; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.function.BiFunction; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import org.jetbrains.annotations.Nullable; |
| import org.springframework.data.domain.Range; |
| import org.springframework.data.domain.Range.Bound; |
| import org.springframework.data.repository.query.SpelQueryContext; |
| import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor; |
| import org.springframework.data.repository.query.parser.Part.Type; |
| import org.springframework.util.Assert; |
| import org.springframework.util.ObjectUtils; |
| import org.springframework.util.StringUtils; |
| |
| import static java.util.regex.Pattern.CASE_INSENSITIVE; |
| import static org.springframework.util.ObjectUtils.nullSafeEquals; |
| import static org.springframework.util.ObjectUtils.nullSafeHashCode; |
| |
| /** |
| * Encapsulation of a JPA query String. Offers access to parameters as bindings. The internal query String is cleaned |
| * from decorated parameters like {@literal %:lastname%} and the matching bindings take care of applying the decorations |
| * in the {@link ParameterBinding#prepare(Object)} method. Note that this class also handles replacing SpEL expressions |
| * with synthetic bind parameters |
| * |
| * @author Oliver Gierke |
| * @author Thomas Darimont |
| * @author Oliver Wehrens |
| * @author Mark Paluch |
| * @author Jens Schauder |
| */ |
| class StringQuery implements DeclaredQuery { |
| /** */ |
| private final String query; |
| |
| /** */ |
| private final List<ParameterBinding> bindings; |
| |
| /** */ |
| @Nullable |
| private final String alias; |
| |
| /** */ |
| private final boolean hasConstructorExpression; |
| |
| /** */ |
| private final boolean containsPageableInSpel; |
| |
| /** */ |
| private final boolean usesJdbcStyleParameters; |
| |
| /** |
| * Creates a new {@link StringQuery} from the given JPQL query. |
| * |
| * @param query must not be {@literal null} or empty. |
| */ |
| StringQuery(String query) { |
| Assert.hasText(query, "Query must not be null or empty!"); |
| |
| bindings = new ArrayList<>(); |
| containsPageableInSpel = query.contains("#pageable"); |
| |
| Metadata queryMeta = new Metadata(); |
| this.query = ParameterBindingParser.INSTANCE |
| .parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, bindings, |
| queryMeta); |
| |
| usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters; |
| alias = QueryUtils.detectAlias(query); |
| hasConstructorExpression = QueryUtils.hasConstructorExpression(query); |
| } |
| |
| /** |
| * Returns whether we have found some like bindings. |
| */ |
| boolean hasParameterBindings() { |
| return !bindings.isEmpty(); |
| } |
| |
| /** */ |
| String getProjection() { |
| return QueryUtils.getProjection(query); |
| } |
| |
| // See org.springframework.data.jpa.repository.query.DeclaredQuery#getParameterBindings() |
| /** {@inheritDoc} */ |
| @Override public List<ParameterBinding> getParameterBindings() { |
| return bindings; |
| } |
| |
| // See org.springframework.data.jpa.repository.query.DeclaredQuery#deriveCountQuery(java.lang.String, java.lang |
| /** {@inheritDoc} */ |
| @Override public DeclaredQuery deriveCountQuery(@Nullable String countQuery, |
| @Nullable String countQueryProjection) { |
| return DeclaredQuery |
| .of(countQuery != null ? countQuery : QueryUtils.createCountQueryFor(query, countQueryProjection)); |
| } |
| |
| // See org.springframework.data.jpa.repository.query.DeclaredQuery#usesJdbcStyleParameters() |
| /** */ |
| @Override public boolean usesJdbcStyleParameters() { |
| return usesJdbcStyleParameters; |
| } |
| |
| // See org.springframework.data.jpa.repository.query.DeclaredQuery#getQueryString() |
| /** {@inheritDoc} */ |
| @Override public String getQueryString() { |
| return query; |
| } |
| |
| // See org.springframework.data.jpa.repository.query.DeclaredQuery#getAlias() |
| /** {@inheritDoc} */ |
| @Override @Nullable |
| public String getAlias() { |
| return alias; |
| } |
| |
| // See org.springframework.data.jpa.repository.query.DeclaredQuery#hasConstructorExpression() |
| /** {@inheritDoc} */ |
| @Override public boolean hasConstructorExpression() { |
| return hasConstructorExpression; |
| } |
| |
| // See org.springframework.data.jpa.repository.query.DeclaredQuery#isDefaultProjection() |
| /** {@inheritDoc} */ |
| @Override public boolean isDefaultProjection() { |
| return getProjection().equalsIgnoreCase(alias); |
| } |
| |
| // See org.springframework.data.jpa.repository.query.DeclaredQuery#hasNamedParameter() |
| /** {@inheritDoc} */ |
| @Override public boolean hasNamedParameter() { |
| return bindings.stream().anyMatch(b -> b.getName() != null); |
| } |
| |
| // See org.springframework.data.jpa.repository.query.DeclaredQuery#usesPaging() |
| /** {@inheritDoc} */ |
| @Override public boolean usesPaging() { |
| return containsPageableInSpel; |
| } |
| |
| /** |
| * A parser that extracts the parameter bindings from a given query string. |
| * |
| * @author Thomas Darimont |
| */ |
| enum ParameterBindingParser { |
| /** */ |
| INSTANCE; |
| |
| /** */ |
| private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__"; |
| |
| /** */ |
| public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))"; |
| // .....................................................................^ not followed by a hash or a letter. |
| // .................................................................^ zero or more digits. |
| // .............................................................^ start with a question mark. |
| |
| /** */ |
| private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER); |
| |
| /** */ |
| private static final Pattern PARAMETER_BINDING_PATTERN; |
| |
| /** */ |
| private static final String MESSAGE = |
| "Already found parameter binding with same index / parameter name but differing binding type! " |
| + "Already have: %s, found %s! If you bind a parameter multiple times make sure they use the same " |
| + "binding."; |
| |
| /** */ |
| private static final int INDEXED_PARAMETER_GROUP = 4; |
| |
| /** */ |
| private static final int NAMED_PARAMETER_GROUP = 6; |
| |
| /** */ |
| private static final int COMPARISION_TYPE_GROUP = 1; |
| |
| static { |
| List<String> keywords = new ArrayList<>(); |
| |
| for (ParameterBindingType type : ParameterBindingType.values()) { |
| if (type.getKeyword() != null) { |
| keywords.add(type.getKeyword()); |
| } |
| } |
| |
| StringBuilder builder = new StringBuilder(); |
| builder.append("("); |
| builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords |
| builder.append(")?"); |
| builder.append("(?: )?"); // some whitespace |
| builder.append("\\(?"); // optional braces around parameters |
| builder.append("("); |
| builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index |
| builder.append("|"); // or |
| |
| // named parameter and the parameter name |
| builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?"); |
| |
| builder.append(")"); |
| builder.append("\\)?"); // optional braces around parameters |
| |
| PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE); |
| } |
| |
| /** |
| * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. |
| * Returns the cleaned up query. |
| */ |
| private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, |
| List<ParameterBinding> bindings, |
| Metadata queryMeta) { |
| int greatestParamIdx = tryFindGreatestParameterIndexIn(query); |
| boolean parametersShouldBeAccessedByIdx = greatestParamIdx != -1; |
| |
| /* |
| * Prefer indexed access over named parameters if only SpEL Expression parameters are present. |
| */ |
| if (!parametersShouldBeAccessedByIdx && query.contains("?#{")) { |
| parametersShouldBeAccessedByIdx = true; |
| greatestParamIdx = 0; |
| } |
| |
| SpelExtractor spelExtractor = createSpelExtractor(query, parametersShouldBeAccessedByIdx, |
| greatestParamIdx); |
| |
| String resultingQry = spelExtractor.getQueryString(); |
| Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQry); |
| QuotationMap quotedAreas = new QuotationMap(resultingQry); |
| |
| int expressionParamIdx = parametersShouldBeAccessedByIdx ? greatestParamIdx : 0; |
| |
| boolean usesJpaStyleParameters = false; |
| |
| while (matcher.find()) { |
| if (quotedAreas.isQuoted(matcher.start())) |
| continue; |
| |
| String paramIdxStr = matcher.group(INDEXED_PARAMETER_GROUP); |
| String paramName = paramIdxStr != null ? null : matcher.group(NAMED_PARAMETER_GROUP); |
| Integer paramIdx = getParameterIndex(paramIdxStr); |
| |
| String typeSrc = matcher.group(COMPARISION_TYPE_GROUP); |
| String expression = spelExtractor |
| .getParameter(paramName == null ? paramIdxStr : paramName); |
| String replacement = null; |
| |
| Assert.isTrue(paramIdxStr != null || paramName != null, |
| () -> String.format("We need either a name or an index! Offending query string: %s", query)); |
| |
| expressionParamIdx++; |
| if (paramIdxStr != null && paramIdxStr.isEmpty()) { |
| queryMeta.usesJdbcStyleParameters = true; |
| paramIdx = expressionParamIdx; |
| } |
| else |
| usesJpaStyleParameters = true; |
| |
| // named parameters (:param) will be untouched by spelExtractor, so replace them by ? as we don't |
| // know position |
| if (paramName != null) |
| replacement = "?"; |
| |
| if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) { |
| throw new IllegalArgumentException( |
| "Mixing of ? (? or :myNamedParam) parameters and other forms like ?1 (SpEL espressions or " |
| + "indexed) is not supported!. Please, if you are using expressions or " |
| + "indexed params, replace all named parameters by expressions. Example :myNamedParam " |
| + "by ?#{#myNamedParam}."); |
| } |
| |
| switch (ParameterBindingType.of(typeSrc)) { |
| case LIKE: |
| Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2)); |
| replacement = matcher.group(3); |
| |
| if (paramIdx != null) |
| checkAndRegister(new LikeParameterBinding(paramIdx, likeType, expression), bindings); |
| else { |
| checkAndRegister(new LikeParameterBinding(paramName, likeType, expression), bindings); |
| |
| replacement = expression != null ? ":" + paramName : matcher.group(5); |
| } |
| |
| break; |
| |
| case IN: |
| if (paramIdx != null) |
| checkAndRegister(new InParameterBinding(paramIdx, expression), bindings); |
| else |
| checkAndRegister(new InParameterBinding(paramName, expression), bindings); |
| |
| break; |
| |
| case AS_IS: // fall-through we don't need a special parameter binding for the given parameter. |
| default: |
| bindings.add(paramIdx != null |
| ? new ParameterBinding(null, paramIdx, expression) |
| : new ParameterBinding(paramName, null, expression)); |
| } |
| |
| if (replacement != null) |
| resultingQry = replaceFirst(resultingQry, matcher.group(2), replacement); |
| } |
| |
| return resultingQry; |
| } |
| |
| /** */ |
| private static SpelExtractor createSpelExtractor(String queryWithSpel, |
| boolean parametersShouldBeAccessedByIndex, |
| int greatestParameterIndex) { |
| |
| /* |
| * If parameters need to be bound by index, we bind the synthetic expression parameters starting from |
| * position of the greatest discovered index parameter in order to |
| * not mix-up with the actual parameter indices. |
| */ |
| int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0; |
| |
| BiFunction<Integer, String, String> indexToParameterName = parametersShouldBeAccessedByIndex |
| ? (index, expression) -> String.valueOf( |
| index + expressionParameterIndex + 1) |
| : (index, expression) -> |
| EXPRESSION_PARAMETER_PREFIX + (index |
| + 1); |
| |
| String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":"; |
| |
| BiFunction<String, String, String> parameterNameToReplacement = (prefix, name) -> fixedPrefix + name; |
| |
| return SpelQueryContext.of(indexToParameterName, parameterNameToReplacement).parse(queryWithSpel); |
| } |
| |
| /** */ |
| private static String replaceFirst(String text, String substring, String replacement) { |
| int index = text.indexOf(substring); |
| if (index < 0) |
| return text; |
| |
| return text.substring(0, index) + replacement + text.substring(index + substring.length()); |
| } |
| |
| /** */ |
| @Nullable |
| private static Integer getParameterIndex(@Nullable String parameterIndexString) { |
| if (parameterIndexString == null || parameterIndexString.isEmpty()) |
| return null; |
| return Integer.valueOf(parameterIndexString); |
| } |
| |
| /** */ |
| private static int tryFindGreatestParameterIndexIn(String query) { |
| Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query); |
| |
| int greatestParameterIndex = -1; |
| while (parameterIndexMatcher.find()) { |
| |
| String parameterIndexString = parameterIndexMatcher.group(1); |
| Integer parameterIndex = getParameterIndex(parameterIndexString); |
| if (parameterIndex != null) |
| greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex); |
| } |
| |
| return greatestParameterIndex; |
| } |
| |
| /** */ |
| private static void checkAndRegister(ParameterBinding binding, List<ParameterBinding> bindings) { |
| |
| bindings.stream() // |
| .filter(it -> it.hasName(binding.getName()) || it.hasPosition(binding.getPosition())) // |
| .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding))); |
| |
| if (!bindings.contains(binding)) |
| bindings.add(binding); |
| } |
| |
| /** |
| * An enum for the different types of bindings. |
| * |
| * @author Thomas Darimont |
| * @author Oliver Gierke |
| */ |
| private enum ParameterBindingType { |
| // Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace |
| // character, while = does not. |
| /** */ |
| LIKE("like "), |
| |
| /** */ |
| IN("in "), |
| |
| /** */ |
| AS_IS(null); |
| |
| /** */ |
| @Nullable |
| private final String keyword; |
| |
| /** */ |
| ParameterBindingType(@Nullable String keyword) { |
| this.keyword = keyword; |
| } |
| |
| /** |
| * Returns the keyword that will tirgger the binding type or {@literal null} if the type is not triggered by |
| * a keyword. |
| * |
| * @return the keyword |
| */ |
| @Nullable |
| public String getKeyword() { |
| return keyword; |
| } |
| |
| /** |
| * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal |
| * #AS_IS} in case no other {@link ParameterBindingType} could be found. |
| */ |
| static ParameterBindingType of(String typeSource) { |
| if (!StringUtils.hasText(typeSource)) |
| return AS_IS; |
| |
| for (ParameterBindingType type : values()) { |
| if (type.name().equalsIgnoreCase(typeSource.trim())) |
| return type; |
| } |
| |
| throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s!", typeSource)); |
| } |
| } |
| } |
| |
| /** |
| * A generic parameter binding with name or position information. |
| * |
| * @author Thomas Darimont |
| */ |
| static class ParameterBinding { |
| /** */ |
| @Nullable |
| private final String name; |
| |
| /** */ |
| @Nullable |
| private final String expression; |
| |
| /** */ |
| @Nullable |
| private final Integer position; |
| |
| /** |
| * Creates a new {@link ParameterBinding} for the parameter with the given position. |
| * |
| * @param position must not be {@literal null}. |
| */ |
| ParameterBinding(Integer position) { |
| this(null, position, null); |
| } |
| |
| /** |
| * Creates a new {@link ParameterBinding} for the parameter with the given name, position and expression |
| * information. Either {@literal name} or {@literal position} must be not {@literal null}. |
| * |
| * @param name of the parameter may be {@literal null}. |
| * @param position of the parameter may be {@literal null}. |
| * @param expression the expression to apply to any value for this parameter. |
| */ |
| ParameterBinding(@Nullable String name, @Nullable Integer position, @Nullable String expression) { |
| |
| if (name == null) |
| Assert.notNull(position, "Position must not be null!"); |
| |
| if (position == null) |
| Assert.notNull(name, "Name must not be null!"); |
| |
| this.name = name; |
| this.position = position; |
| this.expression = expression; |
| } |
| |
| /** |
| * Returns whether the binding has the given name. Will always be {@literal false} in case the {@link |
| * ParameterBinding} has been set up from a position. |
| */ |
| boolean hasName(@Nullable String name) { |
| return position == null && this.name != null && this.name.equals(name); |
| } |
| |
| /** |
| * Returns whether the binding has the given position. Will always be {@literal false} in case the {@link |
| * ParameterBinding} has been set up from a name. |
| */ |
| boolean hasPosition(@Nullable Integer position) { |
| return position != null && name == null && position.equals(this.position); |
| } |
| |
| /** |
| * @return the name |
| */ |
| @Nullable |
| public String getName() { |
| return name; |
| } |
| |
| /** |
| * @return the name |
| * @throws IllegalStateException if the name is not available. |
| */ |
| String getRequiredName() throws IllegalStateException { |
| |
| String name = getName(); |
| |
| if (name != null) |
| return name; |
| |
| throw new IllegalStateException(String.format("Required name for %s not available!", this)); |
| } |
| |
| /** |
| * @return the position |
| */ |
| @Nullable |
| Integer getPosition() { |
| return position; |
| } |
| |
| /** |
| * @return the position |
| * @throws IllegalStateException if the position is not available. |
| */ |
| int getRequiredPosition() throws IllegalStateException { |
| |
| Integer position = getPosition(); |
| |
| if (position != null) |
| return position; |
| |
| throw new IllegalStateException(String.format("Required position for %s not available!", this)); |
| } |
| |
| /** |
| * @return {@literal true} if this parameter binding is a synthetic SpEL expression. |
| */ |
| public boolean isExpression() { |
| return expression != null; |
| } |
| |
| /** */ |
| @Override public int hashCode() { |
| |
| int result = 17; |
| |
| result += nullSafeHashCode(name); |
| result += nullSafeHashCode(position); |
| result += nullSafeHashCode(expression); |
| |
| return result; |
| } |
| |
| /** */ |
| @Override public boolean equals(Object obj) { |
| |
| if (!(obj instanceof ParameterBinding)) |
| return false; |
| |
| ParameterBinding that = (ParameterBinding)obj; |
| |
| return nullSafeEquals(name, that.name) && nullSafeEquals(position, that.position) |
| && nullSafeEquals(expression, that.expression); |
| } |
| |
| /** */ |
| @Override public String toString() { |
| return String.format("ParameterBinding [name: %s, position: %d, expression: %s]", getName(), getPosition(), |
| getExpression()); |
| } |
| |
| /** |
| * @param valueToBind value to prepare |
| */ |
| @Nullable |
| public Object prepare(@Nullable Object valueToBind) { |
| return valueToBind; |
| } |
| |
| /** */ |
| @Nullable |
| public String getExpression() { |
| return expression; |
| } |
| } |
| |
| /** |
| * Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as |
| * an {@code IN} parameter. |
| * |
| * @author Thomas Darimont |
| */ |
| static class InParameterBinding extends ParameterBinding { |
| /** |
| * Creates a new {@link InParameterBinding} for the parameter with the given name. |
| */ |
| InParameterBinding(String name, @Nullable String expression) { |
| super(name, null, expression); |
| } |
| |
| /** |
| * Creates a new {@link InParameterBinding} for the parameter with the given position. |
| */ |
| InParameterBinding(int position, @Nullable String expression) { |
| super(null, position, expression); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * @see org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding#prepare(java.lang.Object) |
| */ |
| @Override public Object prepare(@Nullable Object value) { |
| if (!ObjectUtils.isArray(value)) |
| return value; |
| |
| int length = Array.getLength(value); |
| Collection<Object> result = new ArrayList<>(length); |
| |
| for (int i = 0; i < length; i++) |
| result.add(Array.get(value, i)); |
| |
| return result; |
| } |
| |
| } |
| |
| /** |
| * Represents a parameter binding in a JPQL query augmented with instructions of how to apply a parameter as LIKE |
| * parameter. This allows expressions like {@code …like %?1} in the JPQL query, which is not allowed by plain JPA. |
| * |
| * @author Oliver Gierke |
| * @author Thomas Darimont |
| */ |
| static class LikeParameterBinding extends ParameterBinding { |
| /** */ |
| private static final List<Type> SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH, |
| Type.ENDING_WITH, Type.LIKE); |
| |
| /** */ |
| private final Type type; |
| |
| /** |
| * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type}. |
| * |
| * @param name must not be {@literal null} or empty. |
| * @param type must not be {@literal null}. |
| */ |
| LikeParameterBinding(String name, Type type) { |
| this(name, type, null); |
| } |
| |
| /** |
| * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type} and |
| * parameter binding input. |
| * |
| * @param name must not be {@literal null} or empty. |
| * @param type must not be {@literal null}. |
| * @param expression may be {@literal null}. |
| */ |
| LikeParameterBinding(String name, Type type, @Nullable String expression) { |
| |
| super(name, null, expression); |
| |
| Assert.hasText(name, "Name must not be null or empty!"); |
| Assert.notNull(type, "Type must not be null!"); |
| |
| Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s!", |
| StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES))); |
| |
| this.type = type; |
| } |
| |
| /** |
| * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}. |
| * |
| * @param position position of the parameter in the query. |
| * @param type must not be {@literal null}. |
| */ |
| LikeParameterBinding(int position, Type type) { |
| this(position, type, null); |
| } |
| |
| /** |
| * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}. |
| * |
| * @param position position of the parameter in the query. |
| * @param type must not be {@literal null}. |
| * @param expression may be {@literal null}. |
| */ |
| LikeParameterBinding(int position, Type type, @Nullable String expression) { |
| |
| super(null, position, expression); |
| |
| Assert.isTrue(position > 0, "Position must be greater than zero!"); |
| Assert.notNull(type, "Type must not be null!"); |
| |
| Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s!", |
| StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES))); |
| |
| this.type = type; |
| } |
| |
| /** |
| * Returns the {@link Type} of the binding. |
| * |
| * @return the type |
| */ |
| public Type getType() { |
| return type; |
| } |
| |
| /** |
| * Prepares the given raw keyword according to the like type. |
| */ |
| @Nullable |
| @Override public Object prepare(@Nullable Object value) { |
| if (value == null) |
| return null; |
| |
| switch (type) { |
| case STARTING_WITH: |
| return String.format("%s%%", value.toString()); |
| case ENDING_WITH: |
| return String.format("%%%s", value.toString()); |
| case CONTAINING: |
| return String.format("%%%s%%", value.toString()); |
| case LIKE: |
| default: |
| return value; |
| } |
| } |
| |
| /** */ |
| @Override public boolean equals(Object obj) { |
| if (!(obj instanceof LikeParameterBinding)) |
| return false; |
| |
| LikeParameterBinding that = (LikeParameterBinding)obj; |
| |
| return super.equals(obj) && type.equals(that.type); |
| } |
| |
| /** */ |
| @Override public int hashCode() { |
| |
| int result = super.hashCode(); |
| |
| result += nullSafeHashCode(type); |
| |
| return result; |
| } |
| |
| /** */ |
| @Override public String toString() { |
| return String.format("LikeBinding [name: %s, position: %d, type: %s]", getName(), getPosition(), type); |
| } |
| |
| /** |
| * Extracts the like {@link Type} from the given JPA like expression. |
| * |
| * @param expression must not be {@literal null} or empty. |
| */ |
| private static Type getLikeTypeFrom(String expression) { |
| |
| Assert.hasText(expression, "Expression must not be null or empty!"); |
| |
| if (expression.matches("%.*%")) |
| return Type.CONTAINING; |
| |
| if (expression.startsWith("%")) |
| return Type.ENDING_WITH; |
| |
| if (expression.endsWith("%")) |
| return Type.STARTING_WITH; |
| |
| return Type.LIKE; |
| } |
| |
| } |
| |
| /** */ |
| static class Metadata { |
| /** |
| * Uses jdbc style parameters. |
| */ |
| private boolean usesJdbcStyleParameters; |
| } |
| |
| /** |
| * 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 |
| */ |
| static class QuotationMap { |
| /** */ |
| private static final Collection<Character> QUOTING_CHARACTERS = Arrays.asList('"', '\''); |
| |
| /** */ |
| private final List<Range<Integer>> quotedRanges = new ArrayList<>(); |
| |
| /** |
| * Creates a new instance 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 idx 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 idx) { |
| return quotedRanges.stream().anyMatch(r -> r.contains(idx)); |
| } |
| } |
| } |