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")