From cc891d66552fbfbe3fd4bfa1c18380dfa536e7b2 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Thu, 29 Dec 2022 21:30:34 +0800 Subject: [PATCH] feat: add annotations expression object for thymeleaf (#3076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /area core /milestone 2.1.x #### What this PR does / why we need it: 新增操作 annotations 的表达式对象 在 thymeleaf 模板中使用示例: ```html

``` #### Which issue(s) this PR fixes: Fixes #3073 #### Special notes for your reviewer: /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 新增 Annotations 表达式对象用于在 thymeleaf 中操作自定义模型的 annotations ``` --- .../dialect/HaloExpressionObjectFactory.java | 40 +++++++ .../theme/dialect/HaloProcessorDialect.java | 13 ++- .../theme/dialect/expression/Annotations.java | 65 +++++++++++ .../app/theme/finders/vo/CategoryTreeVo.java | 2 +- .../halo/app/theme/finders/vo/CategoryVo.java | 2 +- .../halo/app/theme/finders/vo/CommentVo.java | 2 +- .../theme/finders/vo/ExtensionVoOperator.java | 16 +++ .../app/theme/finders/vo/ListedPostVo.java | 2 +- .../theme/finders/vo/ListedSinglePageVo.java | 2 +- .../halo/app/theme/finders/vo/MenuItemVo.java | 2 +- .../run/halo/app/theme/finders/vo/MenuVo.java | 2 +- .../halo/app/theme/finders/vo/ReplyVo.java | 2 +- .../run/halo/app/theme/finders/vo/TagVo.java | 2 +- .../halo/app/theme/finders/vo/ThemeVo.java | 2 +- .../run/halo/app/theme/finders/vo/UserVo.java | 2 +- .../dialect/HaloProcessorDialectTest.java | 109 +++++++++++++++++- 16 files changed, 250 insertions(+), 15 deletions(-) create mode 100644 src/main/java/run/halo/app/theme/dialect/HaloExpressionObjectFactory.java create mode 100644 src/main/java/run/halo/app/theme/dialect/expression/Annotations.java create mode 100644 src/main/java/run/halo/app/theme/finders/vo/ExtensionVoOperator.java diff --git a/src/main/java/run/halo/app/theme/dialect/HaloExpressionObjectFactory.java b/src/main/java/run/halo/app/theme/dialect/HaloExpressionObjectFactory.java new file mode 100644 index 000000000..ba4f2f476 --- /dev/null +++ b/src/main/java/run/halo/app/theme/dialect/HaloExpressionObjectFactory.java @@ -0,0 +1,40 @@ +package run.halo.app.theme.dialect; + +import java.util.Set; +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.expression.IExpressionObjectFactory; +import run.halo.app.theme.dialect.expression.Annotations; + +/** + * Builds the expression objects to be used by Halo dialects. + * + * @author guqing + * @since 2.0.0 + */ +public class HaloExpressionObjectFactory implements IExpressionObjectFactory { + + public static final String ANNOTATIONS_EXPRESSION_OBJECT_NAME = "annotations"; + + protected static final Set ALL_EXPRESSION_OBJECT_NAMES = Set.of( + ANNOTATIONS_EXPRESSION_OBJECT_NAME); + + private static final Annotations ANNOTATIONS = new Annotations(); + + @Override + public Set getAllExpressionObjectNames() { + return ALL_EXPRESSION_OBJECT_NAMES; + } + + @Override + public Object buildObject(IExpressionContext context, String expressionObjectName) { + if (ANNOTATIONS_EXPRESSION_OBJECT_NAME.equals(expressionObjectName)) { + return ANNOTATIONS; + } + return null; + } + + @Override + public boolean isCacheable(String expressionObjectName) { + return true; + } +} diff --git a/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java b/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java index 171b7ddf8..cf0aff5c6 100644 --- a/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java +++ b/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java @@ -3,6 +3,8 @@ package run.halo.app.theme.dialect; import java.util.HashSet; import java.util.Set; import org.thymeleaf.dialect.AbstractProcessorDialect; +import org.thymeleaf.dialect.IExpressionObjectDialect; +import org.thymeleaf.expression.IExpressionObjectFactory; import org.thymeleaf.processor.IProcessor; import org.thymeleaf.standard.StandardDialect; @@ -12,9 +14,13 @@ import org.thymeleaf.standard.StandardDialect; * @author guqing * @since 2.0.0 */ -public class HaloProcessorDialect extends AbstractProcessorDialect { +public class HaloProcessorDialect extends AbstractProcessorDialect implements + IExpressionObjectDialect { private static final String DIALECT_NAME = "haloThemeProcessorDialect"; + private static final IExpressionObjectFactory HALO_EXPRESSION_OBJECTS_FACTORY = + new HaloExpressionObjectFactory(); + public HaloProcessorDialect() { // We will set this dialect the same "dialect processor" precedence as // the Standard Dialect, so that processor executions can interleave. @@ -31,4 +37,9 @@ public class HaloProcessorDialect extends AbstractProcessorDialect { processors.add(new CommentElementTagProcessor(dialectPrefix)); return processors; } + + @Override + public IExpressionObjectFactory getExpressionObjectFactory() { + return HALO_EXPRESSION_OBJECTS_FACTORY; + } } diff --git a/src/main/java/run/halo/app/theme/dialect/expression/Annotations.java b/src/main/java/run/halo/app/theme/dialect/expression/Annotations.java new file mode 100644 index 000000000..f78b36b36 --- /dev/null +++ b/src/main/java/run/halo/app/theme/dialect/expression/Annotations.java @@ -0,0 +1,65 @@ +package run.halo.app.theme.dialect.expression; + +import java.util.Map; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import run.halo.app.theme.finders.vo.ExtensionVoOperator; + +/** + *

Expression Object for performing annotations operations inside Halo Extra Expressions.

+ * An object of this class is usually available in variable evaluation expressions with the name + * #annotations. + * + * @author guqing + * @since 2.0.2 + */ +public class Annotations { + + /** + * Get annotation value from extension vo. + * + * @param extension extension vo + * @param key the key of annotation + * @return annotation value if exists, otherwise null + */ + @Nullable + public String get(ExtensionVoOperator extension, String key) { + Map annotations = extension.getMetadata().getAnnotations(); + if (annotations == null) { + return null; + } + return annotations.get(key); + } + + /** + * Returns the value to which the specified key is mapped, or defaultValue if + * extension contains no mapping for the key. + * + * @param extension extension vo + * @param key the key of annotation + * @return annotation value if exists, otherwise defaultValue + */ + @NonNull + public String getOrDefault(ExtensionVoOperator extension, String key, String defaultValue) { + Map annotations = extension.getMetadata().getAnnotations(); + if (annotations == null) { + return defaultValue; + } + return annotations.getOrDefault(key, defaultValue); + } + + /** + * Check if the extension has the specified annotation. + * + * @param extension extension vo + * @param key the key of annotation + * @return true if the extension has the specified annotation, otherwise false + */ + public boolean contains(ExtensionVoOperator extension, String key) { + Map annotations = extension.getMetadata().getAnnotations(); + if (annotations == null) { + return false; + } + return annotations.containsKey(key); + } +} diff --git a/src/main/java/run/halo/app/theme/finders/vo/CategoryTreeVo.java b/src/main/java/run/halo/app/theme/finders/vo/CategoryTreeVo.java index 3981a66c7..6a0617667 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/CategoryTreeVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/CategoryTreeVo.java @@ -20,7 +20,7 @@ import run.halo.app.extension.MetadataOperator; @Builder @ToString @EqualsAndHashCode -public class CategoryTreeVo implements VisualizableTreeNode { +public class CategoryTreeVo implements VisualizableTreeNode, ExtensionVoOperator { private MetadataOperator metadata; diff --git a/src/main/java/run/halo/app/theme/finders/vo/CategoryVo.java b/src/main/java/run/halo/app/theme/finders/vo/CategoryVo.java index c9c25d5bb..c16f0b0dc 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/CategoryVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/CategoryVo.java @@ -15,7 +15,7 @@ import run.halo.app.extension.MetadataOperator; @Value @Builder @EqualsAndHashCode -public class CategoryVo { +public class CategoryVo implements ExtensionVoOperator { MetadataOperator metadata; diff --git a/src/main/java/run/halo/app/theme/finders/vo/CommentVo.java b/src/main/java/run/halo/app/theme/finders/vo/CommentVo.java index f071d7dc5..d71579083 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/CommentVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/CommentVo.java @@ -18,7 +18,7 @@ import run.halo.app.extension.MetadataOperator; @Value @Builder @EqualsAndHashCode -public class CommentVo { +public class CommentVo implements ExtensionVoOperator { @Schema(required = true) MetadataOperator metadata; diff --git a/src/main/java/run/halo/app/theme/finders/vo/ExtensionVoOperator.java b/src/main/java/run/halo/app/theme/finders/vo/ExtensionVoOperator.java new file mode 100644 index 000000000..8992c9935 --- /dev/null +++ b/src/main/java/run/halo/app/theme/finders/vo/ExtensionVoOperator.java @@ -0,0 +1,16 @@ +package run.halo.app.theme.finders.vo; + +import org.springframework.lang.NonNull; +import run.halo.app.extension.MetadataOperator; + +/** + * An operator for extension value object. + * + * @author guqing + * @since 2.0.0 + */ +public interface ExtensionVoOperator { + + @NonNull + MetadataOperator getMetadata(); +} diff --git a/src/main/java/run/halo/app/theme/finders/vo/ListedPostVo.java b/src/main/java/run/halo/app/theme/finders/vo/ListedPostVo.java index dc7e46d30..bf1d5764c 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/ListedPostVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/ListedPostVo.java @@ -19,7 +19,7 @@ import run.halo.app.extension.MetadataOperator; @SuperBuilder @ToString @EqualsAndHashCode -public class ListedPostVo { +public class ListedPostVo implements ExtensionVoOperator { private MetadataOperator metadata; diff --git a/src/main/java/run/halo/app/theme/finders/vo/ListedSinglePageVo.java b/src/main/java/run/halo/app/theme/finders/vo/ListedSinglePageVo.java index 7ac6d90c5..499a6b1fc 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/ListedSinglePageVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/ListedSinglePageVo.java @@ -19,7 +19,7 @@ import run.halo.app.extension.MetadataOperator; @SuperBuilder @ToString @EqualsAndHashCode -public class ListedSinglePageVo { +public class ListedSinglePageVo implements ExtensionVoOperator { private MetadataOperator metadata; diff --git a/src/main/java/run/halo/app/theme/finders/vo/MenuItemVo.java b/src/main/java/run/halo/app/theme/finders/vo/MenuItemVo.java index f30ccfbcd..b2dece150 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/MenuItemVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/MenuItemVo.java @@ -17,7 +17,7 @@ import run.halo.app.extension.MetadataOperator; @Data @ToString @Builder -public class MenuItemVo implements VisualizableTreeNode { +public class MenuItemVo implements VisualizableTreeNode, ExtensionVoOperator { MetadataOperator metadata; diff --git a/src/main/java/run/halo/app/theme/finders/vo/MenuVo.java b/src/main/java/run/halo/app/theme/finders/vo/MenuVo.java index bfeda813e..857e5a315 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/MenuVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/MenuVo.java @@ -18,7 +18,7 @@ import run.halo.app.extension.MetadataOperator; @Value @ToString @Builder -public class MenuVo { +public class MenuVo implements ExtensionVoOperator { MetadataOperator metadata; diff --git a/src/main/java/run/halo/app/theme/finders/vo/ReplyVo.java b/src/main/java/run/halo/app/theme/finders/vo/ReplyVo.java index c3cf83544..3931db54a 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/ReplyVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/ReplyVo.java @@ -20,7 +20,7 @@ import run.halo.app.extension.MetadataOperator; @Builder @ToString @EqualsAndHashCode -public class ReplyVo { +public class ReplyVo implements ExtensionVoOperator { @Schema(required = true) MetadataOperator metadata; diff --git a/src/main/java/run/halo/app/theme/finders/vo/TagVo.java b/src/main/java/run/halo/app/theme/finders/vo/TagVo.java index 0b2c1f59c..fc1e4c997 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/TagVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/TagVo.java @@ -10,7 +10,7 @@ import run.halo.app.extension.MetadataOperator; */ @Value @Builder -public class TagVo { +public class TagVo implements ExtensionVoOperator { MetadataOperator metadata; diff --git a/src/main/java/run/halo/app/theme/finders/vo/ThemeVo.java b/src/main/java/run/halo/app/theme/finders/vo/ThemeVo.java index b6725ef0b..550b9209c 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/ThemeVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/ThemeVo.java @@ -17,7 +17,7 @@ import run.halo.app.extension.MetadataOperator; @Value @Builder @ToString -public class ThemeVo { +public class ThemeVo implements ExtensionVoOperator { MetadataOperator metadata; diff --git a/src/main/java/run/halo/app/theme/finders/vo/UserVo.java b/src/main/java/run/halo/app/theme/finders/vo/UserVo.java index cbe0137b4..fa60d07f2 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/UserVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/UserVo.java @@ -10,7 +10,7 @@ import run.halo.app.infra.utils.JsonUtils; @Value @Builder -public class UserVo { +public class UserVo implements ExtensionVoOperator { MetadataOperator metadata; User.UserSpec spec; diff --git a/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java b/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java index d78d1c182..adc1c04d0 100644 --- a/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java +++ b/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -26,6 +27,7 @@ import org.thymeleaf.templateresolver.StringTemplateResolver; import org.thymeleaf.templateresource.ITemplateResource; import org.thymeleaf.templateresource.StringTemplateResource; import reactor.core.publisher.Mono; +import run.halo.app.core.extension.User; import run.halo.app.core.extension.content.Post; import run.halo.app.extension.Metadata; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; @@ -35,6 +37,7 @@ import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.SinglePageFinder; import run.halo.app.theme.finders.vo.PostVo; +import run.halo.app.theme.finders.vo.UserVo; import run.halo.app.theme.router.strategy.ModelConst; /** @@ -88,7 +91,7 @@ class HaloProcessorDialectTest { codeInjection.setContentHead(""); codeInjection.setGlobalHead(""); codeInjection.setFooter("
hello this is global footer.
"); - when(fetcher.fetch(eq(SystemSetting.CodeInjection.GROUP), + lenient().when(fetcher.fetch(eq(SystemSetting.CodeInjection.GROUP), eq(SystemSetting.CodeInjection.class))).thenReturn(Mono.just(codeInjection)); lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class))) @@ -96,10 +99,10 @@ class HaloProcessorDialectTest { lenient().when(fetcher.fetch(eq(SystemSetting.Seo.GROUP), eq(SystemSetting.Seo.class))).thenReturn(Mono.empty()); - when(applicationContext.getBean(eq(ExtensionComponentsFinder.class))) + lenient().when(applicationContext.getBean(eq(ExtensionComponentsFinder.class))) .thenReturn(extensionComponentsFinder); - when(extensionComponentsFinder.getExtensions(eq(TemplateHeadProcessor.class))) + lenient().when(extensionComponentsFinder.getExtensions(eq(TemplateHeadProcessor.class))) .thenReturn(new ArrayList<>(map.values())); } @@ -242,6 +245,78 @@ class HaloProcessorDialectTest { """); } + @Nested + class AnnotationExpressionObjectFactoryTest { + + @Test + void getWhenAnnotationsIsNull() { + Context context = getContext(); + context.setVariable("user", createUser()); + + String result = templateEngine.process("annotationsGetExpression", context); + assertThat(result).isEqualTo("

\n"); + } + + @Test + void getWhenAnnotationsExists() { + Context context = getContext(); + UserVo user = createUser(); + user.getMetadata().setAnnotations(Map.of("background", "fake-background")); + context.setVariable("user", user); + + String result = templateEngine.process("annotationsGetExpression", context); + assertThat(result).isEqualTo("

fake-background

\n"); + } + + @Test + void getOrDefaultWhenAnnotationsIsNull() { + Context context = getContext(); + UserVo user = createUser(); + user.getMetadata().setAnnotations(Map.of("background", "red")); + context.setVariable("user", user); + + String result = templateEngine.process("annotationsGetOrDefaultExpression", context); + assertThat(result).isEqualTo("

red

\n"); + } + + @Test + void getOrDefaultWhenAnnotationsExists() { + Context context = getContext(); + context.setVariable("user", createUser()); + + String result = templateEngine.process("annotationsGetOrDefaultExpression", context); + assertThat(result).isEqualTo("

default-value

\n"); + } + + @Test + void containsWhenAnnotationsIsNull() { + Context context = getContext(); + context.setVariable("user", createUser()); + + String result = templateEngine.process("annotationsContainsExpression", context); + assertThat(result).isEqualTo("

false

\n"); + } + + @Test + void containsWhenAnnotationsIsNotNull() { + Context context = getContext(); + UserVo user = createUser(); + user.getMetadata().setAnnotations(Map.of("background", "")); + context.setVariable("user", user); + + String result = templateEngine.process("annotationsContainsExpression", context); + assertThat(result).isEqualTo("

true

\n"); + } + + UserVo createUser() { + User user = new User(); + user.setMetadata(new Metadata()); + user.getMetadata().setName("fake-user"); + user.setSpec(new User.UserSpec()); + return UserVo.from(user); + } + } + private Context getContext() { Context context = new Context(); context.setVariable( @@ -266,6 +341,16 @@ class HaloProcessorDialectTest { if (template.equals("seo")) { return new StringTemplateResource(seoTemplate()); } + + if (template.equals("annotationsGetExpression")) { + return new StringTemplateResource(annotationsGetExpression()); + } + if (template.equals("annotationsGetOrDefaultExpression")) { + return new StringTemplateResource(annotationsGetOrDefaultExpression()); + } + if (template.equals("annotationsContainsExpression")) { + return new StringTemplateResource(annotationsContainsExpression()); + } return null; } @@ -316,5 +401,23 @@ class HaloProcessorDialectTest { """; } + + private String annotationsGetExpression() { + return """ +

+ """; + } + + private String annotationsGetOrDefaultExpression() { + return """ +

+ """; + } + + private String annotationsContainsExpression() { + return """ +

+ """; + } } } \ No newline at end of file