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);
} catch (IOException e) {
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);
}

View File

@ -15,7 +15,6 @@ import run.halo.app.service.ThemeSettingService;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Theme controller.
@ -45,7 +44,7 @@ public class ThemeController {
@GetMapping
@ApiOperation("Lists all themes")
public Set<ThemeProperty> listAll() {
public List<ThemeProperty> listAll() {
return themeService.getThemes();
}
@ -91,13 +90,13 @@ public class ThemeController {
@GetMapping("activation/template/custom/sheet")
@ApiOperation("Gets custom sheet templates")
public Set<String> customSheetTemplate() {
public List<String> customSheetTemplate() {
return themeService.listCustomTemplates(themeService.getActivatedThemeId(), ThemeService.CUSTOM_SHEET_PREFIX);
}
@GetMapping("activation/template/custom/post")
@ApiOperation("Gets custom post templates")
public Set<String> customPostTemplate() {
public List<String> customPostTemplate() {
return themeService.listCustomTemplates(themeService.getActivatedThemeId(), ThemeService.CUSTOM_POST_PREFIX);
}

View File

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

View File

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

View File

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

View File

@ -65,7 +65,7 @@ public class StartedListener implements ApplicationListener<ApplicationStartedEv
try {
this.migrate();
} catch (SQLException e) {
e.printStackTrace();
log.error("Failed to migrate database!", e);
}
this.initThemes();
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.ThemeUpdatedEvent;
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.SeoProperties;
import run.halo.app.model.support.HaloConst;
@ -101,7 +100,8 @@ public class FreemarkerConfigAwareListener {
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("context", context);
@ -109,15 +109,15 @@ public class FreemarkerConfigAwareListener {
configuration.setSharedVariable("globalAbsolutePathEnabled", optionService.isEnabledAbsolutePath());
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("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("rss_url", optionService.getBlogBaseUrl() + "/rss.xml");
configuration.setSharedVariable("atom_url", optionService.getBlogBaseUrl() + "/atom.xml");
configuration.setSharedVariable("sitemap_xml_url", optionService.getBlogBaseUrl() + "/sitemap.xml");
configuration.setSharedVariable("sitemap_html_url", optionService.getBlogBaseUrl() + "/sitemap.html");
configuration.setSharedVariable("rss_url", blogBaseUrl + "/rss.xml");
configuration.setSharedVariable("atom_url", blogBaseUrl + "/atom.xml");
configuration.setSharedVariable("sitemap_xml_url", blogBaseUrl + "/sitemap.xml");
configuration.setSharedVariable("sitemap_html_url", blogBaseUrl + "/sitemap.html");
configuration.setSharedVariable("links_url", context + optionService.getLinksPrefix());
configuration.setSharedVariable("photos_url", context + optionService.getPhotosPrefix());
configuration.setSharedVariable("journals_url", context + optionService.getJournalsPrefix());
@ -128,21 +128,24 @@ public class FreemarkerConfigAwareListener {
log.debug("Loaded options");
}
private void loadThemeConfig() throws TemplateModelException {
private void loadThemeConfig() {
// 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("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");
}
}

View File

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

View File

@ -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<ThemeProperty> getThemeBy(@Nullable String themeId);
Optional<ThemeProperty> fetchThemePropertyBy(@Nullable String themeId);
/**
* Gets all themes
@ -118,15 +120,7 @@ public interface ThemeService {
* @return set of themes
*/
@NonNull
Set<ThemeProperty> getThemes();
/**
* Lists theme folder by absolute path.
*
* @param absolutePath absolutePath
* @return List<ThemeFile>
*/
List<ThemeFile> listThemeFolder(@NonNull String absolutePath);
List<ThemeProperty> getThemes();
/**
* Lists theme folder by theme name.
@ -134,6 +128,7 @@ public interface ThemeService {
* @param themeId theme id
* @return List<ThemeFile>
*/
@NonNull
List<ThemeFile> listThemeFolderBy(@NonNull String themeId);
/**
@ -143,7 +138,8 @@ public interface ThemeService {
* @return a set of templates
*/
@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
@ -152,7 +148,8 @@ public interface ThemeService {
* @param prefix post_ or sheet_
* @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
@ -261,6 +258,14 @@ public interface ThemeService {
@NonNull
ThemeProperty getActivatedTheme();
/**
* Fetch activated theme property.
*
* @return activated theme property
*/
@NonNull
Optional<ThemeProperty> fetchActivatedTheme();
/**
* Actives a theme.
*

View File

@ -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<String> 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<ThemeProperty> getThemeBy(String themeId) {
@NonNull
public Optional<ThemeProperty> fetchThemePropertyBy(String themeId) {
if (StringUtils.isBlank(themeId)) {
return Optional.empty();
}
// Get all themes
Set<ThemeProperty> themes = getThemes();
List<ThemeProperty> 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<ThemeProperty> getThemes() {
Optional<ThemeProperty[]> 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<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());
@NonNull
public List<ThemeProperty> getThemes() {
ThemeProperty[] themeProperties = cacheStore.getAny(THEMES_CACHE_KEY, ThemeProperty[].class).orElseGet(() -> {
List<ThemeProperty> 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<ThemeFile> listThemeFolder(String absolutePath) {
return listThemeFileTree(Paths.get(absolutePath));
@NonNull
public List<ThemeFile> listThemeFolderBy(@NonNull String themeId) {
return fetchThemePropertyBy(themeId)
.map(themeProperty -> ThemeFileScanner.INSTANCE.scan(themeProperty.getThemePath()))
.orElse(Collections.emptyList());
}
@Override
public List<ThemeFile> listThemeFolderBy(String themeId) {
// Get the theme property
ThemeProperty themeProperty = getThemeOfNonNullBy(themeId);
// List theme file as tree view
return listThemeFolder(themeProperty.getThemePath());
@NonNull
public List<String> listCustomTemplates(@NonNull String themeId) {
return listCustomTemplates(themeId, CUSTOM_SHEET_PREFIX);
}
@Override
public Set<String> listCustomTemplates(String themeId) {
// 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(), CUSTOM_SHEET_PREFIX))
.map(path -> {
// Remove prefix
String customTemplate = StringUtils.removeStartIgnoreCase(path.getFileName().toString(), CUSTOM_SHEET_PREFIX);
// Remove suffix
return StringUtils.removeEndIgnoreCase(customTemplate, HaloConst.SUFFIX_FTL);
})
.collect(Collectors.toSet());
} catch (IOException e) {
throw new ServiceException("Failed to list files of path " + themePath.toString(), e);
}
}
@Override
public Set<String> listCustomTemplates(String themeId, String prefix) {
// Get the theme path
Path themePath = Paths.get(getThemeOfNonNullBy(themeId).getThemePath());
try (Stream<Path> pathStream = Files.list(themePath)) {
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<String> listCustomTemplates(@NonNull String themeId, @NonNull String prefix) {
return fetchThemePropertyBy(themeId).map(themeProperty -> {
// Get the theme path
Path themePath = Paths.get(themeProperty.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);
})
.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<Group> fetchConfig(String themeId) {
@NonNull
public List<Group> 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<ThemeProperty> 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<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.
*
@ -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<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.
*
@ -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<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();
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();
}

View File

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