From a7dcff8c48195745805207e4ee1d3b19e9164e57 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 21 Nov 2023 14:08:34 +0100 Subject: [PATCH 1/3] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8576363d34..ce9bcb2231 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 3.3.0-SNAPSHOT + 3.3.x-2369-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. From 0d1ffe3a0e8880c1c175bf25b2ac1a4c9caa6df8 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 23 Nov 2023 15:27:25 +0100 Subject: [PATCH 2/3] 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 --- .../context/AbstractMappingContext.java | 48 +++- .../model/AbstractPersistentProperty.java | 12 + .../AnnotationBasedPersistentProperty.java | 1 + .../mapping/model/BasicPersistentEntity.java | 24 +- .../model/MutablePersistentEntity.java | 9 + .../data/support/EnvironmentAccessor.java | 30 ++ .../data/support/PlaceholderResolver.java | 25 ++ .../data/util/ExpressionEvaluator.java | 128 +++++++++ .../model/justme/PropertySourceUnitTests.java | 151 ++++++++++ .../util/ExpressionEvaluatorUnitTests.java | 259 ++++++++++++++++++ .../util/ExpressionResolverUnitTests.java | 221 +++++++++++++++ .../resources/persistent-entity.properties | 1 + 12 files changed, 904 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/springframework/data/support/EnvironmentAccessor.java create mode 100644 src/main/java/org/springframework/data/support/PlaceholderResolver.java create mode 100644 src/main/java/org/springframework/data/util/ExpressionEvaluator.java create mode 100644 src/test/java/org/springframework/data/mapping/model/justme/PropertySourceUnitTests.java create mode 100644 src/test/java/org/springframework/data/util/ExpressionEvaluatorUnitTests.java create mode 100644 src/test/java/org/springframework/data/util/ExpressionResolverUnitTests.java create mode 100644 src/test/resources/persistent-entity.properties diff --git a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java index 4c2ecc88f2..a924e3d308 100644 --- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java +++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java @@ -40,8 +40,11 @@ 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.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.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 @@ * @author Christoph Strobl */ public abstract class AbstractMappingContext, P extends PersistentProperty

