diff --git a/src/main/java/org/springframework/data/couchbase/core/query/Query.java b/src/main/java/org/springframework/data/couchbase/core/query/Query.java index b2a0b960a..da705f070 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/Query.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Query.java @@ -26,6 +26,7 @@ import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser; import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; @@ -189,7 +190,7 @@ public void appendSort(final StringBuilder sb) { sb.deleteCharAt(sb.length() - 1); } - public void appendWhere(final StringBuilder sb, int[] paramIndexPtr) { + public void appendWhere(final StringBuilder sb, int[] paramIndexPtr, CouchbaseConverter converter) { if (!criteria.isEmpty()) { appendWhereOrAnd(sb); boolean first = true; @@ -199,16 +200,11 @@ public void appendWhere(final StringBuilder sb, int[] paramIndexPtr) { } else { sb.append(" AND "); } - sb.append(c.export(paramIndexPtr)); + sb.append(c.export(paramIndexPtr, parameters, converter)); } } } - public void appendCriteria(StringBuilder sb, QueryCriteria criteria) { - appendWhereOrAnd(sb); - sb.append(criteria.export()); - } - public void appendWhereString(StringBuilder sb, String whereString) { appendWhereOrAnd(sb); sb.append(whereString); @@ -257,9 +253,9 @@ private static boolean notQuoted(int start, int end, String querySoFar) { return true; // is not quoted } - public String export() { + public String export(int[]... paramIndexPtrHolder) { // used only by tests StringBuilder sb = new StringBuilder(); - appendWhere(sb, null); + appendWhere(sb, paramIndexPtrHolder.length > 0 ? paramIndexPtrHolder[0] : null, null); appendSort(sb); appendSkipAndLimit(sb); return sb.toString(); @@ -270,7 +266,7 @@ public String toN1qlSelectString(ReactiveCouchbaseTemplate template, Class domai final StringBuilder statement = new StringBuilder(); appendString(statement, n1ql.selectEntity); // select ... appendWhereString(statement, n1ql.filter); // typeKey = typeValue - appendWhere(statement, new int[] { 0 }); // criteria on this Query + appendWhere(statement, new int[] { 0 }, template.getConverter()); // criteria on this Query appendSort(statement); appendSkipAndLimit(statement); return statement.toString(); diff --git a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java index bb6292ff1..022a6fa32 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java @@ -20,6 +20,11 @@ import java.util.LinkedList; import java.util.List; +import com.couchbase.client.core.error.InvalidArgumentException; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.json.JsonValue; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.lang.Nullable; /** @@ -60,6 +65,10 @@ public class QueryCriteria implements QueryCriteriaDefinition { this.format = format; } + Object[] getValue() { + return value; + } + /** * Static factory method to create a Criteria using the provided key. */ @@ -68,8 +77,8 @@ public static QueryCriteria where(String key) { } private static QueryCriteria wrap(QueryCriteria criteria) { - QueryCriteria qc = new QueryCriteria(new LinkedList(), criteria.key, criteria.value, null, - criteria.operator, criteria.format); + QueryCriteria qc = new QueryCriteria(new LinkedList<>(), criteria.key, criteria.value, null, criteria.operator, + criteria.format); return qc; } @@ -167,7 +176,7 @@ public QueryCriteria containing(@Nullable Object o) { public QueryCriteria notContaining(@Nullable Object o) { value = new QueryCriteria[] { wrap(containing(o)) }; operator = "NOT"; - format = format = "not( %3$s )"; + format = "not( %3$s )"; return this; } @@ -196,7 +205,7 @@ public QueryCriteria isNotNull() { operator = "IS_NOT_NULL"; value = null; format = "%1$s is not null"; - return (QueryCriteria) this; + return this; } public QueryCriteria isMissing() { @@ -210,7 +219,7 @@ public QueryCriteria isNotMissing() { operator = "IS_NOT_MiSSING"; value = null; format = "%1$s is not missing"; - return (QueryCriteria) this; + return this; } public QueryCriteria isValued() { @@ -224,68 +233,74 @@ public QueryCriteria isNotValued() { operator = "IS_NOT_VALUED"; value = null; format = "%1$s is not valued"; - return (QueryCriteria) this; + return this; } public QueryCriteria within(@Nullable Object o) { operator = "WITHIN"; value = new Object[] { o }; - format = "%1$s within $3$s"; - return (QueryCriteria) this; + format = "%1$s within %3$s"; + return this; } public QueryCriteria between(@Nullable Object o1, @Nullable Object o2) { operator = "BETWEEN"; value = new Object[] { o1, o2 }; format = "%1$s between %3$s and %4$s"; - return (QueryCriteria) this; + return this; } public QueryCriteria in(@Nullable Object... o) { operator = "IN"; - value = o; - StringBuilder sb = new StringBuilder("%1$s in ( [ "); - for (int i = 1; i <= value.length; i++) { // format indices start at 1 - if (i > 1) - sb.append(", "); - sb.append("%" + (i + 2) + "$s"); // the first is fieldName, second is operator, args start at 3 + format = "%1$s in ( %3$s )"; + // IN takes a single argument that is a list + if (o.length > 0) { + if (o[0] instanceof JsonArray || o[0] instanceof List || o[0] instanceof Object[]) { + if (o.length != 1) { + throw new RuntimeException("IN cannot take multiple lists"); + } + value = o; + } else { + value = new Object[1]; + value[0] = o; // JsonArray.from(o); + } } - format = sb.append(" ] )").toString(); - return (QueryCriteria) this; + return this; } public QueryCriteria notIn(@Nullable Object... o) { value = new QueryCriteria[] { wrap(in(o)) }; operator = "NOT"; - format = format = "not( %3$s )"; // field = 1$, operator = 2$, value=$3, $4, ... - return (QueryCriteria) this; + format = "not( %3$s )"; // field = 1$, operator = 2$, value=$3, $4, ... + return this; } public QueryCriteria TRUE() { // true/false are reserved, use TRUE/FALSE value = null; operator = null; - format = format = "%1$s"; // field = 1$, operator = 2$, value=$3, $4, ... - return (QueryCriteria) this; + format = "%1$s"; // field = 1$, operator = 2$, value=$3, $4, ... + return this; } public QueryCriteria FALSE() { value = new QueryCriteria[] { wrap(TRUE()) }; operator = "not"; - format = format = "not( %3$s )"; - return (QueryCriteria) this; + format = "not( %3$s )"; + return this; } /** - * This exports the query criteria into a string to be appended to the beginning of an N1QL statement + * This exports the query criteria chain into a string to be appended to the beginning of an N1QL statement * - * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters - * There may already be positional parameters in the beginning of the statement, - * so it may not always start at 1. If it has the value -1, the query is using - * named parameters. If the pointer is null, the query is not using parameters. + * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may + * already be positional parameters in the beginning of the statement, so it may not always start at 1. If it + * has the value -1, the query is using named parameters. If the pointer is null, the query is not using + * parameters. + * @param parameters - parameters of the query. If operands are parameterized, their values are added to parameters * @return string containing part of N1QL query */ @Override - public String export(int[] paramIndexPtr) { + public String export(int[] paramIndexPtr, JsonValue parameters, CouchbaseConverter converter) { StringBuilder output = new StringBuilder(); boolean first = true; for (QueryCriteria c : this.criteriaChain) { @@ -298,7 +313,7 @@ public String export(int[] paramIndexPtr) { } else { first = false; } - c.exportSingle(output, paramIndexPtr); + c.exportSingle(output, paramIndexPtr, parameters, converter); } return output.toString(); @@ -310,12 +325,24 @@ public String export(int[] paramIndexPtr) { * @return string containing part of N1QL query */ @Override - public String export() { - return export(null); + public String export() { // used only by tests + return export(null, null, null); } - private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr) { + /** + * Appends the query criteria to a StringBuilder which will be appended to a N1QL statement + * + * @param sb - the string builder + * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may + * already be positional parameters in the beginning of the statement, so it may not always start at 1. If it + * has the value -1, the query is using named parameters. If the pointer is null, the query is not using + * parameters. + * @param parameters - parameters of the query. If operands are parameterized, their values are added to parameters + * @return string containing part of N1QL query + */ + private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr, JsonValue parameters, + CouchbaseConverter converter) { String fieldName = maybeQuote(key); int valueLen = value == null ? 0 : value.length; Object[] v = new Object[valueLen + 2]; @@ -323,9 +350,9 @@ private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr) { v[1] = operator; for (int i = 0; i < valueLen; i++) { if (value[i] instanceof QueryCriteria) { - v[i + 2] = "(" + ((QueryCriteria) value[i]).export(paramIndexPtr) + ")"; + v[i + 2] = "(" + ((QueryCriteria) value[i]).export(paramIndexPtr, parameters, converter) + ")"; } else { - v[i + 2] = maybeWrapValue(key, value[i], paramIndexPtr); + v[i + 2] = maybeWrapValue(key, value[i], paramIndexPtr, parameters, converter); } } @@ -340,24 +367,84 @@ private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr) { return sb; } - private String maybeWrapValue(String key, Object value, int[] paramIndexPtr) { + /** + * Possibly convert an operand to a positional or named parameter + * + * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may + * already be positional parameters in the beginning of the statement, so it may not always start at 1. If it + * has the value -1, the query is using named parameters. If the pointer is null, the query is not using + * parameters. + * @param parameters - parameters of the query. If operands are parameterized, their values are added to parameters + * @return string containing part of N1QL query + */ + private String maybeWrapValue(String key, Object value, int[] paramIndexPtr, JsonValue parameters, + CouchbaseConverter converter) { if (paramIndexPtr != null) { if (paramIndexPtr[0] >= 0) { + JsonArray params = (JsonArray) parameters; + // from StringBasedN1qlQueryParser.getPositionalPlaceholderValues() + try { + params.add(convert(converter, value)); + } catch (InvalidArgumentException iae) { + if (value instanceof Object[]) { + addAsArray(params, value, converter); + } else { + throw iae; + } + } return "$" + (++paramIndexPtr[0]); // these are generated in order } else { + JsonObject params = (JsonObject) parameters; + // from StringBasedN1qlQueryParser.getNamedPlaceholderValues() + try { + params.put(key, convert(converter, value)); + } catch (InvalidArgumentException iae) { + if (value instanceof Object[]) { + params.put(key, JsonArray.from((Object[]) value)); + } else { + throw iae; + } + } return "$" + key; } } + // Did not convert to a parameter. Add quotes or whatever it might need. + if (value instanceof String) { return "\"" + value + "\""; } else if (value == null) { return "null"; + } else if (value instanceof Object[]) { // convert array into sequence of comma-separated values + StringBuffer l = new StringBuffer(); + l.append("["); + Object[] array = (Object[]) value; + for (int i = 0; i < array.length; i++) { + if (i > 0) { + l.append(","); + } + l.append(maybeWrapValue(null, array[i], null, null, converter)); + } + l.append("]"); + return l.toString(); } else { return value.toString(); } } + private static Object convert(CouchbaseConverter converter, Object value) { + return converter != null ? converter.convertForWriteIfNeeded(value) : value; + } + + private void addAsArray(JsonArray posValues, Object o, CouchbaseConverter converter) { + Object[] array = (Object[]) o; + JsonArray ja = JsonValue.ja(); + for (Object e : array) { + ja.add(String.valueOf(convert(converter, e))); + } + posValues.add(ja); + } + private String maybeQuote(String value) { if (value == null || (value.startsWith("\"") && value.endsWith("\""))) { return value; diff --git a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java index 6c3e116be..c9f68341c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java @@ -15,6 +15,9 @@ */ package org.springframework.data.couchbase.core.query; +import com.couchbase.client.java.json.JsonValue; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; + /** * @author Oliver Gierke * @author Christoph Strobl @@ -25,13 +28,15 @@ public interface QueryCriteriaDefinition { /** * This exports the query criteria into a string to be appended to the beginning of an N1QL statement * - * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters - * There may already be positional parameters in the beginning of the statement, - * so it may not always start at 1. If it has the value -1, the query is using - * named parameters. If the pointer is null, the query is not using parameters. + * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may + * already be positional parameters in the beginning of the statement, so it may not always start at 1. If it + * has the value -1, the query is using named parameters. If the pointer is null, the query is not using + * parameters. + * @param parameters - query parameters. Criteria values that are converted to arguments are added to parameters + * @param converter - converter to use for converting criteria values * @return string containing part of N1QL query */ - String export(int[] paramIndexPtr); + String export(int[] paramIndexPtr, JsonValue parameters, CouchbaseConverter converter); /** * Export the query criteria to a string without using positional or named parameters. diff --git a/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java b/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java index f940b54d2..fbcd9552f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java @@ -66,7 +66,8 @@ public String toN1qlSelectString(ReactiveCouchbaseTemplate template, Class domai } else { // named parameters or no parameters, no index required paramIndexPtr = new int[] { -1 }; } - appendWhere(statement, paramIndexPtr); // criteria on this Query - should be empty for StringQuery + appendWhere(statement, paramIndexPtr, template.getConverter()); // criteria on this Query - should be empty for + // StringQuery appendSort(statement); appendSkipAndLimit(statement); return statement.toString(); diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java index 54b7d6bce..44ba7774d 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java @@ -17,12 +17,8 @@ import static org.springframework.data.couchbase.core.query.QueryCriteria.*; -import java.lang.reflect.Array; import java.util.Iterator; -import com.couchbase.client.core.error.InvalidArgumentException; -import com.couchbase.client.java.json.JsonArray; -import com.couchbase.client.java.json.JsonValue; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.Query; @@ -30,7 +26,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.parser.AbstractQueryCreator; @@ -59,8 +54,7 @@ public N1qlQueryCreator(final PartTree tree, final ParameterAccessor accessor, Q @Override protected QueryCriteria create(final Part part, final Iterator iterator) { - PersistentPropertyPath path = context.getPersistentPropertyPath( - part.getProperty()); + PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); CouchbasePersistentProperty property = path.getLeafProperty(); return from(part, property, where(path.toDotPath()), iterator); } @@ -71,8 +65,7 @@ protected QueryCriteria and(final Part part, final QueryCriteria base, final Ite return create(part, iterator); } - PersistentPropertyPath path = context.getPersistentPropertyPath( - part.getProperty()); + PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); CouchbasePersistentProperty property = path.getLeafProperty(); return from(part, property, base.and(path.toDotPath()), iterator); @@ -85,92 +78,70 @@ protected QueryCriteria or(QueryCriteria base, QueryCriteria criteria) { @Override protected Query complete(QueryCriteria criteria, Sort sort) { - JsonArray params = (JsonArray) getPositionalPlaceholderValues(accessor); - return (criteria == null ? new Query() : new Query().addCriteria(criteria)).with(sort).setPositionalParameters( - params); + return (criteria == null ? new Query() : new Query().addCriteria(criteria)).with(sort); } - private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, - final QueryCriteria criteria, final Iterator parameters) { + private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, final QueryCriteria criteria, + final Iterator parameters) { final Part.Type type = part.getType(); /* NEAR(new String[]{"IsNear", "Near"}), */ switch (type) { - case GREATER_THAN: - case AFTER: - return criteria.gt(parameters.next()); - case GREATER_THAN_EQUAL: - return criteria.gte(parameters.next()); - case LESS_THAN: - case BEFORE: - return criteria.lt(parameters.next()); - case LESS_THAN_EQUAL: - return criteria.lte(parameters.next()); - case SIMPLE_PROPERTY: - return criteria.eq(parameters.next()); - case NEGATING_SIMPLE_PROPERTY: - return criteria.ne(parameters.next()); - case CONTAINING: - return criteria.containing(parameters.next()); - case NOT_CONTAINING: - return criteria.notContaining(parameters.next()); - case STARTING_WITH: - return criteria.startingWith(parameters.next()); - case ENDING_WITH: - return criteria.endingWith(parameters.next()); - case LIKE: - return criteria.like(parameters.next()); - case NOT_LIKE: - return criteria.notLike(parameters.next()); - case WITHIN: - return criteria.within(parameters.next()); - case IS_NULL: - return criteria.isNull(/*parameters.next()*/); - case IS_NOT_NULL: - return criteria.isNotNull(/*parameters.next()*/); - case IS_EMPTY: - return criteria.isNotValued(/*parameters.next()*/); - case IS_NOT_EMPTY: - return criteria.isValued(/*parameters.next()*/); - case EXISTS: - return criteria.isNotMissing(/*parameters.next()*/); - case REGEX: - return criteria.regex(parameters.next()); - case BETWEEN: - return criteria.between(parameters.next(), parameters.next()); - case IN: - return criteria.in((Object[]) parameters.next()); - case NOT_IN: - return criteria.notIn((Object[]) parameters.next()); - case TRUE: - return criteria.TRUE(); - case FALSE: - return criteria.FALSE(); - default: - throw new IllegalArgumentException("Unsupported keyword!"); + case GREATER_THAN: + case AFTER: + return criteria.gt(parameters.next()); + case GREATER_THAN_EQUAL: + return criteria.gte(parameters.next()); + case LESS_THAN: + case BEFORE: + return criteria.lt(parameters.next()); + case LESS_THAN_EQUAL: + return criteria.lte(parameters.next()); + case SIMPLE_PROPERTY: + return criteria.eq(parameters.next()); + case NEGATING_SIMPLE_PROPERTY: + return criteria.ne(parameters.next()); + case CONTAINING: + return criteria.containing(parameters.next()); + case NOT_CONTAINING: + return criteria.notContaining(parameters.next()); + case STARTING_WITH: + return criteria.startingWith(parameters.next()); + case ENDING_WITH: + return criteria.endingWith(parameters.next()); + case LIKE: + return criteria.like(parameters.next()); + case NOT_LIKE: + return criteria.notLike(parameters.next()); + case WITHIN: + return criteria.within(parameters.next()); + case IS_NULL: + return criteria.isNull(/*parameters.next()*/); + case IS_NOT_NULL: + return criteria.isNotNull(/*parameters.next()*/); + case IS_EMPTY: + return criteria.isNotValued(/*parameters.next()*/); + case IS_NOT_EMPTY: + return criteria.isValued(/*parameters.next()*/); + case EXISTS: + return criteria.isNotMissing(/*parameters.next()*/); + case REGEX: + return criteria.regex(parameters.next()); + case BETWEEN: + return criteria.between(parameters.next(), parameters.next()); + case IN: + return criteria.in(parameters.next()); + case NOT_IN: + return criteria.notIn(parameters.next()); + case TRUE: + return criteria.TRUE(); + case FALSE: + return criteria.FALSE(); + default: + throw new IllegalArgumentException("Unsupported keyword!"); } } - // from StringN1qlQueryParser - private JsonValue getPositionalPlaceholderValues(ParameterAccessor accessor) { - JsonArray posValues = JsonArray.create(); - if (queryMethod == null) - return posValues; - for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) { - try { - posValues.add(converter.convertForWriteIfNeeded(accessor.getBindableValue(parameter.getIndex()))); - } catch (InvalidArgumentException iae) { - Object o = accessor.getBindableValue(parameter.getIndex()); - if (o instanceof Object[]) { - Object[] array = (Object[]) o; - for (Object e : array) { - posValues.add(converter.convertForWriteIfNeeded(e)); - } - } - } - } - return posValues; - } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java index b1e9e4956..3f3313038 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java @@ -189,14 +189,14 @@ private void checkPlaceholders(String statement) { if (checkNotQuoted(placeholder, namedMatcher.start(), namedMatcher.end(), quotes)) { LOGGER.trace("{}: Found named placeholder {}", this.queryMethod.getName(), placeholder); namedCount++; - parameterNames.add(placeholder.substring(1));//save without the leading $ + parameterNames.add(placeholder.substring(1));// save without the leading $ } } if (posCount > 0 && namedCount > 0) { // actual values from parameterNames might be more useful throw new IllegalArgumentException("Using both named (" + namedCount + ") and positional (" + posCount - + ") placeholders is not supported, please choose one over the other in " - + queryMethod.getClass().getName() + "." + this.queryMethod.getName() + "()"); + + ") placeholders is not supported, please choose one over the other in " + queryMethod.getClass().getName() + + "." + this.queryMethod.getName() + "()"); } if (posCount > 0) { @@ -211,8 +211,7 @@ private void checkPlaceholders(String statement) { private boolean checkNotQuoted(String item, int start, int end, List quotes) { for (int[] quote : quotes) { if (quote[0] <= start && quote[1] >= end) { - LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", this.queryMethod.getName(), - item); + LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", this.queryMethod.getName(), item); return false; } } @@ -220,7 +219,7 @@ private boolean checkNotQuoted(String item, int start, int end, List quot } /** - * Get Postional argument placeholders to use for parameters. $1, $2 etc. + * Get Postional argument placeholders to use for parameters. $1, $2 etc. * * @param accessor * @return - JsonValue holding parameters. @@ -230,7 +229,7 @@ private JsonValue getPositionalPlaceholderValues(ParameterAccessor accessor) { for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) { Object rawValue = accessor.getBindableValue(parameter.getIndex()); Object value = couchbaseConverter.convertForWriteIfNeeded(rawValue); - putPositionalValue(accessor, parameter, posValues, value); + putPositionalValue(posValues, value); } return posValues; } @@ -251,22 +250,20 @@ private JsonObject getNamedPlaceholderValues(ParameterAccessor accessor) { if (placeholder != null && placeholder.charAt(0) == ':') { placeholder = placeholder.replaceFirst(":", ""); - putNamedValue(accessor, parameter, namedValues, placeholder, value); + putNamedValue(namedValues, placeholder, value); if (pNames.contains(placeholder)) { pNames.remove(placeholder); } else { - throw new RuntimeException( - "parameter named " + placeholder + " does not match any named parameter " + parameterNames - + " in " + statement); + throw new RuntimeException("parameter named " + placeholder + " does not match any named parameter " + + parameterNames + " in " + statement); } } else { if (parameter.getName().isPresent()) { - putNamedValue(accessor, parameter, namedValues, parameter.getName().get(), value); + putNamedValue(namedValues, parameter.getName().get(), value); } else { - throw new RuntimeException( - "cannot determine argument for named parameter. " + "Argument " + parameter.getIndex() - + " to " + queryMethod.getClass().getName() + "." + queryMethod.getName() - + "() needs @Param(\"name\") that matches a named parameter in " + statement); + throw new RuntimeException("cannot determine argument for named parameter. " + "Argument " + + parameter.getIndex() + " to " + queryMethod.getClass().getName() + "." + queryMethod.getName() + + "() needs @Param(\"name\") that matches a named parameter in " + statement); } } } @@ -278,18 +275,17 @@ private JsonObject getNamedPlaceholderValues(ParameterAccessor accessor) { protected JsonValue getPlaceholderValues(ParameterAccessor accessor) { switch (this.placeHolderType) { - case NAMED: - return getNamedPlaceholderValues(accessor); - case POSITIONAL: - return getPositionalPlaceholderValues(accessor); - case NONE: - default: - return JsonArray.create(); + case NAMED: + return getNamedPlaceholderValues(accessor); + case POSITIONAL: + return getPositionalPlaceholderValues(accessor); + case NONE: + default: + return JsonArray.create(); } } - private void putPositionalValue(ParameterAccessor accessor, Parameter parameter, JsonArray posValues, - Object value) { + private void putPositionalValue(JsonArray posValues, Object value) { try { posValues.add(value); } catch (InvalidArgumentException iae) { @@ -310,8 +306,7 @@ private void addAsArray(JsonArray posValues, Object o) { posValues.add(ja); } - private void putNamedValue(ParameterAccessor accessor, Parameter parameter, JsonObject namedValues, - String placeholder, Object value) { + private void putNamedValue(JsonObject namedValues, String placeholder, Object value) { try { namedValues.put(placeholder, value); } catch (InvalidArgumentException iae) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java index 9183c1162..df3c4cdaf 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java @@ -145,7 +145,7 @@ private QueryCriteria from(final Part part, final CouchbasePersistentProperty pr final Part.Type type = part.getType(); switch (type) { case SIMPLE_PROPERTY: - return criteria; //.eq(parameters.next()); // this will be the dummy from PartTree + return criteria; // this will be the dummy from PartTree default: throw new IllegalArgumentException("Unsupported keyword!"); } diff --git a/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java b/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java index 47fac703a..0dcbd991c 100644 --- a/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java @@ -19,7 +19,18 @@ import static org.junit.jupiter.api.Assertions.*; import static org.springframework.data.couchbase.core.query.QueryCriteria.*; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; import org.junit.jupiter.api.Test; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserRepository; +import org.springframework.data.couchbase.repository.query.N1qlQueryCreator; +import org.springframework.data.repository.query.parser.PartTree; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; class QueryCriteriaTests { @@ -56,21 +67,25 @@ void testChainedCriteria() { @Test void testNestedAndCriteria() { QueryCriteria c = where("name").is("Bubba").and(where("age").gt(12).or("country").is("Austria")); - assertEquals("`name` = \"Bubba\" and (`age` > 12 or `country` = \"Austria\")", c.export()); + JsonArray parameters = JsonArray.create(); + assertEquals("`name` = $1 and (`age` > $2 or `country` = $3)", c.export(new int[1], parameters, null)); + assertEquals("[\"Bubba\",12,\"Austria\"]", parameters.toString()); } @Test void testNestedOrCriteria() { QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria")); - assertEquals("`name` = \"Bubba\" or (`age` > 12 or `country` = \"Austria\")", c.export()); + JsonArray parameters = JsonArray.create(); + assertEquals("`name` = $1 or (`age` > $2 or `country` = $3)", c.export(new int[1], parameters, null)); + assertEquals("[\"Bubba\",12,\"Austria\"]", parameters.toString()); } @Test void testNestedNotIn() { - QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria")).and( - where("state").notIn(new String[] { "Alabama", "Florida" })); + QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria")) + .and(where("state").notIn(new String[] { "Alabama", "Florida" })); assertEquals("`name` = \"Bubba\" or (`age` > 12 or `country` = \"Austria\") and " - + "(not( (`state` in ( [ \"Alabama\", \"Florida\" ] )) ))", c.export()); + + "(not( (`state` in ( [\"Alabama\",\"Florida\"] )) ))", c.export()); } @Test @@ -110,13 +125,13 @@ void testStartingWith() { } /* cannot do this properly yet because in arg to when() in - * startingWith() cannot be a QueryCriteria + * startingWith() cannot be a QueryCriteria @Test void testStartingWithExpr() { QueryCriteria c = where("name").startingWith(where("name").plus("xxx")); assertEquals("`name` like (((`name` || "xxx") || ""%""))", c.export()); } - */ + */ @Test void testEndingWith() { @@ -204,14 +219,22 @@ void testBetween() { @Test void testIn() { - QueryCriteria c = where("name").in(new String[] { "gump", "davis" }); - assertEquals("`name` in ( [ \"gump\", \"davis\" ] )", c.export()); + String[] args = new String[] { "gump", "davis" }; + QueryCriteria c = where("name").in(args); + assertEquals("`name` in ( [\"gump\",\"davis\"] )", c.export()); + JsonArray parameters = JsonArray.create(); + assertEquals("`name` in ( $1 )", c.export(new int[1], parameters, null)); + assertEquals(arrayToString(args), parameters.get(0).toString()); } @Test void testNotIn() { - QueryCriteria c = where("name").notIn(new String[] { "gump", "davis" }); - assertEquals("not( (`name` in ( [ \"gump\", \"davis\" ] )) )", c.export()); + String[] args = new String[] { "gump", "davis" }; + QueryCriteria c = where("name").notIn(args); + assertEquals("not( (`name` in ( [\"gump\",\"davis\"] )) )", c.export()); + JsonArray parameters = JsonArray.create(); + assertEquals("not( (`name` in ( $1 )) )", c.export(new int[1], parameters, null)); + assertEquals(arrayToString(args), parameters.get(0).toString()); } @Test @@ -225,4 +248,28 @@ void testFalse() { QueryCriteria c = where("name").FALSE(); assertEquals("not( (`name`) )", c.export()); } + + private String arrayToString(Object[] array) { + StringBuilder sb = new StringBuilder(); + if (array != null) { + sb.append("["); + boolean first = true; + for (Object e : array) { + if (!first) { + sb.append(","); + } + first = false; + if (e instanceof Number) + sb.append(e); + else { + sb.append("\""); + sb.append(e); + sb.append("\""); + } + } + sb.append("]"); + } + return sb.toString(); + } + } diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java index 1787a5ff8..458c5d97c 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java @@ -18,6 +18,7 @@ import java.util.List; +import com.couchbase.client.java.json.JsonArray; import org.springframework.data.couchbase.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; @@ -34,6 +35,10 @@ public interface UserRepository extends PagingAndSortingRepository List findByFirstname(String firstname); + List findByFirstnameIn(String... firstnames); + + List findByFirstnameIn(JsonArray firstnames); + List findByFirstnameAndLastname(String firstname, String lastname); @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and firstname = $1 and lastname = $2") diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java index b3b62b9e0..f3bbd618d 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java @@ -19,7 +19,13 @@ import static org.springframework.data.couchbase.core.query.QueryCriteria.*; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; @@ -62,6 +68,69 @@ void createsQueryCorrectly() throws Exception { assertEquals(query.export(), " WHERE " + where("firstname").is("Oliver").export()); } + @Test + void queryParametersArray() throws Exception { + String input = "findByFirstnameIn"; + PartTree tree = new PartTree(input, User.class); + Method method = UserRepository.class.getMethod(input, String[].class); + Query expected = (new Query()).addCriteria(where("firstname").in("Oliver", "Charles")); + N1qlQueryCreator creator = new N1qlQueryCreator(tree, + getAccessor(getParameters(method), new Object[] { new Object[] { "Oliver", "Charles" } }), null, converter); + Query query = creator.createQuery(); + + // Query expected = (new Query()).addCriteria(where("firstname").in("Oliver", "Charles")); + assertEquals(expected.export(new int[1]), query.export(new int[1])); + JsonObject expectedOptions = JsonObject.create(); + expected.buildQueryOptions(null).build().injectParams(expectedOptions); + JsonObject actualOptions = JsonObject.create(); + expected.buildQueryOptions(null).build().injectParams(actualOptions); + assertEquals(expectedOptions.removeKey("client_context_id"), actualOptions.removeKey("client_context_id")); + } + + @Test + void queryParametersJsonArray() throws Exception { + String input = "findByFirstnameIn"; + PartTree tree = new PartTree(input, User.class); + Method method = UserRepository.class.getMethod(input, JsonArray.class); + + JsonArray jsonArray = JsonArray.create(); + jsonArray.add("Oliver"); + jsonArray.add("Charles"); + N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), jsonArray), null, + converter); + Query query = creator.createQuery(); + + Query expected = (new Query()).addCriteria(where("firstname").in("Oliver", "Charles")); + assertEquals(expected.export(new int[1]), query.export(new int[1])); + JsonObject expectedOptions = JsonObject.create(); + expected.buildQueryOptions(null).build().injectParams(expectedOptions); + JsonObject actualOptions = JsonObject.create(); + expected.buildQueryOptions(null).build().injectParams(actualOptions); + assertEquals(expectedOptions.removeKey("client_context_id"), actualOptions.removeKey("client_context_id")); + } + + @Test + void queryParametersList() throws Exception { + String input = "findByFirstnameIn"; + PartTree tree = new PartTree(input, User.class); + Method method = UserRepository.class.getMethod(input, String[].class); + List list = new LinkedList<>(); + list.add("Oliver"); + list.add("Charles"); + N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), new Object[] { list }), + null, converter); + Query query = creator.createQuery(); + + Query expected = (new Query()).addCriteria(where("firstname").in("Oliver", "Charles")); + + assertEquals(expected.export(new int[1]), query.export(new int[1])); + JsonObject expectedOptions = JsonObject.create(); + expected.buildQueryOptions(null).build().injectParams(expectedOptions); + JsonObject actualOptions = JsonObject.create(); + expected.buildQueryOptions(null).build().injectParams(actualOptions); + assertEquals(expectedOptions.removeKey("client_context_id"), actualOptions.removeKey("client_context_id")); + } + @Test void createsAndQueryCorrectly() throws Exception { String input = "findByFirstnameAndLastname"; @@ -71,7 +140,7 @@ void createsAndQueryCorrectly() throws Exception { converter); Query query = creator.createQuery(); - assertEquals(query.export(), " WHERE " + where("firstname").is("John").and("lastname").is("Doe").export()); + assertEquals(" WHERE " + where("firstname").is("John").and("lastname").is("Doe").export(), query.export()); } private ParameterAccessor getAccessor(Parameters params, Object... values) {