diff --git a/spring-pulsar/src/main/java/org/springframework/pulsar/core/DefaultExpressionResolver.java b/spring-pulsar/src/main/java/org/springframework/pulsar/core/DefaultExpressionResolver.java index 585286c2..831d9ae0 100644 --- a/spring-pulsar/src/main/java/org/springframework/pulsar/core/DefaultExpressionResolver.java +++ b/spring-pulsar/src/main/java/org/springframework/pulsar/core/DefaultExpressionResolver.java @@ -25,6 +25,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory; * {@link ConfigurableBeanFactory} capabilities to resolve expressions. * * @author Jonas Geiregat + * @since 1.1.0 */ public class DefaultExpressionResolver implements ExpressionResolver { @@ -40,6 +41,13 @@ public class DefaultExpressionResolver implements ExpressionResolver { this.configurableBeanFactory = configurableBeanFactory; } + /** + * {@inheritDoc} + * @param expression the expression to resolve (can include property placeholders and + * SpEL) + * @return a {@code Resolved} instance containing the resolved string value (can be + * null) or an exception if the resolution failed. + */ @Override public Resolved resolveToString(String expression) { String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(expression); @@ -51,7 +59,7 @@ public class DefaultExpressionResolver implements ExpressionResolver { return Resolved.of(null); } return Resolved - .failed("The expression '%s' must resolve to a string but was: %s".formatted(expression, resolvedObj)); + .failed("The expression '%s' must resolve to a string but was: %s".formatted(expression, resolvedObj)); } } diff --git a/spring-pulsar/src/main/java/org/springframework/pulsar/core/ExpressionResolver.java b/spring-pulsar/src/main/java/org/springframework/pulsar/core/ExpressionResolver.java index b3913619..a2f092ea 100644 --- a/spring-pulsar/src/main/java/org/springframework/pulsar/core/ExpressionResolver.java +++ b/spring-pulsar/src/main/java/org/springframework/pulsar/core/ExpressionResolver.java @@ -17,16 +17,18 @@ package org.springframework.pulsar.core; /** - * Resolves expressions. + * Strategy interface for resolving a value from a string expression. * * @author Jonas Geiregat + * @since 1.1.0 */ public interface ExpressionResolver { /** - * Resolve the given expression. + * Resolve the given expression as a string value. * @param expression the expression to resolve - * @return the resolved value as a {@code Resolved} of {@link String} + * @return a {@code Resolved} instance containing the resolved string value (can be + * null) or an exception if the resolution failed. */ Resolved resolveToString(String expression); diff --git a/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultTopicResolverTests.java b/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultTopicResolverTests.java index 67764064..02466039 100644 --- a/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultTopicResolverTests.java +++ b/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultTopicResolverTests.java @@ -18,11 +18,9 @@ package org.springframework.pulsar.core; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.params.provider.Arguments.arguments; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import java.util.stream.Stream; @@ -35,9 +33,17 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.lang.Nullable; import org.springframework.pulsar.annotation.PulsarMessage; +import org.springframework.pulsar.core.DefaultTopicResolverTests.TopicByAnnotatedMessageType.WithTopicExpression.WithTopicExpressionConfig; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * Unit tests for {@link DefaultTopicResolver}. @@ -138,8 +144,6 @@ class DefaultTopicResolverTests { @Nested class TopicByAnnotatedMessageType { - private static final String bazTopicExpression = "#{someExpression}"; - @Test void customMappingTakesPrecedenceOverAnnotationMapping() { assertThat(resolver.resolveTopic(null, Baz.class, () -> defaultTopic).value().orElse(null)) @@ -169,29 +173,63 @@ class DefaultTopicResolverTests { verify(resolver, times(1)).getAnnotatedTopicInfo(Baz.class); } - @Test - void annotatedMessageTypeWithTopicExpressionIsResolved() { - var mockExpressionResolver = mock(ExpressionResolver.class); - when(mockExpressionResolver.resolveToString(bazTopicExpression)).thenReturn(Resolved.of(bazTopic)); - var expressionTopicResolver = new DefaultTopicResolver(mockExpressionResolver); - assertThat(expressionTopicResolver.resolveTopic(null, BazWithTopicExpression.class, - () -> defaultTopic).value().orElse(null)) - .isEqualTo(bazTopic); - verify(mockExpressionResolver, times(1)).resolveToString(bazTopicExpression); - } + /** + * Lightweight integration tests for the expression resolver functionality in + * {@link DefaultTopicResolver}. + *

+ * Starts up a small Spring context which in turns provides the bean factory and + * expression resolver to the topic resolver. + */ + @Nested + @SpringJUnitConfig + @DirtiesContext(classMode = ClassMode.AFTER_CLASS) + @ContextConfiguration(classes = WithTopicExpressionConfig.class) + @TestPropertySource(properties = { "app.customPropertyTopic = my-custom-property-topic" }) + class WithTopicExpression { - @Test - void deriveExpressionResolverFromBeanFactory() { - var mockBeanFactory = mock(ConfigurableBeanFactory.class); - var expressionTopicResolver = new DefaultTopicResolver(); - expressionTopicResolver.setBeanFactory(mockBeanFactory); - assertThat(expressionTopicResolver) - .extracting("expressionResolver") - .isNotNull(); - } + // @formatter:off + @Test + void propertyPlaceholderExpressionIsResolved(@Autowired DefaultTopicResolver topicResolver) { + assertThat(topicResolver.resolveTopic(null, MsgTypeWithTopicPropertyExpression.class, () -> defaultTopic) + .value().orElse(null)).isEqualTo("my-custom-property-topic"); + + } + + @Test + void spelExpressionIsResolved(@Autowired DefaultTopicResolver topicResolver) { + assertThat(topicResolver.resolveTopic(null, MsgTypeWithTopicSpELExpression.class, () -> defaultTopic) + .value().orElse(null)).isEqualTo("my-custom-spel-topic"); + } + + @Test + void embeddedExpressionIsResolved(@Autowired DefaultTopicResolver topicResolver) { + assertThat(topicResolver.resolveTopic(null, MsgTypeWithTopicEmbeddedExpression.class, () -> defaultTopic) + .value().orElse(null)).isEqualTo("my-custom-property-topic".toUpperCase()); + } + // @formatter:on + + @Configuration(proxyBeanMethods = false) + static class WithTopicExpressionConfig { + + @Bean + DefaultTopicResolver defaultTopicResolver() { + return new DefaultTopicResolver(); + } + + } + + @PulsarMessage(topic = "${app.customPropertyTopic}") + record MsgTypeWithTopicPropertyExpression(String value) { + } + + @PulsarMessage(topic = "#{T(java.lang.String).valueOf('my-custom-spel-topic')}") + record MsgTypeWithTopicSpELExpression(String value) { + } + + @PulsarMessage(topic = "#{T(java.lang.String).valueOf('${app.customPropertyTopic}').toUpperCase()}") + record MsgTypeWithTopicEmbeddedExpression(String value) { + } - @PulsarMessage(topic = bazTopicExpression) - record BazWithTopicExpression(String value) { } }