From 2afede257074118d3a9d5456d21430eee1002a33 Mon Sep 17 00:00:00 2001 From: Diego Alonso Marquez Palacios Date: Mon, 30 Dec 2024 17:51:21 -0500 Subject: [PATCH] add support for value expressions and query method evaluators in datastore --- .../query/DatastoreQueryLookupStrategy.java | 28 ++++- .../repository/query/GqlDatastoreQuery.java | 103 ++++++++++++++---- .../support/DatastoreRepositoryFactory.java | 65 +++++++++++ 3 files changed, 176 insertions(+), 20 deletions(-) diff --git a/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/DatastoreQueryLookupStrategy.java b/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/DatastoreQueryLookupStrategy.java index 92141cbb51..0e0d7ed8b3 100644 --- a/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/DatastoreQueryLookupStrategy.java +++ b/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/DatastoreQueryLookupStrategy.java @@ -24,6 +24,7 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.util.Assert; @@ -41,6 +42,9 @@ public class DatastoreQueryLookupStrategy implements QueryLookupStrategy { private final ValueExpressionDelegate valueExpressionDelegate; + @SuppressWarnings("deprecation") + private final QueryMethodEvaluationContextProvider queryEvaluationContextProvider; + public DatastoreQueryLookupStrategy( DatastoreMappingContext datastoreMappingContext, DatastoreOperations datastoreOperations, @@ -50,6 +54,19 @@ public DatastoreQueryLookupStrategy( Assert.notNull(valueExpressionDelegate, "A non-null ValueExpressionDelegate is required."); this.datastoreMappingContext = datastoreMappingContext; this.valueExpressionDelegate = valueExpressionDelegate; + this.queryEvaluationContextProvider = null; + this.datastoreOperations = datastoreOperations; + } + public DatastoreQueryLookupStrategy( + DatastoreMappingContext datastoreMappingContext, + DatastoreOperations datastoreOperations, + @SuppressWarnings("deprecation") QueryMethodEvaluationContextProvider queryEvaluationContextProvider) { + Assert.notNull(datastoreMappingContext, "A non-null DatastoreMappingContext is required."); + Assert.notNull(datastoreOperations, "A non-null DatastoreOperations is required."); + Assert.notNull(queryEvaluationContextProvider, "A non-null EvaluationContextProvider is required."); + this.datastoreMappingContext = datastoreMappingContext; + this.valueExpressionDelegate = null; + this.queryEvaluationContextProvider = queryEvaluationContextProvider; this.datastoreOperations = datastoreOperations; } @@ -80,12 +97,21 @@ public RepositoryQuery resolveQuery( GqlDatastoreQuery createGqlDatastoreQuery( Class entityType, DatastoreQueryMethod queryMethod, String gql) { + if (valueExpressionDelegate != null) { + return new GqlDatastoreQuery<>( + entityType, + queryMethod, + this.datastoreOperations, + gql, + this.valueExpressionDelegate, + this.datastoreMappingContext); + } return new GqlDatastoreQuery<>( entityType, queryMethod, this.datastoreOperations, gql, - this.valueExpressionDelegate, + this.queryEvaluationContextProvider, this.datastoreMappingContext); } diff --git a/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/GqlDatastoreQuery.java b/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/GqlDatastoreQuery.java index fd0811385c..e5b0053093 100644 --- a/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/GqlDatastoreQuery.java +++ b/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/query/GqlDatastoreQuery.java @@ -41,6 +41,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.BiFunction; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -56,6 +57,9 @@ import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.SpelEvaluator; +import org.springframework.data.repository.query.SpelQueryContext; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.ValueExpressionQueryRewriter; import org.springframework.util.StringUtils; @@ -83,8 +87,12 @@ public class GqlDatastoreQuery extends AbstractDatastoreQuery { private final ValueExpressionDelegate valueExpressionDelegate; + private final QueryMethodEvaluationContextProvider queryEvaluationContextProvider; + private ValueExpressionQueryRewriter.EvaluatingValueExpressionQueryRewriter valueExpressionQueryRewriter; + private SpelQueryContext.EvaluatingSpelQueryContext evaluatingSpelQueryContext; + /** * Constructor. * @@ -104,6 +112,32 @@ public GqlDatastoreQuery( DatastoreMappingContext datastoreMappingContext) { super(queryMethod, datastoreTemplate, datastoreMappingContext, type); this.valueExpressionDelegate = valueExpressionDelegate; + this.queryEvaluationContextProvider = null; + this.originalGql = StringUtils.trimTrailingCharacter(gql.trim(), ';'); + setOriginalParamTags(); + setEvaluatingSpelQueryContext(); + setGqlResolvedEntityClassName(); + } + /** + * Constructor. + * + * @param type the underlying entity type + * @param queryMethod the underlying query method to support. + * @param datastoreTemplate used for executing queries. + * @param gql the query text. + * @param evaluationContextProvider the provider used to evaluate SpEL expressions in queries. + * @param datastoreMappingContext used for getting metadata about entities. + */ + public GqlDatastoreQuery( + Class type, + DatastoreQueryMethod queryMethod, + DatastoreOperations datastoreTemplate, + String gql, + QueryMethodEvaluationContextProvider evaluationContextProvider, + DatastoreMappingContext datastoreMappingContext) { + super(queryMethod, datastoreTemplate, datastoreMappingContext, type); + this.valueExpressionDelegate = null; + this.queryEvaluationContextProvider = evaluationContextProvider; this.originalGql = StringUtils.trimTrailingCharacter(gql.trim(), ';'); setOriginalParamTags(); setEvaluatingSpelQueryContext(); @@ -311,19 +345,30 @@ private void setGqlResolvedEntityClassName() { this.gqlResolvedEntityClassName = result; } + @SuppressWarnings("deprecation") private void setEvaluatingSpelQueryContext() { Set originalTags = new HashSet<>(GqlDatastoreQuery.this.originalParamTags); - GqlDatastoreQuery.this.valueExpressionQueryRewriter = ValueExpressionQueryRewriter.of(valueExpressionDelegate, - (Integer counter, String spelExpression) -> { - String newTag; - do { - counter++; - newTag = "@SpELtag" + counter; - } while (originalTags.contains(newTag)); - originalTags.add(newTag); - return newTag; - }, (left, right) -> right) - .withEvaluationContextAccessor(valueExpressionDelegate.getEvaluationContextAccessor()); + BiFunction parameterNameSource = (Integer counter, String spelExpression) -> { + String newTag; + do { + counter++; + newTag = "@SpELtag" + counter; + } while (originalTags.contains(newTag)); + originalTags.add(newTag); + return newTag; + }; + // We favor ValueExpressionDelegate since it's not deprecated + if (valueExpressionDelegate != null) { + GqlDatastoreQuery.this.valueExpressionQueryRewriter = ValueExpressionQueryRewriter.of(valueExpressionDelegate, + parameterNameSource, (left, right) -> right) + .withEvaluationContextAccessor(valueExpressionDelegate.getEvaluationContextAccessor()); + } else { + GqlDatastoreQuery.this.evaluatingSpelQueryContext = + SpelQueryContext.of( + parameterNameSource, + (prefix, newTag) -> newTag) + .withEvaluationContextProvider(GqlDatastoreQuery.this.queryEvaluationContextProvider); + } } // Convenience class to hold a grouping of GQL, tags, and parameter values. @@ -348,6 +393,32 @@ private class ParsedQueryWithTagsAndValues { int limitPosition; + Map evaluationResults; + + /** + * This method prepares the Gql query and its evaluation results. It will favor + * {@link ValueExpressionDelegate} over the deprecated + * {@link QueryMethodEvaluationContextProvider}. + */ + @SuppressWarnings("deprecation") + private void evaluateGql() { + if (GqlDatastoreQuery.this.valueExpressionDelegate != null) { + ValueExpressionQueryRewriter.QueryExpressionEvaluator spelEvaluator = + GqlDatastoreQuery.this.valueExpressionQueryRewriter.parse( + GqlDatastoreQuery.this.gqlResolvedEntityClassName, + GqlDatastoreQuery.this.queryMethod.getParameters()); + this.evaluationResults = spelEvaluator.evaluate(this.rawParams); + this.finalGql = spelEvaluator.getQueryString(); + } else { + SpelEvaluator spelEvaluator = + GqlDatastoreQuery.this.evaluatingSpelQueryContext.parse( + GqlDatastoreQuery.this.gqlResolvedEntityClassName, + GqlDatastoreQuery.this.queryMethod.getParameters()); + this.evaluationResults = spelEvaluator.evaluate(this.rawParams); + this.finalGql = spelEvaluator.getQueryString(); + } + } + ParsedQueryWithTagsAndValues(List initialTags, Object[] rawParams) { this.params = Arrays.stream(rawParams) @@ -355,15 +426,9 @@ private class ParsedQueryWithTagsAndValues { .collect(Collectors.toList()); this.rawParams = rawParams; this.tagsOrdered = new ArrayList<>(initialTags); + evaluateGql(); - ValueExpressionQueryRewriter.QueryExpressionEvaluator spelEvaluator = - GqlDatastoreQuery.this.valueExpressionQueryRewriter.parse( - GqlDatastoreQuery.this.gqlResolvedEntityClassName, - GqlDatastoreQuery.this.queryMethod.getParameters()); - Map results = spelEvaluator.evaluate(this.rawParams); - this.finalGql = spelEvaluator.getQueryString(); - - for (Map.Entry entry : results.entrySet()) { + for (Map.Entry entry : this.evaluationResults.entrySet()) { this.params.add(entry.getValue()); // Cloud Datastore requires the tag name without the @ this.tagsOrdered.add(entry.getKey().substring(1)); diff --git a/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/support/DatastoreRepositoryFactory.java b/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/support/DatastoreRepositoryFactory.java index d78884be6a..0925031fe4 100644 --- a/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/support/DatastoreRepositoryFactory.java +++ b/spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/repository/support/DatastoreRepositoryFactory.java @@ -25,14 +25,22 @@ import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.expression.BeanFactoryAccessor; +import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.data.mapping.MappingException; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.spel.EvaluationContextProvider; +import org.springframework.data.spel.ExpressionDependencies; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -101,8 +109,65 @@ protected Optional getQueryLookupStrategy( valueExpressionDelegate)); } + /** + * @deprecated in favor of {@link #getQueryLookupStrategy(Key, ValueExpressionDelegate)} + */ + @Override + @SuppressWarnings("deprecation") + @Deprecated(since = "6.0") + protected Optional getQueryLookupStrategy( + @Nullable Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { + + return Optional.of( + new DatastoreQueryLookupStrategy( + this.datastoreMappingContext, + this.datastoreOperations, + delegateContextProvider(evaluationContextProvider))); + + } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } + + @SuppressWarnings("deprecation") + private QueryMethodEvaluationContextProvider delegateContextProvider( + QueryMethodEvaluationContextProvider evaluationContextProvider) { + + return new QueryMethodEvaluationContextProvider() { + @Override + public > EvaluationContext getEvaluationContext( + T parameters, Object[] parameterValues) { + StandardEvaluationContext evaluationContext = + (StandardEvaluationContext) + evaluationContextProvider.getEvaluationContext(parameters, parameterValues); + evaluationContext.setRootObject(DatastoreRepositoryFactory.this.applicationContext); + evaluationContext.addPropertyAccessor(new BeanFactoryAccessor()); + evaluationContext.setBeanResolver( + new BeanFactoryResolver(DatastoreRepositoryFactory.this.applicationContext)); + return evaluationContext; + } + + @Override + public > EvaluationContext getEvaluationContext( + T parameters, Object[] parameterValues, ExpressionDependencies expressionDependencies) { + StandardEvaluationContext evaluationContext = + (StandardEvaluationContext) + evaluationContextProvider.getEvaluationContext( + parameters, parameterValues, expressionDependencies); + + evaluationContext.setRootObject(DatastoreRepositoryFactory.this.applicationContext); + evaluationContext.addPropertyAccessor(new BeanFactoryAccessor()); + evaluationContext.setBeanResolver( + new BeanFactoryResolver(DatastoreRepositoryFactory.this.applicationContext)); + return evaluationContext; + } + + @Override + public EvaluationContextProvider getEvaluationContextProvider() { + return (EvaluationContextProvider) evaluationContextProvider; + } + }; + } }