From f61f846a7f571f46e15930e4a0ce952a9c58932b Mon Sep 17 00:00:00 2001 From: John Niang Date: Thu, 29 Aug 2024 10:39:24 +0800 Subject: [PATCH] Cleanup code with SortableRequest (#6540) #### What type of PR is this? /kind cleanup /kind improvement /area core /milestone 2.19.0 #### What this PR does / why we need it: This PR refactors some requests with sort parameter by reusing SortableRequest, and refactors some queries with indexer. #### Does this PR introduce a user-facing change? ```release-note None ``` --- .../run/halo/app/extension/ExtensionUtil.java | 7 +- .../app/extension/router/SortableRequest.java | 7 +- .../java/run/halo/app/content/PostQuery.java | 116 ++++------ .../run/halo/app/content/SinglePageQuery.java | 208 +++++------------- .../app/content/comment/CommentQuery.java | 73 +++--- .../app/content/impl/PostServiceImpl.java | 7 +- .../content/impl/SinglePageServiceImpl.java | 21 +- .../endpoint/AttachmentEndpoint.java | 157 +++++-------- .../extension/endpoint/PluginEndpoint.java | 122 ++++------ .../core/extension/endpoint/PostEndpoint.java | 2 +- .../core/extension/endpoint/TagEndpoint.java | 64 ++---- .../core/extension/endpoint/UserEndpoint.java | 34 +-- .../run/halo/app/infra/SchemeInitializer.java | 109 ++++++++- .../endpoint/SinglePageQueryEndpoint.java | 14 +- .../finders/SinglePageConversionService.java | 6 + .../app/theme/finders/SinglePageFinder.java | 4 - .../impl/SinglePageConversionServiceImpl.java | 47 ++++ .../finders/impl/SinglePageFinderImpl.java | 61 +---- .../endpoint/PluginEndpointTest.java | 48 +--- .../extension/endpoint/UserEndpointTest.java | 69 ++---- .../endpoint/SinglePageQueryEndpointTest.java | 29 --- 21 files changed, 452 insertions(+), 753 deletions(-) diff --git a/api/src/main/java/run/halo/app/extension/ExtensionUtil.java b/api/src/main/java/run/halo/app/extension/ExtensionUtil.java index cb50cd365..17992403e 100644 --- a/api/src/main/java/run/halo/app/extension/ExtensionUtil.java +++ b/api/src/main/java/run/halo/app/extension/ExtensionUtil.java @@ -1,5 +1,8 @@ package run.halo.app.extension; +import static org.springframework.data.domain.Sort.Order.asc; +import static org.springframework.data.domain.Sort.Order.desc; + import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -53,8 +56,8 @@ public enum ExtensionUtil { */ public static Sort defaultSort() { return Sort.by( - Sort.Order.desc("metadata.creationTimestamp"), - Sort.Order.asc("metadata.name") + desc("metadata.creationTimestamp"), + asc("metadata.name") ); } diff --git a/api/src/main/java/run/halo/app/extension/router/SortableRequest.java b/api/src/main/java/run/halo/app/extension/router/SortableRequest.java index 67769008f..fd7a2cad7 100644 --- a/api/src/main/java/run/halo/app/extension/router/SortableRequest.java +++ b/api/src/main/java/run/halo/app/extension/router/SortableRequest.java @@ -1,10 +1,9 @@ package run.halo.app.extension.router; -import static org.springframework.data.domain.Sort.Order.asc; -import static org.springframework.data.domain.Sort.Order.desc; import static run.halo.app.extension.Comparators.compareCreationTimestamp; import static run.halo.app.extension.Comparators.compareName; import static run.halo.app.extension.Comparators.nullsComparator; +import static run.halo.app.extension.ExtensionUtil.defaultSort; import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions; import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate; @@ -43,9 +42,7 @@ public class SortableRequest extends IListRequest.QueryListRequest { example = "metadata.creationTimestamp,desc")) public Sort getSort() { return SortResolver.defaultInstance.resolve(exchange) - .and(Sort.by(desc("metadata.creationTimestamp"), - asc("metadata.name")) - ); + .and(defaultSort()); } /** diff --git a/application/src/main/java/run/halo/app/content/PostQuery.java b/application/src/main/java/run/halo/app/content/PostQuery.java index c6e73f211..3fab1ccd0 100644 --- a/application/src/main/java/run/halo/app/content/PostQuery.java +++ b/application/src/main/java/run/halo/app/content/PostQuery.java @@ -1,27 +1,23 @@ package run.halo.app.content; import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; +import static run.halo.app.core.extension.content.Post.PUBLISHED_LABEL; +import static run.halo.app.core.extension.content.Post.PostPhase.PENDING_APPROVAL; +import static run.halo.app.extension.index.query.QueryFactory.contains; +import static run.halo.app.extension.index.query.QueryFactory.equal; +import static run.halo.app.extension.index.query.QueryFactory.or; import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter; -import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions; -import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Schema; -import org.apache.commons.lang3.BooleanUtils; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.springdoc.core.fn.builders.operation.Builder; -import org.springframework.data.domain.Sort; import org.springframework.lang.Nullable; import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.server.ServerWebExchange; import run.halo.app.core.extension.content.Post; -import run.halo.app.core.extension.endpoint.SortResolver; import run.halo.app.extension.ListOptions; -import run.halo.app.extension.index.query.QueryFactory; import run.halo.app.extension.router.IListRequest; -import run.halo.app.extension.router.selector.FieldSelector; -import run.halo.app.extension.router.selector.LabelSelector; +import run.halo.app.extension.router.SortableRequest; /** * A query object for {@link Post} list. @@ -29,9 +25,7 @@ import run.halo.app.extension.router.selector.LabelSelector; * @author guqing * @since 2.0.0 */ -public class PostQuery extends IListRequest.QueryListRequest { - - private final ServerWebExchange exchange; +public class PostQuery extends SortableRequest { private final String username; @@ -40,21 +34,13 @@ public class PostQuery extends IListRequest.QueryListRequest { } public PostQuery(ServerRequest request, @Nullable String username) { - super(request.queryParams()); - this.exchange = request.exchange(); + super(request.exchange()); this.username = username; } - @Schema(hidden = true) - @JsonIgnore - public String getUsername() { - return username; - } - @Nullable - public Post.PostPhase getPublishPhase() { - String publishPhase = queryParams.getFirst("publishPhase"); - return Post.PostPhase.from(publishPhase); + public String getPublishPhase() { + return queryParams.getFirst("publishPhase"); } @Nullable @@ -64,72 +50,48 @@ public class PostQuery extends IListRequest.QueryListRequest { } @Nullable - @Schema(description = "Posts filtered by keyword.") public String getKeyword() { return StringUtils.defaultIfBlank(queryParams.getFirst("keyword"), null); } - @ArraySchema(uniqueItems = true, - arraySchema = @Schema(name = "sort", - description = "Sort property and direction of the list result. Supported fields: " - + "creationTimestamp,publishTime"), - schema = @Schema(description = "like field,asc or field,desc", - implementation = String.class, - example = "creationTimestamp,desc")) - public Sort getSort() { - var sort = SortResolver.defaultInstance.resolve(exchange); - sort = sort.and(Sort.by(Sort.Direction.DESC, "metadata.creationTimestamp")); - sort = sort.and(Sort.by(Sort.Direction.DESC, "metadata.name")); - return sort; - } - /** * Build a list options from the query object. * * @return a list options */ + @Override public ListOptions toListOptions() { - var listOptions = - labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector()); - if (listOptions.getFieldSelector() == null) { - listOptions.setFieldSelector(FieldSelector.all()); - } - var labelSelectorBuilder = LabelSelector.builder(); - var fieldQuery = QueryFactory.all(); + var builder = ListOptions.builder(super.toListOptions()); - String keyword = getKeyword(); - if (keyword != null) { - fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.or( - QueryFactory.contains("status.excerpt", keyword), - QueryFactory.contains("spec.slug", keyword), - QueryFactory.contains("spec.title", keyword) - )); - } + Optional.ofNullable(getKeyword()) + .filter(StringUtils::isNotBlank) + .ifPresent(keyword -> builder.andQuery(or( + contains("status.excerpt", keyword), + contains("spec.slug", keyword), + contains("spec.title", keyword) + ))); - Post.PostPhase publishPhase = getPublishPhase(); - if (publishPhase != null) { - if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) { - fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal( - "status.phase", Post.PostPhase.PENDING_APPROVAL.name()) - ); - labelSelectorBuilder.eq(Post.PUBLISHED_LABEL, BooleanUtils.FALSE); - } else if (Post.PostPhase.PUBLISHED.equals(publishPhase)) { - labelSelectorBuilder.eq(Post.PUBLISHED_LABEL, BooleanUtils.TRUE); - } else { - labelSelectorBuilder.eq(Post.PUBLISHED_LABEL, BooleanUtils.FALSE); - } - } + Optional.ofNullable(getPublishPhase()) + .filter(StringUtils::isNotBlank) + .map(Post.PostPhase::from) + .ifPresent(phase -> { + if (PENDING_APPROVAL.equals(phase)) { + builder.andQuery(equal("status.phase", phase.name())); + } + var labelSelector = builder.labelSelector(); + Optional.of(phase) + .filter(Post.PostPhase.PUBLISHED::equals) + .ifPresentOrElse( + published -> labelSelector.eq(PUBLISHED_LABEL, Boolean.TRUE.toString()), + () -> labelSelector.notEq(PUBLISHED_LABEL, Boolean.TRUE.toString()) + ); + }); - if (StringUtils.isNotBlank(username)) { - fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal( - "spec.owner", username) - ); - } + Optional.ofNullable(username) + .filter(StringUtils::isNotBlank) + .ifPresent(username -> builder.andQuery(equal("spec.owner", username))); - listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(fieldQuery)); - listOptions.setLabelSelector( - listOptions.getLabelSelector().and(labelSelectorBuilder.build())); - return listOptions; + return builder.build(); } public static void buildParameters(Builder builder) { diff --git a/application/src/main/java/run/halo/app/content/SinglePageQuery.java b/application/src/main/java/run/halo/app/content/SinglePageQuery.java index 851333bd4..8564e53c6 100644 --- a/application/src/main/java/run/halo/app/content/SinglePageQuery.java +++ b/application/src/main/java/run/halo/app/content/SinglePageQuery.java @@ -1,30 +1,25 @@ package run.halo.app.content; -import static java.util.Comparator.comparing; import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; +import static run.halo.app.core.extension.content.Post.PostPhase.PENDING_APPROVAL; +import static run.halo.app.core.extension.content.SinglePage.PUBLISHED_LABEL; +import static run.halo.app.extension.index.query.QueryFactory.equal; +import static run.halo.app.extension.index.query.QueryFactory.in; +import static run.halo.app.extension.index.query.QueryFactory.or; import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter; -import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate; import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.Set; -import java.util.function.Predicate; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.springdoc.core.fn.builders.operation.Builder; import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.server.ServerWebExchange; import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.SinglePage; -import run.halo.app.core.extension.endpoint.SortResolver; -import run.halo.app.extension.Comparators; +import run.halo.app.extension.ListOptions; +import run.halo.app.extension.index.query.QueryFactory; import run.halo.app.extension.router.IListRequest; +import run.halo.app.extension.router.SortableRequest; /** * Query parameter for {@link SinglePage} list. @@ -32,148 +27,67 @@ import run.halo.app.extension.router.IListRequest; * @author guqing * @since 2.0.0 */ -public class SinglePageQuery extends IListRequest.QueryListRequest { - - private final ServerWebExchange exchange; +public class SinglePageQuery extends SortableRequest { public SinglePageQuery(ServerRequest request) { - super(request.queryParams()); - this.exchange = request.exchange(); + super(request.exchange()); } - @Nullable - @Schema(name = "contributor") - public Set getContributors() { - List contributorList = queryParams.get("contributor"); - return contributorList == null ? null : Set.copyOf(contributorList); + @Override + public ListOptions toListOptions() { + var builder = ListOptions.builder(super.toListOptions()); + + Optional.ofNullable(queryParams.getFirst("keyword")) + .filter(StringUtils::isNotBlank) + .ifPresent(keyword -> builder.andQuery(or( + QueryFactory.contains("spec.title", keyword), + QueryFactory.contains("spec.slug", keyword), + QueryFactory.contains("status.excerpt", keyword) + ))); + + Optional.ofNullable(queryParams.getFirst("publishPhase")) + .filter(StringUtils::isNotBlank) + .map(Post.PostPhase::from) + .ifPresent(phase -> { + if (PENDING_APPROVAL.equals(phase)) { + builder.andQuery(equal("status.phase", phase.name())); + } + var labelSelector = builder.labelSelector(); + Optional.of(phase) + .filter(Post.PostPhase.PUBLISHED::equals) + .ifPresentOrElse( + published -> labelSelector.eq(PUBLISHED_LABEL, Boolean.TRUE.toString()), + () -> labelSelector.notEq(PUBLISHED_LABEL, Boolean.TRUE.toString()) + ); + }); + + Optional.ofNullable(queryParams.getFirst("visible")) + .filter(StringUtils::isNotBlank) + .map(Post.VisibleEnum::from) + .ifPresent(visible -> builder.andQuery(equal("spec.visible", visible.name()))); + + Optional.ofNullable(queryParams.get("contributor")) + .filter(contributors -> !contributors.isEmpty()) + .ifPresent(contributors -> builder.andQuery(in("status.contributors", contributors))); + + return builder.build(); } - @Nullable - public Post.PostPhase getPublishPhase() { - String publishPhase = queryParams.getFirst("publishPhase"); - return Post.PostPhase.from(publishPhase); - } - - @Nullable - public Post.VisibleEnum getVisible() { - String visible = queryParams.getFirst("visible"); - return Post.VisibleEnum.from(visible); - } - - @Nullable - @Schema(description = "SinglePages filtered by keyword.") - public String getKeyword() { - return StringUtils.defaultIfBlank(queryParams.getFirst("keyword"), null); - } - - @ArraySchema(uniqueItems = true, - arraySchema = @Schema(name = "sort", - description = "Sort property and direction of the list result. Supported fields: " - + "creationTimestamp,publishTime"), - schema = @Schema(description = "like field,asc or field,desc", - implementation = String.class, - example = "creationTimestamp,desc")) + @Override public Sort getSort() { - return SortResolver.defaultInstance.resolve(exchange); - } - - /** - * Build a comparator for {@link SinglePageQuery}. - * - * @return comparator - */ - public Comparator toComparator() { - var sort = getSort(); - var creationTimestampOrder = sort.getOrderFor("creationTimestamp"); - List> comparators = new ArrayList<>(); - if (creationTimestampOrder != null) { - Comparator comparator = - comparing(page -> page.getMetadata().getCreationTimestamp()); - if (creationTimestampOrder.isDescending()) { - comparator = comparator.reversed(); - } - comparators.add(comparator); - } - - var publishTimeOrder = sort.getOrderFor("publishTime"); - if (publishTimeOrder != null) { - Comparator nullsComparator = publishTimeOrder.isAscending() - ? org.springframework.util.comparator.Comparators.nullsLow() - : org.springframework.util.comparator.Comparators.nullsHigh(); - Comparator comparator = - comparing(page -> page.getSpec().getPublishTime(), nullsComparator); - if (publishTimeOrder.isDescending()) { - comparator = comparator.reversed(); - } - comparators.add(comparator); - } - comparators.add(Comparators.compareCreationTimestamp(false)); - comparators.add(Comparators.compareName(true)); - return comparators.stream() - .reduce(Comparator::thenComparing) - .orElse(null); - } - - /** - * Build a predicate for {@link SinglePageQuery}. - * - * @return predicate - */ - public Predicate toPredicate() { - Predicate paramPredicate = singlePage -> contains(getContributors(), - singlePage.getStatusOrDefault().getContributors()); - - String keyword = getKeyword(); - if (keyword != null) { - paramPredicate = paramPredicate.and(page -> { - String excerpt = page.getStatusOrDefault().getExcerpt(); - return StringUtils.containsIgnoreCase(excerpt, keyword) - || StringUtils.containsIgnoreCase(page.getSpec().getSlug(), keyword) - || StringUtils.containsIgnoreCase(page.getSpec().getTitle(), keyword); - }); - } - - Post.PostPhase publishPhase = getPublishPhase(); - if (publishPhase != null) { - paramPredicate = paramPredicate.and(page -> { - if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) { - return !page.isPublished() - && Post.PostPhase.PENDING_APPROVAL.name() - .equalsIgnoreCase(page.getStatusOrDefault().getPhase()); + var sort = super.getSort(); + var orders = sort.stream() + .map(order -> { + if ("creationTimestamp".equals(order.getProperty())) { + return order.withProperty("metadata.creationTimestamp"); } - // published - if (Post.PostPhase.PUBLISHED.equals(publishPhase)) { - return page.isPublished(); + if ("publishTime".equals(order.getProperty())) { + return order.withProperty("spec.publishTime"); } - // draft - return !page.isPublished(); - }); - } - - Post.VisibleEnum visible = getVisible(); - if (visible != null) { - paramPredicate = - paramPredicate.and(post -> visible.equals(post.getSpec().getVisible())); - } - - Predicate predicate = labelAndFieldSelectorToPredicate(getLabelSelector(), - getFieldSelector()); - return predicate.and(paramPredicate); - } - - boolean contains(Collection left, List right) { - // parameter is null, it means that ignore this condition - if (left == null) { - return true; - } - // else, it means that right is empty - if (left.isEmpty()) { - return right.isEmpty(); - } - if (right == null) { - return false; - } - return right.stream().anyMatch(left::contains); + return order; + }) + .toList(); + return Sort.by(orders); } public static void buildParameters(Builder builder) { diff --git a/application/src/main/java/run/halo/app/content/comment/CommentQuery.java b/application/src/main/java/run/halo/app/content/comment/CommentQuery.java index 4caef1126..8bb741fc9 100644 --- a/application/src/main/java/run/halo/app/content/comment/CommentQuery.java +++ b/application/src/main/java/run/halo/app/content/comment/CommentQuery.java @@ -1,26 +1,23 @@ package run.halo.app.content.comment; import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; -import static run.halo.app.extension.index.query.QueryFactory.and; +import static org.springframework.data.domain.Sort.Order.desc; import static run.halo.app.extension.index.query.QueryFactory.contains; import static run.halo.app.extension.index.query.QueryFactory.equal; -import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions; import io.swagger.v3.oas.annotations.enums.ParameterIn; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.springdoc.core.fn.builders.operation.Builder; import org.springframework.data.domain.Sort; +import org.springframework.lang.Nullable; import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.server.ServerWebExchange; import run.halo.app.core.extension.User; import run.halo.app.core.extension.content.Comment; -import run.halo.app.core.extension.endpoint.SortResolver; import run.halo.app.extension.ListOptions; -import run.halo.app.extension.PageRequest; -import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.router.IListRequest; import run.halo.app.extension.router.QueryParamBuildUtil; -import run.halo.app.extension.router.selector.FieldSelector; +import run.halo.app.extension.router.SortableRequest; /** * Query criteria for comment list. @@ -28,64 +25,56 @@ import run.halo.app.extension.router.selector.FieldSelector; * @author guqing * @since 2.0.0 */ -public class CommentQuery extends IListRequest.QueryListRequest { - - private final ServerWebExchange exchange; +public class CommentQuery extends SortableRequest { public CommentQuery(ServerRequest request) { - super(request.queryParams()); - this.exchange = request.exchange(); + super(request.exchange()); } + @Nullable public String getKeyword() { - String keyword = queryParams.getFirst("keyword"); - return StringUtils.isBlank(keyword) ? null : keyword; + return queryParams.getFirst("keyword"); } + @Nullable public String getOwnerKind() { - String ownerKind = queryParams.getFirst("ownerKind"); - return StringUtils.isBlank(ownerKind) ? null : ownerKind; + return queryParams.getFirst("ownerKind"); } + @Nullable public String getOwnerName() { - String ownerName = queryParams.getFirst("ownerName"); - return StringUtils.isBlank(ownerName) ? null : ownerName; + return queryParams.getFirst("ownerName"); } + @Override public Sort getSort() { - var sort = SortResolver.defaultInstance.resolve(exchange); - return sort.and(Sort.by("status.lastReplyTime", - "spec.creationTime", - "metadata.name" - ).descending()); - } - - public PageRequest toPageRequest() { - return PageRequestImpl.of(getPage(), getSize(), getSort()); + // set default sort by last reply time + return super.getSort().and(Sort.by(desc("status.lastReplyTime"))); } /** * Convert to list options. */ + @Override public ListOptions toListOptions() { - var listOptions = - labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector()); - var fieldQuery = listOptions.getFieldSelector().query(); + var builder = ListOptions.builder(super.toListOptions()); - String keyword = getKeyword(); - if (StringUtils.isNotBlank(keyword)) { - fieldQuery = and(fieldQuery, contains("spec.raw", keyword)); - } + Optional.ofNullable(getKeyword()) + .filter(StringUtils::isNotBlank) + .ifPresent(keyword -> builder.andQuery(contains("spec.raw", keyword))); - String ownerName = getOwnerName(); - if (StringUtils.isNotBlank(ownerName)) { - String ownerKind = StringUtils.defaultIfBlank(getOwnerKind(), User.KIND); - fieldQuery = and(fieldQuery, - equal("spec.owner", Comment.CommentOwner.ownerIdentity(ownerKind, ownerName))); - } + Optional.ofNullable(getOwnerName()) + .filter(StringUtils::isNotBlank) + .ifPresent(ownerName -> { + var ownerKind = Optional.ofNullable(getOwnerKind()) + .filter(StringUtils::isNotBlank) + .orElse(User.KIND); + builder.andQuery( + equal("spec.owner", Comment.CommentOwner.ownerIdentity(ownerKind, ownerName)) + ); + }); - listOptions.setFieldSelector(FieldSelector.of(fieldQuery)); - return listOptions; + return builder.build(); } public static void buildParameters(Builder builder) { diff --git a/application/src/main/java/run/halo/app/content/impl/PostServiceImpl.java b/application/src/main/java/run/halo/app/content/impl/PostServiceImpl.java index 1b2c97471..bf535a9fa 100644 --- a/application/src/main/java/run/halo/app/content/impl/PostServiceImpl.java +++ b/application/src/main/java/run/halo/app/content/impl/PostServiceImpl.java @@ -38,7 +38,6 @@ import run.halo.app.core.extension.service.UserService; import run.halo.app.extension.ListOptions; import run.halo.app.extension.ListResult; import run.halo.app.extension.MetadataOperator; -import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.Ref; import run.halo.app.extension.router.selector.FieldSelector; @@ -73,9 +72,9 @@ public class PostServiceImpl extends AbstractContentService implements PostServi @Override public Mono> listPost(PostQuery query) { return buildListOptions(query) - .flatMap(listOptions -> client.listBy(Post.class, listOptions, - PageRequestImpl.of(query.getPage(), query.getSize(), query.getSort()) - )) + .flatMap(listOptions -> + client.listBy(Post.class, listOptions, query.toPageRequest()) + ) .flatMap(listResult -> Flux.fromStream(listResult.get()) .map(this::getListedPost) .concatMap(Function.identity()) diff --git a/application/src/main/java/run/halo/app/content/impl/SinglePageServiceImpl.java b/application/src/main/java/run/halo/app/content/impl/SinglePageServiceImpl.java index 8fc760944..ab055b67d 100644 --- a/application/src/main/java/run/halo/app/content/impl/SinglePageServiceImpl.java +++ b/application/src/main/java/run/halo/app/content/impl/SinglePageServiceImpl.java @@ -86,17 +86,16 @@ public class SinglePageServiceImpl extends AbstractContentService implements Sin @Override public Mono> list(SinglePageQuery query) { - return client.list(SinglePage.class, query.toPredicate(), - query.toComparator(), query.getPage(), query.getSize()) - .flatMap(listResult -> Flux.fromStream( - listResult.get().map(this::getListedSinglePage) - ) - .concatMap(Function.identity()) - .collectList() - .map(listedSinglePages -> new ListResult<>(listResult.getPage(), - listResult.getSize(), - listResult.getTotal(), listedSinglePages) - ) + return client.listBy(SinglePage.class, query.toListOptions(), query.toPageRequest()) + .flatMap(listResult -> Flux.fromStream(listResult.get().map(this::getListedSinglePage)) + .concatMap(Function.identity()) + .collectList() + .map(listedSinglePages -> new ListResult<>( + listResult.getPage(), + listResult.getSize(), + listResult.getTotal(), + listedSinglePages) + ) ); } diff --git a/application/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpoint.java b/application/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpoint.java index 0d75c422a..3e9c5b35d 100644 --- a/application/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpoint.java @@ -10,26 +10,20 @@ import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder; import static org.springframework.boot.convert.ApplicationConversionService.getSharedInstance; import static org.springframework.web.reactive.function.server.RequestPredicates.contentType; import static run.halo.app.extension.ListResult.generateGenericClass; -import static run.halo.app.extension.index.query.QueryFactory.all; -import static run.halo.app.extension.index.query.QueryFactory.and; import static run.halo.app.extension.index.query.QueryFactory.contains; import static run.halo.app.extension.index.query.QueryFactory.in; import static run.halo.app.extension.index.query.QueryFactory.isNull; import static run.halo.app.extension.index.query.QueryFactory.not; import static run.halo.app.extension.index.query.QueryFactory.startsWith; -import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions; import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import java.net.URL; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.BooleanUtils; import org.springdoc.core.fn.builders.operation.Builder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; import org.springframework.data.domain.Sort; @@ -45,21 +39,19 @@ import org.springframework.web.reactive.function.BodyExtractors; 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 org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import reactor.core.publisher.Mono; import run.halo.app.core.extension.attachment.Attachment; import run.halo.app.core.extension.attachment.Group; import run.halo.app.core.extension.endpoint.CustomEndpoint; -import run.halo.app.core.extension.endpoint.SortResolver; import run.halo.app.core.extension.service.AttachmentService; import run.halo.app.extension.ListOptions; import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.index.query.QueryFactory; import run.halo.app.extension.router.IListRequest; -import run.halo.app.extension.router.IListRequest.QueryListRequest; import run.halo.app.extension.router.QueryParamBuildUtil; +import run.halo.app.extension.router.SortableRequest; import run.halo.app.extension.router.selector.LabelSelector; @Slf4j @@ -131,7 +123,7 @@ public class AttachmentEndpoint implements CustomEndpoint { .response( responseBuilder().implementation(generateGenericClass(Attachment.class)) ); - ISearchRequest.buildParameters(builder); + SearchRequest.buildParameters(builder); } ) .build(); @@ -159,33 +151,58 @@ public class AttachmentEndpoint implements CustomEndpoint { ); } - public interface ISearchRequest extends IListRequest { + public static class SearchRequest extends SortableRequest { - @Schema(description = "Keyword for searching.") - Optional getKeyword(); + public SearchRequest(ServerRequest request) { + super(request.exchange()); + } - @Schema(description = "Filter attachments without group. This parameter will ignore group" - + " parameter.") - Optional getUngrouped(); + public Optional getKeyword() { + return Optional.ofNullable(queryParams.getFirst("keyword")) + .filter(StringUtils::hasText); + } - @ArraySchema(uniqueItems = true, - arraySchema = @Schema(name = "accepts", - description = "Acceptable media types."), - schema = @Schema(description = "like image/*, video/mp4, text/*", - implementation = String.class, - example = "image/*")) - List getAccepts(); + public Optional getUngrouped() { + return Optional.ofNullable(queryParams.getFirst("ungrouped")) + .map(ungroupedStr -> getSharedInstance().convert(ungroupedStr, Boolean.class)); + } - @ArraySchema(uniqueItems = true, - arraySchema = @Schema(name = "sort", - description = "Sort property and direction of the list result. Supported fields: " - + "creationTimestamp, size"), - schema = @Schema(description = "like field,asc or field,desc", - implementation = String.class, - example = "creationTimestamp,desc")) - Sort getSort(); + public Optional> getAccepts() { + return Optional.ofNullable(queryParams.get("accepts")) + .filter(accepts -> !accepts.isEmpty() + && !accepts.contains("*") + && !accepts.contains("*/*") + ); + } - static void buildParameters(Builder builder) { + public ListOptions toListOptions(List hiddenGroups) { + var builder = ListOptions.builder(super.toListOptions()); + + getKeyword().ifPresent(keyword -> { + builder.andQuery(contains("spec.displayName", keyword)); + }); + + getUngrouped() + .filter(ungrouped -> ungrouped) + .ifPresent(ungrouped -> builder.andQuery(isNull("spec.groupName"))); + + if (!CollectionUtils.isEmpty(hiddenGroups)) { + builder.andQuery(not(in("spec.groupName", hiddenGroups))); + } + + getAccepts().flatMap(accepts -> accepts.stream() + .filter(StringUtils::hasText) + .map(accept -> accept.replace("/*", "/").toLowerCase()) + .distinct() + .map(accept -> startsWith("spec.mediaType", accept)) + .reduce(QueryFactory::or) + ) + .ifPresent(builder::andQuery); + + return builder.build(); + } + + public static void buildParameters(Builder builder) { IListRequest.buildParameters(builder); builder.parameter(QueryParamBuildUtil.sortParameter()) .parameter(parameterBuilder() @@ -220,82 +237,6 @@ public class AttachmentEndpoint implements CustomEndpoint { } } - public static class SearchRequest extends QueryListRequest implements ISearchRequest { - - private final ServerWebExchange exchange; - - public SearchRequest(ServerRequest request) { - super(request.queryParams()); - this.exchange = request.exchange(); - } - - @Override - public Optional getKeyword() { - return Optional.ofNullable(queryParams.getFirst("keyword")) - .filter(StringUtils::hasText); - } - - @Override - public Optional getUngrouped() { - return Optional.ofNullable(queryParams.getFirst("ungrouped")) - .map(ungroupedStr -> getSharedInstance().convert(ungroupedStr, Boolean.class)); - } - - @Override - public List getAccepts() { - return queryParams.getOrDefault("accepts", Collections.emptyList()); - } - - @Override - public Sort getSort() { - var sort = SortResolver.defaultInstance.resolve(exchange); - sort = sort.and(Sort.by( - Sort.Order.desc("metadata.creationTimestamp"), - Sort.Order.asc("metadata.name") - )); - return sort; - } - - public ListOptions toListOptions(List hiddenGroups) { - final var listOptions = - labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector()); - - var fieldQuery = all(); - if (getKeyword().isPresent()) { - fieldQuery = and(fieldQuery, contains("spec.displayName", getKeyword().get())); - } - - if (getUngrouped().isPresent() && BooleanUtils.isTrue(getUngrouped().get())) { - fieldQuery = and(fieldQuery, isNull("spec.groupName")); - } - - if (!hiddenGroups.isEmpty()) { - fieldQuery = and(fieldQuery, not(in("spec.groupName", hiddenGroups))); - } - - if (hasAccepts()) { - var acceptFieldQueryOptional = getAccepts().stream() - .filter(StringUtils::hasText) - .map((accept -> accept.replace("/*", "/").toLowerCase())) - .distinct() - .map(accept -> startsWith("spec.mediaType", accept)) - .reduce(QueryFactory::or); - if (acceptFieldQueryOptional.isPresent()) { - fieldQuery = and(fieldQuery, acceptFieldQueryOptional.get()); - } - } - - listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(fieldQuery)); - return listOptions; - } - - private boolean hasAccepts() { - return !CollectionUtils.isEmpty(getAccepts()) - && !getAccepts().contains("*") - && !getAccepts().contains("*/*"); - } - } - public record UploadFromUrlRequest(@Schema(requiredMode = REQUIRED) URL url, @Schema(requiredMode = REQUIRED) String policyName, String groupName, diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java index 5e3af3aeb..a05a216bd 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java @@ -2,7 +2,6 @@ package run.halo.app.core.extension.endpoint; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; -import static java.util.Comparator.comparing; import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; import static org.springdoc.core.fn.builders.content.Builder.contentBuilder; import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; @@ -12,12 +11,13 @@ import static org.springframework.boot.convert.ApplicationConversionService.getS import static org.springframework.core.io.buffer.DataBufferUtils.write; import static org.springframework.web.reactive.function.server.RequestPredicates.contentType; import static run.halo.app.extension.ListResult.generateGenericClass; +import static run.halo.app.extension.index.query.QueryFactory.contains; +import static run.halo.app.extension.index.query.QueryFactory.equal; +import static run.halo.app.extension.index.query.QueryFactory.or; import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter; -import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate; import static run.halo.app.infra.utils.FileUtils.deleteFileSilently; import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import java.io.FileNotFoundException; import java.io.IOException; @@ -27,13 +27,9 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.time.Instant; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; import java.util.Map; -import java.util.Objects; +import java.util.Optional; import java.util.function.Function; -import java.util.function.Predicate; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; @@ -57,7 +53,6 @@ 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 org.springframework.web.reactive.resource.NoResourceFoundException; -import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; @@ -67,11 +62,11 @@ import run.halo.app.core.extension.Plugin; import run.halo.app.core.extension.Setting; import run.halo.app.core.extension.service.PluginService; import run.halo.app.core.extension.theme.SettingUtils; -import run.halo.app.extension.Comparators; import run.halo.app.extension.ConfigMap; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.router.IListRequest; -import run.halo.app.extension.router.IListRequest.QueryListRequest; +import run.halo.app.extension.router.SortableRequest; import run.halo.app.infra.ReactiveUrlDataBufferFetcher; import run.halo.app.plugin.PluginNotFoundException; @@ -548,13 +543,10 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean { .flatMap(resourceClosure); } - public static class ListRequest extends QueryListRequest { - - private final ServerWebExchange exchange; + public static class ListRequest extends SortableRequest { public ListRequest(ServerRequest request) { - super(request.queryParams()); - this.exchange = request.exchange(); + super(request.exchange()); } @Schema(name = "keyword", description = "Keyword of plugin name or description") @@ -568,69 +560,35 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean { return enabled == null ? null : getSharedInstance().convert(enabled, Boolean.class); } - @ArraySchema(uniqueItems = true, - arraySchema = @Schema(name = "sort", - description = "Sort property and direction of the list result. Supported fields: " - + "creationTimestamp"), - schema = @Schema(description = "like field,asc or field,desc", - implementation = String.class, - example = "creationTimestamp,desc")) + @Override public Sort getSort() { - return SortResolver.defaultInstance.resolve(exchange); + var orders = super.getSort().stream() + .map(order -> { + if ("creationTimestamp".equals(order.getProperty())) { + return order.withProperty("metadata.creationTimestamp"); + } + return order; + }) + .toList(); + return Sort.by(orders); } - public Predicate toPredicate() { - Predicate displayNamePredicate = plugin -> { - var keyword = getKeyword(); - if (!StringUtils.hasText(keyword)) { - return true; - } - var displayName = plugin.getSpec().getDisplayName(); - if (!StringUtils.hasText(displayName)) { - return false; - } - return displayName.toLowerCase().contains(keyword.trim().toLowerCase()); - }; - Predicate descriptionPredicate = plugin -> { - var keyword = getKeyword(); - if (!StringUtils.hasText(keyword)) { - return true; - } - var description = plugin.getSpec().getDescription(); - if (!StringUtils.hasText(description)) { - return false; - } - return description.toLowerCase().contains(keyword.trim().toLowerCase()); - }; - Predicate enablePredicate = plugin -> { - var enabled = getEnabled(); - if (enabled == null) { - return true; - } - return Objects.equals(enabled, plugin.getSpec().getEnabled()); - }; - return displayNamePredicate.or(descriptionPredicate) - .and(enablePredicate) - .and(labelAndFieldSelectorToPredicate(getLabelSelector(), getFieldSelector())); - } + @Override + public ListOptions toListOptions() { + var builder = ListOptions.builder(super.toListOptions()); - public Comparator toComparator() { - var sort = getSort(); - var ctOrder = sort.getOrderFor("creationTimestamp"); - List> comparators = new ArrayList<>(); - if (ctOrder != null) { - Comparator comparator = - comparing(plugin -> plugin.getMetadata().getCreationTimestamp()); - if (ctOrder.isDescending()) { - comparator = comparator.reversed(); - } - comparators.add(comparator); - } - comparators.add(Comparators.compareCreationTimestamp(false)); - comparators.add(Comparators.compareName(true)); - return comparators.stream() - .reduce(Comparator::thenComparing) - .orElse(null); + Optional.ofNullable(queryParams.getFirst("keyword")) + .filter(StringUtils::hasText) + .ifPresent(keyword -> builder.andQuery(or( + contains("spec.displayName", keyword), + contains("spec.description", keyword) + ))); + + Optional.ofNullable(queryParams.getFirst("enabled")) + .map(Boolean::parseBoolean) + .ifPresent(enabled -> builder.andQuery(equal("spec.enabled", enabled.toString()))); + + return builder.build(); } public static void buildParameters(Builder builder) { @@ -654,15 +612,11 @@ public class PluginEndpoint implements CustomEndpoint, InitializingBean { Mono list(ServerRequest request) { return Mono.just(request) .map(ListRequest::new) - .flatMap(listRequest -> { - var predicate = listRequest.toPredicate(); - var comparator = listRequest.toComparator(); - return client.list(Plugin.class, - predicate, - comparator, - listRequest.getPage(), - listRequest.getSize()); - }) + .flatMap(listRequest -> client.listBy( + Plugin.class, + listRequest.toListOptions(), + listRequest.toPageRequest() + )) .flatMap(listResult -> ServerResponse.ok().bodyValue(listResult)); } diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java index 5720ff6a3..07049041a 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java @@ -403,7 +403,7 @@ public class PostEndpoint implements CustomEndpoint { /** * Convenient for testing, to avoid waiting too long for post published when testing. */ - public void setMaxAttemptsWaitForPublish(int maxAttempts) { + void setMaxAttemptsWaitForPublish(int maxAttempts) { this.maxAttemptsWaitForPublish = maxAttempts; } } diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/TagEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/TagEndpoint.java index 47a90b711..ca768092f 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/TagEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/TagEndpoint.java @@ -2,33 +2,29 @@ package run.halo.app.core.extension.endpoint; import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; -import static run.halo.app.extension.index.query.QueryFactory.all; +import static run.halo.app.extension.index.query.QueryFactory.contains; +import static run.halo.app.extension.index.query.QueryFactory.or; import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter; -import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions; import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Schema; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springdoc.core.fn.builders.operation.Builder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; 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 org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Tag; import run.halo.app.extension.ListOptions; import run.halo.app.extension.ListResult; import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.extension.index.query.QueryFactory; import run.halo.app.extension.router.IListRequest; +import run.halo.app.extension.router.SortableRequest; /** * post tag endpoint. @@ -68,61 +64,27 @@ public class TagEndpoint implements CustomEndpoint { .flatMap(tags -> ServerResponse.ok().bodyValue(tags)); } - public interface ITagQuery extends IListRequest { - - @Schema(description = "Keyword for searching.") - Optional getKeyword(); - - @ArraySchema(uniqueItems = true, - arraySchema = @Schema(name = "sort", - description = "Sort property and direction of the list result. Supported fields: " - + "creationTimestamp, name"), - schema = @Schema(description = "like field,asc or field,desc", - implementation = String.class, - example = "creationTimestamp,desc")) - Sort getSort(); - } - - public static class TagQuery extends IListRequest.QueryListRequest - implements ITagQuery { - - private final ServerWebExchange exchange; + public static class TagQuery extends SortableRequest { public TagQuery(ServerRequest request) { - super(request.queryParams()); - this.exchange = request.exchange(); + super(request.exchange()); } - @Override public Optional getKeyword() { return Optional.ofNullable(queryParams.getFirst("keyword")) .filter(StringUtils::hasText); } @Override - public Sort getSort() { - var sort = SortResolver.defaultInstance.resolve(exchange); - sort = sort.and(Sort.by( - Sort.Order.desc("metadata.creationTimestamp"), - Sort.Order.asc("metadata.name") - )); - return sort; - } - public ListOptions toListOptions() { - final var listOptions = - labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector()); - - var fieldQuery = all(); - if (getKeyword().isPresent()) { - fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.or( - QueryFactory.contains("spec.displayName", getKeyword().get()), - QueryFactory.contains("spec.slug", getKeyword().get()) - )); - } - - listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(fieldQuery)); - return listOptions; + var builder = ListOptions.builder(super.toListOptions()); + getKeyword().ifPresent(keyword -> builder.andQuery( + or( + contains("spec.displayName", keyword), + contains("spec.slug", keyword) + ) + )); + return builder.build(); } public static void buildParameters(Builder builder) { diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java index 932a3925b..74d2bb0b5 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java @@ -14,7 +14,6 @@ import static run.halo.app.extension.index.query.QueryFactory.contains; import static run.halo.app.extension.index.query.QueryFactory.equal; import static run.halo.app.extension.index.query.QueryFactory.in; import static run.halo.app.extension.index.query.QueryFactory.or; -import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter; import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions; import static run.halo.app.security.authorization.AuthorityUtils.authoritiesToRoles; @@ -24,7 +23,6 @@ import io.github.resilience4j.ratelimiter.RateLimiterRegistry; import io.github.resilience4j.ratelimiter.RequestNotPermitted; import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator; import io.swagger.v3.oas.annotations.enums.ParameterIn; -import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import java.security.Principal; import java.time.Duration; @@ -48,7 +46,6 @@ import org.springdoc.core.fn.builders.operation.Builder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.dao.OptimisticLockingFailureException; -import org.springframework.data.domain.Sort; import org.springframework.http.MediaType; import org.springframework.http.codec.multipart.FilePart; import org.springframework.http.codec.multipart.Part; @@ -65,7 +62,6 @@ import org.springframework.web.reactive.function.BodyExtractors; 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 org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -84,7 +80,7 @@ import run.halo.app.extension.Metadata; import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.extension.router.IListRequest; +import run.halo.app.extension.router.SortableRequest; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; import run.halo.app.infra.ValidationUtils; @@ -687,13 +683,10 @@ public class UserEndpoint implements CustomEndpoint { } - public static class ListRequest extends IListRequest.QueryListRequest { - - private final ServerWebExchange exchange; + public static class ListRequest extends SortableRequest { public ListRequest(ServerRequest request) { - super(request.queryParams()); - this.exchange = request.exchange(); + super(request.exchange()); } @Schema(name = "keyword") @@ -706,19 +699,6 @@ public class UserEndpoint implements CustomEndpoint { return queryParams.getFirst("role"); } - @ArraySchema(uniqueItems = true, - arraySchema = @Schema(name = "sort", - description = "Sort property and direction of the list result. Supported fields: " - + "creationTimestamp"), - schema = @Schema(description = "like field,asc or field,desc", - implementation = String.class, - example = "creationTimestamp,desc")) - public Sort getSort() { - var sort = SortResolver.defaultInstance.resolve(exchange); - sort = sort.and(Sort.by("metadata.creationTimestamp", "metadata.name").descending()); - return sort; - } - /** * Converts query parameters to list options. */ @@ -743,9 +723,8 @@ public class UserEndpoint implements CustomEndpoint { } public static void buildParameters(Builder builder) { - IListRequest.buildParameters(builder); - builder.parameter(sortParameter()) - .parameter(parameterBuilder() + SortableRequest.buildParameters(builder); + builder.parameter(parameterBuilder() .in(ParameterIn.QUERY) .name("keyword") .description("Keyword to search") @@ -770,8 +749,7 @@ public class UserEndpoint implements CustomEndpoint { .map(UserEndpoint.ListRequest::new) .flatMap(listRequest -> client.listBy(User.class, listRequest.toListOptions(), PageRequestImpl.of( - listRequest.getPage(), listRequest.getSize(), - listRequest.getSort() + listRequest.getPage(), listRequest.getSize(), listRequest.getSort() ) )) .flatMap(this::toListedUser) diff --git a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java index 80292bf2e..eca97c4cd 100644 --- a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java +++ b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java @@ -8,6 +8,7 @@ import static run.halo.app.extension.index.IndexAttributeFactory.multiValueAttri import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute; import com.fasterxml.jackson.core.type.TypeReference; +import java.time.Instant; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -95,7 +96,33 @@ public class SchemeInitializer implements ApplicationListener { + is.add(new IndexSpec() + .setName("spec.displayName") + .setIndexFunc( + simpleAttribute(Plugin.class, plugin -> Optional.ofNullable(plugin.getSpec()) + .map(Plugin.PluginSpec::getDisplayName) + .orElse(null)) + ) + ); + is.add(new IndexSpec() + .setName("spec.description") + .setIndexFunc( + simpleAttribute(Plugin.class, plugin -> Optional.ofNullable(plugin.getSpec()) + .map(Plugin.PluginSpec::getDescription) + .orElse(null)) + ) + ); + is.add(new IndexSpec() + .setName("spec.enabled") + .setIndexFunc( + simpleAttribute(Plugin.class, plugin -> Optional.ofNullable(plugin.getSpec()) + .map(Plugin.PluginSpec::getEnabled) + .map(Object::toString) + .orElse(Boolean.FALSE.toString())) + ) + ); + }); schemeManager.register(SearchEngine.class); schemeManager.register(ExtensionPointDefinition.class, indexSpecs -> { indexSpecs.add(new IndexSpec() @@ -443,7 +470,85 @@ public class SchemeInitializer implements ApplicationListener { + is.add(new IndexSpec() + .setName("spec.publishTime") + .setIndexFunc( + simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getSpec()) + .map(SinglePage.SinglePageSpec::getPublishTime) + .map(Instant::toString) + .orElse(null)) + ) + ); + is.add(new IndexSpec() + .setName("spec.title") + .setIndexFunc( + simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getSpec()) + .map(SinglePage.SinglePageSpec::getTitle) + .orElse(null)) + ) + ); + is.add(new IndexSpec() + .setName("spec.slug") + .setUnique(true) + .setIndexFunc( + simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getSpec()) + .map(SinglePage.SinglePageSpec::getSlug) + .orElse(null)) + ) + ); + is.add(new IndexSpec() + .setName("spec.visible") + .setIndexFunc( + simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getSpec()) + .map(SinglePage.SinglePageSpec::getVisible) + .map(Post.VisibleEnum::name) + .orElse(null)) + ) + ); + is.add(new IndexSpec() + .setName("spec.pinned") + .setIndexFunc( + simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getSpec()) + .map(SinglePage.SinglePageSpec::getPinned) + .map(Object::toString) + .orElse(Boolean.FALSE.toString()))) + ); + is.add(new IndexSpec() + .setName("spec.priority") + .setIndexFunc(simpleAttribute(SinglePage.class, + page -> Optional.ofNullable(page.getSpec()) + .map(SinglePage.SinglePageSpec::getPriority) + .map(Object::toString) + .orElse(Integer.toString(0))) + ) + ); + is.add(new IndexSpec() + .setName("status.excerpt") + .setIndexFunc( + simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getStatus()) + .map(SinglePage.SinglePageStatus::getExcerpt) + .orElse(null)) + ) + ); + is.add(new IndexSpec() + .setName("status.phase") + .setIndexFunc( + simpleAttribute(SinglePage.class, page -> Optional.ofNullable(page.getStatus()) + .map(SinglePage.SinglePageStatus::getPhase) + .orElse(null)) + ) + ); + is.add(new IndexSpec() + .setName("status.contributors") + .setIndexFunc(multiValueAttribute(SinglePage.class, + page -> Optional.ofNullable(page.getStatus()) + .map(SinglePage.SinglePageStatus::getContributors) + .map(Set::copyOf) + .orElse(null)) + ) + ); + }); // storage.halo.run schemeManager.register(Group.class); schemeManager.register(Policy.class); diff --git a/application/src/main/java/run/halo/app/theme/endpoint/SinglePageQueryEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/SinglePageQueryEndpoint.java index e66ace47e..933f3516d 100644 --- a/application/src/main/java/run/halo/app/theme/endpoint/SinglePageQueryEndpoint.java +++ b/application/src/main/java/run/halo/app/theme/endpoint/SinglePageQueryEndpoint.java @@ -19,6 +19,7 @@ import run.halo.app.core.extension.endpoint.CustomEndpoint; import run.halo.app.extension.GroupVersion; import run.halo.app.extension.ListResult; import run.halo.app.extension.router.SortableRequest; +import run.halo.app.theme.finders.SinglePageConversionService; import run.halo.app.theme.finders.SinglePageFinder; import run.halo.app.theme.finders.vo.ListedSinglePageVo; import run.halo.app.theme.finders.vo.SinglePageVo; @@ -35,6 +36,8 @@ public class SinglePageQueryEndpoint implements CustomEndpoint { private final SinglePageFinder singlePageFinder; + private final SinglePageConversionService singlePageConversionService; + @Override public RouterFunction endpoint() { var tag = "SinglePageV1alpha1Public"; @@ -79,15 +82,8 @@ public class SinglePageQueryEndpoint implements CustomEndpoint { private Mono listSinglePages(ServerRequest request) { var query = new SinglePagePublicQuery(request.exchange()); - return singlePageFinder.list(query.getPage(), - query.getSize(), - query.toPredicate(), - query.toComparator() - ) - .flatMap(result -> ServerResponse.ok() - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(result) - ); + return singlePageConversionService.listBy(query.toListOptions(), query.toPageRequest()) + .flatMap(result -> ServerResponse.ok().bodyValue(result)); } static class SinglePagePublicQuery extends SortableRequest { diff --git a/application/src/main/java/run/halo/app/theme/finders/SinglePageConversionService.java b/application/src/main/java/run/halo/app/theme/finders/SinglePageConversionService.java index 15f6a8541..fc88fb9d4 100644 --- a/application/src/main/java/run/halo/app/theme/finders/SinglePageConversionService.java +++ b/application/src/main/java/run/halo/app/theme/finders/SinglePageConversionService.java @@ -3,6 +3,9 @@ package run.halo.app.theme.finders; import org.springframework.lang.NonNull; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.SinglePage; +import run.halo.app.extension.ListOptions; +import run.halo.app.extension.ListResult; +import run.halo.app.extension.PageRequest; import run.halo.app.theme.ReactiveSinglePageContentHandler; import run.halo.app.theme.finders.vo.ContentVo; import run.halo.app.theme.finders.vo.ListedSinglePageVo; @@ -52,4 +55,7 @@ public interface SinglePageConversionService { Mono getContent(String pageName); Mono convertToListedVo(SinglePage singlePage); + + Mono> listBy(ListOptions listOptions, PageRequest pageRequest); + } diff --git a/application/src/main/java/run/halo/app/theme/finders/SinglePageFinder.java b/application/src/main/java/run/halo/app/theme/finders/SinglePageFinder.java index d577b8877..62b76aab5 100644 --- a/application/src/main/java/run/halo/app/theme/finders/SinglePageFinder.java +++ b/application/src/main/java/run/halo/app/theme/finders/SinglePageFinder.java @@ -1,7 +1,5 @@ package run.halo.app.theme.finders; -import java.util.Comparator; -import java.util.function.Predicate; import org.springframework.lang.Nullable; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.SinglePage; @@ -24,6 +22,4 @@ public interface SinglePageFinder { Mono> list(@Nullable Integer page, @Nullable Integer size); - Mono> list(@Nullable Integer page, @Nullable Integer size, - @Nullable Predicate predicate, @Nullable Comparator comparator); } diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/SinglePageConversionServiceImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/SinglePageConversionServiceImpl.java index 655165d40..a7e497a09 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/SinglePageConversionServiceImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/SinglePageConversionServiceImpl.java @@ -1,16 +1,29 @@ package run.halo.app.theme.finders.impl; +import static org.springframework.data.domain.Sort.Order.asc; +import static org.springframework.data.domain.Sort.Order.desc; +import static run.halo.app.core.extension.content.Post.VisibleEnum.PUBLIC; +import static run.halo.app.core.extension.content.SinglePage.PUBLISHED_LABEL; +import static run.halo.app.extension.ExtensionUtil.notDeleting; +import static run.halo.app.extension.index.query.QueryFactory.equal; + import java.util.List; import java.util.function.Function; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.content.ContentWrapper; import run.halo.app.content.SinglePageService; import run.halo.app.core.extension.content.SinglePage; +import run.halo.app.extension.ListOptions; +import run.halo.app.extension.ListResult; +import run.halo.app.extension.PageRequest; +import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.metrics.CounterService; import run.halo.app.metrics.MeterUtils; @@ -102,6 +115,40 @@ public class SinglePageConversionServiceImpl implements SinglePageConversionServ .flatMap(this::populateContributors); } + @Override + public Mono> listBy(ListOptions listOptions, + PageRequest pageRequest) { + // rewrite list options + var rewroteListOptions = ListOptions.builder(listOptions) + .andQuery(notDeleting()) + .andQuery(equal("spec.deleted", Boolean.FALSE.toString())) + .andQuery(equal("spec.visible", PUBLIC.name())) + .labelSelector() + .eq(PUBLISHED_LABEL, Boolean.TRUE.toString()) + .end() + .build(); + + // rewrite sort + var rewroteSort = pageRequest.getSort() + .and(Sort.by( + desc("spec.pinned"), + asc("spec.priority") + )); + + var rewrotePageRequest = + PageRequestImpl.of(pageRequest.getPageNumber(), pageRequest.getPageSize(), rewroteSort); + + return client.listBy(SinglePage.class, rewroteListOptions, rewrotePageRequest) + .flatMap(list -> Flux.fromStream(list.get()) + .concatMap(this::convertToListedVo) + .collectList() + .map(pageVos -> + new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), pageVos) + ) + ); + } + + Mono convert(SinglePage singlePage, String snapshotName) { Assert.notNull(singlePage, "Single page must not be null"); Assert.hasText(snapshotName, "Snapshot name must not be empty"); diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java index 256e7ef81..eeeb08b4a 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java @@ -1,23 +1,17 @@ package run.halo.app.theme.finders.impl; import java.security.Principal; -import java.time.Instant; -import java.util.Comparator; -import java.util.List; import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; import java.util.function.Predicate; import lombok.AllArgsConstructor; -import org.apache.commons.lang3.ObjectUtils; -import org.springframework.lang.Nullable; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.SinglePage; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.ListResult; +import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.AnonymousUserConst; import run.halo.app.theme.finders.Finder; @@ -37,10 +31,6 @@ import run.halo.app.theme.finders.vo.SinglePageVo; @AllArgsConstructor public class SinglePageFinderImpl implements SinglePageFinder { - public static final Predicate FIXED_PREDICATE = page -> page.isPublished() - && Objects.equals(false, page.getSpec().getDeleted()) - && Post.VisibleEnum.PUBLIC.equals(page.getSpec().getVisible()); - private final ReactiveExtensionClient client; private final SinglePageConversionService singlePagePublicQueryService; @@ -59,27 +49,10 @@ public class SinglePageFinderImpl implements SinglePageFinder { @Override public Mono> list(Integer page, Integer size) { - return list(page, size, null, null); - } - - @Override - public Mono> list(@Nullable Integer page, @Nullable Integer size, - @Nullable Predicate predicate, @Nullable Comparator comparator) { - var predicateToUse = Optional.ofNullable(predicate) - .map(p -> p.and(FIXED_PREDICATE)) - .orElse(FIXED_PREDICATE); - var comparatorToUse = Optional.ofNullable(comparator) - .orElse(defaultComparator()); - return client.list(SinglePage.class, predicateToUse, - comparatorToUse, pageNullSafe(page), sizeNullSafe(size)) - .flatMap(list -> Flux.fromStream(list.get()) - .concatMap(singlePagePublicQueryService::convertToListedVo) - .collectList() - .map(pageVos -> new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), - pageVos) - ) - ) - .defaultIfEmpty(new ListResult<>(0, 0, 0, List.of())); + return singlePagePublicQueryService.listBy( + new ListOptions(), + PageRequestImpl.of(page, size) + ); } Mono> queryPredicate() { @@ -101,26 +74,4 @@ public class SinglePageFinderImpl implements SinglePageFinder { .filter(name -> !AnonymousUserConst.isAnonymousUser(name)); } - static Comparator defaultComparator() { - Function pinned = - page -> Objects.requireNonNullElse(page.getSpec().getPinned(), false); - Function priority = - page -> Objects.requireNonNullElse(page.getSpec().getPriority(), 0); - Function creationTimestamp = - page -> page.getMetadata().getCreationTimestamp(); - Function name = page -> page.getMetadata().getName(); - return Comparator.comparing(pinned) - .thenComparing(priority) - .thenComparing(creationTimestamp) - .thenComparing(name) - .reversed(); - } - - int pageNullSafe(Integer page) { - return ObjectUtils.defaultIfNull(page, 1); - } - - int sizeNullSafe(Integer size) { - return ObjectUtils.defaultIfNull(size, 10); - } } diff --git a/application/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java b/application/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java index 75ec310c4..3de56eb72 100644 --- a/application/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java +++ b/application/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java @@ -2,8 +2,6 @@ package run.halo.app.core.extension.endpoint; import static java.util.Objects.requireNonNull; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.same; @@ -23,9 +21,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; -import java.util.ArrayList; import java.util.List; -import java.util.Objects; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -50,8 +46,10 @@ import run.halo.app.core.extension.Plugin; import run.halo.app.core.extension.Setting; import run.halo.app.core.extension.service.PluginService; import run.halo.app.extension.ConfigMap; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.ListResult; import run.halo.app.extension.Metadata; +import run.halo.app.extension.PageRequest; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.SystemVersionSupplier; import run.halo.app.infra.utils.FileUtils; @@ -80,7 +78,7 @@ class PluginEndpointTest { @Test void shouldListEmptyPluginsWhenNoPlugins() { - when(client.list(same(Plugin.class), any(), any(), anyInt(), anyInt())) + when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class))) .thenReturn(Mono.just(ListResult.emptyResult())); bindToRouterFunction(endpoint.endpoint()) @@ -101,7 +99,7 @@ class PluginEndpointTest { createPlugin("fake-plugin-3") ); var expectResult = new ListResult<>(plugins); - when(client.list(same(Plugin.class), any(), any(), anyInt(), anyInt())) + when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class))) .thenReturn(Mono.just(expectResult)); bindToRouterFunction(endpoint.endpoint()) @@ -126,7 +124,7 @@ class PluginEndpointTest { expectPlugin ); var expectResult = new ListResult<>(plugins); - when(client.list(same(Plugin.class), any(), any(), anyInt(), anyInt())) + when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class))) .thenReturn(Mono.just(expectResult)); bindToRouterFunction(endpoint.endpoint()) @@ -134,27 +132,18 @@ class PluginEndpointTest { .get().uri("/plugins?keyword=Expected") .exchange() .expectStatus().isOk(); - - verify(client).list(same(Plugin.class), argThat( - predicate -> predicate.test(expectPlugin) - && !predicate.test(unexpectedPlugin1) - && !predicate.test(unexpectedPlugin2)), - any(), anyInt(), anyInt()); } @Test void shouldFilterPluginsWhenEnabledProvided() { var expectPlugin = createPlugin("fake-plugin-2", "expected display name", "", true); - var unexpectedPlugin1 = - createPlugin("fake-plugin-1", "first fake display name", "", false); - var unexpectedPlugin2 = - createPlugin("fake-plugin-3", "second fake display name", "", false); var plugins = List.of( expectPlugin ); var expectResult = new ListResult<>(plugins); - when(client.list(same(Plugin.class), any(), any(), anyInt(), anyInt())) + + when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class))) .thenReturn(Mono.just(expectResult)); bindToRouterFunction(endpoint.endpoint()) @@ -162,12 +151,6 @@ class PluginEndpointTest { .get().uri("/plugins?enabled=true") .exchange() .expectStatus().isOk(); - - verify(client).list(same(Plugin.class), argThat( - predicate -> predicate.test(expectPlugin) - && !predicate.test(unexpectedPlugin1) - && !predicate.test(unexpectedPlugin2)), - any(), anyInt(), anyInt()); } @Test @@ -175,7 +158,7 @@ class PluginEndpointTest { var expectPlugin = createPlugin("fake-plugin-2", "expected display name", "", true); var expectResult = new ListResult<>(List.of(expectPlugin)); - when(client.list(same(Plugin.class), any(), any(), anyInt(), anyInt())) + when(client.listBy(same(Plugin.class), any(ListOptions.class), any(PageRequest.class))) .thenReturn(Mono.just(expectResult)); bindToRouterFunction(endpoint.endpoint()) @@ -183,21 +166,6 @@ class PluginEndpointTest { .get().uri("/plugins?sort=creationTimestamp,desc") .exchange() .expectStatus().isOk(); - - verify(client).list(same(Plugin.class), any(), argThat(comparator -> { - var now = Instant.now(); - var plugins = new ArrayList<>(List.of( - createPlugin("fake-plugin-a", now), - createPlugin("fake-plugin-b", now.plusSeconds(1)), - createPlugin("fake-plugin-c", now.plusSeconds(2)) - )); - plugins.sort(comparator); - return Objects.deepEquals(plugins, List.of( - createPlugin("fake-plugin-c", now.plusSeconds(2)), - createPlugin("fake-plugin-b", now.plusSeconds(1)), - createPlugin("fake-plugin-a", now) - )); - }), anyInt(), anyInt()); } } diff --git a/application/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java b/application/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java index 9396cfe66..655c9370c 100644 --- a/application/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java +++ b/application/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java @@ -11,9 +11,8 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf; -import static org.springframework.test.web.reactive.server.WebTestClient.bindToRouterFunction; -import static run.halo.app.extension.GroupVersionKind.fromExtension; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; import java.time.Instant; import java.util.HashMap; @@ -24,19 +23,17 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; -import org.springframework.boot.test.context.SpringBootTest; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.client.MultipartBodyBuilder; -import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.BodyInserters; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.core.extension.Role; -import run.halo.app.core.extension.RoleBinding; import run.halo.app.core.extension.User; import run.halo.app.core.extension.attachment.Attachment; import run.halo.app.core.extension.service.AttachmentService; @@ -46,14 +43,12 @@ import run.halo.app.extension.ListResult; import run.halo.app.extension.Metadata; import run.halo.app.extension.PageRequest; import run.halo.app.extension.ReactiveExtensionClient; -import run.halo.app.extension.exception.ExtensionNotFoundException; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; +import run.halo.app.infra.exception.UserNotFoundException; import run.halo.app.infra.utils.JsonUtils; -@SpringBootTest -@AutoConfigureWebTestClient -@WithMockUser(username = "fake-user", password = "fake-password", roles = "fake-super-role") +@ExtendWith(MockitoExtension.class) class UserEndpointTest { WebTestClient webClient; @@ -78,9 +73,10 @@ class UserEndpointTest { @BeforeEach void setUp() { - // disable authorization - webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()).build() - .mutateWith(csrf()); + webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()) + .apply(springSecurity()) + .build() + .mutateWith(mockUser("fake-user").password("fake-password").roles("fake-super-role")); } @Nested @@ -93,9 +89,7 @@ class UserEndpointTest { when(client.listBy(same(User.class), any(), any(PageRequest.class))) .thenReturn(Mono.just(ListResult.emptyResult())); - bindToRouterFunction(endpoint.endpoint()) - .build() - .get().uri("/users") + webClient.get().uri("/users") .exchange() .expectStatus().isOk() .expectBody() @@ -116,9 +110,7 @@ class UserEndpointTest { when(client.listBy(same(User.class), any(), any(PageRequest.class))) .thenReturn(Mono.just(expectResult)); - bindToRouterFunction(endpoint.endpoint()) - .build() - .get().uri("/users") + webClient.get().uri("/users") .exchange() .expectStatus().isOk() .expectBody() @@ -150,9 +142,7 @@ class UserEndpointTest { when(roleService.getRolesByUsernames(any())).thenReturn(Mono.just(Map.of())); when(roleService.list(anySet())).thenReturn(Flux.empty()); - bindToRouterFunction(endpoint.endpoint()) - .build() - .get().uri("/users?role=guest") + webClient.get().uri("/users?role=guest") .exchange() .expectStatus().isOk(); } @@ -167,9 +157,7 @@ class UserEndpointTest { when(roleService.getRolesByUsernames(any())).thenReturn(Mono.just(Map.of())); when(roleService.list(anySet())).thenReturn(Flux.empty()); - bindToRouterFunction(endpoint.endpoint()) - .build() - .get().uri("/users?sort=creationTimestamp,desc") + webClient.get().uri("/users?sort=creationTimestamp,desc") .exchange() .expectStatus().isOk(); } @@ -190,16 +178,6 @@ class UserEndpointTest { return user; } - User createUser(String name, Instant creationTimestamp) { - var metadata = new Metadata(); - metadata.setName(name); - metadata.setCreationTimestamp(creationTimestamp); - var spec = new User.UserSpec(); - var user = new User(); - user.setMetadata(metadata); - user.setSpec(spec); - return user; - } } @Nested @@ -209,8 +187,7 @@ class UserEndpointTest { @Test void shouldResponseErrorIfUserNotFound() { when(userService.getUser("fake-user")) - .thenReturn(Mono.error( - new ExtensionNotFoundException(fromExtension(User.class), "fake-user"))); + .thenReturn(Mono.error(new UserNotFoundException("fake-user"))); webClient.get().uri("/users/-") .exchange() .expectStatus().isNotFound(); @@ -236,7 +213,6 @@ class UserEndpointTest { .expectHeader().contentType(MediaType.APPLICATION_JSON) .expectBody(UserEndpoint.DetailedUser.class) .isEqualTo(new UserEndpoint.DetailedUser(user, List.of(role))); - // verify(roleService).list(eq(Set.of("role-A"))); } } @@ -267,11 +243,9 @@ class UserEndpointTest { @Test void shouldGetErrorIfUsernameMismatch() { var currentUser = createUser("fake-user"); - var updatedUser = createUser("fake-user"); var requestUser = createUser("another-fake-user"); when(client.get(User.class, "fake-user")).thenReturn(Mono.just(currentUser)); - when(client.update(currentUser)).thenReturn(Mono.just(updatedUser)); webClient.put().uri("/users/-") .bodyValue(requestUser) @@ -324,8 +298,6 @@ class UserEndpointTest { @Test void shouldUpdateOtherPasswordCorrectly() { var user = new User(); - when(userService.confirmPassword("another-fake-user", "old-password")) - .thenReturn(Mono.just(true)); when(userService.updateWithRawPassword("another-fake-user", "new-password")) .thenReturn(Mono.just(user)); webClient.put() @@ -347,14 +319,6 @@ class UserEndpointTest { @DisplayName("GrantPermission") class GrantPermissionEndpointTest { - @BeforeEach - void setUp() { - when(client.list(same(RoleBinding.class), any(), any())).thenReturn(Flux.empty()); - when(client.get(User.class, "fake-user")) - .thenReturn(Mono.error( - new ExtensionNotFoundException(fromExtension(User.class), "fake-user"))); - } - @Test void shouldGetBadRequestIfRequestBodyIsEmpty() { webClient.post().uri("/users/fake-user/permissions") @@ -395,7 +359,6 @@ class UserEndpointTest { } """, Role.class); when(roleService.listPermissions(eq(Set.of("test-A")))).thenReturn(Flux.just(roleA)); - when(roleService.listDependenciesFlux(anySet())).thenReturn(Flux.just(roleA)); when(roleService.getRolesByUsername("fake-user")).thenReturn(Flux.just("test-A")); when(roleService.list(Set.of("test-A"), true)).thenReturn(Flux.just(roleA)); @@ -416,8 +379,6 @@ class UserEndpointTest { void createWhenNameDuplicate() { when(userService.createUser(any(User.class), anySet())) .thenReturn(Mono.just(new User())); - when(userService.updateWithRawPassword(anyString(), anyString())) - .thenReturn(Mono.just(new User())); var userRequest = new UserEndpoint.CreateUserRequest("fake-user", "fake-email", "", diff --git a/application/src/test/java/run/halo/app/theme/endpoint/SinglePageQueryEndpointTest.java b/application/src/test/java/run/halo/app/theme/endpoint/SinglePageQueryEndpointTest.java index d9d804a40..4b56b1165 100644 --- a/application/src/test/java/run/halo/app/theme/endpoint/SinglePageQueryEndpointTest.java +++ b/application/src/test/java/run/halo/app/theme/endpoint/SinglePageQueryEndpointTest.java @@ -1,13 +1,10 @@ package run.halo.app.theme.endpoint; 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.verify; import static org.mockito.Mockito.when; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -19,10 +16,8 @@ import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.SinglePage; import run.halo.app.extension.GroupVersion; -import run.halo.app.extension.ListResult; import run.halo.app.extension.Metadata; import run.halo.app.theme.finders.SinglePageFinder; -import run.halo.app.theme.finders.vo.ListedSinglePageVo; import run.halo.app.theme.finders.vo.SinglePageVo; /** @@ -47,30 +42,6 @@ class SinglePageQueryEndpointTest { webTestClient = WebTestClient.bindToRouterFunction(endpoint.endpoint()).build(); } - @Test - void listSinglePages() { - ListedSinglePageVo test = ListedSinglePageVo.builder() - .metadata(metadata("test")) - .spec(new SinglePage.SinglePageSpec()) - .build(); - - ListResult pageResult = new ListResult<>(List.of(test)); - - when(singlePageFinder.list(anyInt(), anyInt(), any(), any())) - .thenReturn(Mono.just(pageResult)); - - webTestClient.get() - .uri("/singlepages?page=0&size=10") - .exchange() - .expectStatus().isOk() - .expectHeader().contentType(MediaType.APPLICATION_JSON) - .expectBody() - .jsonPath("$.total").isEqualTo(1) - .jsonPath("$.items[0].metadata.name").isEqualTo("test"); - - verify(singlePageFinder).list(eq(0), eq(10), any(), any()); - } - @Test void getByName() { SinglePageVo singlePage = SinglePageVo.builder()