[release-2.9] fix: not using the default template when the custom template does not exist (#4648)

This is an automated cherry-pick of #4618

/assign guqing

```release-note
修复文章自定义模板不存在时没有使用默认模板的问题
```
release-2.9
Halo Dev Bot 2023-09-22 17:56:23 +08:00 committed by GitHub
parent 12d25a4300
commit 5a34fb51a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 58 additions and 35 deletions

View File

@ -1,14 +1,14 @@
package run.halo.app.theme.router; package run.halo.app.theme.router;
import java.util.Locale; import java.nio.file.Files;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties; import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.theme.HaloViewResolver; import run.halo.app.theme.ThemeResolver;
/** /**
* The {@link ViewNameResolver} is used to resolve view name. * The {@link ViewNameResolver} is used to resolve view name.
@ -19,7 +19,8 @@ import run.halo.app.theme.HaloViewResolver;
@Component @Component
@AllArgsConstructor @AllArgsConstructor
public class ViewNameResolver { public class ViewNameResolver {
private final HaloViewResolver haloViewResolver; private static final String TEMPLATES = "templates";
private final ThemeResolver themeResolver;
private final ThymeleafProperties thymeleafProperties; private final ThymeleafProperties thymeleafProperties;
/** /**
@ -29,20 +30,22 @@ public class ViewNameResolver {
public Mono<String> resolveViewNameOrDefault(ServerRequest request, String name, public Mono<String> resolveViewNameOrDefault(ServerRequest request, String name,
String defaultName) { String defaultName) {
if (StringUtils.isBlank(name)) { if (StringUtils.isBlank(name)) {
return Mono.just(defaultName); return Mono.justOrEmpty(defaultName);
} }
final String nameToUse = processName(name); return themeResolver.getTheme(request.exchange())
Locale locale = LocaleContextHolder.getLocale(request.exchange().getLocaleContext()); .mapNotNull(themeContext -> {
return haloViewResolver.resolveViewName(nameToUse, locale) String templateResourceName = computeResourceName(name);
.map(view -> nameToUse) var resourcePath = themeContext.getPath()
.switchIfEmpty(Mono.just(defaultName)); .resolve(TEMPLATES)
.resolve(templateResourceName);
return Files.exists(resourcePath) ? name : defaultName;
})
.switchIfEmpty(Mono.justOrEmpty(defaultName));
} }
String processName(String name) { String computeResourceName(String name) {
String nameToLookup = name; Assert.notNull(name, "Name must not be null");
if (StringUtils.endsWith(name, thymeleafProperties.getSuffix())) { return StringUtils.endsWith(name, thymeleafProperties.getSuffix())
nameToLookup = StringUtils.substringBeforeLast(name, thymeleafProperties.getSuffix()); ? name : name + thymeleafProperties.getSuffix();
}
return nameToLookup;
} }
} }

View File

@ -1,15 +1,19 @@
package run.halo.app.theme.router; package run.halo.app.theme.router;
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.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.file.Files;
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.junit.jupiter.api.io.TempDir;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
@ -17,11 +21,11 @@ import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.mock.web.reactive.function.server.MockServerRequest; import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.reactive.result.view.View;
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.HaloViewResolver; import run.halo.app.theme.ThemeContext;
import run.halo.app.theme.ThemeResolver;
/** /**
* Tests for {@link ViewNameResolver}. * Tests for {@link ViewNameResolver}.
@ -33,7 +37,7 @@ import run.halo.app.theme.HaloViewResolver;
class ViewNameResolverTest { class ViewNameResolverTest {
@Mock @Mock
private HaloViewResolver haloViewResolver; private ThemeResolver themeResolver;
@Mock @Mock
private ThymeleafProperties thymeleafProperties; private ThymeleafProperties thymeleafProperties;
@ -41,17 +45,27 @@ class ViewNameResolverTest {
@InjectMocks @InjectMocks
private ViewNameResolver viewNameResolver; private ViewNameResolver viewNameResolver;
@TempDir
private File themePath;
@BeforeEach @BeforeEach
void setUp() { void setUp() throws IOException {
when(thymeleafProperties.getSuffix()).thenReturn(ThymeleafProperties.DEFAULT_SUFFIX); when(thymeleafProperties.getSuffix()).thenReturn(ThymeleafProperties.DEFAULT_SUFFIX);
when(haloViewResolver.resolveViewName(eq("post_news"), any())) var templatesPath = themePath.toPath().resolve("templates");
.thenReturn(Mono.just(Mockito.mock(View.class))); if (!Files.exists(templatesPath)) {
when(haloViewResolver.resolveViewName(eq("post_docs"), any())) Files.createDirectory(templatesPath);
.thenReturn(Mono.just(new EmptyView())); }
Files.createFile(templatesPath.resolve("post_news.html"));
Files.createFile(templatesPath.resolve("post_docs.html"));
when(haloViewResolver.resolveViewName(eq("post_nothing"), any())) when(themeResolver.getTheme(any()))
.thenReturn(Mono.empty()); .thenReturn(Mono.fromSupplier(() -> ThemeContext.builder()
.name("fake-theme")
.path(themePath.toPath())
.active(true)
.build())
);
} }
@Test @Test
@ -71,7 +85,7 @@ class ViewNameResolverTest {
String viewName = "post_docs" + thymeleafProperties.getSuffix(); String viewName = "post_docs" + thymeleafProperties.getSuffix();
viewNameResolver.resolveViewNameOrDefault(request, viewName, "post") viewNameResolver.resolveViewNameOrDefault(request, viewName, "post")
.as(StepVerifier::create) .as(StepVerifier::create)
.expectNext("post_docs") .expectNext(viewName)
.verifyComplete(); .verifyComplete();
viewNameResolver.resolveViewNameOrDefault(request, "post_nothing", "post") viewNameResolver.resolveViewNameOrDefault(request, "post_nothing", "post")
@ -82,11 +96,17 @@ class ViewNameResolverTest {
@Test @Test
void processName() { void processName() {
assertThat(viewNameResolver.processName("post_news")).isEqualTo("post_news"); var suffix = thymeleafProperties.getSuffix();
assertThat(viewNameResolver.processName("post_news" + thymeleafProperties.getSuffix())) assertThat(viewNameResolver.computeResourceName("post_news"))
.isEqualTo("post_news"); .isEqualTo("post_news" + suffix);
assertThat(viewNameResolver.processName("post_news.test")) assertThat(
.isEqualTo("post_news.test"); viewNameResolver.computeResourceName("post_news" + suffix))
assertThat(viewNameResolver.processName(null)).isNull(); .isEqualTo("post_news" + suffix);
assertThat(viewNameResolver.computeResourceName("post_news.test"))
.isEqualTo("post_news.test" + suffix);
assertThatThrownBy(() -> viewNameResolver.computeResourceName(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Name must not be null");
} }
} }