From 1bc460ca0a098fd151e469ed96f981298599068d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 13 Oct 2016 15:44:29 +0200 Subject: [PATCH] DATACASS-117 - Support named and expression parameters in String-based repository query methods. We now support named and expression parameters in String-based repository query methods. Name-based parameters are referenced with :parameter. Expression parameters can reference either parameter names (if provided) with :#{expression}/#{#fieldname} or index-based with ?#{[0]}. String-based query creation now also serializes parameters using the configured CodecRegistry so escaping and serialization is handled by the driver itself which leads to correct queries. public interface SampleRepository extends Repository { @Query("SELECT * FROM person WHERE lastname = ?0;") Person findByLastname(String lastname); @Query("SELECT * FROM person WHERE lastname = :lastname;") Person findByNamedParameter(@Param("lastname") String lastname); @Query("SELECT * FROM person WHERE lastname = ?#{[0]};") Person findByIndexExpressionParameter(String lastname); @Query("SELECT * FROM person WHERE lastnames IN (?0) AND age = ?1;") Person findByLastNamesAndAge(Collection lastname, int age); @Query("SELECT * FROM person WHERE lastname = :#{#lastname == 'Matthews' ? 'Admin' : #lastname};") Person findByConditionalExpressionParameter(@Param("lastname") String lastname); } Related tickets: DATACASS-122, DATACASS-240 --- .../query/CassandraParameterAccessor.java | 8 + .../CassandraParametersParameterAccessor.java | 16 + .../query/CassandraQueryMethod.java | 9 + .../query/ConvertingParameterAccessor.java | 27 +- .../ExpressionEvaluatingParameterBinder.java | 185 ++++++++ .../query/StringBasedCassandraQuery.java | 434 +++++++++++++++--- .../support/CassandraRepositoryFactory.java | 39 +- .../ParameterBindingParserUnitTests.java | 144 ++++++ ...sedCassandraQueryIntegrationUnitTests.java | 301 ++++++++++-- .../query/StubParameterAccessor.java | 7 +- .../PersonRepositoryWithQueryAnnotations.java | 11 +- 11 files changed, 1062 insertions(+), 119 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ExpressionEvaluatingParameterBinder.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ParameterBindingParserUnitTests.java diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java index 745c4106c..87267cf0d 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParameterAccessor.java @@ -59,4 +59,12 @@ public interface CassandraParameterAccessor extends ParameterAccessor { * @return the parameter type, never {@literal null}. */ Class getParameterType(int index); + + /** + * Returns the raw parameter values of the underlying query method. + * + * @return + * @since 1.5 + */ + Object[] getValues(); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParametersParameterAccessor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParametersParameterAccessor.java index 20df4e9e3..212f814f1 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParametersParameterAccessor.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraParametersParameterAccessor.java @@ -15,6 +15,9 @@ */ package org.springframework.data.cassandra.repository.query; +import java.util.Arrays; +import java.util.List; + import org.springframework.data.cassandra.mapping.CassandraSimpleTypeHolder; import org.springframework.data.cassandra.mapping.CassandraType; import org.springframework.data.repository.query.ParameterAccessor; @@ -31,6 +34,8 @@ import com.datastax.driver.core.DataType; public class CassandraParametersParameterAccessor extends ParametersParameterAccessor implements CassandraParameterAccessor { + private final List values; + /** * Creates a new {@link CassandraParametersParameterAccessor}. * @@ -38,7 +43,9 @@ public class CassandraParametersParameterAccessor extends ParametersParameterAcc * @param values must not be {@literal null}. */ public CassandraParametersParameterAccessor(CassandraQueryMethod method, Object... values) { + super(method.getParameters(), values); + this.values = Arrays.asList(values); } /* @@ -79,4 +86,13 @@ public class CassandraParametersParameterAccessor extends ParametersParameterAcc public Class getParameterType(int index) { return getParameters().getParameter(index).getType(); } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.CassandraParameterAccessor#getValues() + */ + @Override + public Object[] getValues() { + return values.toArray(); + } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java index ec47468d6..36dc4f4a5 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java @@ -114,6 +114,15 @@ public class CassandraQueryMethod extends QueryMethod { return this.entityMetadata; } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#getParameters() + */ + @Override + public CassandraParameters getParameters() { + return (CassandraParameters) super.getParameters(); + } + /* (non-Javadoc) * @see org.springframework.data.repository.query.QueryMethod#createParameters(java.lang.reflect.Method) */ diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java index b9d87a1cf..0cb5feab3 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ConvertingParameterAccessor.java @@ -48,11 +48,13 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { private final static TypeInformation SET = ClassTypeInformation.from(Set.class); - private final CassandraConverter cassandraConverter; + private final CassandraConverter converter; private final CassandraParameterAccessor delegate; - ConvertingParameterAccessor(CassandraConverter cassandraConverter, CassandraParameterAccessor delegate) { - this.cassandraConverter = cassandraConverter; + ConvertingParameterAccessor(CassandraConverter converter, + CassandraParameterAccessor delegate) { + + this.converter = converter; this.delegate = delegate; } @@ -101,8 +103,7 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { DataType dataType = delegate.getDataType(index); - return (dataType != null ? dataType - : cassandraConverter.getMappingContext().getDataType(getParameterType(index))); + return (dataType != null ? dataType : converter.getMappingContext().getDataType(getParameterType(index))); } /* (non-Javadoc) @@ -128,6 +129,14 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { return new ConvertingIterator(delegate.iterator()); } + /* (non-Javadoc) + * @see org.springframework.data.cassandra.repository.query.CassandraParameterAccessor#getValues() + */ + @Override + public Object[] getValues() { + return delegate.getValues(); + } + @SuppressWarnings("unchecked") private Object potentiallyConvert(int index, Object bindableValue, CassandraPersistentProperty property) { @@ -164,15 +173,15 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { return bindableValue; } - return cassandraConverter.getConversionService().convert(bindableValue, cassandraType.getJavaType().getRawType()); + return converter.getConversionService().convert(bindableValue, cassandraType.getJavaType().getRawType()); } private CustomConversions getCustomConversions() { - return cassandraConverter.getCustomConversions(); + return converter.getCustomConversions(); } private ConversionService getConversionService() { - return cassandraConverter.getConversionService(); + return converter.getConversionService(); } /** @@ -191,7 +200,7 @@ class ConvertingParameterAccessor implements CassandraParameterAccessor { return CassandraSimpleTypeHolder.getDataTypeFor(cassandraType.type()); } - CassandraMappingContext mappingContext = cassandraConverter.getMappingContext(); + CassandraMappingContext mappingContext = converter.getMappingContext(); TypeInformation typeInformation = ClassTypeInformation.from(getParameterType(index)); if (property == null) { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ExpressionEvaluatingParameterBinder.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ExpressionEvaluatingParameterBinder.java new file mode 100644 index 000000000..2d7ec447d --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ExpressionEvaluatingParameterBinder.java @@ -0,0 +1,185 @@ +/* + * Copyright 2016 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.cassandra.repository.query; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery.ParameterBinding; +import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placeholders within a + * {@link String}. + * + * @author Mark Paluch + * @since 1.5 + */ +class ExpressionEvaluatingParameterBinder { + + private final SpelExpressionParser expressionParser; + private final EvaluationContextProvider evaluationContextProvider; + + /** + * Creates new {@link ExpressionEvaluatingParameterBinder} + * + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + */ + public ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser, + EvaluationContextProvider evaluationContextProvider) { + + Assert.notNull(expressionParser, "ExpressionParser must not be null!"); + Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!"); + + this.expressionParser = expressionParser; + this.evaluationContextProvider = evaluationContextProvider; + } + + /** + * Bind values provided by {@link CassandraParameterAccessor} to placeholders in {@link BindingContext} while + * considering potential conversions and parameter types. + * + * @param raw can be {@literal null} or empty. + * @param accessor must not be {@literal null}. + * @param bindingContext must not be {@literal null}. + * @return {@literal null} if given {@code raw} value is empty. + */ + public List bind(CassandraParameterAccessor accessor, BindingContext bindingContext) { + + if (!bindingContext.hasBindings()) { + return Collections.emptyList(); + } + + List parameters = new ArrayList(bindingContext.getBindings().size()); + + for (ParameterBinding binding : bindingContext.getBindings()) { + parameters.add(getParameterValueForBinding(accessor, bindingContext.getParameters(), binding)); + } + + return parameters; + } + + /** + * Returns the value to be used for the given {@link ParameterBinding}. + * + * @param accessor must not be {@literal null}. + * @param parameters must not be {@literal null}. + * @param binding must not be {@literal null}. + * @return + */ + private Object getParameterValueForBinding(CassandraParameterAccessor accessor, CassandraParameters parameters, + ParameterBinding binding) { + + if (binding.isExpression()) { + return evaluateExpression(binding.getExpression(), parameters, accessor.getValues()); + } + + return binding.isNamed() ? accessor.getBindableValue(getParameterIndex(parameters, binding.getParameterName())) + : accessor.getBindableValue(binding.getParameterIndex()); + } + + private int getParameterIndex(CassandraParameters parameters, String parameterName) { + + for (CassandraParameters.CassandraParameter parameter : parameters) { + if (parameterName.equals(parameter.getName())) { + return parameter.getIndex(); + } + } + + throw new IllegalArgumentException( + String.format("Invalid parameter name! Cannot resolve parameter [%s]", parameterName)); + } + + /** + * Evaluates the given {@code expressionString}. + * + * @param expressionString must not be {@literal null} or empty. + * @param parameters must not be {@literal null}. + * @param parameterValues must not be {@literal null}. + * @return + */ + private Object evaluateExpression(String expressionString, CassandraParameters parameters, Object[] parameterValues) { + + EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(parameters, parameterValues); + Expression expression = expressionParser.parseExpression(expressionString); + + return expression.getValue(evaluationContext, Object.class); + } + + /** + * @author Mark Paluch + * @since 1.5 + */ + static class BindingContext { + + final CassandraQueryMethod queryMethod; + final List bindings; + + /** + * Creates new {@link BindingContext}. + * + * @param queryMethod + * @param bindings + */ + public BindingContext(CassandraQueryMethod queryMethod, List bindings) { + + this.queryMethod = queryMethod; + this.bindings = bindings; + } + + /** + * @return {@literal true} when list of bindings is not empty. + */ + boolean hasBindings() { + return !CollectionUtils.isEmpty(bindings); + } + + /** + * Get unmodifiable list of {@link ParameterBinding}s. + * + * @return never {@literal null}. + */ + public List getBindings() { + return Collections.unmodifiableList(bindings); + } + + /** + * Get the associated {@link CassandraParameters}. + * + * @return + */ + public CassandraParameters getParameters() { + return queryMethod.getParameters(); + } + + /** + * Get the {@link CassandraQueryMethod}. + * + * @return + */ + public CassandraQueryMethod getQueryMethod() { + return queryMethod; + } + + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java index 7bfa113fb..c569ac91f 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQuery.java @@ -15,44 +15,85 @@ */ package org.springframework.data.cassandra.repository.query; -import java.util.Arrays; -import java.util.Date; -import java.util.HashSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.cassandra.core.cql.CqlStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.data.cassandra.core.CassandraOperations; -import org.springframework.util.ClassUtils; +import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.BindingContext; +import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.data.repository.query.QueryCreationException; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; -import com.datastax.driver.core.LocalDate; +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.TypeCodec; +import com.datastax.driver.core.querybuilder.BindMarker; /** - * Query to use a plain String to create the {@link Query} to actually execute. + * String-based {@link AbstractCassandraQuery} implementation. + *

