Skip to content

Commit 9eb2e63

Browse files
DATAMONGO-1290 - Move parameter binding for String based queries.
Moved parameter binding for string based queries into separate class.
1 parent fdc2acd commit 9eb2e63

File tree

2 files changed

+241
-122
lines changed

2 files changed

+241
-122
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
/*
2+
* Copyright 2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.repository.query;
17+
18+
import java.util.Collections;
19+
import java.util.List;
20+
21+
import javax.xml.bind.DatatypeConverter;
22+
23+
import org.bson.BSON;
24+
import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery.ParameterBinding;
25+
import org.springframework.data.repository.query.EvaluationContextProvider;
26+
import org.springframework.expression.EvaluationContext;
27+
import org.springframework.expression.Expression;
28+
import org.springframework.expression.spel.standard.SpelExpressionParser;
29+
import org.springframework.util.Assert;
30+
import org.springframework.util.CollectionUtils;
31+
import org.springframework.util.StringUtils;
32+
33+
import com.mongodb.util.JSON;
34+
35+
/**
36+
* {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placholders within a
37+
* {@link String}.
38+
*
39+
* @author Christoph Strobl
40+
* @author Thomas Darimont
41+
* @since 1.9
42+
*/
43+
class ExpressionEvaluatingParameterBinder {
44+
45+
private final SpelExpressionParser expressionParser;
46+
private final EvaluationContextProvider evaluationContextProvider;
47+
48+
/**
49+
* Creates new {@link ExpressionEvaluatingParameterBinder}
50+
*
51+
* @param expressionParser must not be {@literal null}.
52+
* @param evaluationContextProvider must not be {@literal null}.
53+
*/
54+
public ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser,
55+
EvaluationContextProvider evaluationContextProvider) {
56+
57+
Assert.notNull(expressionParser, "ExpressionParser must not be null!");
58+
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");
59+
60+
this.expressionParser = expressionParser;
61+
this.evaluationContextProvider = evaluationContextProvider;
62+
}
63+
64+
/**
65+
* Bind values provided by {@link MongoParameterAccessor} to placeholders in {@literal raw} while consisdering
66+
* potential conversions and parameter types.
67+
*
68+
* @param raw
69+
* @param accessor
70+
* @param bindingContext
71+
* @return {@literal null} if given {@literal raw} value is empty.
72+
*/
73+
public String bind(String raw, MongoParameterAccessor accessor, BindingContext bindingContext) {
74+
75+
if (!StringUtils.hasText(raw)) {
76+
return null;
77+
}
78+
79+
return replacePlaceholders(raw, accessor, bindingContext);
80+
}
81+
82+
/**
83+
* Replaced the parameter place-holders with the actual parameter values from the given {@link ParameterBinding}s.
84+
*
85+
* @param input
86+
* @param accessor
87+
* @param parameters
88+
* @param bindings
89+
* @return
90+
*/
91+
private String replacePlaceholders(String input, MongoParameterAccessor accessor, BindingContext bindingContext) {
92+
93+
if (!bindingContext.hasBindings()) {
94+
return input;
95+
}
96+
97+
boolean isCompletlyParameterizedQuery = input.matches("^\\?\\d+$");
98+
99+
StringBuilder result = new StringBuilder(input);
100+
101+
for (ParameterBinding binding : bindingContext.getBindings()) {
102+
103+
String parameter = binding.getParameter();
104+
int idx = result.indexOf(parameter);
105+
106+
if (idx != -1) {
107+
String valueForBinding = getParameterValueForBinding(accessor, bindingContext.getParameters(), binding);
108+
109+
// if the value to bind is an object literal we need to remove the quoting around
110+
// the expression insertion point.
111+
boolean shouldPotentiallyRemoveQuotes = valueForBinding.startsWith("{") && !isCompletlyParameterizedQuery;
112+
113+
int start = idx;
114+
int end = idx + parameter.length();
115+
116+
if (shouldPotentiallyRemoveQuotes) {
117+
118+
// is the insertion point actually surrounded by quotes?
119+
char beforeStart = result.charAt(start - 1);
120+
char afterEnd = result.charAt(end);
121+
122+
if ((beforeStart == '\'' || beforeStart == '"') && (afterEnd == '\'' || afterEnd == '"')) {
123+
124+
// skip preceeding and following quote
125+
start -= 1;
126+
end += 1;
127+
}
128+
}
129+
130+
result.replace(start, end, valueForBinding);
131+
}
132+
}
133+
134+
return result.toString();
135+
}
136+
137+
/**
138+
* Returns the serialized value to be used for the given {@link ParameterBinding}.
139+
*
140+
* @param accessor
141+
* @param parameters
142+
* @param binding
143+
* @return
144+
*/
145+
private String getParameterValueForBinding(MongoParameterAccessor accessor, MongoParameters parameters,
146+
ParameterBinding binding) {
147+
148+
Object value = binding.isExpression() ? evaluateExpression(binding.getExpression(), parameters,
149+
accessor.getValues()) : accessor.getBindableValue(binding.getParameterIndex());
150+
151+
if (value instanceof String && binding.isQuoted()) {
152+
return (String) value;
153+
}
154+
155+
if (value instanceof byte[]) {
156+
157+
String base64representation = DatatypeConverter.printBase64Binary((byte[]) value);
158+
if (!binding.isQuoted()) {
159+
return "{ '$binary' : '" + base64representation + "', '$type' : " + BSON.B_GENERAL + "}";
160+
}
161+
return base64representation;
162+
}
163+
164+
return JSON.serialize(value);
165+
}
166+
167+
/**
168+
* Evaluates the given {@code expressionString}.
169+
*
170+
* @param expressionString
171+
* @param parameters
172+
* @param parameterValues
173+
* @return
174+
*/
175+
private Object evaluateExpression(String expressionString, MongoParameters parameters, Object[] parameterValues) {
176+
177+
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(parameters, parameterValues);
178+
Expression expression = expressionParser.parseExpression(expressionString);
179+
180+
return expression.getValue(evaluationContext, Object.class);
181+
}
182+
183+
/**
184+
* @author Christoph Strobl
185+
* @since 1.9
186+
*/
187+
static class BindingContext {
188+
189+
final MongoParameters parameters;
190+
final List<ParameterBinding> bindings;
191+
192+
/**
193+
* Creates new {@link BindingContext}.
194+
*
195+
* @param parameters
196+
* @param bindings
197+
*/
198+
public BindingContext(MongoParameters parameters, List<ParameterBinding> bindings) {
199+
200+
this.parameters = parameters;
201+
this.bindings = bindings;
202+
}
203+
204+
/**
205+
* @return {@literal true} when list of bindings is not empty.
206+
*/
207+
boolean hasBindings() {
208+
return !CollectionUtils.isEmpty(bindings);
209+
}
210+
211+
/**
212+
* Get unmodifiable list of {@link ParameterBinding}s.
213+
*
214+
* @return never {@literal null}.
215+
*/
216+
public List<ParameterBinding> getBindings() {
217+
return Collections.unmodifiableList(bindings);
218+
}
219+
220+
/**
221+
* Get the associated {@link MongoParameters}.
222+
*
223+
* @return
224+
*/
225+
public MongoParameters getParameters() {
226+
return parameters;
227+
}
228+
229+
}
230+
}

0 commit comments

Comments
 (0)