Add support for Value Expressions.

We now support Value Expressions such as `#{1+1}-${spring.application.name:fallback-value}` that are composed of SpEL expressions, literals and Property Placeholders.

See #2369
Original pull request: #3036
This commit is contained in:
Christoph Strobl
2023-11-23 15:27:25 +01:00
committed by Mark Paluch
parent 5ec3c609f6
commit 9f13e5450d
12 changed files with 904 additions and 5 deletions

View File

@@ -40,8 +40,11 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.KotlinDetector;
import org.springframework.core.NativeDetector;
import org.springframework.core.env.Environment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.domain.ManagedTypes;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
@@ -49,6 +52,7 @@ import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.model.AbstractPersistentProperty;
import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory;
import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory;
import org.springframework.data.mapping.model.EntityInstantiators;
@@ -59,6 +63,7 @@ import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.data.util.KotlinReflectionUtils;
import org.springframework.data.util.NullableWrapperConverters;
import org.springframework.data.util.Optionals;
@@ -89,7 +94,7 @@ import org.springframework.util.ReflectionUtils.FieldFilter;
* @author Christoph Strobl
*/
public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?, P>, P extends PersistentProperty<P>>
implements MappingContext<E, P>, ApplicationEventPublisherAware, ApplicationContextAware, InitializingBean {
implements MappingContext<E, P>, ApplicationEventPublisherAware, ApplicationContextAware, InitializingBean, EnvironmentAware {
private static final Log LOGGER = LogFactory.getLog(MappingContext.class);
@@ -100,6 +105,7 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
private @Nullable ApplicationEventPublisher applicationEventPublisher;
private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT;
private @Nullable EnvironmentAccessor environmentAccessor;
private ManagedTypes managedTypes = ManagedTypes.empty();
@@ -138,6 +144,10 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
}
}
public void setEnvironment(Environment environment) {
this.environmentAccessor = new DelegatingEnvironmentAccessor(environment);
}
/**
* Sets the {@link Set} of types to populate the context initially.
*
@@ -406,6 +416,7 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
E entity = createPersistentEntity(userTypeInformation);
entity.setEvaluationContextProvider(evaluationContextProvider);
entity.setEnvironmentAccessor(environmentAccessor);
// Eagerly cache the entity as we might have to find it during recursive lookups.
persistentEntities.put(userTypeInformation, Optional.of(entity));
@@ -477,6 +488,10 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
@Override
public void afterPropertiesSet() {
if(this.environmentAccessor == null) {
this.environmentAccessor = new DelegatingEnvironmentAccessor(new StandardEnvironment());
}
initialize();
}
@@ -579,6 +594,9 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
return;
}
if(property instanceof AbstractPersistentProperty<?> pp) {
pp.setEnvironmentAccessor(environmentAccessor);
}
entity.addPersistentProperty(property);
if (property.isAssociation()) {
@@ -776,4 +794,32 @@ public abstract class AbstractMappingContext<E extends MutablePersistentEntity<?
}
}
}
/**
* @author Christoph Strobl
* @since 3.3
*/
public static class DelegatingEnvironmentAccessor implements EnvironmentAccessor {
private final Environment environment;
static EnvironmentAccessor standard() {
return new DelegatingEnvironmentAccessor(new StandardEnvironment());
}
public DelegatingEnvironmentAccessor(Environment environment) {
this.environment = environment;
}
@Nullable
@Override
public String getProperty(String key) {
return environment.getProperty(key);
}
@Override
public String resolvePlaceholders(String text) {
return environment.resolvePlaceholders(text);
}
}
}

View File

