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("");
- 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 {