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
John Niang 2021-03-06 00:58:12 +08:00 committed by GitHub
parent 696a9ad2ee
commit c22348e03a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 311 additions and 351 deletions

View File

@ -1,5 +1,5 @@
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 "checkstyle"
id "java"
@ -8,11 +8,7 @@ plugins {
group = "run.halo.app"
version = "1.4.5"
description = "Halo, An excellent open source blog publishing application."
java {
archivesBaseName = "halo"
sourceCompatibility = JavaVersion.VERSION_11
}
sourceCompatibility = JavaVersion.VERSION_11
checkstyle {
toolVersion = "8.39"
@ -26,6 +22,7 @@ repositories {
// url "https://maven.aliyun.com/nexus/content/groups/public"
// }
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
jcenter()
}
@ -111,7 +108,7 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-actuator"
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
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-validation'

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists

View File

@ -1 +1,8 @@
pluginManagement {
repositories {
maven { url 'https://repo.spring.io/milestone' }
gradlePluginPortal()
}
}
rootProject.name = 'halo'

View File

@ -6,15 +6,11 @@ import static run.halo.app.utils.HaloUtils.ensureBoth;
import static run.halo.app.utils.HaloUtils.ensureSuffix;
import com.fasterxml.jackson.databind.ObjectMapper;
import freemarker.core.TemplateClassResolver;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.TemplateModel;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import javax.servlet.MultipartConfigElement;
import javax.servlet.http.HttpServletRequest;
@ -25,6 +21,7 @@ import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.servlet.ServletRequestContext;
import org.springframework.beans.factory.annotation.Value;
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.MultipartProperties;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileUrlResource;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
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.MappingJackson2HttpMessageConverter;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.multipart.MultipartResolver;
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.WebMvcConfigurer;
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 run.halo.app.config.properties.HaloProperties;
import run.halo.app.core.PageJacksonSerializer;
@ -85,7 +83,7 @@ public class HaloMvcConfiguration implements WebMvcConfigurer {
this.haloProperties = haloProperties;
}
@Bean
// @Bean
public Map<String, TemplateModel> freemarkerLayoutDirectives() {
Map<String, TemplateModel> freemarkerLayoutDirectives = new HashMap<>();
freemarkerLayoutDirectives.put("extends", new ThemeExtendsDirective());
@ -95,53 +93,16 @@ public class HaloMvcConfiguration implements WebMvcConfigurer {
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..
*
* @return new 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();
CommonsMultipartResolver resolver = new CommonsMultipartResolver() {
@Override
@ -156,10 +117,15 @@ public class HaloMvcConfiguration implements WebMvcConfigurer {
resolver.setDefaultEncoding("UTF-8");
resolver.setMaxUploadSize(multipartConfigElement.getMaxRequestSize());
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
// obtain multipart files
resolver.setResolveLazily(true);
resolver.setResolveLazily(multipartProperties.isResolveLazily());
return resolver;
}

View File

@ -6,8 +6,6 @@ import static run.halo.app.model.support.HaloConst.USER_HOME;
import static run.halo.app.utils.HaloUtils.ensureSuffix;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import run.halo.app.model.enums.Mode;

View File

@ -3,6 +3,7 @@ package run.halo.app.controller.content;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.RequestMapping;
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.controller.content.model.CategoryModel;
import run.halo.app.controller.content.model.JournalModel;
@ -66,8 +68,6 @@ public class ContentContentController {
private final SheetService sheetService;
private final AbstractStringCacheStore cacheStore;
private final AuthenticationService authenticationService;
private final CategoryService categoryService;
@ -82,7 +82,6 @@ public class ContentContentController {
OptionService optionService,
PostService postService,
SheetService sheetService,
AbstractStringCacheStore cacheStore,
AuthenticationService authenticationService,
CategoryService categoryService) {
this.postModel = postModel;
@ -95,7 +94,6 @@ public class ContentContentController {
this.optionService = optionService;
this.postService = postService;
this.sheetService = sheetService;
this.cacheStore = cacheStore;
this.authenticationService = authenticationService;
this.categoryService = categoryService;
}
@ -126,12 +124,14 @@ public class ContentContentController {
Sheet sheet = sheetService.getBySlug(prefix);
return sheetModel.content(sheet, token, model);
}
throw new NotFoundException("Not Found");
throw buildPathNotFoundException();
}
@GetMapping("{prefix}/page/{page:\\d+}")
public String content(@PathVariable("prefix") String prefix,
@PathVariable(value = "page") Integer page,
HttpServletRequest request,
Model model) {
if (optionService.getArchivesPrefix().equals(prefix)) {
return postModel.archives(page, model);
@ -145,7 +145,7 @@ public class ContentContentController {
return photoModel.list(page, model);
}
throw new NotFoundException("Not Found");
throw buildPathNotFoundException();
}
@GetMapping("{prefix}/{slug}")
@ -186,7 +186,7 @@ public class ContentContentController {
return sheetModel.content(sheet, token, model);
}
throw new NotFoundException("Not Found");
throw buildPathNotFoundException();
}
@GetMapping("{prefix}/{slug}/page/{page:\\d+}")
@ -202,7 +202,7 @@ public class ContentContentController {
return tagModel.listPost(model, slug, page);
}
throw new NotFoundException("Not Found");
throw buildPathNotFoundException();
}
@GetMapping("{year:\\d+}/{month:\\d+}/{slug}")
@ -217,7 +217,7 @@ public class ContentContentController {
return postModel.content(post, token, model);
}
throw new NotFoundException("Not Found");
throw buildPathNotFoundException();
}
@GetMapping("{year:\\d+}/{month:\\d+}/{day:\\d+}/{slug}")
@ -233,7 +233,7 @@ public class ContentContentController {
return postModel.content(post, token, model);
}
throw new NotFoundException("Not Found");
throw buildPathNotFoundException();
}
@PostMapping(value = "content/{type}/{slug:.*}/authentication")
@ -254,6 +254,17 @@ public class ContentContentController {
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(
String slug, String password) throws UnsupportedEncodingException {
Post post = postService.getBy(PostStatus.INTIMATE, slug);

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -9,6 +9,7 @@ import org.springframework.http.converter.json.AbstractJackson2HttpMessageConver
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.ControllerAdvice;
@ -61,16 +62,28 @@ public class CommonResultControllerAdvice implements ResponseBodyAdvice<Object>
Object returnBody = bodyContainer.getValue();
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;
response.setStatusCode(HttpStatus.resolve(baseResponse.getStatus()));
HttpStatus status = HttpStatus.resolve(baseResponse.getStatus());
if (status == null) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
}
response.setStatusCode(status);
return;
}
// Wrap the return body
BaseResponse<?> baseResponse = BaseResponse.ok(returnBody);
// get status
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);
response.setStatusCode(HttpStatus.valueOf(baseResponse.getStatus()));
}
}

