DATAMONGO-2199 - Introduce JSON parser capable of binding parameters.

We moved in and adapted some classes from the MongoDB Java driver in order to bind parameters to placeholders while parsing a JSON string. This allows us to move off the deprecated JSON.parse method that will be removed with the 4.0 version of the driver.

Original pull request: #643.
This commit is contained in:
Christoph Strobl
2019-02-14 13:37:14 +01:00
committed by Mark Paluch
parent 45f4b5087c
commit 8e21cc181e
14 changed files with 3263 additions and 996 deletions

View File

@@ -1,671 +0,0 @@
/*
* Copyright 2015-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository.query;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.experimental.UtilityClass;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bson.codecs.BinaryCodec;
import org.bson.codecs.Codec;
import org.bson.codecs.UuidCodec;
import org.bson.json.JsonWriter;
import org.bson.types.Binary;
import org.springframework.data.mongodb.CodecRegistryProvider;
import org.springframework.data.mongodb.core.query.SerializationUtils;
import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery.ParameterBinding;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.Base64Utils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
/**
* {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placeholders within a
* {@link String}.
*
* @author Christoph Strobl
* @author Thomas Darimont
* @author Oliver Gierke
* @author Mark Paluch
* @since 1.9
*/
class ExpressionEvaluatingParameterBinder {
private final SpelExpressionParser expressionParser;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final CodecRegistryProvider codecRegistryProvider;
/**
* Creates new {@link ExpressionEvaluatingParameterBinder}
*
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
*/
public ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
Assert.notNull(expressionParser, "ExpressionParser must not be null!");
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");
this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
this.codecRegistryProvider = () -> MongoClient.getDefaultCodecRegistry();
}
/**
* Bind values provided by {@link MongoParameterAccessor} to placeholders in {@literal raw} while considering
* potential conversions and parameter types.
*
* @param raw can be empty.
* @param accessor must not be {@literal null}.
* @param bindingContext must not be {@literal null}.
* @return {@literal null} if given {@code raw} value is empty.
*/
public String bind(String raw, MongoParameterAccessor accessor, BindingContext bindingContext) {
if (!StringUtils.hasText(raw)) {
return raw;
}
return replacePlaceholders(raw, accessor, bindingContext);
}
/**
* Replaced the parameter placeholders with the actual parameter values from the given {@link ParameterBinding}s.
*
* @param input must not be {@literal null} or empty.
* @param accessor must not be {@literal null}.
* @param bindingContext must not be {@literal null}.
* @return
*/
private String replacePlaceholders(String input, MongoParameterAccessor accessor, BindingContext bindingContext) {
if (!bindingContext.hasBindings()) {
return input;
}
if (input.matches("^\\?\\d+$")) {
return getParameterValueForBinding(accessor, bindingContext.getParameters(),
bindingContext.getBindings().iterator().next());
}
Matcher matcher = createReplacementPattern(bindingContext.getBindings()).matcher(input);
StringBuffer buffer = new StringBuffer();
int parameterIndex = 0;
while (matcher.find()) {
Placeholder placeholder = extractPlaceholder(parameterIndex++, matcher);
ParameterBinding binding = bindingContext.getBindingFor(placeholder);
String valueForBinding = getParameterValueForBinding(accessor, bindingContext.getParameters(), binding);
// appendReplacement does not like unescaped $ sign and others, so we need to quote that stuff first
matcher.appendReplacement(buffer, Matcher.quoteReplacement(valueForBinding));
if (StringUtils.hasText(placeholder.getSuffix())) {
buffer.append(placeholder.getSuffix());
}
if (placeholder.isQuoted()) {
postProcessQuotedBinding(buffer, valueForBinding,
!binding.isExpression() ? accessor.getBindableValue(binding.getParameterIndex()) : null,
binding.isExpression());
}
}
matcher.appendTail(buffer);
return buffer.toString();
}
/**
* Sanitize String binding by replacing single quoted values with double quotes which prevents potential single quotes
* contained in replacement to interfere with the Json parsing. Also take care of complex objects by removing the
* quotation entirely.
*
* @param buffer the {@link StringBuffer} to operate upon.
* @param valueForBinding the actual binding value.
* @param raw the raw binding value
* @param isExpression {@literal true} if the binding value results from a SpEL expression.
*/
private void postProcessQuotedBinding(StringBuffer buffer, String valueForBinding, @Nullable Object raw,
boolean isExpression) {
int quotationMarkIndex = buffer.length() - valueForBinding.length() - 1;
char quotationMark = buffer.charAt(quotationMarkIndex);
while (quotationMark != '\'' && quotationMark != '"') {
quotationMarkIndex--;
if (quotationMarkIndex < 0) {
throw new IllegalArgumentException("Could not find opening quotes for quoted parameter");
}
quotationMark = buffer.charAt(quotationMarkIndex);
}
// remove quotation char before the complex object string
if (valueForBinding.startsWith("{") && (raw instanceof DBObject || isExpression)) {
buffer.deleteCharAt(quotationMarkIndex);
} else {
if (isExpression) {
buffer.deleteCharAt(quotationMarkIndex);
return;
}
if (quotationMark == '\'') {
buffer.replace(quotationMarkIndex, quotationMarkIndex + 1, "\"");
}
buffer.append("\"");
}
}
/**
* Returns the serialized value to be used for the given {@link ParameterBinding}.
*
* @param accessor must not be {@literal null}.
* @param parameters
* @param binding must not be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
private String getParameterValueForBinding(MongoParameterAccessor accessor, MongoParameters parameters,
ParameterBinding binding) {
Object value = binding.isExpression()
? evaluateExpression(binding.getExpression(), parameters, accessor.getValues())
: accessor.getBindableValue(binding.getParameterIndex());
if (value instanceof String && binding.isQuoted()) {
if (binding.isExpression() && ((String) value).startsWith("{")) {
return (String) value;
}
String encodedValue = serialize(value);
return binding.isExpression() ? encodedValue : QuotedString.unquote(encodedValue);
}
return EncodableValue.create(value).encode(codecRegistryProvider, binding.isQuoted());
}
/**
* Evaluates the given {@code expressionString}.
*
* @param expressionString must not be {@literal null} or empty.
* @param parameters must not be {@literal null}.
* @param parameterValues must not be {@literal null}.
* @return
*/
@Nullable
private Object evaluateExpression(String expressionString, MongoParameters parameters, Object[] parameterValues) {
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(parameters, parameterValues);
Expression expression = expressionParser.parseExpression(expressionString);
return expression.getValue(evaluationContext, Object.class);
}
/**
* Creates a replacement {@link Pattern} for all {@link ParameterBinding#getParameter() binding parameters} including
* a potentially trailing quotation mark.
*
* @param bindings
* @return
*/
private Pattern createReplacementPattern(List<ParameterBinding> bindings) {
StringBuilder regex = new StringBuilder();
for (ParameterBinding binding : bindings) {
regex.append("|");
regex.append("(" + Pattern.quote(binding.getParameter()) + ")");
regex.append("([\\w.]*");
regex.append("(\\W?['\"]|\\w*')?)");
}
return Pattern.compile(regex.substring(1));
}
/**
* Extract the placeholder stripping any trailing trailing quotation mark that might have resulted from the
* {@link #createReplacementPattern(List) pattern} used.
*
* @param parameterIndex The actual parameter index.
* @param matcher The actual {@link Matcher}.
* @return
*/
private Placeholder extractPlaceholder(int parameterIndex, Matcher matcher) {
String rawPlaceholder = matcher.group(parameterIndex * 3 + 1);
String suffix = matcher.group(parameterIndex * 3 + 2);
if (!StringUtils.hasText(rawPlaceholder)) {
rawPlaceholder = matcher.group();
if (rawPlaceholder.matches(".*\\d$")) {
suffix = "";
} else {
int index = rawPlaceholder.replaceAll("[^\\?0-9]*$", "").length() - 1;
if (index > 0 && rawPlaceholder.length() > index) {
suffix = rawPlaceholder.substring(index + 1);
}
}
if (QuotedString.endsWithQuote(rawPlaceholder)) {
rawPlaceholder = rawPlaceholder.substring(0,
rawPlaceholder.length() - (StringUtils.hasText(suffix) ? suffix.length() : 1));
}
}
if (StringUtils.hasText(suffix)) {
boolean quoted = QuotedString.endsWithQuote(suffix);
return Placeholder.of(parameterIndex, rawPlaceholder, quoted,
quoted ? QuotedString.unquoteSuffix(suffix) : suffix);
}
return Placeholder.of(parameterIndex, rawPlaceholder, false, null);
}
/**
* @author Christoph Strobl
* @author Mark Paluch
* @since 1.9
*/
static class BindingContext {
final MongoParameters parameters;
final Map<Placeholder, ParameterBinding> bindings;
/**
* Creates new {@link BindingContext}.
*
* @param parameters
* @param bindings
*/
public BindingContext(MongoParameters parameters, List<ParameterBinding> bindings) {
this.parameters = parameters;
this.bindings = mapBindings(bindings);
}
/**
* @return {@literal true} when list of bindings is not empty.
*/
boolean hasBindings() {
return !CollectionUtils.isEmpty(bindings);
}
/**
* Get unmodifiable list of {@link ParameterBinding}s.
*
* @return never {@literal null}.
*/
public List<ParameterBinding> getBindings() {
return new ArrayList<ParameterBinding>(bindings.values());
}
/**
* Get the concrete {@link ParameterBinding} for a given {@literal placeholder}.
*
* @param placeholder must not be {@literal null}.
* @return
* @throws java.util.NoSuchElementException
* @since 1.10
*/
ParameterBinding getBindingFor(Placeholder placeholder) {
if (!bindings.containsKey(placeholder)) {
throw new NoSuchElementException(String.format("Could not to find binding for placeholder '%s'.", placeholder));
}
return bindings.get(placeholder);
}
/**
* Get the associated {@link MongoParameters}.
*
* @return
*/
public MongoParameters getParameters() {
return parameters;
}
private static Map<Placeholder, ParameterBinding> mapBindings(List<ParameterBinding> bindings) {
Map<Placeholder, ParameterBinding> map = new LinkedHashMap<Placeholder, ParameterBinding>(bindings.size(), 1);
int parameterIndex = 0;
for (ParameterBinding binding : bindings) {
map.put(Placeholder.of(parameterIndex++, binding.getParameter(), binding.isQuoted(), null), binding);
}
return map;
}
}
/**
* Encapsulates a quoted/unquoted parameter placeholder.
*
* @author Mark Paluch
* @since 1.9
*/
@Value(staticConstructor = "of")
@EqualsAndHashCode(exclude = { "quoted", "suffix" })
static class Placeholder {
private int parameterIndex;
private final String parameter;
private final boolean quoted;
private final @Nullable String suffix;
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return quoted ? String.format("'%s'", parameter + (suffix != null ? suffix : ""))
: parameter + (suffix != null ? suffix : "");
}
}
/**
* Utility to handle quoted strings using single/double quotes.
*
* @author Mark Paluch
*/
@UtilityClass
static class QuotedString {
/**
* @param string
* @return {@literal true} if {@literal string} ends with a single/double quote.
*/
static boolean endsWithQuote(String string) {
return string.endsWith("'") || string.endsWith("\"");
}
/**
* Remove trailing quoting from {@literal quoted}.
*
* @param quoted
* @return {@literal quoted} with removed quotes.
*/
public static String unquoteSuffix(String quoted) {
return quoted.substring(0, quoted.length() - 1);
}
/**
* Remove leading and trailing quoting from {@literal quoted}.
*
* @param quoted
* @return {@literal quoted} with removed quotes.
*/
public static String unquote(String quoted) {
return quoted.substring(1, quoted.length() - 1);
}
}
/**
* Value object encapsulating a bindable value, that can be encoded to be represented as JSON (BSON).
*
* @author Mark Paluch
*/
abstract static class EncodableValue {
/**
* Obtain a {@link EncodableValue} given {@code value}.
*
* @param value the value to encode, may be {@literal null}.
* @return the {@link EncodableValue} for {@code value}.
*/
@SuppressWarnings("unchecked")
public static EncodableValue create(@Nullable Object value) {
if (value instanceof byte[]) {
return new BinaryValue((byte[]) value);
}
if (value instanceof UUID) {
return new UuidValue((UUID) value);
}
if (value instanceof Collection) {
Collection<?> collection = (Collection<?>) value;
Class<?> commonElement = CollectionUtils.findCommonElementType(collection);
if (commonElement != null) {
if (UUID.class.isAssignableFrom(commonElement)) {
return new UuidCollection((Collection<UUID>) value);
}
if (byte[].class.isAssignableFrom(commonElement)) {
return new BinaryCollectionValue((Collection<byte[]>) value);
}
}
}
return new ObjectValue(value);
}
/**
* Encode the encapsulated value.
*
* @param provider
* @param quoted
* @return
*/
public abstract String encode(CodecRegistryProvider provider, boolean quoted);
/**
* Encode a {@code value} to JSON.
*
* @param provider
* @param value
* @param defaultCodec
* @param <V>
* @return
*/
protected <V> String encode(CodecRegistryProvider provider, V value, Supplier<Codec<V>> defaultCodec) {
StringWriter writer = new StringWriter();
doEncode(provider, writer, value, defaultCodec);
return writer.toString();
}
/**
* Encode a {@link Collection} to JSON and potentially apply a {@link Function mapping function} before encoding.
*
* @param provider
* @param value
* @param mappingFunction
* @param defaultCodec
* @param <I> Input value type.
* @param <V> Target type.
* @return
*/
protected <I, V> String encodeCollection(CodecRegistryProvider provider, Iterable<I> value,
Function<I, V> mappingFunction, Supplier<Codec<V>> defaultCodec) {
StringWriter writer = new StringWriter();
writer.append("[");
value.forEach(it -> {
if (writer.getBuffer().length() > 1) {
writer.append(", ");
}
doEncode(provider, writer, mappingFunction.apply(it), defaultCodec);
});
writer.append("]");
writer.flush();
return writer.toString();
}
@SuppressWarnings("unchecked")
private <V> void doEncode(CodecRegistryProvider provider, StringWriter writer, V value,
Supplier<Codec<V>> defaultCodec) {
Codec<V> codec = provider.getCodecFor((Class<V>) value.getClass()).orElseGet(defaultCodec);
JsonWriter jsonWriter = new JsonWriter(writer);
codec.encode(jsonWriter, value, null);
jsonWriter.flush();
}
}
/**
* {@link EncodableValue} for {@code byte[]} to render to {@literal $binary}.
*/
@RequiredArgsConstructor
static class BinaryValue extends EncodableValue {
private final byte[] value;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.EncodableValue#encode(org.springframework.data.mongodb.CodecRegistryProvider, boolean)
*/
@Override
public String encode(CodecRegistryProvider provider, boolean quoted) {
if (quoted) {
return Base64Utils.encodeToString(this.value);
}
return encode(provider, new Binary(this.value), BinaryCodec::new);
}
}
/**
* {@link EncodableValue} for {@link Collection} containing only {@code byte[]} items to render to a BSON list
* containing {@literal $binary}.
*/
@RequiredArgsConstructor
static class BinaryCollectionValue extends EncodableValue {
private final Collection<byte[]> value;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.EncodableValue#encode(org.springframework.data.mongodb.CodecRegistryProvider, boolean)
*/
@Override
public String encode(CodecRegistryProvider provider, boolean quoted) {
return encodeCollection(provider, this.value, Binary::new, BinaryCodec::new);
}
}
/**
* {@link EncodableValue} for {@link UUID} to render to {@literal $binary}.
*/
@RequiredArgsConstructor
static class UuidValue extends EncodableValue {
private final UUID value;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.EncodableValue#encode(org.springframework.data.mongodb.CodecRegistryProvider, boolean)
*/
@Override
public String encode(CodecRegistryProvider provider, boolean quoted) {
if (quoted) {
return this.value.toString();
}
return encode(provider, this.value, UuidCodec::new);
}
}
/**
* {@link EncodableValue} for {@link Collection} containing only {@link UUID} items to render to a BSON list
* containing {@literal $binary}.
*/
@RequiredArgsConstructor
static class UuidCollection extends EncodableValue {
private final Collection<UUID> value;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.EncodableValue#encode(org.springframework.data.mongodb.CodecRegistryProvider, boolean)
*/
@Override
public String encode(CodecRegistryProvider provider, boolean quoted) {
return encodeCollection(provider, this.value, Function.identity(), UuidCodec::new);
}
}
/**
* Fallback-{@link EncodableValue} for {@link Object}-typed values.
*/
@RequiredArgsConstructor
static class ObjectValue extends EncodableValue {
private final @Nullable Object value;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.EncodableValue#encode(org.springframework.data.mongodb.CodecRegistryProvider, boolean)
*/
@Override
public String encode(CodecRegistryProvider provider, boolean quoted) {
return serialize(this.value);
}
}
static String serialize(Object value) {
return SerializationUtils.serializeValue(value);
}
}

