mirror of https://github.com/halo-dev/halo
Refactor theme sacn (#869)
* Provide theme collector * Refactor theme property scan * Refactor ThemePropertyScanner * Rectify return type * Change activated theme fetch strategy * Refactor theme service * Reformat unexpected codes * Remove unused imports Co-authored-by: johnniang <johnniang@fastmail.com>pull/900/head
parent
7a8c5af258
commit
ccea5ed6c3
|
@ -25,7 +25,7 @@ public abstract class AbstractStringCacheStore extends AbstractCacheStore<String
|
||||||
cacheWrapper = JsonUtils.jsonToObject(json, CacheWrapper.class);
|
cacheWrapper = JsonUtils.jsonToObject(json, CacheWrapper.class);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
log.debug("erro json to wrapper value bytes: [{}]", json, e);
|
log.debug("Failed to convert json to wrapper value bytes: [{}]", json, e);
|
||||||
}
|
}
|
||||||
return Optional.ofNullable(cacheWrapper);
|
return Optional.ofNullable(cacheWrapper);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ import run.halo.app.service.ThemeSettingService;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Theme controller.
|
* Theme controller.
|
||||||
|
@ -45,7 +44,7 @@ public class ThemeController {
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@ApiOperation("Lists all themes")
|
@ApiOperation("Lists all themes")
|
||||||
public Set<ThemeProperty> listAll() {
|
public List<ThemeProperty> listAll() {
|
||||||
return themeService.getThemes();
|
return themeService.getThemes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,13 +90,13 @@ public class ThemeController {
|
||||||
|
|
||||||
@GetMapping("activation/template/custom/sheet")
|
@GetMapping("activation/template/custom/sheet")
|
||||||
@ApiOperation("Gets custom sheet templates")
|
@ApiOperation("Gets custom sheet templates")
|
||||||
public Set<String> customSheetTemplate() {
|
public List<String> customSheetTemplate() {
|
||||||
return themeService.listCustomTemplates(themeService.getActivatedThemeId(), ThemeService.CUSTOM_SHEET_PREFIX);
|
return themeService.listCustomTemplates(themeService.getActivatedThemeId(), ThemeService.CUSTOM_SHEET_PREFIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("activation/template/custom/post")
|
@GetMapping("activation/template/custom/post")
|
||||||
@ApiOperation("Gets custom post templates")
|
@ApiOperation("Gets custom post templates")
|
||||||
public Set<String> customPostTemplate() {
|
public List<String> customPostTemplate() {
|
||||||
return themeService.listCustomTemplates(themeService.getActivatedThemeId(), ThemeService.CUSTOM_POST_PREFIX);
|
return themeService.listCustomTemplates(themeService.getActivatedThemeId(), ThemeService.CUSTOM_POST_PREFIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,19 +83,23 @@ public class ContentContentController {
|
||||||
Model model) {
|
Model model) {
|
||||||
if (optionService.getArchivesPrefix().equals(prefix)) {
|
if (optionService.getArchivesPrefix().equals(prefix)) {
|
||||||
return postModel.archives(1, model);
|
return postModel.archives(1, model);
|
||||||
} else if (optionService.getCategoriesPrefix().equals(prefix)) {
|
|
||||||
return categoryModel.list(model);
|
|
||||||
} else if (optionService.getTagsPrefix().equals(prefix)) {
|
|
||||||
return tagModel.list(model);
|
|
||||||
} else if (optionService.getJournalsPrefix().equals(prefix)) {
|
|
||||||
return journalModel.list(1, model);
|
|
||||||
} else if (optionService.getPhotosPrefix().equals(prefix)) {
|
|
||||||
return photoModel.list(1, model);
|
|
||||||
} else if (optionService.getLinksPrefix().equals(prefix)) {
|
|
||||||
return linkModel.list(model);
|
|
||||||
} else {
|
|
||||||
throw new NotFoundException("Not Found");
|
|
||||||
}
|
}
|
||||||
|
if (optionService.getCategoriesPrefix().equals(prefix)) {
|
||||||
|
return categoryModel.list(model);
|
||||||
|
}
|
||||||
|
if (optionService.getTagsPrefix().equals(prefix)) {
|
||||||
|
return tagModel.list(model);
|
||||||
|
}
|
||||||
|
if (optionService.getJournalsPrefix().equals(prefix)) {
|
||||||
|
return journalModel.list(1, model);
|
||||||
|
}
|
||||||
|
if (optionService.getPhotosPrefix().equals(prefix)) {
|
||||||
|
return photoModel.list(1, model);
|
||||||
|
}
|
||||||
|
if (optionService.getLinksPrefix().equals(prefix)) {
|
||||||
|
return linkModel.list(model);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("{prefix}/page/{page:\\d+}")
|
@GetMapping("{prefix}/page/{page:\\d+}")
|
||||||
|
|
|
@ -26,6 +26,8 @@ import javax.servlet.http.HttpServletResponse;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static run.halo.app.model.support.HaloConst.DEFAULT_ERROR_PATH;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error page Controller
|
* Error page Controller
|
||||||
*
|
*
|
||||||
|
@ -43,16 +45,12 @@ public class CommonController extends AbstractErrorController {
|
||||||
|
|
||||||
private static final String ERROR_TEMPLATE = "error.ftl";
|
private static final String ERROR_TEMPLATE = "error.ftl";
|
||||||
|
|
||||||
private static final String DEFAULT_ERROR_PATH = "common/error/error";
|
|
||||||
|
|
||||||
private static final String COULD_NOT_RESOLVE_VIEW_WITH_NAME_PREFIX = "Could not resolve view with name '";
|
private static final String COULD_NOT_RESOLVE_VIEW_WITH_NAME_PREFIX = "Could not resolve view with name '";
|
||||||
|
|
||||||
private final ThemeService themeService;
|
private final ThemeService themeService;
|
||||||
|
|
||||||
private final ErrorProperties errorProperties;
|
private final ErrorProperties errorProperties;
|
||||||
|
|
||||||
private final ErrorAttributes errorAttributes;
|
|
||||||
|
|
||||||
private final OptionService optionService;
|
private final OptionService optionService;
|
||||||
|
|
||||||
public CommonController(ThemeService themeService,
|
public CommonController(ThemeService themeService,
|
||||||
|
@ -61,7 +59,6 @@ public class CommonController extends AbstractErrorController {
|
||||||
OptionService optionService) {
|
OptionService optionService) {
|
||||||
super(errorAttributes);
|
super(errorAttributes);
|
||||||
this.themeService = themeService;
|
this.themeService = themeService;
|
||||||
this.errorAttributes = errorAttributes;
|
|
||||||
this.errorProperties = serverProperties.getError();
|
this.errorProperties = serverProperties.getError();
|
||||||
this.optionService = optionService;
|
this.optionService = optionService;
|
||||||
}
|
}
|
||||||
|
@ -166,9 +163,9 @@ public class CommonController extends AbstractErrorController {
|
||||||
}
|
}
|
||||||
|
|
||||||
Throwable throwable = (Throwable) throwableObject;
|
Throwable throwable = (Throwable) throwableObject;
|
||||||
log.error("Captured an exception", throwable);
|
|
||||||
|
|
||||||
if (throwable instanceof NestedServletException) {
|
if (throwable instanceof NestedServletException) {
|
||||||
|
log.error("Captured an exception", throwable);
|
||||||
Throwable rootCause = ((NestedServletException) throwable).getRootCause();
|
Throwable rootCause = ((NestedServletException) throwable).getRootCause();
|
||||||
if (rootCause instanceof AbstractHaloException) {
|
if (rootCause instanceof AbstractHaloException) {
|
||||||
AbstractHaloException haloException = (AbstractHaloException) rootCause;
|
AbstractHaloException haloException = (AbstractHaloException) rootCause;
|
||||||
|
@ -177,6 +174,7 @@ public class CommonController extends AbstractErrorController {
|
||||||
request.setAttribute("javax.servlet.error.message", haloException.getMessage());
|
request.setAttribute("javax.servlet.error.message", haloException.getMessage());
|
||||||
}
|
}
|
||||||
} else if (StringUtils.startsWithIgnoreCase(throwable.getMessage(), COULD_NOT_RESOLVE_VIEW_WITH_NAME_PREFIX)) {
|
} 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());
|
request.setAttribute("javax.servlet.error.status_code", HttpStatus.NOT_FOUND.value());
|
||||||
|
|
||||||
NotFoundException viewNotFound = new NotFoundException("该路径没有对应的模板");
|
NotFoundException viewNotFound = new NotFoundException("该路径没有对应的模板");
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package run.halo.app.handler.theme.config.impl;
|
package run.halo.app.handler.theme.config.impl;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import org.springframework.lang.NonNull;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import run.halo.app.handler.theme.config.ThemePropertyResolver;
|
import run.halo.app.handler.theme.config.ThemePropertyResolver;
|
||||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||||
|
import run.halo.app.theme.YamlResolver;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@ -19,17 +18,11 @@ import java.io.IOException;
|
||||||
@Service
|
@Service
|
||||||
public class YamlThemePropertyResolver implements ThemePropertyResolver {
|
public class YamlThemePropertyResolver implements ThemePropertyResolver {
|
||||||
|
|
||||||
private final ObjectMapper yamlMapper;
|
|
||||||
|
|
||||||
public YamlThemePropertyResolver() {
|
|
||||||
yamlMapper = new ObjectMapper(new YAMLFactory());
|
|
||||||
yamlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ThemeProperty resolve(String content) throws IOException {
|
@NonNull
|
||||||
|
public ThemeProperty resolve(@NonNull String content) throws IOException {
|
||||||
Assert.hasText(content, "Theme file content must not be null");
|
Assert.hasText(content, "Theme file content must not be null");
|
||||||
|
|
||||||
return yamlMapper.readValue(content, ThemeProperty.class);
|
return YamlResolver.INSTANCE.getYamlMapper().readValue(content, ThemeProperty.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ public class StartedListener implements ApplicationListener<ApplicationStartedEv
|
||||||
try {
|
try {
|
||||||
this.migrate();
|
this.migrate();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
e.printStackTrace();
|
log.error("Failed to migrate database!", e);
|
||||||
}
|
}
|
||||||
this.initThemes();
|
this.initThemes();
|
||||||
this.initDirectory();
|
this.initDirectory();
|
||||||
|
|
|
@ -12,7 +12,6 @@ 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;
|
||||||
import run.halo.app.event.user.UserUpdatedEvent;
|
import run.halo.app.event.user.UserUpdatedEvent;
|
||||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
|
||||||
import run.halo.app.model.properties.BlogProperties;
|
import run.halo.app.model.properties.BlogProperties;
|
||||||
import run.halo.app.model.properties.SeoProperties;
|
import run.halo.app.model.properties.SeoProperties;
|
||||||
import run.halo.app.model.support.HaloConst;
|
import run.halo.app.model.support.HaloConst;
|
||||||
|
@ -101,7 +100,8 @@ public class FreemarkerConfigAwareListener {
|
||||||
|
|
||||||
private void loadOptionsConfig() throws TemplateModelException {
|
private void loadOptionsConfig() throws TemplateModelException {
|
||||||
|
|
||||||
String context = optionService.isEnabledAbsolutePath() ? optionService.getBlogBaseUrl() + "/" : "/";
|
final String blogBaseUrl = optionService.getBlogBaseUrl();
|
||||||
|
final String context = optionService.isEnabledAbsolutePath() ? blogBaseUrl + "/" : "/";
|
||||||
|
|
||||||
configuration.setSharedVariable("options", optionService.listOptions());
|
configuration.setSharedVariable("options", optionService.listOptions());
|
||||||
configuration.setSharedVariable("context", context);
|
configuration.setSharedVariable("context", context);
|
||||||
|
@ -109,15 +109,15 @@ public class FreemarkerConfigAwareListener {
|
||||||
|
|
||||||
configuration.setSharedVariable("globalAbsolutePathEnabled", optionService.isEnabledAbsolutePath());
|
configuration.setSharedVariable("globalAbsolutePathEnabled", optionService.isEnabledAbsolutePath());
|
||||||
configuration.setSharedVariable("blog_title", optionService.getBlogTitle());
|
configuration.setSharedVariable("blog_title", optionService.getBlogTitle());
|
||||||
configuration.setSharedVariable("blog_url", optionService.getBlogBaseUrl());
|
configuration.setSharedVariable("blog_url", blogBaseUrl);
|
||||||
configuration.setSharedVariable("blog_logo", optionService.getByPropertyOrDefault(BlogProperties.BLOG_LOGO, String.class, BlogProperties.BLOG_LOGO.defaultValue()));
|
configuration.setSharedVariable("blog_logo", optionService.getByPropertyOrDefault(BlogProperties.BLOG_LOGO, String.class, BlogProperties.BLOG_LOGO.defaultValue()));
|
||||||
configuration.setSharedVariable("seo_keywords", optionService.getByPropertyOrDefault(SeoProperties.KEYWORDS, String.class, SeoProperties.KEYWORDS.defaultValue()));
|
configuration.setSharedVariable("seo_keywords", optionService.getByPropertyOrDefault(SeoProperties.KEYWORDS, String.class, SeoProperties.KEYWORDS.defaultValue()));
|
||||||
configuration.setSharedVariable("seo_description", optionService.getByPropertyOrDefault(SeoProperties.DESCRIPTION, String.class, SeoProperties.DESCRIPTION.defaultValue()));
|
configuration.setSharedVariable("seo_description", optionService.getByPropertyOrDefault(SeoProperties.DESCRIPTION, String.class, SeoProperties.DESCRIPTION.defaultValue()));
|
||||||
|
|
||||||
configuration.setSharedVariable("rss_url", optionService.getBlogBaseUrl() + "/rss.xml");
|
configuration.setSharedVariable("rss_url", blogBaseUrl + "/rss.xml");
|
||||||
configuration.setSharedVariable("atom_url", optionService.getBlogBaseUrl() + "/atom.xml");
|
configuration.setSharedVariable("atom_url", blogBaseUrl + "/atom.xml");
|
||||||
configuration.setSharedVariable("sitemap_xml_url", optionService.getBlogBaseUrl() + "/sitemap.xml");
|
configuration.setSharedVariable("sitemap_xml_url", blogBaseUrl + "/sitemap.xml");
|
||||||
configuration.setSharedVariable("sitemap_html_url", optionService.getBlogBaseUrl() + "/sitemap.html");
|
configuration.setSharedVariable("sitemap_html_url", blogBaseUrl + "/sitemap.html");
|
||||||
configuration.setSharedVariable("links_url", context + optionService.getLinksPrefix());
|
configuration.setSharedVariable("links_url", context + optionService.getLinksPrefix());
|
||||||
configuration.setSharedVariable("photos_url", context + optionService.getPhotosPrefix());
|
configuration.setSharedVariable("photos_url", context + optionService.getPhotosPrefix());
|
||||||
configuration.setSharedVariable("journals_url", context + optionService.getJournalsPrefix());
|
configuration.setSharedVariable("journals_url", context + optionService.getJournalsPrefix());
|
||||||
|
@ -128,13 +128,11 @@ public class FreemarkerConfigAwareListener {
|
||||||
log.debug("Loaded options");
|
log.debug("Loaded options");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadThemeConfig() throws TemplateModelException {
|
private void loadThemeConfig() {
|
||||||
|
|
||||||
// Get current activated theme.
|
// Get current activated theme.
|
||||||
ThemeProperty activatedTheme = themeService.getActivatedTheme();
|
themeService.fetchActivatedTheme().ifPresent(activatedTheme -> {
|
||||||
|
|
||||||
String themeBasePath = (optionService.isEnabledAbsolutePath() ? optionService.getBlogBaseUrl() : "") + "/themes/" + activatedTheme.getFolderName();
|
String themeBasePath = (optionService.isEnabledAbsolutePath() ? optionService.getBlogBaseUrl() : "") + "/themes/" + activatedTheme.getFolderName();
|
||||||
|
try {
|
||||||
configuration.setSharedVariable("theme", activatedTheme);
|
configuration.setSharedVariable("theme", activatedTheme);
|
||||||
|
|
||||||
// TODO: It will be removed in future versions
|
// TODO: It will be removed in future versions
|
||||||
|
@ -144,5 +142,10 @@ public class FreemarkerConfigAwareListener {
|
||||||
|
|
||||||
configuration.setSharedVariable("settings", themeSettingService.listAsMapBy(themeService.getActivatedThemeId()));
|
configuration.setSharedVariable("settings", themeSettingService.listAsMapBy(themeService.getActivatedThemeId()));
|
||||||
log.debug("Loaded theme and settings");
|
log.debug("Loaded theme and settings");
|
||||||
|
} catch (TemplateModelException e) {
|
||||||
|
log.error("Failed to set shared variable!", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,11 @@ public class HaloConst {
|
||||||
*/
|
*/
|
||||||
public final static String DEFAULT_THEME_ID = "caicai_anatole";
|
public final static String DEFAULT_THEME_ID = "caicai_anatole";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default error path.
|
||||||
|
*/
|
||||||
|
public static final String DEFAULT_ERROR_PATH = "common/error/error";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path separator.
|
* Path separator.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -11,7 +11,6 @@ import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Theme service interface.
|
* Theme service interface.
|
||||||
|
@ -30,6 +29,7 @@ public interface ThemeService {
|
||||||
/**
|
/**
|
||||||
* Theme property file name.
|
* Theme property file name.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
String[] THEME_PROPERTY_FILE_NAMES = {"theme.yaml", "theme.yml"};
|
String[] THEME_PROPERTY_FILE_NAMES = {"theme.yaml", "theme.yml"};
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,6 +56,7 @@ public interface ThemeService {
|
||||||
/**
|
/**
|
||||||
* Theme screenshots name.
|
* Theme screenshots name.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
String THEME_SCREENSHOTS_NAME = "screenshot";
|
String THEME_SCREENSHOTS_NAME = "screenshot";
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,6 +102,7 @@ public interface ThemeService {
|
||||||
* @return theme property
|
* @return theme property
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@Deprecated
|
||||||
ThemeProperty getThemeOfNonNullBy(@NonNull String themeId);
|
ThemeProperty getThemeOfNonNullBy(@NonNull String themeId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,7 +112,7 @@ public interface ThemeService {
|
||||||
* @return a optional theme property
|
* @return a optional theme property
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
Optional<ThemeProperty> getThemeBy(@Nullable String themeId);
|
Optional<ThemeProperty> fetchThemePropertyBy(@Nullable String themeId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all themes
|
* Gets all themes
|
||||||
|
@ -118,15 +120,7 @@ public interface ThemeService {
|
||||||
* @return set of themes
|
* @return set of themes
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
Set<ThemeProperty> getThemes();
|
List<ThemeProperty> getThemes();
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists theme folder by absolute path.
|
|
||||||
*
|
|
||||||
* @param absolutePath absolutePath
|
|
||||||
* @return List<ThemeFile>
|
|
||||||
*/
|
|
||||||
List<ThemeFile> listThemeFolder(@NonNull String absolutePath);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists theme folder by theme name.
|
* Lists theme folder by theme name.
|
||||||
|
@ -134,6 +128,7 @@ public interface ThemeService {
|
||||||
* @param themeId theme id
|
* @param themeId theme id
|
||||||
* @return List<ThemeFile>
|
* @return List<ThemeFile>
|
||||||
*/
|
*/
|
||||||
|
@NonNull
|
||||||
List<ThemeFile> listThemeFolderBy(@NonNull String themeId);
|
List<ThemeFile> listThemeFolderBy(@NonNull String themeId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -143,7 +138,8 @@ public interface ThemeService {
|
||||||
* @return a set of templates
|
* @return a set of templates
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
Set<String> listCustomTemplates(@NonNull String themeId);
|
@NonNull
|
||||||
|
List<String> listCustomTemplates(@NonNull String themeId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists a set of custom template, such as sheet_xxx.ftl/post_xxx.ftl, and xxx will be template name
|
* Lists a set of custom template, such as sheet_xxx.ftl/post_xxx.ftl, and xxx will be template name
|
||||||
|
@ -152,7 +148,8 @@ public interface ThemeService {
|
||||||
* @param prefix post_ or sheet_
|
* @param prefix post_ or sheet_
|
||||||
* @return a set of templates
|
* @return a set of templates
|
||||||
*/
|
*/
|
||||||
Set<String> listCustomTemplates(@NonNull String themeId, @NonNull String prefix);
|
@NonNull
|
||||||
|
List<String> listCustomTemplates(@NonNull String themeId, @NonNull String prefix);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Judging whether template exists under the specified theme
|
* Judging whether template exists under the specified theme
|
||||||
|
@ -261,6 +258,14 @@ public interface ThemeService {
|
||||||
@NonNull
|
@NonNull
|
||||||
ThemeProperty getActivatedTheme();
|
ThemeProperty getActivatedTheme();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch activated theme property.
|
||||||
|
*
|
||||||
|
* @return activated theme property
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
Optional<ThemeProperty> fetchActivatedTheme();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actives a theme.
|
* Actives a theme.
|
||||||
*
|
*
|
||||||
|
|
|
@ -19,7 +19,6 @@ import org.springframework.lang.NonNull;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import run.halo.app.cache.AbstractStringCacheStore;
|
import run.halo.app.cache.AbstractStringCacheStore;
|
||||||
|
@ -28,7 +27,6 @@ import run.halo.app.event.theme.ThemeActivatedEvent;
|
||||||
import run.halo.app.event.theme.ThemeUpdatedEvent;
|
import run.halo.app.event.theme.ThemeUpdatedEvent;
|
||||||
import run.halo.app.exception.*;
|
import run.halo.app.exception.*;
|
||||||
import run.halo.app.handler.theme.config.ThemeConfigResolver;
|
import run.halo.app.handler.theme.config.ThemeConfigResolver;
|
||||||
import run.halo.app.handler.theme.config.ThemePropertyResolver;
|
|
||||||
import run.halo.app.handler.theme.config.support.Group;
|
import run.halo.app.handler.theme.config.support.Group;
|
||||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||||
import run.halo.app.model.properties.PrimaryProperties;
|
import run.halo.app.model.properties.PrimaryProperties;
|
||||||
|
@ -36,6 +34,8 @@ import run.halo.app.model.support.HaloConst;
|
||||||
import run.halo.app.model.support.ThemeFile;
|
import run.halo.app.model.support.ThemeFile;
|
||||||
import run.halo.app.service.OptionService;
|
import run.halo.app.service.OptionService;
|
||||||
import run.halo.app.service.ThemeService;
|
import run.halo.app.service.ThemeService;
|
||||||
|
import run.halo.app.theme.ThemeFileScanner;
|
||||||
|
import run.halo.app.theme.ThemePropertyScanner;
|
||||||
import run.halo.app.utils.*;
|
import run.halo.app.utils.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -45,12 +45,12 @@ import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
import static run.halo.app.model.support.HaloConst.DEFAULT_ERROR_PATH;
|
||||||
import static run.halo.app.model.support.HaloConst.DEFAULT_THEME_ID;
|
import static run.halo.app.model.support.HaloConst.DEFAULT_THEME_ID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,23 +63,25 @@ import static run.halo.app.model.support.HaloConst.DEFAULT_THEME_ID;
|
||||||
@Service
|
@Service
|
||||||
public class ThemeServiceImpl implements ThemeService {
|
public class ThemeServiceImpl implements ThemeService {
|
||||||
|
|
||||||
/**
|
|
||||||
* in seconds.
|
|
||||||
*/
|
|
||||||
protected static final long ACTIVATED_THEME_SYNC_INTERVAL = 5;
|
|
||||||
/**
|
/**
|
||||||
* Theme work directory.
|
* Theme work directory.
|
||||||
*/
|
*/
|
||||||
private final Path themeWorkDir;
|
private final Path themeWorkDir;
|
||||||
|
|
||||||
private final OptionService optionService;
|
private final OptionService optionService;
|
||||||
|
|
||||||
private final AbstractStringCacheStore cacheStore;
|
private final AbstractStringCacheStore cacheStore;
|
||||||
|
|
||||||
private final ThemeConfigResolver themeConfigResolver;
|
private final ThemeConfigResolver themeConfigResolver;
|
||||||
private final ThemePropertyResolver themePropertyResolver;
|
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activated theme id.
|
* Activated theme id.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
private volatile String activatedThemeId;
|
private volatile String activatedThemeId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -87,123 +89,77 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
*/
|
*/
|
||||||
private volatile ThemeProperty activatedTheme;
|
private volatile ThemeProperty activatedTheme;
|
||||||
|
|
||||||
|
private final AtomicReference<String> activeThemeId = new AtomicReference<>();
|
||||||
|
|
||||||
public ThemeServiceImpl(HaloProperties haloProperties,
|
public ThemeServiceImpl(HaloProperties haloProperties,
|
||||||
OptionService optionService,
|
OptionService optionService,
|
||||||
AbstractStringCacheStore cacheStore,
|
AbstractStringCacheStore cacheStore,
|
||||||
ThemeConfigResolver themeConfigResolver,
|
ThemeConfigResolver themeConfigResolver,
|
||||||
ThemePropertyResolver themePropertyResolver,
|
|
||||||
RestTemplate restTemplate,
|
RestTemplate restTemplate,
|
||||||
ApplicationEventPublisher eventPublisher) {
|
ApplicationEventPublisher eventPublisher) {
|
||||||
this.optionService = optionService;
|
this.optionService = optionService;
|
||||||
this.cacheStore = cacheStore;
|
this.cacheStore = cacheStore;
|
||||||
this.themeConfigResolver = themeConfigResolver;
|
this.themeConfigResolver = themeConfigResolver;
|
||||||
this.themePropertyResolver = themePropertyResolver;
|
|
||||||
this.restTemplate = restTemplate;
|
this.restTemplate = restTemplate;
|
||||||
|
|
||||||
themeWorkDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER);
|
themeWorkDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER);
|
||||||
this.eventPublisher = eventPublisher;
|
this.eventPublisher = eventPublisher;
|
||||||
// check activated theme option changes every 5 seconds.
|
|
||||||
Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(() -> {
|
|
||||||
try {
|
|
||||||
String newActivatedThemeId = optionService.getByPropertyOrDefault(PrimaryProperties.THEME, String.class, DEFAULT_THEME_ID);
|
|
||||||
if (!activatedThemeId.equals(newActivatedThemeId)) {
|
|
||||||
activateTheme(newActivatedThemeId);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("theme option sync exception: {}", e);
|
|
||||||
}
|
|
||||||
}, ACTIVATED_THEME_SYNC_INTERVAL, ACTIVATED_THEME_SYNC_INTERVAL, TimeUnit.SECONDS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ThemeProperty getThemeOfNonNullBy(String themeId) {
|
@NonNull
|
||||||
return getThemeBy(themeId).orElseThrow(() -> new NotFoundException("没有找到 id 为 " + themeId + " 的主题").setErrorData(themeId));
|
public ThemeProperty getThemeOfNonNullBy(@NonNull String themeId) {
|
||||||
|
return fetchThemePropertyBy(themeId).orElseThrow(() -> new NotFoundException(themeId + " 主题不存在或已删除!").setErrorData(themeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<ThemeProperty> getThemeBy(String themeId) {
|
@NonNull
|
||||||
|
public Optional<ThemeProperty> fetchThemePropertyBy(String themeId) {
|
||||||
if (StringUtils.isBlank(themeId)) {
|
if (StringUtils.isBlank(themeId)) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all themes
|
// Get all themes
|
||||||
Set<ThemeProperty> themes = getThemes();
|
List<ThemeProperty> themes = getThemes();
|
||||||
|
|
||||||
// filter and find first
|
// filter and find first
|
||||||
return themes.stream().filter(themeProperty -> StringUtils.equals(themeProperty.getId(), themeId)).findFirst();
|
return themes.stream()
|
||||||
|
.filter(themeProperty -> StringUtils.equals(themeProperty.getId(), themeId))
|
||||||
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<ThemeProperty> getThemes() {
|
@NonNull
|
||||||
|
public List<ThemeProperty> getThemes() {
|
||||||
Optional<ThemeProperty[]> themePropertiesOptional = cacheStore.getAny(THEMES_CACHE_KEY, ThemeProperty[].class);
|
ThemeProperty[] themeProperties = cacheStore.getAny(THEMES_CACHE_KEY, ThemeProperty[].class).orElseGet(() -> {
|
||||||
|
List<ThemeProperty> properties = ThemePropertyScanner.INSTANCE.scan(getBasePath(), getActivatedThemeId());
|
||||||
if (themePropertiesOptional.isPresent()) {
|
|
||||||
// Convert to theme properties
|
|
||||||
ThemeProperty[] themeProperties = themePropertiesOptional.get();
|
|
||||||
return new HashSet<>(Arrays.asList(themeProperties));
|
|
||||||
}
|
|
||||||
|
|
||||||
try (Stream<Path> pathStream = Files.list(getBasePath())) {
|
|
||||||
|
|
||||||
// List and filter sub folders
|
|
||||||
List<Path> themePaths = pathStream.filter(path -> Files.isDirectory(path))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (CollectionUtils.isEmpty(themePaths)) {
|
|
||||||
return Collections.emptySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get theme properties
|
|
||||||
Set<ThemeProperty> themes = themePaths.stream().map(this::getProperty).collect(Collectors.toSet());
|
|
||||||
|
|
||||||
// Cache the themes
|
// Cache the themes
|
||||||
cacheStore.putAny(THEMES_CACHE_KEY, themes);
|
cacheStore.putAny(THEMES_CACHE_KEY, properties);
|
||||||
|
return properties.toArray(new ThemeProperty[0]);
|
||||||
return themes;
|
});
|
||||||
} catch (IOException e) {
|
return Arrays.asList(themeProperties);
|
||||||
throw new ServiceException("Failed to get themes", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ThemeFile> listThemeFolder(String absolutePath) {
|
@NonNull
|
||||||
return listThemeFileTree(Paths.get(absolutePath));
|
public List<ThemeFile> listThemeFolderBy(@NonNull String themeId) {
|
||||||
|
return fetchThemePropertyBy(themeId)
|
||||||
|
.map(themeProperty -> ThemeFileScanner.INSTANCE.scan(themeProperty.getThemePath()))
|
||||||
|
.orElse(Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ThemeFile> listThemeFolderBy(String themeId) {
|
@NonNull
|
||||||
// Get the theme property
|
public List<String> listCustomTemplates(@NonNull String themeId) {
|
||||||
ThemeProperty themeProperty = getThemeOfNonNullBy(themeId);
|
return listCustomTemplates(themeId, CUSTOM_SHEET_PREFIX);
|
||||||
|
|
||||||
// List theme file as tree view
|
|
||||||
return listThemeFolder(themeProperty.getThemePath());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<String> listCustomTemplates(String themeId) {
|
@NonNull
|
||||||
|
public List<String> listCustomTemplates(@NonNull String themeId, @NonNull String prefix) {
|
||||||
|
return fetchThemePropertyBy(themeId).map(themeProperty -> {
|
||||||
// Get the theme path
|
// Get the theme path
|
||||||
Path themePath = Paths.get(getThemeOfNonNullBy(themeId).getThemePath());
|
Path themePath = Paths.get(themeProperty.getThemePath());
|
||||||
|
|
||||||
try (Stream<Path> pathStream = Files.list(themePath)) {
|
|
||||||
return pathStream.filter(path -> StringUtils.startsWithIgnoreCase(path.getFileName().toString(), CUSTOM_SHEET_PREFIX))
|
|
||||||
.map(path -> {
|
|
||||||
// Remove prefix
|
|
||||||
String customTemplate = StringUtils.removeStartIgnoreCase(path.getFileName().toString(), CUSTOM_SHEET_PREFIX);
|
|
||||||
// Remove suffix
|
|
||||||
return StringUtils.removeEndIgnoreCase(customTemplate, HaloConst.SUFFIX_FTL);
|
|
||||||
})
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new ServiceException("Failed to list files of path " + themePath.toString(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<String> listCustomTemplates(String themeId, String prefix) {
|
|
||||||
// Get the theme path
|
|
||||||
Path themePath = Paths.get(getThemeOfNonNullBy(themeId).getThemePath());
|
|
||||||
|
|
||||||
try (Stream<Path> pathStream = Files.list(themePath)) {
|
try (Stream<Path> pathStream = Files.list(themePath)) {
|
||||||
return pathStream.filter(path -> StringUtils.startsWithIgnoreCase(path.getFileName().toString(), prefix))
|
return pathStream.filter(path -> StringUtils.startsWithIgnoreCase(path.getFileName().toString(), prefix))
|
||||||
.map(path -> {
|
.map(path -> {
|
||||||
|
@ -212,10 +168,12 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
// Remove suffix
|
// Remove suffix
|
||||||
return StringUtils.removeEndIgnoreCase(customTemplate, HaloConst.SUFFIX_FTL);
|
return StringUtils.removeEndIgnoreCase(customTemplate, HaloConst.SUFFIX_FTL);
|
||||||
})
|
})
|
||||||
.collect(Collectors.toSet());
|
.distinct()
|
||||||
} catch (IOException e) {
|
.collect(Collectors.toList());
|
||||||
throw new ServiceException("Failed to list files of path " + themePath.toString(), e);
|
} catch (Exception e) {
|
||||||
|
throw new ServiceException("Failed to list files of path " + themePath, e);
|
||||||
}
|
}
|
||||||
|
}).orElse(Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -224,19 +182,19 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fetchActivatedTheme().map(themeProperty -> {
|
||||||
// Resolve template path
|
// Resolve template path
|
||||||
Path templatePath = Paths.get(getActivatedTheme().getThemePath(), template);
|
Path templatePath = Paths.get(themeProperty.getThemePath(), template);
|
||||||
|
|
||||||
// Check the directory
|
// Check the directory
|
||||||
checkDirectory(templatePath.toString());
|
checkDirectory(templatePath.toString());
|
||||||
|
|
||||||
// Check existence
|
// Check existence
|
||||||
return Files.exists(templatePath);
|
return Files.exists(templatePath);
|
||||||
|
}).orElse(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean themeExists(String themeId) {
|
public boolean themeExists(String themeId) {
|
||||||
return getThemeBy(themeId).isPresent();
|
return fetchThemePropertyBy(themeId).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -245,7 +203,7 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getTemplateContent(String absolutePath) {
|
public String getTemplateContent(@NonNull String absolutePath) {
|
||||||
// Check the path
|
// Check the path
|
||||||
checkDirectory(absolutePath);
|
checkDirectory(absolutePath);
|
||||||
|
|
||||||
|
@ -259,7 +217,8 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getTemplateContent(String themeId, String absolutePath) {
|
@NonNull
|
||||||
|
public String getTemplateContent(@NonNull String themeId, @NonNull String absolutePath) {
|
||||||
checkDirectory(themeId, absolutePath);
|
checkDirectory(themeId, absolutePath);
|
||||||
|
|
||||||
// Read file
|
// Read file
|
||||||
|
@ -272,7 +231,7 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void saveTemplateContent(String absolutePath, String content) {
|
public void saveTemplateContent(@NonNull String absolutePath, String content) {
|
||||||
// Check the path
|
// Check the path
|
||||||
checkDirectory(absolutePath);
|
checkDirectory(absolutePath);
|
||||||
|
|
||||||
|
@ -286,7 +245,7 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void saveTemplateContent(String themeId, String absolutePath, String content) {
|
public void saveTemplateContent(@NonNull String themeId, @NonNull String absolutePath, String content) {
|
||||||
// Check the path
|
// Check the path
|
||||||
checkDirectory(themeId, absolutePath);
|
checkDirectory(themeId, absolutePath);
|
||||||
|
|
||||||
|
@ -300,7 +259,7 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteTheme(String themeId) {
|
public void deleteTheme(@NonNull String themeId) {
|
||||||
// Get the theme property
|
// Get the theme property
|
||||||
ThemeProperty themeProperty = getThemeOfNonNullBy(themeId);
|
ThemeProperty themeProperty = getThemeOfNonNullBy(themeId);
|
||||||
|
|
||||||
|
@ -320,7 +279,8 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Group> fetchConfig(String themeId) {
|
@NonNull
|
||||||
|
public List<Group> fetchConfig(@NonNull String themeId) {
|
||||||
Assert.hasText(themeId, "Theme id must not be blank");
|
Assert.hasText(themeId, "Theme id must not be blank");
|
||||||
|
|
||||||
// Get theme property
|
// Get theme property
|
||||||
|
@ -358,10 +318,9 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String render(String pageName) {
|
public String render(String pageName) {
|
||||||
// Get activated theme
|
return fetchActivatedTheme()
|
||||||
ThemeProperty activatedTheme = getActivatedTheme();
|
.map(themeProperty -> String.format(RENDER_TEMPLATE, themeProperty.getFolderName(), pageName))
|
||||||
// Build render url
|
.orElse(DEFAULT_ERROR_PATH);
|
||||||
return String.format(RENDER_TEMPLATE, activatedTheme.getFolderName(), pageName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -373,6 +332,7 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@NonNull
|
||||||
public String getActivatedThemeId() {
|
public String getActivatedThemeId() {
|
||||||
if (activatedThemeId == null) {
|
if (activatedThemeId == null) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
|
@ -381,11 +341,11 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return activatedThemeId;
|
return activatedThemeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@NonNull
|
||||||
public ThemeProperty getActivatedTheme() {
|
public ThemeProperty getActivatedTheme() {
|
||||||
if (activatedTheme == null) {
|
if (activatedTheme == null) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
|
@ -395,10 +355,15 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return activatedTheme;
|
return activatedTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public Optional<ThemeProperty> fetchActivatedTheme() {
|
||||||
|
return fetchThemePropertyBy(getActivatedThemeId());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets activated theme.
|
* Sets activated theme.
|
||||||
*
|
*
|
||||||
|
@ -410,7 +375,8 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ThemeProperty activateTheme(String themeId) {
|
@NonNull
|
||||||
|
public ThemeProperty activateTheme(@NonNull String themeId) {
|
||||||
// Check existence of the theme
|
// Check existence of the theme
|
||||||
ThemeProperty themeProperty = getThemeOfNonNullBy(themeId);
|
ThemeProperty themeProperty = getThemeOfNonNullBy(themeId);
|
||||||
|
|
||||||
|
@ -430,7 +396,8 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ThemeProperty upload(MultipartFile file) {
|
@NonNull
|
||||||
|
public ThemeProperty upload(@NonNull MultipartFile file) {
|
||||||
Assert.notNull(file, "Multipart file must not be null");
|
Assert.notNull(file, "Multipart file must not be null");
|
||||||
|
|
||||||
if (!StringUtils.endsWithIgnoreCase(file.getOriginalFilename(), ".zip")) {
|
if (!StringUtils.endsWithIgnoreCase(file.getOriginalFilename(), ".zip")) {
|
||||||
|
@ -443,7 +410,7 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
try {
|
try {
|
||||||
// Create temp directory
|
// Create temp directory
|
||||||
tempPath = FileUtils.createTempDirectory();
|
tempPath = FileUtils.createTempDirectory();
|
||||||
String basename = FilenameUtils.getBasename(file.getOriginalFilename());
|
String basename = FilenameUtils.getBasename(Objects.requireNonNull(file.getOriginalFilename()));
|
||||||
Path themeTempPath = tempPath.resolve(basename);
|
Path themeTempPath = tempPath.resolve(basename);
|
||||||
|
|
||||||
// Check directory traversal
|
// Check directory traversal
|
||||||
|
@ -455,10 +422,12 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
// Unzip to temp path
|
// Unzip to temp path
|
||||||
FileUtils.unzip(zis, themeTempPath);
|
FileUtils.unzip(zis, themeTempPath);
|
||||||
|
|
||||||
|
Path themePath = FileUtils.tryToSkipZipParentFolder(themeTempPath);
|
||||||
|
|
||||||
// Go to the base folder and add the theme into system
|
// Go to the base folder and add the theme into system
|
||||||
return add(FileUtils.tryToSkipZipParentFolder(themeTempPath));
|
return add(themePath);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ServiceException("上传主题失败: " + file.getOriginalFilename(), e);
|
throw new ServiceException("主题上传失败: " + file.getOriginalFilename(), e);
|
||||||
} finally {
|
} finally {
|
||||||
// Close zip input stream
|
// Close zip input stream
|
||||||
FileUtils.closeQuietly(zis);
|
FileUtils.closeQuietly(zis);
|
||||||
|
@ -468,7 +437,8 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ThemeProperty add(Path themeTmpPath) throws IOException {
|
@NonNull
|
||||||
|
public ThemeProperty add(@NonNull Path themeTmpPath) throws IOException {
|
||||||
Assert.notNull(themeTmpPath, "Theme temporary path must not be null");
|
Assert.notNull(themeTmpPath, "Theme temporary path must not be null");
|
||||||
Assert.isTrue(Files.isDirectory(themeTmpPath), "Theme temporary path must be a directory");
|
Assert.isTrue(Files.isDirectory(themeTmpPath), "Theme temporary path must be a directory");
|
||||||
|
|
||||||
|
@ -509,7 +479,7 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ThemeProperty fetch(String uri) {
|
public ThemeProperty fetch(@NonNull String uri) {
|
||||||
Assert.hasText(uri, "Theme remote uri must not be blank");
|
Assert.hasText(uri, "Theme remote uri must not be blank");
|
||||||
|
|
||||||
Path tmpPath = null;
|
Path tmpPath = null;
|
||||||
|
@ -621,7 +591,8 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pullFromGit(@NonNull ThemeProperty themeProperty) throws IOException, GitAPIException, URISyntaxException {
|
private void pullFromGit(@NonNull ThemeProperty themeProperty) throws
|
||||||
|
IOException, GitAPIException, URISyntaxException {
|
||||||
Assert.notNull(themeProperty, "Theme property must not be null");
|
Assert.notNull(themeProperty, "Theme property must not be null");
|
||||||
|
|
||||||
// Get branch
|
// Get branch
|
||||||
|
@ -637,7 +608,9 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
|
|
||||||
RevWalk revWalk = new RevWalk(repository);
|
RevWalk revWalk = new RevWalk(repository);
|
||||||
|
|
||||||
Ref ref = repository.getAllRefs().get(Constants.HEAD);
|
Ref ref = repository.findRef(Constants.HEAD);
|
||||||
|
|
||||||
|
Assert.notNull(ref, Constants.HEAD + " ref was not found!");
|
||||||
|
|
||||||
RevCommit lastCommit = revWalk.parseCommit(ref.getObjectId());
|
RevCommit lastCommit = revWalk.parseCommit(ref.getObjectId());
|
||||||
|
|
||||||
|
@ -728,74 +701,6 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
FileUtils.unzip(downloadResponse.getBody(), targetPath);
|
FileUtils.unzip(downloadResponse.getBody(), targetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Lists theme files as tree view.
|
|
||||||
*
|
|
||||||
* @param topPath must not be null
|
|
||||||
* @return theme file tree view or null only if the top path is not a directory
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private List<ThemeFile> listThemeFileTree(@NonNull Path topPath) {
|
|
||||||
Assert.notNull(topPath, "Top path must not be null");
|
|
||||||
|
|
||||||
// Check file type
|
|
||||||
if (!Files.isDirectory(topPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (Stream<Path> pathStream = Files.list(topPath)) {
|
|
||||||
List<ThemeFile> themeFiles = new LinkedList<>();
|
|
||||||
|
|
||||||
pathStream.forEach(path -> {
|
|
||||||
// Build theme file
|
|
||||||
ThemeFile themeFile = new ThemeFile();
|
|
||||||
themeFile.setName(path.getFileName().toString());
|
|
||||||
themeFile.setPath(path.toString());
|
|
||||||
themeFile.setIsFile(Files.isRegularFile(path));
|
|
||||||
themeFile.setEditable(isEditable(path));
|
|
||||||
|
|
||||||
if (Files.isDirectory(path)) {
|
|
||||||
themeFile.setNode(listThemeFileTree(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to theme files
|
|
||||||
themeFiles.add(themeFile);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort with isFile param
|
|
||||||
themeFiles.sort(new ThemeFile());
|
|
||||||
|
|
||||||
return themeFiles;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new ServiceException("Failed to list sub files", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given path is editable.
|
|
||||||
*
|
|
||||||
* @param path must not be null
|
|
||||||
* @return true if the given path is editable; false otherwise
|
|
||||||
*/
|
|
||||||
private boolean isEditable(@NonNull Path path) {
|
|
||||||
Assert.notNull(path, "Path must not be null");
|
|
||||||
|
|
||||||
boolean isEditable = Files.isReadable(path) && Files.isWritable(path);
|
|
||||||
|
|
||||||
if (!isEditable) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check suffix
|
|
||||||
for (String suffix : CAN_EDIT_SUFFIX) {
|
|
||||||
if (path.toString().endsWith(suffix)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if directory is valid or not.
|
* Check if directory is valid or not.
|
||||||
*
|
*
|
||||||
|
@ -818,82 +723,6 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
FileUtils.checkDirectoryTraversal(themeProperty.getThemePath(), absoluteName);
|
FileUtils.checkDirectoryTraversal(themeProperty.getThemePath(), absoluteName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets property path of nullable.
|
|
||||||
*
|
|
||||||
* @param themePath theme path.
|
|
||||||
* @return an optional property path
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
private Optional<Path> getThemePropertyPathOfNullable(@NonNull Path themePath) {
|
|
||||||
Assert.notNull(themePath, "Theme path must not be null");
|
|
||||||
|
|
||||||
for (String propertyPathName : THEME_PROPERTY_FILE_NAMES) {
|
|
||||||
Path propertyPath = themePath.resolve(propertyPathName);
|
|
||||||
|
|
||||||
log.debug("Attempting to find property file: [{}]", propertyPath);
|
|
||||||
if (Files.exists(propertyPath) && Files.isReadable(propertyPath)) {
|
|
||||||
log.debug("Found property file: [{}]", propertyPath);
|
|
||||||
return Optional.of(propertyPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.warn("Property file was not found in [{}]", themePath);
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets property path of non null.
|
|
||||||
*
|
|
||||||
* @param themePath theme path.
|
|
||||||
* @return property path won't be null
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
private Path getThemePropertyPath(@NonNull Path themePath) {
|
|
||||||
return getThemePropertyPathOfNullable(themePath).orElseThrow(() -> new ThemePropertyMissingException(themePath + " 没有说明文件").setErrorData(themePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<ThemeProperty> getPropertyOfNullable(Path themePath) {
|
|
||||||
Assert.notNull(themePath, "Theme path must not be null");
|
|
||||||
|
|
||||||
Path propertyPath = getThemePropertyPath(themePath);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get property content
|
|
||||||
String propertyContent = new String(Files.readAllBytes(propertyPath), StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
// Resolve the base properties
|
|
||||||
ThemeProperty themeProperty = themePropertyResolver.resolve(propertyContent);
|
|
||||||
|
|
||||||
// Resolve additional properties
|
|
||||||
themeProperty.setThemePath(themePath.toString());
|
|
||||||
themeProperty.setFolderName(themePath.getFileName().toString());
|
|
||||||
themeProperty.setHasOptions(hasOptions(themePath));
|
|
||||||
themeProperty.setActivated(false);
|
|
||||||
|
|
||||||
// Set screenshots
|
|
||||||
getScreenshotsFileName(themePath).ifPresent(screenshotsName ->
|
|
||||||
themeProperty.setScreenshots(StringUtils.join(optionService.getBlogBaseUrl(),
|
|
||||||
"/themes/",
|
|
||||||
FilenameUtils.getBasename(themeProperty.getThemePath()),
|
|
||||||
"/",
|
|
||||||
screenshotsName)));
|
|
||||||
|
|
||||||
if (StringUtils.equals(themeProperty.getId(), getActivatedThemeId())) {
|
|
||||||
// Set activation
|
|
||||||
themeProperty.setActivated(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.of(themeProperty);
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Failed to load theme property file", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets theme property.
|
* Gets theme property.
|
||||||
*
|
*
|
||||||
|
@ -902,48 +731,8 @@ public class ThemeServiceImpl implements ThemeService {
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
private ThemeProperty getProperty(@NonNull Path themePath) {
|
private ThemeProperty getProperty(@NonNull Path themePath) {
|
||||||
return getPropertyOfNullable(themePath).orElseThrow(() -> new ThemePropertyMissingException(themePath + " 没有说明文件").setErrorData(themePath));
|
return ThemePropertyScanner.INSTANCE.fetchThemeProperty(themePath)
|
||||||
|
.orElseThrow(() -> new ThemePropertyMissingException(themePath + " 没有说明文件").setErrorData(themePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets screenshots file name.
|
|
||||||
*
|
|
||||||
* @param themePath theme path must not be null
|
|
||||||
* @return screenshots file name or null if the given theme path has not screenshots
|
|
||||||
* @throws IOException throws when listing files
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
private Optional<String> getScreenshotsFileName(@NonNull Path themePath) throws IOException {
|
|
||||||
Assert.notNull(themePath, "Theme path must not be null");
|
|
||||||
|
|
||||||
try (Stream<Path> pathStream = Files.list(themePath)) {
|
|
||||||
return pathStream.filter(path -> Files.isRegularFile(path)
|
|
||||||
&& Files.isReadable(path)
|
|
||||||
&& FilenameUtils.getBasename(path.toString()).equalsIgnoreCase(THEME_SCREENSHOTS_NAME))
|
|
||||||
.findFirst()
|
|
||||||
.map(path -> path.getFileName().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check existence of the options.
|
|
||||||
*
|
|
||||||
* @param themePath theme path must not be null
|
|
||||||
* @return true if it has options; false otherwise
|
|
||||||
*/
|
|
||||||
private boolean hasOptions(@NonNull Path themePath) {
|
|
||||||
Assert.notNull(themePath, "Path must not be null");
|
|
||||||
|
|
||||||
for (String optionsName : SETTINGS_NAMES) {
|
|
||||||
// Resolve the options path
|
|
||||||
Path optionsPath = themePath.resolve(optionsName);
|
|
||||||
|
|
||||||
log.debug("Check options file for path: [{}]", optionsPath);
|
|
||||||
|
|
||||||
if (Files.exists(optionsPath)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
package run.halo.app.theme;
|
||||||
|
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import run.halo.app.exception.ServiceException;
|
||||||
|
import run.halo.app.model.support.ThemeFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static run.halo.app.service.ThemeService.CAN_EDIT_SUFFIX;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme file scanner.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public enum ThemeFileScanner {
|
||||||
|
|
||||||
|
INSTANCE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists theme folder by absolute path.
|
||||||
|
*
|
||||||
|
* @param absolutePath absolutePath
|
||||||
|
* @return List<ThemeFile> a list of theme files
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public List<ThemeFile> scan(@NonNull String absolutePath) {
|
||||||
|
Assert.hasText(absolutePath, "Absolute path must not be blank");
|
||||||
|
|
||||||
|
return scan(Paths.get(absolutePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists theme files as tree view.
|
||||||
|
*
|
||||||
|
* @param rootPath theme root path must not be null
|
||||||
|
* @return theme file tree view
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private List<ThemeFile> scan(@NonNull Path rootPath) {
|
||||||
|
Assert.notNull(rootPath, "Root path must not be null");
|
||||||
|
|
||||||
|
// Check file type
|
||||||
|
if (!Files.isDirectory(rootPath)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Stream<Path> pathStream = Files.list(rootPath)) {
|
||||||
|
List<ThemeFile> themeFiles = new LinkedList<>();
|
||||||
|
|
||||||
|
pathStream.forEach(path -> {
|
||||||
|
// Build theme file
|
||||||
|
ThemeFile themeFile = new ThemeFile();
|
||||||
|
themeFile.setName(path.getFileName().toString());
|
||||||
|
themeFile.setPath(path.toString());
|
||||||
|
themeFile.setIsFile(Files.isRegularFile(path));
|
||||||
|
themeFile.setEditable(isEditable(path));
|
||||||
|
|
||||||
|
if (Files.isDirectory(path)) {
|
||||||
|
themeFile.setNode(scan(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to theme files
|
||||||
|
themeFiles.add(themeFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort with isFile param
|
||||||
|
themeFiles.sort(new ThemeFile());
|
||||||
|
|
||||||
|
return themeFiles;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ServiceException("Failed to list sub files", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given path is editable.
|
||||||
|
*
|
||||||
|
* @param path must not be null
|
||||||
|
* @return true if the given path is editable; false otherwise
|
||||||
|
*/
|
||||||
|
private boolean isEditable(@NonNull Path path) {
|
||||||
|
Assert.notNull(path, "Path must not be null");
|
||||||
|
|
||||||
|
boolean isEditable = Files.isReadable(path) && Files.isWritable(path);
|
||||||
|
|
||||||
|
if (!isEditable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check suffix
|
||||||
|
for (String suffix : CAN_EDIT_SUFFIX) {
|
||||||
|
if (path.toString().endsWith(suffix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
package run.halo.app.theme;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import run.halo.app.handler.theme.config.ThemePropertyResolver;
|
||||||
|
import run.halo.app.handler.theme.config.impl.YamlThemePropertyResolver;
|
||||||
|
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||||
|
import run.halo.app.utils.FilenameUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static run.halo.app.service.ThemeService.SETTINGS_NAMES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme property scanner.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public enum ThemePropertyScanner {
|
||||||
|
|
||||||
|
INSTANCE;
|
||||||
|
|
||||||
|
private final ThemePropertyResolver propertyResolver = new YamlThemePropertyResolver();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme property file name.
|
||||||
|
*/
|
||||||
|
private static final String[] THEME_PROPERTY_FILE_NAMES = {"theme.yaml", "theme.yml"};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme screenshots name.
|
||||||
|
*/
|
||||||
|
private static final String THEME_SCREENSHOTS_NAME = "screenshot";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan theme properties.
|
||||||
|
*
|
||||||
|
* @param themePath them path must not be null
|
||||||
|
* @return a list of them property
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public List<ThemeProperty> scan(@NonNull Path themePath, @Nullable String activeThemeId) {
|
||||||
|
// create if absent
|
||||||
|
try {
|
||||||
|
if (Files.notExists(themePath)) {
|
||||||
|
Files.createDirectories(themePath);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to create directory: " + themePath, e);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
try (Stream<Path> pathStream = Files.list(themePath)) {
|
||||||
|
// List and filter sub folders
|
||||||
|
List<Path> themePaths = pathStream.filter(path -> Files.isDirectory(path))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (CollectionUtils.isEmpty(themePaths)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get theme properties
|
||||||
|
ThemeProperty[] properties = themePaths.stream()
|
||||||
|
.map(this::fetchThemeProperty)
|
||||||
|
.filter(Optional::isPresent)
|
||||||
|
.map(Optional::get)
|
||||||
|
.peek(themeProperty -> {
|
||||||
|
if (StringUtils.equals(activeThemeId, themeProperty.getId())) {
|
||||||
|
themeProperty.setActivated(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.toArray(ThemeProperty[]::new);
|
||||||
|
// Cache the themes
|
||||||
|
return Arrays.asList(properties);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to get themes", e);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch theme property
|
||||||
|
*
|
||||||
|
* @param themePath theme path must not be null
|
||||||
|
* @return an optional theme property
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public Optional<ThemeProperty> fetchThemeProperty(@NonNull Path themePath) {
|
||||||
|
Assert.notNull(themePath, "Theme path must not be null");
|
||||||
|
|
||||||
|
Optional<Path> optionalPath = fetchPropertyPath(themePath);
|
||||||
|
|
||||||
|
if (!optionalPath.isPresent()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
Path propertyPath = optionalPath.get();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get property content
|
||||||
|
String propertyContent = new String(Files.readAllBytes(propertyPath), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// Resolve the base properties
|
||||||
|
ThemeProperty themeProperty = propertyResolver.resolve(propertyContent);
|
||||||
|
|
||||||
|
// Resolve additional properties
|
||||||
|
themeProperty.setThemePath(themePath.toString());
|
||||||
|
themeProperty.setFolderName(themePath.getFileName().toString());
|
||||||
|
themeProperty.setHasOptions(hasOptions(themePath));
|
||||||
|
themeProperty.setActivated(false);
|
||||||
|
|
||||||
|
// Set screenshots
|
||||||
|
getScreenshotsFileName(themePath).ifPresent(screenshotsName ->
|
||||||
|
// TODO base url
|
||||||
|
themeProperty.setScreenshots(StringUtils.join(
|
||||||
|
"/themes/",
|
||||||
|
FilenameUtils.getBasename(themeProperty.getThemePath()),
|
||||||
|
"/",
|
||||||
|
screenshotsName)));
|
||||||
|
|
||||||
|
return Optional.of(themeProperty);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to load theme property file", e);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets screenshots file name.
|
||||||
|
*
|
||||||
|
* @param themePath theme path must not be null
|
||||||
|
* @return screenshots file name or null if the given theme path has not screenshots
|
||||||
|
* @throws IOException throws when listing files
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private Optional<String> getScreenshotsFileName(@NonNull Path themePath) throws IOException {
|
||||||
|
Assert.notNull(themePath, "Theme path must not be null");
|
||||||
|
|
||||||
|
try (Stream<Path> pathStream = Files.list(themePath)) {
|
||||||
|
return pathStream.filter(path -> Files.isRegularFile(path)
|
||||||
|
&& Files.isReadable(path)
|
||||||
|
&& FilenameUtils.getBasename(path.toString()).equalsIgnoreCase(THEME_SCREENSHOTS_NAME))
|
||||||
|
.findFirst()
|
||||||
|
.map(path -> path.getFileName().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets property path of nullable.
|
||||||
|
*
|
||||||
|
* @param themePath theme path.
|
||||||
|
* @return an optional property path
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private Optional<Path> fetchPropertyPath(@NonNull Path themePath) {
|
||||||
|
Assert.notNull(themePath, "Theme path must not be null");
|
||||||
|
|
||||||
|
for (String propertyPathName : THEME_PROPERTY_FILE_NAMES) {
|
||||||
|
Path propertyPath = themePath.resolve(propertyPathName);
|
||||||
|
|
||||||
|
log.debug("Attempting to find property file: [{}]", propertyPath);
|
||||||
|
if (Files.exists(propertyPath) && Files.isReadable(propertyPath)) {
|
||||||
|
log.debug("Found property file: [{}]", propertyPath);
|
||||||
|
return Optional.of(propertyPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("Property file was not found in [{}]", themePath);
|
||||||
|
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check existence of the options.
|
||||||
|
*
|
||||||
|
* @param themePath theme path must not be null
|
||||||
|
* @return true if it has options; false otherwise
|
||||||
|
*/
|
||||||
|
private boolean hasOptions(@NonNull Path themePath) {
|
||||||
|
Assert.notNull(themePath, "Path must not be null");
|
||||||
|
|
||||||
|
for (String optionsName : SETTINGS_NAMES) {
|
||||||
|
// Resolve the options path
|
||||||
|
Path optionsPath = themePath.resolve(optionsName);
|
||||||
|
|
||||||
|
log.debug("Check options file for path: [{}]", optionsPath);
|
||||||
|
|
||||||
|
if (Files.exists(optionsPath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package run.halo.app.theme;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yaml resolver.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public enum YamlResolver {
|
||||||
|
|
||||||
|
INSTANCE;
|
||||||
|
|
||||||
|
private final ObjectMapper yamlMapper;
|
||||||
|
|
||||||
|
YamlResolver() {
|
||||||
|
// create a default yaml mapper
|
||||||
|
yamlMapper = new ObjectMapper(new YAMLFactory());
|
||||||
|
yamlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get yaml mapper.
|
||||||
|
*
|
||||||
|
* @return non-null yaml mapper
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public ObjectMapper getYamlMapper() {
|
||||||
|
return yamlMapper;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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!} | ${error.error!}</title>
|
<title>${(error.status)!500} | ${(error.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!}</h2>
|
<h2>${(error.status)!500}</h2>
|
||||||
<h1 class="title">${error.error!}.</h1>
|
<h1 class="title">${(error.error)!'未知错误'}.</h1>
|
||||||
<p>${error.message!}</p>
|
<p>${(error.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>
|
||||||
|
|
Loading…
Reference in New Issue