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 4c2ecc88f..a924e3d30 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.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.EnvironmentAware; import org.springframework.core.KotlinDetector; import org.springframework.core.NativeDetector; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; import org.springframework.data.domain.ManagedTypes; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; @@ -49,6 +52,7 @@ import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.model.AbstractPersistentProperty; import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory; import org.springframework.data.mapping.model.ClassGeneratingPropertyAccessorFactory; import org.springframework.data.mapping.model.EntityInstantiators; @@ -59,6 +63,7 @@ import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; +import org.springframework.data.support.EnvironmentAccessor; import org.springframework.data.util.KotlinReflectionUtils; import org.springframework.data.util.NullableWrapperConverters; import org.springframework.data.util.Optionals; @@ -89,7 +94,7 @@ import org.springframework.util.ReflectionUtils.FieldFilter; * @author Christoph Strobl */ public abstract class AbstractMappingContext, 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 pp) { + pp.setEnvironmentAccessor(environmentAccessor); + } entity.addPersistentProperty(property); if (property.isAssociation()) { @@ -776,4 +794,32 @@ public abstract class AbstractMappingContext private final Lazy readable; private final boolean immutable; + + private @Nullable EnvironmentAccessor environmentAccessor; + public AbstractPersistentProperty(Property property, PersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { @@ -306,6 +310,14 @@ public abstract class AbstractPersistentProperty

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 46548a9db..9b3337ce7 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.Association; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.support.EnvironmentAccessor; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.Lazy; import org.springframework.data.util.Optionals; 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 1f9c15d63..88889a559 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.domain.Persistable; import org.springframework.data.mapping.*; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.ExpressionDependencies; +import org.springframework.data.support.EnvironmentAccessor; import org.springframework.data.support.IsNewStrategy; import org.springframework.data.support.PersistableIsNewStrategy; import org.springframework.data.util.Lazy; @@ -79,6 +80,7 @@ public class BasicPersistentEntity> 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 class BasicPersistentEntity> implement if (versionProperty != null) { throw new MappingException( - String.format( - "Attempt to add version property %s but already have property %s registered " - + "as version; Check your mapping configuration", - property.getField(), versionProperty.getField())); + String.format("Attempt to add version property %s but already have property %s registered " + + "as version; Check your mapping configuration", property.getField(), versionProperty.getField())); } this.versionProperty = property; @@ -220,6 +220,11 @@ public class BasicPersistentEntity> implement this.evaluationContextProvider = provider; } + @Override + public void setEnvironmentAccessor(EnvironmentAccessor accessor) { + this.environmentAccessor = accessor; + } + /** * Returns the given property if it is a better candidate for the id property than the current id property. * @@ -443,6 +448,17 @@ public class BasicPersistentEntity> implement return evaluationContextProvider.getEvaluationContext(rootObject, dependencies); } + /** + * Obtain the {@link EnvironmentAccessor} providing access to the current + * {@link org.springframework.core.env.Environment}. + * + * @return never {@literal null}. + * @since 3.3 + */ + protected EnvironmentAccessor getEnvironmentAccessor() { + return environmentAccessor; + } + /** * Returns the default {@link IsNewStrategy} to be used. Will be a {@link PersistentEntityIsNewStrategy} by default. * Note, that this strategy only gets used if the entity doesn't implement {@link Persistable} as this indicates the 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 d66aa3351..dc41df8bc 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.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.spel.EvaluationContextProvider; +import org.springframework.data.support.EnvironmentAccessor; /** * Interface capturing mutator methods for {@link PersistentEntity}s. @@ -66,4 +67,12 @@ public interface MutablePersistentEntity> 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 000000000..0bf86f06f --- /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 000000000..5fbdd7fa4 --- /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 000000000..c2a5ba835 --- /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 000000000..cedd33155 --- /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 000000000..c1c6b11c2 --- /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 000000000..64b82da51 --- /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 000000000..89c335b61 --- /dev/null +++ b/src/test/resources/persistent-entity.properties @@ -0,0 +1 @@ +my-type.name=foo