View File

@ -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);
}
}

View File

@ -1,5 +1,6 @@
package run.halo.app.listener;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
@ -135,7 +136,7 @@ public class StartedListener implements ApplicationListener<ApplicationStartedEv
}
/**
* Init internal themes
* Init internal themes.
*/
private void initThemes() {
// 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");
}
} 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);
}
}

View File

@ -3,7 +3,12 @@ package run.halo.app.listener.freemarker;
import static run.halo.app.model.support.HaloConst.OPTIONS_CACHE_KEY;
import freemarker.template.Configuration;
import freemarker.template.TemplateModel;
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 org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
@ -11,6 +16,7 @@ import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
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.theme.ThemeActivatedEvent;
import run.halo.app.event.theme.ThemeUpdatedEvent;
@ -51,13 +57,27 @@ public class FreemarkerConfigAwareListener {
ThemeService themeService,
ThemeSettingService themeSettingService,
UserService userService,
AbstractStringCacheStore cacheStore) {
AbstractStringCacheStore cacheStore) throws TemplateModelException {
this.optionService = optionService;
this.configuration = configuration;
this.themeService = themeService;
this.themeSettingService = themeSettingService;
this.userService = userService;
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
@ -72,15 +92,14 @@ public class FreemarkerConfigAwareListener {
}
@EventListener
public void onThemeActivatedEvent(ThemeActivatedEvent themeActivatedEvent)
throws TemplateModelException {
public void onThemeActivatedEvent(ThemeActivatedEvent themeActivatedEvent) {
log.debug("Received theme activated event");
loadThemeConfig();
}
@EventListener
public void onThemeUpdatedEvent(ThemeUpdatedEvent event) throws TemplateModelException {
public void onThemeUpdatedEvent(ThemeUpdatedEvent event) {
log.debug("Received theme updated event");
loadThemeConfig();

View File

@ -5,9 +5,7 @@ import java.util.Optional;
import org.springframework.http.HttpHeaders;
/**
* <pre>
*
* </pre>
* Halo constants.
*
* @author ryanwang
* @date 2017/12/29
@ -17,12 +15,12 @@ public class HaloConst {
/**
* 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.
*/
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://";
@ -65,7 +63,7 @@ public class HaloConst {
*/
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";
/**

View File

@ -13,6 +13,7 @@ import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.NotFoundException;
import run.halo.app.exception.ServiceException;
import run.halo.app.exception.ThemeNotFoundException;
import run.halo.app.exception.ThemeNotSupportException;
import run.halo.app.handler.theme.config.support.ThemeProperty;
import run.halo.app.model.entity.Option;
@ -67,16 +69,30 @@ public class ThemeRepositoryImpl
public ThemeProperty getActivatedThemeProperty() {
ThemeProperty themeProperty = this.currentTheme;
if (themeProperty == null) {
AtomicBoolean fallbackTheme = new AtomicBoolean(false);
synchronized (this) {
if (this.currentTheme == null) {
// get current theme id
String currentThemeId = this.optionRepository.findByKey(THEME.getValue())
.map(Option::getValue)
.orElse(DEFAULT_THEME_ID);
// 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;
}
@ -188,16 +204,22 @@ public class ThemeRepositoryImpl
@Override
public void onApplicationEvent(OptionUpdatedEvent event) {
synchronized (this) {
// reset current theme with null
this.currentTheme = null;
}
}
@NonNull
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)
.stream()
.filter(property -> Objects.equals(themeId, property.getId()))
.findFirst()
.orElseThrow();
.findFirst();
}
}

View File

@ -72,15 +72,12 @@ public class OptionServiceImpl extends AbstractCrudService<Option, Integer>
private final AbstractStringCacheStore cacheStore;
private final Map<String, PropertyEnum> propertyEnumMap;
private final ApplicationEventPublisher eventPublisher;
private final HaloProperties haloProperties;
public OptionServiceImpl(HaloProperties haloProperties,
OptionRepository optionRepository,
public OptionServiceImpl(OptionRepository optionRepository,
ApplicationContext applicationContext,
AbstractStringCacheStore cacheStore,
ApplicationEventPublisher eventPublisher) {
super(optionRepository);
this.haloProperties = haloProperties;
this.optionRepository = optionRepository;
this.applicationContext = applicationContext;
this.cacheStore = cacheStore;

View File

@ -1,6 +1,5 @@
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.deleteFolderQuietly;
import static run.halo.app.utils.VersionUtil.compareVersion;
@ -314,18 +313,14 @@ public class ThemeServiceImpl implements ThemeService {
@Override
public String render(String pageName) {
return fetchActivatedTheme()
.map(themeProperty ->
String.format(RENDER_TEMPLATE, themeProperty.getFolderName(), pageName))
.orElse(DEFAULT_ERROR_PATH);
var folderName = getActivatedTheme().getFolderName();
return "themes/" + folderName + "/" + pageName;
}
@Override
public String renderWithSuffix(String pageName) {
// Get activated theme
ThemeProperty activatedTheme = getActivatedTheme();
// Build render url
return String.format(RENDER_TEMPLATE_SUFFIX, activatedTheme.getFolderName(), pageName);
var folderName = getActivatedTheme().getFolderName();
return "themes/" + folderName + "/" + pageName + ".ftl";
}
@Override
@ -337,13 +332,13 @@ public class ThemeServiceImpl implements ThemeService {
@Override
@NonNull
public ThemeProperty getActivatedTheme() {
return themeRepository.getActivatedThemeProperty();
return fetchActivatedTheme().orElseThrow();
}
@Override
@NonNull
public Optional<ThemeProperty> fetchActivatedTheme() {
return fetchThemePropertyBy(getActivatedThemeId());
return Optional.of(themeRepository.getActivatedThemeProperty());
}
@Override

View File

@ -1,6 +1,8 @@
server:
port: 8090
forward-headers-strategy: native
error:
include-message: always
compression:
enabled: false
spring:
@ -40,7 +42,14 @@ spring:
multipart:
max-file-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:
endpoints:
web:
@ -60,4 +69,5 @@ springfox:
halo:
download-timeout: 5m
cache: memory
cache: memory
work-dir: ${user.home}/.halo/

View File

@ -1 +0,0 @@
404 Not Found

View File

@ -1 +0,0 @@
500 Internal Error

View File

@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<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">
body {
@ -121,9 +121,9 @@
<body>
<div class="container">
<h2>${(error.status)!500}</h2>
<h1 class="title">${(error.error)!'未知错误'}.</h1>
<p>${(error.message)!'未知错误!可能存在的原因:未正确设置主题或主题文件缺失。'}</p>
<h2>${(status)!500}</h2>
<h1 class="title">${(error)!'未知错误'}.</h1>
<p>${(message)!'未知错误!可能存在的原因:未正确设置主题或主题文件缺失。'}</p>
<div class="back-home">
<button onclick="window.location.href='${blog_url!}'">首页</button>
</div>

View File

@ -59,14 +59,14 @@ class ThemeRepositoryImplTest {
expectedTheme.setActivated(true);
given(optionRepository.findByKey(THEME.getValue())).willReturn(Optional.empty());
doReturn(expectedTheme).when(themeRepository)
.getThemeByThemeId(HaloConst.DEFAULT_THEME_ID);
doReturn(Optional.of(expectedTheme)).when(themeRepository)
.fetchThemeByThemeId(HaloConst.DEFAULT_THEME_ID);
ThemeProperty resultTheme = themeRepository.getActivatedThemeProperty();
assertEquals(expectedTheme, resultTheme);
verify(optionRepository, times(1)).findByKey(any());
verify(themeRepository, times(1)).getThemeByThemeId(any());
verify(themeRepository, times(1)).fetchThemeByThemeId(any());
}
@Test
@ -76,8 +76,8 @@ class ThemeRepositoryImplTest {
expectedTheme.setActivated(true);
given(optionRepository.findByKey(THEME.getValue())).willReturn(Optional.empty());
doReturn(expectedTheme).when(themeRepository)
.getThemeByThemeId(HaloConst.DEFAULT_THEME_ID);
doReturn(Optional.of(expectedTheme)).when(themeRepository)
.fetchThemeByThemeId(HaloConst.DEFAULT_THEME_ID);
ExecutorService executorService = Executors.newFixedThreadPool(10);
// define tasks
@ -96,7 +96,7 @@ class ThemeRepositoryImplTest {
});
verify(optionRepository, times(1)).findByKey(any());
verify(themeRepository, times(1)).getThemeByThemeId(any());
verify(themeRepository, times(1)).fetchThemeByThemeId(any());
}
}

View File

@ -133,6 +133,7 @@ class GitTest {
}
@Test
@Disabled("Due to time-consumption fetching")
void getBranchesFromRemote() throws GitAPIException {
Map<String, Ref> refMap = Git.lsRemoteRepository()
.setRemote("https://github.com/halo-dev/halo.git")