View File

@@ -15,18 +15,15 @@
*/
package org.springframework.data.mongodb.repository.query;
import java.util.ArrayList;
import java.util.List;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.BindingContext;
import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery.ParameterBinding;
import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery.ParameterBindingParser;
import org.springframework.data.mongodb.util.json.ParameterBindingContext;
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
@@ -42,16 +39,17 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
private static final String COUNT_EXISTS_AND_DELETE = "Manually defined query for %s cannot be a count and exists or delete query at the same time!";
private static final Logger LOG = LoggerFactory.getLogger(ReactiveStringBasedMongoQuery.class);
private static final ParameterBindingParser BINDING_PARSER = ParameterBindingParser.INSTANCE;
private static final ParameterBindingDocumentCodec CODEC = new ParameterBindingDocumentCodec();
private final String query;
private final String fieldSpec;
private final SpelExpressionParser expressionParser;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final boolean isCountQuery;
private final boolean isExistsQuery;
private final boolean isDeleteQuery;
private final List<ParameterBinding> queryParameterBindings;
private final List<ParameterBinding> fieldSpecParameterBindings;
private final ExpressionEvaluatingParameterBinder parameterBinder;
/**
* Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link MongoQueryMethod} and
@@ -85,13 +83,10 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
Assert.notNull(query, "Query must not be null!");
Assert.notNull(expressionParser, "SpelExpressionParser must not be null!");
this.queryParameterBindings = new ArrayList<ParameterBinding>();
this.query = BINDING_PARSER.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
this.queryParameterBindings);
this.fieldSpecParameterBindings = new ArrayList<ParameterBinding>();
this.fieldSpec = BINDING_PARSER.parseAndCollectParameterBindingsFromQueryIntoBindings(
method.getFieldSpecification(), this.fieldSpecParameterBindings);
this.query = query;
this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
this.fieldSpec = method.getFieldSpecification();
if (method.hasAnnotatedQuery()) {
@@ -111,8 +106,6 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
this.isExistsQuery = false;
this.isDeleteQuery = false;
}
this.parameterBinder = new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider);
}
/*
@@ -122,12 +115,13 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery {
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
String queryString = parameterBinder.bind(this.query, accessor,
new BindingContext(getQueryMethod().getParameters(), queryParameterBindings));
String fieldsString = parameterBinder.bind(this.fieldSpec, accessor,
new BindingContext(getQueryMethod().getParameters(), fieldSpecParameterBindings));
ParameterBindingContext bindingContext = new ParameterBindingContext((accessor::getBindableValue), expressionParser,
evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(), accessor.getValues()));
Query query = new BasicQuery(queryString, fieldsString).with(accessor.getSort());
Document queryObject = CODEC.decode(this.query, bindingContext);
Document fieldsObject = CODEC.decode(this.fieldSpec, bindingContext);
Query query = new BasicQuery(queryObject, fieldsObject).with(accessor.getSort());
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Created query %s for %s fields.", query.getQueryObject(), query.getFieldsObject()));

View File

@@ -15,31 +15,17 @@
*/
package org.springframework.data.mongodb.repository.query;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.bson.BSON;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.query.ExpressionEvaluatingParameterBinder.BindingContext;
import org.springframework.data.mongodb.util.json.ParameterBindingContext;
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.mongodb.DBObject;
import com.mongodb.DBRef;
import com.mongodb.util.JSON;
import com.mongodb.util.JSONCallback;
/**
* Query to use a plain JSON String to create the {@link Query} to actually execute.
@@ -53,16 +39,17 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
private static final String COUNT_EXISTS_AND_DELETE = "Manually defined query for %s cannot be a count and exists or delete query at the same time!";
private static final Logger LOG = LoggerFactory.getLogger(StringBasedMongoQuery.class);
private static final ParameterBindingParser BINDING_PARSER = ParameterBindingParser.INSTANCE;
private static final ParameterBindingDocumentCodec CODEC = new ParameterBindingDocumentCodec();
private final String query;
private final String fieldSpec;
private final SpelExpressionParser expressionParser;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final boolean isCountQuery;
private final boolean isExistsQuery;
private final boolean isDeleteQuery;
private final List<ParameterBinding> queryParameterBindings;
private final List<ParameterBinding> fieldSpecParameterBindings;
private final ExpressionEvaluatingParameterBinder parameterBinder;
/**
* Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod}, {@link MongoOperations},
@@ -95,15 +82,10 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
Assert.notNull(query, "Query must not be null!");
Assert.notNull(expressionParser, "SpelExpressionParser must not be null!");
this.queryParameterBindings = new ArrayList<ParameterBinding>();
this.query = BINDING_PARSER.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
this.queryParameterBindings);
this.fieldSpecParameterBindings = new ArrayList<ParameterBinding>();
this.fieldSpec = BINDING_PARSER.parseAndCollectParameterBindingsFromQueryIntoBindings(
method.getFieldSpecification(), this.fieldSpecParameterBindings);
this.parameterBinder = new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider);
this.query = query;
this.expressionParser = expressionParser;
this.evaluationContextProvider = evaluationContextProvider;
this.fieldSpec = method.getFieldSpecification();
if (method.hasAnnotatedQuery()) {
@@ -132,12 +114,13 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
String queryString = parameterBinder.bind(this.query, accessor,
new BindingContext(getQueryMethod().getParameters(), queryParameterBindings));
String fieldsString = parameterBinder.bind(this.fieldSpec, accessor,
new BindingContext(getQueryMethod().getParameters(), fieldSpecParameterBindings));
ParameterBindingContext bindingContext = new ParameterBindingContext((accessor::getBindableValue), expressionParser,
evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(), accessor.getValues()));
Query query = new BasicQuery(queryString, fieldsString).with(accessor.getSort());
Document queryObject = CODEC.decode(this.query, bindingContext);
Document fieldsObject = CODEC.decode(this.fieldSpec, bindingContext);
Query query = new BasicQuery(queryObject, fieldsObject).with(accessor.getSort());
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Created query %s for %s fields.", query.getQueryObject(), query.getFieldsObject()));
@@ -186,270 +169,4 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
boolean isDeleteQuery) {
return BooleanUtil.countBooleanTrueValues(isCountQuery, isExistsQuery, isDeleteQuery) > 1;
}
/**
* A parser that extracts the parameter bindings from a given query string.
*
* @author Thomas Darimont
*/
enum ParameterBindingParser {
INSTANCE;
private static final String EXPRESSION_PARAM_QUOTE = "'";
private static final String EXPRESSION_PARAM_PREFIX = "?expr";
private static final String INDEX_BASED_EXPRESSION_PARAM_START = "?#{";
private static final String NAME_BASED_EXPRESSION_PARAM_START = ":#{";
private static final char CURRLY_BRACE_OPEN = '{';
private static final char CURRLY_BRACE_CLOSE = '}';
private static final String PARAMETER_PREFIX = "_param_";
private static final String PARSEABLE_PARAMETER = "\"" + PARAMETER_PREFIX + "$1\"";
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
private static final Pattern PARSEABLE_BINDING_PATTERN = Pattern.compile("\"?" + PARAMETER_PREFIX + "(\\d+)\"?");
private final static int PARAMETER_INDEX_GROUP = 1;
/**
* Returns a list of {@link ParameterBinding}s found in the given {@code input} or an
* {@link Collections#emptyList()}.
*
* @param input can be empty.
* @param bindings must not be {@literal null}.
* @return
*/
public String parseAndCollectParameterBindingsFromQueryIntoBindings(String input, List<ParameterBinding> bindings) {
if (!StringUtils.hasText(input)) {
return input;
}
Assert.notNull(bindings, "Parameter bindings must not be null!");
String transformedInput = transformQueryAndCollectExpressionParametersIntoBindings(input, bindings);
String parseableInput = makeParameterReferencesParseable(transformedInput);
// Document.parse(parseableInput)
collectParameterReferencesIntoBindings(bindings,
JSON.parse(parseableInput, new LenientPatternDecodingCallback()));
return transformedInput;
}
private static String transformQueryAndCollectExpressionParametersIntoBindings(String input,
List<ParameterBinding> bindings) {
StringBuilder result = new StringBuilder();
int startIndex = 0;
int currentPos = 0;
int exprIndex = 0;
while (currentPos < input.length()) {
int indexOfExpressionParameter = getIndexOfExpressionParameter(input, currentPos);
// no expression parameter found
if (indexOfExpressionParameter < 0) {
break;
}
int exprStart = indexOfExpressionParameter + 3;
currentPos = exprStart;
// eat parameter expression
int curlyBraceOpenCnt = 1;
while (curlyBraceOpenCnt > 0) {
switch (input.charAt(currentPos++)) {
case CURRLY_BRACE_OPEN:
curlyBraceOpenCnt++;
break;
case CURRLY_BRACE_CLOSE:
curlyBraceOpenCnt--;
break;
default:
}
}
result.append(input.subSequence(startIndex, indexOfExpressionParameter));
result.append(EXPRESSION_PARAM_QUOTE).append(EXPRESSION_PARAM_PREFIX);
result.append(exprIndex);
result.append(EXPRESSION_PARAM_QUOTE);
bindings.add(new ParameterBinding(exprIndex, true, input.substring(exprStart, currentPos - 1)));
startIndex = currentPos;
exprIndex++;
}
return result.append(input.subSequence(currentPos, input.length())).toString();
}
private static String makeParameterReferencesParseable(String input) {
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(input);
return matcher.replaceAll(PARSEABLE_PARAMETER);
}
private static void collectParameterReferencesIntoBindings(List<ParameterBinding> bindings, Object value) {
if (value instanceof String) {
String string = ((String) value).trim();
potentiallyAddBinding(string, bindings);
} else if (value instanceof Pattern) {
String string = value.toString().trim();
Matcher valueMatcher = PARSEABLE_BINDING_PATTERN.matcher(string);
while (valueMatcher.find()) {
int paramIndex = Integer.parseInt(valueMatcher.group(PARAMETER_INDEX_GROUP));
/*
* The pattern is used as a direct parameter replacement, e.g. 'field': ?1,
* therefore we treat it as not quoted to remain backwards compatible.
*/
boolean quoted = !string.equals(PARAMETER_PREFIX + paramIndex);
bindings.add(new ParameterBinding(paramIndex, quoted));
}
} else if (value instanceof DBRef) {
DBRef dbref = (DBRef) value;
potentiallyAddBinding(dbref.getCollectionName(), bindings);
potentiallyAddBinding(dbref.getId().toString(), bindings);
} else if (value instanceof Document) {
Document document = (Document) value;
for (String field : document.keySet()) {
collectParameterReferencesIntoBindings(bindings, field);
collectParameterReferencesIntoBindings(bindings, document.get(field));
}
} else if (value instanceof DBObject) {
DBObject dbo = (DBObject) value;
for (String field : dbo.keySet()) {
collectParameterReferencesIntoBindings(bindings, field);
collectParameterReferencesIntoBindings(bindings, dbo.get(field));
}
}
}
private static void potentiallyAddBinding(String source, List<ParameterBinding> bindings) {
Matcher valueMatcher = PARSEABLE_BINDING_PATTERN.matcher(source);
while (valueMatcher.find()) {
int paramIndex = Integer.parseInt(valueMatcher.group(PARAMETER_INDEX_GROUP));
boolean quoted = source.startsWith("'") || source.startsWith("\"");
bindings.add(new ParameterBinding(paramIndex, quoted));
}
}
private static int getIndexOfExpressionParameter(String input, int position) {
int indexOfExpressionParameter = input.indexOf(INDEX_BASED_EXPRESSION_PARAM_START, position);
return indexOfExpressionParameter < 0 ? input.indexOf(NAME_BASED_EXPRESSION_PARAM_START, position)
: indexOfExpressionParameter;
}
}
/**
* {@link JSONCallback} with lenient handling for {@link PatternSyntaxException} falling back to a placeholder
* {@link Pattern} for intermediate query document rendering.
*/
private static class LenientPatternDecodingCallback extends JSONCallback {
private static final Pattern EMPTY_MARKER = Pattern.compile("__Spring_Data_MongoDB_Bind_Marker__");
/*
* (non-Javadoc)
* @see com.mongodb.util.JSONCallback#objectDone()
*/
@Override
public Object objectDone() {
return exceptionSwallowingStackReducingObjectDone();
}
private Object exceptionSwallowingStackReducingObjectDone/*CauseWeJustNeedTheStructureNotTheActualValue*/() {
Object value;
try {
return super.objectDone();
} catch (PatternSyntaxException e) {
value = EMPTY_MARKER;
}
if (!isStackEmpty()) {
_put(curName(), value);
} else {
value = !BSON.hasDecodeHooks() ? value : BSON.applyDecodingHooks(value);
setRoot(value);
}
return value;
}
}
/**
* A generic parameter binding with name or position information.
*
* @author Thomas Darimont
*/
static class ParameterBinding {
private final int parameterIndex;
private final boolean quoted;
private final @Nullable String expression;
/**
* Creates a new {@link ParameterBinding} with the given {@code parameterIndex} and {@code quoted} information.
*
* @param parameterIndex
* @param quoted whether or not the parameter is already quoted.
*/
public ParameterBinding(int parameterIndex, boolean quoted) {
this(parameterIndex, quoted, null);
}
public ParameterBinding(int parameterIndex, boolean quoted, @Nullable String expression) {
this.parameterIndex = parameterIndex;
this.quoted = quoted;
this.expression = expression;
}
public boolean isQuoted() {
return quoted;
}
public int getParameterIndex() {
return parameterIndex;
}
public String getParameter() {
return "?" + (isExpression() ? "expr" : "") + parameterIndex;
}
@Nullable
public String getExpression() {
return expression;
}
public boolean isExpression() {
return this.expression != null;
}
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.util.json;
import static java.time.format.DateTimeFormatter.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQuery;
import java.util.Calendar;
import java.util.TimeZone;
/**
* JsonBuffer implementation borrowed from <a href=
* "https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/main/org/bson/json/DateTimeFormatter.java">MongoDB
* Inc.</a> licensed under the Apache License, Version 2.0. <br />
* Formatted and modified.
*
* @since 2.2
* @author Jeff Yemin
* @author Ross Lawley
*/
class DateTimeFormatter {
private static final FormatterImpl FORMATTER_IMPL;
static {
FormatterImpl dateTimeHelper;
try {
dateTimeHelper = loadDateTimeFormatter("org.bson.json.DateTimeFormatter$Java8DateTimeFormatter");
} catch (LinkageError e) {
// this is expected if running on a release prior to Java 8: fallback to JAXB.
dateTimeHelper = loadDateTimeFormatter("org.bson.json.DateTimeFormatter$JaxbDateTimeFormatter");
}
FORMATTER_IMPL = dateTimeHelper;
}
private static FormatterImpl loadDateTimeFormatter(final String className) {
try {
return (FormatterImpl) Class.forName(className).getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException e) {
// this is unexpected as it means the class itself is not found
throw new ExceptionInInitializerError(e);
} catch (InstantiationException e) {
// this is unexpected as it means the class can't be instantiated
throw new ExceptionInInitializerError(e);
} catch (IllegalAccessException e) {
// this is unexpected as it means the no-args constructor isn't accessible
throw new ExceptionInInitializerError(e);
} catch (NoSuchMethodException e) {
throw new ExceptionInInitializerError(e);
} catch (InvocationTargetException e) {
throw new ExceptionInInitializerError(e);
}
}
static long parse(final String dateTimeString) {
return FORMATTER_IMPL.parse(dateTimeString);
}
static String format(final long dateTime) {
return FORMATTER_IMPL.format(dateTime);
}
private interface FormatterImpl {
long parse(String dateTimeString);
String format(long dateTime);
}
// Reflective use of DatatypeConverter avoids a compile-time dependency on the java.xml.bind module in Java 9
static class JaxbDateTimeFormatter implements FormatterImpl {
private static final Method DATATYPE_CONVERTER_PARSE_DATE_TIME_METHOD;
private static final Method DATATYPE_CONVERTER_PRINT_DATE_TIME_METHOD;
static {
try {
DATATYPE_CONVERTER_PARSE_DATE_TIME_METHOD = Class.forName("javax.xml.bind.DatatypeConverter")
.getDeclaredMethod("parseDateTime", String.class);
DATATYPE_CONVERTER_PRINT_DATE_TIME_METHOD = Class.forName("javax.xml.bind.DatatypeConverter")
.getDeclaredMethod("printDateTime", Calendar.class);
} catch (NoSuchMethodException e) {
throw new ExceptionInInitializerError(e);
} catch (ClassNotFoundException e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public long parse(final String dateTimeString) {
try {
return ((Calendar) DATATYPE_CONVERTER_PARSE_DATE_TIME_METHOD.invoke(null, dateTimeString)).getTimeInMillis();
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw (RuntimeException) e.getCause();
}
}
@Override
public String format(final long dateTime) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(dateTime);
calendar.setTimeZone(TimeZone.getTimeZone("Z"));
try {
return (String) DATATYPE_CONVERTER_PRINT_DATE_TIME_METHOD.invoke(null, calendar);
} catch (IllegalAccessException e) {
throw new IllegalStateException();
} catch (InvocationTargetException e) {
throw (RuntimeException) e.getCause();
}
}
}
static class Java8DateTimeFormatter implements FormatterImpl {
// if running on Java 8 or above then java.time.format.DateTimeFormatter will be available and initialization will
// succeed.
// Otherwise it will fail.
static {
try {
Class.forName("java.time.format.DateTimeFormatter");
} catch (ClassNotFoundException e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public long parse(final String dateTimeString) {
try {
return ISO_OFFSET_DATE_TIME.parse(dateTimeString, new TemporalQuery<Instant>() {
@Override
public Instant queryFrom(final TemporalAccessor temporal) {
return Instant.from(temporal);
}
}).toEpochMilli();
} catch (DateTimeParseException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
@Override
public String format(final long dateTime) {
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateTime), ZoneId.of("Z")).format(ISO_OFFSET_DATE_TIME);
}
}
private DateTimeFormatter() {}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.util.json;
import org.bson.json.JsonParseException;
/**
* JsonBuffer implementation borrowed from <a href=
* "https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/main/org/bson/json/JsonBuffer.java">MongoDB
* Inc.</a> licensed under the Apache License, Version 2.0. <br />
* Formatted and modified.
*
* @since 2.2
* @author Jeff Yemin
* @author Ross Lawley
*/
class JsonBuffer {
private final String buffer;
private int position;
private boolean eof;
JsonBuffer(final String buffer) {
this.buffer = buffer;
}
public int getPosition() {
return position;
}
public void setPosition(final int position) {
this.position = position;
}
public int read() {
if (eof) {
throw new JsonParseException("Trying to read past EOF.");
} else if (position >= buffer.length()) {
eof = true;
return -1;
} else {
return buffer.charAt(position++);
}
}
public void unread(final int c) {
eof = false;
if (c != -1 && buffer.charAt(position - 1) == c) {
position--;
}
}
public String substring(final int beginIndex) {
return buffer.substring(beginIndex);
}
public String substring(final int beginIndex, final int endIndex) {
return buffer.substring(beginIndex, endIndex);
}
}

View File

@@ -0,0 +1,623 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.util.json;
import org.bson.BsonRegularExpression;
import org.bson.json.JsonParseException;
/**
* Parses the string representation of a JSON object into a set of {@link JsonToken}-derived objects. <br />
* JsonScanner implementation borrowed from <a href=
* "https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/main/org/bson/json/JsonScanner.java">MongoDB
* Inc.</a> licensed under the Apache License, Version 2.0. <br />
* Formatted and modified to allow reading Spring Data specific placeholder values.
*
* @since 2.2
* @author Jeff Yemin
* @author Trisha Gee
* @author Robert Guo
* @author Ross Lawley
* @author Christoph Strobl
*/
class JsonScanner {
private final JsonBuffer buffer;
JsonScanner(final String json) {
this(new JsonBuffer(json));
}
JsonScanner(final JsonBuffer buffer) {
this.buffer = buffer;
}
/**
* @param newPosition the new position of the cursor position in the buffer
*/
public void setBufferPosition(final int newPosition) {
buffer.setPosition(newPosition);
}
/**
* @return the current location of the cursor in the buffer
*/
public int getBufferPosition() {
return buffer.getPosition();
}
/**
* Finds and returns the next complete token from this scanner. If scanner reached the end of the source, it will
* return a token with {@code JSONTokenType.END_OF_FILE} type.
*
* @return The next token.
* @throws JsonParseException if source is invalid.
*/
public JsonToken nextToken() {
int c = buffer.read();
while (c != -1 && Character.isWhitespace(c)) {
c = buffer.read();
}
if (c == -1) {
return new JsonToken(JsonTokenType.END_OF_FILE, "<eof>");
}
switch (c) {
case '{':
return new JsonToken(JsonTokenType.BEGIN_OBJECT, "{");
case '}':
return new JsonToken(JsonTokenType.END_OBJECT, "}");
case '[':
return new JsonToken(JsonTokenType.BEGIN_ARRAY, "[");
case ']':
return new JsonToken(JsonTokenType.END_ARRAY, "]");
case '(':
return new JsonToken(JsonTokenType.LEFT_PAREN, "(");
case ')':
return new JsonToken(JsonTokenType.RIGHT_PAREN, ")");
case ':':
c = buffer.read();
buffer.unread(c);
if (c == '#') { // for binding the SQL style ':#{#firstname}"'
return scanBindString();
}
return new JsonToken(JsonTokenType.COLON, ":");
case ',':
return new JsonToken(JsonTokenType.COMMA, ",");
case '\'':
case '"':
return scanString((char) c);
case '/':
return scanRegularExpression();
default:
if (c == '-' || Character.isDigit(c)) {
return scanNumber((char) c);
} else if (c == '$' || c == '_' || Character.isLetter(c)) {
return scanUnquotedString();
} else if (c == '?') { // for binding parameters. Both simple and SpEL ones.
return scanBindString();
} else {
int position = buffer.getPosition();
buffer.unread(c);
throw new JsonParseException("Invalid JSON input. Position: %d. Character: '%c'.", position, c);
}
}
}
/**
* Reads {@code RegularExpressionToken} from source. The following variants of lexemes are possible:
*
* <pre>
* /pattern/
* /\(pattern\)/
* /pattern/ims
* </pre>
*
* Options can include 'i','m','x','s'
*
* @return The regular expression token.
* @throws JsonParseException if regular expression representation is not valid.
*/
private JsonToken scanRegularExpression() {
int start = buffer.getPosition() - 1;
int options = -1;
RegularExpressionState state = RegularExpressionState.IN_PATTERN;
while (true) {
int c = buffer.read();
switch (state) {
case IN_PATTERN:
switch (c) {
case -1:
state = RegularExpressionState.INVALID;
break;
case '/':
state = RegularExpressionState.IN_OPTIONS;
options = buffer.getPosition();
break;
case '\\':
state = RegularExpressionState.IN_ESCAPE_SEQUENCE;
break;
default:
state = RegularExpressionState.IN_PATTERN;
break;
}
break;
case IN_ESCAPE_SEQUENCE:
state = RegularExpressionState.IN_PATTERN;
break;
case IN_OPTIONS:
switch (c) {
case 'i':
case 'm':
case 'x':
case 's':
state = RegularExpressionState.IN_OPTIONS;
break;
case ',':
case '}':
case ']':
case ')':
case -1:
state = RegularExpressionState.DONE;
break;
default:
if (Character.isWhitespace(c)) {
state = RegularExpressionState.DONE;
} else {
state = RegularExpressionState.INVALID;
}
break;
}
break;
default:
break;
}
switch (state) {
case DONE:
buffer.unread(c);
int end = buffer.getPosition();
BsonRegularExpression regex = new BsonRegularExpression(buffer.substring(start + 1, options - 1),
buffer.substring(options, end));
return new JsonToken(JsonTokenType.REGULAR_EXPRESSION, regex);
case INVALID:
throw new JsonParseException("Invalid JSON regular expression. Position: %d.", buffer.getPosition());
default:
}
}
}
/**
* Reads {@code StringToken} from source.
*
* @return The string token.
*/
private JsonToken scanBindString() {
int start = buffer.getPosition() - 1;
int c = buffer.read();
int charCount = 0;
boolean isExpression = false;
int parenthesisCount = 0;
while (c == '$' || c == '_' || Character.isLetterOrDigit(c) || c == '#' || c == '{' || c == '[' || c == ']'
|| (isExpression && isExpressionAllowedChar(c))) {
if (charCount == 0 && c == '#') {
isExpression = true;
} else if (isExpression) {
if (c == '{') {
parenthesisCount++;
} else if (c == '}') {
parenthesisCount--;
if (parenthesisCount == 0) {
buffer.read();
break;
}
}
}
charCount++;
c = buffer.read();
}
buffer.unread(c);
String lexeme = buffer.substring(start, buffer.getPosition());
return new JsonToken(JsonTokenType.UNQUOTED_STRING, lexeme);
}
private static boolean isExpressionAllowedChar(int c) {
return (c == '+' || //
c == '-' || //
c == ':' || //
c == '.' || //
c == ',' || //
c == '*' || //
c == '/' || //
c == '%' || //
c == '(' || //
c == ')' || //
c == '[' || //
c == ']' || //
c == '#' || //
c == '{' || //
c == '}' || //
c == '@' || //
c == '^' || //
c == '!' || //
c == '=' || //
c == '&' || //
c == '|' || //
c == '?' || //
c == '$' || //
c == '>' || //
c == '<' || //
c == '"' || //
c == '\'' || //
c == ' ');
}
/**
* Reads {@code StringToken} from source.
*
* @return The string token.
*/
private JsonToken scanUnquotedString() {
int start = buffer.getPosition() - 1;
int c = buffer.read();
while (c == '$' || c == '_' || Character.isLetterOrDigit(c)) {
c = buffer.read();
}
buffer.unread(c);
String lexeme = buffer.substring(start, buffer.getPosition());
return new JsonToken(JsonTokenType.UNQUOTED_STRING, lexeme);
}
/**
* Reads number token from source. The following variants of lexemes are possible:
*
* <pre>
* 12
* 123
* -0
* -345
* -0.0
* 0e1
* 0e-1
* -0e-1
* 1e12
* -Infinity
* </pre>
*
* @return The number token.
* @throws JsonParseException if number representation is invalid.
*/
// CHECKSTYLE:OFF
private JsonToken scanNumber(final char firstChar) {
int c = firstChar;
int start = buffer.getPosition() - 1;
NumberState state;
switch (c) {
case '-':
state = NumberState.SAW_LEADING_MINUS;
break;
case '0':
state = NumberState.SAW_LEADING_ZERO;
break;
default:
state = NumberState.SAW_INTEGER_DIGITS;
break;
}
JsonTokenType type = JsonTokenType.INT64;
while (true) {
c = buffer.read();
switch (state) {
case SAW_LEADING_MINUS:
switch (c) {
case '0':
state = NumberState.SAW_LEADING_ZERO;
break;
case 'I':
state = NumberState.SAW_MINUS_I;
break;
default:
if (Character.isDigit(c)) {
state = NumberState.SAW_INTEGER_DIGITS;
} else {
state = NumberState.INVALID;
}
break;
}
break;
case SAW_LEADING_ZERO:
switch (c) {
case '.':
state = NumberState.SAW_DECIMAL_POINT;
break;
case 'e':
case 'E':
state = NumberState.SAW_EXPONENT_LETTER;
break;
case ',':
case '}':
case ']':
case ')':
case -1:
state = NumberState.DONE;
break;
default:
if (Character.isDigit(c)) {
state = NumberState.SAW_INTEGER_DIGITS;
} else if (Character.isWhitespace(c)) {
state = NumberState.DONE;
} else {
state = NumberState.INVALID;
}
break;
}
break;
case SAW_INTEGER_DIGITS:
switch (c) {
case '.':
state = NumberState.SAW_DECIMAL_POINT;
break;
case 'e':
case 'E':
state = NumberState.SAW_EXPONENT_LETTER;
break;
case ',':
case '}':
case ']':
case ')':
case -1:
state = NumberState.DONE;
break;
default:
if (Character.isDigit(c)) {
state = NumberState.SAW_INTEGER_DIGITS;
} else if (Character.isWhitespace(c)) {
state = NumberState.DONE;
} else {
state = NumberState.INVALID;
}
break;
}
break;
case SAW_DECIMAL_POINT:
type = JsonTokenType.DOUBLE;
if (Character.isDigit(c)) {
state = NumberState.SAW_FRACTION_DIGITS;
} else {
state = NumberState.INVALID;
}
break;
case SAW_FRACTION_DIGITS:
switch (c) {
case 'e':
case 'E':
state = NumberState.SAW_EXPONENT_LETTER;
break;
case ',':
case '}':
case ']':
case ')':
case -1:
state = NumberState.DONE;
break;
default:
if (Character.isDigit(c)) {
state = NumberState.SAW_FRACTION_DIGITS;
} else if (Character.isWhitespace(c)) {
state = NumberState.DONE;
} else {
state = NumberState.INVALID;
}
break;
}
break;
case SAW_EXPONENT_LETTER:
type = JsonTokenType.DOUBLE;
switch (c) {
case '+':
case '-':
state = NumberState.SAW_EXPONENT_SIGN;
break;
default:
if (Character.isDigit(c)) {
state = NumberState.SAW_EXPONENT_DIGITS;
} else {
state = NumberState.INVALID;
}
break;
}
break;
case SAW_EXPONENT_SIGN:
if (Character.isDigit(c)) {
state = NumberState.SAW_EXPONENT_DIGITS;
} else {
state = NumberState.INVALID;
}
break;
case SAW_EXPONENT_DIGITS:
switch (c) {
case ',':
case '}':
case ']':
case ')':
state = NumberState.DONE;
break;
default:
if (Character.isDigit(c)) {
state = NumberState.SAW_EXPONENT_DIGITS;
} else if (Character.isWhitespace(c)) {
state = NumberState.DONE;
} else {
state = NumberState.INVALID;
}
break;
}
break;
case SAW_MINUS_I:
boolean sawMinusInfinity = true;
char[] nfinity = new char[] { 'n', 'f', 'i', 'n', 'i', 't', 'y' };
for (int i = 0; i < nfinity.length; i++) {
if (c != nfinity[i]) {
sawMinusInfinity = false;
break;
}
c = buffer.read();
}
if (sawMinusInfinity) {
type = JsonTokenType.DOUBLE;
switch (c) {
case ',':
case '}':
case ']':
case ')':
case -1:
state = NumberState.DONE;
break;
default:
if (Character.isWhitespace(c)) {
state = NumberState.DONE;
} else {
state = NumberState.INVALID;
}
break;
}
} else {
state = NumberState.INVALID;
}
break;
default:
}
switch (state) {
case INVALID:
throw new JsonParseException("Invalid JSON number");
case DONE:
buffer.unread(c);
String lexeme = buffer.substring(start, buffer.getPosition());
if (type == JsonTokenType.DOUBLE) {
return new JsonToken(JsonTokenType.DOUBLE, Double.parseDouble(lexeme));
} else {
long value = Long.parseLong(lexeme);
if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
return new JsonToken(JsonTokenType.INT64, value);
} else {
return new JsonToken(JsonTokenType.INT32, (int) value);
}
}
default:
}
}
}
// CHECKSTYLE:ON
/**
* Reads {@code StringToken} from source.
*
* @return The string token.
*/
// CHECKSTYLE:OFF
private JsonToken scanString(final char quoteCharacter) {
StringBuilder sb = new StringBuilder();
while (true) {
int c = buffer.read();
switch (c) {
case '\\':
c = buffer.read();
switch (c) {
case '\'':
sb.append('\'');
break;
case '"':
sb.append('"');
break;
case '\\':
sb.append('\\');
break;
case '/':
sb.append('/');
break;
case 'b':
sb.append('\b');
break;
case 'f':
sb.append('\f');
break;
case 'n':
sb.append('\n');
break;
case 'r':
sb.append('\r');
break;
case 't':
sb.append('\t');
break;
case 'u':
int u1 = buffer.read();
int u2 = buffer.read();
int u3 = buffer.read();
int u4 = buffer.read();
if (u4 != -1) {
String hex = new String(new char[] { (char) u1, (char) u2, (char) u3, (char) u4 });
sb.append((char) Integer.parseInt(hex, 16));
}
break;
default:
throw new JsonParseException("Invalid escape sequence in JSON string '\\%c'.", c);
}
break;
default:
if (c == quoteCharacter) {
return new JsonToken(JsonTokenType.STRING, sb.toString());
}
if (c != -1) {
sb.append((char) c);
}
}
if (c == -1) {
throw new JsonParseException("End of file in JSON string.");
}
}
}
private enum NumberState {
SAW_LEADING_MINUS, SAW_LEADING_ZERO, SAW_INTEGER_DIGITS, SAW_DECIMAL_POINT, SAW_FRACTION_DIGITS, SAW_EXPONENT_LETTER, SAW_EXPONENT_SIGN, SAW_EXPONENT_DIGITS, SAW_MINUS_I, DONE, INVALID
}
private enum RegularExpressionState {
IN_PATTERN, IN_ESCAPE_SEQUENCE, IN_OPTIONS, DONE, INVALID
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.util.json;
import static java.lang.String.*;
import org.bson.BsonDouble;
import org.bson.json.JsonParseException;
import org.bson.types.Decimal128;
/**
* JsonToken implementation borrowed from <a href=
* "https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/main/org/bson/json/JsonToken.java">MongoDB
* Inc.</a> licensed under the Apache License, Version 2.0. <br />
*
* @since 2.2
* @author Jeff Yemin
* @author Ross Lawley
*/
class JsonToken {
private final Object value;
private final JsonTokenType type;
JsonToken(final JsonTokenType type, final Object value) {
this.value = value;
this.type = type;
}
Object getValue() {
return value;
}
<T> T getValue(final Class<T> clazz) {
try {
if (Long.class == clazz) {
if (value instanceof Integer) {
return clazz.cast(((Integer) value).longValue());
} else if (value instanceof String) {
return clazz.cast(Long.valueOf((String) value));
}
} else if (Integer.class == clazz) {
if (value instanceof String) {
return clazz.cast(Integer.valueOf((String) value));
}
} else if (Double.class == clazz) {
if (value instanceof String) {
return clazz.cast(Double.valueOf((String) value));
}
} else if (Decimal128.class == clazz) {
if (value instanceof Integer) {
return clazz.cast(new Decimal128((Integer) value));
} else if (value instanceof Long) {
return clazz.cast(new Decimal128((Long) value));
} else if (value instanceof Double) {
return clazz.cast(new BsonDouble((Double) value).decimal128Value());
} else if (value instanceof String) {
return clazz.cast(Decimal128.parse((String) value));
}
}
return clazz.cast(value);
} catch (Exception e) {
throw new JsonParseException(format("Exception converting value '%s' to type %s", value, clazz.getName()), e);
}
}
public JsonTokenType getType() {
return type;
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.util.json;
/**
* JsonTokenType implementation borrowed from <a href=
* "https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/main/org/bson/json/JsonTokenType.java">MongoDB
* Inc.</a> licensed under the Apache License, Version 2.0. <br />
*
* @since 2.2
* @author Jeff Yemin
* @author Ross Lawley
*/
enum JsonTokenType {
/**
* An invalid token.
*/
INVALID,
/**
* A begin array token (a '[').
*/
BEGIN_ARRAY,
/**
* A begin object token (a '{').
*/
BEGIN_OBJECT,
/**
* An end array token (a ']').
*/
END_ARRAY,
/**
* A left parenthesis (a '(').
*/
LEFT_PAREN,
/**
* A right parenthesis (a ')').
*/
RIGHT_PAREN,
/**
* An end object token (a '}').
*/
END_OBJECT,
/**
* A colon token (a ':').
*/
COLON,
/**
* A comma token (a ',').
*/
COMMA,
/**
* A Double token.
*/
DOUBLE,
/**
* An Int32 token.
*/
INT32,
/**
* And Int64 token.
*/
INT64,
/**
* A regular expression token.
*/
REGULAR_EXPRESSION,
/**
* A string token.
*/
STRING,
/**
* An unquoted string token.
*/
UNQUOTED_STRING,
/**
* An end of file token.
*/
END_OF_FILE
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.util.json;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
/**
* Reusable context for binding parameters to an placeholder or a SpEL expression within a JSON structure. <br />
* To be used along with {@link ParameterBindingDocumentCodec#decode(String, ParameterBindingContext)}.
*
* @author Christoph Strobl
* @since 2.2
*/
@RequiredArgsConstructor
@Getter
public class ParameterBindingContext {
private final ValueProvider valueProvider;
private final SpelExpressionParser expressionParser;
private final EvaluationContext evaluationContext;
@Nullable
public Object bindableValueForIndex(int index) {
return valueProvider.getBindableValue(index);
}
@Nullable
public Object evaluateExpression(String expressionString) {
Expression expression = expressionParser.parseExpression(expressionString);
return expression.getValue(this.evaluationContext, Object.class);
}
}

View File

@@ -0,0 +1,309 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.util.json;
import static java.util.Arrays.*;
import static org.bson.assertions.Assertions.*;
import static org.bson.codecs.configuration.CodecRegistries.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.bson.AbstractBsonReader.State;
import org.bson.BsonBinarySubType;
import org.bson.BsonDocument;
import org.bson.BsonDocumentWriter;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonValue;
import org.bson.BsonWriter;
import org.bson.Document;
import org.bson.Transformer;
import org.bson.codecs.*;
import org.bson.codecs.configuration.CodecRegistry;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* A {@link Codec} implementation that allows binding parameters to placeholders or SpEL expressions when decoding a
* JSON String. <br />
* Modified version of <a href=
* "https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/main/org/bson/codecs/DocumentCodec.java">MongoDB
* Inc. DocumentCodec</a> licensed under the Apache License, Version 2.0. <br />
*
* @since 2.2
* @author Jeff Yemin
* @author Ross Lawley
* @author Ralph Schaer
* @author Christoph Strobl
*/
public class ParameterBindingDocumentCodec implements CollectibleCodec<Document> {
private static final String ID_FIELD_NAME = "_id";
private static final CodecRegistry DEFAULT_REGISTRY = fromProviders(
asList(new ValueCodecProvider(), new BsonValueCodecProvider(), new DocumentCodecProvider()));
private static final BsonTypeClassMap DEFAULT_BSON_TYPE_CLASS_MAP = new BsonTypeClassMap();
private final BsonTypeCodecMap bsonTypeCodecMap;
private final CodecRegistry registry;
private final IdGenerator idGenerator;
private final Transformer valueTransformer;
/**
* Construct a new instance with a default {@code CodecRegistry}.
*/
public ParameterBindingDocumentCodec() {
this(DEFAULT_REGISTRY);
}
/**
* Construct a new instance with the given registry.
*
* @param registry the registry
* @since 3.5
*/
public ParameterBindingDocumentCodec(final CodecRegistry registry) {
this(registry, DEFAULT_BSON_TYPE_CLASS_MAP);
}
/**
* Construct a new instance with the given registry and BSON type class map.
*
* @param registry the registry
* @param bsonTypeClassMap the BSON type class map
*/
public ParameterBindingDocumentCodec(final CodecRegistry registry, final BsonTypeClassMap bsonTypeClassMap) {
this(registry, bsonTypeClassMap, null);
}
/**
* Construct a new instance with the given registry and BSON type class map. The transformer is applied as a last step
* when decoding values, which allows users of this codec to control the decoding process. For example, a user of this
* class could substitute a value decoded as a Document with an instance of a special purpose class (e.g., one
* representing a DBRef in MongoDB).
*
* @param registry the registry
* @param bsonTypeClassMap the BSON type class map
* @param valueTransformer the value transformer to use as a final step when decoding the value of any field in the
* document
*/
public ParameterBindingDocumentCodec(final CodecRegistry registry, final BsonTypeClassMap bsonTypeClassMap,
final Transformer valueTransformer) {
this.registry = notNull("registry", registry);
this.bsonTypeCodecMap = new BsonTypeCodecMap(notNull("bsonTypeClassMap", bsonTypeClassMap), registry);
this.idGenerator = new ObjectIdGenerator();
this.valueTransformer = valueTransformer != null ? valueTransformer : new Transformer() {
@Override
public Object transform(final Object value) {
return value;
}
};
}
@Override
public boolean documentHasId(final Document document) {
return document.containsKey(ID_FIELD_NAME);
}
@Override
public BsonValue getDocumentId(final Document document) {
if (!documentHasId(document)) {
throw new IllegalStateException("The document does not contain an _id");
}
Object id = document.get(ID_FIELD_NAME);
if (id instanceof BsonValue) {
return (BsonValue) id;
}
BsonDocument idHoldingDocument = new BsonDocument();
BsonWriter writer = new BsonDocumentWriter(idHoldingDocument);
writer.writeStartDocument();
writer.writeName(ID_FIELD_NAME);
writeValue(writer, EncoderContext.builder().build(), id);
writer.writeEndDocument();
return idHoldingDocument.get(ID_FIELD_NAME);
}
@Override
public Document generateIdIfAbsentFromDocument(final Document document) {
if (!documentHasId(document)) {
document.put(ID_FIELD_NAME, idGenerator.generate());
}
return document;
}
@Override
public void encode(final BsonWriter writer, final Document document, final EncoderContext encoderContext) {
writeMap(writer, document, encoderContext);
}
public Document decode(@Nullable String json, Object[] values) {
return decode(json, new ParameterBindingContext((index) -> values[index], new SpelExpressionParser(),
EvaluationContextProvider.DEFAULT.getEvaluationContext(values)));
}
public Document decode(@Nullable String json, ParameterBindingContext bindingContext) {
if (StringUtils.isEmpty(json)) {
return new Document();
}
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json, bindingContext);
return this.decode(reader, DecoderContext.builder().build());
}
@Override
public Document decode(final BsonReader reader, final DecoderContext decoderContext) {
if (reader instanceof ParameterBindingJsonReader) {
ParameterBindingJsonReader bindingReader = (ParameterBindingJsonReader) reader;
// check if the reader has actually found something to replace on top level and did so.
// binds just placeholder queries like: `@Query(?0)`
if (bindingReader.currentValue instanceof org.bson.Document) {
return (Document) bindingReader.currentValue;
}
}
Document document = new Document();
reader.readStartDocument();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
String fieldName = reader.readName();
document.put(fieldName, readValue(reader, decoderContext));
}
reader.readEndDocument();
return document;
}
@Override
public Class<Document> getEncoderClass() {
return Document.class;
}
private void beforeFields(final BsonWriter bsonWriter, final EncoderContext encoderContext,
final Map<String, Object> document) {
if (encoderContext.isEncodingCollectibleDocument() && document.containsKey(ID_FIELD_NAME)) {
bsonWriter.writeName(ID_FIELD_NAME);
writeValue(bsonWriter, encoderContext, document.get(ID_FIELD_NAME));
}
}
private boolean skipField(final EncoderContext encoderContext, final String key) {
return encoderContext.isEncodingCollectibleDocument() && key.equals(ID_FIELD_NAME);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void writeValue(final BsonWriter writer, final EncoderContext encoderContext, final Object value) {
if (value == null) {
writer.writeNull();
} else if (value instanceof Iterable) {
writeIterable(writer, (Iterable<Object>) value, encoderContext.getChildContext());
} else if (value instanceof Map) {
writeMap(writer, (Map<String, Object>) value, encoderContext.getChildContext());
} else {
Codec codec = registry.get(value.getClass());
encoderContext.encodeWithChildContext(codec, writer, value);
}
}
private void writeMap(final BsonWriter writer, final Map<String, Object> map, final EncoderContext encoderContext) {
writer.writeStartDocument();
beforeFields(writer, encoderContext, map);
for (final Map.Entry<String, Object> entry : map.entrySet()) {
if (skipField(encoderContext, entry.getKey())) {
continue;
}
writer.writeName(entry.getKey());
writeValue(writer, encoderContext, entry.getValue());
}
writer.writeEndDocument();
}
private void writeIterable(final BsonWriter writer, final Iterable<Object> list,
final EncoderContext encoderContext) {
writer.writeStartArray();
for (final Object value : list) {
writeValue(writer, encoderContext, value);
}
writer.writeEndArray();
}
private Object readValue(final BsonReader reader, final DecoderContext decoderContext) {
if (reader instanceof ParameterBindingJsonReader) {
ParameterBindingJsonReader bindingReader = (ParameterBindingJsonReader) reader;
// check if the reader has actually found something to replaceand did so.
// resets the reader state to move on after the actual value
// returns the replacement value
if (bindingReader.currentValue != null) {
Object value = bindingReader.currentValue;
bindingReader.setState(State.TYPE);
bindingReader.currentValue = null;
return value;
}
}
BsonType bsonType = reader.getCurrentBsonType();
if (bsonType == BsonType.NULL) {
reader.readNull();
return null;
} else if (bsonType == BsonType.ARRAY) {
return readList(reader, decoderContext);
} else if (bsonType == BsonType.BINARY && BsonBinarySubType.isUuid(reader.peekBinarySubType())
&& reader.peekBinarySize() == 16) {
return registry.get(UUID.class).decode(reader, decoderContext);
}
// By default the registry uses DocumentCodec for parsing.
// We need to reroute that to our very own implementation or we'll end up only mapping half the placeholders.
Codec<?> codecToUse = bsonTypeCodecMap.get(bsonType);
if (codecToUse instanceof org.bson.codecs.DocumentCodec) {
codecToUse = this;
}
return valueTransformer.transform(codecToUse.decode(reader, decoderContext));
}
private List<Object> readList(final BsonReader reader, final DecoderContext decoderContext) {
reader.readStartArray();
List<Object> list = new ArrayList<>();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
Object listValue = readValue(reader, decoderContext);
if (listValue instanceof Collection) {
list.addAll((Collection) listValue);
break;
}
list.add(listValue);
}
reader.readEndArray();
return list;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.util.json;
import org.springframework.lang.Nullable;
/**
* A value provider to retrieve bindable values by their parameter index.
*
* @author Christoph Strobl
* @since 2.2
*/
@FunctionalInterface
public interface ValueProvider {
/**
* @param index parameter index to use.
* @return can be {@literal null}.
* @throws RuntimeException if the requested element does not exist.
*/
@Nullable
Object getBindableValue(int index);
}

View File

@@ -327,7 +327,7 @@ public class StringBasedMongoQueryUnitTests {
byte[] binaryData = "Matthews".getBytes(StandardCharsets.UTF_8);
ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter,
(Object) Arrays.asList(binaryData));
(Object) Collections.singletonList(binaryData));
StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameAsBinaryIn", List.class);
org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor);

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2019. the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.util.json;
import static org.assertj.core.api.Assertions.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.bson.Document;
import org.bson.codecs.DecoderContext;
import org.junit.Test;
/**
* @author Christoph Strobl
* @since 2019/02
*/
public class ParameterBindingJsonReaderUnitTests {
@Test
public void bindUnquotedStringValue() {
Document target = parse("{ 'lastname' : ?0 }", "kohlin");
assertThat(target).isEqualTo(new Document("lastname", "kohlin"));
}
@Test
public void bindQuotedStringValue() {
Document target = parse("{ 'lastname' : '?0' }", "kohlin");
assertThat(target).isEqualTo(new Document("lastname", "kohlin"));
}
@Test
public void bindUnquotedIntegerValue() {
Document target = parse("{ 'lastname' : ?0 } ", 100);
assertThat(target).isEqualTo(new Document("lastname", 100));
}
@Test
public void bindMultiplePlacholders() {
Document target = parse("{ 'lastname' : ?0, 'firstname' : '?1' }", "Kohlin", "Dalinar");
assertThat(target).isEqualTo(Document.parse("{ 'lastname' : 'Kohlin', 'firstname' : 'Dalinar' }"));
}
@Test
public void bindQuotedIntegerValue() {
Document target = parse("{ 'lastname' : '?0' }", 100);
assertThat(target).isEqualTo(new Document("lastname", "100"));
}
@Test
public void bindValueToRegex() {
Document target = parse("{ 'lastname' : { '$regex' : '^(?0)'} }", "kohlin");
assertThat(target).isEqualTo(Document.parse("{ 'lastname' : { '$regex' : '^(kohlin)'} }"));
}
@Test
public void bindValueToMultiRegex() {
Document target = parse(
"{'$or' : [{'firstname': {'$regex': '.*?0.*', '$options': 'i'}}, {'lastname' : {'$regex': '.*?0xyz.*', '$options': 'i'}} ]}",
"calamity");
assertThat(target).isEqualTo(Document.parse(
"{ \"$or\" : [ { \"firstname\" : { \"$regex\" : \".*calamity.*\" , \"$options\" : \"i\"}} , { \"lastname\" : { \"$regex\" : \".*calamityxyz.*\" , \"$options\" : \"i\"}}]}"));
}
@Test
public void bindMultipleValuesToSingleToken() {
Document target = parse("{$where: 'return this.date.getUTCMonth() == ?2 && this.date.getUTCDay() == ?3;'}", 0, 1, 2,
3, 4);
assertThat(target)
.isEqualTo(Document.parse("{$where: 'return this.date.getUTCMonth() == 2 && this.date.getUTCDay() == 3;'}"));
}
@Test
public void bindValueToDbRef() {
Document target = parse("{ 'reference' : { $ref : 'reference', $id : ?0 }}", "kohlin");
assertThat(target).isEqualTo(Document.parse("{ 'reference' : { $ref : 'reference', $id : 'kohlin' }}"));
}
@Test
public void bindToKey() {
Document target = parse("{ ?0 : ?1 }", "firstname", "kaladin");
assertThat(target).isEqualTo(Document.parse("{ 'firstname' : 'kaladin' }"));
}
@Test
public void bindListValue() {
//
Document target = parse("{ 'lastname' : { $in : ?0 } }", Arrays.asList("Kohlin", "Davar"));
assertThat(target).isEqualTo(Document.parse("{ 'lastname' : { $in : ['Kohlin', 'Davar' ]} }"));
}
@Test
public void bindListOfBinaryValue() {
//
byte[] value = "Kohlin".getBytes(StandardCharsets.UTF_8);
List<byte[]> args = Collections.singletonList(value);
Document target = parse("{ 'lastname' : { $in : ?0 } }",args);
assertThat(target).isEqualTo(new Document("lastname", new Document("$in", args)));
}
@Test
public void bindExtendedExpression() {
Document target = parse("{'id':?#{ [0] ? { $exists :true} : [1] }}", true, "firstname", "kaladin");
assertThat(target).isEqualTo(Document.parse("{ \"id\" : { \"$exists\" : true}}"));
}
// {'id':?#{ [0] ? { $exists :true} : [1] }}
@Test
public void bindDocumentValue() {
//
Document target = parse("{ 'lastname' : ?0 }", new Document("$eq", "Kohlin"));
assertThat(target).isEqualTo(Document.parse("{ 'lastname' : { '$eq' : 'Kohlin' } }"));
}
@Test
public void arrayWithoutBinding() {
//
Document target = parse("{ 'lastname' : { $in : [\"Kohlin\", \"Davar\"] } }");
assertThat(target).isEqualTo(Document.parse("{ 'lastname' : { $in : ['Kohlin', 'Davar' ]} }"));
}
@Test
public void bindSpEL() {
// "{ arg0 : ?#{[0]} }"
Document target = parse("{ arg0 : ?#{[0]} }", 100.01D);
assertThat(target).isEqualTo(new Document("arg0", 100.01D));
}
private static Document parse(String json, Object... args) {
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json, args);
return new ParameterBindingDocumentCodec().decode(reader,
DecoderContext.builder().build());
}
}