15
15
*/
16
16
package org .springframework .data .mongodb .repository .query ;
17
17
18
- import java .util .Collections ;
18
+ import java .util .ArrayList ;
19
+ import java .util .LinkedHashMap ;
19
20
import java .util .List ;
21
+ import java .util .Map ;
22
+ import java .util .NoSuchElementException ;
23
+ import java .util .regex .Matcher ;
24
+ import java .util .regex .Pattern ;
20
25
21
26
import javax .xml .bind .DatatypeConverter ;
22
27
@@ -94,47 +99,67 @@ private String replacePlaceholders(String input, MongoParameterAccessor accessor
94
99
return input ;
95
100
}
96
101
97
- boolean isCompletlyParameterizedQuery = input .matches ("^\\ ?\\ d+$" );
98
- StringBuilder result = new StringBuilder ( input );
99
-
100
- for ( ParameterBinding binding : bindingContext . getBindings ()) {
102
+ if ( input .matches ("^\\ ?\\ d+$" )) {
103
+ return getParameterValueForBinding ( accessor , bindingContext . getParameters (),
104
+ bindingContext . getBindings (). iterator (). next ());
105
+ }
101
106
102
- String parameter = binding . getParameter ( );
103
- int idx = result . indexOf ( parameter );
107
+ Matcher matcher = createReplacementPattern ( bindingContext . getBindings ()). matcher ( input );
108
+ StringBuffer buffer = new StringBuffer ( );
104
109
105
- if (idx == -1 ) {
106
- continue ;
107
- }
110
+ while (matcher .find ()) {
108
111
112
+ ParameterBinding binding = bindingContext .getBindingFor (extractPlaceholder (matcher .group ()));
109
113
String valueForBinding = getParameterValueForBinding (accessor , bindingContext .getParameters (), binding );
110
114
111
- int start = idx ;
112
- int end = idx + parameter .length ();
115
+ // appendReplacement does not like unescaped $ sign and others, so we need to quote that stuff first
116
+ matcher .appendReplacement (buffer , Matcher .quoteReplacement (valueForBinding ));
117
+
118
+ if (binding .isQuoted ()) {
119
+ postProcessQuotedBinding (buffer , valueForBinding );
120
+ }
121
+ }
122
+
123
+ matcher .appendTail (buffer );
124
+ return buffer .toString ();
125
+ }
113
126
114
- // If the value to bind is an object literal we need to remove the quoting around the expression insertion point.
115
- if (valueForBinding .startsWith ("{" ) && !isCompletlyParameterizedQuery ) {
127
+ /**
128
+ * Sanitize String binding by replacing single quoted values with double quotes which prevents potential single quotes
129
+ * contained in replacement to interfere with the Json parsing. Also take care of complex objects by removing the
130
+ * quotation entirely.
131
+ *
132
+ * @param buffer the {@link StringBuffer} to operate upon.
133
+ * @param valueForBinding the actual binding value.
134
+ */
135
+ private void postProcessQuotedBinding (StringBuffer buffer , String valueForBinding ) {
116
136
117
- // Is the insertion point actually surrounded by quotes?
118
- char beforeStart = result .charAt (start - 1 );
119
- char afterEnd = result .charAt (end );
137
+ int quotationMarkIndex = buffer .length () - valueForBinding .length () - 1 ;
138
+ char quotationMark = buffer .charAt (quotationMarkIndex );
120
139
121
- if (( beforeStart == '\'' || beforeStart == '"' ) && ( afterEnd == '\'' || afterEnd == '"' ) ) {
140
+ while ( quotationMark != '\'' && quotationMark != '"' ) {
122
141
123
- // Skip preceding and following quote
124
- start -= 1 ;
125
- end += 1 ;
126
- }
142
+ quotationMarkIndex --;
143
+ if (quotationMarkIndex < 0 ) {
144
+ throw new IllegalArgumentException ("Could not find opening quotes for quoted parameter" );
127
145
}
128
-
129
- result .replace (start , end , valueForBinding );
146
+ quotationMark = buffer .charAt (quotationMarkIndex );
130
147
}
131
148
132
- return result .toString ();
149
+ if (valueForBinding .startsWith ("{" )) { // remove quotation char before the complex object string
150
+ buffer .deleteCharAt (quotationMarkIndex );
151
+ } else {
152
+
153
+ if (quotationMark == '\'' ) {
154
+ buffer .replace (quotationMarkIndex , quotationMarkIndex + 1 , "\" " );
155
+ }
156
+ buffer .append ("\" " );
157
+ }
133
158
}
134
159
135
160
/**
136
161
* Returns the serialized value to be used for the given {@link ParameterBinding}.
137
- *
162
+ *
138
163
* @param accessor must not be {@literal null}.
139
164
* @param parameters
140
165
* @param binding must not be {@literal null}.
@@ -148,15 +173,15 @@ private String getParameterValueForBinding(MongoParameterAccessor accessor, Mong
148
173
: accessor .getBindableValue (binding .getParameterIndex ());
149
174
150
175
if (value instanceof String && binding .isQuoted ()) {
151
- return (String ) value ;
176
+ return (( String ) value ). startsWith ( "{" ) ? ( String ) value : (( String ) value ). replace ( " \" " , " \\ \" " ) ;
152
177
}
153
178
154
179
if (value instanceof byte []) {
155
180
156
181
String base64representation = DatatypeConverter .printBase64Binary ((byte []) value );
157
182
158
183
if (!binding .isQuoted ()) {
159
- return "{ '$binary' : '" + base64representation + "', '$type' : " + BSON .B_GENERAL + "}" ;
184
+ return "{ '$binary' : '" + base64representation + "', '$type' : ' " + BSON .B_GENERAL + "' }" ;
160
185
}
161
186
162
187
return base64representation ;
@@ -167,7 +192,7 @@ private String getParameterValueForBinding(MongoParameterAccessor accessor, Mong
167
192
168
193
/**
169
194
* Evaluates the given {@code expressionString}.
170
- *
195
+ *
171
196
* @param expressionString must not be {@literal null} or empty.
172
197
* @param parameters must not be {@literal null}.
173
198
* @param parameterValues must not be {@literal null}.
@@ -181,25 +206,59 @@ private Object evaluateExpression(String expressionString, MongoParameters param
181
206
return expression .getValue (evaluationContext , Object .class );
182
207
}
183
208
209
+ /**
210
+ * Creates a replacement {@link Pattern} for all {@link ParameterBinding#getParameter() binding parameters} including
211
+ * a potentially trailing quotation mark.
212
+ *
213
+ * @param bindings
214
+ * @return
215
+ */
216
+ private Pattern createReplacementPattern (List <ParameterBinding > bindings ) {
217
+
218
+ StringBuilder regex = new StringBuilder ();
219
+ for (ParameterBinding binding : bindings ) {
220
+ regex .append ("|" );
221
+ regex .append (Pattern .quote (binding .getParameter ()));
222
+ regex .append ("['\" ]?" ); // potential quotation char (as in { foo : '?0' }).
223
+ }
224
+
225
+ return Pattern .compile (regex .substring (1 ));
226
+ }
227
+
228
+ /**
229
+ * Extract the placeholder stripping any trailing trailing quotation mark that might have resulted from the
230
+ * {@link #createReplacementPattern(List) pattern} used.
231
+ *
232
+ * @param groupName The actual {@link Matcher#group() group}.
233
+ * @return
234
+ */
235
+ private String extractPlaceholder (String groupName ) {
236
+
237
+ if (!groupName .endsWith ("'" ) && !groupName .endsWith ("\" " )) {
238
+ return groupName ;
239
+ }
240
+ return groupName .substring (0 , groupName .length () - 1 );
241
+ }
242
+
184
243
/**
185
244
* @author Christoph Strobl
186
245
* @since 1.9
187
246
*/
188
247
static class BindingContext {
189
248
190
249
final MongoParameters parameters ;
191
- final List < ParameterBinding > bindings ;
250
+ final Map < String , ParameterBinding > bindings ;
192
251
193
252
/**
194
253
* Creates new {@link BindingContext}.
195
- *
254
+ *
196
255
* @param parameters
197
256
* @param bindings
198
257
*/
199
258
public BindingContext (MongoParameters parameters , List <ParameterBinding > bindings ) {
200
259
201
260
this .parameters = parameters ;
202
- this .bindings = bindings ;
261
+ this .bindings = mapBindings ( bindings ) ;
203
262
}
204
263
205
264
/**
@@ -211,11 +270,28 @@ boolean hasBindings() {
211
270
212
271
/**
213
272
* Get unmodifiable list of {@link ParameterBinding}s.
214
- *
273
+ *
215
274
* @return never {@literal null}.
216
275
*/
217
276
public List <ParameterBinding > getBindings () {
218
- return Collections .unmodifiableList (bindings );
277
+ return new ArrayList <ParameterBinding >(bindings .values ());
278
+ }
279
+
280
+ /**
281
+ * Get the concrete {@link ParameterBinding} for a given {@literal placeholder}.
282
+ *
283
+ * @param placeholder must not be {@literal null}.
284
+ * @return
285
+ * @throws java.util.NoSuchElementException
286
+ * @since 1.10
287
+ */
288
+ ParameterBinding getBindingFor (String placeholder ) {
289
+
290
+ if (!bindings .containsKey (placeholder )) {
291
+ throw new NoSuchElementException (String .format ("Could not to find binding for placeholder '%s'." , placeholder ));
292
+ }
293
+
294
+ return bindings .get (placeholder );
219
295
}
220
296
221
297
/**
@@ -227,5 +303,13 @@ public MongoParameters getParameters() {
227
303
return parameters ;
228
304
}
229
305
306
+ private static Map <String , ParameterBinding > mapBindings (List <ParameterBinding > bindings ) {
307
+
308
+ Map <String , ParameterBinding > map = new LinkedHashMap <String , ParameterBinding >(bindings .size (), 1 );
309
+ for (ParameterBinding binding : bindings ) {
310
+ map .put (binding .getParameter (), binding );
311
+ }
312
+ return map ;
313
+ }
230
314
}
231
315
}
0 commit comments