From ce088b019fca85e588c8c83b7d0866ffd8aaf2a1 Mon Sep 17 00:00:00 2001 From: guqing Date: Thu, 17 Nov 2022 18:39:29 +0800 Subject: [PATCH 1/2] refactor: flux convert for reactive property accessor --- .../app/theme/ReactivePropertyAccessor.java | 2 +- ...activeSpelVariableExpressionEvaluator.java | 2 +- .../ReactiveFinderExpressionParserTests.java | 102 ++++++++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java diff --git a/src/main/java/run/halo/app/theme/ReactivePropertyAccessor.java b/src/main/java/run/halo/app/theme/ReactivePropertyAccessor.java index 572396aefc..f025c62da6 100644 --- a/src/main/java/run/halo/app/theme/ReactivePropertyAccessor.java +++ b/src/main/java/run/halo/app/theme/ReactivePropertyAccessor.java @@ -57,7 +57,7 @@ public TypedValue read(@NonNull EvaluationContext context, Object target, @NonNu if (Mono.class.isAssignableFrom(clazz)) { value = ((Mono) target).block(); } else if (Flux.class.isAssignableFrom(clazz)) { - value = ((Flux) target).toIterable(); + value = ((Flux) target).collectList().block(); } if (value == null) { diff --git a/src/main/java/run/halo/app/theme/ReactiveSpelVariableExpressionEvaluator.java b/src/main/java/run/halo/app/theme/ReactiveSpelVariableExpressionEvaluator.java index b09b25c60c..1794497789 100644 --- a/src/main/java/run/halo/app/theme/ReactiveSpelVariableExpressionEvaluator.java +++ b/src/main/java/run/halo/app/theme/ReactiveSpelVariableExpressionEvaluator.java @@ -37,7 +37,7 @@ public Object evaluate(IExpressionContext context, IStandardVariableExpression e return ((Mono) returnValue).block(); } if (Flux.class.isAssignableFrom(clazz)) { - return ((Flux) returnValue).toIterable(); + return ((Flux) returnValue).collectList().block(); } return returnValue; } diff --git a/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java b/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java new file mode 100644 index 0000000000..c06a865060 --- /dev/null +++ b/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java @@ -0,0 +1,102 @@ +package run.halo.app.theme; + +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationContext; +import org.thymeleaf.IEngineConfiguration; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.dialect.SpringStandardDialect; +import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext; +import org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator; +import org.thymeleaf.templateresolver.StringTemplateResolver; +import org.thymeleaf.templateresource.ITemplateResource; +import org.thymeleaf.templateresource.StringTemplateResource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import run.halo.app.theme.dialect.HaloProcessorDialect; + +/** + * Tests expression parser for reactive return value. + * + * @author guqing + * @see ReactivePropertyAccessor + * @see ReactiveSpelVariableExpressionEvaluator + * @since 2.0.0 + */ +@ExtendWith(MockitoExtension.class) +public class ReactiveFinderExpressionParserTests { + @Mock + private ApplicationContext applicationContext; + + private TemplateEngine templateEngine; + + @BeforeEach + void setUp() { + HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect(); + templateEngine = new TemplateEngine(); + templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect() { + @Override + public IStandardVariableExpressionEvaluator getVariableExpressionEvaluator() { + return new ReactiveSpelVariableExpressionEvaluator(); + } + })); + templateEngine.addTemplateResolver(new TestTemplateResolver()); + } + + @Test + void javascriptInlineParser() { + Context context = getContext(); + context.setVariable("testReactiveFinder", new TestReactiveFinder()); + String result = templateEngine.process("javascriptInline", context); + System.out.println(result); + } + + static class TestReactiveFinder { + public Mono getName() { + return Mono.just("guqing"); + } + + public Flux names() { + return Flux.just("guqing", "johnniang", "ruibaby"); + } + + public Flux users() { + return Flux.just( + new TestUser("guqing"), new TestUser("ruibaby"), new TestUser("johnniang") + ); + } + } + + record TestUser(String name) { + } + + private Context getContext() { + Context context = new Context(); + context.setVariable( + ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME, + new ThymeleafEvaluationContext(applicationContext, null)); + return context; + } + + static class TestTemplateResolver extends StringTemplateResolver { + @Override + protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration, + String ownerTemplate, String template, + Map templateResolutionAttributes) { + return new StringTemplateResource(""" + + """); + } + } +} From c9f840b8516ef2fd3a5ba794b24a237a415e9740 Mon Sep 17 00:00:00 2001 From: guqing <1484563614@qq.com> Date: Thu, 17 Nov 2022 22:26:55 +0800 Subject: [PATCH 2/2] refactor: support chaining calls for flux and mono in javascript inline tag --- .../app/theme/ReactivePropertyAccessor.java | 94 +++++++++---------- .../ReactiveFinderExpressionParserTests.java | 61 ++++++++++-- 2 files changed, 100 insertions(+), 55 deletions(-) diff --git a/src/main/java/run/halo/app/theme/ReactivePropertyAccessor.java b/src/main/java/run/halo/app/theme/ReactivePropertyAccessor.java index f025c62da6..02cad83f0b 100644 --- a/src/main/java/run/halo/app/theme/ReactivePropertyAccessor.java +++ b/src/main/java/run/halo/app/theme/ReactivePropertyAccessor.java @@ -1,18 +1,16 @@ package run.halo.app.theme; -import com.fasterxml.jackson.databind.JsonNode; -import java.util.ArrayList; import java.util.List; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ast.AstUtils; import org.springframework.integration.json.JsonPropertyAccessor; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import run.halo.app.infra.utils.JsonUtils; /** * A SpEL PropertyAccessor that knows how to read properties from {@link Mono} or {@link Flux} @@ -24,25 +22,25 @@ * @since 2.0.0 */ public class ReactivePropertyAccessor implements PropertyAccessor { - private static final Class[] SUPPORTED_CLASSES = { - Mono.class, - Flux.class - }; - private final JsonPropertyAccessor jsonPropertyAccessor = new JsonPropertyAccessor(); @Override public Class[] getSpecificTargetClasses() { - return SUPPORTED_CLASSES; + return null; } @Override public boolean canRead(@NonNull EvaluationContext context, Object target, @NonNull String name) throws AccessException { - if (target == null) { - return false; + if (isReactiveType(target)) { + return true; } - return Mono.class.isAssignableFrom(target.getClass()) - || Flux.class.isAssignableFrom(target.getClass()); + List propertyAccessors = context.getPropertyAccessors(); + for (PropertyAccessor propertyAccessor : propertyAccessors) { + if (propertyAccessor.canRead(context, target, name)) { + return true; + } + } + return false; } @Override @@ -52,29 +50,44 @@ public TypedValue read(@NonNull EvaluationContext context, Object target, @NonNu if (target == null) { return TypedValue.NULL; } - Class clazz = target.getClass(); - Object value = null; - if (Mono.class.isAssignableFrom(clazz)) { - value = ((Mono) target).block(); - } else if (Flux.class.isAssignableFrom(clazz)) { - value = ((Flux) target).collectList().block(); - } - - if (value == null) { - return TypedValue.NULL; - } + Object value = blockingGetForReactive(target); List propertyAccessorsToTry = getPropertyAccessorsToTry(value, context.getPropertyAccessors()); for (PropertyAccessor propertyAccessor : propertyAccessorsToTry) { try { - return propertyAccessor.read(context, target, name); + TypedValue result = propertyAccessor.read(context, value, name); + return new TypedValue(blockingGetForReactive(result.getValue())); } catch (AccessException e) { - // ignore + // ignore this } } - JsonNode jsonNode = JsonUtils.DEFAULT_JSON_MAPPER.convertValue(value, JsonNode.class); - return jsonPropertyAccessor.read(context, jsonNode, name); + + throw new AccessException("Cannot read property '" + name + "' from [" + value + "]"); + } + + @Nullable + private static Object blockingGetForReactive(@Nullable Object target) { + if (target == null) { + return null; + } + Class clazz = target.getClass(); + Object value = target; + if (Mono.class.isAssignableFrom(clazz)) { + value = ((Mono) target).block(); + } else if (Flux.class.isAssignableFrom(clazz)) { + value = ((Flux) target).collectList().block(); + } + return value; + } + + private boolean isReactiveType(Object target) { + if (target == null) { + return false; + } + Class clazz = target.getClass(); + return Mono.class.isAssignableFrom(clazz) + || Flux.class.isAssignableFrom(clazz); } private List getPropertyAccessorsToTry( @@ -82,27 +95,10 @@ private List getPropertyAccessorsToTry( Class targetType = (contextObject != null ? contextObject.getClass() : null); - List specificAccessors = new ArrayList<>(); - List generalAccessors = new ArrayList<>(); - for (PropertyAccessor resolver : propertyAccessors) { - Class[] targets = resolver.getSpecificTargetClasses(); - if (targets == null) { - // generic resolver that says it can be used for any type - generalAccessors.add(resolver); - } else if (targetType != null) { - for (Class clazz : targets) { - if (clazz == targetType) { - specificAccessors.add(resolver); - break; - } else if (clazz.isAssignableFrom(targetType)) { - generalAccessors.add(resolver); - } - } - } - } - List resolvers = new ArrayList<>(specificAccessors); - generalAccessors.removeAll(specificAccessors); - resolvers.addAll(generalAccessors); + List resolvers = + AstUtils.getPropertyAccessorsToTry(targetType, propertyAccessors); + // remove this resolver to avoid infinite loop + resolvers.remove(this); return resolvers; } diff --git a/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java b/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java index c06a865060..a44ff8c0c3 100644 --- a/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java +++ b/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java @@ -1,5 +1,10 @@ package run.halo.app.theme; +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.BeforeEach; @@ -19,6 +24,7 @@ import org.thymeleaf.templateresource.StringTemplateResource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import run.halo.app.infra.utils.JsonUtils; import run.halo.app.theme.dialect.HaloProcessorDialect; /** @@ -52,9 +58,26 @@ public IStandardVariableExpressionEvaluator getVariableExpressionEvaluator() { @Test void javascriptInlineParser() { Context context = getContext(); - context.setVariable("testReactiveFinder", new TestReactiveFinder()); + context.setVariable("target", new TestReactiveFinder()); + context.setVariable("genericMap", Map.of("key", "value")); String result = templateEngine.process("javascriptInline", context); - System.out.println(result); + assertThat(result).isEqualTo(""" +

value

+

ruibaby

+

guqing

+

bar

+ + """); } static class TestReactiveFinder { @@ -71,6 +94,22 @@ public Flux users() { new TestUser("guqing"), new TestUser("ruibaby"), new TestUser("johnniang") ); } + + public Flux objectJsonNodeFlux() { + ObjectNode objectNode = JsonUtils.DEFAULT_JSON_MAPPER.createObjectNode(); + objectNode.put("name", "guqing"); + return Flux.just(objectNode); + } + + public Mono> mapMono() { + return Mono.just(Map.of("foo", "bar")); + } + + public Mono arrayNodeMono() { + ArrayNode arrayNode = JsonUtils.DEFAULT_JSON_MAPPER.createArrayNode(); + arrayNode.add(arrayNode.objectNode().put("foo", "bar")); + return Mono.just(arrayNode); + } } record TestUser(String name) { @@ -90,13 +129,23 @@ protected ITemplateResource computeTemplateResource(IEngineConfiguration configu String ownerTemplate, String template, Map templateResolutionAttributes) { return new StringTemplateResource(""" +

+

+

+

"""); } + } }