Fix expression defining entire query in annotated repository methods.

This fix enables defining an entire JSON-based query in Query and Aggregate annotations using a single parameter or SpEL Expression.

Resolves: #3871
Original Pull Request: #3907
This commit is contained in:
rolag-it
2021-12-07 18:10:40 +01:00
committed by Christoph Strobl
parent 67edae8602
commit a8a0fb5dba
3 changed files with 81 additions and 21 deletions

View File

@@ -20,7 +20,6 @@ import static org.bson.assertions.Assertions.*;
import static org.bson.codecs.configuration.CodecRegistries.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -31,6 +30,7 @@ import org.bson.AbstractBsonReader.State;
import org.bson.BsonBinarySubType;
import org.bson.BsonDocument;
import org.bson.BsonDocumentWriter;
import org.bson.BsonInvalidOperationException;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonValue;
@@ -61,6 +61,7 @@ import org.springframework.util.StringUtils;
* @author Ross Lawley
* @author Ralph Schaer
* @author Christoph Strobl
* @author Rocco Lagrotteria
* @since 2.2
*/
public class ParameterBindingDocumentCodec implements CollectibleCodec<Document> {
@@ -172,7 +173,7 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
public Document decode(@Nullable String json, ParameterBindingContext bindingContext) {
if (StringUtils.isEmpty(json)) {
if (!StringUtils.hasText(json)) {
return new Document();
}
@@ -193,7 +194,7 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
public ExpressionDependencies captureExpressionDependencies(@Nullable String json, ValueProvider valueProvider,
ExpressionParser expressionParser) {
if (StringUtils.isEmpty(json)) {
if (!StringUtils.hasText(json)) {
return ExpressionDependencies.none();
}
@@ -217,19 +218,24 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
if (bindingReader.currentValue instanceof org.bson.Document) {
return (Document) bindingReader.currentValue;
}
}
Document document = new Document();
reader.readStartDocument();
try {
reader.readStartDocument();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
String fieldName = reader.readName();
Object value = readValue(reader, decoderContext);
document.put(fieldName, value);
}
} catch (JsonParseException e) {
reader.readEndDocument();
} catch (JsonParseException | BsonInvalidOperationException e) {
try {
Object value = readValue(reader, decoderContext);
@@ -244,8 +250,6 @@ public class ParameterBindingDocumentCodec implements CollectibleCodec<Document>
}
}
reader.readEndDocument();
return document;
}

View File

@@ -57,11 +57,12 @@ import org.springframework.util.ObjectUtils;
* @author Florian Buecklers
* @author Brendon Puntin
* @author Christoph Strobl
* @author Rocco Lagrotteria
* @since 2.2
*/
public class ParameterBindingJsonReader extends AbstractBsonReader {
private static final Pattern PARAMETER_ONLY_BINDING_PATTERN = Pattern.compile("^\\?(\\d+)$");
private static final Pattern ENTIRE_QUERY_BINDING_PATTERN = Pattern.compile("^\\?(\\d+)$|^[\\?:]#\\{.*\\}$");
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
private static final Pattern EXPRESSION_BINDING_PATTERN = Pattern.compile("[\\?:]#\\{.*\\}");
@@ -70,7 +71,6 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
private final JsonScanner scanner;
private JsonToken pushedToken;
Object currentValue;
private Mark mark;
/**
* Constructs a new instance with the given JSON string.
@@ -106,15 +106,8 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
public ParameterBindingJsonReader(String json, ValueProvider accessor, SpelExpressionParser spelExpressionParser,
Supplier<EvaluationContext> evaluationContext) {
this.scanner = new JsonScanner(json);
setContext(new Context(null, BsonContextType.TOP_LEVEL));
this(json, new ParameterBindingContext(accessor, spelExpressionParser, evaluationContext));
this.bindingContext = new ParameterBindingContext(accessor, spelExpressionParser, evaluationContext);
Matcher matcher = PARAMETER_ONLY_BINDING_PATTERN.matcher(json);
if (matcher.find()) {
currentValue = bindableValueFor(new JsonToken(JsonTokenType.UNQUOTED_STRING, json)).getValue();
}
}
public ParameterBindingJsonReader(String json, ParameterBindingContext bindingContext) {
@@ -124,10 +117,23 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
this.bindingContext = bindingContext;
Matcher matcher = PARAMETER_ONLY_BINDING_PATTERN.matcher(json);
Matcher matcher = ENTIRE_QUERY_BINDING_PATTERN.matcher(json);
if (matcher.find()) {
currentValue = bindableValueFor(new JsonToken(JsonTokenType.UNQUOTED_STRING, json)).getValue();
BindableValue bindingResult = bindableValueFor(new JsonToken(JsonTokenType.UNQUOTED_STRING, json));
try {
if (bindingResult.getValue() instanceof String) {
currentValue = Document.parse((String)bindingResult.getValue());
} else {
currentValue = bindingResult.getValue();
}
} catch (JsonParseException jsonParseException) {
throw new IllegalArgumentException(
String.format("Resulting value of expression '%s' is not a valid json query", json), jsonParseException);
}
}
}
// Spring Data Customization END

View File

@@ -41,6 +41,7 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
*
* @author Christoph Strobl
* @author Mark Paluch
* @author Rocco Lagrotteria
*/
class ParameterBindingJsonReaderUnitTests {
@@ -192,7 +193,6 @@ class ParameterBindingJsonReaderUnitTests {
@Test // DATAMONGO-2315
void bindStringAsDate() {
Date date = new Date();
Document target = parse("{ 'end_date' : { $gte : { $date : ?0 } } }", "2019-07-04T12:19:23.000Z");
assertThat(target).isEqualTo(Document.parse("{ 'end_date' : { $gte : { $date : '2019-07-04T12:19:23.000Z' } } } "));
@@ -347,7 +347,7 @@ class ParameterBindingJsonReaderUnitTests {
evaluationContext.setRootObject(new DummySecurityObject(new DummyWithId("wonderwoman")));
String json = "?#{ T(" + this.getClass().getName()
+ ").isBatman() ? {'_class': { '$eq' : 'region' }} : { '$and' : { {'_class': { '$eq' : 'region' } }, {'user.supervisor': principal.id } } } }";
+ ").isBatman() ? \"{'_class': { '$eq' : 'region' }}\" : \"{ '$and' : [ {'_class': { '$eq' : 'region' } }, {'user.supervisor': '\"+ principal.id +\"' } ] }\" }";
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json,
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
@@ -357,6 +357,43 @@ class ParameterBindingJsonReaderUnitTests {
.isEqualTo(new Document("$and", Arrays.asList(new Document("_class", new Document("$eq", "region")),
new Document("user.supervisor", "wonderwoman"))));
}
@Test
void bindEntireQueryUsingSpelExpression() {
Object[] args = new Object[] {"region"};
StandardEvaluationContext evaluationContext = (StandardEvaluationContext) EvaluationContextProvider.DEFAULT
.getEvaluationContext(args);
evaluationContext.setRootObject(new DummySecurityObject(new DummyWithId("wonderwoman")));
String json = "?#{ T(" + this.getClass().getName() + ").applyFilterByUser('?0' ,principal.id) }";
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json,
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
Document target = new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
assertThat(target)
.isEqualTo(new Document("$and", Arrays.asList(new Document("_class", new Document("$eq", "region")),
new Document("user.supervisor", "wonderwoman"))));
}
@Test
void bindEntireQueryUsingParameter() {
Object[] args = new Object[] {"{ 'itWorks' : true }"};
StandardEvaluationContext evaluationContext = (StandardEvaluationContext) EvaluationContextProvider.DEFAULT
.getEvaluationContext(args);
String json = "?0";
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json,
new ParameterBindingContext((index) -> args[index], new SpelExpressionParser(), evaluationContext));
Document target = new ParameterBindingDocumentCodec().decode(reader, DecoderContext.builder().build());
assertThat(target)
.isEqualTo(new Document("itWorks", true));
}
@Test // DATAMONGO-2571
void shouldParseRegexCorrectly() {
@@ -400,6 +437,19 @@ class ParameterBindingJsonReaderUnitTests {
public static boolean isBatman() {
return false;
}
public static String applyFilterByUser(String _class, String username) {
switch (username) {
case "batman":
return "{'_class': { '$eq' : '"
+ _class
+ "' }}";
default:
return "{ '$and' : [ {'_class': { '$eq' : '"
+ _class
+ "' } }, {'user.supervisor': '" + username + "' } ] }";
}
}
@Data
@AllArgsConstructor