mirror of https://github.com/halo-dev/halo
feat: add annotations expression object for thymeleaf (#3076)
#### 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 <p th:text="${#annotations.get(user, 'background')}"></p> <p th:text="${#annotations.getOrDefault(user, 'background', 'default-value')}"></p> <p th:text="${#annotations.contains(user, 'background')}"></p> ``` #### 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 ```pull/3086/head
parent
77dd5b24dd
commit
cc891d6655
|
@ -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<String> ALL_EXPRESSION_OBJECT_NAMES = Set.of(
|
||||
ANNOTATIONS_EXPRESSION_OBJECT_NAME);
|
||||
|
||||
private static final Annotations ANNOTATIONS = new Annotations();
|
||||
|
||||
@Override
|
||||
public Set<String> 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* <p>Expression Object for performing annotations operations inside Halo Extra Expressions.</p>
|
||||
* An object of this class is usually available in variable evaluation expressions with the name
|
||||
* <code>#annotations</code>.
|
||||
*
|
||||
* @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<String, String> 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
|
||||
* <code>extension</code> 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<String, String> 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<String, String> annotations = extension.getMetadata().getAnnotations();
|
||||
if (annotations == null) {
|
||||
return false;
|
||||
}
|
||||
return annotations.containsKey(key);
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ import run.halo.app.extension.MetadataOperator;
|
|||
@Builder
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public class CategoryTreeVo implements VisualizableTreeNode<CategoryTreeVo> {
|
||||
public class CategoryTreeVo implements VisualizableTreeNode<CategoryTreeVo>, ExtensionVoOperator {
|
||||
|
||||
private MetadataOperator metadata;
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import run.halo.app.extension.MetadataOperator;
|
|||
@Value
|
||||
@Builder
|
||||
@EqualsAndHashCode
|
||||
public class CategoryVo {
|
||||
public class CategoryVo implements ExtensionVoOperator {
|
||||
|
||||
MetadataOperator metadata;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -19,7 +19,7 @@ import run.halo.app.extension.MetadataOperator;
|
|||
@SuperBuilder
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public class ListedPostVo {
|
||||
public class ListedPostVo implements ExtensionVoOperator {
|
||||
|
||||
private MetadataOperator metadata;
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import run.halo.app.extension.MetadataOperator;
|
|||
@SuperBuilder
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public class ListedSinglePageVo {
|
||||
public class ListedSinglePageVo implements ExtensionVoOperator {
|
||||
|
||||
private MetadataOperator metadata;
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import run.halo.app.extension.MetadataOperator;
|
|||
@Data
|
||||
@ToString
|
||||
@Builder
|
||||
public class MenuItemVo implements VisualizableTreeNode<MenuItemVo> {
|
||||
public class MenuItemVo implements VisualizableTreeNode<MenuItemVo>, ExtensionVoOperator {
|
||||
|
||||
MetadataOperator metadata;
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import run.halo.app.extension.MetadataOperator;
|
|||
@Value
|
||||
@ToString
|
||||
@Builder
|
||||
public class MenuVo {
|
||||
public class MenuVo implements ExtensionVoOperator {
|
||||
|
||||
MetadataOperator metadata;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -10,7 +10,7 @@ import run.halo.app.extension.MetadataOperator;
|
|||
*/
|
||||
@Value
|
||||
@Builder
|
||||
public class TagVo {
|
||||
public class TagVo implements ExtensionVoOperator {
|
||||
|
||||
MetadataOperator metadata;
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import run.halo.app.extension.MetadataOperator;
|
|||
@Value
|
||||
@Builder
|
||||
@ToString
|
||||
public class ThemeVo {
|
||||
public class ThemeVo implements ExtensionVoOperator {
|
||||
|
||||
MetadataOperator metadata;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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("<meta name=\"content-head-test\" content=\"test\" />");
|
||||
codeInjection.setGlobalHead("<meta name=\"global-head-test\" content=\"test\" />");
|
||||
codeInjection.setFooter("<footer>hello this is global footer.</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("<p></p>\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("<p>fake-background</p>\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("<p>red</p>\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrDefaultWhenAnnotationsExists() {
|
||||
Context context = getContext();
|
||||
context.setVariable("user", createUser());
|
||||
|
||||
String result = templateEngine.process("annotationsGetOrDefaultExpression", context);
|
||||
assertThat(result).isEqualTo("<p>default-value</p>\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsWhenAnnotationsIsNull() {
|
||||
Context context = getContext();
|
||||
context.setVariable("user", createUser());
|
||||
|
||||
String result = templateEngine.process("annotationsContainsExpression", context);
|
||||
assertThat(result).isEqualTo("<p>false</p>\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("<p>true</p>\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 {
|
|||
</html>
|
||||
""";
|
||||
}
|
||||
|
||||
private String annotationsGetExpression() {
|
||||
return """
|
||||
<p th:text="${#annotations.get(user, 'background')}"></p>
|
||||
""";
|
||||
}
|
||||
|
||||
private String annotationsGetOrDefaultExpression() {
|
||||
return """
|
||||
<p th:text="${#annotations.getOrDefault(user, 'background', 'default-value')}"></p>
|
||||
""";
|
||||
}
|
||||
|
||||
private String annotationsContainsExpression() {
|
||||
return """
|
||||
<p th:text="${#annotations.contains(user, 'background')}"></p>
|
||||
""";
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue