feat: add supports for provide theme templates in plugin class path (#4862)

* feat: add supports for provide theme templates in plugin class path
---------

Co-authored-by: Ryan Wang <i@ryanc.cc>
pull/4942/head^2
guqing 2023-11-30 11:53:58 +08:00 committed by GitHub
parent f5dcb5d925
commit f659a3279e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 340 additions and 21 deletions

View File

@ -0,0 +1,45 @@
package run.halo.app.theme;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* <p>The {@link TemplateNameResolver} is used to resolve template name.</p>
* <code>Halo</code> has a theme mechanism, template files are provided by different themes, so
* we need a method to determine whether the template file exists in the activated theme and if
* it does not exist, provide a default template name.
*
* @author guqing
* @since 2.11.0
*/
public interface TemplateNameResolver {
/**
* Resolve template name if exists or default template name in classpath.
*
* @param exchange exchange to resolve theme to use
* @param name template
* @return template name if exists or default template name in classpath
*/
Mono<String> resolveTemplateNameOrDefault(ServerWebExchange exchange, String name);
/**
* Resolve template name if exists or default template given.
*
* @param exchange exchange to resolve theme to use
* @param name template name
* @param defaultName default template name to use if given template name not exists
* @return template name if exists or default template name given
*/
Mono<String> resolveTemplateNameOrDefault(ServerWebExchange exchange, String name,
String defaultName);
/**
* Determine whether the template file exists in the current theme.
*
* @param exchange exchange to resolve theme to use
* @param name template name
* @return <code>true</code> if the template file exists in the current theme, false otherwise
*/
Mono<Boolean> isTemplateAvailableInTheme(ServerWebExchange exchange, String name);
}

View File

@ -26,6 +26,8 @@ import org.springframework.util.StopWatch;
import reactor.core.Exceptions; import reactor.core.Exceptions;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.properties.HaloProperties; import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.theme.DefaultTemplateNameResolver;
import run.halo.app.theme.DefaultViewNameResolver;
/** /**
* Plugin application initializer will create plugin application context by plugin id and * Plugin application initializer will create plugin application context by plugin id and
@ -93,9 +95,14 @@ public class PluginApplicationInitializer {
pluginApplicationContext.registerBean(AggregatedRouterFunction.class); pluginApplicationContext.registerBean(AggregatedRouterFunction.class);
beanFactory.registerSingleton("pluginContext", createPluginContext(plugin)); beanFactory.registerSingleton("pluginContext", createPluginContext(plugin));
// TODO deprecated // TODO deprecated
beanFactory.registerSingleton("pluginWrapper", haloPluginManager.getPlugin(pluginId)); beanFactory.registerSingleton("pluginWrapper", haloPluginManager.getPlugin(pluginId));
beanFactory.registerSingleton("templateNameResolver",
new DefaultTemplateNameResolver(
rootApplicationContext.getBean(DefaultViewNameResolver.class),
pluginApplicationContext));
populateSettingFetcher(pluginId, beanFactory); populateSettingFetcher(pluginId, beanFactory);
log.debug("Total millis: {} ms -> {}", stopWatch.getTotalTimeMillis(), log.debug("Total millis: {} ms -> {}", stopWatch.getTotalTimeMillis(),

View File

@ -0,0 +1,56 @@
package run.halo.app.theme;
import static run.halo.app.plugin.PluginConst.SYSTEM_PLUGIN_NAME;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.PluginApplicationContext;
/**
* A default implementation of {@link TemplateNameResolver}, It will be provided for plugins to
* resolve template name.
*
* @author guqing
* @since 2.11.0
*/
public class DefaultTemplateNameResolver implements TemplateNameResolver {
private final ApplicationContext applicationContext;
private final ViewNameResolver viewNameResolver;
public DefaultTemplateNameResolver(ViewNameResolver viewNameResolver,
ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
this.viewNameResolver = viewNameResolver;
}
@Override
public Mono<String> resolveTemplateNameOrDefault(ServerWebExchange exchange, String name) {
if (applicationContext instanceof PluginApplicationContext pluginApplicationContext) {
var pluginName = pluginApplicationContext.getPluginId();
return this.resolveTemplateNameOrDefault(exchange, name,
pluginClassPathTemplate(pluginName, name));
}
return resolveTemplateNameOrDefault(exchange, name,
pluginClassPathTemplate(SYSTEM_PLUGIN_NAME, name));
}
@Override
public Mono<String> resolveTemplateNameOrDefault(ServerWebExchange exchange, String name,
String defaultName) {
return viewNameResolver.resolveViewNameOrDefault(exchange, name, defaultName);
}
@Override
public Mono<Boolean> isTemplateAvailableInTheme(ServerWebExchange exchange, String name) {
return this.resolveTemplateNameOrDefault(exchange, name, "")
.filter(StringUtils::isNotBlank)
.hasElement();
}
String pluginClassPathTemplate(String pluginName, String templateName) {
return "plugin:" + pluginName + ":" + templateName;
}
}

View File

@ -1,4 +1,4 @@
package run.halo.app.theme.router; package run.halo.app.theme;
import java.nio.file.Files; import java.nio.file.Files;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -7,18 +7,18 @@ import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.theme.ThemeResolver;
/** /**
* The {@link ViewNameResolver} is used to resolve view name. * The {@link DefaultViewNameResolver} is used to resolve view name.
* *
* @author guqing * @author guqing
* @since 2.0.0 * @since 2.0.0
*/ */
@Component @Component
@AllArgsConstructor @AllArgsConstructor
public class ViewNameResolver { public class DefaultViewNameResolver implements ViewNameResolver {
private static final String TEMPLATES = "templates"; private static final String TEMPLATES = "templates";
private final ThemeResolver themeResolver; private final ThemeResolver themeResolver;
private final ThymeleafProperties thymeleafProperties; private final ThymeleafProperties thymeleafProperties;
@ -27,12 +27,13 @@ public class ViewNameResolver {
* Resolves view name. * Resolves view name.
* If the {@param #name} cannot be resolved to the view, the {@param #defaultName} is returned. * If the {@param #name} cannot be resolved to the view, the {@param #defaultName} is returned.
*/ */
public Mono<String> resolveViewNameOrDefault(ServerRequest request, String name, @Override
public Mono<String> resolveViewNameOrDefault(ServerWebExchange exchange, String name,
String defaultName) { String defaultName) {
if (StringUtils.isBlank(name)) { if (StringUtils.isBlank(name)) {
return Mono.justOrEmpty(defaultName); return Mono.justOrEmpty(defaultName);
} }
return themeResolver.getTheme(request.exchange()) return themeResolver.getTheme(exchange)
.mapNotNull(themeContext -> { .mapNotNull(themeContext -> {
String templateResourceName = computeResourceName(name); String templateResourceName = computeResourceName(name);
var resourcePath = themeContext.getPath() var resourcePath = themeContext.getPath()
@ -43,6 +44,12 @@ public class ViewNameResolver {
.switchIfEmpty(Mono.justOrEmpty(defaultName)); .switchIfEmpty(Mono.justOrEmpty(defaultName));
} }
@Override
public Mono<String> resolveViewNameOrDefault(ServerRequest request, String name,
String defaultName) {
return resolveViewNameOrDefault(request.exchange(), name, defaultName);
}
String computeResourceName(String name) { String computeResourceName(String name) {
Assert.notNull(name, "Name must not be null"); Assert.notNull(name, "Name must not be null");
return StringUtils.endsWith(name, thymeleafProperties.getSuffix()) return StringUtils.endsWith(name, thymeleafProperties.getSuffix())

View File

@ -2,6 +2,7 @@ package run.halo.app.theme;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.nio.file.Path; import java.nio.file.Path;
import lombok.NonNull;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties; import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -17,8 +18,10 @@ import org.thymeleaf.templateresolver.ITemplateResolver;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.infra.ExternalUrlSupplier; import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.plugin.HaloPluginManager;
import run.halo.app.theme.dialect.HaloProcessorDialect; import run.halo.app.theme.dialect.HaloProcessorDialect;
import run.halo.app.theme.engine.HaloTemplateEngine; import run.halo.app.theme.engine.HaloTemplateEngine;
import run.halo.app.theme.engine.PluginClassloaderTemplateResolver;
import run.halo.app.theme.message.ThemeMessageResolver; import run.halo.app.theme.message.ThemeMessageResolver;
/** /**
@ -45,6 +48,8 @@ public class TemplateEngineManager {
private final ExternalUrlSupplier externalUrlSupplier; private final ExternalUrlSupplier externalUrlSupplier;
private final HaloPluginManager haloPluginManager;
private final ObjectProvider<ITemplateResolver> templateResolvers; private final ObjectProvider<ITemplateResolver> templateResolvers;
private final ObjectProvider<IDialect> dialects; private final ObjectProvider<IDialect> dialects;
@ -53,10 +58,11 @@ public class TemplateEngineManager {
public TemplateEngineManager(ThymeleafProperties thymeleafProperties, public TemplateEngineManager(ThymeleafProperties thymeleafProperties,
ExternalUrlSupplier externalUrlSupplier, ExternalUrlSupplier externalUrlSupplier,
ObjectProvider<ITemplateResolver> templateResolvers, HaloPluginManager haloPluginManager, ObjectProvider<ITemplateResolver> templateResolvers,
ObjectProvider<IDialect> dialects, ThemeResolver themeResolver) { ObjectProvider<IDialect> dialects, ThemeResolver themeResolver) {
this.thymeleafProperties = thymeleafProperties; this.thymeleafProperties = thymeleafProperties;
this.externalUrlSupplier = externalUrlSupplier; this.externalUrlSupplier = externalUrlSupplier;
this.haloPluginManager = haloPluginManager;
this.templateResolvers = templateResolvers; this.templateResolvers = templateResolvers;
this.dialects = dialects; this.dialects = dialects;
this.themeResolver = themeResolver; this.themeResolver = themeResolver;
@ -119,6 +125,8 @@ public class TemplateEngineManager {
var mainResolver = haloTemplateResolver(); var mainResolver = haloTemplateResolver();
mainResolver.setPrefix(cacheKey.context().getPath().resolve("templates") + "/"); mainResolver.setPrefix(cacheKey.context().getPath().resolve("templates") + "/");
engine.addTemplateResolver(mainResolver); engine.addTemplateResolver(mainResolver);
var pluginTemplateResolver = createPluginClassloaderTemplateResolver();
engine.addTemplateResolver(pluginTemplateResolver);
// replace StandardDialect with SpringStandardDialect // replace StandardDialect with SpringStandardDialect
engine.setDialect(new SpringStandardDialect() { engine.setDialect(new SpringStandardDialect() {
@Override @Override
@ -134,6 +142,19 @@ public class TemplateEngineManager {
return engine; return engine;
} }
@NonNull
private PluginClassloaderTemplateResolver createPluginClassloaderTemplateResolver() {
var pluginTemplateResolver = new PluginClassloaderTemplateResolver(haloPluginManager);
pluginTemplateResolver.setPrefix(thymeleafProperties.getPrefix());
pluginTemplateResolver.setSuffix(thymeleafProperties.getSuffix());
pluginTemplateResolver.setTemplateMode(thymeleafProperties.getMode());
pluginTemplateResolver.setOrder(1);
if (thymeleafProperties.getEncoding() != null) {
pluginTemplateResolver.setCharacterEncoding(thymeleafProperties.getEncoding().name());
}
return pluginTemplateResolver;
}
FileTemplateResolver haloTemplateResolver() { FileTemplateResolver haloTemplateResolver() {
final var resolver = new FileTemplateResolver(); final var resolver = new FileTemplateResolver();
resolver.setTemplateMode(thymeleafProperties.getMode()); resolver.setTemplateMode(thymeleafProperties.getMode());

View File

@ -0,0 +1,20 @@
package run.halo.app.theme;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* The {@link ViewNameResolver} is used to resolve view name if the view name cannot be resolved
* to the view, the default view name is returned.
*
* @author guqing
* @since 2.10.2
*/
public interface ViewNameResolver {
Mono<String> resolveViewNameOrDefault(ServerWebExchange exchange, String name,
String defaultName);
Mono<String> resolveViewNameOrDefault(ServerRequest request, String name,
String defaultName);
}

View File

@ -0,0 +1,105 @@
package run.halo.app.theme.engine;
import static run.halo.app.plugin.PluginConst.SYSTEM_PLUGIN_NAME;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.pf4j.PluginState;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.lang.Nullable;
import org.thymeleaf.IEngineConfiguration;
import org.thymeleaf.spring6.templateresource.SpringResourceTemplateResource;
import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
import org.thymeleaf.templateresource.ITemplateResource;
import run.halo.app.plugin.HaloPluginManager;
/**
* Plugin classloader template resolver to resolve template by plugin classloader.
*
* @author guqing
* @since 2.11.0
*/
public class PluginClassloaderTemplateResolver extends AbstractConfigurableTemplateResolver {
private final HaloPluginManager haloPluginManager;
static final Pattern PLUGIN_TEMPLATE_PATTERN =
Pattern.compile("plugin:([A-Za-z0-9\\-.]+):(.+)");
/**
* Create a new plugin classloader template resolver, not cacheable.
*
* @param haloPluginManager plugin manager must not be null
*/
public PluginClassloaderTemplateResolver(HaloPluginManager haloPluginManager) {
super();
this.haloPluginManager = haloPluginManager;
setCacheable(false);
}
@Override
protected ITemplateResource computeTemplateResource(
final IEngineConfiguration configuration, final String ownerTemplate, final String template,
final String resourceName, final String characterEncoding,
final Map<String, Object> templateResolutionAttributes) {
var matchResult = matchPluginTemplate(ownerTemplate, template);
if (!matchResult.matches()) {
return null;
}
String pluginName = matchResult.pluginName();
var classloader = getClassloaderByPlugin(pluginName);
if (classloader == null) {
return null;
}
var templateName = matchResult.templateName();
var ownerTemplateName = matchResult.ownerTemplateName();
String handledResourceName = computeResourceName(configuration, ownerTemplateName,
templateName, getPrefix(), getSuffix(), getForceSuffix(), getTemplateAliases(),
templateResolutionAttributes);
var resource = new DefaultResourceLoader(classloader)
.getResource(handledResourceName);
return new SpringResourceTemplateResource(resource, characterEncoding);
}
MatchResult matchPluginTemplate(String ownerTemplate, String template) {
boolean matches = false;
String pluginName = null;
String templateName = template;
String ownerTemplateName = ownerTemplate;
if (StringUtils.isNotBlank(ownerTemplate)) {
Matcher ownerTemplateMatcher = PLUGIN_TEMPLATE_PATTERN.matcher(ownerTemplate);
if (ownerTemplateMatcher.matches()) {
matches = true;
pluginName = ownerTemplateMatcher.group(1);
ownerTemplateName = ownerTemplateMatcher.group(2);
}
}
Matcher templateMatcher = PLUGIN_TEMPLATE_PATTERN.matcher(template);
if (templateMatcher.matches()) {
matches = true;
pluginName = templateMatcher.group(1);
templateName = templateMatcher.group(2);
}
return new MatchResult(pluginName, ownerTemplateName, templateName, matches);
}
record MatchResult(String pluginName, String ownerTemplateName, String templateName,
boolean matches) {
}
@Nullable
private ClassLoader getClassloaderByPlugin(String pluginName) {
if (SYSTEM_PLUGIN_NAME.equals(pluginName)) {
return this.getClass().getClassLoader();
}
var pluginWrapper = haloPluginManager.getPlugin(pluginName);
if (pluginWrapper == null || !PluginState.STARTED.equals(pluginWrapper.getPluginState())) {
return null;
}
return pluginWrapper.getPluginClassLoader();
}
}

View File

@ -24,6 +24,7 @@ import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.AnonymousUserConst; import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.ViewNameResolver;
import run.halo.app.theme.finders.PostPublicQueryService; import run.halo.app.theme.finders.PostPublicQueryService;
import run.halo.app.theme.finders.SinglePageConversionService; import run.halo.app.theme.finders.SinglePageConversionService;
import run.halo.app.theme.finders.vo.ContributorVo; import run.halo.app.theme.finders.vo.ContributorVo;

View File

@ -35,6 +35,7 @@ import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler; import run.halo.app.extension.controller.Reconciler;
import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.ViewNameResolver;
import run.halo.app.theme.finders.SinglePageFinder; import run.halo.app.theme.finders.SinglePageFinder;
/** /**

View File

@ -23,6 +23,7 @@ import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.infra.utils.PathUtils; import run.halo.app.infra.utils.PathUtils;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.ViewNameResolver;
import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.finders.vo.CategoryVo; import run.halo.app.theme.finders.vo.CategoryVo;
import run.halo.app.theme.finders.vo.ListedPostVo; import run.halo.app.theme.finders.vo.ListedPostVo;
@ -30,7 +31,6 @@ import run.halo.app.theme.router.ModelConst;
import run.halo.app.theme.router.PageUrlUtils; import run.halo.app.theme.router.PageUrlUtils;
import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator; import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator;
import run.halo.app.theme.router.UrlContextListResult; import run.halo.app.theme.router.UrlContextListResult;
import run.halo.app.theme.router.ViewNameResolver;
/** /**
* The {@link CategoryPostRouteFactory} for generate {@link RouterFunction} specific to the template * The {@link CategoryPostRouteFactory} for generate {@link RouterFunction} specific to the template

View File

@ -35,12 +35,12 @@ import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.infra.utils.JsonUtils; import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.ViewNameResolver;
import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.finders.vo.PostVo; import run.halo.app.theme.finders.vo.PostVo;
import run.halo.app.theme.router.ModelMapUtils; import run.halo.app.theme.router.ModelMapUtils;
import run.halo.app.theme.router.ReactiveQueryPostPredicateResolver; import run.halo.app.theme.router.ReactiveQueryPostPredicateResolver;
import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator; import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator;
import run.halo.app.theme.router.ViewNameResolver;
/** /**
* The {@link PostRouteFactory} for generate {@link RouterFunction} specific to the template * The {@link PostRouteFactory} for generate {@link RouterFunction} specific to the template

View File

@ -1,4 +1,4 @@
package run.halo.app.theme.router; package run.halo.app.theme;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -24,11 +24,9 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import run.halo.app.theme.ThemeContext;
import run.halo.app.theme.ThemeResolver;
/** /**
* Tests for {@link ViewNameResolver}. * Tests for {@link DefaultViewNameResolver}.
* *
* @author guqing * @author guqing
* @since 2.0.0 * @since 2.0.0
@ -43,7 +41,7 @@ class ViewNameResolverTest {
private ThymeleafProperties thymeleafProperties; private ThymeleafProperties thymeleafProperties;
@InjectMocks @InjectMocks
private ViewNameResolver viewNameResolver; private DefaultViewNameResolver viewNameResolver;
@TempDir @TempDir
private File themePath; private File themePath;

View File

@ -0,0 +1,53 @@
package run.halo.app.theme.engine;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import run.halo.app.plugin.HaloPluginManager;
/**
* Tests for {@link PluginClassloaderTemplateResolver}.
*
* @author guqing
* @since 2.11.0
*/
@ExtendWith(MockitoExtension.class)
class PluginClassloaderTemplateResolverTest {
@Mock
private HaloPluginManager haloPluginManager;
@InjectMocks
private PluginClassloaderTemplateResolver templateResolver;
@Test
void matchPluginTemplateWhenOwnerTemplateMatch() {
var result =
templateResolver.matchPluginTemplate("plugin:fake-plugin:doc", "modules/layout");
assertThat(result.matches()).isTrue();
assertThat(result.pluginName()).isEqualTo("fake-plugin");
assertThat(result.templateName()).isEqualTo("modules/layout");
assertThat(result.ownerTemplateName()).isEqualTo("doc");
}
@Test
void matchPluginTemplateWhenDoesNotMatch() {
var result =
templateResolver.matchPluginTemplate("doc", "modules/layout");
assertThat(result.matches()).isFalse();
}
@Test
void matchPluginTemplateWhenTemplateMatch() {
var result =
templateResolver.matchPluginTemplate("doc", "plugin:fake-plugin:modules/layout");
assertThat(result.matches()).isTrue();
assertThat(result.pluginName()).isEqualTo("fake-plugin");
assertThat(result.templateName()).isEqualTo("modules/layout");
assertThat(result.ownerTemplateName()).isEqualTo("doc");
}
}

View File

@ -17,6 +17,7 @@ import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -26,6 +27,7 @@ import run.halo.app.core.extension.content.SinglePage;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.AnonymousUserConst; import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.theme.ViewNameResolver;
import run.halo.app.theme.finders.PostPublicQueryService; import run.halo.app.theme.finders.PostPublicQueryService;
import run.halo.app.theme.finders.SinglePageConversionService; import run.halo.app.theme.finders.SinglePageConversionService;
import run.halo.app.theme.finders.vo.ContributorVo; import run.halo.app.theme.finders.vo.ContributorVo;
@ -99,8 +101,8 @@ class PreviewRouterFunctionTest {
when(postPublicQueryService.convertToVo(eq(post), eq(post.getSpec().getHeadSnapshot()))) when(postPublicQueryService.convertToVo(eq(post), eq(post.getSpec().getHeadSnapshot())))
.thenReturn(Mono.just(postVo)); .thenReturn(Mono.just(postVo));
when(viewNameResolver.resolveViewNameOrDefault(any(), eq("postTemplate"), when(viewNameResolver.resolveViewNameOrDefault(any(ServerRequest.class),
eq("post"))).thenReturn(Mono.just("postView")); eq("postTemplate"), eq("post"))).thenReturn(Mono.just("postView"));
webTestClient.get().uri("/preview/posts/post1") webTestClient.get().uri("/preview/posts/post1")
.exchange() .exchange()
@ -135,8 +137,8 @@ class PreviewRouterFunctionTest {
when(singlePageConversionService.convertToVo(singlePage, "snapshot1")) when(singlePageConversionService.convertToVo(singlePage, "snapshot1"))
.thenReturn(Mono.just(singlePageVo)); .thenReturn(Mono.just(singlePageVo));
when(viewNameResolver.resolveViewNameOrDefault(any(), eq("pageTemplate"), when(viewNameResolver.resolveViewNameOrDefault(any(ServerRequest.class),
eq("page"))).thenReturn(Mono.just("pageView")); eq("pageTemplate"), eq("page"))).thenReturn(Mono.just("pageView"));
webTestClient.get().uri("/preview/singlepages/page1") webTestClient.get().uri("/preview/singlepages/page1")
.exchange() .exchange()

View File

@ -34,6 +34,7 @@ import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
@ -47,6 +48,7 @@ import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.controller.Reconciler; import run.halo.app.extension.controller.Reconciler;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.ViewNameResolver;
import run.halo.app.theme.finders.SinglePageFinder; import run.halo.app.theme.finders.SinglePageFinder;
import run.halo.app.theme.finders.vo.SinglePageVo; import run.halo.app.theme.finders.vo.SinglePageVo;
import run.halo.app.theme.router.SinglePageRoute.NameSlugPair; import run.halo.app.theme.router.SinglePageRoute.NameSlugPair;
@ -84,7 +86,7 @@ class SinglePageRouteTest {
@Test @Test
void handlerFunction() { void handlerFunction() {
// fix gh-3448 // fix gh-3448
when(viewNameResolver.resolveViewNameOrDefault(any(), any(), any())) when(viewNameResolver.resolveViewNameOrDefault(any(ServerRequest.class), any(), any()))
.thenReturn(Mono.just(DefaultTemplateEnum.POST.getValue())); .thenReturn(Mono.just(DefaultTemplateEnum.POST.getValue()));
String pageName = "fake-page"; String pageName = "fake-page";

View File

@ -16,6 +16,7 @@ import org.springframework.context.i18n.SimpleLocaleContext;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.i18n.LocaleContextResolver; import org.springframework.web.server.i18n.LocaleContextResolver;
@ -26,6 +27,7 @@ import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.MetadataUtil;
import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.ViewNameResolver;
import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.finders.vo.PostVo; import run.halo.app.theme.finders.vo.PostVo;
import run.halo.app.theme.router.DefaultQueryPostPredicateResolver; import run.halo.app.theme.router.DefaultQueryPostPredicateResolver;
@ -33,7 +35,6 @@ import run.halo.app.theme.router.EmptyView;
import run.halo.app.theme.router.ModelConst; import run.halo.app.theme.router.ModelConst;
import run.halo.app.theme.router.ReactiveQueryPostPredicateResolver; import run.halo.app.theme.router.ReactiveQueryPostPredicateResolver;
import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator; import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator;
import run.halo.app.theme.router.ViewNameResolver;
/** /**
* Tests for {@link PostRouteFactory}. * Tests for {@link PostRouteFactory}.
@ -77,7 +78,7 @@ class PostRouteFactoryTest extends RouteFactoryTestSuite {
when(client.fetch(eq(Post.class), eq("fake-name"))).thenReturn(Mono.just(post)); when(client.fetch(eq(Post.class), eq("fake-name"))).thenReturn(Mono.just(post));
when(viewNameResolver.resolveViewNameOrDefault(any(), any(), any())) when(viewNameResolver.resolveViewNameOrDefault(any(ServerRequest.class), any(), any()))
.thenReturn(Mono.just(DefaultTemplateEnum.POST.getValue())); .thenReturn(Mono.just(DefaultTemplateEnum.POST.getValue()));
when(predicateResolver.getPredicate()) when(predicateResolver.getPredicate())
.thenReturn(new DefaultQueryPostPredicateResolver().getPredicate()); .thenReturn(new DefaultQueryPostPredicateResolver().getPredicate());