diff --git a/src/main/java/run/halo/app/cache/AbstractStringCacheStore.java b/src/main/java/run/halo/app/cache/AbstractStringCacheStore.java index 22b30f592..f3e1cc0a4 100644 --- a/src/main/java/run/halo/app/cache/AbstractStringCacheStore.java +++ b/src/main/java/run/halo/app/cache/AbstractStringCacheStore.java @@ -25,7 +25,7 @@ public abstract class AbstractStringCacheStore extends AbstractCacheStore listAll() { + public List listAll() { return themeService.getThemes(); } @@ -91,13 +90,13 @@ public class ThemeController { @GetMapping("activation/template/custom/sheet") @ApiOperation("Gets custom sheet templates") - public Set customSheetTemplate() { + public List customSheetTemplate() { return themeService.listCustomTemplates(themeService.getActivatedThemeId(), ThemeService.CUSTOM_SHEET_PREFIX); } @GetMapping("activation/template/custom/post") @ApiOperation("Gets custom post templates") - public Set customPostTemplate() { + public List customPostTemplate() { return themeService.listCustomTemplates(themeService.getActivatedThemeId(), ThemeService.CUSTOM_POST_PREFIX); } diff --git a/src/main/java/run/halo/app/controller/content/ContentContentController.java b/src/main/java/run/halo/app/controller/content/ContentContentController.java index 57cd61824..2d0673144 100644 --- a/src/main/java/run/halo/app/controller/content/ContentContentController.java +++ b/src/main/java/run/halo/app/controller/content/ContentContentController.java @@ -83,19 +83,23 @@ public class ContentContentController { Model model) { if (optionService.getArchivesPrefix().equals(prefix)) { 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+}") diff --git a/src/main/java/run/halo/app/controller/core/CommonController.java b/src/main/java/run/halo/app/controller/core/CommonController.java index a3dbc482e..7aec776ea 100644 --- a/src/main/java/run/halo/app/controller/core/CommonController.java +++ b/src/main/java/run/halo/app/controller/core/CommonController.java @@ -26,6 +26,8 @@ import javax.servlet.http.HttpServletResponse; import java.util.Collections; import java.util.Map; +import static run.halo.app.model.support.HaloConst.DEFAULT_ERROR_PATH; + /** * Error page Controller * @@ -43,16 +45,12 @@ public class CommonController extends AbstractErrorController { 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 final ThemeService themeService; private final ErrorProperties errorProperties; - private final ErrorAttributes errorAttributes; - private final OptionService optionService; public CommonController(ThemeService themeService, @@ -61,7 +59,6 @@ public class CommonController extends AbstractErrorController { OptionService optionService) { super(errorAttributes); this.themeService = themeService; - this.errorAttributes = errorAttributes; this.errorProperties = serverProperties.getError(); this.optionService = optionService; } @@ -166,9 +163,9 @@ public class CommonController extends AbstractErrorController { } Throwable throwable = (Throwable) throwableObject; - log.error("Captured an exception", throwable); if (throwable instanceof NestedServletException) { + log.error("Captured an exception", throwable); Throwable rootCause = ((NestedServletException) throwable).getRootCause(); if (rootCause instanceof AbstractHaloException) { AbstractHaloException haloException = (AbstractHaloException) rootCause; @@ -177,6 +174,7 @@ public class CommonController extends AbstractErrorController { request.setAttribute("javax.servlet.error.message", haloException.getMessage()); } } else if (StringUtils.startsWithIgnoreCase(throwable.getMessage(), COULD_NOT_RESOLVE_VIEW_WITH_NAME_PREFIX)) { + log.debug("Captured an exception", throwable); request.setAttribute("javax.servlet.error.status_code", HttpStatus.NOT_FOUND.value()); NotFoundException viewNotFound = new NotFoundException("该路径没有对应的模板"); diff --git a/src/main/java/run/halo/app/handler/theme/config/impl/YamlThemePropertyResolver.java b/src/main/java/run/halo/app/handler/theme/config/impl/YamlThemePropertyResolver.java index a1af62bb1..d42e59050 100644 --- a/src/main/java/run/halo/app/handler/theme/config/impl/YamlThemePropertyResolver.java +++ b/src/main/java/run/halo/app/handler/theme/config/impl/YamlThemePropertyResolver.java @@ -1,12 +1,11 @@ package run.halo.app.handler.theme.config.impl; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import run.halo.app.handler.theme.config.ThemePropertyResolver; import run.halo.app.handler.theme.config.support.ThemeProperty; +import run.halo.app.theme.YamlResolver; import java.io.IOException; @@ -19,17 +18,11 @@ import java.io.IOException; @Service 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 - 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"); - return yamlMapper.readValue(content, ThemeProperty.class); + return YamlResolver.INSTANCE.getYamlMapper().readValue(content, ThemeProperty.class); } } diff --git a/src/main/java/run/halo/app/listener/StartedListener.java b/src/main/java/run/halo/app/listener/StartedListener.java index 40e87d6b9..ca8b8f11e 100644 --- a/src/main/java/run/halo/app/listener/StartedListener.java +++ b/src/main/java/run/halo/app/listener/StartedListener.java @@ -65,7 +65,7 @@ public class StartedListener implements ApplicationListener { + 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("static", themeBasePath); + configuration.setSharedVariable("settings", themeSettingService.listAsMapBy(themeService.getActivatedThemeId())); + 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"); } } diff --git a/src/main/java/run/halo/app/model/support/HaloConst.java b/src/main/java/run/halo/app/model/support/HaloConst.java index f5eafb01c..7ab107abb 100644 --- a/src/main/java/run/halo/app/model/support/HaloConst.java +++ b/src/main/java/run/halo/app/model/support/HaloConst.java @@ -50,6 +50,11 @@ public class HaloConst { */ public final static String DEFAULT_THEME_ID = "caicai_anatole"; + /** + * Default error path. + */ + public static final String DEFAULT_ERROR_PATH = "common/error/error"; + /** * Path separator. */ diff --git a/src/main/java/run/halo/app/service/ThemeService.java b/src/main/java/run/halo/app/service/ThemeService.java index 9aaa500da..158a48f2e 100644 --- a/src/main/java/run/halo/app/service/ThemeService.java +++ b/src/main/java/run/halo/app/service/ThemeService.java @@ -11,7 +11,6 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; import java.util.Optional; -import java.util.Set; /** * Theme service interface. @@ -30,6 +29,7 @@ public interface ThemeService { /** * Theme property file name. */ + @Deprecated String[] THEME_PROPERTY_FILE_NAMES = {"theme.yaml", "theme.yml"}; @@ -56,6 +56,7 @@ public interface ThemeService { /** * Theme screenshots name. */ + @Deprecated String THEME_SCREENSHOTS_NAME = "screenshot"; @@ -101,6 +102,7 @@ public interface ThemeService { * @return theme property */ @NonNull + @Deprecated ThemeProperty getThemeOfNonNullBy(@NonNull String themeId); /** @@ -110,7 +112,7 @@ public interface ThemeService { * @return a optional theme property */ @NonNull - Optional getThemeBy(@Nullable String themeId); + Optional fetchThemePropertyBy(@Nullable String themeId); /** * Gets all themes @@ -118,15 +120,7 @@ public interface ThemeService { * @return set of themes */ @NonNull - Set getThemes(); - - /** - * Lists theme folder by absolute path. - * - * @param absolutePath absolutePath - * @return List - */ - List listThemeFolder(@NonNull String absolutePath); + List getThemes(); /** * Lists theme folder by theme name. @@ -134,6 +128,7 @@ public interface ThemeService { * @param themeId theme id * @return List */ + @NonNull List listThemeFolderBy(@NonNull String themeId); /** @@ -143,7 +138,8 @@ public interface ThemeService { * @return a set of templates */ @Deprecated - Set listCustomTemplates(@NonNull String themeId); + @NonNull + List listCustomTemplates(@NonNull String themeId); /** * 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_ * @return a set of templates */ - Set listCustomTemplates(@NonNull String themeId, @NonNull String prefix); + @NonNull + List listCustomTemplates(@NonNull String themeId, @NonNull String prefix); /** * Judging whether template exists under the specified theme @@ -261,6 +258,14 @@ public interface ThemeService { @NonNull ThemeProperty getActivatedTheme(); + /** + * Fetch activated theme property. + * + * @return activated theme property + */ + @NonNull + Optional fetchActivatedTheme(); + /** * Actives a theme. * diff --git a/src/main/java/run/halo/app/service/impl/ThemeServiceImpl.java b/src/main/java/run/halo/app/service/impl/ThemeServiceImpl.java index 2be95a3cd..5b21bfa2b 100644 --- a/src/main/java/run/halo/app/service/impl/ThemeServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/ThemeServiceImpl.java @@ -19,7 +19,6 @@ import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.web.client.RestTemplate; import org.springframework.web.multipart.MultipartFile; 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.exception.*; 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.ThemeProperty; 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.service.OptionService; import run.halo.app.service.ThemeService; +import run.halo.app.theme.ThemeFileScanner; +import run.halo.app.theme.ThemePropertyScanner; import run.halo.app.utils.*; import java.io.IOException; @@ -45,12 +45,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; 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; /** @@ -63,23 +63,25 @@ import static run.halo.app.model.support.HaloConst.DEFAULT_THEME_ID; @Service public class ThemeServiceImpl implements ThemeService { - /** - * in seconds. - */ - protected static final long ACTIVATED_THEME_SYNC_INTERVAL = 5; /** * Theme work directory. */ private final Path themeWorkDir; + private final OptionService optionService; + private final AbstractStringCacheStore cacheStore; + private final ThemeConfigResolver themeConfigResolver; - private final ThemePropertyResolver themePropertyResolver; + private final RestTemplate restTemplate; + private final ApplicationEventPublisher eventPublisher; + /** * Activated theme id. */ + @Nullable private volatile String activatedThemeId; /** @@ -87,135 +89,91 @@ public class ThemeServiceImpl implements ThemeService { */ private volatile ThemeProperty activatedTheme; + private final AtomicReference activeThemeId = new AtomicReference<>(); + public ThemeServiceImpl(HaloProperties haloProperties, OptionService optionService, AbstractStringCacheStore cacheStore, ThemeConfigResolver themeConfigResolver, - ThemePropertyResolver themePropertyResolver, RestTemplate restTemplate, ApplicationEventPublisher eventPublisher) { this.optionService = optionService; this.cacheStore = cacheStore; this.themeConfigResolver = themeConfigResolver; - this.themePropertyResolver = themePropertyResolver; this.restTemplate = restTemplate; themeWorkDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER); 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 - public ThemeProperty getThemeOfNonNullBy(String themeId) { - return getThemeBy(themeId).orElseThrow(() -> new NotFoundException("没有找到 id 为 " + themeId + " 的主题").setErrorData(themeId)); + @NonNull + public ThemeProperty getThemeOfNonNullBy(@NonNull String themeId) { + return fetchThemePropertyBy(themeId).orElseThrow(() -> new NotFoundException(themeId + " 主题不存在或已删除!").setErrorData(themeId)); } @Override - public Optional getThemeBy(String themeId) { + @NonNull + public Optional fetchThemePropertyBy(String themeId) { if (StringUtils.isBlank(themeId)) { return Optional.empty(); } // Get all themes - Set themes = getThemes(); + List themes = getThemes(); // 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 - public Set getThemes() { - - Optional themePropertiesOptional = cacheStore.getAny(THEMES_CACHE_KEY, ThemeProperty[].class); - - if (themePropertiesOptional.isPresent()) { - // Convert to theme properties - ThemeProperty[] themeProperties = themePropertiesOptional.get(); - return new HashSet<>(Arrays.asList(themeProperties)); - } - - try (Stream pathStream = Files.list(getBasePath())) { - - // List and filter sub folders - List themePaths = pathStream.filter(path -> Files.isDirectory(path)) - .collect(Collectors.toList()); - - if (CollectionUtils.isEmpty(themePaths)) { - return Collections.emptySet(); - } - - // Get theme properties - Set themes = themePaths.stream().map(this::getProperty).collect(Collectors.toSet()); - + @NonNull + public List getThemes() { + ThemeProperty[] themeProperties = cacheStore.getAny(THEMES_CACHE_KEY, ThemeProperty[].class).orElseGet(() -> { + List properties = ThemePropertyScanner.INSTANCE.scan(getBasePath(), getActivatedThemeId()); // Cache the themes - cacheStore.putAny(THEMES_CACHE_KEY, themes); - - return themes; - } catch (IOException e) { - throw new ServiceException("Failed to get themes", e); - } + cacheStore.putAny(THEMES_CACHE_KEY, properties); + return properties.toArray(new ThemeProperty[0]); + }); + return Arrays.asList(themeProperties); } @Override - public List listThemeFolder(String absolutePath) { - return listThemeFileTree(Paths.get(absolutePath)); + @NonNull + public List listThemeFolderBy(@NonNull String themeId) { + return fetchThemePropertyBy(themeId) + .map(themeProperty -> ThemeFileScanner.INSTANCE.scan(themeProperty.getThemePath())) + .orElse(Collections.emptyList()); } @Override - public List listThemeFolderBy(String themeId) { - // Get the theme property - ThemeProperty themeProperty = getThemeOfNonNullBy(themeId); - - // List theme file as tree view - return listThemeFolder(themeProperty.getThemePath()); + @NonNull + public List listCustomTemplates(@NonNull String themeId) { + return listCustomTemplates(themeId, CUSTOM_SHEET_PREFIX); } @Override - public Set listCustomTemplates(String themeId) { - // Get the theme path - Path themePath = Paths.get(getThemeOfNonNullBy(themeId).getThemePath()); - - try (Stream 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 listCustomTemplates(String themeId, String prefix) { - // Get the theme path - Path themePath = Paths.get(getThemeOfNonNullBy(themeId).getThemePath()); - - try (Stream 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); - } + @NonNull + public List listCustomTemplates(@NonNull String themeId, @NonNull String prefix) { + return fetchThemePropertyBy(themeId).map(themeProperty -> { + // Get the theme path + Path themePath = Paths.get(themeProperty.getThemePath()); + try (Stream 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); + }) + .distinct() + .collect(Collectors.toList()); + } catch (Exception e) { + throw new ServiceException("Failed to list files of path " + themePath, e); + } + }).orElse(Collections.emptyList()); } @Override @@ -224,19 +182,19 @@ public class ThemeServiceImpl implements ThemeService { return false; } - // Resolve template path - Path templatePath = Paths.get(getActivatedTheme().getThemePath(), template); - - // Check the directory - checkDirectory(templatePath.toString()); - - // Check existence - return Files.exists(templatePath); + return fetchActivatedTheme().map(themeProperty -> { + // Resolve template path + Path templatePath = Paths.get(themeProperty.getThemePath(), template); + // Check the directory + checkDirectory(templatePath.toString()); + // Check existence + return Files.exists(templatePath); + }).orElse(false); } @Override public boolean themeExists(String themeId) { - return getThemeBy(themeId).isPresent(); + return fetchThemePropertyBy(themeId).isPresent(); } @Override @@ -245,7 +203,7 @@ public class ThemeServiceImpl implements ThemeService { } @Override - public String getTemplateContent(String absolutePath) { + public String getTemplateContent(@NonNull String absolutePath) { // Check the path checkDirectory(absolutePath); @@ -259,7 +217,8 @@ public class ThemeServiceImpl implements ThemeService { } @Override - public String getTemplateContent(String themeId, String absolutePath) { + @NonNull + public String getTemplateContent(@NonNull String themeId, @NonNull String absolutePath) { checkDirectory(themeId, absolutePath); // Read file @@ -272,7 +231,7 @@ public class ThemeServiceImpl implements ThemeService { } @Override - public void saveTemplateContent(String absolutePath, String content) { + public void saveTemplateContent(@NonNull String absolutePath, String content) { // Check the path checkDirectory(absolutePath); @@ -286,7 +245,7 @@ public class ThemeServiceImpl implements ThemeService { } @Override - public void saveTemplateContent(String themeId, String absolutePath, String content) { + public void saveTemplateContent(@NonNull String themeId, @NonNull String absolutePath, String content) { // Check the path checkDirectory(themeId, absolutePath); @@ -300,7 +259,7 @@ public class ThemeServiceImpl implements ThemeService { } @Override - public void deleteTheme(String themeId) { + public void deleteTheme(@NonNull String themeId) { // Get the theme property ThemeProperty themeProperty = getThemeOfNonNullBy(themeId); @@ -320,7 +279,8 @@ public class ThemeServiceImpl implements ThemeService { } @Override - public List fetchConfig(String themeId) { + @NonNull + public List fetchConfig(@NonNull String themeId) { Assert.hasText(themeId, "Theme id must not be blank"); // Get theme property @@ -358,10 +318,9 @@ public class ThemeServiceImpl implements ThemeService { @Override public String render(String pageName) { - // Get activated theme - ThemeProperty activatedTheme = getActivatedTheme(); - // Build render url - return String.format(RENDER_TEMPLATE, activatedTheme.getFolderName(), pageName); + return fetchActivatedTheme() + .map(themeProperty -> String.format(RENDER_TEMPLATE, themeProperty.getFolderName(), pageName)) + .orElse(DEFAULT_ERROR_PATH); } @Override @@ -373,6 +332,7 @@ public class ThemeServiceImpl implements ThemeService { } @Override + @NonNull public String getActivatedThemeId() { if (activatedThemeId == null) { synchronized (this) { @@ -381,11 +341,11 @@ public class ThemeServiceImpl implements ThemeService { } } } - return activatedThemeId; } @Override + @NonNull public ThemeProperty getActivatedTheme() { if (activatedTheme == null) { synchronized (this) { @@ -395,10 +355,15 @@ public class ThemeServiceImpl implements ThemeService { } } } - return activatedTheme; } + @Override + @NonNull + public Optional fetchActivatedTheme() { + return fetchThemePropertyBy(getActivatedThemeId()); + } + /** * Sets activated theme. * @@ -410,7 +375,8 @@ public class ThemeServiceImpl implements ThemeService { } @Override - public ThemeProperty activateTheme(String themeId) { + @NonNull + public ThemeProperty activateTheme(@NonNull String themeId) { // Check existence of the theme ThemeProperty themeProperty = getThemeOfNonNullBy(themeId); @@ -430,7 +396,8 @@ public class ThemeServiceImpl implements ThemeService { } @Override - public ThemeProperty upload(MultipartFile file) { + @NonNull + public ThemeProperty upload(@NonNull MultipartFile file) { Assert.notNull(file, "Multipart file must not be null"); if (!StringUtils.endsWithIgnoreCase(file.getOriginalFilename(), ".zip")) { @@ -443,7 +410,7 @@ public class ThemeServiceImpl implements ThemeService { try { // Create temp directory tempPath = FileUtils.createTempDirectory(); - String basename = FilenameUtils.getBasename(file.getOriginalFilename()); + String basename = FilenameUtils.getBasename(Objects.requireNonNull(file.getOriginalFilename())); Path themeTempPath = tempPath.resolve(basename); // Check directory traversal @@ -455,10 +422,12 @@ public class ThemeServiceImpl implements ThemeService { // Unzip to temp path FileUtils.unzip(zis, themeTempPath); + Path themePath = FileUtils.tryToSkipZipParentFolder(themeTempPath); + // Go to the base folder and add the theme into system - return add(FileUtils.tryToSkipZipParentFolder(themeTempPath)); + return add(themePath); } catch (IOException e) { - throw new ServiceException("上传主题失败: " + file.getOriginalFilename(), e); + throw new ServiceException("主题上传失败: " + file.getOriginalFilename(), e); } finally { // Close zip input stream FileUtils.closeQuietly(zis); @@ -468,7 +437,8 @@ public class ThemeServiceImpl implements ThemeService { } @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.isTrue(Files.isDirectory(themeTmpPath), "Theme temporary path must be a directory"); @@ -509,7 +479,7 @@ public class ThemeServiceImpl implements ThemeService { } @Override - public ThemeProperty fetch(String uri) { + public ThemeProperty fetch(@NonNull String uri) { Assert.hasText(uri, "Theme remote uri must not be blank"); 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"); // Get branch @@ -637,7 +608,9 @@ public class ThemeServiceImpl implements ThemeService { 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()); @@ -728,74 +701,6 @@ public class ThemeServiceImpl implements ThemeService { 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 listThemeFileTree(@NonNull Path topPath) { - Assert.notNull(topPath, "Top path must not be null"); - - // Check file type - if (!Files.isDirectory(topPath)) { - return null; - } - - try (Stream pathStream = Files.list(topPath)) { - List 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. * @@ -818,82 +723,6 @@ public class ThemeServiceImpl implements ThemeService { FileUtils.checkDirectoryTraversal(themeProperty.getThemePath(), absoluteName); } - /** - * Gets property path of nullable. - * - * @param themePath theme path. - * @return an optional property path - */ - @NonNull - private Optional 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 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. * @@ -902,48 +731,8 @@ public class ThemeServiceImpl implements ThemeService { */ @NonNull 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 getScreenshotsFileName(@NonNull Path themePath) throws IOException { - Assert.notNull(themePath, "Theme path must not be null"); - - try (Stream 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; - } } diff --git a/src/main/java/run/halo/app/theme/ThemeFileScanner.java b/src/main/java/run/halo/app/theme/ThemeFileScanner.java new file mode 100644 index 000000000..965788bc1 --- /dev/null +++ b/src/main/java/run/halo/app/theme/ThemeFileScanner.java @@ -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 a list of theme files + */ + @NonNull + public List 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 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 pathStream = Files.list(rootPath)) { + List 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; + } +} diff --git a/src/main/java/run/halo/app/theme/ThemePropertyScanner.java b/src/main/java/run/halo/app/theme/ThemePropertyScanner.java new file mode 100644 index 000000000..ec226b238 --- /dev/null +++ b/src/main/java/run/halo/app/theme/ThemePropertyScanner.java @@ -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 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 pathStream = Files.list(themePath)) { + // List and filter sub folders + List 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 fetchThemeProperty(@NonNull Path themePath) { + Assert.notNull(themePath, "Theme path must not be null"); + + Optional 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 getScreenshotsFileName(@NonNull Path themePath) throws IOException { + Assert.notNull(themePath, "Theme path must not be null"); + + try (Stream 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 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; + } +} diff --git a/src/main/java/run/halo/app/theme/YamlResolver.java b/src/main/java/run/halo/app/theme/YamlResolver.java new file mode 100644 index 000000000..c4f4546ea --- /dev/null +++ b/src/main/java/run/halo/app/theme/YamlResolver.java @@ -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; + } +} diff --git a/src/main/java/run/halo/app/utils/HttpClientUtils.java b/src/main/java/run/halo/app/utils/HttpClientUtils.java index 7d494fcf0..afe21667e 100644 --- a/src/main/java/run/halo/app/utils/HttpClientUtils.java +++ b/src/main/java/run/halo/app/utils/HttpClientUtils.java @@ -45,9 +45,9 @@ public class HttpClientUtils { .build(); return HttpClients.custom() - .setSSLContext(sslContext) - .setSSLHostnameVerifier(new NoopHostnameVerifier()) - .setDefaultRequestConfig(getRequestConfig(timeout)) + .setSSLContext(sslContext) + .setSSLHostnameVerifier(new NoopHostnameVerifier()) + .setDefaultRequestConfig(getRequestConfig(timeout)) .build(); } @@ -59,10 +59,10 @@ public class HttpClientUtils { */ private static RequestConfig getRequestConfig(int timeout) { return RequestConfig.custom() - .setConnectTimeout(timeout) - .setConnectionRequestTimeout(timeout) - .setSocketTimeout(timeout) - .build(); + .setConnectTimeout(timeout) + .setConnectionRequestTimeout(timeout) + .setSocketTimeout(timeout) + .build(); } diff --git a/src/main/resources/templates/common/error/error.ftl b/src/main/resources/templates/common/error/error.ftl index 4c549ccc9..aa0fc4e00 100644 --- a/src/main/resources/templates/common/error/error.ftl +++ b/src/main/resources/templates/common/error/error.ftl @@ -5,7 +5,7 @@ - ${error.status!} | ${error.error!} + ${(error.status)!500} | ${(error.error)!'未知错误'}