2020import java .util .LinkedList ;
2121import java .util .List ;
2222
23+ import com .couchbase .client .core .error .InvalidArgumentException ;
24+ import com .couchbase .client .java .json .JsonArray ;
25+ import com .couchbase .client .java .json .JsonObject ;
26+ import com .couchbase .client .java .json .JsonValue ;
27+ import org .springframework .data .couchbase .core .convert .CouchbaseConverter ;
2328import org .springframework .lang .Nullable ;
2429
2530/**
@@ -60,6 +65,10 @@ public class QueryCriteria implements QueryCriteriaDefinition {
6065 this .format = format ;
6166 }
6267
68+ Object [] getValue () {
69+ return value ;
70+ }
71+
6372 /**
6473 * Static factory method to create a Criteria using the provided key.
6574 */
@@ -68,8 +77,8 @@ public static QueryCriteria where(String key) {
6877 }
6978
7079 private static QueryCriteria wrap (QueryCriteria criteria ) {
71- QueryCriteria qc = new QueryCriteria (new LinkedList <QueryCriteria >(), criteria .key , criteria .value , null ,
72- criteria .operator , criteria . format );
80+ QueryCriteria qc = new QueryCriteria (new LinkedList <>(), criteria .key , criteria .value , null , criteria . operator ,
81+ criteria .format );
7382 return qc ;
7483 }
7584
@@ -167,7 +176,7 @@ public QueryCriteria containing(@Nullable Object o) {
167176 public QueryCriteria notContaining (@ Nullable Object o ) {
168177 value = new QueryCriteria [] { wrap (containing (o )) };
169178 operator = "NOT" ;
170- format = format = "not( %3$s )" ;
179+ format = "not( %3$s )" ;
171180 return this ;
172181 }
173182
@@ -196,7 +205,7 @@ public QueryCriteria isNotNull() {
196205 operator = "IS_NOT_NULL" ;
197206 value = null ;
198207 format = "%1$s is not null" ;
199- return ( QueryCriteria ) this ;
208+ return this ;
200209 }
201210
202211 public QueryCriteria isMissing () {
@@ -210,7 +219,7 @@ public QueryCriteria isNotMissing() {
210219 operator = "IS_NOT_MiSSING" ;
211220 value = null ;
212221 format = "%1$s is not missing" ;
213- return ( QueryCriteria ) this ;
222+ return this ;
214223 }
215224
216225 public QueryCriteria isValued () {
@@ -224,68 +233,74 @@ public QueryCriteria isNotValued() {
224233 operator = "IS_NOT_VALUED" ;
225234 value = null ;
226235 format = "%1$s is not valued" ;
227- return ( QueryCriteria ) this ;
236+ return this ;
228237 }
229238
230239 public QueryCriteria within (@ Nullable Object o ) {
231240 operator = "WITHIN" ;
232241 value = new Object [] { o };
233- format = "%1$s within $ 3$s" ;
234- return ( QueryCriteria ) this ;
242+ format = "%1$s within % 3$s" ;
243+ return this ;
235244 }
236245
237246 public QueryCriteria between (@ Nullable Object o1 , @ Nullable Object o2 ) {
238247 operator = "BETWEEN" ;
239248 value = new Object [] { o1 , o2 };
240249 format = "%1$s between %3$s and %4$s" ;
241- return ( QueryCriteria ) this ;
250+ return this ;
242251 }
243252
244253 public QueryCriteria in (@ Nullable Object ... o ) {
245254 operator = "IN" ;
246- value = o ;
247- StringBuilder sb = new StringBuilder ("%1$s in ( [ " );
248- for (int i = 1 ; i <= value .length ; i ++) { // format indices start at 1
249- if (i > 1 )
250- sb .append (", " );
251- sb .append ("%" + (i + 2 ) + "$s" ); // the first is fieldName, second is operator, args start at 3
255+ format = "%1$s in ( %3$s )" ;
256+ // IN takes a single argument that is a list
257+ if (o .length > 0 ) {
258+ if (o [0 ] instanceof JsonArray || o [0 ] instanceof List || o [0 ] instanceof Object []) {
259+ if (o .length != 1 ) {
260+ throw new RuntimeException ("IN cannot take multiple lists" );
261+ }
262+ value = o ;
263+ } else {
264+ value = new Object [1 ];
265+ value [0 ] = o ; // JsonArray.from(o);
266+ }
252267 }
253- format = sb .append (" ] )" ).toString ();
254- return (QueryCriteria ) this ;
268+ return this ;
255269 }
256270
257271 public QueryCriteria notIn (@ Nullable Object ... o ) {
258272 value = new QueryCriteria [] { wrap (in (o )) };
259273 operator = "NOT" ;
260- format = format = "not( %3$s )" ; // field = 1$, operator = 2$, value=$3, $4, ...
261- return ( QueryCriteria ) this ;
274+ format = "not( %3$s )" ; // field = 1$, operator = 2$, value=$3, $4, ...
275+ return this ;
262276 }
263277
264278 public QueryCriteria TRUE () { // true/false are reserved, use TRUE/FALSE
265279 value = null ;
266280 operator = null ;
267- format = format = "%1$s" ; // field = 1$, operator = 2$, value=$3, $4, ...
268- return ( QueryCriteria ) this ;
281+ format = "%1$s" ; // field = 1$, operator = 2$, value=$3, $4, ...
282+ return this ;
269283 }
270284
271285 public QueryCriteria FALSE () {
272286 value = new QueryCriteria [] { wrap (TRUE ()) };
273287 operator = "not" ;
274- format = format = "not( %3$s )" ;
275- return ( QueryCriteria ) this ;
288+ format = "not( %3$s )" ;
289+ return this ;
276290 }
277291
278292 /**
279- * This exports the query criteria into a string to be appended to the beginning of an N1QL statement
293+ * This exports the query criteria chain into a string to be appended to the beginning of an N1QL statement
280294 *
281- * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters
282- * There may already be positional parameters in the beginning of the statement,
283- * so it may not always start at 1. If it has the value -1, the query is using
284- * named parameters. If the pointer is null, the query is not using parameters.
295+ * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may
296+ * already be positional parameters in the beginning of the statement, so it may not always start at 1. If it
297+ * has the value -1, the query is using named parameters. If the pointer is null, the query is not using
298+ * parameters.
299+ * @param parameters - parameters of the query. If operands are parameterized, their values are added to parameters
285300 * @return string containing part of N1QL query
286301 */
287302 @ Override
288- public String export (int [] paramIndexPtr ) {
303+ public String export (int [] paramIndexPtr , JsonValue parameters , CouchbaseConverter converter ) {
289304 StringBuilder output = new StringBuilder ();
290305 boolean first = true ;
291306 for (QueryCriteria c : this .criteriaChain ) {
@@ -298,7 +313,7 @@ public String export(int[] paramIndexPtr) {
298313 } else {
299314 first = false ;
300315 }
301- c .exportSingle (output , paramIndexPtr );
316+ c .exportSingle (output , paramIndexPtr , parameters , converter );
302317 }
303318
304319 return output .toString ();
@@ -310,22 +325,34 @@ public String export(int[] paramIndexPtr) {
310325 * @return string containing part of N1QL query
311326 */
312327 @ Override
313- public String export () {
314- return export (null );
328+ public String export () { // used only by tests
329+ return export (null , null , null );
315330
316331 }
317332
318- private StringBuilder exportSingle (StringBuilder sb , int [] paramIndexPtr ) {
333+ /**
334+ * Appends the query criteria to a StringBuilder which will be appended to a N1QL statement
335+ *
336+ * @param sb - the string builder
337+ * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may
338+ * already be positional parameters in the beginning of the statement, so it may not always start at 1. If it
339+ * has the value -1, the query is using named parameters. If the pointer is null, the query is not using
340+ * parameters.
341+ * @param parameters - parameters of the query. If operands are parameterized, their values are added to parameters
342+ * @return string containing part of N1QL query
343+ */
344+ private StringBuilder exportSingle (StringBuilder sb , int [] paramIndexPtr , JsonValue parameters ,
345+ CouchbaseConverter converter ) {
319346 String fieldName = maybeQuote (key );
320347 int valueLen = value == null ? 0 : value .length ;
321348 Object [] v = new Object [valueLen + 2 ];
322349 v [0 ] = fieldName ;
323350 v [1 ] = operator ;
324351 for (int i = 0 ; i < valueLen ; i ++) {
325352 if (value [i ] instanceof QueryCriteria ) {
326- v [i + 2 ] = "(" + ((QueryCriteria ) value [i ]).export (paramIndexPtr ) + ")" ;
353+ v [i + 2 ] = "(" + ((QueryCriteria ) value [i ]).export (paramIndexPtr , parameters , converter ) + ")" ;
327354 } else {
328- v [i + 2 ] = maybeWrapValue (key , value [i ], paramIndexPtr );
355+ v [i + 2 ] = maybeWrapValue (key , value [i ], paramIndexPtr , parameters , converter );
329356 }
330357 }
331358
@@ -340,24 +367,84 @@ private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr) {
340367 return sb ;
341368 }
342369
343- private String maybeWrapValue (String key , Object value , int [] paramIndexPtr ) {
370+ /**
371+ * Possibly convert an operand to a positional or named parameter
372+ *
373+ * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters There may
374+ * already be positional parameters in the beginning of the statement, so it may not always start at 1. If it
375+ * has the value -1, the query is using named parameters. If the pointer is null, the query is not using
376+ * parameters.
377+ * @param parameters - parameters of the query. If operands are parameterized, their values are added to parameters
378+ * @return string containing part of N1QL query
379+ */
380+ private String maybeWrapValue (String key , Object value , int [] paramIndexPtr , JsonValue parameters ,
381+ CouchbaseConverter converter ) {
344382 if (paramIndexPtr != null ) {
345383 if (paramIndexPtr [0 ] >= 0 ) {
384+ JsonArray params = (JsonArray ) parameters ;
385+ // from StringBasedN1qlQueryParser.getPositionalPlaceholderValues()
386+ try {
387+ params .add (convert (converter , value ));
388+ } catch (InvalidArgumentException iae ) {
389+ if (value instanceof Object []) {
390+ addAsArray (params , value , converter );
391+ } else {
392+ throw iae ;
393+ }
394+ }
346395 return "$" + (++paramIndexPtr [0 ]); // these are generated in order
347396 } else {
397+ JsonObject params = (JsonObject ) parameters ;
398+ // from StringBasedN1qlQueryParser.getNamedPlaceholderValues()
399+ try {
400+ params .put (key , convert (converter , value ));
401+ } catch (InvalidArgumentException iae ) {
402+ if (value instanceof Object []) {
403+ params .put (key , JsonArray .from ((Object []) value ));
404+ } else {
405+ throw iae ;
406+ }
407+ }
348408 return "$" + key ;
349409 }
350410 }
351411
412+ // Did not convert to a parameter. Add quotes or whatever it might need.
413+
352414 if (value instanceof String ) {
353415 return "\" " + value + "\" " ;
354416 } else if (value == null ) {
355417 return "null" ;
418+ } else if (value instanceof Object []) { // convert array into sequence of comma-separated values
419+ StringBuffer l = new StringBuffer ();
420+ l .append ("[" );
421+ Object [] array = (Object []) value ;
422+ for (int i = 0 ; i < array .length ; i ++) {
423+ if (i > 0 ) {
424+ l .append ("," );
425+ }
426+ l .append (maybeWrapValue (null , array [i ], null , null , converter ));
427+ }
428+ l .append ("]" );
429+ return l .toString ();
356430 } else {
357431 return value .toString ();
358432 }
359433 }
360434
435+ private static Object convert (CouchbaseConverter converter , Object value ) {
436+ return converter != null ? converter .convertForWriteIfNeeded (value ) : value ;
437+ }
438+
439+ private void addAsArray (JsonArray posValues , Object o , CouchbaseConverter converter ) {
440+ Object [] array = (Object []) o ;
441+ JsonArray ja = JsonValue .ja ();
442+ for (Object e : array ) {
443+ ja .add (String .valueOf (convert (converter , e )));
444+ }
445+ posValues .add (ja );
446+ }
447+
361448 private String maybeQuote (String value ) {
362449 if (value == null || (value .startsWith ("\" " ) && value .endsWith ("\" " ))) {
363450 return value ;
0 commit comments