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:
committed by
Mark Paluch
parent
45f4b5087c
commit
8e21cc181e
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user