mirror of https://github.com/halo-dev/halo
Upgrade spring boot version (#1289)
* Update gradle wrapper version * Update spring boot version to 2.5.0-M2 * Fix wrong const of temp_dir * Refactor error controller * Fix startup error due to theme not found * Refine error controller handler * Refine multipart resolver config * Fix ThemeRepositoryImplTest error * Fix freemarker not found error * chore: change jetty to undertow. * Remove useless throws Co-authored-by: Ryan Wang <i@ryanc.cc>pull/1298/head
parent
696a9ad2ee
commit
c22348e03a
11
build.gradle
11
build.gradle
|
@ -1,5 +1,5 @@
|
||||||
plugins {
|
plugins {
|
||||||
id "org.springframework.boot" version "2.4.2"
|
id "org.springframework.boot" version "2.5.0-M2"
|
||||||
id "io.spring.dependency-management" version "1.0.11.RELEASE"
|
id "io.spring.dependency-management" version "1.0.11.RELEASE"
|
||||||
id "checkstyle"
|
id "checkstyle"
|
||||||
id "java"
|
id "java"
|
||||||
|
@ -8,11 +8,7 @@ plugins {
|
||||||
group = "run.halo.app"
|
group = "run.halo.app"
|
||||||
version = "1.4.5"
|
version = "1.4.5"
|
||||||
description = "Halo, An excellent open source blog publishing application."
|
description = "Halo, An excellent open source blog publishing application."
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
java {
|
|
||||||
archivesBaseName = "halo"
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
|
|
||||||
checkstyle {
|
checkstyle {
|
||||||
toolVersion = "8.39"
|
toolVersion = "8.39"
|
||||||
|
@ -26,6 +22,7 @@ repositories {
|
||||||
// url "https://maven.aliyun.com/nexus/content/groups/public"
|
// url "https://maven.aliyun.com/nexus/content/groups/public"
|
||||||
// }
|
// }
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url 'https://repo.spring.io/milestone' }
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +108,7 @@ dependencies {
|
||||||
implementation "org.springframework.boot:spring-boot-starter-actuator"
|
implementation "org.springframework.boot:spring-boot-starter-actuator"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-web"
|
implementation "org.springframework.boot:spring-boot-starter-web"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-jetty"
|
implementation "org.springframework.boot:spring-boot-starter-undertow"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-freemarker"
|
implementation "org.springframework.boot:spring-boot-starter-freemarker"
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
@ -1 +1,8 @@
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
maven { url 'https://repo.spring.io/milestone' }
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rootProject.name = 'halo'
|
rootProject.name = 'halo'
|
||||||
|
|
|
@ -6,15 +6,11 @@ import static run.halo.app.utils.HaloUtils.ensureBoth;
|
||||||
import static run.halo.app.utils.HaloUtils.ensureSuffix;
|
import static run.halo.app.utils.HaloUtils.ensureSuffix;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import freemarker.core.TemplateClassResolver;
|
|
||||||
import freemarker.template.TemplateException;
|
|
||||||
import freemarker.template.TemplateExceptionHandler;
|
|
||||||
import freemarker.template.TemplateModel;
|
import freemarker.template.TemplateModel;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import javax.servlet.MultipartConfigElement;
|
import javax.servlet.MultipartConfigElement;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
@ -25,6 +21,7 @@ import org.apache.commons.fileupload.FileUploadBase;
|
||||||
import org.apache.commons.fileupload.servlet.ServletRequestContext;
|
import org.apache.commons.fileupload.servlet.ServletRequestContext;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration;
|
import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
|
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
|
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
|
||||||
|
@ -32,6 +29,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
import org.springframework.boot.jackson.JsonComponentModule;
|
import org.springframework.boot.jackson.JsonComponentModule;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.FileUrlResource;
|
||||||
import org.springframework.data.domain.PageImpl;
|
import org.springframework.data.domain.PageImpl;
|
||||||
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
|
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
|
||||||
import org.springframework.data.web.SortHandlerMethodArgumentResolver;
|
import org.springframework.data.web.SortHandlerMethodArgumentResolver;
|
||||||
|
@ -41,6 +39,7 @@ import org.springframework.http.converter.HttpMessageConverter;
|
||||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||||
import org.springframework.web.multipart.MultipartResolver;
|
import org.springframework.web.multipart.MultipartResolver;
|
||||||
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
|
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
|
||||||
|
@ -49,7 +48,6 @@ import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
|
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||||
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
|
|
||||||
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
|
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
|
||||||
import run.halo.app.config.properties.HaloProperties;
|
import run.halo.app.config.properties.HaloProperties;
|
||||||
import run.halo.app.core.PageJacksonSerializer;
|
import run.halo.app.core.PageJacksonSerializer;
|
||||||
|
@ -85,7 +83,7 @@ public class HaloMvcConfiguration implements WebMvcConfigurer {
|
||||||
this.haloProperties = haloProperties;
|
this.haloProperties = haloProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
// @Bean
|
||||||
public Map<String, TemplateModel> freemarkerLayoutDirectives() {
|
public Map<String, TemplateModel> freemarkerLayoutDirectives() {
|
||||||
Map<String, TemplateModel> freemarkerLayoutDirectives = new HashMap<>();
|
Map<String, TemplateModel> freemarkerLayoutDirectives = new HashMap<>();
|
||||||
freemarkerLayoutDirectives.put("extends", new ThemeExtendsDirective());
|
freemarkerLayoutDirectives.put("extends", new ThemeExtendsDirective());
|
||||||
|
@ -95,53 +93,16 @@ public class HaloMvcConfiguration implements WebMvcConfigurer {
|
||||||
return freemarkerLayoutDirectives;
|
return freemarkerLayoutDirectives;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuring freemarker template file path.
|
|
||||||
*
|
|
||||||
* @return new FreeMarkerConfigurer
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
FreeMarkerConfigurer freemarkerConfig(HaloProperties haloProperties)
|
|
||||||
throws IOException, TemplateException {
|
|
||||||
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
|
||||||
configurer
|
|
||||||
.setTemplateLoaderPaths(FILE_PROTOCOL + haloProperties.getWorkDir() + "templates/",
|
|
||||||
"classpath:/templates/");
|
|
||||||
configurer.setDefaultEncoding("UTF-8");
|
|
||||||
|
|
||||||
Properties properties = new Properties();
|
|
||||||
properties.setProperty("auto_import",
|
|
||||||
"/common/macro/common_macro.ftl as common,/common/macro/global_macro.ftl as global");
|
|
||||||
|
|
||||||
configurer.setFreemarkerSettings(properties);
|
|
||||||
|
|
||||||
// Predefine configuration
|
|
||||||
freemarker.template.Configuration configuration = configurer.createConfiguration();
|
|
||||||
|
|
||||||
configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
|
||||||
|
|
||||||
if (haloProperties.isProductionEnv()) {
|
|
||||||
configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
|
|
||||||
}
|
|
||||||
|
|
||||||
configuration.setSharedVariables(new HashMap<>() {{
|
|
||||||
put("layout", freemarkerLayoutDirectives());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set predefined freemarker configuration
|
|
||||||
configurer.setConfiguration(configuration);
|
|
||||||
|
|
||||||
return configurer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuring multipartResolver for large file upload..
|
* Configuring multipartResolver for large file upload..
|
||||||
*
|
*
|
||||||
* @return new multipartResolver
|
* @return new multipartResolver
|
||||||
*/
|
*/
|
||||||
@Bean(name = "multipartResolver")
|
@Bean(name = "multipartResolver")
|
||||||
MultipartResolver multipartResolver(MultipartProperties multipartProperties) {
|
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled",
|
||||||
|
havingValue = "true", matchIfMissing = true)
|
||||||
|
MultipartResolver multipartResolver(MultipartProperties multipartProperties)
|
||||||
|
throws IOException {
|
||||||
MultipartConfigElement multipartConfigElement = multipartProperties.createMultipartConfig();
|
MultipartConfigElement multipartConfigElement = multipartProperties.createMultipartConfig();
|
||||||
CommonsMultipartResolver resolver = new CommonsMultipartResolver() {
|
CommonsMultipartResolver resolver = new CommonsMultipartResolver() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -156,10 +117,15 @@ public class HaloMvcConfiguration implements WebMvcConfigurer {
|
||||||
resolver.setDefaultEncoding("UTF-8");
|
resolver.setDefaultEncoding("UTF-8");
|
||||||
resolver.setMaxUploadSize(multipartConfigElement.getMaxRequestSize());
|
resolver.setMaxUploadSize(multipartConfigElement.getMaxRequestSize());
|
||||||
resolver.setMaxUploadSizePerFile(multipartConfigElement.getMaxFileSize());
|
resolver.setMaxUploadSizePerFile(multipartConfigElement.getMaxFileSize());
|
||||||
|
var location = multipartProperties.getLocation();
|
||||||
|
if (StringUtils.hasText(location)) {
|
||||||
|
FileUrlResource resource = new FileUrlResource(location);
|
||||||
|
resolver.setUploadTempDir(resource);
|
||||||
|
}
|
||||||
|
|
||||||
//lazy multipart parsing, throwing parse exceptions once the application attempts to
|
//lazy multipart parsing, throwing parse exceptions once the application attempts to
|
||||||
// obtain multipart files
|
// obtain multipart files
|
||||||
resolver.setResolveLazily(true);
|
resolver.setResolveLazily(multipartProperties.isResolveLazily());
|
||||||
|
|
||||||
return resolver;
|
return resolver;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,6 @@ import static run.halo.app.model.support.HaloConst.USER_HOME;
|
||||||
import static run.halo.app.utils.HaloUtils.ensureSuffix;
|
import static run.halo.app.utils.HaloUtils.ensureSuffix;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import run.halo.app.model.enums.Mode;
|
import run.halo.app.model.enums.Mode;
|
||||||
|
|
|
@ -3,6 +3,7 @@ package run.halo.app.controller.content;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
|
@ -12,7 +13,8 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import run.halo.app.cache.AbstractStringCacheStore;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import run.halo.app.cache.lock.CacheLock;
|
import run.halo.app.cache.lock.CacheLock;
|
||||||
import run.halo.app.controller.content.model.CategoryModel;
|
import run.halo.app.controller.content.model.CategoryModel;
|
||||||
import run.halo.app.controller.content.model.JournalModel;
|
import run.halo.app.controller.content.model.JournalModel;
|
||||||
|
@ -66,8 +68,6 @@ public class ContentContentController {
|
||||||
|
|
||||||
private final SheetService sheetService;
|
private final SheetService sheetService;
|
||||||
|
|
||||||
private final AbstractStringCacheStore cacheStore;
|
|
||||||
|
|
||||||
private final AuthenticationService authenticationService;
|
private final AuthenticationService authenticationService;
|
||||||
|
|
||||||
private final CategoryService categoryService;
|
private final CategoryService categoryService;
|
||||||
|
@ -82,7 +82,6 @@ public class ContentContentController {
|
||||||
OptionService optionService,
|
OptionService optionService,
|
||||||
PostService postService,
|
PostService postService,
|
||||||
SheetService sheetService,
|
SheetService sheetService,
|
||||||
AbstractStringCacheStore cacheStore,
|
|
||||||
AuthenticationService authenticationService,
|
AuthenticationService authenticationService,
|
||||||
CategoryService categoryService) {
|
CategoryService categoryService) {
|
||||||
this.postModel = postModel;
|
this.postModel = postModel;
|
||||||
|
@ -95,7 +94,6 @@ public class ContentContentController {
|
||||||
this.optionService = optionService;
|
this.optionService = optionService;
|
||||||
this.postService = postService;
|
this.postService = postService;
|
||||||
this.sheetService = sheetService;
|
this.sheetService = sheetService;
|
||||||
this.cacheStore = cacheStore;
|
|
||||||
this.authenticationService = authenticationService;
|
this.authenticationService = authenticationService;
|
||||||
this.categoryService = categoryService;
|
this.categoryService = categoryService;
|
||||||
}
|
}
|
||||||
|
@ -126,12 +124,14 @@ public class ContentContentController {
|
||||||
Sheet sheet = sheetService.getBySlug(prefix);
|
Sheet sheet = sheetService.getBySlug(prefix);
|
||||||
return sheetModel.content(sheet, token, model);
|
return sheetModel.content(sheet, token, model);
|
||||||
}
|
}
|
||||||
throw new NotFoundException("Not Found");
|
|
||||||
|
throw buildPathNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("{prefix}/page/{page:\\d+}")
|
@GetMapping("{prefix}/page/{page:\\d+}")
|
||||||
public String content(@PathVariable("prefix") String prefix,
|
public String content(@PathVariable("prefix") String prefix,
|
||||||
@PathVariable(value = "page") Integer page,
|
@PathVariable(value = "page") Integer page,
|
||||||
|
HttpServletRequest request,
|
||||||
Model model) {
|
Model model) {
|
||||||
if (optionService.getArchivesPrefix().equals(prefix)) {
|
if (optionService.getArchivesPrefix().equals(prefix)) {
|
||||||
return postModel.archives(page, model);
|
return postModel.archives(page, model);
|
||||||
|
@ -145,7 +145,7 @@ public class ContentContentController {
|
||||||
return photoModel.list(page, model);
|
return photoModel.list(page, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotFoundException("Not Found");
|
throw buildPathNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("{prefix}/{slug}")
|
@GetMapping("{prefix}/{slug}")
|
||||||
|
@ -186,7 +186,7 @@ public class ContentContentController {
|
||||||
return sheetModel.content(sheet, token, model);
|
return sheetModel.content(sheet, token, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotFoundException("Not Found");
|
throw buildPathNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("{prefix}/{slug}/page/{page:\\d+}")
|
@GetMapping("{prefix}/{slug}/page/{page:\\d+}")
|
||||||
|
@ -202,7 +202,7 @@ public class ContentContentController {
|
||||||
return tagModel.listPost(model, slug, page);
|
return tagModel.listPost(model, slug, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotFoundException("Not Found");
|
throw buildPathNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("{year:\\d+}/{month:\\d+}/{slug}")
|
@GetMapping("{year:\\d+}/{month:\\d+}/{slug}")
|
||||||
|
@ -217,7 +217,7 @@ public class ContentContentController {
|
||||||
return postModel.content(post, token, model);
|
return postModel.content(post, token, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotFoundException("Not Found");
|
throw buildPathNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("{year:\\d+}/{month:\\d+}/{day:\\d+}/{slug}")
|
@GetMapping("{year:\\d+}/{month:\\d+}/{day:\\d+}/{slug}")
|
||||||
|
@ -233,7 +233,7 @@ public class ContentContentController {
|
||||||
return postModel.content(post, token, model);
|
return postModel.content(post, token, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotFoundException("Not Found");
|
throw buildPathNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "content/{type}/{slug:.*}/authentication")
|
@PostMapping(value = "content/{type}/{slug:.*}/authentication")
|
||||||
|
@ -254,6 +254,17 @@ public class ContentContentController {
|
||||||
return "redirect:" + redirectUrl;
|
return "redirect:" + redirectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private NotFoundException buildPathNotFoundException() {
|
||||||
|
var requestAttributes = RequestContextHolder.currentRequestAttributes();
|
||||||
|
|
||||||
|
var requestUri = "";
|
||||||
|
if (requestAttributes instanceof ServletRequestAttributes) {
|
||||||
|
requestUri =
|
||||||
|
((ServletRequestAttributes) requestAttributes).getRequest().getRequestURI();
|
||||||
|
}
|
||||||
|
return new NotFoundException("无法定位到该路径:" + requestUri);
|
||||||
|
}
|
||||||
|
|
||||||
private String doAuthenticationPost(
|
private String doAuthenticationPost(
|
||||||
String slug, String password) throws UnsupportedEncodingException {
|
String slug, String password) throws UnsupportedEncodingException {
|
||||||
Post post = postService.getBy(PostStatus.INTIMATE, slug);
|
Post post = postService.getBy(PostStatus.INTIMATE, slug);
|
||||||
|
|
|
@ -1,234 +0,0 @@
|
||||||
package run.halo.app.controller.core;
|
|
||||||
|
|
||||||
import static run.halo.app.model.support.HaloConst.DEFAULT_ERROR_PATH;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Map;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.boot.autoconfigure.web.ErrorProperties;
|
|
||||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
|
|
||||||
import org.springframework.boot.web.error.ErrorAttributeOptions;
|
|
||||||
import org.springframework.boot.web.servlet.error.ErrorAttributes;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.util.NestedServletException;
|
|
||||||
import run.halo.app.exception.AbstractHaloException;
|
|
||||||
import run.halo.app.exception.NotFoundException;
|
|
||||||
import run.halo.app.service.OptionService;
|
|
||||||
import run.halo.app.service.ThemeService;
|
|
||||||
import run.halo.app.utils.FilenameUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error page Controller
|
|
||||||
*
|
|
||||||
* @author ryanwang
|
|
||||||
* @date 2017-12-26
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Controller
|
|
||||||
@RequestMapping("${server.error.path:${error.path:/error}}")
|
|
||||||
public class CommonController extends AbstractErrorController {
|
|
||||||
|
|
||||||
private static final String NOT_FOUND_TEMPLATE = "404.ftl";
|
|
||||||
|
|
||||||
private static final String INTERNAL_ERROR_TEMPLATE = "500.ftl";
|
|
||||||
|
|
||||||
private static final String ERROR_TEMPLATE = "error.ftl";
|
|
||||||
|
|
||||||
private static final String COULD_NOT_RESOLVE_VIEW_WITH_NAME_PREFIX =
|
|
||||||
"Could not resolve view with name '";
|
|
||||||
|
|
||||||
private final ThemeService themeService;
|
|
||||||
|
|
||||||
private final ErrorProperties errorProperties;
|
|
||||||
|
|
||||||
private final OptionService optionService;
|
|
||||||
|
|
||||||
public CommonController(ThemeService themeService,
|
|
||||||
ErrorAttributes errorAttributes,
|
|
||||||
ServerProperties serverProperties,
|
|
||||||
OptionService optionService) {
|
|
||||||
super(errorAttributes);
|
|
||||||
this.themeService = themeService;
|
|
||||||
this.errorProperties = serverProperties.getError();
|
|
||||||
this.optionService = optionService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle error
|
|
||||||
*
|
|
||||||
* @param request request
|
|
||||||
* @return String
|
|
||||||
*/
|
|
||||||
@GetMapping
|
|
||||||
public String handleError(HttpServletRequest request, HttpServletResponse response,
|
|
||||||
Model model) {
|
|
||||||
handleCustomException(request);
|
|
||||||
|
|
||||||
ErrorAttributeOptions options = getErrorAttributeOptions(request);
|
|
||||||
|
|
||||||
Map<String, Object> errorDetail =
|
|
||||||
Collections.unmodifiableMap(getErrorAttributes(request, options));
|
|
||||||
model.addAttribute("error", errorDetail);
|
|
||||||
model.addAttribute("meta_keywords", optionService.getSeoKeywords());
|
|
||||||
model.addAttribute("meta_description", optionService.getSeoDescription());
|
|
||||||
log.debug("Error detail: [{}]", errorDetail);
|
|
||||||
|
|
||||||
HttpStatus status = getStatus(request);
|
|
||||||
|
|
||||||
response.setStatus(status.value());
|
|
||||||
if (status.equals(HttpStatus.INTERNAL_SERVER_ERROR)) {
|
|
||||||
return contentInternalError();
|
|
||||||
} else if (status.equals(HttpStatus.NOT_FOUND)) {
|
|
||||||
return contentNotFound();
|
|
||||||
} else {
|
|
||||||
return defaultErrorHandler();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render 404 error page
|
|
||||||
*
|
|
||||||
* @return String
|
|
||||||
*/
|
|
||||||
@GetMapping(value = "/404")
|
|
||||||
public String contentNotFound() {
|
|
||||||
if (themeService.templateExists(ERROR_TEMPLATE)) {
|
|
||||||
return getActualTemplatePath(ERROR_TEMPLATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (themeService.templateExists(NOT_FOUND_TEMPLATE)) {
|
|
||||||
return getActualTemplatePath(NOT_FOUND_TEMPLATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultErrorHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render 500 error page
|
|
||||||
*
|
|
||||||
* @return template path:
|
|
||||||
*/
|
|
||||||
@GetMapping(value = "/500")
|
|
||||||
public String contentInternalError() {
|
|
||||||
if (themeService.templateExists(ERROR_TEMPLATE)) {
|
|
||||||
return getActualTemplatePath(ERROR_TEMPLATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (themeService.templateExists(INTERNAL_ERROR_TEMPLATE)) {
|
|
||||||
return getActualTemplatePath(INTERNAL_ERROR_TEMPLATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultErrorHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String defaultErrorHandler() {
|
|
||||||
return DEFAULT_ERROR_PATH;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getActualTemplatePath(@NonNull String template) {
|
|
||||||
Assert.hasText(template, "FTL template must not be blank");
|
|
||||||
|
|
||||||
StringBuilder path = new StringBuilder();
|
|
||||||
path.append("themes/")
|
|
||||||
.append(themeService.getActivatedTheme().getFolderName())
|
|
||||||
.append('/')
|
|
||||||
.append(FilenameUtils.getBasename(template));
|
|
||||||
|
|
||||||
return path.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles custom exception, like HaloException.
|
|
||||||
*
|
|
||||||
* @param request http servlet request must not be null
|
|
||||||
*/
|
|
||||||
private void handleCustomException(@NonNull HttpServletRequest request) {
|
|
||||||
Assert.notNull(request, "Http servlet request must not be null");
|
|
||||||
|
|
||||||
Object throwableObject = request.getAttribute("javax.servlet.error.exception");
|
|
||||||
if (throwableObject == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Throwable throwable = (Throwable) throwableObject;
|
|
||||||
|
|
||||||
if (throwable instanceof NestedServletException) {
|
|
||||||
log.error("Captured an exception: [{}]", throwable.getMessage());
|
|
||||||
Throwable rootCause = ((NestedServletException) throwable).getRootCause();
|
|
||||||
if (rootCause instanceof AbstractHaloException) {
|
|
||||||
if (!(rootCause instanceof NotFoundException)) {
|
|
||||||
log.error("Caused by", rootCause);
|
|
||||||
}
|
|
||||||
AbstractHaloException haloException = (AbstractHaloException) rootCause;
|
|
||||||
request.setAttribute("javax.servlet.error.status_code",
|
|
||||||
haloException.getStatus().value());
|
|
||||||
request.setAttribute("javax.servlet.error.exception", rootCause);
|
|
||||||
request.setAttribute("javax.servlet.error.message", haloException.getMessage());
|
|
||||||
}
|
|
||||||
} else if (StringUtils.startsWithIgnoreCase(throwable.getMessage(),
|
|
||||||
COULD_NOT_RESOLVE_VIEW_WITH_NAME_PREFIX)) {
|
|
||||||
log.debug("Captured an exception", throwable);
|
|
||||||
request.setAttribute("javax.servlet.error.status_code", HttpStatus.NOT_FOUND.value());
|
|
||||||
|
|
||||||
NotFoundException viewNotFound = new NotFoundException("该路径没有对应的模板");
|
|
||||||
request.setAttribute("javax.servlet.error.exception", viewNotFound);
|
|
||||||
request.setAttribute("javax.servlet.error.message", viewNotFound.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the path of the error page.
|
|
||||||
*
|
|
||||||
* @return the error path
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public String getErrorPath() {
|
|
||||||
return this.errorProperties.getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the stacktrace attribute should be included.
|
|
||||||
*
|
|
||||||
* @param request the source request
|
|
||||||
* @return if the stacktrace attribute should be included
|
|
||||||
*/
|
|
||||||
private boolean isIncludeStackTrace(HttpServletRequest request) {
|
|
||||||
ErrorProperties.IncludeStacktrace include = errorProperties.getIncludeStacktrace();
|
|
||||||
if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) {
|
|
||||||
return getTraceParameter(request);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ErrorAttributeOptions .
|
|
||||||
*
|
|
||||||
* @param request the source request
|
|
||||||
* @return {@link ErrorAttributeOptions}
|
|
||||||
*/
|
|
||||||
private ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request) {
|
|
||||||
ErrorProperties.IncludeStacktrace include = errorProperties.getIncludeStacktrace();
|
|
||||||
if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {
|
|
||||||
return ErrorAttributeOptions.of(ErrorAttributeOptions.Include.STACK_TRACE);
|
|
||||||
}
|
|
||||||
if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM
|
|
||||||
&& getTraceParameter(request)) {
|
|
||||||
return ErrorAttributeOptions.of(ErrorAttributeOptions.Include.STACK_TRACE);
|
|
||||||
}
|
|
||||||
return ErrorAttributeOptions.defaults();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package run.halo.app.controller.error;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import javax.servlet.RequestDispatcher;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||||
|
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
|
||||||
|
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
|
||||||
|
import org.springframework.boot.web.servlet.error.ErrorAttributes;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.util.NestedServletException;
|
||||||
|
import run.halo.app.exception.AbstractHaloException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default error controller.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class DefaultErrorController extends BasicErrorController {
|
||||||
|
|
||||||
|
public DefaultErrorController(
|
||||||
|
ErrorAttributes errorAttributes,
|
||||||
|
ServerProperties serverProperties,
|
||||||
|
List<ErrorViewResolver> errorViewResolvers) {
|
||||||
|
super(errorAttributes, serverProperties.getError(), errorViewResolvers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HttpStatus getStatus(HttpServletRequest request) {
|
||||||
|
var status = super.getStatus(request);
|
||||||
|
// deduce status
|
||||||
|
var exception = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
|
||||||
|
if (exception instanceof NestedServletException) {
|
||||||
|
var nse = (NestedServletException) exception;
|
||||||
|
if (nse.getCause() instanceof AbstractHaloException) {
|
||||||
|
status = resolveHaloException((AbstractHaloException) nse.getCause(), request);
|
||||||
|
}
|
||||||
|
} else if (exception instanceof AbstractHaloException) {
|
||||||
|
status = resolveHaloException((AbstractHaloException) exception, request);
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpStatus resolveHaloException(AbstractHaloException haloException,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
HttpStatus status = haloException.getStatus();
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.error("Halo exception occurred.", haloException);
|
||||||
|
}
|
||||||
|
// reset status
|
||||||
|
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, status.value());
|
||||||
|
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, haloException);
|
||||||
|
request.setAttribute(RequestDispatcher.ERROR_MESSAGE, haloException.getMessage());
|
||||||
|
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE, haloException.getClass());
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package run.halo.app.controller.error;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
|
||||||
|
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.HttpStatus.Series;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.ModelAndView;
|
||||||
|
import run.halo.app.service.ThemeService;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class DefaultErrorViewResolver implements ErrorViewResolver {
|
||||||
|
|
||||||
|
private static final Map<Series, String> SERIES_VIEWS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
EnumMap<Series, String> views = new EnumMap<>(Series.class);
|
||||||
|
views.put(Series.CLIENT_ERROR, "4xx");
|
||||||
|
views.put(Series.SERVER_ERROR, "5xx");
|
||||||
|
SERIES_VIEWS = Collections.unmodifiableMap(views);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ThemeService themeService;
|
||||||
|
|
||||||
|
private final TemplateAvailabilityProviders templateAvailabilityProviders;
|
||||||
|
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
public DefaultErrorViewResolver(ThemeService themeService,
|
||||||
|
ApplicationContext applicationContext) {
|
||||||
|
this.themeService = themeService;
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
|
||||||
|
Map<String, Object> model) {
|
||||||
|
// for compatibility
|
||||||
|
var errorModel = new HashMap<>(model);
|
||||||
|
|
||||||
|
// resolve with status code. eg: 400.ftl
|
||||||
|
var modelAndView = resolve(String.valueOf(status.value()), errorModel);
|
||||||
|
|
||||||
|
// resolve with status series. eg: 4xx.ftl
|
||||||
|
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
|
||||||
|
modelAndView = resolve(SERIES_VIEWS.get(status.series()), errorModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve error template. eg: error.ftl
|
||||||
|
if (modelAndView == null) {
|
||||||
|
modelAndView = resolve("error", errorModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelAndView == null) {
|
||||||
|
// resolve common error template
|
||||||
|
modelAndView = new ModelAndView("common/error/error", errorModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelAndView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModelAndView resolve(String viewName, Map<String, Object> model) {
|
||||||
|
var errorViewName = this.themeService.render(viewName);
|
||||||
|
var provider =
|
||||||
|
this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
|
||||||
|
if (provider != null) {
|
||||||
|
return new ModelAndView(errorViewName, model);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import org.springframework.http.converter.json.AbstractJackson2HttpMessageConver
|
||||||
import org.springframework.http.converter.json.MappingJacksonValue;
|
import org.springframework.http.converter.json.MappingJacksonValue;
|
||||||
import org.springframework.http.server.ServerHttpRequest;
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
import org.springframework.http.server.ServerHttpResponse;
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
|
import org.springframework.http.server.ServletServerHttpResponse;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
@ -61,16 +62,28 @@ public class CommonResultControllerAdvice implements ResponseBodyAdvice<Object>
|
||||||
Object returnBody = bodyContainer.getValue();
|
Object returnBody = bodyContainer.getValue();
|
||||||
|
|
||||||
if (returnBody instanceof BaseResponse) {
|
if (returnBody instanceof BaseResponse) {
|
||||||
// If the return body is instance of BaseResponse
|
// If the return body is instance of BaseResponse, then just do nothing
|
||||||
BaseResponse<?> baseResponse = (BaseResponse<?>) returnBody;
|
BaseResponse<?> baseResponse = (BaseResponse<?>) returnBody;
|
||||||
response.setStatusCode(HttpStatus.resolve(baseResponse.getStatus()));
|
HttpStatus status = HttpStatus.resolve(baseResponse.getStatus());
|
||||||
|
if (status == null) {
|
||||||
|
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
}
|
||||||
|
response.setStatusCode(status);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap the return body
|
// get status
|
||||||
BaseResponse<?> baseResponse = BaseResponse.ok(returnBody);
|
var status = HttpStatus.OK;
|
||||||
|
if (response instanceof ServletServerHttpResponse) {
|
||||||
|
var servletResponse =
|
||||||
|
((ServletServerHttpResponse) response).getServletResponse();
|
||||||
|
status = HttpStatus.resolve(servletResponse.getStatus());
|
||||||
|
if (status == null) {
|
||||||
|
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var baseResponse = new BaseResponse<>(status.value(), status.getReasonPhrase(), returnBody);
|
||||||
bodyContainer.setValue(baseResponse);
|
bodyContainer.setValue(baseResponse);
|
||||||
response.setStatusCode(HttpStatus.valueOf(baseResponse.getStatus()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package run.halo.app.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme not found exception.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
* @date 2020-03-01
|
||||||
|
*/
|
||||||
|
public class ThemeNotFoundException extends BadRequestException {
|
||||||
|
|
||||||
|
public ThemeNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ThemeNotFoundException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.listener;
|
package run.halo.app.listener;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
|
@ -135,7 +136,7 @@ public class StartedListener implements ApplicationListener<ApplicationStartedEv
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init internal themes
|
* Init internal themes.
|
||||||
*/
|
*/
|
||||||
private void initThemes() {
|
private void initThemes() {
|
||||||
// Whether the blog has initialized
|
// Whether the blog has initialized
|
||||||
|
@ -175,6 +176,9 @@ public class StartedListener implements ApplicationListener<ApplicationStartedEv
|
||||||
log.debug("Skipped copying theme folder due to existence of theme folder");
|
log.debug("Skipped copying theme folder due to existence of theme folder");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
if (e instanceof FileNotFoundException) {
|
||||||
|
log.error("Please check location: classpath:{}", ThemeService.THEME_FOLDER);
|
||||||
|
}
|
||||||
log.error("Initialize internal theme to user path error!", e);
|
log.error("Initialize internal theme to user path error!", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,12 @@ package run.halo.app.listener.freemarker;
|
||||||
import static run.halo.app.model.support.HaloConst.OPTIONS_CACHE_KEY;
|
import static run.halo.app.model.support.HaloConst.OPTIONS_CACHE_KEY;
|
||||||
|
|
||||||
import freemarker.template.Configuration;
|
import freemarker.template.Configuration;
|
||||||
|
import freemarker.template.TemplateModel;
|
||||||
import freemarker.template.TemplateModelException;
|
import freemarker.template.TemplateModelException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import kr.pe.kwonnam.freemarker.inheritance.BlockDirective;
|
||||||
|
import kr.pe.kwonnam.freemarker.inheritance.PutDirective;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
|
@ -11,6 +16,7 @@ import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import run.halo.app.cache.AbstractStringCacheStore;
|
import run.halo.app.cache.AbstractStringCacheStore;
|
||||||
|
import run.halo.app.core.freemarker.inheritance.ThemeExtendsDirective;
|
||||||
import run.halo.app.event.options.OptionUpdatedEvent;
|
import run.halo.app.event.options.OptionUpdatedEvent;
|
||||||
import run.halo.app.event.theme.ThemeActivatedEvent;
|
import run.halo.app.event.theme.ThemeActivatedEvent;
|
||||||
import run.halo.app.event.theme.ThemeUpdatedEvent;
|
import run.halo.app.event.theme.ThemeUpdatedEvent;
|
||||||
|
@ -51,13 +57,27 @@ public class FreemarkerConfigAwareListener {
|
||||||
ThemeService themeService,
|
ThemeService themeService,
|
||||||
ThemeSettingService themeSettingService,
|
ThemeSettingService themeSettingService,
|
||||||
UserService userService,
|
UserService userService,
|
||||||
AbstractStringCacheStore cacheStore) {
|
AbstractStringCacheStore cacheStore) throws TemplateModelException {
|
||||||
this.optionService = optionService;
|
this.optionService = optionService;
|
||||||
this.configuration = configuration;
|
this.configuration = configuration;
|
||||||
this.themeService = themeService;
|
this.themeService = themeService;
|
||||||
this.themeSettingService = themeSettingService;
|
this.themeSettingService = themeSettingService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.cacheStore = cacheStore;
|
this.cacheStore = cacheStore;
|
||||||
|
|
||||||
|
this.initFreemarkerConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, TemplateModel> freemarkerLayoutDirectives() {
|
||||||
|
Map<String, TemplateModel> freemarkerLayoutDirectives = new HashMap<>();
|
||||||
|
freemarkerLayoutDirectives.put("extends", new ThemeExtendsDirective());
|
||||||
|
freemarkerLayoutDirectives.put("block", new BlockDirective());
|
||||||
|
freemarkerLayoutDirectives.put("put", new PutDirective());
|
||||||
|
return freemarkerLayoutDirectives;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initFreemarkerConfig() throws TemplateModelException {
|
||||||
|
configuration.setSharedVariable("layout", freemarkerLayoutDirectives());
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventListener
|
@EventListener
|
||||||
|
@ -72,15 +92,14 @@ public class FreemarkerConfigAwareListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventListener
|
@EventListener
|
||||||
public void onThemeActivatedEvent(ThemeActivatedEvent themeActivatedEvent)
|
public void onThemeActivatedEvent(ThemeActivatedEvent themeActivatedEvent) {
|
||||||
throws TemplateModelException {
|
|
||||||
log.debug("Received theme activated event");
|
log.debug("Received theme activated event");
|
||||||
|
|
||||||
loadThemeConfig();
|
loadThemeConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventListener
|
@EventListener
|
||||||
public void onThemeUpdatedEvent(ThemeUpdatedEvent event) throws TemplateModelException {
|
public void onThemeUpdatedEvent(ThemeUpdatedEvent event) {
|
||||||
log.debug("Received theme updated event");
|
log.debug("Received theme updated event");
|
||||||
|
|
||||||
loadThemeConfig();
|
loadThemeConfig();
|
||||||
|
|
|
@ -5,9 +5,7 @@ import java.util.Optional;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
* Halo constants.
|
||||||
* 公共常量
|
|
||||||
* </pre>
|
|
||||||
*
|
*
|
||||||
* @author ryanwang
|
* @author ryanwang
|
||||||
* @date 2017/12/29
|
* @date 2017/12/29
|
||||||
|
@ -17,12 +15,12 @@ public class HaloConst {
|
||||||
/**
|
/**
|
||||||
* User home directory.
|
* User home directory.
|
||||||
*/
|
*/
|
||||||
public static final String USER_HOME = System.getProperties().getProperty("user.home");
|
public static final String USER_HOME = System.getProperty("user.home");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary directory.
|
* Temporary directory.
|
||||||
*/
|
*/
|
||||||
public static final String TEMP_DIR = "/tmp/run.halo.app";
|
public static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
|
||||||
|
|
||||||
public static final String PROTOCOL_HTTPS = "https://";
|
public static final String PROTOCOL_HTTPS = "https://";
|
||||||
|
|
||||||
|
@ -65,7 +63,7 @@ public class HaloConst {
|
||||||
*/
|
*/
|
||||||
public static final String POST_PASSWORD_TEMPLATE = "post_password";
|
public static final String POST_PASSWORD_TEMPLATE = "post_password";
|
||||||
/**
|
/**
|
||||||
* Suffix of freemarker template file
|
* Suffix of freemarker template file.
|
||||||
*/
|
*/
|
||||||
public static final String SUFFIX_FTL = ".ftl";
|
public static final String SUFFIX_FTL = ".ftl";
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,6 +13,7 @@ import java.nio.file.Paths;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
@ -25,6 +26,7 @@ import run.halo.app.event.options.OptionUpdatedEvent;
|
||||||
import run.halo.app.exception.AlreadyExistsException;
|
import run.halo.app.exception.AlreadyExistsException;
|
||||||
import run.halo.app.exception.NotFoundException;
|
import run.halo.app.exception.NotFoundException;
|
||||||
import run.halo.app.exception.ServiceException;
|
import run.halo.app.exception.ServiceException;
|
||||||
|
import run.halo.app.exception.ThemeNotFoundException;
|
||||||
import run.halo.app.exception.ThemeNotSupportException;
|
import run.halo.app.exception.ThemeNotSupportException;
|
||||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||||
import run.halo.app.model.entity.Option;
|
import run.halo.app.model.entity.Option;
|
||||||
|
@ -67,16 +69,30 @@ public class ThemeRepositoryImpl
|
||||||
public ThemeProperty getActivatedThemeProperty() {
|
public ThemeProperty getActivatedThemeProperty() {
|
||||||
ThemeProperty themeProperty = this.currentTheme;
|
ThemeProperty themeProperty = this.currentTheme;
|
||||||
if (themeProperty == null) {
|
if (themeProperty == null) {
|
||||||
|
AtomicBoolean fallbackTheme = new AtomicBoolean(false);
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
if (this.currentTheme == null) {
|
if (this.currentTheme == null) {
|
||||||
// get current theme id
|
// get current theme id
|
||||||
String currentThemeId = this.optionRepository.findByKey(THEME.getValue())
|
String currentThemeId = this.optionRepository.findByKey(THEME.getValue())
|
||||||
.map(Option::getValue)
|
.map(Option::getValue)
|
||||||
.orElse(DEFAULT_THEME_ID);
|
.orElse(DEFAULT_THEME_ID);
|
||||||
|
|
||||||
// fetch current theme
|
// fetch current theme
|
||||||
this.currentTheme = this.getThemeByThemeId(currentThemeId);
|
this.currentTheme =
|
||||||
|
this.fetchThemeByThemeId(currentThemeId).orElseGet(() -> {
|
||||||
|
if (!StringUtils.equalsIgnoreCase(currentThemeId, DEFAULT_THEME_ID)) {
|
||||||
|
fallbackTheme.set(true);
|
||||||
|
return this.getThemeByThemeId(DEFAULT_THEME_ID);
|
||||||
|
}
|
||||||
|
throw new ThemeNotFoundException(
|
||||||
|
"Default theme: " + DEFAULT_THEME_ID + " was not found!");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (fallbackTheme.get()) {
|
||||||
|
// need set default theme as fallback theme
|
||||||
|
setActivatedTheme(DEFAULT_THEME_ID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this.currentTheme;
|
return this.currentTheme;
|
||||||
}
|
}
|
||||||
|
@ -188,16 +204,22 @@ public class ThemeRepositoryImpl
|
||||||
@Override
|
@Override
|
||||||
public void onApplicationEvent(OptionUpdatedEvent event) {
|
public void onApplicationEvent(OptionUpdatedEvent event) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
|
// reset current theme with null
|
||||||
this.currentTheme = null;
|
this.currentTheme = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
protected ThemeProperty getThemeByThemeId(String themeId) {
|
protected ThemeProperty getThemeByThemeId(String themeId) {
|
||||||
|
return fetchThemeByThemeId(themeId).orElseThrow(
|
||||||
|
() -> new ThemeNotFoundException("Failed to find theme with id: " + themeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
protected Optional<ThemeProperty> fetchThemeByThemeId(String themeId) {
|
||||||
return ThemePropertyScanner.INSTANCE.scan(getThemeRootPath(), null)
|
return ThemePropertyScanner.INSTANCE.scan(getThemeRootPath(), null)
|
||||||
.stream()
|
.stream()
|
||||||
.filter(property -> Objects.equals(themeId, property.getId()))
|
.filter(property -> Objects.equals(themeId, property.getId()))
|
||||||
.findFirst()
|
.findFirst();
|
||||||
.orElseThrow();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,15 +72,12 @@ public class OptionServiceImpl extends AbstractCrudService<Option, Integer>
|
||||||
private final AbstractStringCacheStore cacheStore;
|
private final AbstractStringCacheStore cacheStore;
|
||||||
private final Map<String, PropertyEnum> propertyEnumMap;
|
private final Map<String, PropertyEnum> propertyEnumMap;
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
private final HaloProperties haloProperties;
|
|
||||||
|
|
||||||
public OptionServiceImpl(HaloProperties haloProperties,
|
public OptionServiceImpl(OptionRepository optionRepository,
|
||||||
OptionRepository optionRepository,
|
|
||||||
ApplicationContext applicationContext,
|
ApplicationContext applicationContext,
|
||||||
AbstractStringCacheStore cacheStore,
|
AbstractStringCacheStore cacheStore,
|
||||||
ApplicationEventPublisher eventPublisher) {
|
ApplicationEventPublisher eventPublisher) {
|
||||||
super(optionRepository);
|
super(optionRepository);
|
||||||
this.haloProperties = haloProperties;
|
|
||||||
this.optionRepository = optionRepository;
|
this.optionRepository = optionRepository;
|
||||||
this.applicationContext = applicationContext;
|
this.applicationContext = applicationContext;
|
||||||
this.cacheStore = cacheStore;
|
this.cacheStore = cacheStore;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package run.halo.app.service.impl;
|
package run.halo.app.service.impl;
|
||||||
|
|
||||||
import static run.halo.app.model.support.HaloConst.DEFAULT_ERROR_PATH;
|
|
||||||
import static run.halo.app.utils.FileUtils.copyFolder;
|
import static run.halo.app.utils.FileUtils.copyFolder;
|
||||||
import static run.halo.app.utils.FileUtils.deleteFolderQuietly;
|
import static run.halo.app.utils.FileUtils.deleteFolderQuietly;
|
||||||
import static run.halo.app.utils.VersionUtil.compareVersion;
|
import static run.halo.app.utils.VersionUtil.compareVersion;
|
||||||
|
@ -314,18 +313,14 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String render(String pageName) {
|
public String render(String pageName) {
|
||||||
return fetchActivatedTheme()
|
var folderName = getActivatedTheme().getFolderName();
|
||||||
.map(themeProperty ->
|
return "themes/" + folderName + "/" + pageName;
|
||||||
String.format(RENDER_TEMPLATE, themeProperty.getFolderName(), pageName))
|
|
||||||
.orElse(DEFAULT_ERROR_PATH);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String renderWithSuffix(String pageName) {
|
public String renderWithSuffix(String pageName) {
|
||||||
// Get activated theme
|
var folderName = getActivatedTheme().getFolderName();
|
||||||
ThemeProperty activatedTheme = getActivatedTheme();
|
return "themes/" + folderName + "/" + pageName + ".ftl";
|
||||||
// Build render url
|
|
||||||
return String.format(RENDER_TEMPLATE_SUFFIX, activatedTheme.getFolderName(), pageName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -337,13 +332,13 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
@Override
|
@Override
|
||||||
@NonNull
|
@NonNull
|
||||||
public ThemeProperty getActivatedTheme() {
|
public ThemeProperty getActivatedTheme() {
|
||||||
return themeRepository.getActivatedThemeProperty();
|
return fetchActivatedTheme().orElseThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@NonNull
|
@NonNull
|
||||||
public Optional<ThemeProperty> fetchActivatedTheme() {
|
public Optional<ThemeProperty> fetchActivatedTheme() {
|
||||||
return fetchThemePropertyBy(getActivatedThemeId());
|
return Optional.of(themeRepository.getActivatedThemeProperty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
server:
|
server:
|
||||||
port: 8090
|
port: 8090
|
||||||
forward-headers-strategy: native
|
forward-headers-strategy: native
|
||||||
|
error:
|
||||||
|
include-message: always
|
||||||
compression:
|
compression:
|
||||||
enabled: false
|
enabled: false
|
||||||
spring:
|
spring:
|
||||||
|
@ -40,7 +42,14 @@ spring:
|
||||||
multipart:
|
multipart:
|
||||||
max-file-size: 10240MB
|
max-file-size: 10240MB
|
||||||
max-request-size: 10240MB
|
max-request-size: 10240MB
|
||||||
location: /tmp/run.halo.app
|
resolve-lazily: true
|
||||||
|
freemarker:
|
||||||
|
suffix: .ftl
|
||||||
|
settings:
|
||||||
|
auto_import: /common/macro/global_macro.ftl as global
|
||||||
|
template-loader-path:
|
||||||
|
- file:///${halo.work-dir}templates/
|
||||||
|
- classpath:/templates/
|
||||||
management:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
|
@ -61,3 +70,4 @@ springfox:
|
||||||
halo:
|
halo:
|
||||||
download-timeout: 5m
|
download-timeout: 5m
|
||||||
cache: memory
|
cache: memory
|
||||||
|
work-dir: ${user.home}/.halo/
|
|
@ -1 +0,0 @@
|
||||||
404 Not Found
|
|
|
@ -1 +0,0 @@
|
||||||
500 Internal Error
|
|
|
@ -5,7 +5,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<link rel="alternate" type="application/rss+xml" title="atom 1.0" href="${atom_url!}">
|
<link rel="alternate" type="application/rss+xml" title="atom 1.0" href="${atom_url!}">
|
||||||
<title>${(error.status)!500} | ${(error.error)!'未知错误'}</title>
|
<title>${(status)!500} | ${(error)!'未知错误'}</title>
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
body {
|
body {
|
||||||
|
@ -121,9 +121,9 @@
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>${(error.status)!500}</h2>
|
<h2>${(status)!500}</h2>
|
||||||
<h1 class="title">${(error.error)!'未知错误'}.</h1>
|
<h1 class="title">${(error)!'未知错误'}.</h1>
|
||||||
<p>${(error.message)!'未知错误!可能存在的原因:未正确设置主题或主题文件缺失。'}</p>
|
<p>${(message)!'未知错误!可能存在的原因:未正确设置主题或主题文件缺失。'}</p>
|
||||||
<div class="back-home">
|
<div class="back-home">
|
||||||
<button onclick="window.location.href='${blog_url!}'">首页</button>
|
<button onclick="window.location.href='${blog_url!}'">首页</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -59,14 +59,14 @@ class ThemeRepositoryImplTest {
|
||||||
expectedTheme.setActivated(true);
|
expectedTheme.setActivated(true);
|
||||||
|
|
||||||
given(optionRepository.findByKey(THEME.getValue())).willReturn(Optional.empty());
|
given(optionRepository.findByKey(THEME.getValue())).willReturn(Optional.empty());
|
||||||
doReturn(expectedTheme).when(themeRepository)
|
doReturn(Optional.of(expectedTheme)).when(themeRepository)
|
||||||
.getThemeByThemeId(HaloConst.DEFAULT_THEME_ID);
|
.fetchThemeByThemeId(HaloConst.DEFAULT_THEME_ID);
|
||||||
|
|
||||||
ThemeProperty resultTheme = themeRepository.getActivatedThemeProperty();
|
ThemeProperty resultTheme = themeRepository.getActivatedThemeProperty();
|
||||||
assertEquals(expectedTheme, resultTheme);
|
assertEquals(expectedTheme, resultTheme);
|
||||||
|
|
||||||
verify(optionRepository, times(1)).findByKey(any());
|
verify(optionRepository, times(1)).findByKey(any());
|
||||||
verify(themeRepository, times(1)).getThemeByThemeId(any());
|
verify(themeRepository, times(1)).fetchThemeByThemeId(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -76,8 +76,8 @@ class ThemeRepositoryImplTest {
|
||||||
expectedTheme.setActivated(true);
|
expectedTheme.setActivated(true);
|
||||||
|
|
||||||
given(optionRepository.findByKey(THEME.getValue())).willReturn(Optional.empty());
|
given(optionRepository.findByKey(THEME.getValue())).willReturn(Optional.empty());
|
||||||
doReturn(expectedTheme).when(themeRepository)
|
doReturn(Optional.of(expectedTheme)).when(themeRepository)
|
||||||
.getThemeByThemeId(HaloConst.DEFAULT_THEME_ID);
|
.fetchThemeByThemeId(HaloConst.DEFAULT_THEME_ID);
|
||||||
|
|
||||||
ExecutorService executorService = Executors.newFixedThreadPool(10);
|
ExecutorService executorService = Executors.newFixedThreadPool(10);
|
||||||
// define tasks
|
// define tasks
|
||||||
|
@ -96,7 +96,7 @@ class ThemeRepositoryImplTest {
|
||||||
});
|
});
|
||||||
|
|
||||||
verify(optionRepository, times(1)).findByKey(any());
|
verify(optionRepository, times(1)).findByKey(any());
|
||||||
verify(themeRepository, times(1)).getThemeByThemeId(any());
|
verify(themeRepository, times(1)).fetchThemeByThemeId(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -133,6 +133,7 @@ class GitTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Disabled("Due to time-consumption fetching")
|
||||||
void getBranchesFromRemote() throws GitAPIException {
|
void getBranchesFromRemote() throws GitAPIException {
|
||||||
Map<String, Ref> refMap = Git.lsRemoteRepository()
|
Map<String, Ref> refMap = Git.lsRemoteRepository()
|
||||||
.setRemote("https://github.com/halo-dev/halo.git")
|
.setRemote("https://github.com/halo-dev/halo.git")
|
||||||
|
|
Loading…
Reference in New Issue