Refactor ThemeService

pull/146/head
johnniang 2019-04-09 13:57:24 +08:00
parent 81dea867eb
commit 67409cb792
6 changed files with 184 additions and 179 deletions

View File

@ -99,7 +99,7 @@ public class StartedListener implements ApplicationListener<ApplicationStartedEv
*/ */
private void cacheActiveTheme() { private void cacheActiveTheme() {
try { try {
configuration.setSharedVariable("themeName", themeService.getActivatedTheme()); configuration.setSharedVariable("themeName", themeService.getActivatedThemeId());
} catch (TemplateModelException e) { } catch (TemplateModelException e) {
log.error("", e); log.error("", e);
} }

View File

@ -21,7 +21,7 @@ public class ThemeFile {
private Boolean isFile; private Boolean isFile;
private Boolean canEdit; private Boolean editable;
private List<ThemeFile> node; private List<ThemeFile> node;
} }

View File

@ -47,24 +47,23 @@ public interface ThemeService {
* @param absolutePath absolutePath * @param absolutePath absolutePath
* @return List<ThemeFile> * @return List<ThemeFile>
*/ */
@Deprecated
List<ThemeFile> listThemeFolder(@NonNull String absolutePath); List<ThemeFile> listThemeFolder(@NonNull String absolutePath);
/** /**
* Lists theme folder by theme name. * Lists theme folder by theme name.
* *
* @param theme theme * @param themeId theme id
* @return List<ThemeFile> * @return List<ThemeFile>
*/ */
List<ThemeFile> listThemeFolderBy(@NonNull String theme); List<ThemeFile> listThemeFolderBy(@NonNull String themeId);
/** /**
* Gets custom template, such as page_xxx.ftl, and xxx will be template name * Gets custom template, such as page_xxx.ftl, and xxx will be template name
* *
* @param theme theme name * @param themeId theme id
* @return List * @return List
*/ */
List<String> getCustomTpl(@NonNull String theme); List<String> getCustomTpl(@NonNull String themeId);
/** /**
* Judging whether template exists under the specified theme * Judging whether template exists under the specified theme
@ -75,7 +74,7 @@ public interface ThemeService {
boolean isTemplateExist(@NonNull String template); boolean isTemplateExist(@NonNull String template);
/** /**
* Judging whether theme exists under template path * Checks whether theme exists under template path
* *
* @param themeId theme name * @param themeId theme name
* @return boolean * @return boolean
@ -97,7 +96,7 @@ public interface ThemeService {
Path getBasePath(); Path getBasePath();
/** /**
* Get template content by template absolute path. * Gets template content by template absolute path.
* *
* @param absolutePath absolute path * @param absolutePath absolute path
* @return template content * @return template content
@ -105,32 +104,31 @@ public interface ThemeService {
String getTemplateContent(@NonNull String absolutePath); String getTemplateContent(@NonNull String absolutePath);
/** /**
* Save template content by template absolute path. * Saves template content by template absolute path.
* *
* @param absolutePath absolute path * @param absolutePath absolute path
* @param content new content * @param content new content
*/ */
@Deprecated
void saveTemplateContent(@NonNull String absolutePath, @NonNull String content); void saveTemplateContent(@NonNull String absolutePath, @NonNull String content);
/** /**
* Delete a theme by key. * Deletes a theme by key.
* *
* @param key theme key * @param themeId theme id must not be blank
*/ */
void deleteTheme(@NonNull String key); void deleteTheme(@NonNull String themeId);
/** /**
* Fetchs theme configuration. * Fetches theme configuration.
* *
* @param themeName theme name must not be blank * @param themeId must not be blank
* @return theme configuration or null if not found * @return theme configuration or null if not found
*/ */
@Nullable @Nullable
Object fetchConfig(@NonNull String themeName); Object fetchConfig(@NonNull String themeId);
/** /**
* Render a theme page. * Renders a theme page.
* *
* @param pageName must not be blank * @param pageName must not be blank
* @return full path of the theme page * @return full path of the theme page
@ -144,12 +142,14 @@ public interface ThemeService {
* @return current theme id * @return current theme id
*/ */
@NonNull @NonNull
String getActivatedTheme(); String getActivatedThemeId();
/** /**
* Actives a theme. * Actives a theme.
* *
* @param themeId theme id must not be blank * @param themeId theme id must not be blank
* @return theme property
*/ */
void activeTheme(@NonNull String themeId); @NonNull
ThemeProperty activeTheme(@NonNull String themeId);
} }

View File

@ -1,14 +1,16 @@
package run.halo.app.service.impl; package run.halo.app.service.impl;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileReader; import cn.hutool.core.io.file.FileReader;
import cn.hutool.core.io.file.FileWriter; import cn.hutool.core.io.file.FileWriter;
import cn.hutool.core.text.StrBuilder; import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import freemarker.template.Configuration;
import freemarker.template.TemplateModelException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
@ -23,7 +25,6 @@ import run.halo.app.model.support.ThemeFile;
import run.halo.app.model.support.ThemeProperty; import run.halo.app.model.support.ThemeProperty;
import run.halo.app.service.OptionService; import run.halo.app.service.OptionService;
import run.halo.app.service.ThemeService; import run.halo.app.service.ThemeService;
import run.halo.app.utils.FilenameUtils;
import run.halo.app.utils.JsonUtils; import run.halo.app.utils.JsonUtils;
import java.io.File; import java.io.File;
@ -78,6 +79,9 @@ public class ThemeServiceImpl implements ThemeService {
*/ */
private final static String RENDER_TEMPLATE = "themes/%s/%s"; private final static String RENDER_TEMPLATE = "themes/%s/%s";
/**
* Theme cache key.
*/
private final static String THEMES_CACHE_KEY = "themes"; private final static String THEMES_CACHE_KEY = "themes";
/** /**
@ -91,11 +95,15 @@ public class ThemeServiceImpl implements ThemeService {
private final StringCacheStore cacheStore; private final StringCacheStore cacheStore;
private final Configuration configuration;
public ThemeServiceImpl(HaloProperties haloProperties, public ThemeServiceImpl(HaloProperties haloProperties,
OptionService optionService, OptionService optionService,
StringCacheStore cacheStore) { StringCacheStore cacheStore,
Configuration configuration) {
this.optionService = optionService; this.optionService = optionService;
this.cacheStore = cacheStore; this.cacheStore = cacheStore;
this.configuration = configuration;
yamlMapper = new ObjectMapper(new YAMLFactory()); yamlMapper = new ObjectMapper(new YAMLFactory());
workDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER); workDir = Paths.get(haloProperties.getWorkDir(), THEME_FOLDER);
} }
@ -117,11 +125,6 @@ public class ThemeServiceImpl implements ThemeService {
return themes.stream().filter(themeProperty -> themeProperty.getId().equals(themeId)).findFirst(); return themes.stream().filter(themeProperty -> themeProperty.getId().equals(themeId)).findFirst();
} }
/**
* Gets all themes
*
* @return list of themes
*/
@Override @Override
public List<ThemeProperty> getThemes() { public List<ThemeProperty> getThemes() {
// Fetch themes from cache // Fetch themes from cache
@ -155,80 +158,25 @@ public class ThemeServiceImpl implements ThemeService {
}); });
} }
/**
* Lists theme folder by absolute path.
*
* @param absolutePath absolutePath
* @return List<ThemeFile>
*/
@Override @Override
public List<ThemeFile> listThemeFolder(String absolutePath) { public List<ThemeFile> listThemeFolder(String absolutePath) {
// Check this path return listThemeFileTree(Paths.get(absolutePath));
checkDirectory(absolutePath);
final List<ThemeFile> templates = new ArrayList<>();
try {
File absolutePathFile = new File(absolutePath);
File[] baseFiles = absolutePathFile.listFiles();
if (null != baseFiles) {
baseFileFor:
for (File base : baseFiles) {
for (String filterFile : FILTER_FILES) {
if (filterFile.equals(base.getName())) {
continue baseFileFor;
}
}
ThemeFile file = new ThemeFile();
if (base.isDirectory()) {
file.setName(base.getName());
file.setPath(base.getAbsolutePath());
file.setIsFile(false);
file.setNode(listThemeFolder(base.getAbsolutePath()));
} else {
file.setName(base.getName());
file.setPath(base.getAbsolutePath());
file.setIsFile(true);
for (String suffix : CAN_EDIT_SUFFIX) {
if (FilenameUtils.getExtension(file.getName()).equals(suffix)) {
file.setCanEdit(true);
break;
} else {
file.setCanEdit(false);
}
}
}
templates.add(file);
}
}
} catch (Exception e) {
throw new RuntimeException("Failed to get theme template", e);
}
templates.sort(Comparator.comparing(ThemeFile::getIsFile));
return templates;
} }
/**
* Lists theme folder by theme name.
*
* @param theme theme
* @return List<ThemeFile>
*/
@Override @Override
public List<ThemeFile> listThemeFolderBy(String theme) { public List<ThemeFile> listThemeFolderBy(String themeId) {
File themePath = new File(getThemeBasePath(), theme); // Get the theme property
return listThemeFolder(themePath.getAbsolutePath()); ThemeProperty themeProperty = getThemeOfNonNullBy(themeId);
// List theme file as tree view
return listThemeFolder(themeProperty.getThemePath());
} }
/**
* Gets custom template, such as page_xxx.ftl, and xxx will be template name
*
* @param theme theme name
* @return List
*/
@Override @Override
public List<String> getCustomTpl(String theme) { public List<String> getCustomTpl(String themeId) {
// TODO 这里的参数是有问题的,等待修复。
final List<String> templates = new ArrayList<>(); final List<String> templates = new ArrayList<>();
final File themePath = new File(getThemeBasePath(), theme); final File themePath = new File(getThemeBasePath(), themeId);
final File[] themeFiles = themePath.listFiles(); final File[] themeFiles = themePath.listFiles();
if (null != themeFiles && themeFiles.length > 0) { if (null != themeFiles && themeFiles.length > 0) {
for (File file : themeFiles) { for (File file : themeFiles) {
@ -241,39 +189,20 @@ public class ThemeServiceImpl implements ThemeService {
return templates; return templates;
} }
/**
* Judging whether template exists under the specified theme
*
* @param template template
* @return boolean
*/
@Override @Override
public boolean isTemplateExist(String template) { public boolean isTemplateExist(String template) {
StrBuilder templatePath = new StrBuilder(getActivatedTheme()); StrBuilder templatePath = new StrBuilder(getActivatedThemeId());
templatePath.append("/"); templatePath.append("/");
templatePath.append(template); templatePath.append(template);
File file = new File(getThemeBasePath(), templatePath.toString()); File file = new File(getThemeBasePath(), templatePath.toString());
return file.exists(); return file.exists();
} }
/**
* Judging whether theme exists under template path
*
* @param themeId theme name
* @return boolean
*/
@Override @Override
public boolean isThemeExist(String themeId) { public boolean isThemeExist(String themeId) {
// TODO Get theme folder name by theme id return getThemeBy(themeId).isPresent();
File file = new File(getThemeBasePath(), themeId);
return file.exists();
} }
/**
* Gets theme base path.
*
* @return File
*/
@Override @Override
public File getThemeBasePath() { public File getThemeBasePath() {
return getBasePath().toFile(); return getBasePath().toFile();
@ -284,56 +213,56 @@ public class ThemeServiceImpl implements ThemeService {
return workDir; return workDir;
} }
/**
* Get template content by template absolute path.
*
* @param absolutePath absolute path
* @return template content
*/
@Override @Override
public String getTemplateContent(String absolutePath) { public String getTemplateContent(String absolutePath) {
// Check the path // Check the path
checkDirectory(absolutePath); checkDirectory(absolutePath);
// Read file
return new FileReader(absolutePath).readString(); return new FileReader(absolutePath).readString();
} }
/**
* Save template content by template absolute path.
*
* @param absolutePath absolute path
* @param content new content
*/
@Override @Override
public void saveTemplateContent(String absolutePath, String content) { public void saveTemplateContent(String absolutePath, String content) {
final FileWriter fileWriter = new FileWriter(absolutePath); // Check the path
fileWriter.write(content); checkDirectory(absolutePath);
// Write file
new FileWriter(absolutePath).write(content);
} }
/**
* Delete a theme by key.
*
* @param key theme key
*/
@Override @Override
public void deleteTheme(String key) { public void deleteTheme(String themeId) {
if (!isThemeExist(key)) { // Get the theme property
throw new NotFoundException("该主题不存在!").setErrorData(key); ThemeProperty themeProperty = getThemeOfNonNullBy(themeId);
try {
// Delete the folder
Files.deleteIfExists(Paths.get(themeProperty.getThemePath()));
// Delete theme cache
cacheStore.delete(THEMES_CACHE_KEY);
} catch (IOException e) {
throw new ServiceException("Failed to delete theme folder", e).setErrorData(themeId);
} }
File file = new File(this.getThemeBasePath(), key);
FileUtil.del(file);
cacheStore.delete(THEMES_CACHE_KEY);
} }
@Override @Override
public Object fetchConfig(String themeName) { public Object fetchConfig(String themeId) {
Assert.hasText(themeName, "Theme name must not be blank"); Assert.hasText(themeId, "Theme name must not be blank");
// Get theme property
ThemeProperty themeProperty = getThemeOfNonNullBy(themeId);
if (!themeProperty.getHasOptions()) {
// If this theme dose not has an option, then return null
return null;
}
try { try {
for (String optionsName : OPTIONS_NAMES) { for (String optionsName : OPTIONS_NAMES) {
// Resolve the options path // Resolve the options path
Path optionsPath = workDir.resolve(themeName).resolve(optionsName); Path optionsPath = Paths.get(themeProperty.getThemePath(), optionsName);
log.debug("Finding options in: [{}]", optionsPath.toString()); log.debug("Finding options in: [{}]", optionsPath.toString());
@ -355,17 +284,99 @@ public class ThemeServiceImpl implements ThemeService {
@Override @Override
public String render(String pageName) { public String render(String pageName) {
return String.format(RENDER_TEMPLATE, getActivatedTheme(), pageName); return String.format(RENDER_TEMPLATE, getActivatedThemeId(), pageName);
} }
@Override @Override
public String getActivatedTheme() { public String getActivatedThemeId() {
return optionService.getByProperty(PrimaryProperties.THEME).orElse(DEFAULT_THEME_NAME); return optionService.getByProperty(PrimaryProperties.THEME).orElse(DEFAULT_THEME_NAME);
} }
@Override @Override
public void activeTheme(String themeId) { public ThemeProperty activeTheme(String themeId) {
// TODO Check existence of the theme // Check existence of the theme
ThemeProperty themeProperty = getThemeOfNonNullBy(themeId);
// Save the theme to database
optionService.saveProperty(PrimaryProperties.THEME, themeId);
try {
// TODO Refactor here in the future
configuration.setSharedVariable("themeName", themeId);
configuration.setSharedVariable("options", optionService.listOptions());
} catch (TemplateModelException e) {
throw new ServiceException("Failed to set shared variable", e).setErrorData(themeId);
}
return themeProperty;
}
/**
* 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 {
List<ThemeFile> themeFiles = new LinkedList<>();
Files.list(topPath).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 name
themeFiles.sort(Comparator.comparing(ThemeFile::getName));
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.endsWith(suffix)) {
return true;
}
}
return false;
} }
/** /**
@ -377,7 +388,9 @@ public class ThemeServiceImpl implements ThemeService {
private void checkDirectory(@NonNull String absoluteName) { private void checkDirectory(@NonNull String absoluteName) {
Assert.hasText(absoluteName, "Absolute name must not be blank"); Assert.hasText(absoluteName, "Absolute name must not be blank");
boolean valid = Paths.get(absoluteName).startsWith(workDir); ThemeProperty activeThemeProperty = getThemeOfNonNullBy(getActivatedThemeId());
boolean valid = Paths.get(absoluteName).startsWith(activeThemeProperty.getThemePath());
if (!valid) { if (!valid) {
throw new ForbiddenException("You cannot access " + absoluteName).setErrorData(absoluteName); throw new ForbiddenException("You cannot access " + absoluteName).setErrorData(absoluteName);

View File

@ -1,14 +1,10 @@
package run.halo.app.web.controller.admin.api; package run.halo.app.web.controller.admin.api;
import freemarker.template.Configuration;
import freemarker.template.TemplateModelException;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import run.halo.app.model.properties.PrimaryProperties;
import run.halo.app.model.support.BaseResponse; import run.halo.app.model.support.BaseResponse;
import run.halo.app.model.support.ThemeFile; import run.halo.app.model.support.ThemeFile;
import run.halo.app.model.support.ThemeProperty; import run.halo.app.model.support.ThemeProperty;
import run.halo.app.service.OptionService;
import run.halo.app.service.ThemeService; import run.halo.app.service.ThemeService;
import java.util.List; import java.util.List;
@ -23,17 +19,9 @@ import java.util.List;
@RequestMapping("/admin/api/themes") @RequestMapping("/admin/api/themes")
public class ThemeController { public class ThemeController {
private final OptionService optionService;
private final Configuration configuration;
private final ThemeService themeService; private final ThemeService themeService;
public ThemeController(OptionService optionService, public ThemeController(ThemeService themeService) {
Configuration configuration,
ThemeService themeService) {
this.optionService = optionService;
this.configuration = configuration;
this.themeService = themeService; this.themeService = themeService;
} }
@ -61,7 +49,7 @@ public class ThemeController {
*/ */
@GetMapping("files") @GetMapping("files")
public List<ThemeFile> listFiles() { public List<ThemeFile> listFiles() {
return themeService.listThemeFolderBy(themeService.getActivatedTheme()); return themeService.listThemeFolderBy(themeService.getActivatedThemeId());
} }
@GetMapping("files/content") @GetMapping("files/content")
@ -72,34 +60,38 @@ public class ThemeController {
@PutMapping("files/content") @PutMapping("files/content")
public void updateContentBy(@RequestParam(name = "path") String path, public void updateContentBy(@RequestParam(name = "path") String path,
@RequestParam(name = "content") String content) { @RequestParam(name = "content") String content) {
// TODO Refactor the params to body
themeService.saveTemplateContent(path, content); themeService.saveTemplateContent(path, content);
} }
@GetMapping("files/custom") @GetMapping("files/custom")
public List<String> customTemplate() { public List<String> customTemplate() {
return themeService.getCustomTpl(themeService.getActivatedTheme()); return themeService.getCustomTpl(themeService.getActivatedThemeId());
} }
@PostMapping("{themeId}/activate") @PostMapping("{themeId}/activate")
@ApiOperation("Active a theme") @ApiOperation("Activates a theme")
public void active(@RequestParam("themeId") String themeId) throws TemplateModelException { public ThemeProperty active(@RequestParam("themeId") String themeId) {
themeService.activeTheme(themeId); return themeService.activeTheme(themeId);
// TODO Check existence of the theme
optionService.saveProperty(PrimaryProperties.THEME, themeId);
configuration.setSharedVariable("themeName", themeId);
configuration.setSharedVariable("options", optionService.listOptions());
} }
@DeleteMapping("key/{key}") @GetMapping("activate")
@ApiOperation("Deletes a theme") @ApiOperation("Gets activate theme")
public void deleteBy(@PathVariable("key") String key) { public ThemeProperty getActivateTheme() {
themeService.deleteTheme(key); return themeService.getThemeOfNonNullBy(themeService.getActivatedThemeId());
} }
@GetMapping("configurations") @GetMapping("activate/configurations")
@ApiOperation("Fetches theme configuration") @ApiOperation("Fetches theme configuration")
public BaseResponse<Object> fetchConfig() { public BaseResponse<Object> fetchConfig() {
return BaseResponse.ok(themeService.fetchConfig(themeService.getActivatedTheme())); return BaseResponse.ok(themeService.fetchConfig(themeService.getActivatedThemeId()));
} }
@DeleteMapping("{themeId}")
@ApiOperation("Deletes a theme")
public void deleteBy(@PathVariable("themeId") String themeId) {
themeService.deleteTheme(themeId);
}
} }

View File

@ -114,7 +114,7 @@ public class CommonController implements ErrorController {
return "common/error/404"; return "common/error/404";
} }
StrBuilder path = new StrBuilder("themes/"); StrBuilder path = new StrBuilder("themes/");
path.append(themeService.getActivatedTheme()); path.append(themeService.getActivatedThemeId());
path.append("/404"); path.append("/404");
return path.toString(); return path.toString();
} }
@ -130,7 +130,7 @@ public class CommonController implements ErrorController {
return "common/error/500"; return "common/error/500";
} }
StrBuilder path = new StrBuilder("themes/"); StrBuilder path = new StrBuilder("themes/");
path.append(themeService.getActivatedTheme()); path.append(themeService.getActivatedThemeId());
path.append("/500"); path.append("/500");
return path.toString(); return path.toString();
} }