DATAJDBC-514 - Add infrastructure for query derivation in Spring Data Relational.
Original pull request: spring-projects/spring-data-r2dbc#295.
This commit is contained in:
committed by
Mark Paluch
parent
7802f6e19e
commit
74540327f6
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2020 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
|
||||
*
|
||||
* https://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.relational.core.dialect;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Helper class encapsulating an escape character for LIKE queries and the actually usage of it in escaping
|
||||
* {@link String}s.
|
||||
*
|
||||
* @author Roman Chigvintsev
|
||||
* @author Mark Paluch
|
||||
* @since 2.0
|
||||
*/
|
||||
public class Escaper {
|
||||
|
||||
public static final Escaper DEFAULT = Escaper.of('\\');
|
||||
|
||||
private final char escapeCharacter;
|
||||
private final List<String> toReplace;
|
||||
|
||||
private Escaper(char escapeCharacter, List<String> toReplace) {
|
||||
|
||||
if (toReplace.contains(Character.toString(escapeCharacter))) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("'%s' and cannot be used as escape character as it should be replaced", escapeCharacter));
|
||||
}
|
||||
|
||||
this.escapeCharacter = escapeCharacter;
|
||||
this.toReplace = toReplace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new instance of this class with the given escape character.
|
||||
*
|
||||
* @param escapeCharacter escape character
|
||||
* @return new instance of {@link Escaper}.
|
||||
* @throws IllegalArgumentException if escape character is one of special characters ('_' and '%')
|
||||
*/
|
||||
public static Escaper of(char escapeCharacter) {
|
||||
return new Escaper(escapeCharacter, Arrays.asList("_", "%"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the {@link Escaper} to the given {@code chars}.
|
||||
*
|
||||
* @param chars characters/char sequences that should be escaped.
|
||||
* @return
|
||||
*/
|
||||
public Escaper withRewriteFor(String... chars) {
|
||||
|
||||
List<String> toReplace = new ArrayList<>(this.toReplace.size() + chars.length);
|
||||
toReplace.addAll(this.toReplace);
|
||||
toReplace.addAll(Arrays.asList(chars));
|
||||
|
||||
return new Escaper(this.escapeCharacter, toReplace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the escape character.
|
||||
*
|
||||
* @return the escape character to use.
|
||||
*/
|
||||
public char getEscapeCharacter() {
|
||||
return escapeCharacter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes all special like characters ({@code _}, {@code %}) using the configured escape character.
|
||||
*
|
||||
* @param value value to be escaped
|
||||
* @return escaped value
|
||||
*/
|
||||
@Nullable
|
||||
public String escape(@Nullable String value) {
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toReplace.stream().reduce(value, (it, character) -> it.replace(character, escapeCharacter + character));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2020 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
|
||||
*
|
||||
* https://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.relational.repository.query;
|
||||
|
||||
import org.springframework.data.relational.core.query.Criteria;
|
||||
import org.springframework.data.relational.core.sql.Expression;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Simple factory to contain logic to create {@link Criteria}s from {@link Part}s.
|
||||
*
|
||||
* @author Roman Chigvintsev
|
||||
*/
|
||||
class CriteriaFactory {
|
||||
|
||||
private final ParameterMetadataProvider parameterMetadataProvider;
|
||||
|
||||
/**
|
||||
* Creates new instance of this class with the given {@link ParameterMetadataProvider}.
|
||||
*
|
||||
* @param parameterMetadataProvider parameter metadata provider (must not be {@literal null})
|
||||
*/
|
||||
public CriteriaFactory(ParameterMetadataProvider parameterMetadataProvider) {
|
||||
Assert.notNull(parameterMetadataProvider, "Parameter metadata provider must not be null!");
|
||||
this.parameterMetadataProvider = parameterMetadataProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link Criteria} for the given {@link Part}.
|
||||
*
|
||||
* @param part method name part (must not be {@literal null})
|
||||
* @return {@link Criteria} instance
|
||||
* @throws IllegalArgumentException if part type is not supported
|
||||
*/
|
||||
public Criteria createCriteria(Part part) {
|
||||
Part.Type type = part.getType();
|
||||
|
||||
String propertyName = part.getProperty().getSegment();
|
||||
Class<?> propertyType = part.getProperty().getType();
|
||||
|
||||
Criteria.CriteriaStep criteriaStep = Criteria.where(propertyName);
|
||||
|
||||
if (type == Part.Type.IS_NULL || type == Part.Type.IS_NOT_NULL) {
|
||||
return part.getType() == Part.Type.IS_NULL ? criteriaStep.isNull() : criteriaStep.isNotNull();
|
||||
}
|
||||
|
||||
if (type == Part.Type.TRUE || type == Part.Type.FALSE) {
|
||||
return part.getType() == Part.Type.TRUE ? criteriaStep.isTrue() : criteriaStep.isFalse();
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case BETWEEN: {
|
||||
ParameterMetadata geParamMetadata = parameterMetadataProvider.next(part);
|
||||
ParameterMetadata leParamMetadata = parameterMetadataProvider.next(part);
|
||||
return criteriaStep.greaterThanOrEquals(geParamMetadata.getValue()).and(propertyName)
|
||||
.lessThanOrEquals(leParamMetadata.getValue());
|
||||
}
|
||||
case AFTER:
|
||||
case GREATER_THAN: {
|
||||
ParameterMetadata paramMetadata = parameterMetadataProvider.next(part);
|
||||
return criteriaStep.greaterThan(paramMetadata.getValue());
|
||||
}
|
||||
case GREATER_THAN_EQUAL: {
|
||||
ParameterMetadata paramMetadata = parameterMetadataProvider.next(part);
|
||||
return criteriaStep.greaterThanOrEquals(paramMetadata.getValue());
|
||||
}
|
||||
case BEFORE:
|
||||
case LESS_THAN: {
|
||||
ParameterMetadata paramMetadata = parameterMetadataProvider.next(part);
|
||||
return criteriaStep.lessThan(paramMetadata.getValue());
|
||||
}
|
||||
case LESS_THAN_EQUAL: {
|
||||
ParameterMetadata paramMetadata = parameterMetadataProvider.next(part);
|
||||
return criteriaStep.lessThanOrEquals(paramMetadata.getValue());
|
||||
}
|
||||
case IN:
|
||||
case NOT_IN: {
|
||||
ParameterMetadata paramMetadata = parameterMetadataProvider.next(part);
|
||||
Criteria criteria = part.getType() == Part.Type.IN ? criteriaStep.in(paramMetadata.getValue())
|
||||
: criteriaStep.notIn(paramMetadata.getValue());
|
||||
return criteria.ignoreCase(shouldIgnoreCase(part) && checkCanUpperCase(part, part.getProperty().getType()));
|
||||
}
|
||||
case STARTING_WITH:
|
||||
case ENDING_WITH:
|
||||
case CONTAINING:
|
||||
case NOT_CONTAINING:
|
||||
case LIKE:
|
||||
case NOT_LIKE: {
|
||||
ParameterMetadata paramMetadata = parameterMetadataProvider.next(part);
|
||||
Criteria criteria = part.getType() == Part.Type.NOT_LIKE || part.getType() == Part.Type.NOT_CONTAINING
|
||||
? criteriaStep.notLike(paramMetadata.getValue())
|
||||
: criteriaStep.like(paramMetadata.getValue());
|
||||
return criteria
|
||||
.ignoreCase(shouldIgnoreCase(part) && checkCanUpperCase(part, propertyType, paramMetadata.getType()));
|
||||
}
|
||||
case SIMPLE_PROPERTY: {
|
||||
ParameterMetadata paramMetadata = parameterMetadataProvider.next(part);
|
||||
if (paramMetadata.getValue() == null) {
|
||||
return criteriaStep.isNull();
|
||||
}
|
||||
return criteriaStep.is(paramMetadata.getValue())
|
||||
.ignoreCase(shouldIgnoreCase(part) && checkCanUpperCase(part, propertyType, paramMetadata.getType()));
|
||||
}
|
||||
case NEGATING_SIMPLE_PROPERTY: {
|
||||
ParameterMetadata paramMetadata = parameterMetadataProvider.next(part);
|
||||
return criteriaStep.not(paramMetadata.getValue())
|
||||
.ignoreCase(shouldIgnoreCase(part) && checkCanUpperCase(part, propertyType, paramMetadata.getType()));
|
||||
}
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported keyword " + type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether comparison should be done in case-insensitive way.
|
||||
*
|
||||
* @param part method name part (must not be {@literal null})
|
||||
* @return {@literal true} if comparison should be done in case-insensitive way
|
||||
*/
|
||||
private boolean shouldIgnoreCase(Part part) {
|
||||
return part.shouldIgnoreCase() == Part.IgnoreCaseType.ALWAYS
|
||||
|| part.shouldIgnoreCase() == Part.IgnoreCaseType.WHEN_POSSIBLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether "upper-case" conversion can be applied to the given {@link Expression}s in case the underlying
|
||||
* {@link Part} requires ignoring case.
|
||||
*
|
||||
* @param part method name part (must not be {@literal null})
|
||||
* @param expressionTypes types of the given expressions (must not be {@literal null} or empty)
|
||||
* @throws IllegalStateException if {@link Part} requires ignoring case but "upper-case" conversion cannot be applied
|
||||
* to at least one of the given {@link Expression}s
|
||||
*/
|
||||
private boolean checkCanUpperCase(Part part, Class<?>... expressionTypes) {
|
||||
Assert.notEmpty(expressionTypes, "Expression types must not be null or empty");
|
||||
boolean strict = part.shouldIgnoreCase() == Part.IgnoreCaseType.ALWAYS;
|
||||
for (Class<?> expressionType : expressionTypes) {
|
||||
if (!canUpperCase(expressionType)) {
|
||||
if (strict) {
|
||||
throw new IllegalStateException("Unable to ignore case of " + expressionType.getName()
|
||||
+ " type, the property '" + part.getProperty().getSegment() + "' must reference a string");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean canUpperCase(Class<?> expressionType) {
|
||||
return expressionType == String.class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2020 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
|
||||
*
|
||||
* https://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.relational.repository.query;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Helper class for holding information about query parameter.
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
class ParameterMetadata {
|
||||
|
||||
private final String name;
|
||||
private final @Nullable Object value;
|
||||
private final Class<?> type;
|
||||
|
||||
public ParameterMetadata(String name, @Nullable Object value, Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "Parameter type must not be null");
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Class<?> getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2020 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
|
||||
*
|
||||
* https://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.relational.repository.query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.relational.core.dialect.Escaper;
|
||||
import org.springframework.data.repository.query.Parameter;
|
||||
import org.springframework.data.repository.query.Parameters;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Helper class to allow easy creation of {@link ParameterMetadata}s.
|
||||
* <p>
|
||||
* This class is an adapted version of {@code org.springframework.data.jpa.repository.query.ParameterMetadataProvider}
|
||||
* from Spring Data JPA project.
|
||||
*
|
||||
* @author Roman Chigvintsev
|
||||
* @since 2.0
|
||||
*/
|
||||
public class ParameterMetadataProvider implements Iterable<ParameterMetadata> {
|
||||
|
||||
private static final Object VALUE_PLACEHOLDER = new Object();
|
||||
|
||||
private final Iterator<? extends Parameter> bindableParameterIterator;
|
||||
private final Iterator<Object> bindableParameterValueIterator;
|
||||
private final List<ParameterMetadata> parameterMetadata = new ArrayList<>();
|
||||
private final Escaper escaper;
|
||||
|
||||
/**
|
||||
* Creates new instance of this class with the given {@link RelationalParameterAccessor} and {@link Escaper}.
|
||||
*
|
||||
* @param accessor relational parameter accessor (must not be {@literal null}).
|
||||
* @param escaper escaper for LIKE operator parameters (must not be {@literal null})
|
||||
*/
|
||||
public ParameterMetadataProvider(RelationalParameterAccessor accessor, Escaper escaper) {
|
||||
this(accessor.getBindableParameters(), accessor.iterator(), escaper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new instance of this class with the given {@link Parameters} and {@link Escaper}.
|
||||
*
|
||||
* @param parameters method parameters (must not be {@literal null})
|
||||
* @param escaper escaper for LIKE operator parameters (must not be {@literal null})
|
||||
*/
|
||||
public ParameterMetadataProvider(Parameters<?, ?> parameters, Escaper escaper) {
|
||||
this(parameters, null, escaper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new instance of this class with the given {@link Parameters}, {@link Iterator} over all bindable parameter
|
||||
* values and {@link Escaper}.
|
||||
*
|
||||
* @param bindableParameterValueIterator iterator over bindable parameter values
|
||||
* @param parameters method parameters (must not be {@literal null})
|
||||
* @param escaper escaper for LIKE operator parameters (must not be {@literal null})
|
||||
*/
|
||||
private ParameterMetadataProvider(Parameters<?, ?> parameters,
|
||||
@Nullable Iterator<Object> bindableParameterValueIterator, Escaper escaper) {
|
||||
Assert.notNull(parameters, "Parameters must not be null!");
|
||||
Assert.notNull(escaper, "Like escaper must not be null!");
|
||||
|
||||
this.bindableParameterIterator = parameters.getBindableParameters().iterator();
|
||||
this.bindableParameterValueIterator = bindableParameterValueIterator;
|
||||
this.escaper = escaper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<ParameterMetadata> iterator() {
|
||||
return parameterMetadata.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new instance of {@link ParameterMetadata} for the given {@link Part} and next {@link Parameter}.
|
||||
*/
|
||||
public ParameterMetadata next(Part part) {
|
||||
Assert.isTrue(bindableParameterIterator.hasNext(),
|
||||
() -> String.format("No parameter available for part %s.", part));
|
||||
Parameter parameter = bindableParameterIterator.next();
|
||||
String parameterName = getParameterName(parameter, part.getProperty().getSegment());
|
||||
Object parameterValue = getParameterValue();
|
||||
Part.Type partType = part.getType();
|
||||
|
||||
checkNullIsAllowed(parameterName, parameterValue, partType);
|
||||
Class<?> parameterType = parameter.getType();
|
||||
Object preparedParameterValue = prepareParameterValue(parameterValue, parameterType, partType);
|
||||
|
||||
ParameterMetadata metadata = new ParameterMetadata(parameterName, preparedParameterValue, parameterType);
|
||||
parameterMetadata.add(metadata);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private String getParameterName(Parameter parameter, String defaultName) {
|
||||
if (parameter.isExplicitlyNamed()) {
|
||||
return parameter.getName().orElseThrow(() -> new IllegalArgumentException("Parameter needs to be named"));
|
||||
}
|
||||
return defaultName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Object getParameterValue() {
|
||||
return bindableParameterValueIterator == null ? VALUE_PLACEHOLDER : bindableParameterValueIterator.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether {@literal null} is allowed as parameter value.
|
||||
*
|
||||
* @param parameterName parameter name
|
||||
* @param parameterValue parameter value
|
||||
* @param partType method name part type (must not be {@literal null})
|
||||
* @throws IllegalArgumentException if {@literal null} is not allowed as parameter value
|
||||
*/
|
||||
private void checkNullIsAllowed(String parameterName, @Nullable Object parameterValue, Part.Type partType) {
|
||||
|
||||
if (parameterValue == null && !Part.Type.SIMPLE_PROPERTY.equals(partType)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Value of parameter with name %s must not be null!", parameterName));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares parameter value before it's actually bound to the query.
|
||||
*
|
||||
* @param value must not be {@literal null}
|
||||
* @return prepared query parameter value
|
||||
*/
|
||||
@Nullable
|
||||
protected Object prepareParameterValue(@Nullable Object value, Class<?> valueType, Part.Type partType) {
|
||||
|
||||
if (value != null && String.class == valueType) {
|
||||
switch (partType) {
|
||||
case STARTING_WITH:
|
||||
return escaper.escape(value.toString()) + "%";
|
||||
case ENDING_WITH:
|
||||
return "%" + escaper.escape(value.toString());
|
||||
case CONTAINING:
|
||||
case NOT_CONTAINING:
|
||||
return "%" + escaper.escape(value.toString()) + "%";
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2020 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
|
||||
*
|
||||
* https://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.relational.repository.query;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.springframework.data.relational.core.query.Criteria;
|
||||
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of {@link AbstractQueryCreator} that creates {@link PreparedOperation} from a {@link PartTree}.
|
||||
*
|
||||
* @author Roman Chigvintsev
|
||||
* @since 2.0
|
||||
*/
|
||||
public abstract class RelationalQueryCreator<T> extends AbstractQueryCreator<T, Criteria> {
|
||||
|
||||
private final CriteriaFactory criteriaFactory;
|
||||
|
||||
/**
|
||||
* Creates new instance of this class with the given {@link PartTree}, {@link RelationalEntityMetadata} and
|
||||
* {@link ParameterMetadataProvider}.
|
||||
*
|
||||
* @param tree part tree (must not be {@literal null})
|
||||
* @param parameterMetadataProvider parameter metadata provider (must not be {@literal null})
|
||||
*/
|
||||
public RelationalQueryCreator(PartTree tree, ParameterMetadataProvider parameterMetadataProvider) {
|
||||
super(tree);
|
||||
|
||||
Assert.notNull(parameterMetadataProvider, "Parameter metadata provider must not be null");
|
||||
this.criteriaFactory = new CriteriaFactory(parameterMetadataProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link Criteria} for the given method name part.
|
||||
*
|
||||
* @param part method name part (must not be {@literal null})
|
||||
* @param iterator iterator over query parameter values
|
||||
* @return new instance of {@link Criteria}
|
||||
*/
|
||||
@Override
|
||||
protected Criteria create(Part part, Iterator<Object> iterator) {
|
||||
return criteriaFactory.createCriteria(part);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the given {@link Criteria} with the new one created for the given method name part using {@code AND}.
|
||||
*
|
||||
* @param part method name part (must not be {@literal null})
|
||||
* @param base {@link Criteria} to be combined (must not be {@literal null})
|
||||
* @param iterator iterator over query parameter values
|
||||
* @return {@link Criteria} combination
|
||||
*/
|
||||
@Override
|
||||
protected Criteria and(Part part, Criteria base, Iterator<Object> iterator) {
|
||||
return base.and(criteriaFactory.createCriteria(part));
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines two {@link Criteria}s using {@code OR}.
|
||||
*
|
||||
* @param base {@link Criteria} to be combined (must not be {@literal null})
|
||||
* @param criteria another {@link Criteria} to be combined (must not be {@literal null})
|
||||
* @return {@link Criteria} combination
|
||||
*/
|
||||
@Override
|
||||
protected Criteria or(Criteria base, Criteria criteria) {
|
||||
return base.or(criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate parameters for the derived query. Specifically checking that the query method defines scalar parameters
|
||||
* and collection parameters where required and that invalid parameter declarations are rejected.
|
||||
*
|
||||
* @param tree
|
||||
* @param parameters
|
||||
*/
|
||||
public static void validate(PartTree tree, RelationalParameters parameters) {
|
||||
|
||||
int argCount = 0;
|
||||
Iterable<Part> parts = () -> tree.stream().flatMap(Streamable::stream).iterator();
|
||||
for (Part part : parts) {
|
||||
int numberOfArguments = part.getNumberOfArguments();
|
||||
for (int i = 0; i < numberOfArguments; i++) {
|
||||
throwExceptionOnArgumentMismatch(part, parameters, argCount);
|
||||
argCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void throwExceptionOnArgumentMismatch(Part part, RelationalParameters parameters, int index) {
|
||||
|
||||
Part.Type type = part.getType();
|
||||
String property = part.getProperty().toDotPath();
|
||||
|
||||
if (!parameters.getBindableParameters().hasParameterAt(index)) {
|
||||
String msgTemplate = "Query method expects at least %d arguments but only found %d. "
|
||||
+ "This leaves an operator of type %s for property %s unbound.";
|
||||
String formattedMsg = String.format(msgTemplate, index + 1, index, type.name(), property);
|
||||
throw new IllegalStateException(formattedMsg);
|
||||
}
|
||||
|
||||
RelationalParameters.RelationalParameter parameter = parameters.getBindableParameter(index);
|
||||
if (expectsCollection(type) && !parameterIsCollectionLike(parameter)) {
|
||||
String message = wrongParameterTypeMessage(property, type, "Collection", parameter);
|
||||
throw new IllegalStateException(message);
|
||||
} else if (!expectsCollection(type) && !parameterIsScalarLike(parameter)) {
|
||||
String message = wrongParameterTypeMessage(property, type, "scalar", parameter);
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean expectsCollection(Part.Type type) {
|
||||
return type == Part.Type.IN || type == Part.Type.NOT_IN;
|
||||
}
|
||||
|
||||
private static boolean parameterIsCollectionLike(RelationalParameters.RelationalParameter parameter) {
|
||||
return parameter.getType().isArray() || Collection.class.isAssignableFrom(parameter.getType());
|
||||
}
|
||||
|
||||
private static boolean parameterIsScalarLike(RelationalParameters.RelationalParameter parameter) {
|
||||
return !Collection.class.isAssignableFrom(parameter.getType());
|
||||
}
|
||||
|
||||
private static String wrongParameterTypeMessage(String property, Part.Type operatorType, String expectedArgumentType,
|
||||
RelationalParameters.RelationalParameter parameter) {
|
||||
return String.format("Operator %s on %s requires a %s argument, found %s", operatorType.name(), property,
|
||||
expectedArgumentType, parameter.getType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2020 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
|
||||
*
|
||||
* https://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.relational.core.dialect;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Escaper}.
|
||||
*
|
||||
* @author Roman Chigvintsev
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class EscaperUnitTests {
|
||||
|
||||
@Test // DATAJDBC-514
|
||||
public void ignoresNulls() {
|
||||
assertThat((Escaper.DEFAULT.escape(null))).isNull();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-514
|
||||
public void ignoresEmptyString() {
|
||||
assertThat(Escaper.DEFAULT.escape("")).isEmpty();
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-514
|
||||
public void ignoresBlankString() {
|
||||
assertThat(Escaper.DEFAULT.escape(" ")).isEqualTo(" ");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-514
|
||||
public void throwsExceptionWhenEscapeCharacterIsUnderscore() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> Escaper.of('_'));
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-514
|
||||
public void throwsExceptionWhenEscapeCharacterIsPercent() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> Escaper.of('%'));
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-514
|
||||
public void escapesUnderscoresUsingDefaultEscapeCharacter() {
|
||||
assertThat(Escaper.DEFAULT.escape("_test_")).isEqualTo("\\_test\\_");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-514
|
||||
public void escapesPercentsUsingDefaultEscapeCharacter() {
|
||||
assertThat(Escaper.DEFAULT.escape("%test%")).isEqualTo("\\%test\\%");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-514
|
||||
public void escapesSpecialCharactersUsingCustomEscapeCharacter() {
|
||||
assertThat(Escaper.of('$').escape("_%")).isEqualTo("$_$%");
|
||||
}
|
||||
|
||||
@Test // DATAJDBC-514
|
||||
public void escapesAdditionalCharacters() {
|
||||
assertThat(Escaper.DEFAULT.withRewriteFor("[", "]").escape("Hello Wo[Rr]ld")).isEqualTo("Hello Wo\\[Rr\\]ld");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user