mirror of https://github.com/halo-dev/halo
feat: add theme-side business data query model (#2347)
<!-- Thanks for sending a pull request! Here are some tips for you: 1. 如果这是你的第一次,请阅读我们的贡献指南:<https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>。 1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>. 2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。 2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. 3. 请确保你已经添加并运行了适当的测试。 3. Ensure you have added or ran the appropriate tests for your PR. --> #### What type of PR is this? /kind feature /milestone 2.0 /area core <!-- 添加其中一个类别: Add one of the following kinds: /kind bug /kind cleanup /kind documentation /kind feature /kind improvement 适当添加其中一个或多个类别(可选): Optionally add one or more of the following kinds if applicable: /kind api-change /kind deprecation /kind failing-test /kind flake /kind regression --> #### What this PR does / why we need it: 新增主题端数据查询模型 #### Which issue(s) this PR fixes: <!-- PR 合并时自动关闭 issue。 Automatically closes linked issue when PR is merged. 用法:`Fixes #<issue 号>`,或者 `Fixes (粘贴 issue 完整链接)` Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`. --> Fixes #2346 #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? <!-- 如果当前 Pull Request 的修改不会造成用户侧的任何变更,在 `release-note` 代码块儿中填写 `NONE`。 否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新(Break Change), Release Note 需要以 `action required` 开头。 If no, just write "NONE" in the release-note block below. If yes, a release note is required: Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required". --> ```release-note None ```pull/2365/head
parent
7682318ae6
commit
d124c01a84
|
@ -1,5 +1,6 @@
|
|||
package run.halo.app.core.extension;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
@ -47,6 +48,14 @@ public class Category extends AbstractExtension {
|
|||
private List<String> children;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public CategoryStatus getStatusOrDefault() {
|
||||
if (this.status == null) {
|
||||
this.status = new CategoryStatus();
|
||||
}
|
||||
return this.status;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CategoryStatus {
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package run.halo.app.core.extension;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
@ -55,6 +56,15 @@ public class Tag extends AbstractExtension {
|
|||
private String cover;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public TagStatus getStatusOrDefault() {
|
||||
if (this.status == null) {
|
||||
this.status = new TagStatus();
|
||||
}
|
||||
return this.status;
|
||||
}
|
||||
|
||||
|
||||
@Data
|
||||
public static class TagStatus {
|
||||
|
||||
|
|
|
@ -1,19 +1,37 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.result.view.View;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveView;
|
||||
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveViewResolver;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.theme.finders.FinderRegistry;
|
||||
|
||||
@Component("thymeleafReactiveViewResolver")
|
||||
public class HaloViewResolver extends ThymeleafReactiveViewResolver {
|
||||
|
||||
public HaloViewResolver() {
|
||||
private final FinderRegistry finderRegistry;
|
||||
|
||||
public HaloViewResolver(FinderRegistry finderRegistry) {
|
||||
setViewClass(HaloView.class);
|
||||
this.finderRegistry = finderRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mono<View> loadView(String viewName, Locale locale) {
|
||||
return super.loadView(viewName, locale)
|
||||
.cast(HaloView.class)
|
||||
.map(view -> {
|
||||
// populate finders to view static variables
|
||||
finderRegistry.getFinders().forEach(view::addStaticVariable);
|
||||
return view;
|
||||
});
|
||||
}
|
||||
|
||||
public static class HaloView extends ThymeleafReactiveView {
|
||||
|
@ -30,7 +48,8 @@ public class HaloViewResolver extends ThymeleafReactiveViewResolver {
|
|||
return themeResolver.getTheme(exchange.getRequest()).flatMap(theme -> {
|
||||
// calculate the engine before rendering
|
||||
setTemplateEngine(engineManager.getTemplateEngine(theme));
|
||||
return super.render(model, contentType, exchange);
|
||||
return super.render(model, contentType, exchange)
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.thymeleaf.util.LoggingUtils;
|
|||
import org.thymeleaf.web.IWebExchange;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -47,6 +48,10 @@ import reactor.core.publisher.Mono;
|
|||
* Code from
|
||||
* <a href="https://github.com/thymeleaf/thymeleaf/blob/3.1-master/lib/thymeleaf-spring6/src/main/java/org/thymeleaf/spring6/SpringWebFluxTemplateEngine.java">thymeleaf SpringWebFluxTemplateEngine</a>
|
||||
*
|
||||
* <p>Note that: We need to subscribe on a new thread to support blocking operations
|
||||
* for theme finders in {@link #createDataDrivenStream} and {@link #createFullStream} and
|
||||
* {@link #createChunkedStream}.</p>
|
||||
*
|
||||
* @author Daniel Fernández
|
||||
* @see ISpringWebFluxTemplateEngine
|
||||
* @see org.thymeleaf.spring6.SpringWebFluxTemplateEngine
|
||||
|
@ -125,7 +130,9 @@ public class SpringWebFluxTemplateEngine extends SpringTemplateEngine
|
|||
// We should be executing in data-driven mode
|
||||
return createDataDrivenStream(
|
||||
template, markupSelectors, context, dataDriverVariableName, bufferFactory,
|
||||
charset, chunkSizeBytes, sse);
|
||||
charset, chunkSizeBytes, sse)
|
||||
// We need to subscribe on a new thread to support blocking operations
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
} catch (final Throwable t) {
|
||||
return Flux.error(t);
|
||||
|
@ -151,14 +158,18 @@ public class SpringWebFluxTemplateEngine extends SpringTemplateEngine
|
|||
*/
|
||||
if (chunkSizeBytes == Integer.MAX_VALUE) {
|
||||
// No limit on buffer size, so there is no reason to throttle: using FULL mode instead.
|
||||
return createFullStream(template, markupSelectors, context, bufferFactory, charset);
|
||||
return createFullStream(template, markupSelectors, context, bufferFactory, charset)
|
||||
// We need to subscribe on a new thread to support blocking operations
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
/*
|
||||
* CREATE A CHUNKED STREAM
|
||||
*/
|
||||
return createChunkedStream(
|
||||
template, markupSelectors, context, bufferFactory, charset, responseMaxChunkSizeBytes);
|
||||
template, markupSelectors, context, bufferFactory, charset, responseMaxChunkSizeBytes)
|
||||
// We need to subscribe on a new thread to support blocking operations
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import java.util.List;
|
||||
import run.halo.app.core.extension.Category;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.theme.finders.vo.CategoryTreeVo;
|
||||
import run.halo.app.theme.finders.vo.CategoryVo;
|
||||
|
||||
/**
|
||||
* A finder for {@link Category}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface CategoryFinder {
|
||||
|
||||
CategoryVo getByName(String name);
|
||||
|
||||
List<CategoryVo> getByNames(List<String> names);
|
||||
|
||||
ListResult<CategoryVo> list(int page, int size);
|
||||
|
||||
List<CategoryVo> listAll();
|
||||
|
||||
List<CategoryTreeVo> listAsTree();
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import run.halo.app.core.extension.Comment;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.theme.finders.vo.CommentVo;
|
||||
import run.halo.app.theme.finders.vo.ReplyVo;
|
||||
|
||||
/**
|
||||
* A finder for finding {@link Comment comments} in template.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface CommentFinder {
|
||||
|
||||
CommentVo getByName(String name);
|
||||
|
||||
ListResult<CommentVo> list(Comment.CommentSubjectRef ref, int page, int size);
|
||||
|
||||
ListResult<ReplyVo> listReply(String commentName, int page, int size);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import run.halo.app.core.extension.User;
|
||||
|
||||
/**
|
||||
* A value object for {@link run.halo.app.core.extension.User}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Value
|
||||
@Builder
|
||||
public class Contributor {
|
||||
String name;
|
||||
|
||||
String displayName;
|
||||
|
||||
String avatar;
|
||||
|
||||
String bio;
|
||||
|
||||
/**
|
||||
* Convert {@link User} to {@link Contributor}.
|
||||
*
|
||||
* @param user user extension
|
||||
* @return contributor value object
|
||||
*/
|
||||
public static Contributor from(User user) {
|
||||
return builder().name(user.getMetadata().getName())
|
||||
.displayName(user.getSpec().getDisplayName())
|
||||
.avatar(user.getSpec().getAvatar())
|
||||
.bio(user.getSpec().getBio())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import java.util.List;
|
||||
import run.halo.app.core.extension.User;
|
||||
|
||||
/**
|
||||
* A finder for {@link User}.
|
||||
*/
|
||||
public interface ContributorFinder {
|
||||
|
||||
Contributor getContributor(String name);
|
||||
|
||||
List<Contributor> getContributors(List<String> names);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Template model data finder for theme.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Service
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface Finder {
|
||||
|
||||
/**
|
||||
* The name of the theme model variable.
|
||||
*
|
||||
* @return variable name, class simple name if not specified
|
||||
*/
|
||||
String value() default "";
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.plugin.ExtensionContextRegistry;
|
||||
import run.halo.app.plugin.PluginApplicationContext;
|
||||
import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
||||
|
||||
/**
|
||||
* Finder registry for class annotated with {@link Finder}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class FinderRegistry implements InitializingBean {
|
||||
private final Map<String, List<String>> pluginFindersLookup = new ConcurrentHashMap<>();
|
||||
private final Map<String, Object> finders = new ConcurrentHashMap<>(64);
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public FinderRegistry(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
Object get(String name) {
|
||||
return finders.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a name, register a Finder for it.
|
||||
*
|
||||
* @param name the canonical name
|
||||
* @param finder the finder to be registered
|
||||
* @throws IllegalStateException if the name is already existing
|
||||
*/
|
||||
public void registerFinder(String name, Object finder) {
|
||||
if (finders.containsKey(name)) {
|
||||
throw new IllegalStateException(
|
||||
"Finder with name '" + name + "' is already registered");
|
||||
}
|
||||
finders.put(name, finder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a finder.
|
||||
*
|
||||
* @param finder register a finder that annotated with {@link Finder}
|
||||
* @return the name of the finder
|
||||
*/
|
||||
public String registerFinder(Object finder) {
|
||||
Finder annotation = finder.getClass().getAnnotation(Finder.class);
|
||||
if (annotation == null) {
|
||||
throw new IllegalStateException("Finder must be annotated with @Finder");
|
||||
}
|
||||
String name = annotation.value();
|
||||
if (name == null) {
|
||||
name = finder.getClass().getSimpleName();
|
||||
}
|
||||
this.registerFinder(name, finder);
|
||||
return name;
|
||||
}
|
||||
|
||||
public void removeFinder(String name) {
|
||||
finders.remove(name);
|
||||
}
|
||||
|
||||
public Map<String, Object> getFinders() {
|
||||
return Map.copyOf(finders);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
// initialize finders from application context
|
||||
applicationContext.getBeansWithAnnotation(Finder.class)
|
||||
.forEach((k, v) -> {
|
||||
registerFinder(v);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register finders for a plugin.
|
||||
*
|
||||
* @param event plugin started event
|
||||
*/
|
||||
@EventListener(HaloPluginStartedEvent.class)
|
||||
public void onPluginStarted(HaloPluginStartedEvent event) {
|
||||
String pluginId = event.getPlugin().getPluginId();
|
||||
PluginApplicationContext pluginApplicationContext = ExtensionContextRegistry.getInstance()
|
||||
.getByPluginId(pluginId);
|
||||
pluginApplicationContext.getBeansWithAnnotation(Finder.class)
|
||||
.forEach((beanName, finderObject) -> {
|
||||
// register finder
|
||||
String finderName = registerFinder(finderObject);
|
||||
// add to plugin finder lookup
|
||||
pluginFindersLookup.computeIfAbsent(pluginId, k -> new ArrayList<>())
|
||||
.add(finderName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove finders registered by the plugin.
|
||||
*
|
||||
* @param event plugin stopped event
|
||||
*/
|
||||
@EventListener(HaloPluginStoppedEvent.class)
|
||||
public void onPluginStopped(HaloPluginStoppedEvent event) {
|
||||
String pluginId = event.getPlugin().getPluginId();
|
||||
boolean containsKey = pluginFindersLookup.containsKey(pluginId);
|
||||
if (!containsKey) {
|
||||
return;
|
||||
}
|
||||
pluginFindersLookup.get(pluginId).forEach(this::removeFinder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only for test.
|
||||
*
|
||||
* @param pluginId plugin id
|
||||
* @param finderName finder name
|
||||
*/
|
||||
void addPluginFinder(String pluginId, String finderName) {
|
||||
pluginFindersLookup.computeIfAbsent(pluginId, k -> new ArrayList<>())
|
||||
.add(finderName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import run.halo.app.theme.finders.vo.MenuVo;
|
||||
|
||||
/**
|
||||
* A finder for {@link run.halo.app.core.extension.Menu}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface MenuFinder {
|
||||
|
||||
MenuVo getByName(String name);
|
||||
|
||||
MenuVo getDefault();
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
|
||||
/**
|
||||
* A finder for {@link Post}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface PostFinder {
|
||||
|
||||
PostVo getByName(String postName);
|
||||
|
||||
ContentVo content(String postName);
|
||||
|
||||
ListResult<PostVo> list(int page, int size);
|
||||
|
||||
ListResult<PostVo> listByCategory(int page, int size, String categoryName);
|
||||
|
||||
ListResult<PostVo> listByTag(int page, int size, String tag);
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import java.util.List;
|
||||
import run.halo.app.core.extension.Tag;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.theme.finders.vo.TagVo;
|
||||
|
||||
/**
|
||||
* A finder for {@link Tag}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface TagFinder {
|
||||
|
||||
TagVo getByName(String name);
|
||||
|
||||
List<TagVo> getByNames(List<String> names);
|
||||
|
||||
ListResult<TagVo> list(int page, int size);
|
||||
|
||||
List<TagVo> listAll();
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import run.halo.app.core.extension.Category;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import run.halo.app.theme.finders.vo.CategoryTreeVo;
|
||||
import run.halo.app.theme.finders.vo.CategoryVo;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link CategoryFinder}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Finder("categoryFinder")
|
||||
public class CategoryFinderImpl implements CategoryFinder {
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public CategoryFinderImpl(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CategoryVo getByName(String name) {
|
||||
return client.fetch(Category.class, name)
|
||||
.map(CategoryVo::from)
|
||||
.block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CategoryVo> getByNames(List<String> names) {
|
||||
if (names == null) {
|
||||
return List.of();
|
||||
}
|
||||
return names.stream().map(this::getByName)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListResult<CategoryVo> list(int page, int size) {
|
||||
return client.list(Category.class, null,
|
||||
defaultComparator(), page, size)
|
||||
.map(list -> {
|
||||
List<CategoryVo> categoryVos = list.stream()
|
||||
.map(CategoryVo::from)
|
||||
.collect(Collectors.toList());
|
||||
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
|
||||
categoryVos);
|
||||
})
|
||||
.block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CategoryVo> listAll() {
|
||||
return client.list(Category.class, null, defaultComparator())
|
||||
.map(CategoryVo::from)
|
||||
.collectList()
|
||||
.block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CategoryTreeVo> listAsTree() {
|
||||
List<CategoryVo> categoryVos = listAll();
|
||||
Map<String, CategoryVo> nameIdentityMap = categoryVos.stream()
|
||||
.collect(Collectors.toMap(CategoryVo::getName, Function.identity()));
|
||||
|
||||
Map<String, CategoryTreeVo> treeVoMap = new HashMap<>();
|
||||
// populate parentName
|
||||
categoryVos.forEach(categoryVo -> {
|
||||
final String parentName = categoryVo.getName();
|
||||
treeVoMap.putIfAbsent(parentName, CategoryTreeVo.from(categoryVo));
|
||||
List<String> children = categoryVo.getChildren();
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return;
|
||||
}
|
||||
children.forEach(childrenName -> {
|
||||
CategoryVo childrenVo = nameIdentityMap.get(childrenName);
|
||||
CategoryTreeVo treeVo = CategoryTreeVo.from(childrenVo);
|
||||
treeVo.setParentName(parentName);
|
||||
treeVoMap.putIfAbsent(treeVo.getName(), treeVo);
|
||||
});
|
||||
});
|
||||
nameIdentityMap.clear();
|
||||
return listToTree(treeVoMap.values());
|
||||
}
|
||||
|
||||
static List<CategoryTreeVo> listToTree(Collection<CategoryTreeVo> list) {
|
||||
Map<String, List<CategoryTreeVo>> nameIdentityMap = list.stream()
|
||||
.filter(item -> item.getParentName() != null)
|
||||
.collect(Collectors.groupingBy(CategoryTreeVo::getParentName));
|
||||
list.forEach(node -> node.setChildren(nameIdentityMap.get(node.getName())));
|
||||
return list.stream()
|
||||
.filter(v -> v.getParentName() == null)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
static Comparator<Category> defaultComparator() {
|
||||
Function<Category, Integer> priority =
|
||||
category -> Objects.requireNonNullElse(category.getSpec().getPriority(), 0);
|
||||
Function<Category, Instant> creationTimestamp =
|
||||
category -> category.getMetadata().getCreationTimestamp();
|
||||
Function<Category, String> name =
|
||||
category -> category.getMetadata().getName();
|
||||
return Comparator.comparing(priority)
|
||||
.thenComparing(creationTimestamp)
|
||||
.thenComparing(name)
|
||||
.reversed();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.core.extension.Comment;
|
||||
import run.halo.app.core.extension.Reply;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.finders.CommentFinder;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import run.halo.app.theme.finders.vo.CommentVo;
|
||||
import run.halo.app.theme.finders.vo.ReplyVo;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link CommentFinder}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Finder("commentFinder")
|
||||
public class CommentFinderImpl implements CommentFinder {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public CommentFinderImpl(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommentVo getByName(String name) {
|
||||
return client.fetch(Comment.class, name)
|
||||
.map(CommentVo::from)
|
||||
.block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListResult<CommentVo> list(Comment.CommentSubjectRef ref, int page, int size) {
|
||||
return client.list(Comment.class, fixedPredicate(ref),
|
||||
defaultComparator(),
|
||||
page, size)
|
||||
.map(list -> {
|
||||
List<CommentVo> commentVos = list.get().map(CommentVo::from).toList();
|
||||
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
|
||||
commentVos);
|
||||
})
|
||||
.block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListResult<ReplyVo> listReply(String commentName, int page, int size) {
|
||||
Comparator<Reply> comparator =
|
||||
Comparator.comparing(reply -> reply.getMetadata().getCreationTimestamp());
|
||||
return client.list(Reply.class,
|
||||
reply -> reply.getSpec().getCommentName().equals(commentName)
|
||||
&& Objects.equals(false, reply.getSpec().getHidden())
|
||||
&& Objects.equals(true, reply.getSpec().getApproved()),
|
||||
comparator.reversed(), page, size)
|
||||
.map(list -> {
|
||||
List<ReplyVo> replyVos = list.get().map(ReplyVo::from).toList();
|
||||
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
|
||||
replyVos);
|
||||
})
|
||||
.block();
|
||||
}
|
||||
|
||||
private Predicate<Comment> fixedPredicate(Comment.CommentSubjectRef ref) {
|
||||
Assert.notNull(ref, "Comment subject reference must not be null");
|
||||
return comment -> ref.equals(comment.getSpec().getSubjectRef())
|
||||
&& Objects.equals(false, comment.getSpec().getHidden())
|
||||
&& Objects.equals(true, comment.getSpec().getApproved());
|
||||
}
|
||||
|
||||
static Comparator<Comment> defaultComparator() {
|
||||
Function<Comment, Boolean> top =
|
||||
comment -> Objects.requireNonNullElse(comment.getSpec().getTop(), false);
|
||||
Function<Comment, Integer> priority =
|
||||
comment -> Objects.requireNonNullElse(comment.getSpec().getPriority(), 0);
|
||||
Function<Comment, Instant> creationTimestamp =
|
||||
comment -> comment.getMetadata().getCreationTimestamp();
|
||||
Function<Comment, String> name =
|
||||
comment -> comment.getMetadata().getName();
|
||||
return Comparator.comparing(top)
|
||||
.thenComparing(priority)
|
||||
.thenComparing(creationTimestamp)
|
||||
.thenComparing(name)
|
||||
.reversed();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.finders.Contributor;
|
||||
import run.halo.app.theme.finders.ContributorFinder;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link ContributorFinder}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Finder("contributorFinder")
|
||||
public class ContributorFinderImpl implements ContributorFinder {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public ContributorFinderImpl(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Contributor getContributor(String name) {
|
||||
return client.fetch(User.class, name)
|
||||
.map(Contributor::from)
|
||||
.block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Contributor> getContributors(List<String> names) {
|
||||
if (names == null) {
|
||||
return List.of();
|
||||
}
|
||||
return names.stream()
|
||||
.map(this::getContributor)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import run.halo.app.core.extension.Menu;
|
||||
import run.halo.app.core.extension.MenuItem;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import run.halo.app.theme.finders.MenuFinder;
|
||||
import run.halo.app.theme.finders.vo.MenuItemVo;
|
||||
import run.halo.app.theme.finders.vo.MenuVo;
|
||||
|
||||
/**
|
||||
* A default implementation for {@link MenuFinder}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Finder("menuFinder")
|
||||
public class MenuFinderImpl implements MenuFinder {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public MenuFinderImpl(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MenuVo getByName(String name) {
|
||||
return listAsTree().stream()
|
||||
.filter(menu -> menu.getName().equals(name))
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MenuVo getDefault() {
|
||||
List<MenuVo> menuVos = listAsTree();
|
||||
if (CollectionUtils.isEmpty(menuVos)) {
|
||||
return null;
|
||||
}
|
||||
// TODO If there are multiple groups of menus,
|
||||
// return the first as the default, and consider optimizing it later
|
||||
return menuVos.get(0);
|
||||
}
|
||||
|
||||
|
||||
List<MenuVo> listAll() {
|
||||
return client.list(Menu.class, null, null)
|
||||
.map(MenuVo::from)
|
||||
.collectList()
|
||||
.block();
|
||||
}
|
||||
|
||||
List<MenuVo> listAsTree() {
|
||||
List<MenuItemVo> menuItemVos = populateParentName(listAllMenuItem());
|
||||
List<MenuItemVo> treeList = listToTree(menuItemVos);
|
||||
Map<String, MenuItemVo> nameItemRootNodeMap = treeList.stream()
|
||||
.collect(Collectors.toMap(MenuItemVo::getName, Function.identity()));
|
||||
return listAll().stream()
|
||||
.map(menuVo -> {
|
||||
List<MenuItemVo> menuItems = menuVo.getMenuItemNames().stream()
|
||||
.map(nameItemRootNodeMap::get)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
return menuVo.withMenuItems(menuItems);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
static List<MenuItemVo> listToTree(List<MenuItemVo> list) {
|
||||
Map<String, List<MenuItemVo>> nameIdentityMap = list.stream()
|
||||
.filter(item -> item.getParentName() != null)
|
||||
.collect(Collectors.groupingBy(MenuItemVo::getParentName));
|
||||
list.forEach(node -> node.setChildren(nameIdentityMap.get(node.getName())));
|
||||
// clear map to release memory
|
||||
nameIdentityMap.clear();
|
||||
return list.stream()
|
||||
.filter(v -> v.getParentName() == null)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
List<MenuItemVo> listAllMenuItem() {
|
||||
Function<MenuItem, Integer> priority = menuItem -> menuItem.getSpec().getPriority();
|
||||
Function<MenuItem, String> name = menuItem -> menuItem.getMetadata().getName();
|
||||
|
||||
return client.list(MenuItem.class, null,
|
||||
Comparator.comparing(priority).thenComparing(name).reversed()
|
||||
)
|
||||
.map(MenuItemVo::from)
|
||||
.collectList()
|
||||
.block();
|
||||
}
|
||||
|
||||
static List<MenuItemVo> populateParentName(List<MenuItemVo> menuItemVos) {
|
||||
Map<String, MenuItemVo> nameIdentityMap = menuItemVos.stream()
|
||||
.collect(Collectors.toMap(MenuItemVo::getName, Function.identity()));
|
||||
|
||||
Map<String, MenuItemVo> treeVoMap = new HashMap<>();
|
||||
// populate parentName
|
||||
menuItemVos.forEach(menuItemVo -> {
|
||||
final String parentName = menuItemVo.getName();
|
||||
treeVoMap.putIfAbsent(parentName, menuItemVo);
|
||||
LinkedHashSet<String> children = menuItemVo.getChildrenNames();
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return;
|
||||
}
|
||||
children.forEach(childrenName -> {
|
||||
MenuItemVo childrenVo = nameIdentityMap.get(childrenName);
|
||||
childrenVo.setParentName(parentName);
|
||||
treeVoMap.putIfAbsent(childrenVo.getName(), childrenVo);
|
||||
});
|
||||
});
|
||||
// clear map to release memory
|
||||
nameIdentityMap.clear();
|
||||
return treeVoMap.values()
|
||||
.stream()
|
||||
.sorted(Comparator.comparing(MenuItemVo::getPriority))
|
||||
.toList();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import run.halo.app.content.ContentService;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
import run.halo.app.theme.finders.Contributor;
|
||||
import run.halo.app.theme.finders.ContributorFinder;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
import run.halo.app.theme.finders.vo.CategoryVo;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
import run.halo.app.theme.finders.vo.TagVo;
|
||||
|
||||
/**
|
||||
* A finder for {@link Post}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Finder("postFinder")
|
||||
public class PostFinderImpl implements PostFinder {
|
||||
|
||||
public static final Predicate<Post> FIXED_PREDICATE = post ->
|
||||
Objects.equals(false, post.getSpec().getDeleted())
|
||||
&& Objects.equals(true, post.getSpec().getPublished())
|
||||
&& Post.VisibleEnum.PUBLIC.equals(post.getSpec().getVisible());
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private final ContentService contentService;
|
||||
|
||||
private final TagFinder tagFinder;
|
||||
|
||||
private final CategoryFinder categoryFinder;
|
||||
|
||||
private final ContributorFinder contributorFinder;
|
||||
|
||||
public PostFinderImpl(ReactiveExtensionClient client,
|
||||
ContentService contentService,
|
||||
TagFinder tagFinder,
|
||||
CategoryFinder categoryFinder,
|
||||
ContributorFinder contributorFinder) {
|
||||
this.client = client;
|
||||
this.contentService = contentService;
|
||||
this.tagFinder = tagFinder;
|
||||
this.categoryFinder = categoryFinder;
|
||||
this.contributorFinder = contributorFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PostVo getByName(String postName) {
|
||||
Post post = client.fetch(Post.class, postName)
|
||||
.block();
|
||||
if (post == null) {
|
||||
return null;
|
||||
}
|
||||
return getPostVo(post);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentVo content(String postName) {
|
||||
return client.fetch(Post.class, postName)
|
||||
.map(post -> post.getSpec().getReleaseSnapshot())
|
||||
.flatMap(contentService::getContent)
|
||||
.map(wrapper -> ContentVo.builder().content(wrapper.content())
|
||||
.raw(wrapper.raw()).build())
|
||||
.block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListResult<PostVo> list(int page, int size) {
|
||||
return listPost(page, size, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListResult<PostVo> listByCategory(int page, int size, String categoryName) {
|
||||
return listPost(page, size,
|
||||
post -> contains(post.getSpec().getCategories(), categoryName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListResult<PostVo> listByTag(int page, int size, String tag) {
|
||||
return listPost(page, size,
|
||||
post -> contains(post.getSpec().getTags(), tag));
|
||||
}
|
||||
|
||||
private boolean contains(List<String> c, String key) {
|
||||
if (StringUtils.isBlank(key) || c == null) {
|
||||
return false;
|
||||
}
|
||||
return c.contains(key);
|
||||
}
|
||||
|
||||
private ListResult<PostVo> listPost(int page, int size, Predicate<Post> postPredicate) {
|
||||
Predicate<Post> predicate = FIXED_PREDICATE
|
||||
.and(postPredicate == null ? post -> true : postPredicate);
|
||||
ListResult<Post> list = client.list(Post.class, predicate,
|
||||
defaultComparator(), page, size)
|
||||
.block();
|
||||
if (list == null) {
|
||||
return new ListResult<>(0, 0, 0, List.of());
|
||||
}
|
||||
List<PostVo> postVos = list.get()
|
||||
.map(this::getPostVo)
|
||||
.toList();
|
||||
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), postVos);
|
||||
}
|
||||
|
||||
private PostVo getPostVo(@NonNull Post post) {
|
||||
List<TagVo> tags = tagFinder.getByNames(post.getSpec().getTags());
|
||||
List<CategoryVo> categoryVos = categoryFinder.getByNames(post.getSpec().getCategories());
|
||||
List<Contributor> contributors =
|
||||
contributorFinder.getContributors(post.getStatus().getContributors());
|
||||
PostVo postVo = PostVo.from(post);
|
||||
postVo.setCategories(categoryVos);
|
||||
postVo.setTags(tags);
|
||||
postVo.setContributors(contributors);
|
||||
return postVo;
|
||||
}
|
||||
|
||||
static Comparator<Post> defaultComparator() {
|
||||
Function<Post, Boolean> pinned =
|
||||
post -> Objects.requireNonNullElse(post.getSpec().getPinned(), false);
|
||||
Function<Post, Integer> priority =
|
||||
post -> Objects.requireNonNullElse(post.getSpec().getPriority(), 0);
|
||||
Function<Post, Instant> creationTimestamp =
|
||||
post -> post.getMetadata().getCreationTimestamp();
|
||||
Function<Post, String> name = post -> post.getMetadata().getName();
|
||||
return Comparator.comparing(pinned)
|
||||
.thenComparing(priority)
|
||||
.thenComparing(creationTimestamp)
|
||||
.thenComparing(name)
|
||||
.reversed();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import run.halo.app.core.extension.Tag;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
import run.halo.app.theme.finders.vo.TagVo;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link TagFinder}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Finder("tagFinder")
|
||||
public class TagFinderImpl implements TagFinder {
|
||||
|
||||
public static final Comparator<Tag> DEFAULT_COMPARATOR =
|
||||
Comparator.comparing(tag -> tag.getMetadata().getCreationTimestamp());
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public TagFinderImpl(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TagVo getByName(String name) {
|
||||
return client.fetch(Tag.class, name)
|
||||
.map(TagVo::from)
|
||||
.block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TagVo> getByNames(List<String> names) {
|
||||
return names.stream().map(this::getByName)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListResult<TagVo> list(int page, int size) {
|
||||
return client.list(Tag.class, null,
|
||||
DEFAULT_COMPARATOR.reversed(), page, size)
|
||||
.map(list -> {
|
||||
List<TagVo> tagVos = list.stream()
|
||||
.map(TagVo::from)
|
||||
.collect(Collectors.toList());
|
||||
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), tagVos);
|
||||
})
|
||||
.block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TagVo> listAll() {
|
||||
return client.list(Tag.class, null,
|
||||
DEFAULT_COMPARATOR.reversed())
|
||||
.map(TagVo::from)
|
||||
.collectList()
|
||||
.block();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
/**
|
||||
* Base class for category value object.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Getter
|
||||
@SuperBuilder
|
||||
public class BaseCategoryVo {
|
||||
String name;
|
||||
|
||||
String displayName;
|
||||
|
||||
String slug;
|
||||
|
||||
String description;
|
||||
|
||||
String cover;
|
||||
|
||||
String template;
|
||||
|
||||
Integer priority;
|
||||
|
||||
String permalink;
|
||||
|
||||
List<String> posts;
|
||||
|
||||
public int getPostCount() {
|
||||
return posts == null ? 0 : posts.size();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.util.Map;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import run.halo.app.core.extension.Comment;
|
||||
|
||||
@SuperBuilder
|
||||
public class BaseCommentVo {
|
||||
String name;
|
||||
|
||||
String raw;
|
||||
|
||||
String content;
|
||||
|
||||
Comment.CommentOwner owner;
|
||||
|
||||
String userAgent;
|
||||
|
||||
Integer priority;
|
||||
|
||||
Boolean top;
|
||||
|
||||
Boolean allowNotification;
|
||||
|
||||
Map<String, String> annotations;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.core.extension.Category;
|
||||
|
||||
/**
|
||||
* A tree vo for {@link Category}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CategoryTreeVo extends BaseCategoryVo {
|
||||
|
||||
private List<CategoryTreeVo> children;
|
||||
|
||||
private String parentName;
|
||||
|
||||
/**
|
||||
* Convert {@link CategoryVo} to {@link CategoryTreeVo}.
|
||||
*
|
||||
* @param category category value object
|
||||
* @return category tree value object
|
||||
*/
|
||||
public static CategoryTreeVo from(CategoryVo category) {
|
||||
Assert.notNull(category, "The category must not be null");
|
||||
return CategoryTreeVo.builder()
|
||||
.name(category.getName())
|
||||
.displayName(category.getDisplayName())
|
||||
.slug(category.getSlug())
|
||||
.description(category.getDescription())
|
||||
.cover(category.getCover())
|
||||
.template(category.getTemplate())
|
||||
.priority(category.getPriority())
|
||||
.permalink(category.getPermalink())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import run.halo.app.core.extension.Category;
|
||||
|
||||
/**
|
||||
* A value object for {@link Category}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Value
|
||||
@SuperBuilder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CategoryVo extends BaseCategoryVo {
|
||||
|
||||
List<String> children;
|
||||
|
||||
/**
|
||||
* Convert {@link Category} to {@link CategoryVo}.
|
||||
*
|
||||
* @param category category extension
|
||||
* @return category value object
|
||||
*/
|
||||
public static CategoryVo from(Category category) {
|
||||
Category.CategorySpec spec = category.getSpec();
|
||||
return CategoryVo.builder()
|
||||
.name(category.getMetadata().getName())
|
||||
.displayName(spec.getDisplayName())
|
||||
.slug(spec.getSlug())
|
||||
.description(spec.getDescription())
|
||||
.cover(spec.getCover())
|
||||
.template(spec.getTemplate())
|
||||
.priority(spec.getPriority())
|
||||
.children(spec.getChildren())
|
||||
.permalink(category.getStatusOrDefault().getPermalink())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import run.halo.app.core.extension.Comment;
|
||||
|
||||
/**
|
||||
* A value object for {@link Comment}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Value
|
||||
@SuperBuilder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CommentVo extends BaseCommentVo {
|
||||
|
||||
Comment.CommentSubjectRef subjectRef;
|
||||
|
||||
/**
|
||||
* Convert {@link Comment} to {@link CommentVo}.
|
||||
*
|
||||
* @param comment comment extension
|
||||
* @return a value object for {@link Comment}
|
||||
*/
|
||||
public static CommentVo from(Comment comment) {
|
||||
Comment.CommentSpec spec = comment.getSpec();
|
||||
return CommentVo.builder()
|
||||
.name(comment.getMetadata().getName())
|
||||
.subjectRef(spec.getSubjectRef())
|
||||
.raw(spec.getRaw())
|
||||
.content(spec.getContent())
|
||||
.owner(spec.getOwner())
|
||||
.userAgent(spec.getUserAgent())
|
||||
.priority(spec.getPriority())
|
||||
.top(spec.getTop())
|
||||
.allowNotification(spec.getAllowNotification())
|
||||
.annotations(comment.getMetadata().getAnnotations())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import run.halo.app.core.extension.Snapshot;
|
||||
|
||||
/**
|
||||
* A value object for Content from {@link Snapshot}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Value
|
||||
@Builder
|
||||
public class ContentVo {
|
||||
|
||||
String raw;
|
||||
|
||||
String content;
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import run.halo.app.core.extension.MenuItem;
|
||||
|
||||
/**
|
||||
* A value object for {@link MenuItem}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class MenuItemVo {
|
||||
|
||||
String name;
|
||||
|
||||
String parentName;
|
||||
|
||||
String displayName;
|
||||
|
||||
String href;
|
||||
|
||||
Integer priority;
|
||||
|
||||
LinkedHashSet<String> childrenNames;
|
||||
|
||||
List<MenuItemVo> children;
|
||||
|
||||
/**
|
||||
* Convert {@link MenuItem} to {@link MenuItemVo}.
|
||||
*
|
||||
* @param menuItem menu item extension
|
||||
* @return menu item value object
|
||||
*/
|
||||
public static MenuItemVo from(MenuItem menuItem) {
|
||||
MenuItem.MenuItemStatus status = menuItem.getStatus();
|
||||
return MenuItemVo.builder()
|
||||
.name(menuItem.getMetadata().getName())
|
||||
.priority(menuItem.getSpec().getPriority())
|
||||
.childrenNames(menuItem.getSpec().getChildren())
|
||||
.displayName(status.getDisplayName())
|
||||
.href(status.getHref())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
import run.halo.app.core.extension.Menu;
|
||||
|
||||
/**
|
||||
* A value object for {@link Menu}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Value
|
||||
@Builder
|
||||
public class MenuVo {
|
||||
|
||||
String name;
|
||||
|
||||
String displayName;
|
||||
|
||||
@JsonIgnore
|
||||
LinkedHashSet<String> menuItemNames;
|
||||
|
||||
@With
|
||||
List<MenuItemVo> menuItems;
|
||||
|
||||
/**
|
||||
* Convert {@link Menu} to {@link MenuVo}.
|
||||
*
|
||||
* @param menu menu extension
|
||||
* @return menu value object
|
||||
*/
|
||||
public static MenuVo from(Menu menu) {
|
||||
return builder()
|
||||
.name(menu.getMetadata().getName())
|
||||
.displayName(menu.getSpec().getDisplayName())
|
||||
.menuItemNames(menu.getSpec().getMenuItems())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.theme.finders.Contributor;
|
||||
|
||||
/**
|
||||
* A value object for {@link Post}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class PostVo {
|
||||
|
||||
String name;
|
||||
|
||||
String title;
|
||||
|
||||
String slug;
|
||||
|
||||
String owner;
|
||||
|
||||
String template;
|
||||
|
||||
String cover;
|
||||
|
||||
Boolean published;
|
||||
|
||||
Instant publishTime;
|
||||
|
||||
Boolean pinned;
|
||||
|
||||
Boolean allowComment;
|
||||
|
||||
Post.VisibleEnum visible;
|
||||
|
||||
Integer version;
|
||||
|
||||
Integer priority;
|
||||
|
||||
String excerpt;
|
||||
|
||||
List<CategoryVo> categories;
|
||||
|
||||
List<TagVo> tags;
|
||||
|
||||
List<Map<String, String>> htmlMetas;
|
||||
|
||||
String permalink;
|
||||
|
||||
List<Contributor> contributors;
|
||||
|
||||
Map<String, String> annotations;
|
||||
|
||||
/**
|
||||
* Convert {@link Post} to {@link PostVo}.
|
||||
*
|
||||
* @param post post extension
|
||||
* @return post value object
|
||||
*/
|
||||
public static PostVo from(Post post) {
|
||||
Assert.notNull(post, "The post must not be null.");
|
||||
Post.PostSpec spec = post.getSpec();
|
||||
Post.PostStatus postStatus = post.getStatusOrDefault();
|
||||
return PostVo.builder()
|
||||
.name(post.getMetadata().getName())
|
||||
.annotations(post.getMetadata().getAnnotations())
|
||||
.title(spec.getTitle())
|
||||
.cover(spec.getCover())
|
||||
.allowComment(spec.getAllowComment())
|
||||
.categories(List.of())
|
||||
.tags(List.of())
|
||||
.owner(spec.getOwner())
|
||||
.pinned(spec.getPinned())
|
||||
.slug(spec.getSlug())
|
||||
.htmlMetas(nullSafe(spec.getHtmlMetas()))
|
||||
.published(spec.getPublished())
|
||||
.publishTime(spec.getPublishTime())
|
||||
.priority(spec.getPriority())
|
||||
.version(spec.getVersion())
|
||||
.visible(spec.getVisible())
|
||||
.template(spec.getTemplate())
|
||||
.permalink(postStatus.getPermalink())
|
||||
.excerpt(postStatus.getExcerpt())
|
||||
.contributors(List.of())
|
||||
.build();
|
||||
}
|
||||
|
||||
static <T> List<T> nullSafe(List<T> t) {
|
||||
return Objects.requireNonNullElse(t, List.of());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import run.halo.app.core.extension.Reply;
|
||||
|
||||
/**
|
||||
* A value object for {@link Reply}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Value
|
||||
@SuperBuilder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ReplyVo extends BaseCommentVo {
|
||||
|
||||
String commentName;
|
||||
|
||||
String quoteReply;
|
||||
|
||||
/**
|
||||
* Convert {@link Reply} to {@link ReplyVo}.
|
||||
*
|
||||
* @param reply reply extension
|
||||
* @return a value object for {@link Reply}
|
||||
*/
|
||||
public static ReplyVo from(Reply reply) {
|
||||
Reply.ReplySpec spec = reply.getSpec();
|
||||
return ReplyVo.builder()
|
||||
.name(reply.getMetadata().getName())
|
||||
.commentName(spec.getCommentName())
|
||||
.quoteReply(spec.getQuoteReply())
|
||||
.raw(spec.getRaw())
|
||||
.content(spec.getContent())
|
||||
.owner(spec.getOwner())
|
||||
.userAgent(spec.getUserAgent())
|
||||
.priority(spec.getPriority())
|
||||
.top(spec.getTop())
|
||||
.allowNotification(spec.getAllowNotification())
|
||||
.annotations(reply.getMetadata().getAnnotations())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import run.halo.app.core.extension.Tag;
|
||||
|
||||
/**
|
||||
* A value object for {@link Tag}.
|
||||
*/
|
||||
@Value
|
||||
@Builder
|
||||
public class TagVo {
|
||||
|
||||
String name;
|
||||
|
||||
String displayName;
|
||||
|
||||
String slug;
|
||||
|
||||
String color;
|
||||
|
||||
String cover;
|
||||
|
||||
String permalink;
|
||||
|
||||
List<String> posts;
|
||||
|
||||
Map<String, String> annotations;
|
||||
|
||||
/**
|
||||
* Convert {@link Tag} to {@link TagVo}.
|
||||
*
|
||||
* @param tag tag extension
|
||||
* @return tag value object
|
||||
*/
|
||||
public static TagVo from(Tag tag) {
|
||||
Tag.TagSpec spec = tag.getSpec();
|
||||
Tag.TagStatus status = tag.getStatusOrDefault();
|
||||
return TagVo.builder()
|
||||
.name(tag.getMetadata().getName())
|
||||
.displayName(spec.getDisplayName())
|
||||
.slug(spec.getSlug())
|
||||
.color(spec.getColor())
|
||||
.cover(spec.getCover())
|
||||
.permalink(status.getPermalink())
|
||||
.posts(status.getPosts())
|
||||
.annotations(tag.getMetadata().getAnnotations())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package run.halo.app.theme.finders;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
||||
|
||||
/**
|
||||
* Tests for {@link FinderRegistry}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FinderRegistryTest {
|
||||
|
||||
private FinderRegistry finderRegistry;
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
finderRegistry = new FinderRegistry(applicationContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerFinder() {
|
||||
assertThatThrownBy(() -> {
|
||||
finderRegistry.registerFinder(new Object());
|
||||
}).isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Finder must be annotated with @Finder");
|
||||
|
||||
String s = finderRegistry.registerFinder(new FakeFinder());
|
||||
assertThat(s).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeFinder() {
|
||||
String s = finderRegistry.registerFinder(new FakeFinder());
|
||||
assertThat(s).isEqualTo("test");
|
||||
Object test = finderRegistry.get("test");
|
||||
assertThat(test).isNotNull();
|
||||
finderRegistry.removeFinder(s);
|
||||
|
||||
test = finderRegistry.get("test");
|
||||
assertThat(test).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getFinders() {
|
||||
assertThat(finderRegistry.getFinders()).hasSize(0);
|
||||
|
||||
finderRegistry.registerFinder(new FakeFinder());
|
||||
Map<String, Object> finders = finderRegistry.getFinders();
|
||||
assertThat(finders).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onPluginStopped() {
|
||||
finderRegistry.registerFinder("a", new Object());
|
||||
finderRegistry.addPluginFinder("fake", "a");
|
||||
|
||||
HaloPluginStoppedEvent event = Mockito.mock(HaloPluginStoppedEvent.class);
|
||||
PluginWrapper pluginWrapper = Mockito.mock(PluginWrapper.class);
|
||||
when(event.getPlugin()).thenReturn(pluginWrapper);
|
||||
when(pluginWrapper.getPluginId()).thenReturn("fake");
|
||||
|
||||
finderRegistry.onPluginStopped(event);
|
||||
assertThat(finderRegistry.get("a")).isNull();
|
||||
}
|
||||
|
||||
@Finder("test")
|
||||
static class FakeFinder {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.Category;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.theme.finders.vo.CategoryTreeVo;
|
||||
import run.halo.app.theme.finders.vo.CategoryVo;
|
||||
|
||||
/**
|
||||
* Tests for {@link CategoryFinderImpl}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CategoryFinderImplTest {
|
||||
|
||||
@Mock
|
||||
private ReactiveExtensionClient client;
|
||||
|
||||
private CategoryFinderImpl categoryFinder;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
categoryFinder = new CategoryFinderImpl(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getByName() throws JSONException {
|
||||
when(client.fetch(eq(Category.class), eq("hello")))
|
||||
.thenReturn(Mono.just(category()));
|
||||
CategoryVo categoryVo = categoryFinder.getByName("hello");
|
||||
JSONAssert.assertEquals("""
|
||||
{
|
||||
"name": "hello",
|
||||
"displayName": "displayName-1",
|
||||
"slug": "slug-1",
|
||||
"description": "description-1",
|
||||
"cover": "cover-1",
|
||||
"template": "template-1",
|
||||
"priority": 0,
|
||||
"children": [
|
||||
"C1",
|
||||
"C2"
|
||||
],
|
||||
"postCount": 0
|
||||
}
|
||||
""",
|
||||
JsonUtils.objectToJson(categoryVo),
|
||||
true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void list() {
|
||||
ListResult<Category> categories = new ListResult<>(1, 10, 3,
|
||||
categories().stream()
|
||||
.sorted(CategoryFinderImpl.defaultComparator())
|
||||
.toList());
|
||||
when(client.list(eq(Category.class), eq(null), any(), anyInt(), anyInt()))
|
||||
.thenReturn(Mono.just(categories));
|
||||
ListResult<CategoryVo> list = categoryFinder.list(1, 10);
|
||||
assertThat(list.getItems()).hasSize(3);
|
||||
assertThat(list.get().map(CategoryVo::getName).toList())
|
||||
.isEqualTo(List.of("c3", "c2", "hello"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAsTree() {
|
||||
when(client.list(eq(Category.class), eq(null), any()))
|
||||
.thenReturn(Flux.fromIterable(categoriesForTree()));
|
||||
List<CategoryTreeVo> treeVos = categoryFinder.listAsTree();
|
||||
assertThat(treeVos).hasSize(1);
|
||||
}
|
||||
|
||||
private List<Category> categoriesForTree() {
|
||||
/*
|
||||
* D
|
||||
* ├── E
|
||||
* │ ├── A
|
||||
* │ │ └── B
|
||||
* │ └── C
|
||||
* └── G
|
||||
* ├── F
|
||||
* └── H
|
||||
*/
|
||||
Category d = category();
|
||||
d.getMetadata().setName("D");
|
||||
d.getSpec().setChildren(List.of("E", "G", "F"));
|
||||
|
||||
Category e = category();
|
||||
e.getMetadata().setName("E");
|
||||
e.getSpec().setChildren(List.of("A", "C"));
|
||||
|
||||
Category a = category();
|
||||
a.getMetadata().setName("A");
|
||||
a.getSpec().setChildren(List.of("B"));
|
||||
|
||||
Category b = category();
|
||||
b.getMetadata().setName("B");
|
||||
b.getSpec().setChildren(null);
|
||||
|
||||
Category c = category();
|
||||
c.getMetadata().setName("C");
|
||||
c.getSpec().setChildren(null);
|
||||
|
||||
Category g = category();
|
||||
g.getMetadata().setName("G");
|
||||
g.getSpec().setChildren(null);
|
||||
|
||||
Category f = category();
|
||||
f.getMetadata().setName("F");
|
||||
f.getSpec().setChildren(List.of("H"));
|
||||
|
||||
Category h = category();
|
||||
h.getMetadata().setName("H");
|
||||
h.getSpec().setChildren(null);
|
||||
return List.of(d, e, a, b, c, g, f, h);
|
||||
}
|
||||
|
||||
private List<Category> categories() {
|
||||
Category category2 = JsonUtils.deepCopy(category());
|
||||
category2.getMetadata().setName("c2");
|
||||
category2.getSpec().setPriority(2);
|
||||
|
||||
Category category3 = JsonUtils.deepCopy(category());
|
||||
category3.getMetadata().setName("c3");
|
||||
category3.getMetadata().setCreationTimestamp(Instant.now().plusSeconds(20));
|
||||
category3.getSpec().setPriority(2);
|
||||
return List.of(category2, category(), category3);
|
||||
}
|
||||
|
||||
private Category category() {
|
||||
final Category category = new Category();
|
||||
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("hello");
|
||||
metadata.setAnnotations(Map.of("K1", "V1"));
|
||||
metadata.setCreationTimestamp(Instant.now());
|
||||
category.setMetadata(metadata);
|
||||
|
||||
Category.CategorySpec categorySpec = new Category.CategorySpec();
|
||||
categorySpec.setSlug("slug-1");
|
||||
categorySpec.setDisplayName("displayName-1");
|
||||
categorySpec.setCover("cover-1");
|
||||
categorySpec.setDescription("description-1");
|
||||
categorySpec.setTemplate("template-1");
|
||||
categorySpec.setPriority(0);
|
||||
categorySpec.setChildren(List.of("C1", "C2"));
|
||||
category.setSpec(categorySpec);
|
||||
return category;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
import run.halo.app.core.extension.Menu;
|
||||
import run.halo.app.core.extension.MenuItem;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.theme.finders.vo.MenuVo;
|
||||
|
||||
/**
|
||||
* Tests for {@link MenuFinderImpl}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MenuFinderImplTest {
|
||||
|
||||
@Mock
|
||||
private ReactiveExtensionClient client;
|
||||
|
||||
private MenuFinderImpl menuFinder;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
menuFinder = new MenuFinderImpl(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAsTree() throws JSONException {
|
||||
Tuple2<List<Menu>, List<MenuItem>> tuple = testTree();
|
||||
Mockito.when(client.list(eq(Menu.class), eq(null), eq(null)))
|
||||
.thenReturn(Flux.fromIterable(tuple.getT1()));
|
||||
Mockito.when(client.list(eq(MenuItem.class), eq(null), any()))
|
||||
.thenReturn(Flux.fromIterable(tuple.getT2()));
|
||||
|
||||
List<MenuVo> menuVos = menuFinder.listAsTree();
|
||||
JSONAssert.assertEquals("""
|
||||
[
|
||||
{
|
||||
"name": "D",
|
||||
"displayName": "D",
|
||||
"menuItems": [
|
||||
{
|
||||
"name": "E",
|
||||
"priority": 0,
|
||||
"childrenNames": [
|
||||
"A",
|
||||
"C"
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"name": "A",
|
||||
"parentName": "E",
|
||||
"priority": 0,
|
||||
"childrenNames": [
|
||||
"B"
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"name": "B",
|
||||
"parentName": "A",
|
||||
"priority": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "C",
|
||||
"parentName": "E",
|
||||
"priority": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "X",
|
||||
"displayName": "X",
|
||||
"menuItems": [
|
||||
{
|
||||
"name": "G",
|
||||
"priority": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Y",
|
||||
"displayName": "Y",
|
||||
"menuItems": [
|
||||
{
|
||||
"name": "F",
|
||||
"priority": 0,
|
||||
"childrenNames": [
|
||||
"H"
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"name": "H",
|
||||
"parentName": "F",
|
||||
"priority": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
""",
|
||||
JsonUtils.objectToJson(menuVos),
|
||||
true);
|
||||
}
|
||||
|
||||
Tuple2<List<Menu>, List<MenuItem>> testTree() {
|
||||
/*
|
||||
* D
|
||||
* ├── E
|
||||
* │ ├── A
|
||||
* │ │ └── B
|
||||
* │ └── C
|
||||
* X── G
|
||||
* Y── F
|
||||
* └── H
|
||||
*/
|
||||
Menu menuD = menu("D", of("E"));
|
||||
Menu menuX = menu("X", of("G"));
|
||||
Menu menuY = menu("Y", of("F"));
|
||||
|
||||
MenuItem itemE = menuItem("E", of("A", "C"));
|
||||
MenuItem itemG = menuItem("G", null);
|
||||
MenuItem itemF = menuItem("F", of("H"));
|
||||
MenuItem itemA = menuItem("A", of("B"));
|
||||
MenuItem itemB = menuItem("B", null);
|
||||
MenuItem itemC = menuItem("C", null);
|
||||
MenuItem itemH = menuItem("H", null);
|
||||
return Tuples.of(List.of(menuD, menuX, menuY),
|
||||
List.of(itemE, itemG, itemF, itemA, itemB, itemC, itemH));
|
||||
}
|
||||
|
||||
LinkedHashSet<String> of(String... names) {
|
||||
LinkedHashSet<String> list = new LinkedHashSet<>();
|
||||
Collections.addAll(list, names);
|
||||
return list;
|
||||
}
|
||||
|
||||
Menu menu(String name, LinkedHashSet<String> menuItemNames) {
|
||||
Menu menu = new Menu();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName(name);
|
||||
menu.setMetadata(metadata);
|
||||
|
||||
Menu.Spec spec = new Menu.Spec();
|
||||
spec.setDisplayName(name);
|
||||
spec.setMenuItems(menuItemNames);
|
||||
menu.setSpec(spec);
|
||||
return menu;
|
||||
}
|
||||
|
||||
MenuItem menuItem(String name, LinkedHashSet<String> childrenNames) {
|
||||
MenuItem menuItem = new MenuItem();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName(name);
|
||||
menuItem.setMetadata(metadata);
|
||||
|
||||
MenuItem.MenuItemSpec spec = new MenuItem.MenuItemSpec();
|
||||
spec.setPriority(0);
|
||||
spec.setDisplayName(name);
|
||||
spec.setChildren(childrenNames);
|
||||
menuItem.setSpec(spec);
|
||||
|
||||
MenuItem.MenuItemStatus status = new MenuItem.MenuItemStatus();
|
||||
menuItem.setStatus(status);
|
||||
return menuItem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.ContentService;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
import run.halo.app.theme.finders.ContributorFinder;
|
||||
import run.halo.app.theme.finders.TagFinder;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
|
||||
/**
|
||||
* Tests for {@link PostFinderImpl}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PostFinderImplTest {
|
||||
|
||||
@Mock
|
||||
private ReactiveExtensionClient client;
|
||||
|
||||
@Mock
|
||||
private ContentService contentService;
|
||||
|
||||
@Mock
|
||||
private CategoryFinder categoryFinder;
|
||||
|
||||
@Mock
|
||||
private TagFinder tagFinder;
|
||||
|
||||
@Mock
|
||||
private ContributorFinder contributorFinder;
|
||||
|
||||
private PostFinderImpl postFinder;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
postFinder = new PostFinderImpl(client, contentService, tagFinder, categoryFinder,
|
||||
contributorFinder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void content() {
|
||||
Post post = post(1);
|
||||
post.getSpec().setReleaseSnapshot("release-snapshot");
|
||||
ContentWrapper contentWrapper = new ContentWrapper("snapshot", "raw", "content", "rawType");
|
||||
when(client.fetch(eq(Post.class), eq("post-1")))
|
||||
.thenReturn(Mono.just(post));
|
||||
when(contentService.getContent(post.getSpec().getReleaseSnapshot()))
|
||||
.thenReturn(Mono.just(contentWrapper));
|
||||
ContentVo content = postFinder.content("post-1");
|
||||
assertThat(content.getContent()).isEqualTo(contentWrapper.content());
|
||||
assertThat(content.getRaw()).isEqualTo(contentWrapper.raw());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compare() {
|
||||
List<String> strings = posts().stream().sorted(PostFinderImpl.defaultComparator())
|
||||
.map(post -> post.getMetadata().getName())
|
||||
.toList();
|
||||
assertThat(strings).isEqualTo(
|
||||
List.of("post-6", "post-2", "post-1", "post-5", "post-4", "post-3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void predicate() {
|
||||
List<String> strings = posts().stream().filter(PostFinderImpl.FIXED_PREDICATE)
|
||||
.map(post -> post.getMetadata().getName())
|
||||
.toList();
|
||||
assertThat(strings).isEqualTo(List.of("post-1", "post-2", "post-6"));
|
||||
}
|
||||
|
||||
List<Post> posts() {
|
||||
// 置顶的排前面按 priority 排序
|
||||
// 再根据创建时间排序
|
||||
// 相同再根据名称排序
|
||||
// 6, 2, 1, 5, 4, 3
|
||||
Post post1 = post(1);
|
||||
post1.getSpec().setPinned(false);
|
||||
post1.getMetadata().setCreationTimestamp(Instant.now().plusSeconds(20));
|
||||
|
||||
Post post2 = post(2);
|
||||
post2.getSpec().setPinned(true);
|
||||
post2.getSpec().setPriority(2);
|
||||
post2.getMetadata().setCreationTimestamp(Instant.now());
|
||||
|
||||
Post post3 = post(3);
|
||||
post3.getSpec().setDeleted(true);
|
||||
post3.getMetadata().setCreationTimestamp(Instant.now());
|
||||
|
||||
Post post4 = post(4);
|
||||
post4.getSpec().setVisible(Post.VisibleEnum.PRIVATE);
|
||||
post4.getMetadata().setCreationTimestamp(Instant.now());
|
||||
|
||||
Post post5 = post(5);
|
||||
post5.getSpec().setPublished(false);
|
||||
post5.getMetadata().setCreationTimestamp(Instant.now());
|
||||
|
||||
Post post6 = post(6);
|
||||
post6.getSpec().setPinned(true);
|
||||
post6.getSpec().setPriority(3);
|
||||
post6.getMetadata().setCreationTimestamp(Instant.now());
|
||||
|
||||
return List.of(post1, post2, post3, post4, post5, post6);
|
||||
}
|
||||
|
||||
Post post(int i) {
|
||||
final Post post = new Post();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("post-" + i);
|
||||
metadata.setCreationTimestamp(Instant.now());
|
||||
metadata.setAnnotations(Map.of("K1", "V1"));
|
||||
post.setMetadata(metadata);
|
||||
|
||||
Post.PostSpec postSpec = new Post.PostSpec();
|
||||
postSpec.setDeleted(false);
|
||||
postSpec.setAllowComment(true);
|
||||
postSpec.setPublishTime(Instant.now());
|
||||
postSpec.setPinned(false);
|
||||
postSpec.setPriority(0);
|
||||
postSpec.setPublished(true);
|
||||
postSpec.setVisible(Post.VisibleEnum.PUBLIC);
|
||||
postSpec.setTitle("title-" + i);
|
||||
postSpec.setSlug("slug-" + i);
|
||||
post.setSpec(postSpec);
|
||||
|
||||
Post.PostStatus postStatus = new Post.PostStatus();
|
||||
postStatus.setPermalink("/post-" + i);
|
||||
postStatus.setContributors(List.of("contributor-1", "contributor-2"));
|
||||
postStatus.setExcerpt("hello world!");
|
||||
post.setStatus(postStatus);
|
||||
return post;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.Tag;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.theme.finders.vo.TagVo;
|
||||
|
||||
/**
|
||||
* Tests for {@link TagFinderImpl}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TagFinderImplTest {
|
||||
|
||||
@Mock
|
||||
private ReactiveExtensionClient client;
|
||||
|
||||
private TagFinderImpl tagFinder;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
tagFinder = new TagFinderImpl(client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getByName() throws JSONException {
|
||||
when(client.fetch(eq(Tag.class), eq("t1")))
|
||||
.thenReturn(Mono.just(tag(1)));
|
||||
TagVo tagVo = tagFinder.getByName("t1");
|
||||
JSONAssert.assertEquals("""
|
||||
{
|
||||
"name": "t1",
|
||||
"displayName": "displayName-1",
|
||||
"slug": "slug-1",
|
||||
"color": "color-1",
|
||||
"cover": "cover-1",
|
||||
"permalink": "permalink-1",
|
||||
"posts": [
|
||||
"p1",
|
||||
"p2"
|
||||
],
|
||||
"annotations": {
|
||||
"K1": "V1"
|
||||
}
|
||||
}
|
||||
""",
|
||||
JsonUtils.objectToJson(tagVo),
|
||||
true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAll() {
|
||||
when(client.list(eq(Tag.class), eq(null), any()))
|
||||
.thenReturn(Flux.fromIterable(
|
||||
tags().stream().sorted(TagFinderImpl.DEFAULT_COMPARATOR.reversed()).toList()
|
||||
)
|
||||
);
|
||||
List<TagVo> tags = tagFinder.listAll();
|
||||
assertThat(tags).hasSize(3);
|
||||
assertThat(tags.stream().map(TagVo::getName).collect(Collectors.toList()))
|
||||
.isEqualTo(List.of("t3", "t2", "t1"));
|
||||
}
|
||||
|
||||
List<Tag> tags() {
|
||||
Tag tag1 = tag(1);
|
||||
|
||||
Tag tag2 = tag(2);
|
||||
tag2.getMetadata().setCreationTimestamp(Instant.now().plusSeconds(1));
|
||||
|
||||
Tag tag3 = tag(3);
|
||||
tag3.getMetadata().setCreationTimestamp(Instant.now().plusSeconds(2));
|
||||
// sorted: 3, 2, 1
|
||||
return List.of(tag2, tag1, tag3);
|
||||
}
|
||||
|
||||
Tag tag(int i) {
|
||||
final Tag tag = new Tag();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("t" + i);
|
||||
metadata.setAnnotations(Map.of("K1", "V1"));
|
||||
metadata.setCreationTimestamp(Instant.now());
|
||||
tag.setMetadata(metadata);
|
||||
|
||||
Tag.TagSpec tagSpec = new Tag.TagSpec();
|
||||
tagSpec.setDisplayName("displayName-" + i);
|
||||
tagSpec.setSlug("slug-" + i);
|
||||
tagSpec.setColor("color-" + i);
|
||||
tagSpec.setCover("cover-" + i);
|
||||
tag.setSpec(tagSpec);
|
||||
|
||||
Tag.TagStatus tagStatus = new Tag.TagStatus();
|
||||
tagStatus.setPermalink("permalink-" + i);
|
||||
tagStatus.setPosts(List.of("p1", "p2"));
|
||||
tag.setStatus(tagStatus);
|
||||
return tag;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue