mirror of https://github.com/halo-dev/halo
Respond not found if no theme template found (#6511)
#### What type of PR is this? /kind improvement /area core /milestone 2.19.0 #### What this PR does / why we need it: This PR refactors "Template Not Found Exception" into "NotFoundException" to prevent too many exception stacktraces in logs file. #### Which issue(s) this PR fixes: Fixes #6501 #### Special notes for your reviewer: 1. Activate default theme 2. Request <http://localhost:8090/categories> 3. See the result #### Does this PR introduce a user-facing change? ```release-note 优化当主题模板找不到的异常提示 ```pull/6515/head
parent
3767a5a239
commit
50adc29e42
|
@ -103,6 +103,7 @@ tasks.named('build') {
|
||||||
|
|
||||||
tasks.named('test', Test) {
|
tasks.named('test', Test) {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
maxHeapSize = '1G'
|
||||||
finalizedBy jacocoTestReport
|
finalizedBy jacocoTestReport
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
package run.halo.app.theme;
|
||||||
|
|
||||||
|
import static java.util.Comparator.comparing;
|
||||||
|
import static java.util.Comparator.naturalOrder;
|
||||||
|
import static java.util.Comparator.nullsLast;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.thymeleaf.IEngineConfiguration;
|
||||||
|
import org.thymeleaf.templateresolver.ITemplateResolver;
|
||||||
|
import org.thymeleaf.templateresolver.TemplateResolution;
|
||||||
|
import run.halo.app.infra.exception.NotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composite template resolver to control execution flow of multiple template resolvers.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
class CompositeTemplateResolver implements ITemplateResolver {
|
||||||
|
|
||||||
|
private final List<ITemplateResolver> resolvers;
|
||||||
|
|
||||||
|
public CompositeTemplateResolver(Collection<ITemplateResolver> resolvers) {
|
||||||
|
this.resolvers = Optional.ofNullable(resolvers).orElseGet(List::of)
|
||||||
|
.stream()
|
||||||
|
.distinct()
|
||||||
|
// we keep the same order comparison as
|
||||||
|
// org.thymeleaf.EngineConfiguration.TemplateResolverComparator
|
||||||
|
.sorted(comparing(ITemplateResolver::getOrder, nullsLast(naturalOrder())))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return resolvers.stream()
|
||||||
|
.map(ITemplateResolver::getName)
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getOrder() {
|
||||||
|
// null order means to be the end of the resolvers.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TemplateResolution resolveTemplate(IEngineConfiguration configuration,
|
||||||
|
String ownerTemplate, String template, Map<String, Object> templateResolutionAttributes) {
|
||||||
|
return resolvers.stream()
|
||||||
|
.map(resolver -> resolver.resolveTemplate(
|
||||||
|
configuration, ownerTemplate, template, templateResolutionAttributes)
|
||||||
|
)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new NotFoundException("Template " + template + " was not found."));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,13 +4,18 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.attoparser.ParseException;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.ErrorResponse;
|
||||||
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.exceptions.TemplateInputException;
|
||||||
|
import org.thymeleaf.exceptions.TemplateProcessingException;
|
||||||
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.Flux;
|
||||||
|
@ -54,7 +59,26 @@ public class HaloViewResolver extends ThymeleafReactiveViewResolver {
|
||||||
// calculate the engine before rendering
|
// calculate the engine before rendering
|
||||||
setTemplateEngine(engineManager.getTemplateEngine(theme));
|
setTemplateEngine(engineManager.getTemplateEngine(theme));
|
||||||
exchange.getAttributes().put(ModelConst.POWERED_BY_HALO_TEMPLATE_ENGINE, true);
|
exchange.getAttributes().put(ModelConst.POWERED_BY_HALO_TEMPLATE_ENGINE, true);
|
||||||
return super.render(model, contentType, exchange);
|
return super.render(model, contentType, exchange)
|
||||||
|
.onErrorMap(TemplateProcessingException.class::isInstance, tee -> {
|
||||||
|
if (tee instanceof TemplateInputException) {
|
||||||
|
// map the error response exception while fragment not found
|
||||||
|
return Optional.of(tee)
|
||||||
|
.map(Throwable::getCause)
|
||||||
|
.filter(ParseException.class::isInstance)
|
||||||
|
.map(Throwable::getCause)
|
||||||
|
.filter(TemplateProcessingException.class::isInstance)
|
||||||
|
.map(Throwable::getCause)
|
||||||
|
.filter(ErrorResponse.class::isInstance)
|
||||||
|
.orElse(tee);
|
||||||
|
}
|
||||||
|
// map the error response exception while template not found
|
||||||
|
return Optional.of(tee)
|
||||||
|
.map(Throwable::getCause)
|
||||||
|
.filter(ErrorResponse.class::isInstance)
|
||||||
|
.orElse(tee);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,13 @@ public class TemplateEngineManager {
|
||||||
engine.addDialect(new HaloProcessorDialect());
|
engine.addDialect(new HaloProcessorDialect());
|
||||||
|
|
||||||
templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
|
templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
|
||||||
|
|
||||||
|
// we collect all template resolvers and add them into composite template resolver
|
||||||
|
// to control the resolution flow
|
||||||
|
var compositeTemplateResolver =
|
||||||
|
new CompositeTemplateResolver(engine.getTemplateResolvers());
|
||||||
|
engine.setTemplateResolver(compositeTemplateResolver);
|
||||||
|
|
||||||
dialects.orderedStream().forEach(engine::addDialect);
|
dialects.orderedStream().forEach(engine::addDialect);
|
||||||
|
|
||||||
return engine;
|
return engine;
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
package run.halo.app.theme;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.thymeleaf.templateresolver.ITemplateResolver;
|
||||||
|
import org.thymeleaf.templateresolver.TemplateResolution;
|
||||||
|
import run.halo.app.infra.exception.NotFoundException;
|
||||||
|
|
||||||
|
class CompositeTemplateResolverTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetBlankNameIfNoResolvers() {
|
||||||
|
var resolver = new CompositeTemplateResolver(null);
|
||||||
|
assertEquals("", resolver.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetNameIfResolversProvided() {
|
||||||
|
var resolverA = mock(ITemplateResolver.class);
|
||||||
|
when(resolverA.getName()).thenReturn("A");
|
||||||
|
var resolverB = mock(ITemplateResolver.class);
|
||||||
|
when(resolverB.getName()).thenReturn("B");
|
||||||
|
var resolver = new CompositeTemplateResolver(List.of(resolverB, resolverA));
|
||||||
|
assertEquals("B, A", resolver.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldGetNullOrder() {
|
||||||
|
var resolver = new CompositeTemplateResolver(null);
|
||||||
|
assertNull(resolver.getOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowNotFoundExceptionIfNoResolvers() {
|
||||||
|
var resolver = new CompositeTemplateResolver(null);
|
||||||
|
assertThrows(
|
||||||
|
NotFoundException.class,
|
||||||
|
() -> resolver.resolveTemplate(null, null, null, null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowNotFoundExceptionIfAllResolversReturnNull() {
|
||||||
|
var resolverA = mock(ITemplateResolver.class);
|
||||||
|
when(resolverA.resolveTemplate(null, null, null, null)).thenReturn(null);
|
||||||
|
var resolverB = mock(ITemplateResolver.class);
|
||||||
|
when(resolverB.resolveTemplate(null, null, null, null)).thenReturn(null);
|
||||||
|
var resolver = new CompositeTemplateResolver(List.of(resolverA, resolverB));
|
||||||
|
assertThrows(
|
||||||
|
NotFoundException.class,
|
||||||
|
() -> resolver.resolveTemplate(null, null, null, null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldResolveTemplateIfResolvedByOneOfResolvers() {
|
||||||
|
var resolverA = mock(ITemplateResolver.class);
|
||||||
|
var resolution = mock(TemplateResolution.class);
|
||||||
|
when(resolverA.resolveTemplate(null, null, null, null))
|
||||||
|
.thenReturn(resolution);
|
||||||
|
var resolverB = mock(ITemplateResolver.class);
|
||||||
|
when(resolverB.resolveTemplate(null, null, null, null))
|
||||||
|
.thenReturn(null);
|
||||||
|
var resolver = new CompositeTemplateResolver(List.of(resolverA, resolverB));
|
||||||
|
assertEquals(resolution, resolver.resolveTemplate(null, null, null, null));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package run.halo.app.theme;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.core.extension.Menu;
|
||||||
|
import run.halo.app.core.extension.MenuItem;
|
||||||
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.infra.InitializationStateGetter;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@Import(ThemeIntegrationTest.TestConfig.class)
|
||||||
|
@AutoConfigureWebTestClient
|
||||||
|
@DirtiesContext
|
||||||
|
public class ThemeIntegrationTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
WebTestClient webClient;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
InitializationStateGetter initializationStateGetter;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ExtensionClient client;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(true));
|
||||||
|
|
||||||
|
// create a menu item
|
||||||
|
var menuItem = new MenuItem();
|
||||||
|
menuItem.setMetadata(new Metadata());
|
||||||
|
menuItem.getMetadata().setName("main-menu-home");
|
||||||
|
menuItem.setSpec(new MenuItem.MenuItemSpec());
|
||||||
|
menuItem.getSpec().setDisplayName("Home");
|
||||||
|
menuItem.getSpec().setHref("/");
|
||||||
|
client.create(menuItem);
|
||||||
|
|
||||||
|
// create a primary menu
|
||||||
|
var menu = new Menu();
|
||||||
|
menu.setMetadata(new Metadata());
|
||||||
|
menu.getMetadata().setName("main-menu");
|
||||||
|
menu.setSpec(new Menu.Spec());
|
||||||
|
menu.getSpec().setDisplayName("Mail Menu");
|
||||||
|
menu.getSpec().setMenuItems(new LinkedHashSet<>(List.of("main-menu-home")));
|
||||||
|
client.create(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
static class TestConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
RouterFunction<ServerResponse> noTemplateExistsRoute() {
|
||||||
|
return RouterFunctions.route()
|
||||||
|
.GET(
|
||||||
|
"/no-template-exists",
|
||||||
|
request -> ServerResponse.ok().render("no-template-exists")
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRespondNotFoundIfNoTemplateFound() {
|
||||||
|
webClient.get().uri("/no-template-exists")
|
||||||
|
.accept(MediaType.TEXT_HTML)
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isNotFound()
|
||||||
|
.expectBody(String.class)
|
||||||
|
.value(Matchers.containsString("Template no-template-exists was not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue