feat: theme finders supports the reactive API (#2695)

#### What type of PR is this?
/kind feature
/area core
/milestone 2.0

#### What this PR does / why we need it:
将所有 Finder 的返回值都修改为 Mono 或 Flux

#### Which issue(s) this PR fixes:

Fixes #2671

#### Special notes for your reviewer:
how to test it?
切换几个主题都点一下,没有报错即为正常

/cc @halo-dev/sig-halo 
#### Does this PR introduce a user-facing change?

```release-note
主题端 Finder 支持 Reactive API
```
pull/2712/head
guqing 2022-11-17 10:48:22 +08:00 committed by GitHub
parent cc984b2bb3
commit 9f53cd1123
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 750 additions and 513 deletions

View File

@ -0,0 +1,121 @@
package run.halo.app.theme;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.ArrayList;
import java.util.List;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.integration.json.JsonPropertyAccessor;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.infra.utils.JsonUtils;
/**
* A SpEL PropertyAccessor that knows how to read properties from {@link Mono} or {@link Flux}
* object. It first converts the target to the actual value and then calls other
* {@link PropertyAccessor}s to parse the result, If it still cannot be resolved,
* {@link JsonPropertyAccessor} will be used to resolve finally.
*
* @author guqing
* @since 2.0.0
*/
public class ReactivePropertyAccessor implements PropertyAccessor {
private static final Class<?>[] SUPPORTED_CLASSES = {
Mono.class,
Flux.class
};
private final JsonPropertyAccessor jsonPropertyAccessor = new JsonPropertyAccessor();
@Override
public Class<?>[] getSpecificTargetClasses() {
return SUPPORTED_CLASSES;
}
@Override
public boolean canRead(@NonNull EvaluationContext context, Object target, @NonNull String name)
throws AccessException {
if (target == null) {
return false;
}
return Mono.class.isAssignableFrom(target.getClass())
|| Flux.class.isAssignableFrom(target.getClass());
}
@Override
@NonNull
public TypedValue read(@NonNull EvaluationContext context, Object target, @NonNull String name)
throws AccessException {
if (target == null) {
return TypedValue.NULL;
}
Class<?> clazz = target.getClass();
Object value = null;
if (Mono.class.isAssignableFrom(clazz)) {
value = ((Mono<?>) target).block();
} else if (Flux.class.isAssignableFrom(clazz)) {
value = ((Flux<?>) target).toIterable();
}
if (value == null) {
return TypedValue.NULL;
}
List<PropertyAccessor> propertyAccessorsToTry =
getPropertyAccessorsToTry(value, context.getPropertyAccessors());
for (PropertyAccessor propertyAccessor : propertyAccessorsToTry) {
try {
return propertyAccessor.read(context, target, name);
} catch (AccessException e) {
// ignore
}
}
JsonNode jsonNode = JsonUtils.DEFAULT_JSON_MAPPER.convertValue(value, JsonNode.class);
return jsonPropertyAccessor.read(context, jsonNode, name);
}
private List<PropertyAccessor> getPropertyAccessorsToTry(
@Nullable Object contextObject, List<PropertyAccessor> propertyAccessors) {
Class<?> targetType = (contextObject != null ? contextObject.getClass() : null);
List<PropertyAccessor> specificAccessors = new ArrayList<>();
List<PropertyAccessor> generalAccessors = new ArrayList<>();
for (PropertyAccessor resolver : propertyAccessors) {
Class<?>[] targets = resolver.getSpecificTargetClasses();
if (targets == null) {
// generic resolver that says it can be used for any type
generalAccessors.add(resolver);
} else if (targetType != null) {
for (Class<?> clazz : targets) {
if (clazz == targetType) {
specificAccessors.add(resolver);
break;
} else if (clazz.isAssignableFrom(targetType)) {
generalAccessors.add(resolver);
}
}
}
}
List<PropertyAccessor> resolvers = new ArrayList<>(specificAccessors);
generalAccessors.removeAll(specificAccessors);
resolvers.addAll(generalAccessors);
return resolvers;
}
@Override
public boolean canWrite(@NonNull EvaluationContext context, Object target, @NonNull String name)
throws AccessException {
return false;
}
@Override
public void write(@NonNull EvaluationContext context, Object target, @NonNull String name,
Object newValue)
throws AccessException {
throw new UnsupportedOperationException("Write is not supported");
}
}

View File

@ -0,0 +1,44 @@
package run.halo.app.theme;
import org.thymeleaf.context.IExpressionContext;
import org.thymeleaf.spring6.expression.SPELVariableExpressionEvaluator;
import org.thymeleaf.standard.expression.IStandardVariableExpression;
import org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator;
import org.thymeleaf.standard.expression.StandardExpressionExecutionContext;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Reactive SPEL variable expression evaluator.
*
* @author guqing
* @since 2.0.0
*/
public class ReactiveSpelVariableExpressionEvaluator
implements IStandardVariableExpressionEvaluator {
private final SPELVariableExpressionEvaluator delegate =
SPELVariableExpressionEvaluator.INSTANCE;
public static final ReactiveSpelVariableExpressionEvaluator INSTANCE =
new ReactiveSpelVariableExpressionEvaluator();
@Override
public Object evaluate(IExpressionContext context, IStandardVariableExpression expression,
StandardExpressionExecutionContext expContext) {
Object returnValue = delegate.evaluate(context, expression, expContext);
if (returnValue == null) {
return null;
}
Class<?> clazz = returnValue.getClass();
// Note that: 3 instanceof Foo -> syntax error
if (Mono.class.isAssignableFrom(clazz)) {
return ((Mono<?>) returnValue).block();
}
if (Flux.class.isAssignableFrom(clazz)) {
return ((Flux<?>) returnValue).toIterable();
}
return returnValue;
}
}

View File

@ -9,6 +9,8 @@ import org.springframework.util.ConcurrentLruCache;
import org.springframework.util.ResourceUtils;
import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring6.dialect.SpringStandardDialect;
import org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator;
import org.thymeleaf.templateresolver.FileTemplateResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;
import run.halo.app.infra.ExternalUrlSupplier;
@ -86,6 +88,13 @@ public class TemplateEngineManager {
var mainResolver = haloTemplateResolver();
mainResolver.setPrefix(theme.getPath() + "/templates/");
engine.addTemplateResolver(mainResolver);
// replace StandardDialect with SpringStandardDialect
engine.setDialect(new SpringStandardDialect() {
@Override
public IStandardVariableExpressionEvaluator getVariableExpressionEvaluator() {
return ReactiveSpelVariableExpressionEvaluator.INSTANCE;
}
});
engine.addDialect(new HaloProcessorDialect());
templateResolvers.orderedStream().forEach(engine::addTemplateResolver);

View File

@ -4,9 +4,7 @@ import java.util.Map;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.theme.finders.ThemeFinder;
import run.halo.app.theme.finders.vo.ThemeVo;
/**
* Theme context based variables acquirer.
@ -28,14 +26,11 @@ public class ThemeContextBasedVariablesAcquirer implements ViewContextBasedVaria
@Override
public Mono<Map<String, Object>> acquire(ServerWebExchange exchange) {
return themeResolver.getTheme(exchange.getRequest())
.publishOn(Schedulers.boundedElastic())
.map(themeContext -> {
.flatMap(themeContext -> {
String name = themeContext.getName();
ThemeVo themeVo = themeFinder.getByName(name);
if (themeVo == null) {
return Map.of();
}
return Map.of("theme", themeVo);
});
return themeFinder.getByName(name);
})
.map(themeVo -> Map.<String, Object>of("theme", themeVo))
.defaultIfEmpty(Map.of());
}
}

View File

@ -9,6 +9,7 @@ import org.thymeleaf.processor.templateboundaries.ITemplateBoundariesStructureHa
import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;
import org.thymeleaf.standard.StandardDialect;
import org.thymeleaf.templatemode.TemplateMode;
import run.halo.app.theme.ReactivePropertyAccessor;
/**
* A template boundaries processor for add {@link JsonPropertyAccessor} to
@ -21,6 +22,8 @@ public class JsonNodePropertyAccessorBoundariesProcessor
extends AbstractTemplateBoundariesProcessor {
private static final int PRECEDENCE = StandardDialect.PROCESSOR_PRECEDENCE;
private static final JsonPropertyAccessor JSON_PROPERTY_ACCESSOR = new JsonPropertyAccessor();
private static final ReactivePropertyAccessor REACTIVE_PROPERTY_ACCESSOR =
new ReactivePropertyAccessor();
public JsonNodePropertyAccessorBoundariesProcessor() {
super(TemplateMode.HTML, PRECEDENCE);
@ -34,6 +37,7 @@ public class JsonNodePropertyAccessorBoundariesProcessor
ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME);
if (evaluationContext != null) {
evaluationContext.addPropertyAccessor(JSON_PROPERTY_ACCESSOR);
evaluationContext.addPropertyAccessor(REACTIVE_PROPERTY_ACCESSOR);
}
}

View File

@ -36,7 +36,7 @@ public class PostTemplateHeadProcessor implements TemplateHeadProcessor {
return Mono.empty();
}
return Mono.justOrEmpty((String) context.getVariable(POST_NAME_VARIABLE))
.map(postFinder::getByName)
.flatMap(postFinder::getByName)
.doOnNext(postVo -> {
List<Map<String, String>> htmlMetas = postVo.getSpec().getHtmlMetas();
String metaHtml = headMetaBuilder(htmlMetas);

View File

@ -165,10 +165,8 @@ public class CommentFinderEndpoint implements CustomEndpoint {
Mono<ServerResponse> listComments(ServerRequest request) {
CommentQuery commentQuery = new CommentQuery(request.queryParams());
return Mono.defer(() -> Mono.just(
commentFinder.list(commentQuery.toRef(), commentQuery.getPage(),
commentQuery.getSize())))
.subscribeOn(Schedulers.boundedElastic())
return commentFinder.list(commentQuery.toRef(), commentQuery.getPage(),
commentQuery.getSize())
.flatMap(list -> ServerResponse.ok().bodyValue(list));
}
@ -183,9 +181,7 @@ public class CommentFinderEndpoint implements CustomEndpoint {
String commentName = request.pathVariable("name");
IListRequest.QueryListRequest queryParams =
new IListRequest.QueryListRequest(request.queryParams());
return Mono.defer(() -> Mono.just(
commentFinder.listReply(commentName, queryParams.getPage(), queryParams.getSize())))
.subscribeOn(Schedulers.boundedElastic())
return commentFinder.listReply(commentName, queryParams.getPage(), queryParams.getSize())
.flatMap(list -> ServerResponse.ok().bodyValue(list));
}

View File

@ -2,6 +2,8 @@ package run.halo.app.theme.finders;
import java.util.List;
import org.springframework.lang.Nullable;
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.theme.finders.vo.CategoryTreeVo;
@ -15,13 +17,13 @@ import run.halo.app.theme.finders.vo.CategoryVo;
*/
public interface CategoryFinder {
CategoryVo getByName(String name);
Mono<CategoryVo> getByName(String name);
List<CategoryVo> getByNames(List<String> names);
Flux<CategoryVo> getByNames(List<String> names);
ListResult<CategoryVo> list(@Nullable Integer page, @Nullable Integer size);
Mono<ListResult<CategoryVo>> list(@Nullable Integer page, @Nullable Integer size);
List<CategoryVo> listAll();
Flux<CategoryVo> listAll();
List<CategoryTreeVo> listAsTree();
Flux<CategoryTreeVo> listAsTree();
}

View File

@ -1,6 +1,7 @@
package run.halo.app.theme.finders;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Comment;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Ref;
@ -15,11 +16,11 @@ import run.halo.app.theme.finders.vo.ReplyVo;
*/
public interface CommentFinder {
CommentVo getByName(String name);
Mono<CommentVo> getByName(String name);
ListResult<CommentVo> list(Ref ref, @Nullable Integer page,
Mono<ListResult<CommentVo>> list(Ref ref, @Nullable Integer page,
@Nullable Integer size);
ListResult<ReplyVo> listReply(String commentName, @Nullable Integer page,
Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page,
@Nullable Integer size);
}

View File

@ -1,6 +1,8 @@
package run.halo.app.theme.finders;
import java.util.List;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.User;
import run.halo.app.theme.finders.vo.Contributor;
@ -9,7 +11,7 @@ import run.halo.app.theme.finders.vo.Contributor;
*/
public interface ContributorFinder {
Contributor getContributor(String name);
Mono<Contributor> getContributor(String name);
List<Contributor> getContributors(List<String> names);
Flux<Contributor> getContributors(List<String> names);
}

View File

@ -1,5 +1,6 @@
package run.halo.app.theme.finders;
import reactor.core.publisher.Mono;
import run.halo.app.theme.finders.vo.MenuVo;
/**
@ -10,7 +11,7 @@ import run.halo.app.theme.finders.vo.MenuVo;
*/
public interface MenuFinder {
MenuVo getByName(String name);
Mono<MenuVo> getByName(String name);
MenuVo getPrimary();
Mono<MenuVo> getPrimary();
}

View File

@ -1,9 +1,11 @@
package run.halo.app.theme.finders;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Mono;
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.ListedPostVo;
import run.halo.app.theme.finders.vo.NavigationPostVo;
import run.halo.app.theme.finders.vo.PostArchiveVo;
import run.halo.app.theme.finders.vo.PostVo;
@ -16,22 +18,23 @@ import run.halo.app.theme.finders.vo.PostVo;
*/
public interface PostFinder {
PostVo getByName(String postName);
Mono<PostVo> getByName(String postName);
ContentVo content(String postName);
Mono<ContentVo> content(String postName);
NavigationPostVo cursor(String current);
Mono<NavigationPostVo> cursor(String current);
ListResult<PostVo> list(@Nullable Integer page, @Nullable Integer size);
Mono<ListResult<ListedPostVo>> list(@Nullable Integer page, @Nullable Integer size);
ListResult<PostVo> listByCategory(@Nullable Integer page, @Nullable Integer size,
Mono<ListResult<ListedPostVo>> listByCategory(@Nullable Integer page, @Nullable Integer size,
String categoryName);
ListResult<PostVo> listByTag(@Nullable Integer page, @Nullable Integer size, String tag);
Mono<ListResult<ListedPostVo>> listByTag(@Nullable Integer page, @Nullable Integer size,
String tag);
ListResult<PostArchiveVo> archives(Integer page, Integer size);
Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size);
ListResult<PostArchiveVo> archives(Integer page, Integer size, String year);
Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year);
ListResult<PostArchiveVo> archives(Integer page, Integer size, String year, String month);
Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year, String month);
}

View File

@ -1,9 +1,11 @@
package run.halo.app.theme.finders;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.SinglePage;
import run.halo.app.extension.ListResult;
import run.halo.app.theme.finders.vo.ContentVo;
import run.halo.app.theme.finders.vo.ListedSinglePageVo;
import run.halo.app.theme.finders.vo.SinglePageVo;
/**
@ -14,9 +16,9 @@ import run.halo.app.theme.finders.vo.SinglePageVo;
*/
public interface SinglePageFinder {
SinglePageVo getByName(String pageName);
Mono<SinglePageVo> getByName(String pageName);
ContentVo content(String pageName);
Mono<ContentVo> content(String pageName);
ListResult<SinglePageVo> list(@Nullable Integer page, @Nullable Integer size);
Mono<ListResult<ListedSinglePageVo>> list(@Nullable Integer page, @Nullable Integer size);
}

View File

@ -1,5 +1,6 @@
package run.halo.app.theme.finders;
import reactor.core.publisher.Mono;
import run.halo.app.theme.finders.vo.SiteStatsVo;
/**
@ -10,5 +11,5 @@ import run.halo.app.theme.finders.vo.SiteStatsVo;
*/
public interface SiteStatsFinder {
SiteStatsVo getStats();
Mono<SiteStatsVo> getStats();
}

View File

@ -2,6 +2,8 @@ package run.halo.app.theme.finders;
import java.util.List;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Tag;
import run.halo.app.extension.ListResult;
import run.halo.app.theme.finders.vo.TagVo;
@ -14,11 +16,11 @@ import run.halo.app.theme.finders.vo.TagVo;
*/
public interface TagFinder {
TagVo getByName(String name);
Mono<TagVo> getByName(String name);
List<TagVo> getByNames(List<String> names);
Flux<TagVo> getByNames(List<String> names);
ListResult<TagVo> list(@Nullable Integer page, @Nullable Integer size);
Mono<ListResult<TagVo>> list(@Nullable Integer page, @Nullable Integer size);
List<TagVo> listAll();
Flux<TagVo> listAll();
}

View File

@ -1,5 +1,6 @@
package run.halo.app.theme.finders;
import reactor.core.publisher.Mono;
import run.halo.app.theme.finders.vo.ThemeVo;
/**
@ -10,7 +11,7 @@ import run.halo.app.theme.finders.vo.ThemeVo;
*/
public interface ThemeFinder {
ThemeVo activation();
Mono<ThemeVo> activation();
ThemeVo getByName(String themeName);
Mono<ThemeVo> getByName(String themeName);
}

View File

@ -10,6 +10,8 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
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.ReactiveExtensionClient;
@ -34,24 +36,22 @@ public class CategoryFinderImpl implements CategoryFinder {
}
@Override
public CategoryVo getByName(String name) {
public Mono<CategoryVo> getByName(String name) {
return client.fetch(Category.class, name)
.map(CategoryVo::from)
.block();
.map(CategoryVo::from);
}
@Override
public List<CategoryVo> getByNames(List<String> names) {
public Flux<CategoryVo> getByNames(List<String> names) {
if (names == null) {
return List.of();
return Flux.empty();
}
return names.stream().map(this::getByName)
.filter(Objects::nonNull)
.toList();
return Flux.fromIterable(names)
.flatMap(this::getByName);
}
@Override
public ListResult<CategoryVo> list(Integer page, Integer size) {
public Mono<ListResult<CategoryVo>> list(Integer page, Integer size) {
return client.list(Category.class, null,
defaultComparator(), pageNullSafe(page), sizeNullSafe(size))
.map(list -> {
@ -61,38 +61,39 @@ public class CategoryFinderImpl implements CategoryFinder {
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
categoryVos);
})
.block();
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()));
}
@Override
public List<CategoryVo> listAll() {
public Flux<CategoryVo> listAll() {
return client.list(Category.class, null, defaultComparator())
.map(CategoryVo::from)
.collectList()
.block();
.map(CategoryVo::from);
}
@Override
public List<CategoryTreeVo> listAsTree() {
List<CategoryVo> categoryVos = listAll();
Map<String, CategoryTreeVo> nameIdentityMap = categoryVos.stream()
.map(CategoryTreeVo::from)
.collect(Collectors.toMap(categoryVo -> categoryVo.getMetadata().getName(),
Function.identity()));
public Flux<CategoryTreeVo> listAsTree() {
return listAll()
.collectList()
.flatMapIterable(categoryVos -> {
Map<String, CategoryTreeVo> nameIdentityMap = categoryVos.stream()
.map(CategoryTreeVo::from)
.collect(Collectors.toMap(categoryVo -> categoryVo.getMetadata().getName(),
Function.identity()));
nameIdentityMap.forEach((name, value) -> {
List<String> children = value.getSpec().getChildren();
if (children == null) {
return;
}
for (String child : children) {
CategoryTreeVo childNode = nameIdentityMap.get(child);
if (childNode != null) {
childNode.setParentName(name);
}
}
});
return listToTree(nameIdentityMap.values());
nameIdentityMap.forEach((name, value) -> {
List<String> children = value.getSpec().getChildren();
if (children == null) {
return;
}
for (String child : children) {
CategoryTreeVo childNode = nameIdentityMap.get(child);
if (childNode != null) {
childNode.setParentName(name);
}
}
});
return listToTree(nameIdentityMap.values());
});
}
static List<CategoryTreeVo> listToTree(Collection<CategoryTreeVo> list) {

View File

@ -2,6 +2,7 @@ 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;
@ -37,14 +38,13 @@ public class CommentFinderImpl implements CommentFinder {
}
@Override
public CommentVo getByName(String name) {
public Mono<CommentVo> getByName(String name) {
return client.fetch(Comment.class, name)
.flatMap(this::toCommentVo)
.block();
.flatMap(this::toCommentVo);
}
@Override
public ListResult<CommentVo> list(Ref ref, Integer page, Integer size) {
public Mono<ListResult<CommentVo>> list(Ref ref, Integer page, Integer size) {
return client.list(Comment.class, fixedPredicate(ref),
defaultComparator(),
pageNullSafe(page), sizeNullSafe(size))
@ -55,11 +55,11 @@ public class CommentFinderImpl implements CommentFinder {
commentVos)
)
)
.block();
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()));
}
@Override
public ListResult<ReplyVo> listReply(String commentName, Integer page, Integer size) {
public Mono<ListResult<ReplyVo>> listReply(String commentName, Integer page, Integer size) {
Comparator<Reply> comparator =
Comparator.comparing(reply -> reply.getMetadata().getCreationTimestamp());
return client.list(Reply.class,
@ -73,7 +73,7 @@ public class CommentFinderImpl implements CommentFinder {
.map(replyVos -> new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
replyVos))
)
.block();
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()));
}
private Mono<CommentVo> toCommentVo(Comment comment) {
@ -81,6 +81,7 @@ public class CommentFinderImpl implements CommentFinder {
return Mono.just(CommentVo.from(comment))
.flatMap(commentVo -> getOwnerInfo(owner)
.map(commentVo::withOwner)
.defaultIfEmpty(commentVo)
);
}
@ -88,6 +89,7 @@ public class CommentFinderImpl implements CommentFinder {
return Mono.just(ReplyVo.from(reply))
.flatMap(replyVo -> getOwnerInfo(reply.getSpec().getOwner())
.map(replyVo::withOwner)
.defaultIfEmpty(replyVo)
);
}
@ -97,7 +99,7 @@ public class CommentFinderImpl implements CommentFinder {
}
return client.fetch(User.class, owner.getName())
.map(OwnerInfo::from)
.switchIfEmpty(Mono.just(OwnerInfo.ghostUser()));
.defaultIfEmpty(OwnerInfo.ghostUser());
}
private Predicate<Comment> fixedPredicate(Ref ref) {

View File

@ -1,7 +1,8 @@
package run.halo.app.theme.finders.impl;
import java.util.List;
import java.util.Objects;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.User;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.theme.finders.ContributorFinder;
@ -24,20 +25,17 @@ public class ContributorFinderImpl implements ContributorFinder {
}
@Override
public Contributor getContributor(String name) {
public Mono<Contributor> getContributor(String name) {
return client.fetch(User.class, name)
.map(Contributor::from)
.block();
.map(Contributor::from);
}
@Override
public List<Contributor> getContributors(List<String> names) {
public Flux<Contributor> getContributors(List<String> names) {
if (names == null) {
return List.of();
return Flux.empty();
}
return names.stream()
.map(this::getContributor)
.filter(Objects::nonNull)
.toList();
return Flux.fromIterable(names)
.flatMap(this::getContributor);
}
}

View File

@ -10,9 +10,10 @@ import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.comparator.Comparators;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Menu;
import run.halo.app.core.extension.MenuItem;
import run.halo.app.extension.ReactiveExtensionClient;
@ -37,56 +38,58 @@ public class MenuFinderImpl implements MenuFinder {
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
@Override
public MenuVo getByName(String name) {
return listAsTree().stream()
public Mono<MenuVo> getByName(String name) {
return listAsTree()
.filter(menu -> menu.getMetadata().getName().equals(name))
.findAny()
.orElse(null);
.next();
}
@Override
public MenuVo getPrimary() {
List<MenuVo> menuVos = listAsTree();
if (CollectionUtils.isEmpty(menuVos)) {
return null;
}
return environmentFetcher.fetch(SystemSetting.Menu.GROUP, SystemSetting.Menu.class)
.blockOptional()
.map(SystemSetting.Menu::getPrimary)
.filter(StringUtils::isNotBlank)
.flatMap(primary -> menuVos.stream()
.filter(menuVo -> menuVo.getMetadata().getName().equals(primary))
.findAny())
.orElse(menuVos.get(0));
}
List<MenuVo> listAll() {
return client.list(Menu.class, null, null)
.map(MenuVo::from)
.collectList()
.block();
}
List<MenuVo> listAsTree() {
Collection<MenuItemVo> menuItemVos = populateParentName(listAllMenuItem());
List<MenuItemVo> treeList = listToTree(menuItemVos);
Map<String, MenuItemVo> nameItemRootNodeMap = treeList.stream()
.collect(Collectors.toMap(item -> item.getMetadata().getName(), Function.identity()));
return listAll().stream()
.map(menuVo -> {
LinkedHashSet<String> menuItemNames = menuVo.getSpec().getMenuItems();
if (menuItemNames == null) {
return menuVo.withMenuItems(List.of());
public Mono<MenuVo> getPrimary() {
return listAsTree().collectList()
.flatMap(menuVos -> {
if (CollectionUtils.isEmpty(menuVos)) {
return Mono.empty();
}
List<MenuItemVo> menuItems = menuItemNames.stream()
.map(nameItemRootNodeMap::get)
.filter(Objects::nonNull)
.sorted(defaultTreeNodeComparator())
.toList();
return menuVo.withMenuItems(menuItems);
})
.toList();
return environmentFetcher.fetch(SystemSetting.Menu.GROUP, SystemSetting.Menu.class)
.map(SystemSetting.Menu::getPrimary)
.map(primaryConfig -> menuVos.stream()
.filter(menuVo -> menuVo.getMetadata().getName().equals(primaryConfig))
.findAny()
.orElse(menuVos.get(0))
)
.defaultIfEmpty(menuVos.get(0));
});
}
Flux<MenuVo> listAll() {
return client.list(Menu.class, null, null)
.map(MenuVo::from);
}
Flux<MenuVo> listAsTree() {
return listAllMenuItem()
.collectList()
.map(MenuFinderImpl::populateParentName)
.flatMapMany(menuItemVos -> {
List<MenuItemVo> treeList = listToTree(menuItemVos);
Map<String, MenuItemVo> nameItemRootNodeMap = treeList.stream()
.collect(Collectors.toMap(item -> item.getMetadata().getName(),
Function.identity()));
return listAll()
.map(menuVo -> {
LinkedHashSet<String> menuItemNames = menuVo.getSpec().getMenuItems();
if (menuItemNames == null) {
return menuVo.withMenuItems(List.of());
}
List<MenuItemVo> menuItems = menuItemNames.stream()
.map(nameItemRootNodeMap::get)
.filter(Objects::nonNull)
.sorted(defaultTreeNodeComparator())
.toList();
return menuVo.withMenuItems(menuItems);
});
});
}
static List<MenuItemVo> listToTree(Collection<MenuItemVo> list) {
@ -109,11 +112,9 @@ public class MenuFinderImpl implements MenuFinder {
.collect(Collectors.toList());
}
List<MenuItemVo> listAllMenuItem() {
Flux<MenuItemVo> listAllMenuItem() {
return client.list(MenuItem.class, null, null)
.map(MenuItemVo::from)
.collectList()
.block();
.map(MenuItemVo::from);
}
static Comparator<MenuItemVo> defaultTreeNodeComparator() {

View File

@ -15,7 +15,10 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.lang.NonNull;
import org.springframework.util.CollectionUtils;
import org.springframework.util.comparator.Comparators;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import run.halo.app.content.ContentService;
import run.halo.app.core.extension.Counter;
@ -30,15 +33,13 @@ 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.Contributor;
import run.halo.app.theme.finders.vo.ListedPostVo;
import run.halo.app.theme.finders.vo.NavigationPostVo;
import run.halo.app.theme.finders.vo.PostArchiveVo;
import run.halo.app.theme.finders.vo.PostArchiveYearMonthVo;
import run.halo.app.theme.finders.vo.PostVo;
import run.halo.app.theme.finders.vo.StatsVo;
import run.halo.app.theme.finders.vo.TagVo;
/**
* A finder for {@link Post}.
@ -50,8 +51,8 @@ import run.halo.app.theme.finders.vo.TagVo;
public class PostFinderImpl implements PostFinder {
public static final Predicate<Post> FIXED_PREDICATE = post -> post.isPublished()
&& Objects.equals(false, post.getSpec().getDeleted())
&& Post.VisibleEnum.PUBLIC.equals(post.getSpec().getVisible());
&& Objects.equals(false, post.getSpec().getDeleted())
&& Post.VisibleEnum.PUBLIC.equals(post.getSpec().getVisible());
private final ReactiveExtensionClient client;
private final ContentService contentService;
@ -78,53 +79,49 @@ public class PostFinderImpl implements PostFinder {
}
@Override
public PostVo getByName(String postName) {
Post post = client.fetch(Post.class, postName)
.block();
if (post == null) {
return null;
}
PostVo postVo = getPostVo(post);
postVo.setContent(content(postName));
return postVo;
public Mono<PostVo> getByName(String postName) {
return client.fetch(Post.class, postName)
.flatMap(this::getListedPostVo)
.map(PostVo::from)
.flatMap(postVo -> content(postName)
.doOnNext(postVo::setContent)
.thenReturn(postVo)
);
}
@Override
public ContentVo content(String postName) {
public Mono<ContentVo> content(String postName) {
return client.fetch(Post.class, postName)
.map(post -> post.getSpec().getReleaseSnapshot())
.flatMap(contentService::getContent)
.map(wrapper -> ContentVo.builder().content(wrapper.getContent())
.raw(wrapper.getRaw()).build())
.block();
.raw(wrapper.getRaw()).build());
}
@Override
public NavigationPostVo cursor(String currentName) {
public Mono<NavigationPostVo> cursor(String currentName) {
// TODO Optimize the post names query here
List<String> postNames = client.list(Post.class, FIXED_PREDICATE, defaultComparator())
return client.list(Post.class, FIXED_PREDICATE, defaultComparator())
.map(post -> post.getMetadata().getName())
.collectList()
.block();
if (postNames == null) {
return NavigationPostVo.empty();
}
NavigationPostVo.NavigationPostVoBuilder builder = NavigationPostVo.builder()
.current(getByName(currentName));
Pair<String, String> previousNextPair = postPreviousNextPair(postNames, currentName);
String previousPostName = previousNextPair.getLeft();
String nextPostName = previousNextPair.getRight();
if (previousPostName != null) {
builder.previous(getByName(previousPostName));
}
if (nextPostName != null) {
builder.next(getByName(nextPostName));
}
return builder.build();
.flatMap(postNames -> Mono.just(NavigationPostVo.builder())
.flatMap(builder -> getByName(currentName)
.doOnNext(builder::current)
.thenReturn(builder)
)
.flatMap(builder -> {
Pair<String, String> previousNextPair =
postPreviousNextPair(postNames, currentName);
String previousPostName = previousNextPair.getLeft();
String nextPostName = previousNextPair.getRight();
return getByName(previousPostName)
.doOnNext(builder::previous)
.then(getByName(nextPostName))
.doOnNext(builder::next)
.thenReturn(builder);
})
.map(NavigationPostVo.NavigationPostVoBuilder::build))
.defaultIfEmpty(NavigationPostVo.empty());
}
static Pair<String, String> postPreviousNextPair(List<String> postNames,
@ -201,36 +198,37 @@ public class PostFinderImpl implements PostFinder {
}
@Override
public ListResult<PostVo> list(Integer page, Integer size) {
public Mono<ListResult<ListedPostVo>> list(Integer page, Integer size) {
return listPost(page, size, null, defaultComparator());
}
@Override
public ListResult<PostVo> listByCategory(Integer page, Integer size, String categoryName) {
public Mono<ListResult<ListedPostVo>> listByCategory(Integer page, Integer size,
String categoryName) {
return listPost(page, size,
post -> contains(post.getSpec().getCategories(), categoryName), defaultComparator());
}
@Override
public ListResult<PostVo> listByTag(Integer page, Integer size, String tag) {
public Mono<ListResult<ListedPostVo>> listByTag(Integer page, Integer size, String tag) {
return listPost(page, size,
post -> contains(post.getSpec().getTags(), tag), defaultComparator());
}
@Override
public ListResult<PostArchiveVo> archives(Integer page, Integer size) {
public Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size) {
return archives(page, size, null, null);
}
@Override
public ListResult<PostArchiveVo> archives(Integer page, Integer size, String year) {
public Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year) {
return archives(page, size, year, null);
}
@Override
public ListResult<PostArchiveVo> archives(Integer page, Integer size, String year,
public Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year,
String month) {
ListResult<PostVo> list = listPost(page, size, post -> {
return listPost(page, size, post -> {
Map<String, String> labels = post.getMetadata().getLabels();
if (labels == null) {
return false;
@ -240,34 +238,37 @@ public class PostFinderImpl implements PostFinder {
boolean monthMatch = StringUtils.isBlank(month)
|| month.equals(labels.get(Post.ARCHIVE_MONTH_LABEL));
return yearMatch && monthMatch;
}, archiveComparator());
Map<String, List<PostVo>> yearPosts = list.get()
.collect(Collectors.groupingBy(
post -> HaloUtils.getYearText(post.getSpec().getPublishTime())));
List<PostArchiveVo> postArchives =
yearPosts.entrySet().stream().map(entry -> {
String key = entry.getKey();
// archives by month
Map<String, List<PostVo>> monthPosts = entry.getValue().stream()
}, archiveComparator())
.map(list -> {
Map<String, List<ListedPostVo>> yearPosts = list.get()
.collect(Collectors.groupingBy(
post -> HaloUtils.getMonthText(post.getSpec().getPublishTime())));
// convert to archive year month value objects
List<PostArchiveYearMonthVo> monthArchives = monthPosts.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.map(monthEntry -> PostArchiveYearMonthVo.builder()
.posts(monthEntry.getValue())
.month(monthEntry.getKey())
.build()
)
.toList();
return PostArchiveVo.builder()
.year(String.valueOf(key))
.months(monthArchives)
.build();
}).toList();
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), postArchives);
post -> HaloUtils.getYearText(post.getSpec().getPublishTime())));
List<PostArchiveVo> postArchives =
yearPosts.entrySet().stream().map(entry -> {
String key = entry.getKey();
// archives by month
Map<String, List<ListedPostVo>> monthPosts = entry.getValue().stream()
.collect(Collectors.groupingBy(
post -> HaloUtils.getMonthText(post.getSpec().getPublishTime())));
// convert to archive year month value objects
List<PostArchiveYearMonthVo> monthArchives = monthPosts.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.map(monthEntry -> PostArchiveYearMonthVo.builder()
.posts(monthEntry.getValue())
.month(monthEntry.getKey())
.build()
)
.toList();
return PostArchiveVo.builder()
.year(String.valueOf(key))
.months(monthArchives)
.build();
}).toList();
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
postArchives);
})
.defaultIfEmpty(new ListResult<>(page, size, 0, List.of()));
}
private boolean contains(List<String> c, String key) {
@ -277,24 +278,29 @@ public class PostFinderImpl implements PostFinder {
return c.contains(key);
}
private ListResult<PostVo> listPost(Integer page, Integer size, Predicate<Post> postPredicate,
private Mono<ListResult<ListedPostVo>> listPost(Integer page, Integer size,
Predicate<Post> postPredicate,
Comparator<Post> comparator) {
Predicate<Post> predicate = FIXED_PREDICATE
.and(postPredicate == null ? post -> true : postPredicate);
ListResult<Post> list = client.list(Post.class, predicate,
return client.list(Post.class, predicate,
comparator, pageNullSafe(page), sizeNullSafe(size))
.block();
if (list == null) {
return new ListResult<>(List.of());
}
List<PostVo> postVos = list.get()
.map(this::getPostVo)
.peek(this::populateStats)
.toList();
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), postVos);
.flatMap(list -> Flux.fromStream(list.get())
.flatMap(post -> getListedPostVo(post)
.map(postVo -> {
populateStats(postVo);
return postVo;
})
)
.collectList()
.map(postVos -> new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
postVos)
)
)
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()));
}
private void populateStats(PostVo postVo) {
private <T extends ListedPostVo> void populateStats(T postVo) {
Counter counter =
counterService.getByName(MeterUtils.nameOf(Post.class, postVo.getMetadata()
.getName()));
@ -306,18 +312,45 @@ public class PostFinderImpl implements PostFinder {
postVo.setStats(statsVo);
}
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);
postVo.setOwner(contributorFinder.getContributor(post.getSpec().getOwner()));
private Mono<ListedPostVo> getListedPostVo(@NonNull Post post) {
ListedPostVo postVo = ListedPostVo.from(post);
postVo.setCategories(List.of());
postVo.setTags(List.of());
postVo.setContributors(List.of());
populateStats(postVo);
return postVo;
return Mono.just(postVo)
.flatMap(p -> {
String owner = p.getSpec().getOwner();
return contributorFinder.getContributor(owner)
.doOnNext(p::setOwner)
.thenReturn(p);
})
.flatMap(p -> {
List<String> tagNames = p.getSpec().getTags();
if (CollectionUtils.isEmpty(tagNames)) {
return Mono.just(p);
}
return tagFinder.getByNames(tagNames)
.collectList()
.doOnNext(p::setTags)
.thenReturn(p);
})
.flatMap(p -> {
List<String> categoryNames = p.getSpec().getCategories();
if (CollectionUtils.isEmpty(categoryNames)) {
return Mono.just(p);
}
return categoryFinder.getByNames(categoryNames)
.collectList()
.doOnNext(p::setCategories)
.thenReturn(p);
})
.flatMap(p -> contributorFinder.getContributors(p.getStatus().getContributors())
.collectList()
.doOnNext(p::setContributors)
.thenReturn(p)
)
.defaultIfEmpty(postVo);
}
static Comparator<Post> defaultComparator() {

View File

@ -7,6 +7,9 @@ import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.content.ContentService;
import run.halo.app.core.extension.Counter;
import run.halo.app.core.extension.Post;
@ -19,7 +22,7 @@ import run.halo.app.theme.finders.ContributorFinder;
import run.halo.app.theme.finders.Finder;
import run.halo.app.theme.finders.SinglePageFinder;
import run.halo.app.theme.finders.vo.ContentVo;
import run.halo.app.theme.finders.vo.Contributor;
import run.halo.app.theme.finders.vo.ListedSinglePageVo;
import run.halo.app.theme.finders.vo.SinglePageVo;
import run.halo.app.theme.finders.vo.StatsVo;
@ -53,54 +56,56 @@ public class SinglePageFinderImpl implements SinglePageFinder {
}
@Override
public SinglePageVo getByName(String pageName) {
SinglePage page = client.fetch(SinglePage.class, pageName)
.block();
if (page == null) {
return null;
}
List<Contributor> contributors =
contributorFinder.getContributors(page.getStatus().getContributors());
SinglePageVo pageVo = SinglePageVo.from(page);
pageVo.setContributors(contributors);
pageVo.setContent(content(pageName));
pageVo.setOwner(contributorFinder.getContributor(page.getSpec().getOwner()));
populateStats(pageVo);
return pageVo;
public Mono<SinglePageVo> getByName(String pageName) {
return client.fetch(SinglePage.class, pageName)
.map(page -> {
SinglePageVo pageVo = SinglePageVo.from(page);
pageVo.setContributors(List.of());
pageVo.setContent(ContentVo.empty());
populateStats(pageVo);
return pageVo;
})
.flatMap(this::populateContributors)
.flatMap(page -> content(pageName)
.doOnNext(page::setContent)
.thenReturn(page)
)
.flatMap(page -> contributorFinder.getContributor(page.getSpec().getOwner())
.doOnNext(page::setOwner)
.thenReturn(page)
);
}
@Override
public ContentVo content(String pageName) {
public Mono<ContentVo> content(String pageName) {
return client.fetch(SinglePage.class, pageName)
.map(page -> page.getSpec().getReleaseSnapshot())
.flatMap(contentService::getContent)
.map(wrapper -> ContentVo.builder().content(wrapper.getContent())
.raw(wrapper.getRaw()).build())
.block();
.raw(wrapper.getRaw()).build());
}
@Override
public ListResult<SinglePageVo> list(Integer page, Integer size) {
ListResult<SinglePage> list = client.list(SinglePage.class, FIXED_PREDICATE,
public Mono<ListResult<ListedSinglePageVo>> list(Integer page, Integer size) {
return client.list(SinglePage.class, FIXED_PREDICATE,
defaultComparator(), pageNullSafe(page), sizeNullSafe(size))
.block();
if (list == null) {
return new ListResult<>(0, 0, 0, List.of());
}
List<SinglePageVo> pageVos = list.get()
.map(sp -> {
List<Contributor> contributors =
contributorFinder.getContributors(sp.getStatus().getContributors());
SinglePageVo pageVo = SinglePageVo.from(sp);
pageVo.setContributors(contributors);
populateStats(pageVo);
return pageVo;
})
.toList();
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), pageVos);
.flatMap(list -> Flux.fromStream(list.get())
.map(singlePage -> {
ListedSinglePageVo pageVo = ListedSinglePageVo.from(singlePage);
pageVo.setContributors(List.of());
populateStats(pageVo);
return pageVo;
})
.flatMap(this::populateContributors)
.collectList()
.map(pageVos -> new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
pageVos)
)
)
.defaultIfEmpty(new ListResult<>(0, 0, 0, List.of()));
}
void populateStats(SinglePageVo pageVo) {
<T extends ListedSinglePageVo> void populateStats(T pageVo) {
String name = pageVo.getMetadata().getName();
Counter counter =
counterService.getByName(MeterUtils.nameOf(SinglePage.class, name));
@ -112,6 +117,17 @@ public class SinglePageFinderImpl implements SinglePageFinder {
pageVo.setStats(statsVo);
}
<T extends ListedSinglePageVo> Mono<T> populateContributors(T pageVo) {
List<String> names = pageVo.getStatus().getContributors();
if (CollectionUtils.isEmpty(names)) {
return Mono.just(pageVo);
}
return contributorFinder.getContributors(names)
.collectList()
.doOnNext(pageVo::setContributors)
.thenReturn(pageVo);
}
static Comparator<SinglePage> defaultComparator() {
Function<SinglePage, Boolean> pinned =
page -> Objects.requireNonNullElse(page.getSpec().getPinned(), false);

View File

@ -22,7 +22,7 @@ public class SiteStatsFinderImpl implements SiteStatsFinder {
private final ReactiveExtensionClient client;
@Override
public SiteStatsVo getStats() {
public Mono<SiteStatsVo> getStats() {
return client.list(Counter.class, null, null)
.reduce(SiteStatsVo.empty(), (stats, counter) -> {
stats.setVisit(stats.getVisit() + counter.getVisit());
@ -36,8 +36,7 @@ public class SiteStatsFinderImpl implements SiteStatsFinder {
)
.flatMap(siteStatsVo -> categoryCount()
.doOnNext(siteStatsVo::setCategory)
.thenReturn(siteStatsVo))
.block();
.thenReturn(siteStatsVo));
}
Mono<Integer> postCount() {

View File

@ -2,9 +2,10 @@ 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 org.apache.commons.lang3.ObjectUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Tag;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.ReactiveExtensionClient;
@ -31,21 +32,19 @@ public class TagFinderImpl implements TagFinder {
}
@Override
public TagVo getByName(String name) {
public Mono<TagVo> getByName(String name) {
return client.fetch(Tag.class, name)
.map(TagVo::from)
.block();
.map(TagVo::from);
}
@Override
public List<TagVo> getByNames(List<String> names) {
return names.stream().map(this::getByName)
.filter(Objects::nonNull)
.toList();
public Flux<TagVo> getByNames(List<String> names) {
return Flux.fromIterable(names)
.flatMap(this::getByName);
}
@Override
public ListResult<TagVo> list(Integer page, Integer size) {
public Mono<ListResult<TagVo>> list(Integer page, Integer size) {
return client.list(Tag.class, null,
DEFAULT_COMPARATOR.reversed(), pageNullSafe(page), sizeNullSafe(size))
.map(list -> {
@ -54,16 +53,14 @@ public class TagFinderImpl implements TagFinder {
.collect(Collectors.toList());
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), tagVos);
})
.block();
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()));
}
@Override
public List<TagVo> listAll() {
public Flux<TagVo> listAll() {
return client.list(Tag.class, null,
DEFAULT_COMPARATOR.reversed())
.map(TagVo::from)
.collectList()
.block();
.map(TagVo::from);
}
int pageNullSafe(Integer page) {

View File

@ -34,19 +34,17 @@ public class ThemeFinderImpl implements ThemeFinder {
}
@Override
public ThemeVo activation() {
public Mono<ThemeVo> activation() {
return environmentFetcher.fetch(SystemSetting.Theme.GROUP, SystemSetting.Theme.class)
.map(SystemSetting.Theme::getActive)
.flatMap(themeName -> client.fetch(Theme.class, themeName))
.flatMap(theme -> themeWithConfig(ThemeVo.from(theme)))
.block();
.flatMap(theme -> themeWithConfig(ThemeVo.from(theme)));
}
@Override
public ThemeVo getByName(String themeName) {
public Mono<ThemeVo> getByName(String themeName) {
return client.fetch(Theme.class, themeName)
.flatMap(theme -> themeWithConfig(ThemeVo.from(theme)))
.block();
.flatMap(theme -> themeWithConfig(ThemeVo.from(theme)));
}
private Mono<ThemeVo> themeWithConfig(ThemeVo themeVo) {
@ -63,6 +61,6 @@ public class ThemeFinderImpl implements ThemeFinder {
JsonNode configJson = JsonUtils.mapToObject(config, JsonNode.class);
return themeVo.withConfig(configJson);
})
.switchIfEmpty(Mono.just(themeVo));
.defaultIfEmpty(themeVo);
}
}

View File

@ -19,4 +19,14 @@ public class ContentVo {
String raw;
String content;
/**
* Empty content object.
*/
public static ContentVo empty() {
return ContentVo.builder()
.raw("")
.content("")
.build();
}
}

View File

@ -0,0 +1,59 @@
package run.halo.app.theme.finders.vo;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.springframework.util.Assert;
import run.halo.app.core.extension.Post;
import run.halo.app.extension.MetadataOperator;
/**
* A value object for {@link Post}.
*
* @author guqing
* @since 2.0.0
*/
@Data
@SuperBuilder
@ToString
@EqualsAndHashCode
public class ListedPostVo {
private MetadataOperator metadata;
private Post.PostSpec spec;
private Post.PostStatus status;
private List<CategoryVo> categories;
private List<TagVo> tags;
private List<Contributor> contributors;
private Contributor owner;
private StatsVo stats;
/**
* Convert {@link Post} to {@link ListedPostVo}.
*
* @param post post extension
* @return post value object
*/
public static ListedPostVo from(Post post) {
Assert.notNull(post, "The post must not be null.");
Post.PostSpec spec = post.getSpec();
Post.PostStatus postStatus = post.getStatusOrDefault();
return ListedPostVo.builder()
.metadata(post.getMetadata())
.spec(spec)
.status(postStatus)
.categories(List.of())
.tags(List.of())
.contributors(List.of())
.build();
}
}

View File

@ -0,0 +1,53 @@
package run.halo.app.theme.finders.vo;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.springframework.util.Assert;
import run.halo.app.core.extension.SinglePage;
import run.halo.app.extension.MetadataOperator;
/**
* A value object for {@link SinglePage}.
*
* @author guqing
* @since 2.0.0
*/
@Data
@SuperBuilder
@ToString
@EqualsAndHashCode
public class ListedSinglePageVo {
private MetadataOperator metadata;
private SinglePage.SinglePageSpec spec;
private SinglePage.SinglePageStatus status;
private StatsVo stats;
private List<Contributor> contributors;
private Contributor owner;
/**
* Convert {@link SinglePage} to {@link ListedSinglePageVo}.
*
* @param singlePage single page extension
* @return special page value object
*/
public static ListedSinglePageVo from(SinglePage singlePage) {
Assert.notNull(singlePage, "The singlePage must not be null.");
SinglePage.SinglePageSpec spec = singlePage.getSpec();
SinglePage.SinglePageStatus pageStatus = singlePage.getStatus();
return ListedSinglePageVo.builder()
.metadata(singlePage.getMetadata())
.spec(spec)
.status(pageStatus)
.contributors(List.of())
.build();
}
}

View File

@ -16,5 +16,5 @@ public class PostArchiveYearMonthVo {
String month;
List<PostVo> posts;
List<ListedPostVo> posts;
}

View File

@ -1,13 +1,12 @@
package run.halo.app.theme.finders.vo;
import java.util.List;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.springframework.util.Assert;
import run.halo.app.core.extension.Post;
import run.halo.app.extension.MetadataOperator;
/**
* A value object for {@link Post}.
@ -16,29 +15,13 @@ import run.halo.app.extension.MetadataOperator;
* @since 2.0.0
*/
@Data
@Builder
@SuperBuilder
@ToString
@EqualsAndHashCode
public class PostVo {
private MetadataOperator metadata;
private Post.PostSpec spec;
private Post.PostStatus status;
@EqualsAndHashCode(callSuper = true)
public class PostVo extends ListedPostVo {
private ContentVo content;
private List<CategoryVo> categories;
private List<TagVo> tags;
private List<Contributor> contributors;
private Contributor owner;
private StatsVo stats;
/**
* Convert {@link Post} to {@link PostVo}.
*
@ -59,4 +42,21 @@ public class PostVo {
.content(new ContentVo(null, null))
.build();
}
/**
* Convert {@link Post} to {@link PostVo}.
*/
public static PostVo from(ListedPostVo postVo) {
return builder()
.metadata(postVo.getMetadata())
.spec(postVo.getSpec())
.status(postVo.getStatus())
.categories(postVo.getCategories())
.tags(postVo.getTags())
.contributors(postVo.getContributors())
.owner(postVo.getOwner())
.stats(postVo.getStats())
.content(new ContentVo("", ""))
.build();
}
}

View File

@ -1,13 +1,12 @@
package run.halo.app.theme.finders.vo;
import java.util.List;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.springframework.util.Assert;
import run.halo.app.core.extension.SinglePage;
import run.halo.app.extension.MetadataOperator;
/**
* A value object for {@link SinglePage}.
@ -16,25 +15,13 @@ import run.halo.app.extension.MetadataOperator;
* @since 2.0.0
*/
@Data
@Builder
@SuperBuilder
@ToString
@EqualsAndHashCode
public class SinglePageVo {
private MetadataOperator metadata;
private SinglePage.SinglePageSpec spec;
private SinglePage.SinglePageStatus status;
@EqualsAndHashCode(callSuper = true)
public class SinglePageVo extends ListedSinglePageVo {
private ContentVo content;
private StatsVo stats;
private List<Contributor> contributors;
private Contributor owner;
/**
* Convert {@link SinglePage} to {@link SinglePageVo}.
*

View File

@ -13,8 +13,6 @@ import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.extension.ListResult;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.utils.PathUtils;
import run.halo.app.theme.DefaultTemplateEnum;
@ -47,7 +45,7 @@ public class ArchivesRouteStrategy implements ListPageRouteHandlerStrategy {
String path = request.path();
return environmentFetcher.fetchPost()
.map(postSetting -> defaultIfNull(postSetting.getArchivePageSize(), DEFAULT_PAGE_SIZE))
.flatMap(pageSize -> listPost(pageNum(request), pageSize, year, month))
.flatMap(pageSize -> postFinder.archives(pageNum(request), pageSize, year, month))
.map(list -> new UrlContextListResult.Builder<PostArchiveVo>()
.listResult(list)
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
@ -55,11 +53,6 @@ public class ArchivesRouteStrategy implements ListPageRouteHandlerStrategy {
.build());
}
Mono<ListResult<PostArchiveVo>> listPost(int pageNum, int pageSize, String year, String month) {
return Mono.fromCallable(() -> postFinder.archives(pageNum, pageSize, year, month))
.subscribeOn(Schedulers.boundedElastic());
}
private String pathVariable(ServerRequest request, String name) {
Map<String, String> pathVariables = request.pathVariables();
if (pathVariables.containsKey(name)) {

View File

@ -7,11 +7,8 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.CategoryFinder;
import run.halo.app.theme.finders.vo.CategoryTreeVo;
/**
* Categories router strategy for generate {@link HandlerFunction} specific to the template
@ -25,16 +22,11 @@ import run.halo.app.theme.finders.vo.CategoryTreeVo;
public class CategoriesRouteStrategy implements ListPageRouteHandlerStrategy {
private final CategoryFinder categoryFinder;
private Mono<List<CategoryTreeVo>> categories() {
return Mono.defer(() -> Mono.just(categoryFinder.listAsTree()))
.publishOn(Schedulers.boundedElastic());
}
@Override
public HandlerFunction<ServerResponse> getHandler() {
return request -> ServerResponse.ok()
.render(DefaultTemplateEnum.CATEGORIES.getValue(),
Map.of("categories", categories(),
Map.of("categories", categoryFinder.listAsTree(),
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORIES.getValue()));
}

View File

@ -12,17 +12,14 @@ import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.core.extension.Category;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.ListResult;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;
import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.CategoryFinder;
import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.finders.vo.CategoryVo;
import run.halo.app.theme.finders.vo.PostVo;
import run.halo.app.theme.finders.vo.ListedPostVo;
import run.halo.app.theme.router.PageUrlUtils;
import run.halo.app.theme.router.UrlContextListResult;
import run.halo.app.theme.router.ViewNameResolver;
@ -46,29 +43,20 @@ public class CategoryRouteStrategy implements DetailsPageRouteHandlerStrategy {
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
private Mono<UrlContextListResult<PostVo>> postListByCategoryName(String name,
private Mono<UrlContextListResult<ListedPostVo>> postListByCategoryName(String name,
ServerRequest request) {
String path = request.path();
return environmentFetcher.fetchPost()
.map(post -> defaultIfNull(post.getCategoryPageSize(), ModelConst.DEFAULT_PAGE_SIZE))
.flatMap(pageSize -> listPostsByCategory(pageNum(request), pageSize, name))
.map(list -> new UrlContextListResult.Builder<PostVo>()
.flatMap(
pageSize -> postFinder.listByCategory(pageNum(request), pageSize, name))
.map(list -> new UrlContextListResult.Builder<ListedPostVo>()
.listResult(list)
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
.prevUrl(PageUrlUtils.prevPageUrl(path))
.build());
}
private Mono<CategoryVo> categoryByName(String name) {
return Mono.fromCallable(() -> categoryFinder.getByName(name))
.subscribeOn(Schedulers.boundedElastic());
}
public Mono<ListResult<PostVo>> listPostsByCategory(int page, int size, String categoryName) {
return Mono.fromCallable(() -> postFinder.listByCategory(page, size, categoryName))
.subscribeOn(Schedulers.boundedElastic());
}
@Override
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
String name) {
@ -78,7 +66,7 @@ public class CategoryRouteStrategy implements DetailsPageRouteHandlerStrategy {
model.put("posts", postListByCategoryName(name, request));
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORY.getValue());
return categoryByName(name).flatMap(categoryVo -> {
return categoryFinder.getByName(name).flatMap(categoryVo -> {
model.put("category", categoryVo);
String template = categoryVo.getSpec().getTemplate();
return viewNameResolver.resolveViewNameOrDefault(request, template,

View File

@ -13,12 +13,10 @@ import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.extension.ListResult;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.finders.vo.PostVo;
import run.halo.app.theme.finders.vo.ListedPostVo;
import run.halo.app.theme.router.PageUrlUtils;
import run.halo.app.theme.router.UrlContextListResult;
@ -36,23 +34,18 @@ public class IndexRouteStrategy implements ListPageRouteHandlerStrategy {
private final PostFinder postFinder;
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
private Mono<UrlContextListResult<PostVo>> postList(ServerRequest request) {
private Mono<UrlContextListResult<ListedPostVo>> postList(ServerRequest request) {
String path = request.path();
return environmentFetcher.fetchPost()
.map(p -> defaultIfNull(p.getPostPageSize(), DEFAULT_PAGE_SIZE))
.flatMap(pageSize -> listPost(pageNum(request), pageSize))
.map(list -> new UrlContextListResult.Builder<PostVo>()
.flatMap(pageSize -> postFinder.list(pageNum(request), pageSize))
.map(list -> new UrlContextListResult.Builder<ListedPostVo>()
.listResult(list)
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
.prevUrl(PageUrlUtils.prevPageUrl(path))
.build());
}
private Mono<ListResult<PostVo>> listPost(int page, int size) {
return Mono.fromCallable(() -> postFinder.list(page, size))
.subscribeOn(Schedulers.boundedElastic());
}
@Override
public HandlerFunction<ServerResponse> getHandler() {
return request -> ServerResponse.ok()

View File

@ -8,15 +8,12 @@ import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.core.extension.Post;
import run.halo.app.extension.GVK;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.infra.SystemSetting;
import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.finders.vo.PostVo;
import run.halo.app.theme.router.ViewNameResolver;
/**
@ -57,7 +54,7 @@ public class PostRouteStrategy implements DetailsPageRouteHandlerStrategy {
model.put("plural", gvk.plural());
// used by TemplateGlobalHeadProcessor and PostTemplateHeadProcessor
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue());
return postByName(name)
return postFinder.getByName(name)
.flatMap(postVo -> {
model.put("post", postVo);
String template = postVo.getSpec().getTemplate();
@ -72,9 +69,4 @@ public class PostRouteStrategy implements DetailsPageRouteHandlerStrategy {
public boolean supports(GroupVersionKind gvk) {
return groupVersionKind.equals(gvk);
}
private Mono<PostVo> postByName(String name) {
return Mono.fromCallable(() -> postFinder.getByName(name))
.subscribeOn(Schedulers.boundedElastic());
}
}

View File

@ -5,15 +5,12 @@ import java.util.Map;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.core.extension.SinglePage;
import run.halo.app.extension.GVK;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.infra.SystemSetting;
import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.SinglePageFinder;
import run.halo.app.theme.finders.vo.SinglePageVo;
import run.halo.app.theme.router.ViewNameResolver;
/**
@ -40,11 +37,6 @@ public class SinglePageRouteStrategy implements DetailsPageRouteHandlerStrategy
return annotation.plural();
}
private Mono<SinglePageVo> singlePageByName(String name) {
return Mono.fromCallable(() -> singlePageFinder.getByName(name))
.subscribeOn(Schedulers.boundedElastic());
}
@Override
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
String name) {
@ -54,7 +46,7 @@ public class SinglePageRouteStrategy implements DetailsPageRouteHandlerStrategy
model.put("plural", getPlural());
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.SINGLE_PAGE.getValue());
return singlePageByName(name).flatMap(singlePageVo -> {
return singlePageFinder.getByName(name).flatMap(singlePageVo -> {
model.put("singlePage", singlePageVo);
String template = singlePageVo.getSpec().getTemplate();
return viewNameResolver.resolveViewNameOrDefault(request, template,

View File

@ -12,17 +12,14 @@ import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.core.extension.Tag;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.ListResult;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;
import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.finders.TagFinder;
import run.halo.app.theme.finders.vo.PostVo;
import run.halo.app.theme.finders.vo.TagVo;
import run.halo.app.theme.finders.vo.ListedPostVo;
import run.halo.app.theme.router.PageUrlUtils;
import run.halo.app.theme.router.UrlContextListResult;
@ -43,28 +40,18 @@ public class TagRouteStrategy implements DetailsPageRouteHandlerStrategy {
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
private Mono<UrlContextListResult<PostVo>> postList(ServerRequest request, String name) {
private Mono<UrlContextListResult<ListedPostVo>> postList(ServerRequest request, String name) {
String path = request.path();
return environmentFetcher.fetchPost()
.map(p -> defaultIfNull(p.getTagPageSize(), ModelConst.DEFAULT_PAGE_SIZE))
.flatMap(pageSize -> listPostByTag(pageNum(request), pageSize, name))
.map(list -> new UrlContextListResult.Builder<PostVo>()
.flatMap(pageSize -> postFinder.listByTag(pageNum(request), pageSize, name))
.map(list -> new UrlContextListResult.Builder<ListedPostVo>()
.listResult(list)
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))
.prevUrl(PageUrlUtils.prevPageUrl(path))
.build());
}
private Mono<ListResult<PostVo>> listPostByTag(int page, int size, String name) {
return Mono.fromCallable(() -> postFinder.listByTag(page, size, name))
.subscribeOn(Schedulers.boundedElastic());
}
private Mono<TagVo> tagByName(String name) {
return Mono.defer(() -> Mono.just(tagFinder.getByName(name)))
.publishOn(Schedulers.boundedElastic());
}
@Override
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
String name) {
@ -72,7 +59,7 @@ public class TagRouteStrategy implements DetailsPageRouteHandlerStrategy {
.render(DefaultTemplateEnum.TAG.getValue(),
Map.of("name", name,
"posts", postList(request, name),
"tag", tagByName(name),
"tag", tagFinder.getByName(name),
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.TAG.getValue()
)
);

View File

@ -6,11 +6,8 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.TagFinder;
import run.halo.app.theme.finders.vo.TagVo;
/**
* The {@link TagsRouteStrategy} for generate {@link HandlerFunction} specific to the template
@ -28,16 +25,11 @@ public class TagsRouteStrategy implements ListPageRouteHandlerStrategy {
this.tagFinder = tagFinder;
}
private Mono<List<TagVo>> tags() {
return Mono.defer(() -> Mono.just(tagFinder.listAll()))
.publishOn(Schedulers.boundedElastic());
}
@Override
public HandlerFunction<ServerResponse> getHandler() {
return request -> ServerResponse.ok()
.render(DefaultTemplateEnum.TAGS.getValue(),
Map.of("tags", tags(),
Map.of("tags", tagFinder.listAll(),
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.TAGS.getValue()
)
);

View File

@ -142,7 +142,7 @@ class HaloProcessorDialectTest {
PostVo postVo = PostVo.builder()
.spec(postSpec)
.metadata(metadata).build();
when(postFinder.getByName(eq("fake-post"))).thenReturn(postVo);
when(postFinder.getByName(eq("fake-post"))).thenReturn(Mono.just(postVo));
SystemSetting.Basic basic = new SystemSetting.Basic();
basic.setFavicon(null);

View File

@ -60,7 +60,7 @@ class CommentFinderEndpointTest {
@Test
void listComments() {
when(commentFinder.list(any(), anyInt(), anyInt()))
.thenReturn(new ListResult<>(1, 10, 0, List.of()));
.thenReturn(Mono.just(new ListResult<>(1, 10, 0, List.of())));
Ref ref = new Ref();
ref.setGroup("content.halo.run");
@ -104,15 +104,13 @@ class CommentFinderEndpointTest {
@Test
void listCommentReplies() {
when(commentFinder.listReply(any(), anyInt(), anyInt()))
.thenReturn(new ListResult<>(2, 20, 0, List.of()));
.thenReturn(Mono.just(new ListResult<>(2, 20, 0, List.of())));
webTestClient.get()
.uri(uriBuilder -> {
return uriBuilder.path("/comments/test-comment/reply")
.queryParam("page", 2)
.queryParam("size", 20)
.build();
})
.uri(uriBuilder -> uriBuilder.path("/comments/test-comment/reply")
.queryParam("page", 2)
.queryParam("size", 20)
.build())
.exchange()
.expectStatus()
.isOk();

View File

@ -51,7 +51,7 @@ class CategoryFinderImplTest {
void getByName() throws JSONException {
when(client.fetch(eq(Category.class), eq("hello")))
.thenReturn(Mono.just(category()));
CategoryVo categoryVo = categoryFinder.getByName("hello");
CategoryVo categoryVo = categoryFinder.getByName("hello").block();
categoryVo.getMetadata().setCreationTimestamp(null);
JSONAssert.assertEquals("""
{
@ -87,7 +87,7 @@ class CategoryFinderImplTest {
.toList());
when(client.list(eq(Category.class), eq(null), any(), anyInt(), anyInt()))
.thenReturn(Mono.just(categories));
ListResult<CategoryVo> list = categoryFinder.list(1, 10);
ListResult<CategoryVo> list = categoryFinder.list(1, 10).block();
assertThat(list.getItems()).hasSize(3);
assertThat(list.get().map(categoryVo -> categoryVo.getMetadata().getName()).toList())
.isEqualTo(List.of("c3", "c2", "hello"));
@ -97,7 +97,7 @@ class CategoryFinderImplTest {
void listAsTree() {
when(client.list(eq(Category.class), eq(null), any()))
.thenReturn(Flux.fromIterable(categoriesForTree()));
List<CategoryTreeVo> treeVos = categoryFinder.listAsTree();
List<CategoryTreeVo> treeVos = categoryFinder.listAsTree().collectList().block();
assertThat(treeVos).hasSize(1);
}
@ -110,7 +110,7 @@ class CategoryFinderImplTest {
void listAsTreeMore() {
when(client.list(eq(Category.class), eq(null), any()))
.thenReturn(Flux.fromIterable(moreCategories()));
List<CategoryTreeVo> treeVos = categoryFinder.listAsTree();
List<CategoryTreeVo> treeVos = categoryFinder.listAsTree().collectList().block();
String s = visualizeTree(treeVos);
assertThat(s).isEqualTo("""
(5)

View File

@ -45,7 +45,7 @@ class MenuFinderImplTest {
Mockito.when(client.list(eq(MenuItem.class), eq(null), any()))
.thenReturn(Flux.fromIterable(tuple.getT2()));
List<MenuVo> menuVos = menuFinder.listAsTree();
List<MenuVo> menuVos = menuFinder.listAsTree().collectList().block();
assertThat(visualizeTree(menuVos)).isEqualTo("""
D
E

View File

@ -18,6 +18,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.content.ContentService;
import run.halo.app.content.ContentWrapper;
@ -78,7 +79,7 @@ class PostFinderImplTest {
.thenReturn(Mono.just(post));
when(contentService.getContent(post.getSpec().getReleaseSnapshot()))
.thenReturn(Mono.just(contentWrapper));
ContentVo content = postFinder.content("post-1");
ContentVo content = postFinder.content("post-1").block();
assertThat(content.getContent()).isEqualTo(contentWrapper.getContent());
assertThat(content.getRaw()).isEqualTo(contentWrapper.getRaw());
}
@ -108,7 +109,12 @@ class PostFinderImplTest {
ListResult<Post> listResult = new ListResult<>(1, 10, 3, postsForArchives());
when(client.list(eq(Post.class), any(), any(), anyInt(), anyInt()))
.thenReturn(Mono.just(listResult));
ListResult<PostArchiveVo> archives = postFinder.archives(1, 10);
when(contributorFinder.getContributor(any())).thenReturn(Mono.empty());
when(contributorFinder.getContributors(any())).thenReturn(Flux.empty());
ListResult<PostArchiveVo> archives = postFinder.archives(1, 10).block();
assertThat(archives).isNotNull();
List<PostArchiveVo> items = archives.getItems();
assertThat(items.size()).isEqualTo(2);
assertThat(items.get(0).getYear()).isEqualTo("2022");

View File

@ -47,7 +47,7 @@ class TagFinderImplTest {
void getByName() throws JSONException {
when(client.fetch(eq(Tag.class), eq("t1")))
.thenReturn(Mono.just(tag(1)));
TagVo tagVo = tagFinder.getByName("t1");
TagVo tagVo = tagFinder.getByName("t1").block();
tagVo.getMetadata().setCreationTimestamp(null);
JSONAssert.assertEquals("""
{
@ -82,7 +82,7 @@ class TagFinderImplTest {
tags().stream().sorted(TagFinderImpl.DEFAULT_COMPARATOR.reversed()).toList()
)
);
List<TagVo> tags = tagFinder.listAll();
List<TagVo> tags = tagFinder.listAll().collectList().block();
assertThat(tags).hasSize(3);
assertThat(tags.stream()
.map(tag -> tag.getMetadata().getName())

View File

@ -1,8 +1,5 @@
package run.halo.app.theme.router.strategy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -15,7 +12,6 @@ import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.router.UrlContextListResult;
/**
* Tests for {@link ArchivesRouteStrategy}.
@ -31,12 +27,6 @@ class ArchivesRouteStrategyTest extends RouterStrategyTestSuite {
@InjectMocks
private ArchivesRouteStrategy archivesRouteStrategy;
@Override
public void setUp() {
lenient().when(postFinder.list(any(), any())).thenReturn(
new UrlContextListResult<>(1, 10, 1, List.of(), null, null));
}
@Test
void getRouteFunctionWhenDefaultPattern() {
HandlerFunction<ServerResponse> handler = archivesRouteStrategy.getHandler();

View File

@ -1,6 +1,6 @@
package run.halo.app.theme.router.strategy;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import java.util.List;
import org.junit.jupiter.api.Test;
@ -13,6 +13,7 @@ import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import run.halo.app.theme.finders.CategoryFinder;
/**
@ -29,12 +30,6 @@ class CategoriesRouteStrategyTest extends RouterStrategyTestSuite {
@InjectMocks
private CategoriesRouteStrategy categoriesRouteStrategy;
@Override
public void setUp() {
lenient().when(categoryFinder.listAsTree())
.thenReturn(List.of());
}
@Test
void getRouteFunction() {
HandlerFunction<ServerResponse> handler = categoriesRouteStrategy.getHandler();
@ -47,6 +42,7 @@ class CategoriesRouteStrategyTest extends RouterStrategyTestSuite {
permalinkHttpGetRouter.insert(routerPath, handler);
}
when(categoryFinder.listAsTree()).thenReturn(Flux.empty());
client.get()
.uri("/categories-test")
.exchange()

View File

@ -1,10 +1,8 @@
package run.halo.app.theme.router.strategy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@ -14,7 +12,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.extension.ListResult;
import reactor.core.publisher.Mono;
import run.halo.app.theme.finders.CategoryFinder;
import run.halo.app.theme.finders.PostFinder;
@ -36,12 +34,6 @@ class CategoryRouteStrategyTest extends RouterStrategyTestSuite {
@InjectMocks
private CategoryRouteStrategy categoryRouteStrategy;
@Override
public void setUp() {
lenient().when(postFinder.listByCategory(anyInt(), anyInt(), any()))
.thenReturn(new ListResult<>(1, 10, 0, List.of()));
}
@Test
void getRouteFunction() {
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
@ -52,6 +44,8 @@ class CategoryRouteStrategyTest extends RouterStrategyTestSuite {
permalinkHttpGetRouter.insert("/categories-test/category-slug-2",
categoryRouteStrategy.getHandler(null, "category-slug-2"));
when(categoryFinder.getByName(any())).thenReturn(Mono.empty());
// /{prefix}/{slug}
client.get()
.uri("/categories-test/category-slug-1")

View File

@ -1,8 +1,5 @@
package run.halo.app.theme.router.strategy;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.lenient;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -14,7 +11,6 @@ import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.extension.ListResult;
import run.halo.app.theme.finders.PostFinder;
/**
@ -31,12 +27,6 @@ class IndexRouteStrategyTest extends RouterStrategyTestSuite {
@InjectMocks
private IndexRouteStrategy indexRouteStrategy;
@Override
public void setUp() {
lenient().when(postFinder.list(anyInt(), anyInt()))
.thenReturn(new ListResult<>(1, 10, 0, List.of()));
}
@Test
void getRouteFunction() {
HandlerFunction<ServerResponse> handler = indexRouteStrategy.getHandler();

View File

@ -42,7 +42,8 @@ class PostRouteStrategyTest extends RouterStrategyTestSuite {
public void setUp() {
lenient().when(viewNameResolver.resolveViewNameOrDefault(any(), any(), any()))
.thenReturn(Mono.just(DefaultTemplateEnum.POST.getValue()));
lenient().when(postFinder.getByName(any())).thenReturn(PostVo.from(TestPost.postV1()));
lenient().when(postFinder.getByName(any()))
.thenReturn(Mono.just(PostVo.from(TestPost.postV1())));
}
@Test

View File

@ -1,12 +1,11 @@
package run.halo.app.theme.router.strategy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import static run.halo.app.theme.DefaultTemplateEnum.SINGLE_PAGE;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@ -16,7 +15,6 @@ import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListResult;
import run.halo.app.theme.finders.SinglePageFinder;
/**
@ -35,8 +33,6 @@ class SinglePageRouteStrategyTest extends RouterStrategyTestSuite {
@Override
public void setUp() {
lenient().when(singlePageFinder.list(anyInt(), anyInt()))
.thenReturn(new ListResult<>(1, 10, 0, List.of()));
lenient().when(viewResolver.resolveViewName(eq(SINGLE_PAGE.getValue()), any()))
.thenReturn(Mono.just(new EmptyView()));
}
@ -53,6 +49,7 @@ class SinglePageRouteStrategyTest extends RouterStrategyTestSuite {
void shouldResponse200IfPermalinkFound() {
permalinkHttpGetRouter.insert("/fake-slug",
strategy.getHandler(getThemeRouteRules(), "fake-name"));
when(singlePageFinder.getByName(any())).thenReturn(Mono.empty());
createClient().get()
.uri("/fake-slug")
.exchange()
@ -64,6 +61,8 @@ class SinglePageRouteStrategyTest extends RouterStrategyTestSuite {
void shouldResponse200IfSlugNameContainsSpecialChars() {
permalinkHttpGetRouter.insert("/fake / slug",
strategy.getHandler(getThemeRouteRules(), "fake-name"));
when(singlePageFinder.getByName(any())).thenReturn(Mono.empty());
createClient().get()
.uri("/fake / slug")
.exchange()
@ -74,6 +73,9 @@ class SinglePageRouteStrategyTest extends RouterStrategyTestSuite {
void shouldResponse200IfSlugNameContainsChineseChars() {
permalinkHttpGetRouter.insert("/中文",
strategy.getHandler(getThemeRouteRules(), "fake-name"));
when(singlePageFinder.getByName(any())).thenReturn(Mono.empty());
createClient().get()
.uri("/中文")
.exchange()

View File

@ -1,10 +1,8 @@
package run.halo.app.theme.router.strategy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@ -14,8 +12,9 @@ import org.springframework.http.HttpStatus;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.extension.ListResult;
import reactor.core.publisher.Mono;
import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.finders.TagFinder;
/**
* Tests for {@link TagRouteStrategy}.
@ -28,16 +27,12 @@ class TagRouteStrategyTest extends RouterStrategyTestSuite {
@Mock
private PostFinder postFinder;
@Mock
private TagFinder tagFinder;
@InjectMocks
private TagRouteStrategy tagRouteStrategy;
@Override
public void setUp() {
lenient().when(postFinder.listByTag(anyInt(), anyInt(), any()))
.thenReturn(new ListResult<>(1, 10, 0, List.of()));
}
@Test
void getRouteFunction() {
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
@ -45,6 +40,7 @@ class TagRouteStrategyTest extends RouterStrategyTestSuite {
permalinkHttpGetRouter.insert("/tags-test/fake-slug",
tagRouteStrategy.getHandler(getThemeRouteRules(), "fake-name"));
when(tagFinder.getByName(any())).thenReturn(Mono.empty());
client.get()
.uri("/tags-test/fake-slug")

View File

@ -1,6 +1,6 @@
package run.halo.app.theme.router.strategy;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import java.util.List;
import org.junit.jupiter.api.Test;
@ -13,6 +13,7 @@ import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import run.halo.app.theme.finders.TagFinder;
/**
@ -30,11 +31,6 @@ class TagsRouteStrategyTest extends RouterStrategyTestSuite {
@InjectMocks
private TagsRouteStrategy tagsRouteStrategy;
@Override
public void setUp() {
lenient().when(tagFinder.listAll()).thenReturn(List.of());
}
@Test
void getRouteFunction() {
RouterFunction<ServerResponse> routeFunction = getRouterFunction();
@ -45,6 +41,7 @@ class TagsRouteStrategyTest extends RouterStrategyTestSuite {
for (String routerPath : routerPaths) {
permalinkHttpGetRouter.insert(routerPath, handler);
}
when(tagFinder.listAll()).thenReturn(Flux.empty());
client.get()
.uri("/tags-test")