@@ -27,6 +27,7 @@ import java.util.stream.Collectors;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.data.util.KotlinReflectionUtils;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.ReflectionUtils;
@@ -74,6 +75,9 @@ public abstract class AbstractPersistentProperty<P extends PersistentProperty<P>
private final Lazy<Boolean> readable;
private final boolean immutable;
private @Nullable EnvironmentAccessor environmentAccessor;
public AbstractPersistentProperty(Property property, PersistentEntity<?, P> owner,
SimpleTypeHolder simpleTypeHolder) {
@@ -306,6 +310,14 @@ public abstract class AbstractPersistentProperty<P extends PersistentProperty<P>
return targetType == null ? information.getRequiredActualType() : targetType;
}
protected EnvironmentAccessor getEnvironmentAccessor() {
return environmentAccessor;
}
public void setEnvironmentAccessor(EnvironmentAccessor environmentAccessor) {
this.environmentAccessor = environmentAccessor;
}
@Override
public boolean equals(@Nullable Object obj) {

View File

@@ -37,6 +37,7 @@ import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.Optionals;

View File

@@ -36,6 +36,7 @@ import org.springframework.data.domain.Persistable;
import org.springframework.data.mapping.*;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.data.support.IsNewStrategy;
import org.springframework.data.support.PersistableIsNewStrategy;
import org.springframework.data.util.Lazy;
@@ -79,6 +80,7 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
private @Nullable P versionProperty;
private PersistentPropertyAccessorFactory propertyAccessorFactory;
private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT;
private @Nullable EnvironmentAccessor environmentAccessor;
private final Lazy<Alias> typeAlias;
private final Lazy<IsNewStrategy> isNewStrategy;
@@ -205,10 +207,8 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
if (versionProperty != null) {
throw new MappingException(
String.format(
"Attempt to add version property %s but already have property %s registered "
+ "as version; Check your mapping configuration",
property.getField(), versionProperty.getField()));
String.format("Attempt to add version property %s but already have property %s registered "
+ "as version; Check your mapping configuration", property.getField(), versionProperty.getField()));
}
this.versionProperty = property;
@@ -220,6 +220,11 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
this.evaluationContextProvider = provider;
}
@Override
public void setEnvironmentAccessor(EnvironmentAccessor accessor) {
this.environmentAccessor = accessor;
}
/**
* Returns the given property if it is a better candidate for the id property than the current id property.
*
@@ -443,6 +448,17 @@ public class BasicPersistentEntity<T, P extends PersistentProperty<P>> implement
return evaluationContextProvider.getEvaluationContext(rootObject, dependencies);
}
/**
* Obtain the {@link EnvironmentAccessor} providing access to the current
* {@link org.springframework.core.env.Environment}.
*
* @return never {@literal null}.
* @since 3.3
*/
protected EnvironmentAccessor getEnvironmentAccessor() {
return environmentAccessor;
}
/**
* Returns the default {@link IsNewStrategy} to be used. Will be a {@link PersistentEntityIsNewStrategy} by default.
* Note, that this strategy only gets used if the entity doesn't implement {@link Persistable} as this indicates the

View File

@@ -21,6 +21,7 @@ import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.support.EnvironmentAccessor;
/**
* Interface capturing mutator methods for {@link PersistentEntity}s.
@@ -66,4 +67,12 @@ public interface MutablePersistentEntity<T, P extends PersistentProperty<P>> ext
* @param provider must not be {@literal null}.
*/
void setEvaluationContextProvider(EvaluationContextProvider provider);
/**
* Sets the {@link EnvironmentAccessor} to be used by the entity.
*
* @param accessor must not be {@literal null}.
* @since 3.3
*/
void setEnvironmentAccessor(EnvironmentAccessor accessor);
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2023 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
*
* https://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.support;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @since 3.3
*/
public interface EnvironmentAccessor extends PlaceholderResolver {
@Nullable
String getProperty(String key);
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2023 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
*
* https://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.support;
/**
* @author Christoph Strobl
* @since 3.3
*/
public interface PlaceholderResolver {
String resolvePlaceholders(String text);
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2023. 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.util;
import java.util.List;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.ConstructorResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.OperatorOverloader;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypeComparator;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.TypeLocator;
import org.springframework.expression.TypedValue;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @since 2023/11
*/
public abstract class ExpressionEvaluator {
private EnvironmentAccessor environmentAccessor;
private EvaluationContextProvider evaluationContextProvider;
public ExpressionEvaluator(EnvironmentAccessor environmentAccessor, EvaluationContextProvider evaluationContextProvider) {
this.environmentAccessor = environmentAccessor;
this.evaluationContextProvider = evaluationContextProvider;
}
abstract <T> T evaluate(String text, EnvironmentAwareEvaluationContext context);
public class EnvironmentAwareEvaluationContext implements EvaluationContext, EnvironmentAccessor, EvaluationContextProvider {
@Override
public EvaluationContext getEvaluationContext(Object rootObject) {
return null;
}
@Nullable
@Override
public String getProperty(String key) {
return null;
}
@Override
public String resolvePlaceholders(String text) {
return null;
}
@Override
public TypedValue getRootObject() {
return null;
}
@Override
public List<PropertyAccessor> getPropertyAccessors() {
return null;
}
@Override
public List<ConstructorResolver> getConstructorResolvers() {
return null;
}
@Override
public List<MethodResolver> getMethodResolvers() {
return null;
}
@Nullable
@Override
public BeanResolver getBeanResolver() {
return null;
}
@Override
public TypeLocator getTypeLocator() {
return null;
}
@Override
public TypeConverter getTypeConverter() {
return null;
}
@Override
public TypeComparator getTypeComparator() {
return null;
}
@Override
public OperatorOverloader getOperatorOverloader() {
return null;
}
@Override
public void setVariable(String name, @Nullable Object value) {
}
@Nullable
@Override
public Object lookupVariable(String name) {
return null;
}
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2023. 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 2023. 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 2023 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.mapping.model.justme;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.PropertyResolver;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author Christoph Strobl
* @since 2023/11
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class PropertySourceUnitTests {
@Value("${my-type.name}") String entityName;
@Autowired ListableBeanFactory beanFactory;
SpelExpressionParser parser = new SpelExpressionParser();
@Configuration
@PropertySource("classpath:persistent-entity.properties")
static class Config {
}
// @Test
void plainContext() {
System.getProperties().forEach((key,value) -> System.out.printf("%s:%s\n", key, value));
Expression expression = parse("#{systemProperties['os.arch']}");
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
Object value = expression.getValue();
System.out.println("va: " + value);
}
Expression parse(String source) {
return parser.parseExpression(source, getParserContext());
}
// @Test
void loadsContext() {
Map<String, PropertyResolver> beansOfType = beanFactory.getBeansOfType(PropertyResolver.class, false, false);
ExtensionAwareEvaluationContextProvider contextProvider = new ExtensionAwareEvaluationContextProvider(beanFactory);
// Expression spelExpression = parser.parseExpression("#{ T(java.lang.Math).random() * 100.0 }",
// getParserContext());
// Expression expression = parser.parseExpression("#{ T(java.lang.Math).random() * 100.0 }");
// Expression expression = parser.parseExpression("#{ systemProperties['user.region'] }", getParserContext());
StandardEvaluationContext evaluationContext = contextProvider.getEvaluationContext(new StandardEnvironment());
SpelExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(SpelCompilerMode.OFF, null));
Expression expression = parser.parseExpression("#{systemProperties['os.arch']}", getParserContext());
Object value = expression.getValue(evaluationContext);
System.out.println("value: " + value);
// Expression expression = parser.parseExpression("#{ ${x.y.z} ?: 'defaultValue' }");
// Expression expression = parser.parseExpression("#{'${my-type.name}'}");
// System.out.println("expression: " + expression);
// Object value = expression.getValue(evaluationContext);
// System.out.println("value: " + value);
}
private static ParserContext getParserContext() {
return new ParserContext() {
@Override
public boolean isTemplate() {
return true;
}
@Override
public String getExpressionPrefix() {
return "#{";
}
@Override
public String getExpressionSuffix() {
return "}";
}
};
}
}

View File

@@ -0,0 +1,259 @@
/*
* Copyright 2023. 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 2023 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.util;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.mapping.context.AbstractMappingContext.DelegatingEnvironmentAccessor;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @since 2023/11
*/
public class ExpressionEvaluatorUnitTests {
public static final SpelExpressionParser SPEL_EXPRESSION_PARSER = new SpelExpressionParser(new SpelParserConfiguration(SpelCompilerMode.OFF, null));
@Test
void intialTests() {
ExpressionEvaluator evaluator = new ExpressionEvaluator(new DelegatingEnvironmentAccessor(new StandardEnvironment()));
{
Object result =
evaluator.prepare("foo-${os.arch}")
.parseWith(SpelExpressionParser::new)
.withContext(StandardEvaluationContext::new)
.evaluate();
System.out.println("result: " + result);
}
{
Object result = evaluator.prepare("#{systemProperties['os.arch']}")
.withContext(() -> new StandardEvaluationContext(new StandardEnvironment()))
.evaluate();
System.out.println("result: " + result);
}
// @Value("#{1+1}-${os.arch}") -> 2-aarch64
// StandardBeanExpressionResolver
{
Expression expression = SPEL_EXPRESSION_PARSER.parseExpression("#{systemProperties['os.arch']}-foo-${os.arch}", ParserContext.TEMPLATE_EXPRESSION);
Object value = expression.getValue(new StandardEvaluationContext(new StandardEnvironment()));
if(value instanceof String s) {
//
}
System.out.println("value: " + value);
}
{
Object result = evaluator.prepare("${os.arch}").evaluate();
System.out.println("result: " + result);
}
{
Object result = evaluator.prepare("foo-${os.arch}").evaluate();
System.out.println("result: " + result);
}
{
// check against - boot
// plcaechoder replacement after spel
Object result = evaluator.prepare("'#{systemProperties['os.arch']}-foo-${os.arch}'")
.withContext(() -> new StandardEvaluationContext(new StandardEnvironment()))
.evaluate();
System.out.println("result: " + result);
}
// evaluator.prepare(source)evaluate(() -> context);
}
static class ExpressionEvaluator {
private final EnvironmentAccessor environmentAccessor;
public ExpressionEvaluator(EnvironmentAccessor environmentAccessor) {
this.environmentAccessor = environmentAccessor;
}
PreparedExpression prepare(String source) {
return this.prepare(() -> source);
}
PreparedExpression prepare(Supplier<String> source) {
return new PreparedExpression(new Supplier<>() {
@Nullable private String cachedValue;
@Override
public String get() {
if (cachedValue != null) {
return cachedValue;
}
String expressionSource = source.get();
String expression = environmentAccessor.resolvePlaceholders(expressionSource);
if (ObjectUtils.nullSafeEquals(expressionSource, expression)) {
cachedValue = expression;
}
System.out.println("using: '" + expression + "' for " + expressionSource);
return expression;
}
});
}
}
static class PreparedExpression {
Supplier<SpelExpressionParser> expressionParser;
Supplier<String> source;
Map<String, Optional<Expression>> expressionCache = new HashMap<>(1, 1F);
public PreparedExpression(Supplier<String> source) {
this.source = source;
expressionParser = Lazy.of(SpelExpressionParser::new);
}
PreparedExpression parseWith(SpelExpressionParser expressionParser) {
return parseWith(() -> expressionParser);
}
PreparedExpression parseWith(Supplier<SpelExpressionParser> expressionParser) {
this.expressionParser = expressionParser;
return this;
}
<T> T evaluate() {
return withContext(SimpleEvaluationContext.forReadOnlyDataBinding().build()).evaluate();
}
ExpressionEvaluation applyReadonlyBinding() {
return withContext(SimpleEvaluationContext.forReadOnlyDataBinding().build());
}
ExpressionEvaluation withContext(EvaluationContext evaluationContext) {
return withContext(() -> evaluationContext);
}
ExpressionEvaluation withContext(Supplier<EvaluationContext> evaluationContext) {
return new ExpressionEvaluation(this, evaluationContext);
}
<T> T doEvaluate(Supplier<EvaluationContext> evaluationContext) {
String expressionString = source.get();
Optional<Expression> expression = expressionCache.computeIfAbsent(expressionString,
(String key) -> Optional.ofNullable(detectExpression(expressionParser.get(), key)));
return (T) expression.map(it -> it.getValue(evaluationContext.get())).orElse(expressionString);
}
@Nullable
private static Expression detectExpression(SpelExpressionParser parser, @Nullable String potentialExpression) {
if (!StringUtils.hasText(potentialExpression)) {
return null;
}
Expression expression = parser.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION);
return expression instanceof LiteralExpression ? null : expression;
}
}
static class ExpressionEvaluation {
final PreparedExpression source;
final Supplier<EvaluationContext> evaluationContext;
boolean cacheResult;
Optional<?> cached;
public ExpressionEvaluation(PreparedExpression source, Supplier<EvaluationContext> evaluationContext) {
this.source = source;
this.evaluationContext = evaluationContext;
}
<T> T evaluate() {
if(cacheResult && cached != null) {
return (T) cached.orElse(null);
}
T result = source.doEvaluate(evaluationContext);
if(cacheResult) {
cached = Optional.ofNullable(result);
}
return result;
}
ExpressionEvaluation cache() {
cacheResult = true;
return this;
}
<T> T reevaluate() {
if(cacheResult) {
cached = null;
}
return evaluate();
}
}
}

View File

@@ -0,0 +1,221 @@
/*
* Copyright 2023 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
*
* https://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.util;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.data.mapping.context.AbstractMappingContext.DelegatingEnvironmentAccessor;
import org.springframework.data.support.EnvironmentAccessor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* @author Christoph Strobl
* @since 2023/11
*/
public class ExpressionResolverUnitTests {
@Test
void xxx() {
ParserContext parserContext = new DefaultParserContext(new DelegatingEnvironmentAccessor(new StandardEnvironment()), new SpelExpressionParser());
ValueParser parser = ValueParser.create(parserContext);
ParsableStatement statement = parser.prepareStatement("foo-${os.arch}");
// PreparedStatement bindNow = parser.parse("foo-${os.arch}");
statement.evaluate(ValueContext.spelContext(new StandardEvaluationContext()));
}
interface ValueParser {
ParsableStatement prepareStatement(String expression);
static ValueParser create(ParserContext parserContext) {
return new DefaultValueParser(parserContext);
}
}
static class DefaultValueParser implements ValueParser {
private final ParserContext parserContext;
public DefaultValueParser(ParserContext parserContext) {
this.parserContext = parserContext;
}
@Override
public ParsableStatement prepareStatement(String expression) {
return new RawStatement(expression, parserContext);
}
}
interface ParsableStatement {
<T> T evaluate(ValueContext context);
default ParsableStatement transform(Function<ParsableStatement, ParsableStatement> mappingFunction) {
return mappingFunction.apply(this);
}
String toString();
}
class LazyEvaluatingStatement {
}
abstract static class AbstractStatement implements ParsableStatement {
final ParserContext parserContext;
public AbstractStatement(ParserContext parserContext) {
this.parserContext = parserContext;
}
@Override
public <T> T evaluate(ValueContext context) {
ParsableStatement executableStatement = transform(parserContext::resolvePlaceholders)
.transform(parserContext::parseExpressions);
return executableStatement.evaluate(context);
}
abstract String getValue();
}
static class RawStatement extends AbstractStatement {
final String value;
public RawStatement(String value, ParserContext context) {
super(context);
this.value = value;
}
@Override
String getValue() {
return value;
}
}
static class PreparedStatement extends RawStatement {
public PreparedStatement(String value, ParserContext context) {
super(value, context);
}
}
static class ExpressionStatement implements ParsableStatement {
Expression expression;
public ExpressionStatement(Expression expression) {
this.expression = expression;
}
@Override
public <T> T evaluate(ValueContext context) {
if(context instanceof SpELValueContext spelContext) {
return (T) expression.getValue(spelContext.getEvaluationContext());
}
return (T) expression.getValue();
}
@Override
public String toString() {
return expression.getExpressionString();
}
}
interface ParserContext {
ParsableStatement resolvePlaceholders(ParsableStatement statement);
ParsableStatement parseExpressions(ParsableStatement statement);
}
static class DefaultParserContext implements ParserContext {
private final EnvironmentAccessor environmentAccessor;
private final SpelExpressionParser spelExpressionParser;
public DefaultParserContext(EnvironmentAccessor environmentAccessor, SpelExpressionParser spelExpressionParser) {
this.environmentAccessor = environmentAccessor;
this.spelExpressionParser = spelExpressionParser;
}
@Override
public ParsableStatement resolvePlaceholders(ParsableStatement statement) {
return statement.transform(it -> {
if (it instanceof PreparedStatement) {
return it;
}
return new PreparedStatement(environmentAccessor.resolvePlaceholders(it.toString()), this);
});
}
@Override
public ParsableStatement parseExpressions(ParsableStatement statement) {
return statement.transform(it -> {
Expression expression = detectExpression(spelExpressionParser, it.toString());
if (expression == null || expression instanceof LiteralExpression) {
return it;
}
return new ExpressionStatement(expression);
});
}
@Nullable
private static Expression detectExpression(SpelExpressionParser parser, @Nullable String potentialExpression) {
if (!StringUtils.hasText(potentialExpression)) {
return null;
}
Expression expression = parser.parseExpression(potentialExpression, org.springframework.expression.ParserContext.TEMPLATE_EXPRESSION);
return expression instanceof LiteralExpression ? null : expression;
}
}
sealed interface ValueContext permits SpELValueContext {
static ValueContext spelContext(EvaluationContext evaluationContext) {
return new SpELValueContext(evaluationContext);
}
}
static final class SpELValueContext implements ValueContext {
EvaluationContext evaluationContext;
public SpELValueContext(EvaluationContext evaluationContext) {
this.evaluationContext = evaluationContext;
}
EvaluationContext getEvaluationContext() {
return evaluationContext;
}
}
}

View File

@@ -0,0 +1 @@
my-type.name=foo