mirror of https://github.com/halo-dev/halo
Refactor theme sacn (#869)
* Provide theme collector * Refactor theme property scan * Refactor ThemePropertyScanner * Rectify return type * Change activated theme fetch strategy * Refactor theme service * Reformat unexpected codes * Remove unused imports Co-authored-by: johnniang <johnniang@fastmail.com>pull/900/head
parent
7a8c5af258
commit
ccea5ed6c3
|
@ -25,7 +25,7 @@ public abstract class AbstractStringCacheStore extends AbstractCacheStore<String
|
|||
cacheWrapper = JsonUtils.jsonToObject(json, CacheWrapper.class);
|
||||
} 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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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+}")
|
||||
|
|
|
@ -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("该路径没有对应的模板");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.exception.ServiceException;
|
||||
import run.halo.app.model.support.ThemeFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static run.halo.app.service.ThemeService.CAN_EDIT_SUFFIX;
|
||||
|
||||
/**
|
||||
* Theme file scanner.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
public enum ThemeFileScanner {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
/**
|
||||
* Lists theme folder by absolute path.
|
||||
*
|
||||
* @param absolutePath absolutePath
|
||||
* @return List<ThemeFile> a list of theme files
|
||||
*/
|
||||
@NonNull
|
||||
public List<ThemeFile> scan(@NonNull String absolutePath) {
|
||||
Assert.hasText(absolutePath, "Absolute path must not be blank");
|
||||
|
||||
return scan(Paths.get(absolutePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists theme files as tree view.
|
||||
*
|
||||
* @param rootPath theme root path must not be null
|
||||
* @return theme file tree view
|
||||
*/
|
||||
@NonNull
|
||||
private List<ThemeFile> scan(@NonNull Path rootPath) {
|
||||
Assert.notNull(rootPath, "Root path must not be null");
|
||||
|
||||
// Check file type
|
||||
if (!Files.isDirectory(rootPath)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try (Stream<Path> pathStream = Files.list(rootPath)) {
|
||||
List<ThemeFile> themeFiles = new LinkedList<>();
|
||||
|
||||
pathStream.forEach(path -> {
|
||||
// Build theme file
|
||||
ThemeFile themeFile = new ThemeFile();
|
||||
themeFile.setName(path.getFileName().toString());
|
||||
themeFile.setPath(path.toString());
|
||||
themeFile.setIsFile(Files.isRegularFile(path));
|
||||
themeFile.setEditable(isEditable(path));
|
||||
|
||||
if (Files.isDirectory(path)) {
|
||||
themeFile.setNode(scan(path));
|
||||
}
|
||||
|
||||
// Add to theme files
|
||||
themeFiles.add(themeFile);
|
||||
});
|
||||
|
||||
// Sort with isFile param
|
||||
themeFiles.sort(new ThemeFile());
|
||||
|
||||
return themeFiles;
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("Failed to list sub files", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given path is editable.
|
||||
*
|
||||
* @param path must not be null
|
||||
* @return true if the given path is editable; false otherwise
|
||||
*/
|
||||
private boolean isEditable(@NonNull Path path) {
|
||||
Assert.notNull(path, "Path must not be null");
|
||||
|
||||
boolean isEditable = Files.isReadable(path) && Files.isWritable(path);
|
||||
|
||||
if (!isEditable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check suffix
|
||||
for (String suffix : CAN_EDIT_SUFFIX) {
|
||||
if (path.toString().endsWith(suffix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import run.halo.app.handler.theme.config.ThemePropertyResolver;
|
||||
import run.halo.app.handler.theme.config.impl.YamlThemePropertyResolver;
|
||||
import run.halo.app.handler.theme.config.support.ThemeProperty;
|
||||
import run.halo.app.utils.FilenameUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static run.halo.app.service.ThemeService.SETTINGS_NAMES;
|
||||
|
||||
/**
|
||||
* Theme property scanner.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Slf4j
|
||||
public enum ThemePropertyScanner {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
private final ThemePropertyResolver propertyResolver = new YamlThemePropertyResolver();
|
||||
|
||||
/**
|
||||
* Theme property file name.
|
||||
*/
|
||||
private static final String[] THEME_PROPERTY_FILE_NAMES = {"theme.yaml", "theme.yml"};
|
||||
|
||||
/**
|
||||
* Theme screenshots name.
|
||||
*/
|
||||
private static final String THEME_SCREENSHOTS_NAME = "screenshot";
|
||||
|
||||
/**
|
||||
* Scan theme properties.
|
||||
*
|
||||
* @param themePath them path must not be null
|
||||
* @return a list of them property
|
||||
*/
|
||||
@NonNull
|
||||
public List<ThemeProperty> scan(@NonNull Path themePath, @Nullable String activeThemeId) {
|
||||
// create if absent
|
||||
try {
|
||||
if (Files.notExists(themePath)) {
|
||||
Files.createDirectories(themePath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to create directory: " + themePath, e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try (Stream<Path> pathStream = Files.list(themePath)) {
|
||||
// List and filter sub folders
|
||||
List<Path> themePaths = pathStream.filter(path -> Files.isDirectory(path))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (CollectionUtils.isEmpty(themePaths)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Get theme properties
|
||||
ThemeProperty[] properties = themePaths.stream()
|
||||
.map(this::fetchThemeProperty)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.peek(themeProperty -> {
|
||||
if (StringUtils.equals(activeThemeId, themeProperty.getId())) {
|
||||
themeProperty.setActivated(true);
|
||||
}
|
||||
})
|
||||
.toArray(ThemeProperty[]::new);
|
||||
// Cache the themes
|
||||
return Arrays.asList(properties);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to get themes", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch theme property
|
||||
*
|
||||
* @param themePath theme path must not be null
|
||||
* @return an optional theme property
|
||||
*/
|
||||
@NonNull
|
||||
public Optional<ThemeProperty> fetchThemeProperty(@NonNull Path themePath) {
|
||||
Assert.notNull(themePath, "Theme path must not be null");
|
||||
|
||||
Optional<Path> optionalPath = fetchPropertyPath(themePath);
|
||||
|
||||
if (!optionalPath.isPresent()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Path propertyPath = optionalPath.get();
|
||||
|
||||
try {
|
||||
// Get property content
|
||||
String propertyContent = new String(Files.readAllBytes(propertyPath), StandardCharsets.UTF_8);
|
||||
|
||||
// Resolve the base properties
|
||||
ThemeProperty themeProperty = propertyResolver.resolve(propertyContent);
|
||||
|
||||
// Resolve additional properties
|
||||
themeProperty.setThemePath(themePath.toString());
|
||||
themeProperty.setFolderName(themePath.getFileName().toString());
|
||||
themeProperty.setHasOptions(hasOptions(themePath));
|
||||
themeProperty.setActivated(false);
|
||||
|
||||
// Set screenshots
|
||||
getScreenshotsFileName(themePath).ifPresent(screenshotsName ->
|
||||
// TODO base url
|
||||
themeProperty.setScreenshots(StringUtils.join(
|
||||
"/themes/",
|
||||
FilenameUtils.getBasename(themeProperty.getThemePath()),
|
||||
"/",
|
||||
screenshotsName)));
|
||||
|
||||
return Optional.of(themeProperty);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to load theme property file", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets screenshots file name.
|
||||
*
|
||||
* @param themePath theme path must not be null
|
||||
* @return screenshots file name or null if the given theme path has not screenshots
|
||||
* @throws IOException throws when listing files
|
||||
*/
|
||||
@NonNull
|
||||
private Optional<String> getScreenshotsFileName(@NonNull Path themePath) throws IOException {
|
||||
Assert.notNull(themePath, "Theme path must not be null");
|
||||
|
||||
try (Stream<Path> pathStream = Files.list(themePath)) {
|
||||
return pathStream.filter(path -> Files.isRegularFile(path)
|
||||
&& Files.isReadable(path)
|
||||
&& FilenameUtils.getBasename(path.toString()).equalsIgnoreCase(THEME_SCREENSHOTS_NAME))
|
||||
.findFirst()
|
||||
.map(path -> path.getFileName().toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets property path of nullable.
|
||||
*
|
||||
* @param themePath theme path.
|
||||
* @return an optional property path
|
||||
*/
|
||||
@NonNull
|
||||
private Optional<Path> fetchPropertyPath(@NonNull Path themePath) {
|
||||
Assert.notNull(themePath, "Theme path must not be null");
|
||||
|
||||
for (String propertyPathName : THEME_PROPERTY_FILE_NAMES) {
|
||||
Path propertyPath = themePath.resolve(propertyPathName);
|
||||
|
||||
log.debug("Attempting to find property file: [{}]", propertyPath);
|
||||
if (Files.exists(propertyPath) && Files.isReadable(propertyPath)) {
|
||||
log.debug("Found property file: [{}]", propertyPath);
|
||||
return Optional.of(propertyPath);
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("Property file was not found in [{}]", themePath);
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check existence of the options.
|
||||
*
|
||||
* @param themePath theme path must not be null
|
||||
* @return true if it has options; false otherwise
|
||||
*/
|
||||
private boolean hasOptions(@NonNull Path themePath) {
|
||||
Assert.notNull(themePath, "Path must not be null");
|
||||
|
||||
for (String optionsName : SETTINGS_NAMES) {
|
||||
// Resolve the options path
|
||||
Path optionsPath = themePath.resolve(optionsName);
|
||||
|
||||
log.debug("Check options file for path: [{}]", optionsPath);
|
||||
|
||||
if (Files.exists(optionsPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
/**
|
||||
* Yaml resolver.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
public enum YamlResolver {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
private final ObjectMapper yamlMapper;
|
||||
|
||||
YamlResolver() {
|
||||
// create a default yaml mapper
|
||||
yamlMapper = new ObjectMapper(new YAMLFactory());
|
||||
yamlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get yaml mapper.
|
||||
*
|
||||
* @return non-null yaml mapper
|
||||
*/
|
||||
@NonNull
|
||||
public ObjectMapper getYamlMapper() {
|
||||
return yamlMapper;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue