mirror of https://github.com/halo-dev/halo
feat: theme side provides variables for theme and some system settings (#2406)
#### What type of PR is this? /kind feature /milestone 2.0 /area core #### What this PR does / why we need it: 提供当前使用主题(预览或激活)的 configMap 变量和部分系统设置等变量。 提供了以下变量: - `${theme}` 当前主题的信息,theme.yaml - `${theme.config}` 获取当前主题的设置项 - ~`${siteSetting}`~ `${site}` 提供必要系统变量 #### Which issue(s) this PR fixes: Fixes #2389 #### Special notes for your reviewer: how to test it? 再任意主题模板上使用表达式获取例如:`${theme}`,`${theme.config.sns?.email}` /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note None ```pull/2415/head
parent
069ff04c84
commit
a0d55c58f6
|
@ -74,6 +74,7 @@ dependencies {
|
||||||
implementation "com.google.guava:guava:$guava"
|
implementation "com.google.guava:guava:$guava"
|
||||||
implementation "org.jsoup:jsoup:$jsoup"
|
implementation "org.jsoup:jsoup:$jsoup"
|
||||||
implementation "io.github.java-diff-utils:java-diff-utils:$javaDiffUtils"
|
implementation "io.github.java-diff-utils:java-diff-utils:$javaDiffUtils"
|
||||||
|
implementation "org.springframework.integration:spring-integration-core"
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
testCompileOnly 'org.projectlombok:lombok'
|
testCompileOnly 'org.projectlombok:lombok'
|
||||||
|
|
|
@ -2,6 +2,7 @@ package run.halo.app;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
import run.halo.app.infra.properties.JwtProperties;
|
import run.halo.app.infra.properties.JwtProperties;
|
||||||
|
@ -14,7 +15,8 @@ import run.halo.app.infra.properties.JwtProperties;
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @date 2017-11-14
|
* @date 2017-11-14
|
||||||
*/
|
*/
|
||||||
@SpringBootApplication
|
@SpringBootApplication(scanBasePackages = "run.halo.app", exclude =
|
||||||
|
IntegrationAutoConfiguration.class)
|
||||||
@EnableConfigurationProperties({HaloProperties.class, JwtProperties.class})
|
@EnableConfigurationProperties({HaloProperties.class, JwtProperties.class})
|
||||||
public class Application {
|
public class Application {
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package run.halo.app.core.extension.reconciler;
|
package run.halo.app.core.extension.reconciler;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
|
@ -19,13 +22,16 @@ import run.halo.app.theme.router.PermalinkRuleChangedEvent;
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
public class SystemSettingReconciler implements Reconciler<Reconciler.Request> {
|
public class SystemSettingReconciler implements Reconciler<Reconciler.Request> {
|
||||||
|
public static final String OLD_THEME_ROUTE_RULES = "halo.run/old-theme-route-rules";
|
||||||
private static final String OLD_THEME_ROUTE_RULES = "halo.run/old-theme-route-rules";
|
public static final String FINALIZER_NAME = "system-setting-protection";
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ExtensionClient client;
|
||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
private final RouteRuleReconciler routeRuleReconciler = new RouteRuleReconciler();
|
||||||
|
|
||||||
public SystemSettingReconciler(ExtensionClient client, ApplicationContext applicationContext) {
|
public SystemSettingReconciler(ExtensionClient client, ApplicationContext applicationContext) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.applicationContext = applicationContext;
|
this.applicationContext = applicationContext;
|
||||||
|
@ -39,101 +45,187 @@ public class SystemSettingReconciler implements Reconciler<Reconciler.Request> {
|
||||||
}
|
}
|
||||||
client.fetch(ConfigMap.class, name)
|
client.fetch(ConfigMap.class, name)
|
||||||
.ifPresent(configMap -> {
|
.ifPresent(configMap -> {
|
||||||
ConfigMap oldConfigMap = JsonUtils.deepCopy(configMap);
|
addFinalizerIfNecessary(configMap);
|
||||||
|
routeRuleReconciler.reconcile(name);
|
||||||
ruleChangedDispatcher(configMap);
|
|
||||||
|
|
||||||
if (!configMap.equals(oldConfigMap)) {
|
|
||||||
client.update(configMap);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return new Result(false, null);
|
return new Result(false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ruleChangedDispatcher(ConfigMap configMap) {
|
private void addFinalizerIfNecessary(ConfigMap oldConfigMap) {
|
||||||
Map<String, String> data = configMap.getData();
|
Set<String> finalizers = oldConfigMap.getMetadata().getFinalizers();
|
||||||
|
if (finalizers != null && finalizers.contains(FINALIZER_NAME)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.fetch(ConfigMap.class, oldConfigMap.getMetadata().getName())
|
||||||
|
.ifPresent(configMap -> {
|
||||||
|
Set<String> newFinalizers = configMap.getMetadata().getFinalizers();
|
||||||
|
if (newFinalizers == null) {
|
||||||
|
newFinalizers = new HashSet<>();
|
||||||
|
configMap.getMetadata().setFinalizers(newFinalizers);
|
||||||
|
}
|
||||||
|
newFinalizers.add(FINALIZER_NAME);
|
||||||
|
client.update(configMap);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class RouteRuleReconciler {
|
||||||
|
|
||||||
|
public void reconcile(String name) {
|
||||||
|
reconcileArchivesRule(name);
|
||||||
|
reconcileTagsRule(name);
|
||||||
|
reconcileCategoriesRule(name);
|
||||||
|
reconcilePostRule(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reconcileArchivesRule(String name) {
|
||||||
|
client.fetch(ConfigMap.class, name).ifPresent(configMap -> {
|
||||||
|
SystemSetting.ThemeRouteRules oldRules = getOldRouteRulesFromAnno(configMap);
|
||||||
|
SystemSetting.ThemeRouteRules newRules = getRouteRules(configMap);
|
||||||
|
|
||||||
|
final String oldArchivesPrefix = oldRules.getArchives();
|
||||||
|
final String oldPostPattern = oldRules.getPost();
|
||||||
|
|
||||||
|
// dispatch event
|
||||||
|
final boolean archivesPrefixChanged =
|
||||||
|
!StringUtils.equals(oldRules.getArchives(), newRules.getArchives());
|
||||||
|
|
||||||
|
final boolean postPatternChanged =
|
||||||
|
changePostPatternPrefixIfNecessary(oldArchivesPrefix, newRules);
|
||||||
|
|
||||||
|
if (archivesPrefixChanged || postPatternChanged) {
|
||||||
|
oldRules.setPost(newRules.getPost());
|
||||||
|
oldRules.setArchives(newRules.getArchives());
|
||||||
|
updateNewRuleToConfigMap(configMap, oldRules, newRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
// archives rule changed
|
||||||
|
if (archivesPrefixChanged) {
|
||||||
|
log.debug("Archives prefix changed from [{}] to [{}].", oldArchivesPrefix,
|
||||||
|
newRules.getArchives());
|
||||||
|
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
||||||
|
DefaultTemplateEnum.ARCHIVES,
|
||||||
|
newRules.getArchives()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postPatternChanged) {
|
||||||
|
log.debug("Post pattern changed from [{}] to [{}].", oldPostPattern,
|
||||||
|
newRules.getPost());
|
||||||
|
// post rule changed
|
||||||
|
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
||||||
|
DefaultTemplateEnum.POST, newRules.getPost()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNewRuleToConfigMap(ConfigMap configMap,
|
||||||
|
SystemSetting.ThemeRouteRules oldRules,
|
||||||
|
SystemSetting.ThemeRouteRules newRules) {
|
||||||
|
Map<String, String> annotations = getAnnotationsSafe(configMap);
|
||||||
|
annotations.put(OLD_THEME_ROUTE_RULES, JsonUtils.objectToJson(oldRules));
|
||||||
|
configMap.getData().put(SystemSetting.ThemeRouteRules.GROUP,
|
||||||
|
JsonUtils.objectToJson(newRules));
|
||||||
|
client.update(configMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reconcileTagsRule(String name) {
|
||||||
|
client.fetch(ConfigMap.class, name).ifPresent(configMap -> {
|
||||||
|
SystemSetting.ThemeRouteRules oldRules = getOldRouteRulesFromAnno(configMap);
|
||||||
|
SystemSetting.ThemeRouteRules newRules = getRouteRules(configMap);
|
||||||
|
final String oldTagsPrefix = oldRules.getTags();
|
||||||
|
if (!StringUtils.equals(oldTagsPrefix, newRules.getTags())) {
|
||||||
|
oldRules.setTags(newRules.getTags());
|
||||||
|
updateNewRuleToConfigMap(configMap, oldRules, newRules);
|
||||||
|
|
||||||
|
log.debug("Tags prefix changed from [{}] to [{}].", oldTagsPrefix,
|
||||||
|
newRules.getTags());
|
||||||
|
// then publish event
|
||||||
|
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
||||||
|
DefaultTemplateEnum.TAGS,
|
||||||
|
newRules.getTags()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reconcileCategoriesRule(String name) {
|
||||||
|
client.fetch(ConfigMap.class, name).ifPresent(configMap -> {
|
||||||
|
SystemSetting.ThemeRouteRules oldRules = getOldRouteRulesFromAnno(configMap);
|
||||||
|
SystemSetting.ThemeRouteRules newRules = getRouteRules(configMap);
|
||||||
|
final String oldCategoriesPrefix = oldRules.getCategories();
|
||||||
|
if (!StringUtils.equals(oldCategoriesPrefix, newRules.getCategories())) {
|
||||||
|
oldRules.setCategories(newRules.getCategories());
|
||||||
|
updateNewRuleToConfigMap(configMap, oldRules, newRules);
|
||||||
|
|
||||||
|
log.debug("Categories prefix changed from [{}] to [{}].", oldCategoriesPrefix,
|
||||||
|
newRules.getCategories());
|
||||||
|
// categories rule changed
|
||||||
|
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
||||||
|
DefaultTemplateEnum.CATEGORIES,
|
||||||
|
newRules.getCategories()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reconcilePostRule(String name) {
|
||||||
|
client.fetch(ConfigMap.class, name).ifPresent(configMap -> {
|
||||||
|
SystemSetting.ThemeRouteRules oldRules = getOldRouteRulesFromAnno(configMap);
|
||||||
|
SystemSetting.ThemeRouteRules newRules = getRouteRules(configMap);
|
||||||
|
|
||||||
|
final String oldPostPattern = oldRules.getPost();
|
||||||
|
if (!StringUtils.equals(oldPostPattern, newRules.getPost())) {
|
||||||
|
oldRules.setPost(newRules.getPost());
|
||||||
|
updateNewRuleToConfigMap(configMap, oldRules, newRules);
|
||||||
|
|
||||||
|
log.debug("Categories prefix changed from [{}] to [{}].", oldPostPattern,
|
||||||
|
newRules.getPost());
|
||||||
|
// post rule changed
|
||||||
|
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
||||||
|
DefaultTemplateEnum.POST,
|
||||||
|
newRules.getPost()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean changePostPatternPrefixIfNecessary(String oldArchivePrefix,
|
||||||
|
SystemSetting.ThemeRouteRules newRules) {
|
||||||
|
if (StringUtils.isBlank(oldArchivePrefix)
|
||||||
|
|| StringUtils.isBlank(newRules.getPost())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String newArchivesPrefix = newRules.getArchives();
|
||||||
|
if (StringUtils.equals(oldArchivePrefix, newArchivesPrefix)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String oldPrefix = StringUtils.removeStart(oldArchivePrefix, "/");
|
||||||
|
String postPattern = StringUtils.removeStart(newRules.getPost(), "/");
|
||||||
|
|
||||||
|
if (postPattern.startsWith(oldPrefix)) {
|
||||||
|
String postPatternToUpdate = PathUtils.combinePath(newArchivesPrefix,
|
||||||
|
StringUtils.removeStart(postPattern, oldPrefix));
|
||||||
|
newRules.setPost(postPatternToUpdate);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SystemSetting.ThemeRouteRules getOldRouteRulesFromAnno(ConfigMap configMap) {
|
||||||
Map<String, String> annotations = getAnnotationsSafe(configMap);
|
Map<String, String> annotations = getAnnotationsSafe(configMap);
|
||||||
String oldRulesJson = annotations.get(OLD_THEME_ROUTE_RULES);
|
String oldRulesJson = annotations.get(OLD_THEME_ROUTE_RULES);
|
||||||
|
|
||||||
String routeRulesJson = data.get(SystemSetting.ThemeRouteRules.GROUP);
|
|
||||||
// get new rules and replace old rules to new rules
|
|
||||||
SystemSetting.ThemeRouteRules newRouteRules =
|
|
||||||
JsonUtils.jsonToObject(routeRulesJson, SystemSetting.ThemeRouteRules.class);
|
|
||||||
|
|
||||||
// old rules is empty, means this is the first time to update theme route rules
|
// old rules is empty, means this is the first time to update theme route rules
|
||||||
if (oldRulesJson == null) {
|
if (oldRulesJson == null) {
|
||||||
oldRulesJson = "{}";
|
oldRulesJson = "{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// diff old rules and new rules
|
// diff old rules and new rules
|
||||||
SystemSetting.ThemeRouteRules oldRules =
|
return JsonUtils.jsonToObject(oldRulesJson, SystemSetting.ThemeRouteRules.class);
|
||||||
JsonUtils.jsonToObject(oldRulesJson, SystemSetting.ThemeRouteRules.class);
|
|
||||||
|
|
||||||
// dispatch event
|
|
||||||
if (!StringUtils.equals(oldRules.getArchives(), newRouteRules.getArchives())) {
|
|
||||||
// archives rule changed
|
|
||||||
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
|
||||||
DefaultTemplateEnum.ARCHIVES,
|
|
||||||
newRouteRules.getArchives()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StringUtils.equals(oldRules.getTags(), newRouteRules.getTags())) {
|
private SystemSetting.ThemeRouteRules getRouteRules(ConfigMap configMap) {
|
||||||
// tags rule changed
|
Map<String, String> data = configMap.getData();
|
||||||
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
// get new rules and replace old rules to new rules
|
||||||
DefaultTemplateEnum.TAGS,
|
return JsonUtils.jsonToObject(data.get(SystemSetting.ThemeRouteRules.GROUP),
|
||||||
newRouteRules.getTags()));
|
SystemSetting.ThemeRouteRules.class);
|
||||||
}
|
|
||||||
|
|
||||||
if (!StringUtils.equals(oldRules.getCategories(), newRouteRules.getCategories())) {
|
|
||||||
// categories rule changed
|
|
||||||
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
|
||||||
DefaultTemplateEnum.CATEGORIES,
|
|
||||||
newRouteRules.getCategories()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StringUtils.equals(oldRules.getPost(), newRouteRules.getPost())) {
|
|
||||||
// post rule changed
|
|
||||||
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
|
||||||
DefaultTemplateEnum.POST,
|
|
||||||
newRouteRules.getPost()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO 此处立即同步 post 的新 pattern 到数据库,才能更新到文章页面的 permalink 地址
|
|
||||||
// 但会导致乐观锁失效会失败一次 reconcile
|
|
||||||
if (changePostPatternPrefixIfNecessary(oldRules, newRouteRules)) {
|
|
||||||
data.put(SystemSetting.ThemeRouteRules.GROUP, JsonUtils.objectToJson(newRouteRules));
|
|
||||||
annotations.put(OLD_THEME_ROUTE_RULES, JsonUtils.objectToJson(newRouteRules));
|
|
||||||
// update config map immediately
|
|
||||||
client.update(configMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update theme setting
|
|
||||||
data.put(SystemSetting.ThemeRouteRules.GROUP, JsonUtils.objectToJson(newRouteRules));
|
|
||||||
annotations.put(OLD_THEME_ROUTE_RULES, JsonUtils.objectToJson(newRouteRules));
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean changePostPatternPrefixIfNecessary(SystemSetting.ThemeRouteRules oldRules,
|
|
||||||
SystemSetting.ThemeRouteRules newRules) {
|
|
||||||
if (StringUtils.isBlank(oldRules.getArchives())
|
|
||||||
|| StringUtils.isBlank(newRules.getPost())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String oldArchivesPrefix = StringUtils.removeStart(oldRules.getArchives(), "/");
|
|
||||||
|
|
||||||
String postPattern = StringUtils.removeStart(newRules.getPost(), "/");
|
|
||||||
String newArchivesPrefix = newRules.getArchives();
|
|
||||||
if (postPattern.startsWith(oldArchivesPrefix)) {
|
|
||||||
String postPatternToUpdate = PathUtils.combinePath(newArchivesPrefix,
|
|
||||||
StringUtils.removeStart(postPattern, oldArchivesPrefix));
|
|
||||||
newRules.setPost(postPatternToUpdate);
|
|
||||||
|
|
||||||
// post rule changed
|
|
||||||
applicationContext.publishEvent(new PermalinkRuleChangedEvent(this,
|
|
||||||
DefaultTemplateEnum.POST, postPatternToUpdate));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, String> getAnnotationsSafe(ConfigMap configMap) {
|
private Map<String, String> getAnnotationsSafe(ConfigMap configMap) {
|
||||||
|
@ -144,6 +236,7 @@ public class SystemSettingReconciler implements Reconciler<Reconciler.Request> {
|
||||||
}
|
}
|
||||||
return annotations;
|
return annotations;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSystemSetting(String name) {
|
public boolean isSystemSetting(String name) {
|
||||||
return SystemSetting.SYSTEM_CONFIG.equals(name);
|
return SystemSetting.SYSTEM_CONFIG.equals(name);
|
||||||
|
|
|
@ -36,9 +36,13 @@ public class SystemConfigurableEnvironmentFetcher {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private Mono<Map<String, String>> getValuesInternal() {
|
private Mono<Map<String, String>> getValuesInternal() {
|
||||||
return extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)
|
return getConfigMap()
|
||||||
.filter(configMap -> configMap.getData() != null)
|
.filter(configMap -> configMap.getData() != null)
|
||||||
.map(ConfigMap::getData)
|
.map(ConfigMap::getData)
|
||||||
.defaultIfEmpty(Map.of());
|
.defaultIfEmpty(Map.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Mono<ConfigMap> getConfigMap() {
|
||||||
|
return extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,4 +38,44 @@ public class SystemSetting {
|
||||||
|
|
||||||
private String footer;
|
private String footer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Basic {
|
||||||
|
public static final String GROUP = "basic";
|
||||||
|
String title;
|
||||||
|
String subtitle;
|
||||||
|
String logo;
|
||||||
|
String favicon;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class User {
|
||||||
|
public static final String GROUP = "user";
|
||||||
|
Boolean allowRegistration;
|
||||||
|
String defaultRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Post {
|
||||||
|
public static final String GROUP = "post";
|
||||||
|
String sortOrder;
|
||||||
|
Integer pageSize;
|
||||||
|
Boolean review;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Seo {
|
||||||
|
public static final String GROUP = "seo";
|
||||||
|
Boolean blockSpiders;
|
||||||
|
String keywords;
|
||||||
|
String description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Comment {
|
||||||
|
public static final String GROUP = "comment";
|
||||||
|
Boolean enable;
|
||||||
|
Boolean requireReviewForNew;
|
||||||
|
Boolean systemUserOnly;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
package run.halo.app.theme;
|
package run.halo.app.theme;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.reactive.result.view.View;
|
import org.springframework.web.reactive.result.view.View;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveView;
|
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveView;
|
||||||
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveViewResolver;
|
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveViewResolver;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.scheduler.Schedulers;
|
||||||
import run.halo.app.theme.finders.FinderRegistry;
|
import run.halo.app.theme.finders.FinderRegistry;
|
||||||
|
@ -52,6 +57,48 @@ public class HaloViewResolver extends ThymeleafReactiveViewResolver {
|
||||||
.subscribeOn(Schedulers.boundedElastic());
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
protected Mono<Map<String, Object>> getModelAttributes(Map<String, ?> model,
|
||||||
|
@NonNull ServerWebExchange exchange) {
|
||||||
|
Mono<Map<String, Object>> contextBasedStaticVariables =
|
||||||
|
getContextBasedStaticVariables(exchange);
|
||||||
|
Mono<Map<String, Object>> modelAttributes = super.getModelAttributes(model, exchange);
|
||||||
|
return Flux.merge(modelAttributes, contextBasedStaticVariables)
|
||||||
|
.collectList()
|
||||||
|
.map(modelMapList -> {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
modelMapList.forEach(result::putAll);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Mono<Map<String, Object>> getContextBasedStaticVariables(
|
||||||
|
ServerWebExchange exchange) {
|
||||||
|
ApplicationContext applicationContext = obtainApplicationContext();
|
||||||
|
|
||||||
|
return Mono.just(new HashMap<String, Object>())
|
||||||
|
.flatMap(staticVariables -> {
|
||||||
|
List<Mono<Map<String, Object>>> monoList = applicationContext.getBeansOfType(
|
||||||
|
ViewContextBasedVariablesAcquirer.class)
|
||||||
|
.values()
|
||||||
|
.stream()
|
||||||
|
.map(acquirer -> acquirer.acquire(exchange))
|
||||||
|
.toList();
|
||||||
|
return Flux.merge(monoList)
|
||||||
|
.collectList()
|
||||||
|
.map(modelList -> {
|
||||||
|
Map<String, Object> mergedModel = new HashMap<>();
|
||||||
|
modelList.forEach(mergedModel::putAll);
|
||||||
|
return mergedModel;
|
||||||
|
})
|
||||||
|
.map(mergedModel -> {
|
||||||
|
staticVariables.putAll(mergedModel);
|
||||||
|
return staticVariables;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package run.halo.app.theme;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||||
|
import run.halo.app.theme.finders.vo.SiteSettingVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Site setting variables acquirer.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class SiteSettingVariablesAcquirer implements ViewContextBasedVariablesAcquirer {
|
||||||
|
|
||||||
|
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||||
|
|
||||||
|
public SiteSettingVariablesAcquirer(SystemConfigurableEnvironmentFetcher environmentFetcher) {
|
||||||
|
this.environmentFetcher = environmentFetcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Map<String, Object>> acquire(ServerWebExchange exchange) {
|
||||||
|
return environmentFetcher.getConfigMap()
|
||||||
|
.filter(configMap -> configMap.getData() != null)
|
||||||
|
.map(configMap -> {
|
||||||
|
SiteSettingVo siteSettingVo = SiteSettingVo.from(configMap);
|
||||||
|
return Map.of("site", siteSettingVo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package run.halo.app.theme;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
import run.halo.app.theme.finders.ThemeFinder;
|
||||||
|
import run.halo.app.theme.finders.vo.ThemeVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme context based variables acquirer.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ThemeContextBasedVariablesAcquirer implements ViewContextBasedVariablesAcquirer {
|
||||||
|
private final ThemeFinder themeFinder;
|
||||||
|
private final ThemeResolver themeResolver;
|
||||||
|
|
||||||
|
public ThemeContextBasedVariablesAcquirer(ThemeFinder themeFinder,
|
||||||
|
ThemeResolver themeResolver) {
|
||||||
|
this.themeFinder = themeFinder;
|
||||||
|
this.themeResolver = themeResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Map<String, Object>> acquire(ServerWebExchange exchange) {
|
||||||
|
return themeResolver.getTheme(exchange.getRequest())
|
||||||
|
.publishOn(Schedulers.boundedElastic())
|
||||||
|
.map(themeContext -> {
|
||||||
|
String name = themeContext.getName();
|
||||||
|
ThemeVo themeVo = themeFinder.getByName(name);
|
||||||
|
if (themeVo == null) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
return Map.of("theme", themeVo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package run.halo.app.theme;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ViewContextBasedVariablesAcquirer {
|
||||||
|
|
||||||
|
Mono<Map<String, Object>> acquire(ServerWebExchange exchange);
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ import org.thymeleaf.standard.StandardDialect;
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public class HaloProcessorDialect extends AbstractProcessorDialect {
|
public class HaloProcessorDialect extends AbstractProcessorDialect {
|
||||||
private static final String DIALECT_NAME = "Halo Theme Dialect";
|
private static final String DIALECT_NAME = "haloThemeProcessorDialect";
|
||||||
|
|
||||||
public HaloProcessorDialect() {
|
public HaloProcessorDialect() {
|
||||||
// We will set this dialect the same "dialect processor" precedence as
|
// We will set this dialect the same "dialect processor" precedence as
|
||||||
|
@ -27,6 +27,7 @@ public class HaloProcessorDialect extends AbstractProcessorDialect {
|
||||||
// add more processors
|
// add more processors
|
||||||
processors.add(new GlobalHeadInjectionProcessor(dialectPrefix));
|
processors.add(new GlobalHeadInjectionProcessor(dialectPrefix));
|
||||||
processors.add(new TemplateFooterElementTagProcessor(dialectPrefix));
|
processors.add(new TemplateFooterElementTagProcessor(dialectPrefix));
|
||||||
|
processors.add(new JsonNodePropertyAccessorBoundariesProcessor());
|
||||||
return processors;
|
return processors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package run.halo.app.theme.dialect;
|
||||||
|
|
||||||
|
import org.springframework.integration.json.JsonPropertyAccessor;
|
||||||
|
import org.thymeleaf.context.ITemplateContext;
|
||||||
|
import org.thymeleaf.model.ITemplateEnd;
|
||||||
|
import org.thymeleaf.model.ITemplateStart;
|
||||||
|
import org.thymeleaf.processor.templateboundaries.AbstractTemplateBoundariesProcessor;
|
||||||
|
import org.thymeleaf.processor.templateboundaries.ITemplateBoundariesStructureHandler;
|
||||||
|
import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;
|
||||||
|
import org.thymeleaf.standard.StandardDialect;
|
||||||
|
import org.thymeleaf.templatemode.TemplateMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A template boundaries processor for add {@link JsonPropertyAccessor} to
|
||||||
|
* {@link ThymeleafEvaluationContext}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class JsonNodePropertyAccessorBoundariesProcessor
|
||||||
|
extends AbstractTemplateBoundariesProcessor {
|
||||||
|
private static final int PRECEDENCE = StandardDialect.PROCESSOR_PRECEDENCE;
|
||||||
|
private static final JsonPropertyAccessor JSON_PROPERTY_ACCESSOR = new JsonPropertyAccessor();
|
||||||
|
|
||||||
|
public JsonNodePropertyAccessorBoundariesProcessor() {
|
||||||
|
super(TemplateMode.HTML, PRECEDENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doProcessTemplateStart(ITemplateContext context, ITemplateStart templateStart,
|
||||||
|
ITemplateBoundariesStructureHandler structureHandler) {
|
||||||
|
ThymeleafEvaluationContext evaluationContext =
|
||||||
|
(ThymeleafEvaluationContext) context.getVariable(
|
||||||
|
ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME);
|
||||||
|
if (evaluationContext != null) {
|
||||||
|
evaluationContext.addPropertyAccessor(JSON_PROPERTY_ACCESSOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doProcessTemplateEnd(ITemplateContext context, ITemplateEnd templateEnd,
|
||||||
|
ITemplateBoundariesStructureHandler structureHandler) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package run.halo.app.theme.finders;
|
||||||
|
|
||||||
|
import run.halo.app.theme.finders.vo.ThemeVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A finder for theme.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public interface ThemeFinder {
|
||||||
|
|
||||||
|
ThemeVo activation();
|
||||||
|
|
||||||
|
ThemeVo getByName(String themeName);
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package run.halo.app.theme.finders.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.core.extension.Theme;
|
||||||
|
import run.halo.app.extension.ConfigMap;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||||
|
import run.halo.app.infra.SystemSetting;
|
||||||
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
import run.halo.app.theme.finders.Finder;
|
||||||
|
import run.halo.app.theme.finders.ThemeFinder;
|
||||||
|
import run.halo.app.theme.finders.vo.ThemeVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A default implementation for {@link ThemeFinder}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Finder("themeFinder")
|
||||||
|
public class ThemeFinderImpl implements ThemeFinder {
|
||||||
|
|
||||||
|
private final ReactiveExtensionClient client;
|
||||||
|
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||||
|
|
||||||
|
public ThemeFinderImpl(ReactiveExtensionClient client,
|
||||||
|
SystemConfigurableEnvironmentFetcher environmentFetcher) {
|
||||||
|
this.client = client;
|
||||||
|
this.environmentFetcher = environmentFetcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ThemeVo activation() {
|
||||||
|
return environmentFetcher.fetch(SystemSetting.Theme.GROUP, SystemSetting.Theme.class)
|
||||||
|
.map(SystemSetting.Theme::getActive)
|
||||||
|
.flatMap(themeName -> client.fetch(Theme.class, themeName))
|
||||||
|
.flatMap(theme -> themeWithConfig(ThemeVo.from(theme)))
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ThemeVo getByName(String themeName) {
|
||||||
|
return client.fetch(Theme.class, themeName)
|
||||||
|
.flatMap(theme -> themeWithConfig(ThemeVo.from(theme)))
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<ThemeVo> themeWithConfig(ThemeVo themeVo) {
|
||||||
|
if (StringUtils.isBlank(themeVo.getSpec().getConfigMapName())) {
|
||||||
|
return Mono.just(themeVo);
|
||||||
|
}
|
||||||
|
return client.fetch(ConfigMap.class, themeVo.getSpec().getConfigMapName())
|
||||||
|
.map(configMap -> {
|
||||||
|
Map<String, JsonNode> config = new HashMap<>();
|
||||||
|
configMap.getData().forEach((k, v) -> {
|
||||||
|
JsonNode jsonNode = JsonUtils.jsonToObject(v, JsonNode.class);
|
||||||
|
config.put(k, jsonNode);
|
||||||
|
});
|
||||||
|
JsonNode configJson = JsonUtils.mapToObject(config, JsonNode.class);
|
||||||
|
return themeVo.withConfig(configJson);
|
||||||
|
})
|
||||||
|
.switchIfEmpty(Mono.just(themeVo));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package run.halo.app.theme.finders.vo;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Value;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import run.halo.app.extension.ConfigMap;
|
||||||
|
import run.halo.app.infra.SystemSetting;
|
||||||
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Site setting value object for theme.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Value
|
||||||
|
@Builder
|
||||||
|
public class SiteSettingVo {
|
||||||
|
|
||||||
|
String title;
|
||||||
|
|
||||||
|
String subtitle;
|
||||||
|
|
||||||
|
String logo;
|
||||||
|
|
||||||
|
String favicon;
|
||||||
|
|
||||||
|
Boolean allowRegistration;
|
||||||
|
|
||||||
|
PostSetting post;
|
||||||
|
|
||||||
|
SeoSetting seo;
|
||||||
|
|
||||||
|
CommentSetting comment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert to system {@link ConfigMap} to {@link SiteSettingVo}.
|
||||||
|
*
|
||||||
|
* @param configMap config map named system
|
||||||
|
* @return site setting value object
|
||||||
|
*/
|
||||||
|
public static SiteSettingVo from(ConfigMap configMap) {
|
||||||
|
Assert.notNull(configMap, "The configMap must not be null.");
|
||||||
|
Map<String, String> data = configMap.getData();
|
||||||
|
if (data == null) {
|
||||||
|
return builder().build();
|
||||||
|
}
|
||||||
|
SystemSetting.Basic basicSetting =
|
||||||
|
toObject(data.get(SystemSetting.Basic.GROUP), SystemSetting.Basic.class);
|
||||||
|
|
||||||
|
SystemSetting.User userSetting =
|
||||||
|
toObject(data.get(SystemSetting.User.GROUP), SystemSetting.User.class);
|
||||||
|
|
||||||
|
SystemSetting.Post postSetting =
|
||||||
|
toObject(data.get(SystemSetting.Post.GROUP), SystemSetting.Post.class);
|
||||||
|
|
||||||
|
SystemSetting.Seo seoSetting =
|
||||||
|
toObject(data.get(SystemSetting.Seo.GROUP), SystemSetting.Seo.class);
|
||||||
|
|
||||||
|
SystemSetting.Comment commentSetting = toObject(data.get(SystemSetting.Comment.GROUP),
|
||||||
|
SystemSetting.Comment.class);
|
||||||
|
return builder()
|
||||||
|
.title(basicSetting.getTitle())
|
||||||
|
.subtitle(basicSetting.getSubtitle())
|
||||||
|
.logo(basicSetting.getLogo())
|
||||||
|
.favicon(basicSetting.getFavicon())
|
||||||
|
.allowRegistration(userSetting.getAllowRegistration())
|
||||||
|
.post(PostSetting.builder()
|
||||||
|
.sortOrder(postSetting.getSortOrder())
|
||||||
|
.pageSize(postSetting.getPageSize())
|
||||||
|
.build())
|
||||||
|
.seo(SeoSetting.builder()
|
||||||
|
.blockSpiders(seoSetting.getBlockSpiders())
|
||||||
|
.keywords(seoSetting.getKeywords())
|
||||||
|
.description(seoSetting.getDescription())
|
||||||
|
.build())
|
||||||
|
.comment(CommentSetting.builder()
|
||||||
|
.enable(commentSetting.getEnable())
|
||||||
|
.requireReviewForNew(commentSetting.getRequireReviewForNew())
|
||||||
|
.systemUserOnly(commentSetting.getSystemUserOnly())
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T toObject(String json, Class<T> type) {
|
||||||
|
if (json == null) {
|
||||||
|
// empty object
|
||||||
|
json = "{}";
|
||||||
|
}
|
||||||
|
return JsonUtils.jsonToObject(json, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Value
|
||||||
|
@Builder
|
||||||
|
public static class PostSetting {
|
||||||
|
String sortOrder;
|
||||||
|
|
||||||
|
Integer pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Value
|
||||||
|
@Builder
|
||||||
|
public static class SeoSetting {
|
||||||
|
Boolean blockSpiders;
|
||||||
|
|
||||||
|
String keywords;
|
||||||
|
|
||||||
|
String description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Value
|
||||||
|
@Builder
|
||||||
|
public static class CommentSetting {
|
||||||
|
Boolean enable;
|
||||||
|
|
||||||
|
Boolean systemUserOnly;
|
||||||
|
|
||||||
|
Boolean requireReviewForNew;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package run.halo.app.theme.finders.vo;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.ToString;
|
||||||
|
import lombok.Value;
|
||||||
|
import lombok.With;
|
||||||
|
import run.halo.app.core.extension.Theme;
|
||||||
|
import run.halo.app.extension.MetadataOperator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A value object for {@link Theme}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Value
|
||||||
|
@Builder
|
||||||
|
@ToString
|
||||||
|
public class ThemeVo {
|
||||||
|
|
||||||
|
MetadataOperator metadata;
|
||||||
|
|
||||||
|
Theme.ThemeSpec spec;
|
||||||
|
|
||||||
|
@With
|
||||||
|
JsonNode config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert {@link Theme} to {@link ThemeVo}.
|
||||||
|
*
|
||||||
|
* @param theme theme extension
|
||||||
|
* @return theme value object
|
||||||
|
*/
|
||||||
|
public static ThemeVo from(Theme theme) {
|
||||||
|
return ThemeVo.builder()
|
||||||
|
.metadata(theme.getMetadata())
|
||||||
|
.spec(theme.getSpec())
|
||||||
|
.config(null)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -91,7 +91,7 @@ spec:
|
||||||
label: SEO 设置
|
label: SEO 设置
|
||||||
formSchema:
|
formSchema:
|
||||||
- $formkit: checkbox
|
- $formkit: checkbox
|
||||||
name: blockSpides
|
name: blockSpiders
|
||||||
label: "屏蔽搜索引擎"
|
label: "屏蔽搜索引擎"
|
||||||
value: false
|
value: false
|
||||||
- $formkit: textarea
|
- $formkit: textarea
|
||||||
|
|
|
@ -1,15 +1,30 @@
|
||||||
package run.halo.app.core.extension.reconciler;
|
package run.halo.app.core.extension.reconciler;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import run.halo.app.extension.ConfigMap;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
import run.halo.app.infra.SystemSetting;
|
import run.halo.app.infra.SystemSetting;
|
||||||
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link SystemSettingReconciler}.
|
* Tests for {@link SystemSettingReconciler}.
|
||||||
|
@ -34,16 +49,133 @@ class SystemSettingReconcilerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void changePostPatternPrefixIfNecessary() {
|
void reconcileArchivesRouteRule() {
|
||||||
SystemSetting.ThemeRouteRules oldRouteRules = new SystemSetting.ThemeRouteRules();
|
ConfigMap configMap = systemConfigMapForRouteRule(rules -> {
|
||||||
oldRouteRules.setPost("/archives/{slug}");
|
rules.setArchives("archives-new");
|
||||||
oldRouteRules.setArchives("archives");
|
return rules;
|
||||||
|
});
|
||||||
|
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
|
||||||
|
.thenReturn(Optional.of(configMap));
|
||||||
|
systemSettingReconciler.reconcile(new Reconciler.Request(SystemSetting.SYSTEM_CONFIG));
|
||||||
|
ArgumentCaptor<ConfigMap> captor = ArgumentCaptor.forClass(ConfigMap.class);
|
||||||
|
verify(client, times(2)).update(captor.capture());
|
||||||
|
|
||||||
|
List<ConfigMap> allValues = captor.getAllValues();
|
||||||
|
ConfigMap updatedConfigMap = allValues.get(1);
|
||||||
|
assertThat(rulesFrom(updatedConfigMap).getArchives()).isEqualTo("archives-new");
|
||||||
|
assertThat(rulesFrom(updatedConfigMap).getPost()).isEqualTo("/archives-new/{slug}");
|
||||||
|
|
||||||
|
assertThat(oldRulesFromAnno(updatedConfigMap).getArchives()).isEqualTo("archives-new");
|
||||||
|
assertThat(oldRulesFromAnno(updatedConfigMap).getPost()).isEqualTo("/archives-new/{slug}");
|
||||||
|
|
||||||
|
// archives and post
|
||||||
|
verify(applicationContext, times(2)).publishEvent(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reconcileTagsRule() {
|
||||||
|
ConfigMap configMap = systemConfigMapForRouteRule(rules -> {
|
||||||
|
rules.setTags("tags-new");
|
||||||
|
return rules;
|
||||||
|
});
|
||||||
|
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
|
||||||
|
.thenReturn(Optional.of(configMap));
|
||||||
|
systemSettingReconciler.reconcile(new Reconciler.Request(SystemSetting.SYSTEM_CONFIG));
|
||||||
|
ArgumentCaptor<ConfigMap> captor = ArgumentCaptor.forClass(ConfigMap.class);
|
||||||
|
verify(client, times(2)).update(captor.capture());
|
||||||
|
|
||||||
|
List<ConfigMap> allValues = captor.getAllValues();
|
||||||
|
ConfigMap updatedConfigMap = allValues.get(1);
|
||||||
|
assertThat(rulesFrom(updatedConfigMap).getTags()).isEqualTo("tags-new");
|
||||||
|
|
||||||
|
assertThat(oldRulesFromAnno(updatedConfigMap).getTags()).isEqualTo("tags-new");
|
||||||
|
|
||||||
|
verify(applicationContext, times(1)).publishEvent(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reconcileCategoriesRule() {
|
||||||
|
ConfigMap configMap = systemConfigMapForRouteRule(rules -> {
|
||||||
|
rules.setCategories("categories-new");
|
||||||
|
return rules;
|
||||||
|
});
|
||||||
|
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
|
||||||
|
.thenReturn(Optional.of(configMap));
|
||||||
|
systemSettingReconciler.reconcile(new Reconciler.Request(SystemSetting.SYSTEM_CONFIG));
|
||||||
|
ArgumentCaptor<ConfigMap> captor = ArgumentCaptor.forClass(ConfigMap.class);
|
||||||
|
verify(client, times(2)).update(captor.capture());
|
||||||
|
|
||||||
|
List<ConfigMap> allValues = captor.getAllValues();
|
||||||
|
ConfigMap updatedConfigMap = allValues.get(1);
|
||||||
|
assertThat(rulesFrom(updatedConfigMap).getCategories()).isEqualTo("categories-new");
|
||||||
|
|
||||||
|
assertThat(oldRulesFromAnno(updatedConfigMap).getCategories()).isEqualTo("categories-new");
|
||||||
|
|
||||||
|
verify(applicationContext, times(1)).publishEvent(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reconcilePostRule() {
|
||||||
|
ConfigMap configMap = systemConfigMapForRouteRule(rules -> {
|
||||||
|
rules.setPost("/post-new/{slug}");
|
||||||
|
return rules;
|
||||||
|
});
|
||||||
|
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
|
||||||
|
.thenReturn(Optional.of(configMap));
|
||||||
|
systemSettingReconciler.reconcile(new Reconciler.Request(SystemSetting.SYSTEM_CONFIG));
|
||||||
|
ArgumentCaptor<ConfigMap> captor = ArgumentCaptor.forClass(ConfigMap.class);
|
||||||
|
verify(client, times(2)).update(captor.capture());
|
||||||
|
|
||||||
|
List<ConfigMap> allValues = captor.getAllValues();
|
||||||
|
ConfigMap updatedConfigMap = allValues.get(1);
|
||||||
|
assertThat(rulesFrom(updatedConfigMap).getPost()).isEqualTo("/post-new/{slug}");
|
||||||
|
|
||||||
|
assertThat(oldRulesFromAnno(updatedConfigMap).getPost()).isEqualTo("/post-new/{slug}");
|
||||||
|
|
||||||
|
verify(applicationContext, times(1)).publishEvent(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SystemSetting.ThemeRouteRules rulesFrom(ConfigMap configMap) {
|
||||||
|
String s = configMap.getData().get(SystemSetting.ThemeRouteRules.GROUP);
|
||||||
|
return JsonUtils.jsonToObject(s, SystemSetting.ThemeRouteRules.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SystemSetting.ThemeRouteRules oldRulesFromAnno(ConfigMap configMap) {
|
||||||
|
Map<String, String> annotations = configMap.getMetadata().getAnnotations();
|
||||||
|
String s = annotations.get(SystemSettingReconciler.OLD_THEME_ROUTE_RULES);
|
||||||
|
return JsonUtils.jsonToObject(s, SystemSetting.ThemeRouteRules.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigMap systemConfigMapForRouteRule(
|
||||||
|
Function<SystemSetting.ThemeRouteRules, SystemSetting.ThemeRouteRules> function) {
|
||||||
|
ConfigMap configMap = new ConfigMap();
|
||||||
|
Metadata metadata = new Metadata();
|
||||||
|
metadata.setName(SystemSetting.SYSTEM_CONFIG);
|
||||||
|
configMap.setMetadata(metadata);
|
||||||
|
|
||||||
|
SystemSetting.ThemeRouteRules themeRouteRules = new SystemSetting.ThemeRouteRules();
|
||||||
|
themeRouteRules.setArchives("archives");
|
||||||
|
themeRouteRules.setTags("tags");
|
||||||
|
themeRouteRules.setCategories("categories");
|
||||||
|
themeRouteRules.setPost("/archives/{slug}");
|
||||||
|
Map<String, String> annotations = new HashMap<>();
|
||||||
|
annotations.put(SystemSettingReconciler.OLD_THEME_ROUTE_RULES,
|
||||||
|
JsonUtils.objectToJson(themeRouteRules));
|
||||||
|
metadata.setAnnotations(annotations);
|
||||||
|
|
||||||
|
SystemSetting.ThemeRouteRules newRules = function.apply(themeRouteRules);
|
||||||
|
configMap.putDataItem(SystemSetting.ThemeRouteRules.GROUP,
|
||||||
|
JsonUtils.objectToJson(newRules));
|
||||||
|
return configMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void changePostPatternPrefixIfNecessary() {
|
||||||
SystemSetting.ThemeRouteRules newRouteRules = new SystemSetting.ThemeRouteRules();
|
SystemSetting.ThemeRouteRules newRouteRules = new SystemSetting.ThemeRouteRules();
|
||||||
newRouteRules.setPost("/archives/{slug}");
|
newRouteRules.setPost("/archives/{slug}");
|
||||||
newRouteRules.setArchives("new");
|
newRouteRules.setArchives("new");
|
||||||
boolean result = systemSettingReconciler.changePostPatternPrefixIfNecessary(oldRouteRules,
|
boolean result = SystemSettingReconciler.RouteRuleReconciler
|
||||||
newRouteRules);
|
.changePostPatternPrefixIfNecessary("archives", newRouteRules);
|
||||||
assertThat(result).isTrue();
|
assertThat(result).isTrue();
|
||||||
|
|
||||||
assertThat(newRouteRules.getPost()).isEqualTo("/new/{slug}");
|
assertThat(newRouteRules.getPost()).isEqualTo("/new/{slug}");
|
||||||
|
|
|
@ -13,7 +13,6 @@ import java.time.ZoneId;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import java.util.function.Function;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -53,8 +52,6 @@ class ThemeJava8TimeDialectIntegrationTest {
|
||||||
|
|
||||||
private URL defaultThemeUrl;
|
private URL defaultThemeUrl;
|
||||||
|
|
||||||
Function<ServerHttpRequest, ThemeContext> themeContextFunction;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private WebTestClient webTestClient;
|
private WebTestClient webTestClient;
|
||||||
|
|
||||||
|
@ -63,8 +60,8 @@ class ThemeJava8TimeDialectIntegrationTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws FileNotFoundException {
|
void setUp() throws FileNotFoundException {
|
||||||
defaultThemeUrl = ResourceUtils.getURL("classpath:themes/default");
|
defaultThemeUrl = ResourceUtils.getURL("classpath:themes/default");
|
||||||
when(themeResolver.getTheme(any(ServerHttpRequest.class))).thenReturn(
|
when(themeResolver.getTheme(any(ServerHttpRequest.class)))
|
||||||
Mono.just(createDefaultContext()));
|
.thenReturn(Mono.just(createDefaultContext()));
|
||||||
defaultTimeZone = TimeZone.getDefault();
|
defaultTimeZone = TimeZone.getDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue