From 74540327f6cebdf13158065c6424ae975d79280c Mon Sep 17 00:00:00 2001 From: Roman Chigvintsev Date: Fri, 27 Mar 2020 12:27:43 +0100 Subject: [PATCH] DATAJDBC-514 - Add infrastructure for query derivation in Spring Data Relational. Original pull request: spring-projects/spring-data-r2dbc#295. --- .../data/relational/core/dialect/Escaper.java | 100 +++++++++++ .../repository/query/CriteriaFactory.java | 166 ++++++++++++++++++ .../repository/query/ParameterMetadata.java | 52 ++++++ .../query/ParameterMetadataProvider.java | 161 +++++++++++++++++ .../query/RelationalQueryCreator.java | 148 ++++++++++++++++ .../core/dialect/EscaperUnitTests.java | 74 ++++++++ 6 files changed, 701 insertions(+) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Escaper.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/CriteriaFactory.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadata.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadataProvider.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/EscaperUnitTests.java diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Escaper.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Escaper.java new file mode 100644 index 00000000..a50c4421 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/Escaper.java @@ -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 toReplace; + + private Escaper(char escapeCharacter, List 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 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)); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/CriteriaFactory.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/CriteriaFactory.java new file mode 100644 index 00000000..686a8e01 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/CriteriaFactory.java @@ -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; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadata.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadata.java new file mode 100644 index 00000000..cf986c04 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadata.java @@ -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; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadataProvider.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadataProvider.java new file mode 100644 index 00000000..a2413ab5 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/ParameterMetadataProvider.java @@ -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. + *

+ * 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 { + + private static final Object VALUE_PLACEHOLDER = new Object(); + + private final Iterator bindableParameterIterator; + private final Iterator bindableParameterValueIterator; + private final List 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 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 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; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java new file mode 100644 index 00000000..1a86462b --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java @@ -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 extends AbstractQueryCreator { + + 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 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 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 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()); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/EscaperUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/EscaperUnitTests.java new file mode 100644 index 00000000..a751010d --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/EscaperUnitTests.java @@ -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"); + } +}