+ * A {@link StringBasedCassandraQuery} expects a query method to be annotated with + * {@link org.springframework.data.cassandra.repository.Query} with a CQL query. String-based queries support named, + * index-based and expression parameters that are resolved during query execution. * * @author Matthew Adams * @author Mark Paluch + * @see org.springframework.data.cassandra.repository.Query */ public class StringBasedCassandraQuery extends AbstractCassandraQuery { - @SuppressWarnings("unchecked") - private static final Set> STRING_LIKE_PARAMETER_TYPES = new HashSet>( - Arrays.asList(CharSequence.class, char.class, Character.class, char[].class)); + private static final Logger LOG = LoggerFactory.getLogger(StringBasedCassandraQuery.class); + private static final ParameterBindingParser BINDING_PARSER = ParameterBindingParser.INSTANCE; - private static final Pattern PLACEHOLDER = Pattern.compile("\\?(\\d+)"); + private final String query; + private final List queryParameterBindings; + private final ExpressionEvaluatingParameterBinder parameterBinder; + private final CodecRegistry codecRegistry; - protected final String query; + /** + * Creates a new {@link StringBasedCassandraQuery} for the given {@link CassandraQueryMethod}, + * {@link CassandraOperations}, {@link SpelExpressionParser}, and {@link EvaluationContextProvider}. + * + * @param queryMethod + * @param operations + * @param expressionParser + * @param evaluationContextProvider + */ + public StringBasedCassandraQuery(CassandraQueryMethod queryMethod, CassandraOperations operations, + SpelExpressionParser expressionParser, EvaluationContextProvider evaluationContextProvider) { - public StringBasedCassandraQuery(String query, CassandraQueryMethod queryMethod, CassandraOperations operations) { + this(queryMethod.getAnnotatedQuery(), queryMethod, operations, expressionParser, evaluationContextProvider); + } + + /** + * Creates a new {@link StringBasedCassandraQuery} for the given {@code query}, {@link CassandraQueryMethod}, + * {@link CassandraOperations}, {@link SpelExpressionParser}, and {@link EvaluationContextProvider}. + * + * @param query + * @param queryMethod + * @param operations + * @param expressionParser + * @param evaluationContextProvider + */ + public StringBasedCassandraQuery(String query, CassandraQueryMethod queryMethod, CassandraOperations operations, + SpelExpressionParser expressionParser, EvaluationContextProvider evaluationContextProvider) { super(queryMethod, operations); - this.query = query; - } - - public StringBasedCassandraQuery(CassandraQueryMethod queryMethod, CassandraOperations operations) { - this(queryMethod.getAnnotatedQuery(), queryMethod, operations); + this.queryParameterBindings = new ArrayList(); + this.query = BINDING_PARSER.parseAndCollectParameterBindingsFromQueryIntoBindings(query, + this.queryParameterBindings); + this.parameterBinder = new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider); + this.codecRegistry = operations.getSession().getCluster().getConfiguration().getCodecRegistry(); } /* (non-Javadoc) @@ -60,59 +101,344 @@ public class StringBasedCassandraQuery extends AbstractCassandraQuery { */ @Override public String createQuery(CassandraParameterAccessor accessor) { - return replacePlaceholders(query, accessor); - } - private String replacePlaceholders(String input, CassandraParameterAccessor accessor) { + try { + List arguments = this.parameterBinder.bind(accessor, + new BindingContext(getQueryMethod(), queryParameterBindings)); - Matcher matcher = PLACEHOLDER.matcher(input); - String result = input; + String boundQuery = bind(query, arguments); - while (matcher.find()) { - String group = matcher.group(); - int index = Integer.parseInt(matcher.group(1)); - Object value = getParameterWithIndex(accessor, index); - String stringValue; - - if (isStringLike(value)) { - stringValue = String.format("'%s'", CqlStringUtils.escapeSingle(value)); - } else if (isTimestampParameter(value)) { - stringValue = String.format("%d", ((Date) value).getTime()); - } else if (isDateParameter(value)) { - stringValue = String.format("'%s'", value); - } else { - stringValue = value.toString(); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Created query [%s].", boundQuery)); } - result = result.replace(group, stringValue); + return boundQuery; + } catch (RuntimeException e) { + throw QueryCreationException.create(getQueryMethod(), e); + } + } + + private String bind(String query, List arguments) { + return ParameterBinder.INSTANCE.bind(query, codecRegistry, arguments); + } + + /** + * A parser that extracts the parameter bindings from a given query string. + * + * @author Mark Paluch + */ + enum ParameterBinder { + + INSTANCE; + + private static final String ARGUMENT_PLACEHOLDER = "?_param_?"; + private static final Pattern ARGUMENT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARGUMENT_PLACEHOLDER)); + + public String bind(String input, CodecRegistry codecRegistry, List parameters) { + + if (parameters.isEmpty()) { + return input; + } + + StringBuilder result = new StringBuilder(); + + int startIndex = 0; + int currentPos = 0; + int parameterIndex = 0; + + Matcher matcher = ARGUMENT_PLACEHOLDER_PATTERN.matcher(input); + + while (currentPos < input.length()) { + + if (!matcher.find()) { + break; + } + int exprStart = matcher.start(); + result.append(input.subSequence(startIndex, exprStart)); + + result = appendValue(parameters.get(parameterIndex++), codecRegistry, result); + + currentPos = matcher.end(); + startIndex = currentPos; + } + + return result.append(input.subSequence(currentPos, input.length())).toString(); } - return result; + static StringBuilder appendValue(Object value, CodecRegistry codecRegistry, StringBuilder sb) { + if (value == null) { + sb.append("null"); + } else if (value instanceof BindMarker) { + sb.append(value); + } else if (value instanceof List && isSerializable(value)) { + // bind variables are not supported inside collection literals + appendList((List) value, codecRegistry, sb); + } else if (value instanceof Set && isSerializable(value)) { + // bind variables are not supported inside collection literals + appendSet((Set) value, codecRegistry, sb); + } else if (value instanceof Map && isSerializable(value)) { + // bind variables are not supported inside collection literals + appendMap((Map) value, codecRegistry, sb); + } else if (isSerializable(value)) { + TypeCodec codec = codecRegistry.codecFor(value); + sb.append(codec.format(value)); + } else { + throw new IllegalArgumentException(String.format("Argument value [%s] is not serializable", value.toString())); + } + return sb; + } + + private static StringBuilder appendList(List l, CodecRegistry codecRegistry, StringBuilder sb) { + for (int i = 0; i < l.size(); i++) { + if (i > 0) + sb.append(','); + appendValue(l.get(i), codecRegistry, sb); + } + return sb; + } + + private static StringBuilder appendSet(Set s, CodecRegistry codecRegistry, StringBuilder sb) { + boolean first = true; + for (Object elt : s) { + if (first) + first = false; + else + sb.append(','); + appendValue(elt, codecRegistry, sb); + } + return sb; + } + + private static StringBuilder appendMap(Map m, CodecRegistry codecRegistry, StringBuilder sb) { + sb.append('{'); + boolean first = true; + for (Map.Entry entry : m.entrySet()) { + if (first) + first = false; + else + sb.append(','); + appendValue(entry.getKey(), codecRegistry, sb); + sb.append(':'); + appendValue(entry.getValue(), codecRegistry, sb); + } + sb.append('}'); + return sb; + } + + /** + * Return true if the given value is likely to find a suitable codec to be serialized as a query parameter. If the + * value is not serializable, it must be included in the query string. Non serializable values include special + * values such as function calls, column names and bind markers, and collections thereof. We also don't serialize + * fixed size number types. The reason is that if we do it, we will force a particular size (4 bytes for ints, ...) + * and for the query builder, we don't want users to have to bother with that. + * + * @param value the value to inspect. + * @return true if the value is serializable, false otherwise. + */ + static boolean isSerializable(Object value) { + if (containsSpecialValue(value)) + return false; + if (value instanceof Collection) + for (Object elt : (Collection) value) + if (!isSerializable(elt)) + return false; + if (value instanceof Map) + for (Map.Entry entry : ((Map) value).entrySet()) + if (!isSerializable(entry.getKey()) || !isSerializable(entry.getValue())) + return false; + return true; + } + + static boolean containsSpecialValue(Object value) { + if (value instanceof BindMarker) + return true; + if (value instanceof Collection) + for (Object elt : (Collection) value) + if (containsSpecialValue(elt)) + return true; + if (value instanceof Map) + for (Map.Entry entry : ((Map) value).entrySet()) + if (containsSpecialValue(entry.getKey()) || containsSpecialValue(entry.getValue())) + return true; + return false; + } } - private boolean isTimestampParameter(Object value) { - return value instanceof Date; - } + /** + * A parser that extracts the parameter bindings from a given query string. + * + * @author Mark Paluch + */ + enum ParameterBindingParser { - private boolean isDateParameter(Object value) { - return value instanceof LocalDate; - } + INSTANCE; - private boolean isStringLike(Object value) { + private static final char CURRLY_BRACE_OPEN = '{'; + private static final char CURRLY_BRACE_CLOSE = '}'; + private static final Pattern INDEX_PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); + private static final Pattern NAMED_PARAMETER_BINDING_PATTERN = Pattern.compile("\\:(\\w+)"); - if (value != null) { - for (Class type : STRING_LIKE_PARAMETER_TYPES) { + private static final Pattern INDEX_BASED_EXPRESSION_PATTERN = Pattern.compile("\\?\\#\\{"); + private static final Pattern NAME_BASED_EXPRESSION_PATTERN = Pattern.compile("\\:\\#\\{"); + private static final String ARGUMENT_PLACEHOLDER = "?_param_?"; - if (ClassUtils.isAssignableValue(type, value)) { - return true; + /** + * Returns a list of {@link ParameterBinding}s found in the given {@code input}. + * + * @param input can be {@literal null} or empty. + * @param bindings must not be {@literal null}. + * @return + */ + public String parseAndCollectParameterBindingsFromQueryIntoBindings(String input, List bindings) { + + if (!StringUtils.hasText(input)) { + return input; + } + + Assert.notNull(bindings, "Parameter bindings must not be null!"); + + return transformQueryAndCollectExpressionParametersIntoBindings(input, bindings); + } + + private static String transformQueryAndCollectExpressionParametersIntoBindings(String input, + List bindings) { + + StringBuilder result = new StringBuilder(); + + int startIndex = 0; + int currentPos = 0; + + while (currentPos < input.length()) { + + Matcher matcher = findNextBindingOrExpression(input, currentPos); + + // no expression parameter found + if (matcher == null) { + break; + } + + int exprStart = matcher.start(); + currentPos = exprStart; + + if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) { + // eat parameter expression + int curlyBraceOpenCnt = 1; + currentPos += 3; + + while (curlyBraceOpenCnt > 0 && currentPos < input.length()) { + switch (input.charAt(currentPos++)) { + case CURRLY_BRACE_OPEN: + curlyBraceOpenCnt++; + break; + case CURRLY_BRACE_CLOSE: + curlyBraceOpenCnt--; + break; + default: + } + } + + result.append(input.subSequence(startIndex, exprStart)); + } else { + result.append(input.subSequence(startIndex, exprStart)); + } + + result.append(ARGUMENT_PLACEHOLDER); + + if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) { + bindings.add(ParameterBinding.expression(input.substring(exprStart + 3, currentPos - 1), true)); + } else { + + if (matcher.pattern() == INDEX_PARAMETER_BINDING_PATTERN) { + bindings.add(ParameterBinding.indexed(Integer.parseInt(matcher.group(1)))); + } else { + bindings.add(ParameterBinding.named(matcher.group(1))); + } + + currentPos = matcher.end(); + } + + startIndex = currentPos; + } + + return result.append(input.subSequence(currentPos, input.length())).toString(); + } + + private static Matcher findNextBindingOrExpression(String input, int position) { + + List matchers = new ArrayList(); + matchers.add(INDEX_PARAMETER_BINDING_PATTERN.matcher(input)); + matchers.add(NAMED_PARAMETER_BINDING_PATTERN.matcher(input)); + matchers.add(INDEX_BASED_EXPRESSION_PATTERN.matcher(input)); + matchers.add(NAME_BASED_EXPRESSION_PATTERN.matcher(input)); + + TreeMap matcherMap = new TreeMap(); + for (Matcher matcher : matchers) { + if (matcher.find(position)) { + matcherMap.put(matcher.start(), matcher); } } + + if (matcherMap.isEmpty()) { + return null; + } + + return matcherMap.values().iterator().next(); + } + } + + /** + * A generic parameter binding with name or position information. + * + * @author Mark Paluch + */ + static class ParameterBinding { + + private final int parameterIndex; + private final boolean quoted; + private final String expression; + private final String parameterName; + + private ParameterBinding(int parameterIndex, boolean quoted, String expression, String parameterName) { + this.parameterIndex = parameterIndex; + this.quoted = quoted; + this.expression = expression; + this.parameterName = parameterName; } - return false; - } + public static ParameterBinding expression(String expression, boolean quoted) { + return new ParameterBinding(-1, quoted, expression, null); + } - private Object getParameterWithIndex(CassandraParameterAccessor accessor, int index) { - return accessor.getBindableValue(index); + public static ParameterBinding indexed(int parameterIndex) { + return new ParameterBinding(parameterIndex, false, null, null); + } + + public static ParameterBinding named(String name) { + return new ParameterBinding(-1, false, null, name); + } + + public boolean isNamed() { + return parameterName != null; + } + + public int getParameterIndex() { + return parameterIndex; + } + + public String getParameter() { + return "?" + (isExpression() ? "expr" : "") + parameterIndex; + } + + public String getExpression() { + return expression; + } + + public boolean isExpression() { + return this.expression != null; + } + + public String getParameterName() { + return parameterName; + } } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java index 14d92e071..469044dc3 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/CassandraRepositoryFactory.java @@ -36,6 +36,7 @@ import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; /** @@ -49,20 +50,22 @@ import org.springframework.util.Assert; */ public class CassandraRepositoryFactory extends RepositoryFactorySupport { - private final CassandraOperations cassandraOperations; + private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + + private final CassandraOperations operations; private final CassandraMappingContext mappingContext; /** * Creates a new {@link CassandraRepositoryFactory} with the given {@link CassandraOperations}. * - * @param cassandraOperations must not be {@literal null} + * @param operations must not be {@literal null} */ - public CassandraRepositoryFactory(CassandraOperations cassandraOperations) { + public CassandraRepositoryFactory(CassandraOperations operations) { - Assert.notNull(cassandraOperations); + Assert.notNull(operations); - this.cassandraOperations = cassandraOperations; - this.mappingContext = cassandraOperations.getConverter().getMappingContext(); + this.operations = operations; + this.mappingContext = operations.getConverter().getMappingContext(); } /* @@ -82,7 +85,7 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { protected Object getTargetRepository(RepositoryInformation information) { CassandraEntityInformation entityInformation = getEntityInformation(information.getDomainType()); - return getTargetRepositoryViaReflection(information, entityInformation, cassandraOperations); + return getTargetRepositoryViaReflection(information, entityInformation, operations); } /* @@ -101,7 +104,7 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { } return new MappingCassandraEntityInformation((CassandraPersistentEntity) entity, - cassandraOperations.getConverter()); + operations.getConverter()); } /* @@ -119,11 +122,22 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { */ @Override protected QueryLookupStrategy getQueryLookupStrategy(Key key, EvaluationContextProvider evaluationContextProvider) { - return new CassandraQueryLookupStrategy(); + return new CassandraQueryLookupStrategy(operations, evaluationContextProvider, mappingContext); } private class CassandraQueryLookupStrategy implements QueryLookupStrategy { + private final CassandraOperations operations; + private final EvaluationContextProvider evaluationContextProvider; + private final CassandraMappingContext mappingContext; + + public CassandraQueryLookupStrategy(CassandraOperations operations, + EvaluationContextProvider evaluationContextProvider, CassandraMappingContext mappingContext) { + this.operations = operations; + this.evaluationContextProvider = evaluationContextProvider; + this.mappingContext = mappingContext; + } + /* * (non-Javadoc) * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) @@ -137,11 +151,12 @@ public class CassandraRepositoryFactory extends RepositoryFactorySupport { if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); - return new StringBasedCassandraQuery(namedQuery, queryMethod, cassandraOperations); + return new StringBasedCassandraQuery(namedQuery, queryMethod, operations, EXPRESSION_PARSER, + evaluationContextProvider); } else if (queryMethod.hasAnnotatedQuery()) { - return new StringBasedCassandraQuery(queryMethod, cassandraOperations); + return new StringBasedCassandraQuery(queryMethod, operations, EXPRESSION_PARSER, evaluationContextProvider); } else { - return new PartTreeCassandraQuery(queryMethod, cassandraOperations); + return new PartTreeCassandraQuery(queryMethod, operations); } } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ParameterBindingParserUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ParameterBindingParserUnitTests.java new file mode 100644 index 000000000..19bec19dd --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ParameterBindingParserUnitTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2016 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.cassandra.repository.query; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery.ParameterBindingParser.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery.ParameterBinding; + +/** + * Unit tests for + * {@link org.springframework.data.cassandra.repository.query.StringBasedCassandraQuery.ParameterBindingParser}. + * + * @author Mark Paluch + */ +public class ParameterBindingParserUnitTests { + + /** + * @see DATACASS-117 + */ + @Test + public void parseWithoutParameters() { + + String query = "SELECT * FROM hello_world"; + List bindings = new ArrayList(); + + String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings); + + assertThat(transformed, is(equalTo(query))); + assertThat(bindings, is(empty())); + } + + /** + * @see DATACASS-117 + */ + @Test + public void parseWithStaticParameters() { + + String query = "SELECT * FROM hello_world WHERE a = 1 AND b = {'list'} AND c = {'key':'value'}"; + List bindings = new ArrayList(); + + String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings); + + assertThat(transformed, is(equalTo(query))); + assertThat(bindings, is(empty())); + } + + /** + * @see DATACASS-117 + */ + @Test + public void parseWithPositionalParameters() { + + String query = "SELECT * FROM hello_world WHERE a = ?0 and b = ?13"; + List bindings = new ArrayList(); + + String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings); + + assertThat(transformed, is(equalTo("SELECT * FROM hello_world WHERE a = ?_param_? and b = ?_param_?"))); + assertThat(bindings, hasSize(2)); + + assertThat(bindings.get(0).getParameterIndex(), is(equalTo(0))); + assertThat(bindings.get(1).getParameterIndex(), is(equalTo(13))); + } + + /** + * @see DATACASS-117 + */ + @Test + public void parseWithNamedParameters() { + + String query = "SELECT * FROM hello_world WHERE a = :hello and b = :world"; + List bindings = new ArrayList(); + + String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings); + + assertThat(transformed, is(equalTo("SELECT * FROM hello_world WHERE a = ?_param_? and b = ?_param_?"))); + assertThat(bindings, hasSize(2)); + } + + /** + * @see DATACASS-117 + */ + @Test + public void parseWithIndexExpressionParameters() { + + String query = "SELECT * FROM hello_world WHERE a = ?#{[0]} and b = ?#{[2]}"; + List bindings = new ArrayList(); + + String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings); + + assertThat(transformed, is(equalTo("SELECT * FROM hello_world WHERE a = ?_param_? and b = ?_param_?"))); + assertThat(bindings, hasSize(2)); + } + + /** + * @see DATACASS-117 + */ + @Test + public void parseWithNameExpressionParameters() { + + String query = "SELECT * FROM hello_world WHERE a = :#{#a} and b = :#{#b}"; + List bindings = new ArrayList(); + + String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings); + + assertThat(transformed, is(equalTo("SELECT * FROM hello_world WHERE a = ?_param_? and b = ?_param_?"))); + assertThat(bindings, hasSize(2)); + } + + /** + * @see DATACASS-117 + */ + @Test + public void parseWithMixedParameters() { + + String query = "SELECT * FROM hello_world WHERE (a = ?1 and b = :name) and c = (:#{#a}) and (d = ?#{[1]})"; + List bindings = new ArrayList(); + + String transformed = INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query, bindings); + + assertThat(transformed, is(equalTo( + "SELECT * FROM hello_world WHERE (a = ?_param_? and b = ?_param_?) and c = (?_param_?) and (d = ?_param_?)"))); + assertThat(bindings, hasSize(4)); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryIntegrationUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryIntegrationUnitTests.java index 388f0458c..0359d53de 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryIntegrationUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StringBasedCassandraQueryIntegrationUnitTests.java @@ -20,14 +20,17 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; +import java.nio.ByteBuffer; import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.core.CassandraOperations; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; @@ -38,8 +41,16 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.query.ExtensionAwareEvaluationContextProvider; +import org.springframework.data.repository.query.Param; +import org.springframework.data.repository.query.QueryCreationException; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.ReflectionUtils; -import com.datastax.driver.core.SimpleStatement; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.CodecRegistry; +import com.datastax.driver.core.Configuration; +import com.datastax.driver.core.Session; import com.datastax.driver.core.querybuilder.QueryBuilder; import com.datastax.driver.core.querybuilder.Select; @@ -53,7 +64,12 @@ import com.datastax.driver.core.querybuilder.Select; @RunWith(MockitoJUnitRunner.class) public class StringBasedCassandraQueryIntegrationUnitTests { + SpelExpressionParser PARSER = new SpelExpressionParser(); + @Mock CassandraOperations operations; + @Mock Session session; + @Mock Cluster cluster; + @Mock Configuration configuration; RepositoryMetadata metadata; MappingCassandraConverter converter; @@ -63,6 +79,10 @@ public class StringBasedCassandraQueryIntegrationUnitTests { public void setUp() { when(operations.getConverter()).thenReturn(converter); + when(operations.getSession()).thenReturn(session); + when(session.getCluster()).thenReturn(cluster); + when(cluster.getConfiguration()).thenReturn(configuration); + when(configuration.getCodecRegistry()).thenReturn(CodecRegistry.DEFAULT_INSTANCE); this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class); this.converter = new MappingCassandraConverter(new BasicCassandraMappingContext()); @@ -71,76 +91,283 @@ public class StringBasedCassandraQueryIntegrationUnitTests { this.converter.afterPropertiesSet(); } + /** + * @see DATACASS-117 + */ @Test - public void bindsSimplePropertyCorrectly() throws Exception { + public void bindsIndexParameterCorrectly() { - Method method = SampleRepository.class.getMethod("findByLastname", String.class); - CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, factory, - converter.getMappingContext()); - StringBasedCassandraQuery cassandraQuery = new StringBasedCassandraQuery(queryMethod, operations); - CassandraParametersParameterAccessor accesor = new CassandraParametersParameterAccessor(queryMethod, "Matthews"); + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastname", String.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), "Matthews"); - String stringQuery = cassandraQuery.createQuery(accesor); - SimpleStatement actual = new SimpleStatement(stringQuery); + String actual = cassandraQuery.createQuery(accessor); - String table = Person.class.getSimpleName().toLowerCase(); - Select expected = QueryBuilder.select().all().from(table); - expected.setForceNoValues(true); - expected.where(QueryBuilder.eq("lastname", "Matthews")); - - assertThat(actual.getQueryString(), is(expected.getQueryString())); + assertThat(actual, is(equalTo("SELECT * FROM person WHERE lastname = 'Matthews';"))); } + /** + * @see DATACASS-117 + */ @Test - public void bindsMultipleParametersCorrectly() throws Exception { + public void bindsAndEscapesIndexParameterCorrectly() { - Method method = SampleRepository.class.getMethod("findByLastnameAndFirstname", String.class, String.class); - CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, factory, - converter.getMappingContext()); - StringBasedCassandraQuery cassandraQuery = new StringBasedCassandraQuery(queryMethod, operations); - CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor(queryMethod, "Matthews", - "John"); + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastname", String.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), "Mat\th'ew\"s"); - String stringQuery = cassandraQuery.createQuery(accessor); - SimpleStatement actual = new SimpleStatement(stringQuery); + String actual = cassandraQuery.createQuery(accessor); + + assertThat(actual, is(equalTo("SELECT * FROM person WHERE lastname = 'Mat\th''ew\"s';"))); + } + + /** + * @see DATACASS-117 + */ + @Test + public void bindsAndEscapesBytesIndexParameterCorrectly() { + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastname", String.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), ByteBuffer.wrap(new byte[] { 1, 2, 3, 4 })); + + String actual = cassandraQuery.createQuery(accessor); + + assertThat(actual, is(equalTo("SELECT * FROM person WHERE lastname = 0x01020304;"))); + } + + /** + * @see DATACASS-117 + */ + @Test + public void bindsIndexParameterInListCorrectly() { + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastNameIn", Collection.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), Arrays.asList("White", "Heisenberg")); + + String actual = cassandraQuery.createQuery(accessor); + + assertThat(actual, is(equalTo("SELECT * FROM person WHERE lastname IN ('White','Heisenberg');"))); + } + + /** + * @see DATACASS-117 + */ + @Test + public void bindsIndexParameterIsListCorrectly() { + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastNamesAndAge", Collection.class, int.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), Arrays.asList("White", "Heisenberg"), 42); + + String actual = cassandraQuery.createQuery(accessor); + + assertThat(actual, is(equalTo("SELECT * FROM person WHERE lastnames = ['White','Heisenberg'] AND age = 42;"))); + } + + /** + * @see DATACASS-117 + */ + @Test(expected = QueryCreationException.class) + public void referencingUnknownIndexedParameterShouldFail() { + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByOutOfBoundsLastNameShouldFail", String.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), "Hello"); + + cassandraQuery.createQuery(accessor); + } + + /** + * @see DATACASS-117 + */ + @Test(expected = QueryCreationException.class) + public void referencingUnknownNamedParameterShouldFail() { + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByUnknownParameterLastNameShouldFail", String.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), "Hello"); + + cassandraQuery.createQuery(accessor); + } + + /** + * @see DATACASS-117 + */ + @Test + public void bindsIndexParameterInSetCorrectly() { + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastNameIn", Collection.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), new HashSet(Arrays.asList("White", "Heisenberg"))); + + String actual = cassandraQuery.createQuery(accessor); + + assertThat(actual, is(equalTo("SELECT * FROM person WHERE lastname IN ('White','Heisenberg');"))); + } + + /** + * @see DATACASS-117 + */ + @Test + public void bindsNamedParameterCorrectly() { + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByNamedParameter", String.class, String.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), "Walter", "Matthews"); + + String actual = cassandraQuery.createQuery(accessor); + + assertThat(actual, is(equalTo("SELECT * FROM person WHERE lastname = 'Matthews';"))); + } + + /** + * @see DATACASS-117 + */ + @Test + public void bindsIndexExpressionParameterCorrectly() { + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByIndexExpressionParameter", String.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), "Matthews"); + + String actual = cassandraQuery.createQuery(accessor); + + assertThat(actual, is(equalTo("SELECT * FROM person WHERE lastname = 'Matthews';"))); + } + + /** + * @see DATACASS-117 + */ + @Test + public void bindsExpressionParameterCorrectly() { + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByExpressionParameter", String.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), "Matthews"); + + String actual = cassandraQuery.createQuery(accessor); + + assertThat(actual, is(equalTo("SELECT * FROM person WHERE lastname = 'Matthews';"))); + } + + /** + * @see DATACASS-117 + */ + @Test + public void bindsConditionalExpressionParameterCorrectly() { + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByConditionalExpressionParameter", String.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), "Matthews"); + + String actual = cassandraQuery.createQuery(accessor); + + assertThat(actual, is(equalTo("SELECT * FROM person WHERE lastname = 'Woohoo';"))); + + accessor = new CassandraParametersParameterAccessor(cassandraQuery.getQueryMethod(), "Walter"); + + actual = cassandraQuery.createQuery(accessor); + + assertThat(actual, is(equalTo("SELECT * FROM person WHERE lastname = 'Walter';"))); + } + + /** + * @see DATACASS-117 + */ + @Test + public void bindsReusedParametersCorrectly() { + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastnameUsedTwice", String.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), "Matthews"); + + String actual = cassandraQuery.createQuery(accessor); + + assertThat(actual, is(equalTo("SELECT * FROM person WHERE lastname='Matthews' or firstname = 'Matthews';"))); + } + + /** + * @see DATACASS-117 + */ + @Test + public void bindsMultipleParametersCorrectly() { + + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByLastnameAndFirstname", String.class, String.class); + CassandraParametersParameterAccessor accessor = new CassandraParametersParameterAccessor( + cassandraQuery.getQueryMethod(), "Matthews", "John"); + + String actual = cassandraQuery.createQuery(accessor); String table = Person.class.getSimpleName().toLowerCase(); Select expected = QueryBuilder.select().all().from(table); expected.setForceNoValues(true); expected.where(QueryBuilder.eq("lastname", "Matthews")).and(QueryBuilder.eq("firstname", "John")); - assertThat(actual.getQueryString(), is(expected.getQueryString())); + assertThat(actual, is(expected.toString())); } /** * @see DATACASS-296 */ @Test - public void bindsConvertedPropertyCorrectly() throws Exception { + public void bindsConvertedParameterCorrectly() { - Method method = SampleRepository.class.getMethod("findByCreatedDate", LocalDate.class); - CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, factory, - converter.getMappingContext()); - StringBasedCassandraQuery cassandraQuery = new StringBasedCassandraQuery(queryMethod, operations); - CassandraParameterAccessor accessor = new ConvertingParameterAccessor(converter, new CassandraParametersParameterAccessor(queryMethod, - LocalDate.of(2010, 7, 4))); + StringBasedCassandraQuery cassandraQuery = getQueryMethod("findByCreatedDate", LocalDate.class); + CassandraParameterAccessor accessor = new ConvertingParameterAccessor(converter, + new CassandraParametersParameterAccessor(cassandraQuery.getQueryMethod(), LocalDate.of(2010, 7, 4))); - String stringQuery = cassandraQuery.createQuery(accessor); - SimpleStatement actual = new SimpleStatement(stringQuery); + String actual = cassandraQuery.createQuery(accessor); String table = Person.class.getSimpleName().toLowerCase(); Select expected = QueryBuilder.select().all().from(table); expected.setForceNoValues(true); expected.where(QueryBuilder.eq("createdDate", com.datastax.driver.core.LocalDate.fromYearMonthDay(2010, 7, 4))); - assertThat(actual.getQueryString(), is(expected.getQueryString())); + assertThat(actual, is(equalTo("SELECT * FROM person WHERE createdDate='2010-07-04';"))); + } + + private StringBasedCassandraQuery getQueryMethod(String name, Class... args) { + Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args); + CassandraQueryMethod queryMethod = new CassandraQueryMethod(method, metadata, factory, + converter.getMappingContext()); + return new StringBasedCassandraQuery(queryMethod, operations, PARSER, + new ExtensionAwareEvaluationContextProvider()); } private interface SampleRepository extends Repository { - @Query("SELECT * FROM person WHERE lastname=?0;") + @Query("SELECT * FROM person WHERE lastname = ?0;") Person findByLastname(String lastname); + @Query("SELECT * FROM person WHERE lastname=?0 or firstname = ?0;") + Person findByLastnameUsedTwice(String lastname); + + @Query("SELECT * FROM person WHERE lastname = :lastname;") + Person findByNamedParameter(@Param("another") String another, @Param("lastname") String lastname); + + @Query("SELECT * FROM person WHERE lastname = :#{[0]};") + Person findByIndexExpressionParameter(String lastname); + + @Query("SELECT * FROM person WHERE lastnames = [?0] AND age = ?1;") + Person findByLastNamesAndAge(Collection lastname, int age); + + @Query("SELECT * FROM person WHERE lastname = ?0 AND age = ?2;") + Person findByOutOfBoundsLastNameShouldFail(String lastname); + + @Query("SELECT * FROM person WHERE lastname = :unknown;") + Person findByUnknownParameterLastNameShouldFail(String lastname); + + @Query("SELECT * FROM person WHERE lastname IN (?0);") + Person findByLastNameIn(Collection lastNames); + + @Query("SELECT * FROM person WHERE lastname = :#{#lastname};") + Person findByExpressionParameter(@Param("lastname") String lastname); + + @Query("SELECT * FROM person WHERE lastname = :#{#lastname == 'Matthews' ? 'Woohoo' : #lastname};") + Person findByConditionalExpressionParameter(@Param("lastname") String lastname); + @Query("SELECT * FROM person WHERE lastname=?0 AND firstname=?1;") Person findByLastnameAndFirstname(String lastname, String firstname); diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StubParameterAccessor.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StubParameterAccessor.java index 75abb515d..408185079 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StubParameterAccessor.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/StubParameterAccessor.java @@ -21,9 +21,7 @@ import java.util.Iterator; import org.springframework.data.cassandra.convert.CassandraConverter; import org.springframework.data.cassandra.mapping.CassandraType; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Range; import org.springframework.data.domain.Sort; -import org.springframework.data.geo.Distance; import org.springframework.data.repository.query.ParameterAccessor; import com.datastax.driver.core.CodecRegistry; @@ -85,6 +83,11 @@ class StubParameterAccessor implements CassandraParameterAccessor { return values[index]; } + @Override + public Object[] getValues() { + return new Object[0]; + } + @Override public boolean hasBindableNullValue() { return false; diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/anno/PersonRepositoryWithQueryAnnotations.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/anno/PersonRepositoryWithQueryAnnotations.java index 5325f02cd..8e86cc902 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/anno/PersonRepositoryWithQueryAnnotations.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/declared/anno/PersonRepositoryWithQueryAnnotations.java @@ -29,6 +29,7 @@ import org.springframework.data.cassandra.test.integration.repository.querymetho import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.named.PersonRepositoryWithNamedQueries; import com.datastax.driver.core.ResultSet; +import org.springframework.data.repository.query.Param; /** * we extend {@link PersonRepositoryWithNamedQueries} here just to keep the test codebase in sync. @@ -50,16 +51,16 @@ public interface PersonRepositoryWithQueryAnnotations extends PersonRepository { Person[] findFolksWithLastnameAsArray(String lastname); @Override - @Query("select * from person where lastname = ?0 and firstname = ?1") + @Query("select * from person where lastname = ?#{[0]} and firstname = ?1") Person findSingle(String last, String first); @Override - @Query("select * from person where lastname = ?0") - List> findFolksWithLastnameAsListOfMapOfStringToObject(String last); + @Query("select * from person where lastname = :last") + List> findFolksWithLastnameAsListOfMapOfStringToObject(@Param("last") String last); @Override - @Query("select nickname from person where lastname = ?0 and firstname = ?1") - String findSingleNickname(String last, String first); + @Query("select nickname from person where lastname = :#{#last} and firstname = ?1") + String findSingleNickname(@Param("last") String last, @Param("first") String first); @Override @Query("select birthdate from person where lastname = ?0 and firstname = ?1")