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
John Niang 2020-06-08 10:26:32 +08:00 committed by GitHub
parent 7a8c5af258
commit ccea5ed6c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 538 additions and 393 deletions

View File

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

View File

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

View File

@ -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+}")

View File

@ -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("该路径没有对应的模板");

View File

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

View File

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

View File

@ -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,21 +128,24 @@ 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();
try {
configuration.setSharedVariable("theme", activatedTheme);
String themeBasePath = (optionService.isEnabledAbsolutePath() ? optionService.getBlogBaseUrl() : "") + "/themes/" + activatedTheme.getFolderName(); // TODO: It will be removed in future versions
configuration.setSharedVariable("static", themeBasePath);
configuration.setSharedVariable("theme", activatedTheme); configuration.setSharedVariable("theme_base", themeBasePath);
// TODO: It will be removed in future versions configuration.setSharedVariable("settings", themeSettingService.listAsMapBy(themeService.getActivatedThemeId()));
configuration.setSharedVariable("static", themeBasePath); log.debug("Loaded theme and settings");
} catch (TemplateModelException e) {
log.error("Failed to set shared variable!", e);
}
});
configuration.setSharedVariable("theme_base", themeBasePath);
configuration.setSharedVariable("settings", themeSettingService.listAsMapBy(themeService.getActivatedThemeId()));
log.debug("Loaded theme and settings");
} }
} }

View File

@ -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.
*/ */

View File

@ -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.
* *

View File

@ -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,135 +89,91 @@ 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
// Get the theme path public List<String> listCustomTemplates(@NonNull String themeId, @NonNull String prefix) {
Path themePath = Paths.get(getThemeOfNonNullBy(themeId).getThemePath()); return fetchThemePropertyBy(themeId).map(themeProperty -> {
// Get the theme path
try (Stream<Path> pathStream = Files.list(themePath)) { Path themePath = Paths.get(themeProperty.getThemePath());
return pathStream.filter(path -> StringUtils.startsWithIgnoreCase(path.getFileName().toString(), CUSTOM_SHEET_PREFIX)) try (Stream<Path> pathStream = Files.list(themePath)) {
.map(path -> { return pathStream.filter(path -> StringUtils.startsWithIgnoreCase(path.getFileName().toString(), prefix))
// Remove prefix .map(path -> {
String customTemplate = StringUtils.removeStartIgnoreCase(path.getFileName().toString(), CUSTOM_SHEET_PREFIX); // Remove prefix
// Remove suffix String customTemplate = StringUtils.removeStartIgnoreCase(path.getFileName().toString(), prefix);
return StringUtils.removeEndIgnoreCase(customTemplate, HaloConst.SUFFIX_FTL); // Remove suffix
}) return StringUtils.removeEndIgnoreCase(customTemplate, HaloConst.SUFFIX_FTL);
.collect(Collectors.toSet()); })
} catch (IOException e) { .distinct()
throw new ServiceException("Failed to list files of path " + themePath.toString(), e); .collect(Collectors.toList());
} } catch (Exception e) {
} throw new ServiceException("Failed to list files of path " + themePath, e);
}
@Override }).orElse(Collections.emptyList());
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)) {
return pathStream.filter(path -> StringUtils.startsWithIgnoreCase(path.getFileName().toString(), prefix))
.map(path -> {
// Remove prefix
String customTemplate = StringUtils.removeStartIgnoreCase(path.getFileName().toString(), 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 @Override
@ -224,19 +182,19 @@ public class ThemeServiceImpl implements ThemeService {
return false; return false;
} }
// Resolve template path return fetchActivatedTheme().map(themeProperty -> {
Path templatePath = Paths.get(getActivatedTheme().getThemePath(), template); // Resolve template path
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;
}
} }

View File

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

View File

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

View File

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

View File

@ -45,9 +45,9 @@ public class HttpClientUtils {
.build(); .build();
return HttpClients.custom() return HttpClients.custom()
.setSSLContext(sslContext) .setSSLContext(sslContext)
.setSSLHostnameVerifier(new NoopHostnameVerifier()) .setSSLHostnameVerifier(new NoopHostnameVerifier())
.setDefaultRequestConfig(getRequestConfig(timeout)) .setDefaultRequestConfig(getRequestConfig(timeout))
.build(); .build();
} }
@ -59,10 +59,10 @@ public class HttpClientUtils {
*/ */
private static RequestConfig getRequestConfig(int timeout) { private static RequestConfig getRequestConfig(int timeout) {
return RequestConfig.custom() return RequestConfig.custom()
.setConnectTimeout(timeout) .setConnectTimeout(timeout)
.setConnectionRequestTimeout(timeout) .setConnectionRequestTimeout(timeout)
.setSocketTimeout(timeout) .setSocketTimeout(timeout)
.build(); .build();
} }

View File

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