> - implements MappingContext, ApplicationEventPublisherAware, ApplicationContextAware, InitializingBean { + implements MappingContext, ApplicationEventPublisherAware, ApplicationContextAware, InitializingBean, EnvironmentAware { private static final Log LOGGER = LogFactory.getLog(MappingContext.class); @@ -100,6 +105,7 @@ public abstract class AbstractMappingContext typeInformation) { 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 Collection> getManagedTypes() { @Override public void afterPropertiesSet() { + + if(this.environmentAccessor == null) { + this.environmentAccessor = new DelegatingEnvironmentAccessor(new StandardEnvironment()); + } initialize(); } @@ -579,6 +594,9 @@ private void createAndRegisterProperty(Property input) { return; } + if(property instanceof AbstractPersistentProperty pp) { + pp.setEnvironmentAccessor(environmentAccessor); + } entity.addPersistentProperty(property); if (property.isAssociation()) { @@ -776,4 +794,32 @@ public boolean matches(String name, Class type) { } } } + + /** + * @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); + } + } } diff --git a/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java b/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java index 7ccdff4a5c..d2f754dcf8 100644 --- a/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java @@ -27,6 +27,7 @@ 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

private final Lazy readable; private final boolean immutable; + + private @Nullable EnvironmentAccessor environmentAccessor; + public AbstractPersistentProperty(Property property, PersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { @@ -306,6 +310,14 @@ protected TypeInformation getActualTypeInformation() { 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) { diff --git a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java index 46548a9dbb..9b3337ce75 100644 --- a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java @@ -37,6 +37,7 @@ 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; diff --git a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java index 1f9c15d637..88889a5599 100644 --- a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java @@ -36,6 +36,7 @@ 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> implement private @Nullable P versionProperty; private PersistentPropertyAccessorFactory propertyAccessorFactory; private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT; + private @Nullable EnvironmentAccessor environmentAccessor; private final Lazy typeAlias; private final Lazy isNewStrategy; @@ -205,10 +207,8 @@ public void addPersistentProperty(P property) { 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 void setEvaluationContextProvider(EvaluationContextProvider provider) { 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 @@ protected EvaluationContext getEvaluationContext(Object rootObject, ExpressionDe 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 diff --git a/src/main/java/org/springframework/data/mapping/model/MutablePersistentEntity.java b/src/main/java/org/springframework/data/mapping/model/MutablePersistentEntity.java index d66aa33517..dc41df8bc9 100644 --- a/src/main/java/org/springframework/data/mapping/model/MutablePersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/model/MutablePersistentEntity.java @@ -21,6 +21,7 @@ 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> 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); } diff --git a/src/main/java/org/springframework/data/support/EnvironmentAccessor.java b/src/main/java/org/springframework/data/support/EnvironmentAccessor.java new file mode 100644 index 0000000000..0bf86f06fa --- /dev/null +++ b/src/main/java/org/springframework/data/support/EnvironmentAccessor.java @@ -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); + +} diff --git a/src/main/java/org/springframework/data/support/PlaceholderResolver.java b/src/main/java/org/springframework/data/support/PlaceholderResolver.java new file mode 100644 index 0000000000..5fbdd7fa4e --- /dev/null +++ b/src/main/java/org/springframework/data/support/PlaceholderResolver.java @@ -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); +} diff --git a/src/main/java/org/springframework/data/util/ExpressionEvaluator.java b/src/main/java/org/springframework/data/util/ExpressionEvaluator.java new file mode 100644 index 0000000000..c2a5ba835c --- /dev/null +++ b/src/main/java/org/springframework/data/util/ExpressionEvaluator.java @@ -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 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 getPropertyAccessors() { + return null; + } + + @Override + public List getConstructorResolvers() { + return null; + } + + @Override + public List 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; + } + } + + +} diff --git a/src/test/java/org/springframework/data/mapping/model/justme/PropertySourceUnitTests.java b/src/test/java/org/springframework/data/mapping/model/justme/PropertySourceUnitTests.java new file mode 100644 index 0000000000..cedd331556 --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/model/justme/PropertySourceUnitTests.java @@ -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 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 "}"; + } + }; + } +} diff --git a/src/test/java/org/springframework/data/util/ExpressionEvaluatorUnitTests.java b/src/test/java/org/springframework/data/util/ExpressionEvaluatorUnitTests.java new file mode 100644 index 0000000000..c1c6b11c2a --- /dev/null +++ b/src/test/java/org/springframework/data/util/ExpressionEvaluatorUnitTests.java @@ -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 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 expressionParser; + Supplier source; + Map> expressionCache = new HashMap<>(1, 1F); + + public PreparedExpression(Supplier source) { + this.source = source; + expressionParser = Lazy.of(SpelExpressionParser::new); + } + + PreparedExpression parseWith(SpelExpressionParser expressionParser) { + return parseWith(() -> expressionParser); + } + + PreparedExpression parseWith(Supplier expressionParser) { + this.expressionParser = expressionParser; + return this; + } + + 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) { + return new ExpressionEvaluation(this, evaluationContext); + } + T doEvaluate(Supplier evaluationContext) { + + String expressionString = source.get(); + Optional 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; + boolean cacheResult; + Optional cached; + + public ExpressionEvaluation(PreparedExpression source, Supplier evaluationContext) { + this.source = source; + this.evaluationContext = evaluationContext; + } + + 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 reevaluate() { + + if(cacheResult) { + cached = null; + } + return evaluate(); + } + } + +} diff --git a/src/test/java/org/springframework/data/util/ExpressionResolverUnitTests.java b/src/test/java/org/springframework/data/util/ExpressionResolverUnitTests.java new file mode 100644 index 0000000000..64b82da514 --- /dev/null +++ b/src/test/java/org/springframework/data/util/ExpressionResolverUnitTests.java @@ -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 evaluate(ValueContext context); + + default ParsableStatement transform(Function 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 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 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; + } + } +} diff --git a/src/test/resources/persistent-entity.properties b/src/test/resources/persistent-entity.properties new file mode 100644 index 0000000000..89c335b615 --- /dev/null +++ b/src/test/resources/persistent-entity.properties @@ -0,0 +1 @@ +my-type.name=foo From 1711f496c3e1935f673175b438e598197a0bc804 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 29 Jan 2024 15:45:07 +0100 Subject: [PATCH 3/3] Evolve ValueExpressionParser. Introduce literal, expression and placeholder variants. Add parser for composite expressions. Closes #2369 Original pull request: #3036 --- src/main/antora/modules/ROOT/nav.adoc | 1 + .../modules/ROOT/pages/value-expressions.adoc | 246 +++++++++++++++++ .../expression/CompositeValueExpression.java | 75 +++++ .../DefaultValueEvaluationContext.java | 39 +++ .../DefaultValueExpressionParser.java | 180 ++++++++++++ .../data/expression/ExpressionExpression.java | 55 ++++ .../expression/LiteralValueExpression.java | 42 +++ .../expression/PlaceholderExpression.java | 54 ++++ .../expression/ValueEvaluationContext.java | 58 ++++ .../ValueEvaluationContextProvider.java | 50 ++++ .../data/expression/ValueExpression.java | 65 +++++ .../expression/ValueExpressionParser.java | 58 ++++ .../expression/ValueParserConfiguration.java | 36 +++ .../data/expression/package-info.java | 5 + .../data/mapping/Parameter.java | 63 ++++- .../context/AbstractMappingContext.java | 86 +++--- .../model/AbstractPersistentProperty.java | 12 - .../AnnotationBasedPersistentProperty.java | 6 +- .../mapping/model/BasicPersistentEntity.java | 52 +++- ...achingValueExpressionEvaluatorFactory.java | 88 ++++++ .../model/DefaultSpELExpressionEvaluator.java | 2 + .../model/MutablePersistentEntity.java | 12 +- ...ersistentEntityParameterValueProvider.java | 1 + .../data/mapping/model/SpELContext.java | 4 +- .../model/SpELExpressionEvaluator.java | 13 +- .../SpELExpressionParameterValueProvider.java | 37 +-- .../model/ValueExpressionEvaluator.java} | 22 +- ...ValueExpressionParameterValueProvider.java | 97 +++++++ .../SpelAwareProxyProjectionFactory.java | 22 +- .../SpelEvaluatingMethodInterceptor.java | 6 +- .../data/spel/package-info.java | 5 + .../data/util/ExpressionEvaluator.java | 128 --------- .../expression/ValueEvaluationUnitTests.java | 141 ++++++++++ ...eferredConstructorDiscovererUnitTests.java | 2 +- ...ueExpressionEvaluatorFactoryUnitTests.java | 64 +++++ ...lExpressionParameterProviderUnitTests.java | 14 +- ...essionParameterValueProviderUnitTests.java | 89 ++++++ .../model/justme/PropertySourceUnitTests.java | 151 ---------- .../util/ExpressionEvaluatorUnitTests.java | 259 ------------------ .../util/ExpressionResolverUnitTests.java | 221 --------------- 40 files changed, 1650 insertions(+), 911 deletions(-) create mode 100644 src/main/antora/modules/ROOT/pages/value-expressions.adoc create mode 100644 src/main/java/org/springframework/data/expression/CompositeValueExpression.java create mode 100644 src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java create mode 100644 src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java create mode 100644 src/main/java/org/springframework/data/expression/ExpressionExpression.java create mode 100644 src/main/java/org/springframework/data/expression/LiteralValueExpression.java create mode 100644 src/main/java/org/springframework/data/expression/PlaceholderExpression.java create mode 100644 src/main/java/org/springframework/data/expression/ValueEvaluationContext.java create mode 100644 src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java create mode 100644 src/main/java/org/springframework/data/expression/ValueExpression.java create mode 100644 src/main/java/org/springframework/data/expression/ValueExpressionParser.java create mode 100644 src/main/java/org/springframework/data/expression/ValueParserConfiguration.java create mode 100644 src/main/java/org/springframework/data/expression/package-info.java create mode 100644 src/main/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactory.java rename src/main/java/org/springframework/data/{support/EnvironmentAccessor.java => mapping/model/ValueExpressionEvaluator.java} (63%) create mode 100644 src/main/java/org/springframework/data/mapping/model/ValueExpressionParameterValueProvider.java create mode 100644 src/main/java/org/springframework/data/spel/package-info.java delete mode 100644 src/main/java/org/springframework/data/util/ExpressionEvaluator.java create mode 100644 src/test/java/org/springframework/data/expression/ValueEvaluationUnitTests.java create mode 100644 src/test/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactoryUnitTests.java create mode 100644 src/test/java/org/springframework/data/mapping/model/ValueExpressionParameterValueProviderUnitTests.java delete mode 100644 src/test/java/org/springframework/data/mapping/model/justme/PropertySourceUnitTests.java delete mode 100644 src/test/java/org/springframework/data/util/ExpressionEvaluatorUnitTests.java delete mode 100644 src/test/java/org/springframework/data/util/ExpressionResolverUnitTests.java diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index d5cd181f8e..2f483a6549 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -15,6 +15,7 @@ ** xref:repositories/null-handling.adoc[] ** xref:repositories/projections.adoc[] * xref:query-by-example.adoc[] +* xref:value-expressions.adoc[] * xref:auditing.adoc[] * xref:custom-conversions.adoc[] * xref:entity-callbacks.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/value-expressions.adoc b/src/main/antora/modules/ROOT/pages/value-expressions.adoc new file mode 100644 index 0000000000..752d3d69ec --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/value-expressions.adoc @@ -0,0 +1,246 @@ +[[valueexpressions.fundamentals]] += Value Expressions Fundamentals + +Value Expressions are a combination of {spring-framework-docs}/core/expressions.html[Spring Expression Language (SpEL)] and {spring-framework-docs}/core/beans/environment.html#beans-placeholder-resolution-in-statements[Property Placeholder Resolution]. +They combine powerful evaluation of programmatic expressions with the simplicity to resort to property-placeholder resolution to obtain values from the `Environment` such as configuration properties. + +Expressions are expected to be defined by a trusted input such as an annotation value and not to be determined from user input. + +The following code demonstrates how to use expressions in the context of annotations. + +.Annotation Usage +==== +[source,java] +---- +@Document("orders-#{tenantService.getOrderCollection()}-${tenant-config.suffix}") +class Order { + // … +} +---- +==== + +Value Expressions can be defined from a sole SpEL Expression, a Property Placeholder or a composite expression mixing various expressions including literals. + +.Expression Examples +==== +[source] +---- +#{tenantService.getOrderCollection()} <1> +#{(1+1) + '-hello-world'} <2> +${tenant-config.suffix} <3> +orders-${tenant-config.suffix} <4> +#{tenantService.getOrderCollection()}-${tenant-config.suffix} <5> +---- + +<1> Value Expression using a single SpEL Expression. +<2> Value Expression using a static SpEL Expression evaluating to `2-hello-world`. +<3> Value Expression using a single Property Placeholder. +<4> Composite expression comprised of the literal `orders-` and the Property Placeholder `${tenant-config.suffix}`. +<5> Composite expression using SpEL, Property Placeholders and literals. +==== + +NOTE: Using value expressions introduces a lot of flexibility to your code. +Doing so requires evaluation of the expression on each usage and, therefore, value expression evaluation has an impact on the performance profile. + +[[valueexpressions.api]] +== Parsing and Evaluation + +Value Expressions are parsed by the `ValueExpressionParser` API. +Instances of `ValueExpression` are thread-safe and can be cached for later use to avoid repeated parsing. + +The following example shows the Value Expression API usage: + +.Parsing and Evaluation +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +ValueParserConfiguration configuration = SpelExpressionParser::new; +ValueEvaluationContext context = ValueEvaluationContext.of(environment, evaluationContext); + +ValueExpressionParser parser = ValueExpressionParser.create(configuration); +ValueExpression expression = parser.parse("Hello, World"); +Object result = expression.evaluate(context); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +val configuration = ValueParserConfiguration { SpelExpressionParser() } +val context = ValueEvaluationContext.of(environment, evaluationContext) + +val parser = ValueExpressionParser.create(configuration) +val expression: ValueExpression = parser.parse("Hello, World") +val result: Any = expression.evaluate(context) +---- +====== + +[[valueexpressions.spel]] +== SpEL Expressions + +{spring-framework-docs}/core/expressions.html[SpEL Expressions] follow the Template style where the expression is expected to be enclosed within the `#{…}` format. +Expressions are evaluated using an `EvaluationContext` that is provided by `EvaluationContextProvider`. +The context itself is a powerful `StandardEvaluationContext` allowing a wide range of operations, access to static types and context extensions. + +NOTE: Make sure to parse and evaluate only expressions from trusted sources such as annotations. +Accepting user-provided expressions can create an entry path to exploit the application context and your system resulting in a potential security vulnerability. + +=== Extending the Evaluation Context + +`EvaluationContextProvider` and its reactive variant `ReactiveEvaluationContextProvider` provide access to an `EvaluationContext`. +`ExtensionAwareEvaluationContextProvider` and its reactive variant `ReactiveExtensionAwareEvaluationContextProvider` are default implementations that determine context extensions from an application context, specifically `ListableBeanFactory`. + +Extensions implement either `EvaluationContextExtension` or `ReactiveEvaluationContextExtension` to provide extension support to hydrate `EvaluationContext`. +That are a root object, properties and functions (top-level methods). + +The following example shows a context extension that provides a root object, properties, functions and an aliased function. + +.Implementing a `EvaluationContextExtension` +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Component +public class MyExtension implements EvaluationContextExtension { + + @Override + public String getExtensionId() { + return "my-extension"; + } + + @Override + public Object getRootObject() { + return new CustomExtensionRootObject(); + } + + @Override + public Map getProperties() { + + Map properties = new HashMap<>(); + + properties.put("key", "Hello"); + + return properties; + } + + @Override + public Map getFunctions() { + + Map functions = new HashMap<>(); + + try { + functions.put("aliasedMethod", new Function(getClass().getMethod("extensionMethod"))); + return functions; + } catch (Exception o_O) { + throw new RuntimeException(o_O); + } + } + + public static String extensionMethod() { + return "Hello World"; + } + + public static int add(int i1, int i2) { + return i1 + i2; + } + +} + +public class CustomExtensionRootObject { + + public boolean rootObjectInstanceMethod() { + return true; + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Component +class MyExtension : EvaluationContextExtension { + + override fun getExtensionId(): String { + return "my-extension" + } + + override fun getRootObject(): Any? { + return CustomExtensionRootObject() + } + + override fun getProperties(): Map { + val properties: MutableMap = HashMap() + + properties["key"] = "Hello" + + return properties + } + + override fun getFunctions(): Map { + val functions: MutableMap = HashMap() + + try { + functions["aliasedMethod"] = Function(javaClass.getMethod("extensionMethod")) + return functions + } catch (o_O: Exception) { + throw RuntimeException(o_O) + } + } + + companion object { + fun extensionMethod(): String { + return "Hello World" + } + + fun add(i1: Int, i2: Int): Int { + return i1 + i2 + } + } +} + +class CustomExtensionRootObject { + fun rootObjectInstanceMethod(): Boolean { + return true + } +} +---- +====== + +Once the above shown extension is registered, you can use its exported methods, properties and root object to evaluate SpEL expressions: + +.Expression Evaluation Examples +==== +[source] +---- +#{add(1, 2)} <1> +#{extensionMethod()} <2> +#{aliasedMethod()} <3> +#{key} <4> +#{rootObjectInstanceMethod()} <5> +---- + +<1> Invoke the method `add` declared by `MyExtension` resulting in `3` as the method adds both numeric parameters and returns the sum. +<2> Invoke the method `extensionMethod` declared by `MyExtension` resulting in `Hello World`. +<3> Invoke the method `aliasedMethod`. +The method is exposed as function and redirects into the method `extensionMethod` declared by `MyExtension` resulting in `Hello World`. +<4> Evaluate the `key` property resulting in `Hello`. +<5> Invoke the method `rootObjectInstanceMethod` on the root object instance `CustomExtensionRootObject`. +==== + +You can find real-life context extensions at https://github.com/spring-projects/spring-security/blob/main/data/src/main/java/org/springframework/security/data/repository/query/SecurityEvaluationContextExtension.java[`SecurityEvaluationContextExtension`]. + +[[valueexpressions.property-placeholders]] +== Property Placeholders + +Property placeholders following the form `${…}` refer to properties provided typically by a `PropertySource` through `Environment`. +Properties are useful to resolve against system properties, application configuration files, environment configuration or property sources contributed by secret management systems. +You can find more details on the property placeholders in {spring-framework-docs}/core/beans/annotation-config/value-annotations.html#page-title[Spring Framework's documentation on `@Value` usage]. + + diff --git a/src/main/java/org/springframework/data/expression/CompositeValueExpression.java b/src/main/java/org/springframework/data/expression/CompositeValueExpression.java new file mode 100644 index 0000000000..d2dc64a48a --- /dev/null +++ b/src/main/java/org/springframework/data/expression/CompositeValueExpression.java @@ -0,0 +1,75 @@ +/* + * Copyright 2024 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.expression; + +import java.util.List; + +import org.springframework.data.spel.ExpressionDependencies; + +/** + * Composite {@link ValueExpression} consisting of multiple placeholder, SpEL, and literal expressions. + * + * @param raw + * @param expressions + * @author Mark Paluch + * @since 3.3 + */ +record CompositeValueExpression(String raw, List expressions) implements ValueExpression { + + @Override + public String getExpressionString() { + return raw; + } + + @Override + public ExpressionDependencies getExpressionDependencies() { + + ExpressionDependencies dependencies = ExpressionDependencies.none(); + + for (ValueExpression expression : expressions) { + ExpressionDependencies dependency = expression.getExpressionDependencies(); + if (!dependency.equals(ExpressionDependencies.none())) { + dependencies = dependencies.mergeWith(dependency); + } + } + + return dependencies; + } + + @Override + public boolean isLiteral() { + + for (ValueExpression expression : expressions) { + if (!expression.isLiteral()) { + return false; + } + } + + return true; + } + + @Override + public String evaluate(ValueEvaluationContext context) { + + StringBuilder builder = new StringBuilder(); + + for (ValueExpression expression : expressions) { + builder.append((String) expression.evaluate(context)); + } + + return builder.toString(); + } +} diff --git a/src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java b/src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java new file mode 100644 index 0000000000..d498560135 --- /dev/null +++ b/src/main/java/org/springframework/data/expression/DefaultValueEvaluationContext.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 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.expression; + +import org.springframework.core.env.Environment; +import org.springframework.expression.EvaluationContext; + +/** + * Default {@link ValueEvaluationContext}. + * + * @author Mark Paluch + * @since 3.3 + */ +record DefaultValueEvaluationContext(Environment environment, + EvaluationContext evaluationContext) implements ValueEvaluationContext { + + @Override + public Environment getEnvironment() { + return environment(); + } + + @Override + public EvaluationContext getEvaluationContext() { + return evaluationContext(); + } +} diff --git a/src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java b/src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java new file mode 100644 index 0000000000..ceea642641 --- /dev/null +++ b/src/main/java/org/springframework/data/expression/DefaultValueExpressionParser.java @@ -0,0 +1,180 @@ +/* + * Copyright 2024 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.expression; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.spel.ExpressionDependencies; +import org.springframework.expression.Expression; +import org.springframework.expression.ParseException; +import org.springframework.expression.ParserContext; +import org.springframework.util.Assert; +import org.springframework.util.SystemPropertyUtils; + +/** + * Default {@link ValueExpressionParser} implementation. Instances are thread-safe. + * + * @author Mark Paluch + * @since 3.3 + */ +class DefaultValueExpressionParser implements ValueExpressionParser { + + public static final String PLACEHOLDER_PREFIX = SystemPropertyUtils.PLACEHOLDER_PREFIX; + public static final String EXPRESSION_PREFIX = ParserContext.TEMPLATE_EXPRESSION.getExpressionPrefix(); + public static final char SUFFIX = '}'; + public static final int PLACEHOLDER_PREFIX_LENGTH = PLACEHOLDER_PREFIX.length(); + public static final char[] QUOTE_CHARS = { '\'', '"' }; + + private final ValueParserConfiguration configuration; + + public DefaultValueExpressionParser(ValueParserConfiguration configuration) { + + Assert.notNull(configuration, "ValueParserConfiguration must not be null"); + + this.configuration = configuration; + } + + @Override + public ValueExpression parse(String expressionString) { + + int placerholderIndex = expressionString.indexOf(PLACEHOLDER_PREFIX); + int expressionIndex = expressionString.indexOf(EXPRESSION_PREFIX); + + if (placerholderIndex == -1 && expressionIndex == -1) { + return new LiteralValueExpression(expressionString); + } + + if (placerholderIndex != -1 && expressionIndex == -1 + && findPlaceholderEndIndex(expressionString, placerholderIndex) != expressionString.length()) { + return createPlaceholder(expressionString); + } + + if (placerholderIndex == -1 + && findPlaceholderEndIndex(expressionString, expressionIndex) != expressionString.length()) { + return createExpression(expressionString); + } + + return parseComposite(expressionString, placerholderIndex, expressionIndex); + } + + private CompositeValueExpression parseComposite(String expressionString, int placerholderIndex, int expressionIndex) { + + List expressions = new ArrayList<>(PLACEHOLDER_PREFIX_LENGTH); + int startIndex = getStartIndex(placerholderIndex, expressionIndex); + + if (startIndex != 0) { + expressions.add(new LiteralValueExpression(expressionString.substring(0, startIndex))); + } + + while (startIndex != -1) { + + int endIndex = findPlaceholderEndIndex(expressionString, startIndex); + + if (endIndex == -1) { + throw new ParseException(expressionString, startIndex, + "No ending suffix '}' for expression starting at character %d: %s".formatted(startIndex, + expressionString.substring(startIndex))); + } + + int afterClosingParenthesisIndex = endIndex + 1; + String part = expressionString.substring(startIndex, afterClosingParenthesisIndex); + + if (part.startsWith(PLACEHOLDER_PREFIX)) { + expressions.add(createPlaceholder(part)); + } else { + expressions.add(createExpression(part)); + } + + placerholderIndex = expressionString.indexOf(PLACEHOLDER_PREFIX, endIndex); + expressionIndex = expressionString.indexOf(EXPRESSION_PREFIX, endIndex); + + startIndex = getStartIndex(placerholderIndex, expressionIndex); + + if (startIndex == -1) { + // no next expression but we're capturing everything after the expression as literal. + expressions.add(new LiteralValueExpression(expressionString.substring(afterClosingParenthesisIndex))); + } else { + // capture literal after the expression ends and before the next starts. + expressions + .add(new LiteralValueExpression(expressionString.substring(afterClosingParenthesisIndex, startIndex))); + } + } + + return new CompositeValueExpression(expressionString, expressions); + } + + private static int getStartIndex(int placerholderIndex, int expressionIndex) { + return placerholderIndex != -1 && expressionIndex != -1 ? Math.min(placerholderIndex, expressionIndex) + : placerholderIndex != -1 ? placerholderIndex : expressionIndex; + } + + private PlaceholderExpression createPlaceholder(String part) { + return new PlaceholderExpression(part); + } + + private ExpressionExpression createExpression(String expression) { + + Expression expr = configuration.getExpressionParser().parseExpression(expression, + ParserContext.TEMPLATE_EXPRESSION); + ExpressionDependencies dependencies = ExpressionDependencies.discover(expr); + return new ExpressionExpression(expr, dependencies); + } + + private static int findPlaceholderEndIndex(CharSequence buf, int startIndex) { + + int index = startIndex + PLACEHOLDER_PREFIX_LENGTH; + char quotationChar = 0; + char nestingLevel = 0; + boolean skipEscape = false; + + while (index < buf.length()) { + + char c = buf.charAt(index); + + if (!skipEscape && c == '\\') { + skipEscape = true; + } else if (skipEscape) { + skipEscape = false; + } else if (quotationChar == 0) { + + for (char quoteChar : QUOTE_CHARS) { + if (quoteChar == c) { + quotationChar = c; + break; + } + } + } else if (quotationChar == c) { + quotationChar = 0; + } + + if (!skipEscape && quotationChar == 0) { + + if (nestingLevel != 0 && c == SUFFIX) { + nestingLevel--; + } else if (c == '{') { + nestingLevel++; + } else if (nestingLevel == 0 && c == SUFFIX) { + return index; + } + } + + index++; + } + + return -1; + } +} diff --git a/src/main/java/org/springframework/data/expression/ExpressionExpression.java b/src/main/java/org/springframework/data/expression/ExpressionExpression.java new file mode 100644 index 0000000000..83da26614e --- /dev/null +++ b/src/main/java/org/springframework/data/expression/ExpressionExpression.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024 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.expression; + +import org.springframework.data.spel.ExpressionDependencies; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; + +/** + * SpEL expression. + * + * @param expression + * @author Mark Paluch + * @since 3.3 + */ +record ExpressionExpression(Expression expression, ExpressionDependencies dependencies) implements ValueExpression { + + @Override + public String getExpressionString() { + return expression.getExpressionString(); + } + + @Override + public ExpressionDependencies getExpressionDependencies() { + return dependencies(); + } + + @Override + public boolean isLiteral() { + return false; + } + + @Override + public Object evaluate(ValueEvaluationContext context) { + + EvaluationContext evaluationContext = context.getEvaluationContext(); + if (evaluationContext != null) { + return expression.getValue(evaluationContext); + } + return expression.getValue(); + } +} diff --git a/src/main/java/org/springframework/data/expression/LiteralValueExpression.java b/src/main/java/org/springframework/data/expression/LiteralValueExpression.java new file mode 100644 index 0000000000..764220bfc6 --- /dev/null +++ b/src/main/java/org/springframework/data/expression/LiteralValueExpression.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 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.expression; + +/** + * Literal expression returning the underlying expression string upon evaluation. + * + * @param expression + * @author Mark Paluch + * @since 3.3 + */ +record LiteralValueExpression(String expression) implements ValueExpression { + + @Override + public String getExpressionString() { + return expression; + } + + @Override + public boolean isLiteral() { + return true; + } + + @Override + public String evaluate(ValueEvaluationContext context) { + return expression; + } + +} diff --git a/src/main/java/org/springframework/data/expression/PlaceholderExpression.java b/src/main/java/org/springframework/data/expression/PlaceholderExpression.java new file mode 100644 index 0000000000..1473f1ed28 --- /dev/null +++ b/src/main/java/org/springframework/data/expression/PlaceholderExpression.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 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.expression; + +import org.springframework.core.env.Environment; +import org.springframework.expression.EvaluationException; + +/** + * Property placeholder expression evaluated against a {@link Environment}. + * + * @param expression + * @author Mark Paluch + * @since 3.3 + */ +record PlaceholderExpression(String expression) implements ValueExpression { + + @Override + public String getExpressionString() { + return expression; + } + + @Override + public boolean isLiteral() { + return false; + } + + @Override + public Object evaluate(ValueEvaluationContext context) { + + Environment environment = context.getEnvironment(); + if (environment != null) { + try { + return environment.resolveRequiredPlaceholders(expression); + } catch (IllegalArgumentException e) { + throw new EvaluationException(e.getMessage(), e); + } + } + return expression; + } + +} diff --git a/src/main/java/org/springframework/data/expression/ValueEvaluationContext.java b/src/main/java/org/springframework/data/expression/ValueEvaluationContext.java new file mode 100644 index 0000000000..5be091d8ca --- /dev/null +++ b/src/main/java/org/springframework/data/expression/ValueEvaluationContext.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024 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.expression; + +import org.springframework.core.env.Environment; +import org.springframework.expression.EvaluationContext; +import org.springframework.lang.Nullable; + +/** + * Expressions are executed in an evaluation context. It is in this context that references are resolved when + * encountered during expression evaluation. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 3.3 + */ +public interface ValueEvaluationContext { + + /** + * Returns a new {@link ValueEvaluationContext}. + * + * @param environment + * @param evaluationContext + * @return a new {@link ValueEvaluationContext} for the given environment and evaluation context. + */ + static ValueEvaluationContext of(Environment environment, EvaluationContext evaluationContext) { + return new DefaultValueEvaluationContext(environment, evaluationContext); + } + + /** + * Returns the {@link Environment} if provided. + * + * @return the {@link Environment} or {@literal null}. + */ + @Nullable + Environment getEnvironment(); + + /** + * Returns the {@link EvaluationContext} if provided. + * + * @return the {@link EvaluationContext} or {@literal null}. + */ + @Nullable + EvaluationContext getEvaluationContext(); +} diff --git a/src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java b/src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java new file mode 100644 index 0000000000..05d7d4de2f --- /dev/null +++ b/src/main/java/org/springframework/data/expression/ValueEvaluationContextProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024 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.expression; + +import org.springframework.data.spel.ExpressionDependencies; +import org.springframework.expression.EvaluationContext; + +/** + * SPI to provide to access a centrally defined potentially shared {@link ValueEvaluationContext}. + * + * @author Mark Paluch + * @since 3.3 + */ +public interface ValueEvaluationContextProvider { + + /** + * Return a {@link EvaluationContext} built using the given parameter values. + * + * @param rootObject the root object to set in the {@link EvaluationContext}. + * @return + */ + ValueEvaluationContext getEvaluationContext(Object rootObject); + + /** + * Return a tailored {@link EvaluationContext} built using the given parameter values and considering + * {@link ExpressionDependencies expression dependencies}. The returned {@link EvaluationContext} may contain a + * reduced visibility of methods and properties/fields according to the required {@link ExpressionDependencies + * expression dependencies}. + * + * @param rootObject the root object to set in the {@link EvaluationContext}. + * @param dependencies the requested expression dependencies to be available. + * @return + */ + default ValueEvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) { + return getEvaluationContext(rootObject); + } +} diff --git a/src/main/java/org/springframework/data/expression/ValueExpression.java b/src/main/java/org/springframework/data/expression/ValueExpression.java new file mode 100644 index 0000000000..8f4545bcd8 --- /dev/null +++ b/src/main/java/org/springframework/data/expression/ValueExpression.java @@ -0,0 +1,65 @@ +/* + * Copyright 2024 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.expression; + +import org.springframework.data.spel.ExpressionDependencies; +import org.springframework.expression.EvaluationException; +import org.springframework.lang.Nullable; + +/** + * An expression capable of evaluating itself against context objects. Encapsulates the details of a previously parsed + * expression string. Provides a common abstraction for expression evaluation. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 3.3 + */ +public interface ValueExpression { + + /** + * Returns the original string used to create this expression (unmodified). + * + * @return the original expression string. + */ + String getExpressionString(); + + /** + * Returns the expression dependencies. + * + * @return the dependencies the underlying expression requires. Can be {@link ExpressionDependencies#none()}. + */ + default ExpressionDependencies getExpressionDependencies() { + return ExpressionDependencies.none(); + } + + /** + * Returns whether the expression is a literal expression (that doesn't actually require evaluation). + * + * @return {@code true} if the expression is a literal expression; {@code false} if the expression can yield a + * different result upon {@link #evaluate(ValueEvaluationContext) evaluation}. + */ + boolean isLiteral(); + + /** + * Evaluates this expression using the given evaluation context. + * + * @return the evaluation result. + * @throws EvaluationException if there is a problem during evaluation + */ + @Nullable + Object evaluate(ValueEvaluationContext context) throws EvaluationException; + +} diff --git a/src/main/java/org/springframework/data/expression/ValueExpressionParser.java b/src/main/java/org/springframework/data/expression/ValueExpressionParser.java new file mode 100644 index 0000000000..89f9f65cdc --- /dev/null +++ b/src/main/java/org/springframework/data/expression/ValueExpressionParser.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024 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.expression; + +import org.springframework.expression.ParseException; + +/** + * Parses expression strings into expressions that can be evaluated. Supports parsing expression, configuration + * templates as well as literal strings. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 3.3 + */ +public interface ValueExpressionParser { + + /** + * Creates a new parser to parse expression strings. + * + * @param configuration the parser context configuration. + * @return the parser instance. + */ + static ValueExpressionParser create(ValueParserConfiguration configuration) { + return new DefaultValueExpressionParser(configuration); + } + + /** + * Parses the expression string and return an Expression object you can use for repeated evaluation. + *

+ * Some examples: + * + *

+	 *     #{3 + 4}
+	 *     #{name.firstName}
+	 *     ${key.one}
+	 *     #{name.lastName}-${key.one}
+	 * 
+ * + * @param expressionString the raw expression string to parse. + * @return an evaluator for the parsed expression. + * @throws ParseException an exception occurred during parsing. + */ + ValueExpression parse(String expressionString) throws ParseException; + +} diff --git a/src/main/java/org/springframework/data/expression/ValueParserConfiguration.java b/src/main/java/org/springframework/data/expression/ValueParserConfiguration.java new file mode 100644 index 0000000000..424ece5856 --- /dev/null +++ b/src/main/java/org/springframework/data/expression/ValueParserConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 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.expression; + +import org.springframework.expression.ExpressionParser; + +/** + * Configuration for {@link ValueExpressionParser}. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 3.3 + */ +public interface ValueParserConfiguration { + + /** + * Parser for {@link org.springframework.expression.Expression SpEL expressions}. + * + * @return for {@link org.springframework.expression.Expression SpEL expressions}. + */ + ExpressionParser getExpressionParser(); + +} diff --git a/src/main/java/org/springframework/data/expression/package-info.java b/src/main/java/org/springframework/data/expression/package-info.java new file mode 100644 index 0000000000..35ebb9ed7b --- /dev/null +++ b/src/main/java/org/springframework/data/expression/package-info.java @@ -0,0 +1,5 @@ +/** + * Value Expression implementation. + */ +@org.springframework.lang.NonNullApi +package org.springframework.data.expression; diff --git a/src/main/java/org/springframework/data/mapping/Parameter.java b/src/main/java/org/springframework/data/mapping/Parameter.java index 9738f93fc8..1254c2a37d 100644 --- a/src/main/java/org/springframework/data/mapping/Parameter.java +++ b/src/main/java/org/springframework/data/mapping/Parameter.java @@ -39,11 +39,11 @@ public class Parameter> { private final @Nullable String name; private final TypeInformation type; private final MergedAnnotations annotations; - private final String key; + private final @Nullable String expression; private final @Nullable PersistentEntity entity; private final Lazy enclosingClassCache; - private final Lazy hasSpelExpression; + private final Lazy hasExpression; /** * Creates a new {@link Parameter} with the given name, {@link TypeInformation} as well as an array of @@ -64,7 +64,7 @@ public Parameter(@Nullable String name, TypeInformation type, Annotation[] an this.name = name; this.type = type; this.annotations = MergedAnnotations.from(annotations); - this.key = getValue(this.annotations); + this.expression = getValue(this.annotations); this.entity = entity; this.enclosingClassCache = Lazy.of(() -> { @@ -77,7 +77,7 @@ public Parameter(@Nullable String name, TypeInformation type, Annotation[] an return ClassUtils.isInnerClass(owningType) && type.getType().equals(owningType.getEnclosingClass()); }); - this.hasSpelExpression = Lazy.of(() -> StringUtils.hasText(getSpelExpression())); + this.hasExpression = Lazy.of(() -> StringUtils.hasText(getValueExpression())); } @Nullable @@ -128,21 +128,62 @@ public Class getRawType() { } /** - * Returns the key to be used when looking up a source data structure to populate the actual parameter value. + * Returns the expression to be used when looking up a source data structure to populate the actual parameter value. * - * @return + * @return the expression to be used when looking up a source data structure. + * @deprecated since 3.3, use {@link #getValueExpression()} instead. */ + @Nullable public String getSpelExpression() { - return key; + return getValueExpression(); + } + + /** + * Returns the expression to be used when looking up a source data structure to populate the actual parameter value. + * + * @return the expression to be used when looking up a source data structure. + * @since 3.3 + */ + @Nullable + public String getValueExpression() { + return expression; + } + + /** + * Returns the required expression to be used when looking up a source data structure to populate the actual parameter + * value or throws {@link IllegalStateException} if there's no expression. + * + * @return the expression to be used when looking up a source data structure. + * @since 3.3 + */ + public String getRequiredValueExpression() { + + if (!hasValueExpression()) { + throw new IllegalStateException("No expression associated with this parameter"); + } + + return getValueExpression(); } /** * Returns whether the constructor parameter is equipped with a SpEL expression. * - * @return + * @return {@literal true}} if the parameter is equipped with a SpEL expression. + * @deprecated since 3.3, use {@link #hasValueExpression()} instead. */ + @Deprecated(since = "3.3") public boolean hasSpelExpression() { - return this.hasSpelExpression.get(); + return hasValueExpression(); + } + + /** + * Returns whether the constructor parameter is equipped with a value expression. + * + * @return {@literal true}} if the parameter is equipped with a value expression. + * @since 3.3 + */ + public boolean hasValueExpression() { + return this.hasExpression.get(); } @Override @@ -157,12 +198,12 @@ public boolean equals(@Nullable Object o) { } return Objects.equals(this.name, that.name) && Objects.equals(this.type, that.type) - && Objects.equals(this.key, that.key) && Objects.equals(this.entity, that.entity); + && Objects.equals(this.expression, that.expression) && Objects.equals(this.entity, that.entity); } @Override public int hashCode() { - return Objects.hash(name, type, key, entity); + return Objects.hash(name, type, expression, entity); } /** diff --git a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java index a924e3d308..145ce20c80 100644 --- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java +++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java @@ -35,7 +35,10 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; @@ -44,7 +47,6 @@ 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; @@ -52,7 +54,6 @@ 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; @@ -63,7 +64,6 @@ 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; @@ -94,7 +94,8 @@ * @author Christoph Strobl */ public abstract class AbstractMappingContext, P extends PersistentProperty

> - implements MappingContext, ApplicationEventPublisherAware, ApplicationContextAware, InitializingBean, EnvironmentAware { + implements MappingContext, ApplicationEventPublisherAware, ApplicationContextAware, BeanFactoryAware, + EnvironmentAware, InitializingBean { private static final Log LOGGER = LogFactory.getLog(MappingContext.class); @@ -105,7 +106,7 @@ public abstract class AbstractMappingContext> initialEntitySet) { setManagedTypes(ManagedTypes.fromIterable(initialEntitySet)); @@ -210,6 +239,7 @@ public Collection getPersistentEntities() { } } + @Override @Nullable public E getPersistentEntity(Class type) { return getPersistentEntity(TypeInformation.of(type)); @@ -416,7 +446,9 @@ private E doAddPersistentEntity(TypeInformation typeInformation) { E entity = createPersistentEntity(userTypeInformation); entity.setEvaluationContextProvider(evaluationContextProvider); - entity.setEnvironmentAccessor(environmentAccessor); + if (environment != null) { + entity.setEnvironment(environment); + } // Eagerly cache the entity as we might have to find it during recursive lookups. persistentEntities.put(userTypeInformation, Optional.of(entity)); @@ -488,10 +520,6 @@ public Collection> getManagedTypes() { @Override public void afterPropertiesSet() { - - if(this.environmentAccessor == null) { - this.environmentAccessor = new DelegatingEnvironmentAccessor(new StandardEnvironment()); - } initialize(); } @@ -594,9 +622,6 @@ private void createAndRegisterProperty(Property input) { return; } - if(property instanceof AbstractPersistentProperty pp) { - pp.setEnvironmentAccessor(environmentAccessor); - } entity.addPersistentProperty(property); if (property.isAssociation()) { @@ -795,31 +820,4 @@ public boolean matches(String name, Class type) { } } - /** - * @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); - } - } } diff --git a/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java b/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java index d2f754dcf8..7ccdff4a5c 100644 --- a/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java @@ -27,7 +27,6 @@ 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; @@ -75,9 +74,6 @@ public abstract class AbstractPersistentProperty

private final Lazy readable; private final boolean immutable; - - private @Nullable EnvironmentAccessor environmentAccessor; - public AbstractPersistentProperty(Property property, PersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { @@ -310,14 +306,6 @@ protected TypeInformation getActualTypeInformation() { 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) { diff --git a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java index 9b3337ce75..4bd60cc391 100644 --- a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java @@ -37,8 +37,6 @@ 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; import org.springframework.data.util.ReflectionUtils; @@ -198,10 +196,12 @@ public boolean isTransient() { return isTransient.get(); } + @Override public boolean isIdProperty() { return isId.get(); } + @Override public boolean isVersionProperty() { return isVersion.get(); } @@ -227,6 +227,7 @@ public boolean isWritable() { * @param annotationType must not be {@literal null}. * @return {@literal null} if annotation type not found on property. */ + @Override @Nullable public A findAnnotation(Class annotationType) { @@ -268,6 +269,7 @@ public A findPropertyOrOwnerAnnotation(Class annotatio * @param annotationType the annotation type to look up. * @return */ + @Override public boolean isAnnotationPresent(Class annotationType) { return doFindAnnotation(annotationType).isPresent(); } diff --git a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java index 88889a5599..edf0917225 100644 --- a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java @@ -30,13 +30,14 @@ import java.util.stream.Collectors; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.env.Environment; import org.springframework.data.annotation.Immutable; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.domain.Persistable; +import org.springframework.data.expression.ValueEvaluationContext; 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; @@ -80,7 +81,7 @@ public class BasicPersistentEntity> implement private @Nullable P versionProperty; private PersistentPropertyAccessorFactory propertyAccessorFactory; private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT; - private @Nullable EnvironmentAccessor environmentAccessor; + private @Nullable Environment environment = null; private final Lazy typeAlias; private final Lazy isNewStrategy; @@ -148,36 +149,44 @@ public boolean isCreatorArgument(PersistentProperty property) { return creator != null && creator.isCreatorParameter(property); } + @Override public boolean isIdProperty(PersistentProperty property) { return idProperty != null && idProperty.equals(property); } + @Override public boolean isVersionProperty(PersistentProperty property) { return versionProperty != null && versionProperty.equals(property); } + @Override public String getName() { return getType().getName(); } + @Override @Nullable public P getIdProperty() { return idProperty; } + @Override @Nullable public P getVersionProperty() { return versionProperty; } + @Override public boolean hasIdProperty() { return idProperty != null; } + @Override public boolean hasVersionProperty() { return versionProperty != null; } + @Override public void addPersistentProperty(P property) { Assert.notNull(property, "Property must not be null"); @@ -220,9 +229,13 @@ public void setEvaluationContextProvider(EvaluationContextProvider provider) { this.evaluationContextProvider = provider; } + /** + * @param environment the {@code Environment} that this component runs in. + * @since 3.3 + */ @Override - public void setEnvironmentAccessor(EnvironmentAccessor accessor) { - this.environmentAccessor = accessor; + public void setEnvironment(Environment environment) { + this.environment = environment; } /** @@ -248,6 +261,7 @@ protected P returnPropertyIfBetterIdPropertyCandidateOrNull(P property) { return property; } + @Override public void addAssociation(Association

association) { Assert.notNull(association, "Association must not be null"); @@ -283,18 +297,22 @@ private List

doFindPersistentProperty(Class annotationT .filter(it -> it.isAnnotationPresent(annotationType)).collect(Collectors.toList()); } + @Override public Class getType() { return information.getType(); } + @Override public Alias getTypeAlias() { return typeAlias.get(); } + @Override public TypeInformation getTypeInformation() { return information; } + @Override public void doWithProperties(PropertyHandler

handler) { Assert.notNull(handler, "PropertyHandler must not be null"); @@ -314,6 +332,7 @@ public void doWithProperties(SimplePropertyHandler handler) { } } + @Override public void doWithAssociations(AssociationHandler

handler) { Assert.notNull(handler, "Handler must not be null"); @@ -323,6 +342,7 @@ public void doWithAssociations(AssociationHandler

handler) { } } + @Override public void doWithAssociations(SimpleAssociationHandler handler) { Assert.notNull(handler, "Handler must not be null"); @@ -350,6 +370,7 @@ private Optional doFindAnnotation(Class annotationT it -> Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(getType(), it))); } + @Override public void verify() { if (comparator != null) { @@ -449,14 +470,26 @@ protected EvaluationContext getEvaluationContext(Object rootObject, ExpressionDe } /** - * Obtain the {@link EnvironmentAccessor} providing access to the current - * {@link org.springframework.core.env.Environment}. + * Obtain a {@link ValueEvaluationContext} for a {@code rootObject}. * - * @return never {@literal null}. + * @param rootObject must not be {@literal null}. + * @return the evaluation context including all potential extensions. * @since 3.3 */ - protected EnvironmentAccessor getEnvironmentAccessor() { - return environmentAccessor; + protected ValueEvaluationContext getValueEvaluationContext(Object rootObject) { + return ValueEvaluationContext.of(this.environment, getEvaluationContext(rootObject)); + } + + /** + * Obtain a {@link ValueEvaluationContext} for a {@code rootObject} given {@link ExpressionDependencies}. + * + * @param rootObject must not be {@literal null}. + * @param dependencies must not be {@literal null}. + * @return the evaluation context with extensions loaded that satisfy {@link ExpressionDependencies}. + * @since 3.3 + */ + protected ValueEvaluationContext getValueEvaluationContext(Object rootObject, ExpressionDependencies dependencies) { + return ValueEvaluationContext.of(this.environment, getEvaluationContext(rootObject, dependencies)); } /** @@ -534,6 +567,7 @@ private static final class AssociationComparator

this.delegate = delegate; } + @Override public int compare(@Nullable Association

left, @Nullable Association

right) { if (left == null) { diff --git a/src/main/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactory.java b/src/main/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactory.java new file mode 100644 index 0000000000..91be052dc2 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactory.java @@ -0,0 +1,88 @@ +/* + * Copyright 2024 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.mapping.model; + +import org.springframework.core.env.EnvironmentCapable; +import org.springframework.data.expression.ValueEvaluationContext; +import org.springframework.data.expression.ValueEvaluationContextProvider; +import org.springframework.data.expression.ValueExpression; +import org.springframework.data.expression.ValueExpressionParser; +import org.springframework.data.spel.EvaluationContextProvider; +import org.springframework.data.spel.ExpressionDependencies; +import org.springframework.expression.ExpressionParser; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentLruCache; + +/** + * Factory to create a ValueExpressionEvaluator + * + * @author Mark Paluch + * @since 3.3 + */ +public class CachingValueExpressionEvaluatorFactory implements ValueEvaluationContextProvider { + + private final ConcurrentLruCache expressionCache; + private final EnvironmentCapable environmentProvider; + private final EvaluationContextProvider evaluationContextProvider; + + public CachingValueExpressionEvaluatorFactory(ExpressionParser expressionParser, + EnvironmentCapable environmentProvider, EvaluationContextProvider evaluationContextProvider) { + this(expressionParser, environmentProvider, evaluationContextProvider, 256); + } + + public CachingValueExpressionEvaluatorFactory(ExpressionParser expressionParser, + EnvironmentCapable environmentProvider, EvaluationContextProvider evaluationContextProvider, int cacheSize) { + + Assert.notNull(expressionParser, "ExpressionParser must not be null"); + + ValueExpressionParser parser = ValueExpressionParser.create(() -> expressionParser); + this.expressionCache = new ConcurrentLruCache<>(cacheSize, parser::parse); + this.environmentProvider = environmentProvider; + this.evaluationContextProvider = evaluationContextProvider; + } + + @Override + public ValueEvaluationContext getEvaluationContext(Object rootObject) { + return ValueEvaluationContext.of(environmentProvider.getEnvironment(), + evaluationContextProvider.getEvaluationContext(rootObject)); + } + + @Override + public ValueEvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) { + return ValueEvaluationContext.of(environmentProvider.getEnvironment(), + evaluationContextProvider.getEvaluationContext(rootObject, dependencies)); + } + + /** + * Creates a new {@link ValueExpressionEvaluator} using the given {@code source} as root object. + * + * @param source the root object for evaluating the expression. + * @return a new {@link ValueExpressionEvaluator} to evaluate the expression in the context of the given + * {@code source} object. + */ + public ValueExpressionEvaluator create(Object source) { + + return new ValueExpressionEvaluator() { + @SuppressWarnings("unchecked") + @Override + public T evaluate(String expression) { + ValueExpression valueExpression = expressionCache.get(expression); + return (T) valueExpression.evaluate(getEvaluationContext(source, valueExpression.getExpressionDependencies())); + } + }; + } + +} diff --git a/src/main/java/org/springframework/data/mapping/model/DefaultSpELExpressionEvaluator.java b/src/main/java/org/springframework/data/mapping/model/DefaultSpELExpressionEvaluator.java index 693056b590..9b88632a43 100644 --- a/src/main/java/org/springframework/data/mapping/model/DefaultSpELExpressionEvaluator.java +++ b/src/main/java/org/springframework/data/mapping/model/DefaultSpELExpressionEvaluator.java @@ -28,7 +28,9 @@ * {@link SpelExpressionParser} and {@link EvaluationContext}. * * @author Oliver Gierke + * @deprecated since 3.3, use {@link CachingValueExpressionEvaluatorFactory} instead. */ +@Deprecated(since = "3.3") public class DefaultSpELExpressionEvaluator implements SpELExpressionEvaluator { private final Object source; diff --git a/src/main/java/org/springframework/data/mapping/model/MutablePersistentEntity.java b/src/main/java/org/springframework/data/mapping/model/MutablePersistentEntity.java index dc41df8bc9..d5c493b5a5 100644 --- a/src/main/java/org/springframework/data/mapping/model/MutablePersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/model/MutablePersistentEntity.java @@ -15,13 +15,13 @@ */ package org.springframework.data.mapping.model; +import org.springframework.context.EnvironmentAware; 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.mapping.PersistentPropertyAccessor; import org.springframework.data.spel.EvaluationContextProvider; -import org.springframework.data.support.EnvironmentAccessor; /** * Interface capturing mutator methods for {@link PersistentEntity}s. @@ -29,7 +29,8 @@ * @author Oliver Gierke * @author Mark Paluch */ -public interface MutablePersistentEntity> extends PersistentEntity { +public interface MutablePersistentEntity> + extends PersistentEntity, EnvironmentAware { /** * Adds a {@link PersistentProperty} to the entity. @@ -68,11 +69,4 @@ public interface MutablePersistentEntity> ext */ 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); } diff --git a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java index bfc4762c00..708c91dd6d 100644 --- a/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java +++ b/src/main/java/org/springframework/data/mapping/model/PersistentEntityParameterValueProvider.java @@ -45,6 +45,7 @@ public PersistentEntityParameterValueProvider(PersistentEntity entity, Pro this.parent = parent; } + @Override @Nullable @SuppressWarnings("unchecked") public T getParameterValue(Parameter parameter) { diff --git a/src/main/java/org/springframework/data/mapping/model/SpELContext.java b/src/main/java/org/springframework/data/mapping/model/SpELContext.java index 113e643ca0..005333c73a 100644 --- a/src/main/java/org/springframework/data/mapping/model/SpELContext.java +++ b/src/main/java/org/springframework/data/mapping/model/SpELContext.java @@ -17,6 +17,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.expression.EvaluationContext; import org.springframework.expression.ExpressionParser; import org.springframework.expression.PropertyAccessor; @@ -30,7 +31,7 @@ * * @author Oliver Gierke */ -public class SpELContext { +public class SpELContext implements EvaluationContextProvider { private final SpelExpressionParser parser; private final PropertyAccessor accessor; @@ -90,6 +91,7 @@ public ExpressionParser getParser() { return this.parser; } + @Override public EvaluationContext getEvaluationContext(Object source) { StandardEvaluationContext evaluationContext = new StandardEvaluationContext(source); diff --git a/src/main/java/org/springframework/data/mapping/model/SpELExpressionEvaluator.java b/src/main/java/org/springframework/data/mapping/model/SpELExpressionEvaluator.java index 8e559c53e0..92aa4a2c76 100644 --- a/src/main/java/org/springframework/data/mapping/model/SpELExpressionEvaluator.java +++ b/src/main/java/org/springframework/data/mapping/model/SpELExpressionEvaluator.java @@ -15,21 +15,12 @@ */ package org.springframework.data.mapping.model; -import org.springframework.lang.Nullable; - /** * SPI for components that can evaluate Spring EL expressions. * * @author Oliver Gierke */ -public interface SpELExpressionEvaluator { +@Deprecated(since = "3.3") +public interface SpELExpressionEvaluator extends ValueExpressionEvaluator { - /** - * Evaluates the given expression. - * - * @param expression - * @return - */ - @Nullable - T evaluate(String expression); } diff --git a/src/main/java/org/springframework/data/mapping/model/SpELExpressionParameterValueProvider.java b/src/main/java/org/springframework/data/mapping/model/SpELExpressionParameterValueProvider.java index 71df544f39..117f0a5cea 100644 --- a/src/main/java/org/springframework/data/mapping/model/SpELExpressionParameterValueProvider.java +++ b/src/main/java/org/springframework/data/mapping/model/SpELExpressionParameterValueProvider.java @@ -16,9 +16,7 @@ package org.springframework.data.mapping.model; import org.springframework.core.convert.ConversionService; -import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.PersistentProperty; -import org.springframework.lang.Nullable; /** * {@link ParameterValueProvider} that can be used to front a {@link ParameterValueProvider} delegate to prefer a SpEL @@ -26,43 +24,16 @@ * * @author Oliver Gierke * @author Mark Paluch + * @deprecated since 3.3, use {@link ValueExpressionParameterValueProvider} instead. */ +@Deprecated(since = "3.3") public class SpELExpressionParameterValueProvider

> - implements ParameterValueProvider

{ - - private final SpELExpressionEvaluator evaluator; - private final ConversionService conversionService; - private final ParameterValueProvider

delegate; + extends ValueExpressionParameterValueProvider

implements ParameterValueProvider

{ public SpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator, ConversionService conversionService, ParameterValueProvider

delegate) { - this.evaluator = evaluator; - this.conversionService = conversionService; - this.delegate = delegate; - } - - @Nullable - public T getParameterValue(Parameter parameter) { - - if (!parameter.hasSpelExpression()) { - return delegate == null ? null : delegate.getParameterValue(parameter); - } - - Object object = evaluator.evaluate(parameter.getSpelExpression()); - return object == null ? null : potentiallyConvertSpelValue(object, parameter); + super(evaluator, conversionService, delegate); } - /** - * Hook to allow to massage the value resulting from the Spel expression evaluation. Default implementation will - * leverage the configured {@link ConversionService} to massage the value into the parameter type. - * - * @param object the value to massage, will never be {@literal null}. - * @param parameter the {@link Parameter} we create the value for - * @return - */ - @Nullable - protected T potentiallyConvertSpelValue(Object object, Parameter parameter) { - return conversionService.convert(object, parameter.getRawType()); - } } diff --git a/src/main/java/org/springframework/data/support/EnvironmentAccessor.java b/src/main/java/org/springframework/data/mapping/model/ValueExpressionEvaluator.java similarity index 63% rename from src/main/java/org/springframework/data/support/EnvironmentAccessor.java rename to src/main/java/org/springframework/data/mapping/model/ValueExpressionEvaluator.java index 0bf86f06fa..bac22cd7ec 100644 --- a/src/main/java/org/springframework/data/support/EnvironmentAccessor.java +++ b/src/main/java/org/springframework/data/mapping/model/ValueExpressionEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2012-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. @@ -13,18 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -package org.springframework.data.support; +package org.springframework.data.mapping.model; import org.springframework.lang.Nullable; /** - * @author Christoph Strobl - * @since 3.3 + * SPI for components that can evaluate Value expressions. + * + * @author Mark Paluch + * @since 3.2 */ -public interface EnvironmentAccessor extends PlaceholderResolver { +public interface ValueExpressionEvaluator { + /** + * Evaluates the given expression. + * + * @param expression + * @return + */ @Nullable - String getProperty(String key); - + T evaluate(String expression); } diff --git a/src/main/java/org/springframework/data/mapping/model/ValueExpressionParameterValueProvider.java b/src/main/java/org/springframework/data/mapping/model/ValueExpressionParameterValueProvider.java new file mode 100644 index 0000000000..96ba2f11a8 --- /dev/null +++ b/src/main/java/org/springframework/data/mapping/model/ValueExpressionParameterValueProvider.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-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.mapping.model; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.mapping.Parameter; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link ParameterValueProvider} that can be used to front a {@link ParameterValueProvider} delegate to prefer a SpEL + * expression evaluation over directly resolving the parameter value with the delegate. + * + * @author Oliver Gierke + * @author Mark Paluch + * @since 3.3 + */ +public class ValueExpressionParameterValueProvider

> + implements ParameterValueProvider

{ + + private final ValueExpressionEvaluator evaluator; + private final ConversionService conversionService; + private final ParameterValueProvider

delegate; + + public ValueExpressionParameterValueProvider(ValueExpressionEvaluator evaluator, ConversionService conversionService, + ParameterValueProvider

delegate) { + + Assert.notNull(evaluator, "ValueExpressionEvaluator must not be null"); + Assert.notNull(conversionService, "ConversionService must not be null"); + Assert.notNull(delegate, "Delegate must not be null"); + + this.evaluator = evaluator; + this.conversionService = conversionService; + this.delegate = delegate; + } + + @Override + @Nullable + public T getParameterValue(Parameter parameter) { + + if (!parameter.hasValueExpression()) { + return delegate.getParameterValue(parameter); + } + + // retain compatibility where we accepted bare expressions in @Value + String rawExpressionString = parameter.getRequiredValueExpression(); + String expressionString = rawExpressionString.contains("#{") || rawExpressionString.contains("${") + ? rawExpressionString + : "#{" + rawExpressionString + "}"; + + Object object = evaluator.evaluate(expressionString); + return object == null ? null : potentiallyConvertExpressionValue(object, parameter); + } + + /** + * Hook to allow to massage the value resulting from the Spel expression evaluation. Default implementation will + * leverage the configured {@link ConversionService} to massage the value into the parameter type. + * + * @param object the value to massage, will never be {@literal null}. + * @param parameter the {@link Parameter} we create the value for + * @return the converted parameter value. + * @deprecated since 3.3, use {@link #potentiallyConvertExpressionValue(Object, Parameter)} instead. + */ + @Nullable + @Deprecated(since = "3.3") + protected T potentiallyConvertSpelValue(Object object, Parameter parameter) { + return conversionService.convert(object, parameter.getRawType()); + } + + /** + * Hook to allow to massage the value resulting from the Spel expression evaluation. Default implementation will + * leverage the configured {@link ConversionService} to massage the value into the parameter type. + * + * @param object the value to massage, will never be {@literal null}. + * @param parameter the {@link Parameter} we create the value for + * @return the converted parameter value. + * @since 3.3 + */ + @Nullable + protected T potentiallyConvertExpressionValue(Object object, Parameter parameter) { + return potentiallyConvertSpelValue(object, parameter); + } +} diff --git a/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java b/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java index bc63daffde..edf3150ece 100644 --- a/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java +++ b/src/main/java/org/springframework/data/projection/SpelAwareProxyProjectionFactory.java @@ -28,6 +28,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.util.AnnotationDetectionMethodCallback; +import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -46,10 +47,29 @@ public class SpelAwareProxyProjectionFactory extends ProxyProjectionFactory implements BeanFactoryAware { private final Map, Boolean> typeCache = new ConcurrentHashMap<>(); - private final SpelExpressionParser parser = new SpelExpressionParser(); + private final ExpressionParser parser; private @Nullable BeanFactory beanFactory; + /** + * Create a new {@link SpelAwareProxyProjectionFactory}. + */ + public SpelAwareProxyProjectionFactory() { + this(new SpelExpressionParser()); + } + + /** + * Create a new {@link SpelAwareProxyProjectionFactory} for a given {@link ExpressionParser}. + * + * @param parser the parser to use. + * @since 3.3 + */ + public SpelAwareProxyProjectionFactory(ExpressionParser parser) { + + Assert.notNull(parser, "ExpressionParser must not be null"); + this.parser = parser; + } + @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; diff --git a/src/main/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptor.java b/src/main/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptor.java index a2039da1eb..c5d5fc3641 100644 --- a/src/main/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptor.java +++ b/src/main/java/org/springframework/data/projection/SpelEvaluatingMethodInterceptor.java @@ -30,9 +30,9 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParserContext; import org.springframework.expression.common.TemplateParserContext; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -71,7 +71,7 @@ class SpelEvaluatingMethodInterceptor implements MethodInterceptor { * @param targetInterface must not be {@literal null}. */ public SpelEvaluatingMethodInterceptor(MethodInterceptor delegate, Object target, @Nullable BeanFactory beanFactory, - SpelExpressionParser parser, Class targetInterface) { + ExpressionParser parser, Class targetInterface) { Assert.notNull(delegate, "Delegate MethodInterceptor must not be null"); Assert.notNull(target, "Target object must not be null"); @@ -105,7 +105,7 @@ public SpelEvaluatingMethodInterceptor(MethodInterceptor delegate, Object target * @return */ private static Map potentiallyCreateExpressionsForMethodsOnTargetInterface( - SpelExpressionParser parser, Class targetInterface) { + ExpressionParser parser, Class targetInterface) { Map expressions = new HashMap<>(); diff --git a/src/main/java/org/springframework/data/spel/package-info.java b/src/main/java/org/springframework/data/spel/package-info.java new file mode 100644 index 0000000000..d45330a59c --- /dev/null +++ b/src/main/java/org/springframework/data/spel/package-info.java @@ -0,0 +1,5 @@ +/** + * SpEL support. + */ +@org.springframework.lang.NonNullApi +package org.springframework.data.spel; diff --git a/src/main/java/org/springframework/data/util/ExpressionEvaluator.java b/src/main/java/org/springframework/data/util/ExpressionEvaluator.java deleted file mode 100644 index c2a5ba835c..0000000000 --- a/src/main/java/org/springframework/data/util/ExpressionEvaluator.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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 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 getPropertyAccessors() { - return null; - } - - @Override - public List getConstructorResolvers() { - return null; - } - - @Override - public List 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; - } - } - - -} diff --git a/src/test/java/org/springframework/data/expression/ValueEvaluationUnitTests.java b/src/test/java/org/springframework/data/expression/ValueEvaluationUnitTests.java new file mode 100644 index 0000000000..59269b2b4a --- /dev/null +++ b/src/test/java/org/springframework/data/expression/ValueEvaluationUnitTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2024 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.expression; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.ParseException; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +/** + * Unit tests for {@link ValueExpression} and its parsing. + * + * @author Christoph Strobl + * @author Mark Paluch + */ +public class ValueEvaluationUnitTests { + + private ValueEvaluationContext evaluationContext; + + @BeforeEach + void setUp() { + + MapPropertySource propertySource = new MapPropertySource("map", Map.of("env.key.one", "value", "env.key.two", 42L)); + StandardEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().addFirst(propertySource); + + this.evaluationContext = new ValueEvaluationContext() { + @Override + public Environment getEnvironment() { + return environment; + } + + @Override + public EvaluationContext getEvaluationContext() { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("contextVar", "contextVal"); + + return context; + } + }; + } + + @Test // GH-2369 + void shouldParseAndEvaluateExpressions() { + + assertThat(eval("foo")).isEqualTo("foo"); + assertThat(eval("${env.key.one}")).isEqualTo("value"); + assertThat(eval("${env.key.two}")).isEqualTo("42"); + assertThat(eval("${env.key.one")).isEqualTo("${env.key.one"); + assertThat(eval("#{'foo'}-")).isEqualTo("foo-"); + assertThat(eval("#{'fo\"o'}-")).isEqualTo("fo\"o-"); + assertThat(eval("#{\"foo\"}-")).isEqualTo("foo-"); + assertThat(eval("#{\"fo'o\"}-")).isEqualTo("fo'o-"); + assertThat(eval("${env.key.one}-")).isEqualTo("value-"); + assertThat(eval("#{#contextVar}")).isEqualTo("contextVal"); + assertThat(eval("${env.does.not.exist:some-default}-")).isEqualTo("some-default-"); + + assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> eval("${env.does.not.exist}-")) + .withMessageContaining("Could not resolve placeholder 'env.does.not.exist'"); + } + + @Test // GH-2369 + void shouldParseLiteral() { + + ValueParserConfiguration parserContext = () -> new SpelExpressionParser(); + ValueExpressionParser parser = ValueExpressionParser.create(parserContext); + + assertThat(parser.parse("#{'foo'}-${key.one}").isLiteral()).isFalse(); + assertThat(parser.parse("foo").isLiteral()).isTrue(); + assertThat(parser.parse("#{'foo'}").isLiteral()).isFalse(); + assertThat(parser.parse("${key.one}").isLiteral()).isFalse(); + } + + @Test // GH-2369 + void shouldParseCompoundExpression() { + + assertThat(eval("#{'foo'}-")).isEqualTo("foo-"); + assertThat(eval("#{'fo\"o'}-")).isEqualTo("fo\"o-"); + assertThat(eval("#{\"foo\"}-")).isEqualTo("foo-"); + assertThat(eval("#{\"fo'o\"}-")).isEqualTo("fo'o-"); + assertThat(eval("${env.key.one}-")).isEqualTo("value-"); + assertThat(eval("${env.key.one}-and-some-#{'foo'}-#{#contextVar}")).isEqualTo("value-and-some-foo-contextVal"); + } + + @Test // GH-2369 + void shouldRaiseParseException() { + + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> eval("#{'foo'}-${key.one")).withMessageContaining( + "Expression [#{'foo'}-${key.one] @9: No ending suffix '}' for expression starting at character 9: ${key.one"); + + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> eval("#{'foo'}-${env.key.one")); + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> eval("#{'foo'")); + + assertThatExceptionOfType(ParseException.class).isThrownBy(() -> eval("#{#foo - ${numeric.key}}")) + .withMessageContaining("[#foo - ${numeric.key}]"); + + } + + @Test // GH-2369 + void shouldParseQuoted() { + + assertThat(eval("#{(1+1) + '-foo'+\"-bar\"}")).isEqualTo("2-foo-bar"); + assertThat(eval("#{(1+1) + '-foo'+\"-bar}\"}")).isEqualTo("2-foo-bar}"); + assertThat(eval("#{(1+1) + '-foo}'}")).isEqualTo("2-foo}"); + assertThat(eval("#{(1+1) + '-foo\\}'}")).isEqualTo("2-foo\\}"); + assertThat(eval("#{(1+1) + \"-foo'}\" + '-bar}'}")).isEqualTo("2-foo'}-bar}"); + } + + private String eval(String expressionString) { + + ValueParserConfiguration parserContext = SpelExpressionParser::new; + + ValueExpressionParser parser = ValueExpressionParser.create(parserContext); + return (String) parser.parse(expressionString).evaluate(evaluationContext); + } + +} diff --git a/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java b/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java index 2a4ac1028e..08853a7e67 100755 --- a/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java @@ -147,7 +147,7 @@ void detectsMetaAnnotatedValueAnnotation() { var constructor = PreferredConstructorDiscoverer.discover(ClassWithMetaAnnotatedParameter.class); assertThat(constructor).isNotNull(); - assertThat(constructor.getParameters().get(0).getSpelExpression()).isEqualTo("${hello-world}"); + assertThat(constructor.getParameters().get(0).getValueExpression()).isEqualTo("${hello-world}"); assertThat(constructor.getParameters().get(0).getAnnotations()).isNotNull(); } diff --git a/src/test/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactoryUnitTests.java b/src/test/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactoryUnitTests.java new file mode 100644 index 0000000000..41670d46c0 --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/model/CachingValueExpressionEvaluatorFactoryUnitTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2024 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.mapping.model; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.core.env.StandardEnvironment; +import org.springframework.data.spel.EvaluationContextProvider; +import org.springframework.data.spel.ExpressionDependencies; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +/** + * Unit tests for {@link CachingValueExpressionEvaluatorFactory}. + * + * @author Mark Paluch + */ +class CachingValueExpressionEvaluatorFactoryUnitTests { + + @Test // GH-2369 + void shouldEvaluateWithDependencies() { + + EvaluationContextProvider contextProviderMock = mock(EvaluationContextProvider.class); + CachingValueExpressionEvaluatorFactory factory = new CachingValueExpressionEvaluatorFactory( + new SpelExpressionParser(), StandardEnvironment::new, contextProviderMock); + ValueExpressionEvaluator evaluator = factory.create(new MyRoot("foo")); + + when(contextProviderMock.getEvaluationContext(any(), any(ExpressionDependencies.class))) + .then(invocation -> new StandardEvaluationContext(invocation.getArgument(0))); + + Object result = evaluator.evaluate("#{root}"); + + assertThat(result).isEqualTo("foo"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ExpressionDependencies.class); + + verify(contextProviderMock).getEvaluationContext(any(), captor.capture()); + + ExpressionDependencies value = captor.getValue(); + assertThat(value).hasSize(1).isNotEqualTo(ExpressionDependencies.none()); + } + + record MyRoot(String root) { + + } + +} diff --git a/src/test/java/org/springframework/data/mapping/model/SpelExpressionParameterProviderUnitTests.java b/src/test/java/org/springframework/data/mapping/model/SpelExpressionParameterProviderUnitTests.java index 04425f395e..929515e2de 100755 --- a/src/test/java/org/springframework/data/mapping/model/SpelExpressionParameterProviderUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/SpelExpressionParameterProviderUnitTests.java @@ -53,7 +53,7 @@ void setUp() { provider = new SpELExpressionParameterValueProvider<>(evaluator, conversionService, delegate); parameter = mock(Parameter.class); - when(parameter.hasSpelExpression()).thenReturn(true); + when(parameter.hasValueExpression()).thenReturn(true); when(parameter.getRawType()).thenReturn(Object.class); } @@ -62,7 +62,7 @@ void setUp() { void delegatesIfParameterDoesNotHaveASpELExpression() { Parameter parameter = mock(Parameter.class); - when(parameter.hasSpelExpression()).thenReturn(false); + when(parameter.hasValueExpression()).thenReturn(false); provider.getParameterValue(parameter); verify(delegate, times(1)).getParameterValue(parameter); @@ -72,17 +72,17 @@ void delegatesIfParameterDoesNotHaveASpELExpression() { @Test void evaluatesSpELExpression() { - when(parameter.getSpelExpression()).thenReturn("expression"); + when(parameter.getRequiredValueExpression()).thenReturn("expression"); provider.getParameterValue(parameter); verify(delegate, times(0)).getParameterValue(parameter); - verify(evaluator, times(1)).evaluate("expression"); + verify(evaluator, times(1)).evaluate("#{expression}"); } @Test void handsSpELValueToConversionService() { - doReturn("source").when(parameter).getSpelExpression(); + doReturn("source").when(parameter).getRequiredValueExpression(); doReturn("value").when(evaluator).evaluate(any()); provider.getParameterValue(parameter); @@ -94,7 +94,7 @@ void handsSpELValueToConversionService() { @Test void doesNotConvertNullValue() { - doReturn("source").when(parameter).getSpelExpression(); + doReturn("source").when(parameter).getRequiredValueExpression(); doReturn(null).when(evaluator).evaluate(any()); provider.getParameterValue(parameter); @@ -116,7 +116,7 @@ protected T potentiallyConvertSpelValue(Object object, Parameter { + + SpELContext spELContext = new SpELContext(new MapAccessor()); + return spELContext.getEvaluationContext(rootObject); + }); + + ValueExpressionParameterValueProvider provider = new ValueExpressionParameterValueProvider<>( + factory.create(Map.of("name", "Walter")), DefaultConversionService.getSharedInstance(), + new ParameterValueProvider() { + @Override + public T getParameterValue(Parameter parameter) { + return null; + } + }); + + @Test // GH-2369 + void considersValueCompatibilityFormat() { + + Annotation[] annotations = CompatibilityConstructor.class.getConstructors()[0].getParameterAnnotations()[0]; + String name = provider + .getParameterValue(new Parameter<>("name", TypeInformation.of(String.class), annotations, null)); + + assertThat(name).isEqualTo("Walter"); + } + + @Test // GH-2369 + void considersValueTemplateFormat() { + + Annotation[] annotations = TemplateConstructor.class.getConstructors()[0].getParameterAnnotations()[0]; + String name = provider + .getParameterValue(new Parameter<>("name", TypeInformation.of(String.class), annotations, null)); + + assertThat(name).isEqualTo("Walter"); + } + + static class CompatibilityConstructor { + + public CompatibilityConstructor(@Value("#root.name") String name) {} + + } + + static class TemplateConstructor { + + public TemplateConstructor(@Value("#{#root.name}") String name) {} + + } + +} diff --git a/src/test/java/org/springframework/data/mapping/model/justme/PropertySourceUnitTests.java b/src/test/java/org/springframework/data/mapping/model/justme/PropertySourceUnitTests.java deleted file mode 100644 index cedd331556..0000000000 --- a/src/test/java/org/springframework/data/mapping/model/justme/PropertySourceUnitTests.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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 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 "}"; - } - }; - } -} diff --git a/src/test/java/org/springframework/data/util/ExpressionEvaluatorUnitTests.java b/src/test/java/org/springframework/data/util/ExpressionEvaluatorUnitTests.java deleted file mode 100644 index c1c6b11c2a..0000000000 --- a/src/test/java/org/springframework/data/util/ExpressionEvaluatorUnitTests.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * 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 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 expressionParser; - Supplier source; - Map> expressionCache = new HashMap<>(1, 1F); - - public PreparedExpression(Supplier source) { - this.source = source; - expressionParser = Lazy.of(SpelExpressionParser::new); - } - - PreparedExpression parseWith(SpelExpressionParser expressionParser) { - return parseWith(() -> expressionParser); - } - - PreparedExpression parseWith(Supplier expressionParser) { - this.expressionParser = expressionParser; - return this; - } - - 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) { - return new ExpressionEvaluation(this, evaluationContext); - } - T doEvaluate(Supplier evaluationContext) { - - String expressionString = source.get(); - Optional 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; - boolean cacheResult; - Optional cached; - - public ExpressionEvaluation(PreparedExpression source, Supplier evaluationContext) { - this.source = source; - this.evaluationContext = evaluationContext; - } - - 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 reevaluate() { - - if(cacheResult) { - cached = null; - } - return evaluate(); - } - } - -} diff --git a/src/test/java/org/springframework/data/util/ExpressionResolverUnitTests.java b/src/test/java/org/springframework/data/util/ExpressionResolverUnitTests.java deleted file mode 100644 index 64b82da514..0000000000 --- a/src/test/java/org/springframework/data/util/ExpressionResolverUnitTests.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * 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 evaluate(ValueContext context); - - default ParsableStatement transform(Function 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 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 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; - } - } -}