Add support for ValueExpression.

Closes #1477
This commit is contained in:
Mark Paluch
2024-02-12 15:49:15 +01:00
parent d01caa4795
commit 5279c5fae2
10 changed files with 138 additions and 70 deletions

View File

@@ -32,9 +32,13 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.Environment;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.cassandra.core.mapping.*;
import org.springframework.data.cassandra.core.mapping.Embedded.OnEmpty;
@@ -46,19 +50,20 @@ import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.CachingValueExpressionEvaluatorFactory;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
import org.springframework.data.mapping.model.EntityInstantiator;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.mapping.model.PropertyValueProvider;
import org.springframework.data.mapping.model.SpELContext;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.data.mapping.model.ValueExpressionParameterValueProvider;
import org.springframework.data.projection.EntityProjection;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.util.Predicates;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -87,7 +92,7 @@ import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry;
* @author Frank Spitulski
*/
public class MappingCassandraConverter extends AbstractCassandraConverter
implements ApplicationContextAware, BeanClassLoaderAware {
implements ApplicationContextAware, EnvironmentAware, EnvironmentCapable, BeanClassLoaderAware {
private final Log log = LogFactory.getLog(getClass());
@@ -99,11 +104,21 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
private @Nullable ClassLoader beanClassLoader;
private @Nullable Environment environment;
private SpELContext spELContext;
private final DefaultColumnTypeResolver cassandraTypeResolver;
private final EmbeddedEntityOperations embeddedEntityOperations;
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
private final SpelExpressionParser expressionParser = new SpelExpressionParser();
private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(
expressionParser);
private final CachingValueExpressionEvaluatorFactory expressionEvaluatorFactory = new CachingValueExpressionEvaluatorFactory(
expressionParser, this, o -> spELContext.getEvaluationContext(o));
/**
* Create a new {@link MappingCassandraConverter} with a {@link CassandraMappingContext}.
@@ -171,10 +186,30 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.spELContext = new SpELContext(this.spELContext, applicationContext);
this.environment = applicationContext.getEnvironment();
this.projectionFactory.setBeanFactory(applicationContext);
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public Environment getEnvironment() {
if (this.environment == null) {
this.environment = new StandardEnvironment();
}
return this.environment;
}
public void setSpELContext(SpELContext spELContext) {
this.spELContext = spELContext;
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
@@ -294,8 +329,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
this::doReadTupleValue, this::doReadUdtValue, this::readCollectionOrArray, this::readMap,
this::getPotentiallyConvertedSimpleRead, projection);
return doReadProjection(context, new RowValueProvider(row, new DefaultSpELExpressionEvaluator(row, spELContext)),
projection);
return doReadProjection(context, new RowValueProvider(row, expressionEvaluatorFactory.create(row)), projection);
}
@SuppressWarnings("unchecked")
@@ -334,11 +368,10 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
ParameterValueProvider<CassandraPersistentProperty> provider;
if (persistenceCreator != null && persistenceCreator.hasParameters()) {
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(valueProviderToUse.getSource(),
spELContext);
ValueExpressionEvaluator evaluator = expressionEvaluatorFactory.create(valueProviderToUse.getSource());
ParameterValueProvider<CassandraPersistentProperty> parameterValueProvider = newParameterValueProvider(context,
entity, valueProviderToUse);
provider = new ConverterAwareSpELExpressionParameterValueProvider(evaluator, getConversionService(),
provider = new ConverterAwareValueExpressionParameterValueProvider(evaluator, getConversionService(),
parameterValueProvider, context);
} else {
provider = NoOpParameterValueProvider.INSTANCE;
@@ -362,8 +395,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
if (typeDescriptor.isProjection()) {
CassandraValueProvider valueProvider = new RowValueProvider(row,
new DefaultSpELExpressionEvaluator(row, this.spELContext));
CassandraValueProvider valueProvider = new RowValueProvider(row, expressionEvaluatorFactory.create(row));
return doReadProjection(context, valueProvider, typeDescriptor);
}
@@ -376,7 +408,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
if (typeDescriptor.isProjection()) {
CassandraValueProvider valueProvider = new UdtValueProvider(udtValue,
new DefaultSpELExpressionEvaluator(udtValue, this.spELContext));
expressionEvaluatorFactory.create(udtValue));
return doReadProjection(context, valueProvider, typeDescriptor);
}
@@ -389,7 +421,7 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
if (typeDescriptor.isProjection()) {
CassandraValueProvider valueProvider = new TupleValueProvider(tupleValue,
new DefaultSpELExpressionEvaluator(tupleValue, this.spELContext));
expressionEvaluatorFactory.create(tupleValue));
return doReadProjection(context, valueProvider, typeDescriptor);
}
@@ -436,10 +468,10 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
}
private <S> S doReadEntity(ConversionContext context, Object value,
Function<SpELExpressionEvaluator, CassandraValueProvider> valueProviderSupplier,
Function<ValueExpressionEvaluator, CassandraValueProvider> valueProviderSupplier,
TypeInformation<? extends S> typeHint) {
SpELExpressionEvaluator expressionEvaluator = new DefaultSpELExpressionEvaluator(value, this.spELContext);
ValueExpressionEvaluator expressionEvaluator = expressionEvaluatorFactory.create(value);
CassandraValueProvider valueProvider = valueProviderSupplier.apply(expressionEvaluator);
return doReadEntity(context, valueProvider, typeHint);
@@ -507,10 +539,10 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
ParameterValueProvider<CassandraPersistentProperty> provider;
if (persistenceCreator != null && persistenceCreator.hasParameters()) {
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(valueProvider.getSource(), spELContext);
ValueExpressionEvaluator evaluator = expressionEvaluatorFactory.create(valueProvider.getSource());
ParameterValueProvider<CassandraPersistentProperty> parameterValueProvider = newParameterValueProvider(context,
entity, valueProvider);
provider = new ConverterAwareSpELExpressionParameterValueProvider(evaluator, getConversionService(),
provider = new ConverterAwareValueExpressionParameterValueProvider(evaluator, getConversionService(),
parameterValueProvider, context);
} else {
provider = NoOpParameterValueProvider.INSTANCE;
@@ -1261,23 +1293,23 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
}
/**
* Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw
* Extension of {@link ValueExpressionParameterValueProvider} to recursively trigger value conversion on the raw
* resolved SpEL value.
*/
private static class ConverterAwareSpELExpressionParameterValueProvider
extends SpELExpressionParameterValueProvider<CassandraPersistentProperty> {
private static class ConverterAwareValueExpressionParameterValueProvider
extends ValueExpressionParameterValueProvider<CassandraPersistentProperty> {
private final ConversionContext context;
/**
* Creates a new {@link ConverterAwareSpELExpressionParameterValueProvider}.
* Creates a new {@link ConverterAwareValueExpressionParameterValueProvider}.
*
* @param evaluator must not be {@literal null}.
* @param conversionService must not be {@literal null}.
* @param delegate must not be {@literal null}.
* @param context must not be {@literal null}.
*/
public ConverterAwareSpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator,
public ConverterAwareValueExpressionParameterValueProvider(ValueExpressionEvaluator evaluator,
ConversionService conversionService, ParameterValueProvider<CassandraPersistentProperty> delegate,
ConversionContext context) {
@@ -1286,8 +1318,9 @@ public class MappingCassandraConverter extends AbstractCassandraConverter
}
@Override
protected <T> T potentiallyConvertSpelValue(Object object, Parameter<T, CassandraPersistentProperty> parameter) {
return (T) context.convert(object, parameter.getType());
protected <T> T potentiallyConvertExpressionValue(Object object,
Parameter<T, CassandraPersistentProperty> parameter) {
return context.convert(object, parameter.getType());
}
}

View File

@@ -16,7 +16,7 @@
package org.springframework.data.cassandra.core.convert;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -33,16 +33,16 @@ public class RowValueProvider implements CassandraValueProvider {
private final RowReader reader;
private final SpELExpressionEvaluator evaluator;
private final ValueExpressionEvaluator evaluator;
/**
* Create a new {@link RowValueProvider} with the given {@link Row}, {@link CodecRegistry} and
* {@link SpELExpressionEvaluator}.
* {@link ValueExpressionEvaluator}.
*
* @param source must not be {@literal null}.
* @param evaluator must not be {@literal null}.
*/
public RowValueProvider(Row source, SpELExpressionEvaluator evaluator) {
public RowValueProvider(Row source, ValueExpressionEvaluator evaluator) {
Assert.notNull(source, "Source Row must not be null");
Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null");

View File

@@ -16,7 +16,7 @@
package org.springframework.data.cassandra.core.convert;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -34,17 +34,17 @@ public class TupleValueProvider implements CassandraValueProvider {
private final CodecRegistry codecRegistry;
private final SpELExpressionEvaluator evaluator;
private final ValueExpressionEvaluator evaluator;
private final TupleValue tupleValue;
/**
* Create a new {@link TupleValueProvider} with the given {@link TupleValue} and {@link SpELExpressionEvaluator}.
* Create a new {@link TupleValueProvider} with the given {@link TupleValue} and {@link ValueExpressionEvaluator}.
*
* @param tupleValue must not be {@literal null}.
* @param evaluator must not be {@literal null}.
*/
public TupleValueProvider(TupleValue tupleValue, SpELExpressionEvaluator evaluator) {
public TupleValueProvider(TupleValue tupleValue, ValueExpressionEvaluator evaluator) {
Assert.notNull(tupleValue, "TupleValue must not be null");
Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null");

View File

@@ -16,7 +16,7 @@
package org.springframework.data.cassandra.core.convert;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.ValueExpressionEvaluator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -32,15 +32,15 @@ public class UdtValueProvider implements CassandraValueProvider {
private final UdtValue udtValue;
private final SpELExpressionEvaluator evaluator;
private final ValueExpressionEvaluator evaluator;
/**
* Create a new {@link UdtValueProvider} with the given {@link UdtValue} and {@link SpELExpressionEvaluator}.
* Create a new {@link UdtValueProvider} with the given {@link UdtValue} and {@link ValueExpressionEvaluator}.
*
* @param udtValue must not be {@literal null}.
* @param evaluator must not be {@literal null}.
*/
public UdtValueProvider(UdtValue udtValue, SpELExpressionEvaluator evaluator) {
public UdtValueProvider(UdtValue udtValue, ValueExpressionEvaluator evaluator) {
Assert.notNull(udtValue, "UDTValue must not be null");
Assert.notNull(evaluator, "SpELExpressionEvaluator must not be null");

View File

@@ -23,18 +23,23 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
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.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -52,18 +57,20 @@ import com.datastax.oss.driver.api.core.CqlIdentifier;
public class BasicCassandraPersistentEntity<T> extends BasicPersistentEntity<T, CassandraPersistentProperty>
implements CassandraPersistentEntity<T>, ApplicationContextAware {
static final ValueExpressionParser PARSER = ValueExpressionParser.create(SpelExpressionParser::new);
private static final CassandraPersistentEntityMetadataVerifier DEFAULT_VERIFIER = new CompositeCassandraPersistentEntityMetadataVerifier();
private final CqlIdentifierGenerator namingAccessor = new CqlIdentifierGenerator();
private @Nullable ApplicationContext applicationContext;
private Boolean forceQuote;
private CassandraPersistentEntityMetadataVerifier verifier = DEFAULT_VERIFIER;
private CqlIdentifier tableName;
private @Nullable StandardEvaluationContext spelContext;
private final Map<Parameter<?, CassandraPersistentProperty>, CassandraPersistentProperty> constructorProperties = new ConcurrentHashMap<>();
/**
@@ -117,11 +124,45 @@ public class BasicCassandraPersistentEntity<T> extends BasicPersistentEntity<T,
if (annotation != null) {
return this.namingAccessor.generate((String) AnnotationUtils.getValue(annotation),
(Boolean) AnnotationUtils.getValue(annotation, "forceQuote"), defaultNameGenerator, this, this.spelContext);
(Boolean) AnnotationUtils.getValue(annotation, "forceQuote"), defaultNameGenerator, this, PARSER,
this::getValueEvaluationContext);
}
return this.namingAccessor.generate(null, forceQuote != null ? forceQuote : false, defaultNameGenerator, this,
this.spelContext);
PARSER, this::getValueEvaluationContext);
}
@Override
protected EvaluationContext getEvaluationContext(Object rootObject) {
return postProcess(super.getEvaluationContext(rootObject == null ? applicationContext : rootObject));
}
@Override
protected EvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
return postProcess(super.getEvaluationContext(rootObject == null ? applicationContext : rootObject, dependencies));
}
// TODO: Do we want to keep the Context customization? Ideally, we align with EvaluationContextProvider.
private EvaluationContext postProcess(EvaluationContext evaluationContext) {
if (evaluationContext instanceof StandardEvaluationContext sec) {
if (sec.getRootObject().getValue() instanceof BeanFactory) {
sec.addPropertyAccessor(new BeanFactoryAccessor());
}
}
return evaluationContext;
}
@Override
protected ValueEvaluationContext getValueEvaluationContext(Object rootObject) {
return super.getValueEvaluationContext(rootObject);
}
@Override
protected ValueEvaluationContext getValueEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
return super.getValueEvaluationContext(rootObject, dependencies);
}
@Override
@@ -151,13 +192,7 @@ public class BasicCassandraPersistentEntity<T> extends BasicPersistentEntity<T,
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
Assert.notNull(context, "ApplicationContext must not be null");
spelContext = new StandardEvaluationContext();
spelContext.addPropertyAccessor(new BeanFactoryAccessor());
spelContext.setBeanResolver(new BeanFactoryResolver(context));
spelContext.setRootObject(context);
this.applicationContext = context;
}
@Override

View File

@@ -25,8 +25,6 @@ import java.util.Optional;
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.core.annotation.AnnotatedElementUtils;
import org.springframework.data.cassandra.core.cql.Ordering;
import org.springframework.data.cassandra.core.cql.PrimaryKeyType;
@@ -36,7 +34,6 @@ import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -64,8 +61,6 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP
private @Nullable CqlIdentifier columnName;
private @Nullable StandardEvaluationContext spelContext;
/**
* Create a new {@link BasicCassandraPersistentProperty}.
*
@@ -80,15 +75,7 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP
}
@Override
public void setApplicationContext(ApplicationContext context) {
Assert.notNull(context, "ApplicationContext must not be null");
this.spelContext = new StandardEvaluationContext();
this.spelContext.addPropertyAccessor(new BeanFactoryAccessor());
this.spelContext.setBeanResolver(new BeanFactoryResolver(context));
this.spelContext.setRootObject(context);
}
public void setApplicationContext(ApplicationContext context) {}
@Override
public CassandraPersistentEntity<?> getOwner() {
@@ -175,7 +162,10 @@ public class BasicCassandraPersistentProperty extends AnnotationBasedPersistentP
forceQuote = column.forceQuote();
}
return namingAccessor.generate(overriddenName, forceQuote, NamingStrategy::getColumnName, this, this.spelContext);
BasicCassandraPersistentEntity<?> entity = (BasicCassandraPersistentEntity<?>) getOwner();
return namingAccessor.generate(overriddenName, forceQuote, NamingStrategy::getColumnName, this,
BasicCassandraPersistentEntity.PARSER, entity::getValueEvaluationContext);
}
@Override

View File

@@ -18,10 +18,13 @@ package org.springframework.data.cassandra.core.mapping;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.springframework.data.cassandra.util.SpelUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.data.expression.ValueEvaluationContext;
import org.springframework.data.expression.ValueExpression;
import org.springframework.data.expression.ValueExpressionParser;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import com.datastax.oss.driver.api.core.CqlIdentifier;
@@ -59,18 +62,21 @@ class CqlIdentifierGenerator {
* @param forceQuote whether to enforce quoting.
* @param defaultNameGenerator the default name generator.
* @param source source to be used for name generation.
* @param spelContext the SpEL evaluation context for evaluating SpEL expressions provided through
* {@code providedName}.
* @param parser expression parser.
* @param contextFunction evaluation context provider function.
* @return the generated name.
*/
public <T> CqlIdentifier generate(@Nullable String providedName, boolean forceQuote,
BiFunction<NamingStrategy, T, String> defaultNameGenerator, T source, @Nullable EvaluationContext spelContext) {
BiFunction<NamingStrategy, T, String> defaultNameGenerator, T source, ValueExpressionParser parser,
BiFunction<Object, ExpressionDependencies, ValueEvaluationContext> contextFunction) {
String name;
boolean useForceQuote = forceQuote;
if (StringUtils.hasText(providedName)) {
name = spelContext != null ? SpelUtils.evaluate(providedName, spelContext) : providedName;
ValueExpression expression = parser.parse(providedName);
name = ObjectUtils
.nullSafeToString(expression.evaluate(contextFunction.apply(null, expression.getExpressionDependencies())));
useForceQuote = true;
} else {
name = defaultNameGenerator.apply(getNamingStrategy(forceQuote), source);

View File

@@ -33,6 +33,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.data.util.TypeInformation;
import com.datastax.oss.driver.api.core.CqlIdentifier;
@@ -81,6 +82,7 @@ class BasicCassandraPersistentEntityUnitTests {
BasicCassandraPersistentEntity<UserLine> entity = new BasicCassandraPersistentEntity<>(
TypeInformation.of(UserLine.class));
entity.setEvaluationContextProvider(new ExtensionAwareEvaluationContextProvider(context));
entity.setApplicationContext(context);
assertThat(entity.getTableName()).hasToString(bean.tableName);

View File

@@ -19,6 +19,7 @@
** xref:cassandra/property-converters.adoc[]
** xref:cassandra/events.adoc[]
** xref:cassandra/auditing.adoc[]
** xref:cassandra/value-expressions.adoc[]
* xref:repositories.adoc[]
** xref:repositories/core-concepts.adoc[]

View File

@@ -0,0 +1 @@
include::{commons}@data-commons::page$value-expressions.adoc[]