Skip to content

Commit

Permalink
refactor: support chaining calls for flux and mono in javascript inli…
Browse files Browse the repository at this point in the history
…ne tag (#2715)

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.0

#### What this PR does / why we need it:
支持在 JavaScript 中对 Flux 或 Mono 的泛型类型属性链式调用
测试参考:
https://github.com/halo-dev/halo/blob/7e9bdec4923348b98df17b7e1071154ecec8ead1/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java#L131-L147
期望
https://github.com/halo-dev/halo/blob/7e9bdec4923348b98df17b7e1071154ecec8ead1/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java#L64-L79

#### Special notes for your reviewer:
/cc @halo-dev/sig-halo 
#### Does this PR introduce a user-facing change?

```release-note
支持在 JavaScript 中对 Flux 或 Mono 的泛型类型属性链式调用
```
  • Loading branch information
guqing committed Nov 18, 2022
1 parent eeb9c4d commit c8bc96f
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 50 deletions.
94 changes: 45 additions & 49 deletions src/main/java/run/halo/app/theme/ReactivePropertyAccessor.java
Original file line number Diff line number Diff line change
@@ -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}
Expand All @@ -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;
}
List<PropertyAccessor> propertyAccessors = context.getPropertyAccessors();
for (PropertyAccessor propertyAccessor : propertyAccessors) {
if (propertyAccessor.canRead(context, target, name)) {
return true;
}
}
return Mono.class.isAssignableFrom(target.getClass())
|| Flux.class.isAssignableFrom(target.getClass());
return false;
}

@Override
Expand All @@ -52,57 +50,55 @@ 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).toIterable();
}

if (value == null) {
return TypedValue.NULL;
}
Object value = blockingGetForReactive(target);

List<PropertyAccessor> 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<PropertyAccessor> getPropertyAccessorsToTry(
@Nullable Object contextObject, List<PropertyAccessor> propertyAccessors) {

Class<?> targetType = (contextObject != null ? contextObject.getClass() : null);

List<PropertyAccessor> specificAccessors = new ArrayList<>();
List<PropertyAccessor> 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<PropertyAccessor> resolvers = new ArrayList<>(specificAccessors);
generalAccessors.removeAll(specificAccessors);
resolvers.addAll(generalAccessors);
List<PropertyAccessor> resolvers =
AstUtils.getPropertyAccessorsToTry(targetType, propertyAccessors);
// remove this resolver to avoid infinite loop
resolvers.remove(this);
return resolvers;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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;
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.infra.utils.JsonUtils;
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("target", new TestReactiveFinder());
context.setVariable("genericMap", Map.of("key", "value"));
String result = templateEngine.process("javascriptInline", context);
assertThat(result).isEqualTo("""
<p>value</p>
<p>ruibaby</p>
<p>guqing</p>
<p>bar</p>
<script>
var genericValue = "value";
var name = "guqing";
var names = ["guqing","johnniang","ruibaby"];
var users = [{"name":"guqing"},{"name":"ruibaby"},{"name":"johnniang"}];
var userListItem = "guqing";
var objectJsonNodeFlux = [{"name":"guqing"}];
var objectJsonNodeFluxChain = "guqing";
var mapMono = "bar";
var arrayNodeMono = "bar";
</script>
""");
}

static class TestReactiveFinder {
public Mono<String> getName() {
return Mono.just("guqing");
}

public Flux<String> names() {
return Flux.just("guqing", "johnniang", "ruibaby");
}

public Flux<TestUser> users() {
return Flux.just(
new TestUser("guqing"), new TestUser("ruibaby"), new TestUser("johnniang")
);
}

public Flux<JsonNode> objectJsonNodeFlux() {
ObjectNode objectNode = JsonUtils.DEFAULT_JSON_MAPPER.createObjectNode();
objectNode.put("name", "guqing");
return Flux.just(objectNode);
}

public Mono<Map<String, Object>> mapMono() {
return Mono.just(Map.of("foo", "bar"));
}

public Mono<JsonNode> arrayNodeMono() {
ArrayNode arrayNode = JsonUtils.DEFAULT_JSON_MAPPER.createArrayNode();
arrayNode.add(arrayNode.objectNode().put("foo", "bar"));
return Mono.just(arrayNode);
}
}

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<String, Object> templateResolutionAttributes) {
return new StringTemplateResource("""
<p th:text="${genericMap.key}"></p>
<p th:text="${target.users[1].name}"></p>
<p th:text="${target.objectJsonNodeFlux[0].name}"></p>
<p th:text="${target.arrayNodeMono.get(0).foo}"></p>
<script th:inline="javascript">
var genericValue = /*[[${genericMap.key}]]*/;
var name = /*[[${target.getName()}]]*/;
var names = /*[[${target.names()}]]*/;
var users = /*[[${target.users()}]]*/;
var userListItem = /*[[${target.users[0].name}]]*/;
var objectJsonNodeFlux = /*[[${target.objectJsonNodeFlux()}]]*/;
var objectJsonNodeFluxChain = /*[[${target.objectJsonNodeFlux[0].name}]]*/;
var mapMono = /*[[${target.mapMono.foo}]]*/;
var arrayNodeMono = /*[[${target.arrayNodeMono.get(0).foo}]]*/;
</script>
""");
}

}
}

0 comments on commit c8bc96f

Please sign in to comment.