1
1
/*
2
- * Copyright 2015-2016 the original author or authors.
2
+ * Copyright 2015-2017 the original author or authors.
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
15
15
*/
16
16
package org .springframework .data .mongodb .repository .query ;
17
17
18
+ import lombok .EqualsAndHashCode ;
18
19
import lombok .Value ;
20
+ import lombok .experimental .UtilityClass ;
19
21
20
22
import java .util .ArrayList ;
21
23
import java .util .LinkedHashMap ;
37
39
import org .springframework .util .CollectionUtils ;
38
40
import org .springframework .util .StringUtils ;
39
41
42
+ import com .mongodb .DBObject ;
40
43
import com .mongodb .util .JSON ;
41
44
42
45
/**
43
- * {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placholders within a
46
+ * {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placeholders within a
44
47
* {@link String}.
45
- *
48
+ *
46
49
* @author Christoph Strobl
47
50
* @author Thomas Darimont
48
51
* @author Oliver Gierke
@@ -56,7 +59,7 @@ class ExpressionEvaluatingParameterBinder {
56
59
57
60
/**
58
61
* Creates new {@link ExpressionEvaluatingParameterBinder}
59
- *
62
+ *
60
63
* @param expressionParser must not be {@literal null}.
61
64
* @param evaluationContextProvider must not be {@literal null}.
62
65
*/
@@ -73,7 +76,7 @@ public ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser
73
76
/**
74
77
* Bind values provided by {@link MongoParameterAccessor} to placeholders in {@literal raw} while considering
75
78
* potential conversions and parameter types.
76
- *
79
+ *
77
80
* @param raw can be {@literal null} or empty.
78
81
* @param accessor must not be {@literal null}.
79
82
* @param bindingContext must not be {@literal null}.
@@ -90,7 +93,7 @@ public String bind(String raw, MongoParameterAccessor accessor, BindingContext b
90
93
91
94
/**
92
95
* Replaced the parameter placeholders with the actual parameter values from the given {@link ParameterBinding}s.
93
- *
96
+ *
94
97
* @param input must not be {@literal null} or empty.
95
98
* @param accessor must not be {@literal null}.
96
99
* @param bindingContext must not be {@literal null}.
@@ -110,16 +113,23 @@ private String replacePlaceholders(String input, MongoParameterAccessor accessor
110
113
Matcher matcher = createReplacementPattern (bindingContext .getBindings ()).matcher (input );
111
114
StringBuffer buffer = new StringBuffer ();
112
115
116
+ int parameterIndex = 0 ;
113
117
while (matcher .find ()) {
114
118
115
- ParameterBinding binding = bindingContext .getBindingFor (extractPlaceholder (matcher .group ()));
119
+ Placeholder placeholder = extractPlaceholder (parameterIndex ++, matcher );
120
+ ParameterBinding binding = bindingContext .getBindingFor (placeholder );
116
121
String valueForBinding = getParameterValueForBinding (accessor , bindingContext .getParameters (), binding );
117
122
118
123
// appendReplacement does not like unescaped $ sign and others, so we need to quote that stuff first
119
124
matcher .appendReplacement (buffer , Matcher .quoteReplacement (valueForBinding ));
125
+ if (StringUtils .hasText (placeholder .getSuffix ())) {
126
+ buffer .append (placeholder .getSuffix ());
127
+ }
120
128
121
- if (binding .isQuoted ()) {
122
- postProcessQuotedBinding (buffer , valueForBinding );
129
+ if (placeholder .isQuoted ()) {
130
+ postProcessQuotedBinding (buffer , valueForBinding ,
131
+ !binding .isExpression () ? accessor .getBindableValue (binding .getParameterIndex ()) : null ,
132
+ binding .isExpression ());
123
133
}
124
134
}
125
135
@@ -134,8 +144,10 @@ private String replacePlaceholders(String input, MongoParameterAccessor accessor
134
144
*
135
145
* @param buffer the {@link StringBuffer} to operate upon.
136
146
* @param valueForBinding the actual binding value.
147
+ * @param raw the raw binding value
148
+ * @param isExpression {@literal true} if the binding value results from a SpEL expression.
137
149
*/
138
- private void postProcessQuotedBinding (StringBuffer buffer , String valueForBinding ) {
150
+ private void postProcessQuotedBinding (StringBuffer buffer , String valueForBinding , Object raw , boolean isExpression ) {
139
151
140
152
int quotationMarkIndex = buffer .length () - valueForBinding .length () - 1 ;
141
153
char quotationMark = buffer .charAt (quotationMarkIndex );
@@ -151,7 +163,8 @@ private void postProcessQuotedBinding(StringBuffer buffer, String valueForBindin
151
163
quotationMark = buffer .charAt (quotationMarkIndex );
152
164
}
153
165
154
- if (valueForBinding .startsWith ("{" )) { // remove quotation char before the complex object string
166
+ // remove quotation char before the complex object string
167
+ if (valueForBinding .startsWith ("{" ) && (raw instanceof DBObject || isExpression )) {
155
168
156
169
buffer .deleteCharAt (quotationMarkIndex );
157
170
@@ -181,7 +194,12 @@ private String getParameterValueForBinding(MongoParameterAccessor accessor, Mong
181
194
: accessor .getBindableValue (binding .getParameterIndex ());
182
195
183
196
if (value instanceof String && binding .isQuoted ()) {
184
- return ((String ) value ).startsWith ("{" ) ? (String ) value : ((String ) value ).replace ("\" " , "\\ \" " );
197
+
198
+ if (binding .isExpression () && ((String ) value ).startsWith ("{" )) {
199
+ return (String ) value ;
200
+ }
201
+
202
+ return QuotedString .unquote (JSON .serialize (value ));
185
203
}
186
204
187
205
if (value instanceof byte []) {
@@ -228,8 +246,9 @@ private Pattern createReplacementPattern(List<ParameterBinding> bindings) {
228
246
for (ParameterBinding binding : bindings ) {
229
247
230
248
regex .append ("|" );
231
- regex .append (Pattern .quote (binding .getParameter ()));
232
- regex .append ("['\" ]?" ); // potential quotation char (as in { foo : '?0' }).
249
+ regex .append ("(" + Pattern .quote (binding .getParameter ()) + ")" );
250
+ regex .append ("([\\ w.]*" );
251
+ regex .append ("(\\ W?['\" ]|\\ w*')?)" );
233
252
}
234
253
235
254
return Pattern .compile (regex .substring (1 ));
@@ -239,14 +258,40 @@ private Pattern createReplacementPattern(List<ParameterBinding> bindings) {
239
258
* Extract the placeholder stripping any trailing trailing quotation mark that might have resulted from the
240
259
* {@link #createReplacementPattern(List) pattern} used.
241
260
*
242
- * @param groupName The actual {@link Matcher#group() group}.
261
+ * @param parameterIndex The actual parameter index.
262
+ * @param matcher The actual {@link Matcher}.
243
263
* @return
244
264
*/
245
- private Placeholder extractPlaceholder (String groupName ) {
265
+ private Placeholder extractPlaceholder (int parameterIndex , Matcher matcher ) {
266
+
267
+ String rawPlaceholder = matcher .group (parameterIndex * 3 + 1 );
268
+ String suffix = matcher .group (parameterIndex * 3 + 2 );
269
+
270
+ if (!StringUtils .hasText (rawPlaceholder )) {
246
271
247
- return !groupName .endsWith ("'" ) && !groupName .endsWith ("\" " ) ? //
248
- Placeholder .of (groupName , false ) : //
249
- Placeholder .of (groupName .substring (0 , groupName .length () - 1 ), true );
272
+ rawPlaceholder = matcher .group ();
273
+ if (rawPlaceholder .matches (".*\\ d$" )) {
274
+ suffix = "" ;
275
+ } else {
276
+ int index = rawPlaceholder .replaceAll ("[^\\ ?0-9]*$" , "" ).length () - 1 ;
277
+ if (index > 0 && rawPlaceholder .length () > index ) {
278
+ suffix = rawPlaceholder .substring (index + 1 );
279
+ }
280
+ }
281
+ if (QuotedString .endsWithQuote (rawPlaceholder )) {
282
+ rawPlaceholder = rawPlaceholder .substring (0 ,
283
+ rawPlaceholder .length () - (StringUtils .hasText (suffix ) ? suffix .length () : 1 ));
284
+ }
285
+ }
286
+
287
+ if (StringUtils .hasText (suffix )) {
288
+
289
+ boolean quoted = QuotedString .endsWithQuote (suffix );
290
+
291
+ return Placeholder .of (parameterIndex , rawPlaceholder , quoted ,
292
+ quoted ? QuotedString .unquoteSuffix (suffix ) : suffix );
293
+ }
294
+ return Placeholder .of (parameterIndex , rawPlaceholder , false , null );
250
295
}
251
296
252
297
/**
@@ -317,8 +362,9 @@ private static Map<Placeholder, ParameterBinding> mapBindings(List<ParameterBind
317
362
318
363
Map <Placeholder , ParameterBinding > map = new LinkedHashMap <Placeholder , ParameterBinding >(bindings .size (), 1 );
319
364
365
+ int parameterIndex = 0 ;
320
366
for (ParameterBinding binding : bindings ) {
321
- map .put (Placeholder .of (binding .getParameter (), binding .isQuoted ()), binding );
367
+ map .put (Placeholder .of (parameterIndex ++, binding .getParameter (), binding .isQuoted (), null ), binding );
322
368
}
323
369
324
370
return map ;
@@ -332,18 +378,60 @@ private static Map<Placeholder, ParameterBinding> mapBindings(List<ParameterBind
332
378
* @since 1.9
333
379
*/
334
380
@ Value (staticConstructor = "of" )
381
+ @ EqualsAndHashCode (exclude = { "quoted" , "suffix" })
335
382
static class Placeholder {
336
383
384
+ private int parameterIndex ;
337
385
private final String parameter ;
338
386
private final boolean quoted ;
387
+ private final String suffix ;
339
388
340
389
/*
341
390
* (non-Javadoc)
342
391
* @see java.lang.Object#toString()
343
392
*/
344
393
@ Override
345
394
public String toString () {
346
- return quoted ? String .format ("'%s'" , parameter ) : parameter ;
395
+ return quoted ? String .format ("'%s'" , parameter + (suffix != null ? suffix : "" ))
396
+ : parameter + (suffix != null ? suffix : "" );
397
+ }
398
+
399
+ }
400
+
401
+ /**
402
+ * Utility to handle quoted strings using single/double quotes.
403
+ *
404
+ * @author Mark Paluch
405
+ */
406
+ @ UtilityClass
407
+ static class QuotedString {
408
+
409
+ /**
410
+ * @param string
411
+ * @return {@literal true} if {@literal string} ends with a single/double quote.
412
+ */
413
+ static boolean endsWithQuote (String string ) {
414
+ return string .endsWith ("'" ) || string .endsWith ("\" " );
415
+ }
416
+
417
+ /**
418
+ * Remove trailing quoting from {@literal quoted}.
419
+ *
420
+ * @param quoted
421
+ * @return {@literal quoted} with removed quotes.
422
+ */
423
+ public static String unquoteSuffix (String quoted ) {
424
+ return quoted .substring (0 , quoted .length () - 1 );
425
+ }
426
+
427
+ /**
428
+ * Remove leading and trailing quoting from {@literal quoted}.
429
+ *
430
+ * @param quoted
431
+ * @return {@literal quoted} with removed quotes.
432
+ */
433
+ public static String unquote (String quoted ) {
434
+ return quoted .substring (1 , quoted .length () - 1 );
347
435
}
348
436
}
349
437
}
0 commit comments