From cc13f0bb6d7def9fd0cb58dc78d6e44c32444639 Mon Sep 17 00:00:00 2001 From: guqing Date: Mon, 22 Jan 2024 17:36:15 +0800 Subject: [PATCH] refactor: using indexes to query post lists --- .../app/core/extension/content/Snapshot.java | 3 + .../router/selector/FieldSelector.java | 13 +- .../router/selector/LabelSelector.java | 16 + .../router/selector/SelectorUtil.java | 2 + .../run/halo/app/content/DefaultIndexer.java | 129 ------ .../java/run/halo/app/content/Indexer.java | 79 ---- .../halo/app/content/PostIndexInformer.java | 221 ----------- .../java/run/halo/app/content/PostQuery.java | 143 ++----- .../app/content/impl/PostServiceImpl.java | 37 +- .../app/content/impl/SnapshotServiceImpl.java | 2 +- .../extension/endpoint/StatsEndpoint.java | 22 +- .../reconciler/CategoryReconciler.java | 13 +- .../extension/reconciler/PostReconciler.java | 21 +- .../reconciler/SinglePageReconciler.java | 18 +- .../extension/reconciler/TagReconciler.java | 39 +- .../run/halo/app/infra/SchemeInitializer.java | 82 +++- .../theme/endpoint/CategoryQueryEndpoint.java | 16 +- .../app/theme/endpoint/PostQueryEndpoint.java | 3 +- .../app/theme/endpoint/TagQueryEndpoint.java | 16 +- .../theme/finders/PostPublicQueryService.java | 18 +- .../theme/finders/impl/PostFinderImpl.java | 253 +++++------- .../impl/PostPublicQueryServiceImpl.java | 38 +- .../finders/impl/SiteStatsFinderImpl.java | 21 +- .../DefaultQueryPostPredicateResolver.java | 29 ++ .../ExtensionPermalinkPatternUpdater.java | 4 +- .../ReactiveQueryPostPredicateResolver.java | 3 + .../router/factories/PostRouteFactory.java | 15 +- .../halo/app/content/DefaultIndexerTest.java | 368 ------------------ .../run/halo/app/content/PostQueryTest.java | 72 +--- .../reconciler/CategoryReconcilerTest.java | 13 +- .../reconciler/PostReconcilerTest.java | 8 +- .../reconciler/SinglePageReconcilerTest.java | 6 +- .../reconciler/TagReconcilerTest.java | 21 +- .../endpoint/CategoryQueryEndpointTest.java | 3 +- .../theme/endpoint/PostQueryEndpointTest.java | 6 +- .../finders/impl/PostFinderImplTest.java | 62 +-- 36 files changed, 526 insertions(+), 1289 deletions(-) delete mode 100644 application/src/main/java/run/halo/app/content/DefaultIndexer.java delete mode 100644 application/src/main/java/run/halo/app/content/Indexer.java delete mode 100644 application/src/main/java/run/halo/app/content/PostIndexInformer.java delete mode 100644 application/src/test/java/run/halo/app/content/DefaultIndexerTest.java diff --git a/api/src/main/java/run/halo/app/core/extension/content/Snapshot.java b/api/src/main/java/run/halo/app/core/extension/content/Snapshot.java index 0694a23b82f..3e58127913e 100644 --- a/api/src/main/java/run/halo/app/core/extension/content/Snapshot.java +++ b/api/src/main/java/run/halo/app/core/extension/content/Snapshot.java @@ -84,4 +84,7 @@ public static boolean isBaseSnapshot(@NonNull Snapshot snapshot) { return Boolean.parseBoolean(annotations.get(Snapshot.KEEP_RAW_ANNO)); } + public static String toSubjectRefKey(Ref subjectRef) { + return subjectRef.getGroup() + "/" + subjectRef.getKind() + "/" + subjectRef.getName(); + } } diff --git a/api/src/main/java/run/halo/app/extension/router/selector/FieldSelector.java b/api/src/main/java/run/halo/app/extension/router/selector/FieldSelector.java index a0a90252a94..44d261f97c7 100644 --- a/api/src/main/java/run/halo/app/extension/router/selector/FieldSelector.java +++ b/api/src/main/java/run/halo/app/extension/router/selector/FieldSelector.java @@ -1,10 +1,12 @@ package run.halo.app.extension.router.selector; import java.util.Objects; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; import run.halo.app.extension.index.query.Query; import run.halo.app.extension.index.query.QueryFactory; -public record FieldSelector(Query query) { +public record FieldSelector(@NonNull Query query) { public FieldSelector(Query query) { this.query = Objects.requireNonNullElseGet(query, QueryFactory::all); } @@ -12,4 +14,13 @@ public FieldSelector(Query query) { public static FieldSelector of(Query query) { return new FieldSelector(query); } + + public static FieldSelector all() { + return new FieldSelector(QueryFactory.all()); + } + + public FieldSelector andQuery(Query other) { + Assert.notNull(other, "Query must not be null"); + return of(QueryFactory.and(query(), other)); + } } diff --git a/api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java b/api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java index 82be7dfab9d..e2cead3b471 100644 --- a/api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java +++ b/api/src/main/java/run/halo/app/extension/router/selector/LabelSelector.java @@ -24,6 +24,22 @@ public boolean test(@NonNull Map labels) { .allMatch(matcher -> matcher.test(labels.get(matcher.getKey()))); } + /** + * Returns a new label selector that is the result of ANDing the current selector with the + * given selector. + * + * @param other the selector to AND with + * @return a new label selector + */ + public LabelSelector and(LabelSelector other) { + var labelSelector = new LabelSelector(); + var matchers = new ArrayList(); + matchers.addAll(this.matchers); + matchers.addAll(other.matchers); + labelSelector.setMatchers(matchers); + return labelSelector; + } + public static LabelSelectorBuilder builder() { return new LabelSelectorBuilder(); } diff --git a/api/src/main/java/run/halo/app/extension/router/selector/SelectorUtil.java b/api/src/main/java/run/halo/app/extension/router/selector/SelectorUtil.java index bad729cbc24..d5066706b96 100644 --- a/api/src/main/java/run/halo/app/extension/router/selector/SelectorUtil.java +++ b/api/src/main/java/run/halo/app/extension/router/selector/SelectorUtil.java @@ -96,6 +96,8 @@ public static ListOptions labelAndFieldSelectorToListOptions( listOptions.setLabelSelector(new LabelSelector().setMatchers(labelMatchers)); if (!fieldQuery.isEmpty()) { listOptions.setFieldSelector(FieldSelector.of(QueryFactory.and(fieldQuery))); + } else { + listOptions.setFieldSelector(FieldSelector.all()); } return listOptions; } diff --git a/application/src/main/java/run/halo/app/content/DefaultIndexer.java b/application/src/main/java/run/halo/app/content/DefaultIndexer.java deleted file mode 100644 index de47bec441e..00000000000 --- a/application/src/main/java/run/halo/app/content/DefaultIndexer.java +++ /dev/null @@ -1,129 +0,0 @@ -package run.halo.app.content; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.SetMultimap; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import org.springframework.lang.NonNull; -import org.springframework.util.Assert; -import run.halo.app.extension.Extension; - -/** - *

A default implementation of {@link Indexer}.

- *

Note that this Indexer is not thread-safe, If multiple threads access this indexer - * concurrently and one of the threads modifies the indexer, it must be synchronized externally.

- * - * @param the type of object to be indexed - * @author guqing - * @see - * kubernetes index - * @see informer机制之cache.indexer机制 - * @since 2.0.0 - */ -public class DefaultIndexer implements Indexer { - private final Map> indices = new HashMap<>(); - private final Map> indexFuncMap = new HashMap<>(); - private final Map> indexValues = new HashMap<>(); - - @Override - public void addIndexFunc(String indexName, IndexFunc indexFunc) { - indexFuncMap.put(indexName, indexFunc); - indices.put(indexName, HashMultimap.create()); - indexValues.put(indexName, HashMultimap.create()); - } - - @Override - public Set indexNames() { - return Set.copyOf(indexFuncMap.keySet()); - } - - @Override - public void add(String indexName, T obj) { - IndexFunc indexFunc = getIndexFunc(indexName); - Set indexKeys = indexFunc.apply(obj); - for (String indexKey : indexKeys) { - SetMultimap index = indices.get(indexName); - index.put(indexKey, getObjectKey(obj)); - - SetMultimap indexValue = indexValues.get(indexName); - indexValue.put(getObjectKey(obj), indexKey); - } - } - - @NonNull - private IndexFunc getIndexFunc(String indexName) { - IndexFunc indexFunc = indexFuncMap.get(indexName); - if (indexFunc == null) { - throw new IllegalArgumentException( - "Index function not found for index name: " + indexName); - } - return indexFunc; - } - - @Override - public void update(String indexName, T obj) { - IndexFunc indexFunc = getIndexFunc(indexName); - Set indexKeys = indexFunc.apply(obj); - Set oldIndexKeys = new HashSet<>(); - SetMultimap indexValue = indexValues.get(indexName); - if (indexValue.containsKey(getObjectKey(obj))) { - oldIndexKeys.addAll(indexValue.get(getObjectKey(obj))); - } - // delete old index first - for (String oldIndexKey : oldIndexKeys) { - SetMultimap index = indices.get(indexName); - index.remove(oldIndexKey, getObjectKey(obj)); - indexValue.remove(getObjectKey(obj), oldIndexKey); - } - // add new index - for (String indexKey : indexKeys) { - SetMultimap index = indices.get(indexName); - index.put(indexKey, getObjectKey(obj)); - - indexValue.put(getObjectKey(obj), indexKey); - } - } - - @Override - public Set getByIndex(String indexName, String indexKey) { - SetMultimap index = indices.get(indexName); - if (index != null) { - return Set.copyOf(index.get(indexKey)); - } - return Set.of(); - } - - @Override - public void delete(String indexName, T obj) { - IndexFunc indexFunc = getIndexFunc(indexName); - SetMultimap indexValue = indexValues.get(indexName); - Set indexKeys = indexFunc.apply(obj); - for (String indexKey : indexKeys) { - String objectKey = getObjectKey(obj); - SetMultimap index = indices.get(indexName); - index.remove(indexKey, objectKey); - - indexValue.remove(indexKey, objectKey); - } - } - - /** - * This method is only used for testing. - * - * @param indexName index name - * @return all indices of the given index name - */ - public Map> getIndices(String indexName) { - return indices.get(indexName).asMap(); - } - - private String getObjectKey(T obj) { - Assert.notNull(obj, "Object must not be null"); - Assert.notNull(obj.getMetadata(), "Object metadata must not be null"); - Assert.notNull(obj.getMetadata().getName(), "Object name must not be null"); - return obj.getMetadata().getName(); - } -} diff --git a/application/src/main/java/run/halo/app/content/Indexer.java b/application/src/main/java/run/halo/app/content/Indexer.java deleted file mode 100644 index 6dde49ce2de..00000000000 --- a/application/src/main/java/run/halo/app/content/Indexer.java +++ /dev/null @@ -1,79 +0,0 @@ -package run.halo.app.content; - -import java.util.Set; -import run.halo.app.extension.Extension; - -/** - *

Indexer is used to index objects by index name and index key.

- *

For example, if you want to index posts by category, you can use the following code:

- *
- *     Indexer<Post> indexer = new Indexer<>();
- *     indexer.addIndexFunc("category", post -> {
- *       List<String> tags = post.getSpec().getTags();
- *       return tags == null ? Set.of() : Set.copyOf(tags);
- *     });
- *     indexer.add("category", post);
- *     indexer.getByIndex("category", "category-slug");
- *     indexer.update("category", post);
- *     indexer.delete("category", post);
- * 
- * - * @param the type of object to be indexed - * @author guqing - * @since 2.0.0 - */ -public interface Indexer { - - /** - * Adds an index function for a given index name. - * - * @param indexName The name of the index. - * @param indexFunc The function to use for indexing. - */ - void addIndexFunc(String indexName, DefaultIndexer.IndexFunc indexFunc); - - Set indexNames(); - - /** - * The {@code add} method adds an object of type T to the index - * with the given name. It does this by first getting the index function for the given index - * name and applying it to the object to get a set of index keys. For each index key, it adds - * the object key to the index and the index key to the object's index values. - * - *

For example, if you want to index Person objects by name and age, you can use the - * following:

- *
-     * // Create an Indexer that indexes Person objects by name and age
-     * Indexer<Person> indexer = new Indexer<>();
-     * indexer.addIndexFunc("name", person -> Collections.singleton(person.getName()));
-     * indexer.addIndexFunc("age", person -> Collections.singleton(String.valueOf(person
-     *  .getAge())));
-     *
-     * // Create some Person objects
-     * Person alice = new Person("Alice", 25);
-     * Person bob = new Person("Bob", 30);
-     *
-     * // Add the Person objects to the index
-     * indexer.add("name", alice);
-     * indexer.add("name", bob);
-     * indexer.add("age", alice);
-     * indexer.add("age", bob);
-     *  
- * - * @param indexName The name of the index. - * @param obj The function to use for indexing. - * @throws IllegalArgumentException if the index name is not found. - */ - void add(String indexName, T obj); - - void update(String indexName, T obj); - - Set getByIndex(String indexName, String indexKey); - - void delete(String indexName, T obj); - - @FunctionalInterface - interface IndexFunc { - Set apply(T obj); - } -} diff --git a/application/src/main/java/run/halo/app/content/PostIndexInformer.java b/application/src/main/java/run/halo/app/content/PostIndexInformer.java deleted file mode 100644 index a79075580d1..00000000000 --- a/application/src/main/java/run/halo/app/content/PostIndexInformer.java +++ /dev/null @@ -1,221 +0,0 @@ -package run.halo.app.content; - -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.locks.StampedLock; -import java.util.function.BiConsumer; -import org.springframework.beans.factory.DisposableBean; -import org.springframework.boot.context.event.ApplicationStartedEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Component; -import run.halo.app.core.extension.content.Post; -import run.halo.app.extension.DefaultExtensionMatcher; -import run.halo.app.extension.Extension; -import run.halo.app.extension.ExtensionClient; -import run.halo.app.extension.GroupVersionKind; -import run.halo.app.extension.Metadata; -import run.halo.app.extension.MetadataUtil; -import run.halo.app.extension.Unstructured; -import run.halo.app.extension.Watcher; -import run.halo.app.extension.controller.RequestSynchronizer; - -/** - *

Monitor changes to {@link Post} resources and establish a local, in-memory cache in an - * Indexer. - * When changes to posts are detected, the Indexer is updated using the indexFunc to maintain - * its integrity. - * This enables quick retrieval of the unique identifier(It is usually {@link Metadata#getName()}) - * for article objects using the getByIndex method when needed.

- * - * @author guqing - * @since 2.0.0 - */ -@Component -public class PostIndexInformer implements ApplicationListener, - DisposableBean { - public static final String TAG_POST_INDEXER = "tag-post-indexer"; - public static final String LABEL_INDEXER_NAME = "post-label-indexer"; - - private final RequestSynchronizer synchronizer; - - private final Indexer postIndexer; - - private final PostWatcher postWatcher; - - public PostIndexInformer(ExtensionClient client) { - postIndexer = new DefaultIndexer<>(); - postIndexer.addIndexFunc(TAG_POST_INDEXER, post -> { - List tags = post.getSpec().getTags(); - return tags != null ? Set.copyOf(tags) : Set.of(); - }); - postIndexer.addIndexFunc(LABEL_INDEXER_NAME, labelIndexFunc()); - - this.postWatcher = new PostWatcher(); - var emptyPost = new Post(); - this.synchronizer = new RequestSynchronizer(true, - client, - emptyPost, - postWatcher, - DefaultExtensionMatcher.builder(client, emptyPost.groupVersionKind()).build() - ); - } - - private DefaultIndexer.IndexFunc labelIndexFunc() { - return post -> { - Map labels = MetadataUtil.nullSafeLabels(post); - Set indexKeys = new HashSet<>(); - for (Map.Entry entry : labels.entrySet()) { - indexKeys.add(labelKey(entry.getKey(), entry.getValue())); - } - return indexKeys; - }; - } - - public Set getByTagName(String tagName) { - return postIndexer.getByIndex(TAG_POST_INDEXER, tagName); - } - - public Set getByLabels(Map labels) { - if (labels == null) { - return Set.of(); - } - Set result = new HashSet<>(); - for (Map.Entry entry : labels.entrySet()) { - Set values = postIndexer.getByIndex(LABEL_INDEXER_NAME, - labelKey(entry.getKey(), entry.getValue())); - if (values == null) { - // No objects have this label, no need to continue searching - return Set.of(); - } - if (result.isEmpty()) { - result.addAll(values); - } else { - result.retainAll(values); - } - } - return result; - } - - String labelKey(String labelName, String labelValue) { - return labelName + "=" + labelValue; - } - - @Override - public void destroy() throws Exception { - if (postWatcher != null) { - postWatcher.dispose(); - } - if (synchronizer != null) { - synchronizer.dispose(); - } - } - - @Override - public void onApplicationEvent(@NonNull ApplicationStartedEvent event) { - if (!synchronizer.isStarted()) { - synchronizer.start(); - } - } - - class PostWatcher implements Watcher { - private Runnable disposeHook; - private boolean disposed = false; - private final StampedLock lock = new StampedLock(); - - @Override - public void onAdd(Extension extension) { - if (!checkExtension(extension)) { - return; - } - handleIndicates(extension, postIndexer::add); - } - - @Override - public void onUpdate(Extension oldExt, Extension newExt) { - if (!checkExtension(newExt)) { - return; - } - handleIndicates(newExt, postIndexer::update); - } - - @Override - public void onDelete(Extension extension) { - if (!checkExtension(extension)) { - return; - } - handleIndicates(extension, postIndexer::delete); - } - - @Override - public void registerDisposeHook(Runnable dispose) { - this.disposeHook = dispose; - } - - @Override - public void dispose() { - if (isDisposed()) { - return; - } - this.disposed = true; - if (this.disposeHook != null) { - this.disposeHook.run(); - } - } - - @Override - public boolean isDisposed() { - return this.disposed; - } - - void handleIndicates(Extension extension, BiConsumer consumer) { - Post post = convertTo(extension); - Set indexNames = getIndexNames(); - for (String indexName : indexNames) { - maintainIndicates(indexName, post, consumer); - } - } - - Set getIndexNames() { - long stamp = lock.tryOptimisticRead(); - Set indexNames = postIndexer.indexNames(); - if (!lock.validate(stamp)) { - stamp = lock.readLock(); - try { - return postIndexer.indexNames(); - } finally { - lock.unlockRead(stamp); - } - } - return indexNames; - } - - void maintainIndicates(String indexName, Post post, BiConsumer consumer) { - long stamp = lock.writeLock(); - try { - consumer.accept(indexName, post); - } finally { - lock.unlockWrite(stamp); - } - } - } - - private Post convertTo(Extension extension) { - if (extension instanceof Post) { - return (Post) extension; - } - return Unstructured.OBJECT_MAPPER.convertValue(extension, Post.class); - } - - private boolean checkExtension(Extension extension) { - return !postWatcher.isDisposed() - && extension.getMetadata().getDeletionTimestamp() == null - && isPost(extension); - } - - private boolean isPost(Extension extension) { - return GroupVersionKind.fromExtension(Post.class).equals(extension.groupVersionKind()); - } -} 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 c5992d2aa28..12fa2a1c698 100644 --- a/application/src/main/java/run/halo/app/content/PostQuery.java +++ b/application/src/main/java/run/halo/app/content/PostQuery.java @@ -1,28 +1,25 @@ package run.halo.app.content; -import static java.util.Comparator.comparing; -import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate; +import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions; import com.fasterxml.jackson.annotation.JsonIgnore; 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.Objects; import java.util.Set; -import java.util.function.Predicate; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Sort; import org.springframework.lang.Nullable; -import org.springframework.util.CollectionUtils; 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.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.selector.FieldSelector; +import run.halo.app.extension.router.selector.LabelSelector; /** * A query object for {@link Post} list. @@ -105,118 +102,58 @@ private Set listToSet(List param) { } /** - * Build a comparator from the query object. + * Build a list options from the query object. * - * @return a comparator + * @return a list options */ - public Comparator toComparator() { - var sort = getSort(); - var creationTimestampOrder = sort.getOrderFor("creationTimestamp"); - List> comparators = new ArrayList<>(); - if (creationTimestampOrder != null) { - Comparator comparator = - comparing(post -> post.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(post -> post.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 from the query object. - * - * @return a predicate - */ - public Predicate toPredicate() { - Predicate predicate = labelAndFieldSelectorToPredicate(getLabelSelector(), - getFieldSelector()); - - if (!CollectionUtils.isEmpty(getCategories())) { - predicate = - predicate.and(post -> contains(getCategories(), post.getSpec().getCategories())); - } - if (!CollectionUtils.isEmpty(getTags())) { - predicate = predicate.and(post -> contains(getTags(), post.getSpec().getTags())); - } - if (!CollectionUtils.isEmpty(getContributors())) { - Predicate hasStatus = post -> post.getStatus() != null; - var containsContributors = hasStatus.and( - post -> contains(getContributors(), post.getStatus().getContributors()) - ); - predicate = predicate.and(containsContributors); + public ListOptions toListOptions() { + var listOptions = + labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector()); + if (listOptions.getFieldSelector() == null) { + listOptions.setFieldSelector(FieldSelector.all()); } + var labelSelectorBuilder = LabelSelector.builder(); + var fieldQuery = QueryFactory.all(); String keyword = getKeyword(); if (keyword != null) { - predicate = predicate.and(post -> { - String excerpt = post.getStatusOrDefault().getExcerpt(); - return StringUtils.containsIgnoreCase(excerpt, keyword) - || StringUtils.containsIgnoreCase(post.getSpec().getSlug(), keyword) - || StringUtils.containsIgnoreCase(post.getSpec().getTitle(), keyword); - }); + fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.or( + QueryFactory.contains("status.excerpt", keyword), + QueryFactory.contains("spec.slug", keyword), + QueryFactory.contains("spec.title", keyword) + )); } Post.PostPhase publishPhase = getPublishPhase(); if (publishPhase != null) { - predicate = predicate.and(post -> { - if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) { - return !post.isPublished() - && Post.PostPhase.PENDING_APPROVAL.name() - .equalsIgnoreCase(post.getStatusOrDefault().getPhase()); - } - // published - if (Post.PostPhase.PUBLISHED.equals(publishPhase)) { - return post.isPublished(); - } - // draft - return !post.isPublished(); - }); + 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); + } } Post.VisibleEnum visible = getVisible(); if (visible != null) { - predicate = - predicate.and(post -> visible.equals(post.getSpec().getVisible())); + fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal( + "spec.visible", visible.name()) + ); } if (StringUtils.isNotBlank(username)) { - Predicate isOwner = post -> Objects.equals(username, post.getSpec().getOwner()); - predicate = predicate.and(isOwner); + fieldQuery = QueryFactory.and(fieldQuery, QueryFactory.equal( + "spec.owner", username) + ); } - return predicate; - } - 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); + listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(fieldQuery)); + listOptions.setLabelSelector( + listOptions.getLabelSelector().and(labelSelectorBuilder.build())); + return listOptions; } } 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 3892d765cb3..86ff0322867 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 @@ -1,5 +1,7 @@ package run.halo.app.content.impl; +import static run.halo.app.extension.index.query.QueryFactory.in; + import java.time.Duration; import java.time.Instant; import java.util.List; @@ -8,6 +10,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.data.domain.Sort; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.util.Assert; @@ -27,9 +30,12 @@ import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Tag; 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.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.Ref; +import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.infra.Condition; import run.halo.app.infra.ConditionStatus; import run.halo.app.metrics.CounterService; @@ -58,16 +64,17 @@ public PostServiceImpl(ReactiveExtensionClient client, CounterService counterSer @Override public Mono> listPost(PostQuery query) { - return client.list(Post.class, query.toPredicate(), - query.toComparator(), query.getPage(), query.getSize()) - .flatMap(listResult -> Flux.fromStream( - listResult.get().map(this::getListedPost) - ) - .concatMap(Function.identity()) - .collectList() - .map(listedPosts -> new ListResult<>(listResult.getPage(), listResult.getSize(), - listResult.getTotal(), listedPosts) - ) + return client.listBy(Post.class, query.toListOptions(), + PageRequestImpl.of(query.getPage(), query.getSize(), query.getSort()) + ) + .flatMap(listResult -> Flux.fromStream(listResult.get()) + .map(this::getListedPost) + .concatMap(Function.identity()) + .collectList() + .map(listedPosts -> new ListResult<>(listResult.getPage(), listResult.getSize(), + listResult.getTotal(), listedPosts) + ) + .defaultIfEmpty(ListResult.emptyResult()) ); } @@ -144,16 +151,18 @@ private Flux listTags(List tagNames) { if (tagNames == null) { return Flux.empty(); } - return Flux.fromIterable(tagNames) - .flatMapSequential(tagName -> client.fetch(Tag.class, tagName)); + var listOptions = new ListOptions(); + listOptions.setFieldSelector(FieldSelector.of(in("metadata.name", tagNames))); + return client.listAll(Tag.class, listOptions, Sort.by("metadata.creationTimestamp")); } private Flux listCategories(List categoryNames) { if (categoryNames == null) { return Flux.empty(); } - return Flux.fromIterable(categoryNames) - .flatMapSequential(categoryName -> client.fetch(Category.class, categoryName)); + var listOptions = new ListOptions(); + listOptions.setFieldSelector(FieldSelector.of(in("metadata.name", categoryNames))); + return client.listAll(Category.class, listOptions, Sort.by("metadata.creationTimestamp")); } private Flux listContributors(List usernames) { diff --git a/application/src/main/java/run/halo/app/content/impl/SnapshotServiceImpl.java b/application/src/main/java/run/halo/app/content/impl/SnapshotServiceImpl.java index 00e758d6e74..d9ff3b3e0c2 100644 --- a/application/src/main/java/run/halo/app/content/impl/SnapshotServiceImpl.java +++ b/application/src/main/java/run/halo/app/content/impl/SnapshotServiceImpl.java @@ -19,7 +19,7 @@ public class SnapshotServiceImpl implements SnapshotService { private final ReactiveExtensionClient client; - private Clock clock; + private final Clock clock; public SnapshotServiceImpl(ReactiveExtensionClient client) { this.client = client; diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/StatsEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/StatsEndpoint.java index 8ea04b4a769..c00e53062fe 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/StatsEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/StatsEndpoint.java @@ -2,6 +2,8 @@ import static java.lang.Boolean.parseBoolean; import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder; +import static run.halo.app.extension.index.query.QueryFactory.and; +import static run.halo.app.extension.index.query.QueryFactory.equal; import lombok.Data; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; @@ -13,8 +15,11 @@ import run.halo.app.core.extension.Counter; import run.halo.app.core.extension.User; import run.halo.app.core.extension.content.Post; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.MetadataUtil; +import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.extension.router.selector.FieldSelector; /** * Stats endpoint. @@ -67,13 +72,16 @@ Mono getStats(ServerRequest request) { stats.setUsers(count.intValue()); return stats; })) - .flatMap(stats -> client.list(Post.class, post -> !post.isDeleted(), null) - .count() - .map(count -> { - stats.setPosts(count.intValue()); - return stats; - }) - ) + .flatMap(stats -> { + var listOptions = new ListOptions(); + listOptions.setFieldSelector(FieldSelector.of( + and(equal("metadata.deletionTimestamp", null), + equal("spec.deleted", "false"))) + ); + return client.listBy(Post.class, listOptions, PageRequestImpl.ofSize(1)) + .doOnNext(list -> stats.setPosts((int) list.getTotal())) + .thenReturn(stats); + }) .flatMap(stats -> ServerResponse.ok().bodyValue(stats)); } diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java index 85b7a929ba6..48935c569c4 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java @@ -1,5 +1,8 @@ package run.halo.app.core.extension.reconciler; +import static run.halo.app.extension.index.query.QueryFactory.and; +import static run.halo.app.extension.index.query.QueryFactory.equal; + import java.time.Duration; import java.util.ArrayDeque; import java.util.ArrayList; @@ -13,6 +16,7 @@ import java.util.stream.Collectors; import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Sort; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import run.halo.app.content.permalinks.CategoryPermalinkPolicy; @@ -20,10 +24,12 @@ import run.halo.app.core.extension.content.Constant; import run.halo.app.core.extension.content.Post; import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.Reconciler; +import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.infra.utils.JsonUtils; /** @@ -138,7 +144,12 @@ private void populatePosts(Category category) { .map(item -> item.getMetadata().getName()) .toList(); - List posts = client.list(Post.class, post -> !post.isDeleted(), null); + var postListOptions = new ListOptions(); + postListOptions.setFieldSelector(FieldSelector.of( + and(equal("metadata.deletionTimestamp", null), + equal("spec.deleted", "false"))) + ); + var posts = client.listAll(Post.class, postListOptions, Sort.unsorted()); // populate post to status List compactPosts = posts.stream() diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java index fafdd67a2ce..8c9a9509515 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java @@ -9,6 +9,7 @@ import java.time.Instant; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -18,6 +19,7 @@ import org.jsoup.Jsoup; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import run.halo.app.content.ContentWrapper; import run.halo.app.content.NotificationReasonConst; @@ -36,10 +38,13 @@ import run.halo.app.event.post.PostVisibleChangedEvent; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionOperator; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.Ref; import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.Reconciler; +import run.halo.app.extension.index.query.QueryFactory; +import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.infra.Condition; import run.halo.app.infra.ConditionStatus; import run.halo.app.infra.utils.HaloUtils; @@ -189,8 +194,7 @@ public Result reconcile(Request request) { var ref = Ref.of(post); // handle contributors var headSnapshot = post.getSpec().getHeadSnapshot(); - var contributors = client.list(Snapshot.class, - snapshot -> ref.equals(snapshot.getSpec().getSubjectRef()), null) + var contributors = listSnapshots(ref) .stream() .map(snapshot -> { Set usernames = snapshot.getSpec().getContributors(); @@ -292,7 +296,7 @@ void unPublishPost(Post post, Set events) { } var labels = post.getMetadata().getLabels(); labels.put(Post.PUBLISHED_LABEL, Boolean.FALSE.toString()); - var status = post.getStatus(); + final var status = post.getStatus(); var condition = new Condition(); condition.setType("CancelledPublish"); @@ -310,9 +314,7 @@ void unPublishPost(Post post, Set events) { private void cleanUpResources(Post post) { // clean up snapshots final Ref ref = Ref.of(post); - client.list(Snapshot.class, - snapshot -> ref.equals(snapshot.getSpec().getSubjectRef()), null) - .forEach(client::delete); + listSnapshots(ref).forEach(client::delete); // clean up comments client.list(Comment.class, comment -> ref.equals(comment.getSpec().getSubjectRef()), @@ -330,4 +332,11 @@ private String getExcerpt(String htmlContent) { // TODO The default capture 150 words as excerpt return StringUtils.substring(text, 0, 150); } + + List listSnapshots(Ref ref) { + var snapshotListOptions = new ListOptions(); + snapshotListOptions.setFieldSelector(FieldSelector.of( + QueryFactory.equal("spec.subjectRef", Snapshot.toSubjectRefKey(ref)))); + return client.listAll(Snapshot.class, snapshotListOptions, Sort.unsorted()); + } } diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java index ad46213b5eb..06eb01e52c7 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/SinglePageReconciler.java @@ -14,6 +14,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.jsoup.Jsoup; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import run.halo.app.content.NotificationReasonConst; @@ -26,11 +27,14 @@ import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.ExtensionOperator; import run.halo.app.extension.ExtensionUtil; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.Ref; import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.Reconciler; +import run.halo.app.extension.index.query.QueryFactory; +import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.infra.Condition; import run.halo.app.infra.ConditionList; import run.halo.app.infra.ConditionStatus; @@ -243,9 +247,7 @@ private void publishFailed(String name, Throwable error) { private void cleanUpResources(SinglePage singlePage) { // clean up snapshot Ref ref = Ref.of(singlePage); - client.list(Snapshot.class, - snapshot -> ref.equals(snapshot.getSpec().getSubjectRef()), null) - .forEach(client::delete); + listSnapshots(ref).forEach(client::delete); // clean up comments client.list(Comment.class, comment -> comment.getSpec().getSubjectRef().equals(ref), @@ -332,8 +334,7 @@ private void reconcileStatus(String name) { // handle contributors String headSnapshot = singlePage.getSpec().getHeadSnapshot(); - List contributors = client.list(Snapshot.class, - snapshot -> Ref.of(singlePage).equals(snapshot.getSpec().getSubjectRef()), null) + List contributors = listSnapshots(Ref.of(singlePage)) .stream() .peek(snapshot -> { snapshot.getSpec().setContentPatch(StringUtils.EMPTY); @@ -377,4 +378,11 @@ private boolean isDeleted(SinglePage singlePage) { return Objects.equals(true, singlePage.getSpec().getDeleted()) || singlePage.getMetadata().getDeletionTimestamp() != null; } + + List listSnapshots(Ref ref) { + var snapshotListOptions = new ListOptions(); + snapshotListOptions.setFieldSelector(FieldSelector.of( + QueryFactory.equal("spec.subjectRef", Snapshot.toSubjectRefKey(ref)))); + return client.listAll(Snapshot.class, snapshotListOptions, Sort.unsorted()); + } } diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/TagReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/TagReconciler.java index 887ca0ae083..3015e6229a3 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/TagReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/TagReconciler.java @@ -1,5 +1,9 @@ package run.halo.app.core.extension.reconciler; +import static org.apache.commons.lang3.BooleanUtils.isFalse; +import static run.halo.app.extension.MetadataUtil.nullSafeLabels; +import static run.halo.app.extension.index.query.QueryFactory.equal; + import java.time.Duration; import java.util.HashSet; import java.util.Map; @@ -7,17 +11,19 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; -import run.halo.app.content.PostIndexInformer; import run.halo.app.content.permalinks.TagPermalinkPolicy; import run.halo.app.core.extension.content.Constant; import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Tag; import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.controller.Controller; import run.halo.app.extension.controller.ControllerBuilder; import run.halo.app.extension.controller.Reconciler; +import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.infra.utils.JsonUtils; /** @@ -32,7 +38,6 @@ public class TagReconciler implements Reconciler { private static final String FINALIZER_NAME = "tag-protection"; private final ExtensionClient client; private final TagPermalinkPolicy tagPermalinkPolicy; - private final PostIndexInformer postIndexInformer; @Override public Result reconcile(Request request) { @@ -128,20 +133,22 @@ private void reconcileStatusPosts(String tagName) { } private void populatePosts(Tag tag) { - // populate post count - Set postNames = postIndexInformer.getByTagName(tag.getMetadata().getName()); - tag.getStatusOrDefault().setPostCount(postNames.size()); - - // populate visible post count - Map labelToQuery = Map.of(Post.PUBLISHED_LABEL, BooleanUtils.TRUE, - Post.VISIBLE_LABEL, Post.VisibleEnum.PUBLIC.name(), - Post.DELETED_LABEL, BooleanUtils.FALSE); - Set hasAllLabelPosts = postIndexInformer.getByLabels(labelToQuery); - - // retain all posts that has all labels - Set postNamesWithTag = new HashSet<>(postNames); - postNamesWithTag.retainAll(hasAllLabelPosts); - tag.getStatusOrDefault().setVisiblePostCount(postNamesWithTag.size()); + // populate post-count + var listOptions = new ListOptions(); + listOptions.setFieldSelector(FieldSelector.of( + equal("spec.tags", tag.getMetadata().getName())) + ); + var posts = client.listAll(Post.class, listOptions, Sort.unsorted()); + tag.getStatusOrDefault().setPostCount(posts.size()); + + var publicPosts = posts.stream() + .filter(post -> post.getMetadata().getDeletionTimestamp() == null + && isFalse(post.getSpec().getDeleted()) + && BooleanUtils.TRUE.equals(nullSafeLabels(post).get(Post.PUBLISHED_LABEL)) + && Post.VisibleEnum.PUBLIC.equals(post.getSpec().getVisible()) + ) + .toList(); + tag.getStatusOrDefault().setVisiblePostCount(publicPosts.size()); } private boolean isDeleted(Tag tag) { 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 dfaabce05d4..f87d48637ee 100644 --- a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java +++ b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java @@ -1,5 +1,9 @@ package run.halo.app.infra; +import static run.halo.app.extension.index.IndexAttributeFactory.multiValueAttribute; +import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute; + +import java.util.Set; import org.springframework.boot.context.event.ApplicationContextInitializedEvent; import org.springframework.context.ApplicationListener; import org.springframework.lang.NonNull; @@ -38,6 +42,7 @@ import run.halo.app.extension.DefaultSchemeManager; import run.halo.app.extension.DefaultSchemeWatcherManager; import run.halo.app.extension.Secret; +import run.halo.app.extension.index.IndexSpec; import run.halo.app.extension.index.IndexSpecRegistryImpl; import run.halo.app.migration.Backup; import run.halo.app.plugin.extensionpoint.ExtensionDefinition; @@ -70,10 +75,83 @@ public void onApplicationEvent(@NonNull ApplicationContextInitializedEvent event schemeManager.register(Theme.class); schemeManager.register(Menu.class); schemeManager.register(MenuItem.class); - schemeManager.register(Post.class); + schemeManager.register(Post.class, indexSpecs -> { + indexSpecs.add(new IndexSpec() + .setName("spec.title") + .setIndexFunc(simpleAttribute(Post.class, post -> post.getSpec().getTitle()))); + indexSpecs.add(new IndexSpec() + .setName("spec.slug") + // Compatible with old data, hoping to set it to true in the future + .setUnique(false) + .setIndexFunc(simpleAttribute(Post.class, post -> post.getSpec().getSlug()))); + indexSpecs.add(new IndexSpec() + .setName("spec.publishTime") + .setIndexFunc(simpleAttribute(Post.class, post -> { + var publishTime = post.getSpec().getPublishTime(); + return publishTime == null ? null : publishTime.toString(); + }))); + indexSpecs.add(new IndexSpec() + .setName("spec.owner") + .setIndexFunc(simpleAttribute(Post.class, post -> post.getSpec().getOwner()))); + indexSpecs.add(new IndexSpec() + .setName("spec.deleted") + .setIndexFunc(simpleAttribute(Post.class, post -> { + var deleted = post.getSpec().getDeleted(); + return deleted == null ? "false" : deleted.toString(); + }))); + indexSpecs.add(new IndexSpec() + .setName("spec.pinned") + .setIndexFunc(simpleAttribute(Post.class, post -> { + var pinned = post.getSpec().getPinned(); + return pinned == null ? "false" : pinned.toString(); + }))); + indexSpecs.add(new IndexSpec() + .setName("spec.priority") + .setIndexFunc(simpleAttribute(Post.class, post -> { + var priority = post.getSpec().getPriority(); + return priority == null ? "0" : priority.toString(); + }))); + indexSpecs.add(new IndexSpec() + .setName("spec.visible") + .setIndexFunc( + simpleAttribute(Post.class, post -> post.getSpec().getVisible().name()))); + indexSpecs.add(new IndexSpec() + .setName("spec.tags") + .setIndexFunc(multiValueAttribute(Post.class, post -> { + var tags = post.getSpec().getTags(); + return tags == null ? Set.of() : Set.copyOf(tags); + }))); + indexSpecs.add(new IndexSpec() + .setName("spec.categories") + .setIndexFunc(multiValueAttribute(Post.class, post -> { + var categories = post.getSpec().getCategories(); + return categories == null ? Set.of() : Set.copyOf(categories); + }))); + indexSpecs.add(new IndexSpec() + .setName("status.contributors") + .setIndexFunc(multiValueAttribute(Post.class, post -> { + var contributors = post.getStatusOrDefault().getContributors(); + return contributors == null ? Set.of() : Set.copyOf(contributors); + }))); + indexSpecs.add(new IndexSpec() + .setName("status.categories") + .setIndexFunc( + simpleAttribute(Post.class, post -> post.getStatusOrDefault().getExcerpt()))); + indexSpecs.add(new IndexSpec() + .setName("status.phase") + .setIndexFunc( + simpleAttribute(Post.class, post -> post.getStatusOrDefault().getPhase()))); + }); schemeManager.register(Category.class); schemeManager.register(Tag.class); - schemeManager.register(Snapshot.class); + schemeManager.register(Snapshot.class, indexSpecs -> { + indexSpecs.add(new IndexSpec() + .setName("spec.subjectRef") + .setIndexFunc(simpleAttribute(Snapshot.class, + snapshot -> Snapshot.toSubjectRefKey(snapshot.getSpec().getSubjectRef())) + ) + ); + }); schemeManager.register(Comment.class); schemeManager.register(Reply.class); schemeManager.register(SinglePage.class); diff --git a/application/src/main/java/run/halo/app/theme/endpoint/CategoryQueryEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/CategoryQueryEndpoint.java index de9831b7988..4e21b261207 100644 --- a/application/src/main/java/run/halo/app/theme/endpoint/CategoryQueryEndpoint.java +++ b/application/src/main/java/run/halo/app/theme/endpoint/CategoryQueryEndpoint.java @@ -2,11 +2,9 @@ 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.theme.endpoint.PublicApiUtils.containsElement; import static run.halo.app.theme.endpoint.PublicApiUtils.toAnotherListResult; import io.swagger.v3.oas.annotations.enums.ParameterIn; -import java.util.function.Predicate; import lombok.RequiredArgsConstructor; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; import org.springframework.http.MediaType; @@ -17,11 +15,11 @@ import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Category; -import run.halo.app.core.extension.content.Post; 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.ReactiveExtensionClient; +import run.halo.app.extension.index.query.QueryFactory; import run.halo.app.extension.router.QueryParamBuildUtil; import run.halo.app.extension.router.SortableRequest; import run.halo.app.theme.finders.PostPublicQueryService; @@ -93,13 +91,11 @@ public RouterFunction endpoint() { private Mono listPostsByCategoryName(ServerRequest request) { final var name = request.pathVariable("name"); final var query = new PostPublicQuery(request.exchange()); - Predicate categoryContainsPredicate = - post -> containsElement(post.getSpec().getCategories(), name); - return postPublicQueryService.list(query.getPage(), - query.getSize(), - categoryContainsPredicate.and(query.toPredicate()), - query.toComparator() - ) + var listOptions = query.toListOptions(); + var newFieldSelector = listOptions.getFieldSelector() + .andQuery(QueryFactory.equal("spec.categories", name)); + listOptions.setFieldSelector(newFieldSelector); + return postPublicQueryService.list(listOptions, query.toPageRequest()) .flatMap(result -> ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) .bodyValue(result) diff --git a/application/src/main/java/run/halo/app/theme/endpoint/PostQueryEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/PostQueryEndpoint.java index 632ef1c50b1..c0e922f44e0 100644 --- a/application/src/main/java/run/halo/app/theme/endpoint/PostQueryEndpoint.java +++ b/application/src/main/java/run/halo/app/theme/endpoint/PostQueryEndpoint.java @@ -107,8 +107,7 @@ private Mono getPostByName(ServerRequest request) { private Mono listPosts(ServerRequest request) { PostPublicQuery query = new PostPublicQuery(request.exchange()); - return postPublicQueryService.list(query.getPage(), query.getSize(), query.toPredicate(), - query.toComparator()) + return postPublicQueryService.list(query.toListOptions(), query.toPageRequest()) .flatMap(result -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON) .bodyValue(result) ); diff --git a/application/src/main/java/run/halo/app/theme/endpoint/TagQueryEndpoint.java b/application/src/main/java/run/halo/app/theme/endpoint/TagQueryEndpoint.java index c9f1e086e77..c3ed1baada5 100644 --- a/application/src/main/java/run/halo/app/theme/endpoint/TagQueryEndpoint.java +++ b/application/src/main/java/run/halo/app/theme/endpoint/TagQueryEndpoint.java @@ -2,10 +2,8 @@ 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.theme.endpoint.PublicApiUtils.containsElement; import io.swagger.v3.oas.annotations.enums.ParameterIn; -import java.util.function.Predicate; import lombok.RequiredArgsConstructor; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; import org.springframework.http.MediaType; @@ -15,11 +13,11 @@ 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.Post; import run.halo.app.core.extension.content.Tag; 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.index.query.QueryFactory; import run.halo.app.extension.router.QueryParamBuildUtil; import run.halo.app.extension.router.SortableRequest; import run.halo.app.theme.finders.PostPublicQueryService; @@ -102,13 +100,11 @@ private Mono getTagByName(ServerRequest request) { private Mono listPostsByTagName(ServerRequest request) { final var name = request.pathVariable("name"); final var query = new PostPublicQuery(request.exchange()); - final Predicate containsTagPredicate = - post -> containsElement(post.getSpec().getTags(), name); - return postPublicQueryService.list(query.getPage(), - query.getSize(), - containsTagPredicate.and(query.toPredicate()), - query.toComparator() - ) + var listOptions = query.toListOptions(); + var newFieldSelector = listOptions.getFieldSelector() + .andQuery(QueryFactory.equal("spec.tags", name)); + listOptions.setFieldSelector(newFieldSelector); + return postPublicQueryService.list(listOptions, query.toPageRequest()) .flatMap(result -> ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) .bodyValue(result) diff --git a/application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java b/application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java index 92c4e5f90e0..e1ef8e68049 100644 --- a/application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java +++ b/application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java @@ -1,11 +1,11 @@ package run.halo.app.theme.finders; -import java.util.Comparator; -import java.util.function.Predicate; import org.springframework.lang.NonNull; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Post; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.ListResult; +import run.halo.app.extension.PageRequest; import run.halo.app.theme.ReactivePostContentHandler; import run.halo.app.theme.finders.vo.ContentVo; import run.halo.app.theme.finders.vo.ListedPostVo; @@ -14,17 +14,13 @@ public interface PostPublicQueryService { /** - * Lists posts page by predicate and comparator. + * Lists public posts by the given list options and page request. * - * @param page page number - * @param size page size - * @param postPredicate post predicate - * @param comparator post comparator - * @return list result + * @param listOptions additional list options + * @param page page request must not be null + * @return a list of listed post vo */ - Mono> list(Integer page, Integer size, - Predicate postPredicate, - Comparator comparator); + Mono> list(ListOptions listOptions, PageRequest page); /** * Converts post to listed post vo. diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java index 09b5c312a0c..c936f8a8dcd 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java @@ -1,25 +1,26 @@ package run.halo.app.theme.finders.impl; -import java.time.Instant; -import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.Comparator; -import java.util.Deque; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.function.Function; import java.util.stream.Collectors; import lombok.AllArgsConstructor; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.springframework.util.comparator.Comparators; +import org.springframework.data.domain.Sort; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Post; +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.exception.ExtensionNotFoundException; +import run.halo.app.extension.index.query.QueryFactory; +import run.halo.app.extension.router.selector.FieldSelector; +import run.halo.app.extension.router.selector.LabelSelector; import run.halo.app.infra.utils.HaloUtils; import run.halo.app.theme.finders.Finder; import run.halo.app.theme.finders.PostFinder; @@ -64,12 +65,47 @@ public Mono content(String postName) { return postPublicQueryService.getContent(postName); } + static Sort defaultSort() { + return Sort.by(Sort.Order.desc("spec.pinned"), + Sort.Order.desc("spec.priority"), + Sort.Order.desc("spec.publishTime"), + Sort.Order.desc("metadata.name") + ); + } + + @NonNull + static LinkNavigation findPostNavigation(List postNames, String target) { + Assert.notNull(target, "Target post name must not be null"); + for (int i = 0; i < postNames.size(); i++) { + var item = postNames.get(i); + if (target.equals(item)) { + var prevLink = (i > 0) ? postNames.get(i - 1) : null; + var nextLink = (i < postNames.size() - 1) ? postNames.get(i + 1) : null; + return new LinkNavigation(prevLink, target, nextLink); + } + } + return new LinkNavigation(null, target, null); + } + + static Sort archiveSort() { + return Sort.by(Sort.Order.desc("spec.publishTime"), + Sort.Order.desc("metadata.name") + ); + } + + private Mono fetchByName(String name) { + if (StringUtils.isBlank(name)) { + return Mono.empty(); + } + return getByName(name) + .onErrorResume(ExtensionNotFoundException.class::isInstance, (error) -> Mono.empty()); + } + @Override public Mono cursor(String currentName) { - // TODO Optimize the post names query here - return postPredicateResolver.getPredicate() - .flatMapMany(postPredicate -> - client.list(Post.class, postPredicate, defaultComparator()) + return postPredicateResolver.getListOptions() + .flatMapMany(postListOption -> + client.listAll(Post.class, postListOption, defaultSort()) ) .map(post -> post.getMetadata().getName()) .collectList() @@ -79,10 +115,9 @@ public Mono cursor(String currentName) { .thenReturn(builder) ) .flatMap(builder -> { - Pair previousNextPair = - postPreviousNextPair(postNames, currentName); - String previousPostName = previousNextPair.getLeft(); - String nextPostName = previousNextPair.getRight(); + var previousNextPair = findPostNavigation(postNames, currentName); + String previousPostName = previousNextPair.prev(); + String nextPostName = previousNextPair.next(); return fetchByName(previousPostName) .doOnNext(builder::previous) .then(fetchByName(nextPostName)) @@ -93,115 +128,50 @@ public Mono cursor(String currentName) { .defaultIfEmpty(NavigationPostVo.empty()); } - private Mono fetchByName(String name) { - if (StringUtils.isBlank(name)) { - return Mono.empty(); - } - return getByName(name) - .onErrorResume(ExtensionNotFoundException.class::isInstance, (error) -> Mono.empty()); - } - @Override - public Flux listAll() { - return postPredicateResolver.getPredicate() - .flatMapMany(predicate -> client.list(Post.class, predicate, defaultComparator())) - .concatMap(postPublicQueryService::convertToListedVo); - } - - static Pair postPreviousNextPair(List postNames, - String currentName) { - FixedSizeSlidingWindow window = new FixedSizeSlidingWindow<>(3); - for (String postName : postNames) { - window.add(postName); - if (!window.isFull()) { - continue; - } - int index = window.indexOf(currentName); - if (index == -1) { - continue; - } - // got expected window - if (index < 2) { - break; - } - } - - List elements = window.elements(); - // current post index - int index = elements.indexOf(currentName); - - String previousPostName = null; - if (index > 0) { - previousPostName = elements.get(index - 1); - } - - String nextPostName = null; - if (elements.size() - 1 > index) { - nextPostName = elements.get(index + 1); - } - return Pair.of(previousPostName, nextPostName); - } - - static class FixedSizeSlidingWindow { - Deque queue; - int size; - - public FixedSizeSlidingWindow(int size) { - this.size = size; - // FIFO - queue = new ArrayDeque<>(size); - } - - /** - * Add element to the window. - * The element added first will be deleted when the element in the collection exceeds - * {@code size}. - */ - public void add(T t) { - if (queue.size() == size) { - // remove first - queue.poll(); - } - // add to last - queue.add(t); - } - - public int indexOf(T o) { - List elements = elements(); - return elements.indexOf(o); - } - - public List elements() { - return new ArrayList<>(queue); - } - - public boolean isFull() { - return queue.size() == size; - } + public Mono> list(Integer page, Integer size) { + return postPublicQueryService.list(new ListOptions(), getPageRequest(page, size)); } - @Override - public Mono> list(Integer page, Integer size) { - return postPublicQueryService.list(page, size, null, defaultComparator()); + private PageRequestImpl getPageRequest(Integer page, Integer size) { + return PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort()); } @Override public Mono> listByCategory(Integer page, Integer size, String categoryName) { - return postPublicQueryService.list(page, size, - post -> contains(post.getSpec().getCategories(), categoryName), defaultComparator()); + var fieldQuery = QueryFactory.all(); + if (StringUtils.isNotBlank(categoryName)) { + fieldQuery = + QueryFactory.and(fieldQuery, QueryFactory.equal("spec.categories", categoryName)); + } + var listOptions = new ListOptions(); + listOptions.setFieldSelector(FieldSelector.of(fieldQuery)); + return postPublicQueryService.list(listOptions, getPageRequest(page, size)); } @Override public Mono> listByTag(Integer page, Integer size, String tag) { - return postPublicQueryService.list(page, size, - post -> contains(post.getSpec().getTags(), tag), defaultComparator()); + var fieldQuery = QueryFactory.all(); + if (StringUtils.isNotBlank(tag)) { + fieldQuery = + QueryFactory.and(fieldQuery, QueryFactory.equal("spec.tags", tag)); + } + var listOptions = new ListOptions(); + listOptions.setFieldSelector(FieldSelector.of(fieldQuery)); + return postPublicQueryService.list(listOptions, getPageRequest(page, size)); } @Override public Mono> listByOwner(Integer page, Integer size, String owner) { - return postPublicQueryService.list(page, size, - post -> post.getSpec().getOwner().equals(owner), defaultComparator()); + var fieldQuery = QueryFactory.all(); + if (StringUtils.isNotBlank(owner)) { + fieldQuery = + QueryFactory.and(fieldQuery, QueryFactory.equal("spec.owner", owner)); + } + var listOptions = new ListOptions(); + listOptions.setFieldSelector(FieldSelector.of(fieldQuery)); + return postPublicQueryService.list(listOptions, getPageRequest(page, size)); } @Override @@ -217,23 +187,23 @@ public Mono> archives(Integer page, Integer size, Stri @Override public Mono> archives(Integer page, Integer size, String year, String month) { - return postPublicQueryService.list(page, size, post -> { - Map labels = post.getMetadata().getLabels(); - if (labels == null) { - return false; - } - boolean yearMatch = StringUtils.isBlank(year) - || year.equals(labels.get(Post.ARCHIVE_YEAR_LABEL)); - boolean monthMatch = StringUtils.isBlank(month) - || month.equals(labels.get(Post.ARCHIVE_MONTH_LABEL)); - return yearMatch && monthMatch; - }, archiveComparator()) + var listOptions = new ListOptions(); + var labelSelectorBuilder = LabelSelector.builder(); + if (StringUtils.isNotBlank(year)) { + labelSelectorBuilder.eq(Post.ARCHIVE_YEAR_LABEL, year); + } + if (StringUtils.isNotBlank(month)) { + labelSelectorBuilder.eq(Post.ARCHIVE_MONTH_LABEL, month); + } + listOptions.setLabelSelector(labelSelectorBuilder.build()); + var pageRequest = PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), archiveSort()); + return postPublicQueryService.list(listOptions, pageRequest) .map(list -> { Map> yearPosts = list.get() .collect(Collectors.groupingBy( post -> HaloUtils.getYearText(post.getSpec().getPublishTime()))); - List postArchives = - yearPosts.entrySet().stream().map(entry -> { + List postArchives = yearPosts.entrySet().stream() + .map(entry -> { String key = entry.getKey(); // archives by month Map> monthPosts = entry.getValue().stream() @@ -260,37 +230,24 @@ public Mono> archives(Integer page, Integer size, Stri return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), postArchives); }) - .defaultIfEmpty(new ListResult<>(page, size, 0, List.of())); + .defaultIfEmpty(ListResult.emptyResult()); } - private boolean contains(List c, String key) { - if (StringUtils.isBlank(key) || c == null) { - return false; - } - return c.contains(key); + @Override + public Flux listAll() { + return postPredicateResolver.getListOptions() + .flatMapMany(listOptions -> client.listAll(Post.class, listOptions, defaultSort())) + .concatMap(postPublicQueryService::convertToListedVo); + } + + int pageNullSafe(Integer page) { + return ObjectUtils.defaultIfNull(page, 1); } - static Comparator defaultComparator() { - Function pinned = - post -> Objects.requireNonNullElse(post.getSpec().getPinned(), false); - Function priority = - post -> Objects.requireNonNullElse(post.getSpec().getPriority(), 0); - Function publishTime = - post -> post.getSpec().getPublishTime(); - Function name = post -> post.getMetadata().getName(); - return Comparator.comparing(pinned) - .thenComparing(priority) - .thenComparing(publishTime, Comparators.nullsLow()) - .thenComparing(name) - .reversed(); + int sizeNullSafe(Integer size) { + return ObjectUtils.defaultIfNull(size, 10); } - static Comparator archiveComparator() { - Function publishTime = - post -> post.getSpec().getPublishTime(); - Function name = post -> post.getMetadata().getName(); - return Comparator.comparing(publishTime, Comparators.nullsLow()) - .thenComparing(name) - .reversed(); + record LinkNavigation(String prev, String current, String next) { } } diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java index 5bc024a4bd6..7b5701d3c12 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java @@ -1,11 +1,8 @@ package run.halo.app.theme.finders.impl; -import java.util.Comparator; import java.util.List; import java.util.function.Function; -import java.util.function.Predicate; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.ObjectUtils; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.util.Assert; @@ -15,7 +12,9 @@ import run.halo.app.content.ContentWrapper; import run.halo.app.content.PostService; import run.halo.app.core.extension.content.Post; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.ListResult; +import run.halo.app.extension.PageRequest; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.metrics.CounterService; import run.halo.app.metrics.MeterUtils; @@ -52,13 +51,21 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService { private final ReactiveQueryPostPredicateResolver postPredicateResolver; @Override - public Mono> list(Integer page, Integer size, - Predicate postPredicate, Comparator comparator) { - return postPredicateResolver.getPredicate() - .map(predicate -> predicate.and(postPredicate == null ? post -> true : postPredicate)) - .flatMap(predicate -> client.list(Post.class, predicate, - comparator, pageNullSafe(page), sizeNullSafe(size)) - ) + public Mono> list(ListOptions queryOptions, PageRequest page) { + return postPredicateResolver.getListOptions() + .map(option -> { + var fieldSelector = queryOptions.getFieldSelector(); + if (fieldSelector != null) { + option.setFieldSelector(option.getFieldSelector() + .andQuery(fieldSelector.query())); + } + var labelSelector = queryOptions.getLabelSelector(); + if (labelSelector != null) { + option.setLabelSelector(option.getLabelSelector().and(labelSelector)); + } + return option; + }) + .flatMap(listOptions -> client.listBy(Post.class, listOptions, page)) .flatMap(list -> Flux.fromStream(list.get()) .concatMap(post -> convertToListedVo(post) .flatMap(postVo -> populateStats(postVo) @@ -70,9 +77,10 @@ comparator, pageNullSafe(page), sizeNullSafe(size)) postVos) ) ) - .defaultIfEmpty(new ListResult<>(page, size, 0L, List.of())); + .defaultIfEmpty(ListResult.emptyResult()); } + @Override public Mono convertToListedVo(@NonNull Post post) { Assert.notNull(post, "Post must not be null"); @@ -180,12 +188,4 @@ private Mono populateStats(T postVo) { ) .defaultIfEmpty(StatsVo.empty()); } - - int pageNullSafe(Integer page) { - return ObjectUtils.defaultIfNull(page, 1); - } - - int sizeNullSafe(Integer size) { - return ObjectUtils.defaultIfNull(size, 10); - } } diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java index a24440cd561..52f2d027944 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/SiteStatsFinderImpl.java @@ -1,11 +1,18 @@ package run.halo.app.theme.finders.impl; +import static run.halo.app.extension.index.query.QueryFactory.and; +import static run.halo.app.extension.index.query.QueryFactory.equal; + import lombok.AllArgsConstructor; import reactor.core.publisher.Mono; import run.halo.app.core.extension.Counter; import run.halo.app.core.extension.content.Category; import run.halo.app.core.extension.content.Post; +import run.halo.app.extension.ListOptions; +import run.halo.app.extension.PageRequestImpl; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.extension.router.selector.FieldSelector; +import run.halo.app.extension.router.selector.LabelSelector; import run.halo.app.theme.finders.Finder; import run.halo.app.theme.finders.SiteStatsFinder; import run.halo.app.theme.finders.vo.SiteStatsVo; @@ -40,9 +47,17 @@ public Mono getStats() { } Mono postCount() { - return client.list(Post.class, post -> !post.isDeleted() && post.isPublished(), null) - .count() - .map(Long::intValue); + var listOptions = new ListOptions(); + listOptions.setLabelSelector(LabelSelector.builder() + .eq(Post.PUBLISHED_LABEL, "true") + .build()); + var fieldQuery = and( + equal("metadata.deletionTimestamp", null), + equal("spec.deleted", "false") + ); + listOptions.setFieldSelector(FieldSelector.of(fieldQuery)); + return client.listBy(Post.class, listOptions, PageRequestImpl.ofSize(1)) + .map(result -> (int) result.getTotal()); } Mono categoryCount() { diff --git a/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java b/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java index d111014f0c5..cf6e8690cef 100644 --- a/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java +++ b/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java @@ -1,5 +1,9 @@ package run.halo.app.theme.router; +import static run.halo.app.extension.index.query.QueryFactory.and; +import static run.halo.app.extension.index.query.QueryFactory.equal; +import static run.halo.app.extension.index.query.QueryFactory.or; + import java.security.Principal; import java.util.Objects; import java.util.function.Predicate; @@ -9,6 +13,9 @@ import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Post; import run.halo.app.extension.ExtensionUtil; +import run.halo.app.extension.ListOptions; +import run.halo.app.extension.router.selector.FieldSelector; +import run.halo.app.extension.router.selector.LabelSelector; import run.halo.app.infra.AnonymousUserConst; /** @@ -34,6 +41,28 @@ public Mono> getPredicate() { .defaultIfEmpty(predicate.and(visiblePredicate)); } + @Override + public Mono getListOptions() { + var listOptions = new ListOptions(); + listOptions.setLabelSelector(LabelSelector.builder() + .eq(Post.PUBLISHED_LABEL, "true").build()); + + var fieldQuery = and( + equal("metadata.deletionTimestamp", null), + equal("spec.deleted", "false") + ); + var visibleQuery = equal("spec.visible", Post.VisibleEnum.PUBLIC.name()); + return currentUserName() + .map(username -> and(fieldQuery, + or(visibleQuery, equal("spec.owner", username))) + ) + .defaultIfEmpty(and(fieldQuery, visibleQuery)) + .map(query -> { + listOptions.setFieldSelector(FieldSelector.of(query)); + return listOptions; + }); + } + Mono currentUserName() { return ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication) diff --git a/application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java b/application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java index 369d9be44d9..01b7f3b68bb 100644 --- a/application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java +++ b/application/src/main/java/run/halo/app/theme/router/ExtensionPermalinkPatternUpdater.java @@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.context.ApplicationListener; +import org.springframework.data.domain.Sort; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import run.halo.app.core.extension.content.Category; @@ -14,6 +15,7 @@ import run.halo.app.extension.AbstractExtension; import run.halo.app.extension.Extension; import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.MetadataOperator; import run.halo.app.extension.MetadataUtil; import run.halo.app.theme.DefaultTemplateEnum; @@ -52,7 +54,7 @@ public void onApplicationEvent(@NonNull PermalinkRuleChangedEvent event) { private void updatePostPermalink(String pattern) { log.debug("Update post permalink by new policy [{}]", pattern); - client.list(Post.class, null, null) + client.listAll(Post.class, new ListOptions(), Sort.unsorted()) .forEach(post -> updateIfPermalinkPatternChanged(post, pattern)); } diff --git a/application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java b/application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java index ee754dc602c..1d571ce01cc 100644 --- a/application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java +++ b/application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java @@ -3,6 +3,7 @@ import java.util.function.Predicate; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Post; +import run.halo.app.extension.ListOptions; /** * The reactive query post predicate resolver. @@ -13,4 +14,6 @@ public interface ReactiveQueryPostPredicateResolver { Mono> getPredicate(); + + Mono getListOptions(); } diff --git a/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java b/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java index 5874c84c471..a13f58575d0 100644 --- a/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java +++ b/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java @@ -15,6 +15,7 @@ import lombok.Data; import lombok.Getter; import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Sort; import org.springframework.http.MediaType; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; @@ -32,6 +33,7 @@ import run.halo.app.core.extension.content.Post; import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.extension.index.query.QueryFactory; import run.halo.app.infra.exception.NotFoundException; import run.halo.app.infra.utils.JsonUtils; import run.halo.app.theme.DefaultTemplateEnum; @@ -159,11 +161,14 @@ private Flux fetchPostsByName(String name) { } private Flux fetchPostsBySlug(String slug) { - return queryPostPredicateResolver.getPredicate() - .flatMapMany(predicate -> client.list(Post.class, - predicate.and(post -> matchIfPresent(slug, post.getSpec().getSlug())), - null) - ); + return queryPostPredicateResolver.getListOptions() + .flatMapMany(listOptions -> { + if (StringUtils.isNotBlank(slug)) { + var other = QueryFactory.equal("spec.slug", slug); + listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(other)); + } + return client.listAll(Post.class, listOptions, Sort.unsorted()); + }); } private boolean matchIfPresent(String variable, String target) { diff --git a/application/src/test/java/run/halo/app/content/DefaultIndexerTest.java b/application/src/test/java/run/halo/app/content/DefaultIndexerTest.java deleted file mode 100644 index 46918818d3b..00000000000 --- a/application/src/test/java/run/halo/app/content/DefaultIndexerTest.java +++ /dev/null @@ -1,368 +0,0 @@ -package run.halo.app.content; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.json.JSONException; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import run.halo.app.core.extension.content.Post; -import run.halo.app.extension.Metadata; -import run.halo.app.infra.utils.JsonUtils; - -/** - * Tests for {@link DefaultIndexer}. - * - * @author guqing - * @since 2.0.0 - */ -class DefaultIndexerTest { - @Test - public void testTagsIndexer() throws JSONException { - // Create a new Indexer that indexes Post objects by tags. - DefaultIndexer indexer = new DefaultIndexer<>(); - String tagsIndexName = "tags"; - indexer.addIndexFunc(tagsIndexName, post -> { - List tags = post.getSpec().getTags(); - return tags == null ? Set.of() : Set.copyOf(tags); - }); - - // Create some Post objects. - Post post1 = new Post(); - post1.setMetadata(new Metadata()); - post1.getMetadata().setName("post-1"); - post1.setSpec(new Post.PostSpec()); - post1.getSpec().setTags(List.of("t1", "t2")); - - Post post2 = new Post(); - post2.setMetadata(new Metadata()); - post2.getMetadata().setName("post-2"); - post2.setSpec(new Post.PostSpec()); - post2.getSpec().setTags(List.of("t2", "t3")); - - Post post3 = new Post(); - post3.setMetadata(new Metadata()); - post3.getMetadata().setName("post-3"); - post3.setSpec(new Post.PostSpec()); - post3.getSpec().setTags(List.of("t3", "t4")); - - // Add the Post objects to the Indexer. - indexer.add(tagsIndexName, post1); - indexer.add(tagsIndexName, post2); - indexer.add(tagsIndexName, post3); - - // Verify that the Indexer has the correct indices. - JSONAssert.assertEquals(""" - { - "t4": [ - "post-3" - ], - "t3": [ - "post-2", - "post-3" - ], - "t2": [ - "post-1", - "post-2" - ], - "t1": [ - "post-1" - ] - } - """, - JsonUtils.objectToJson(indexer.getIndices("tags")), - true); - - // Remove post2 from the Indexer. - indexer.delete(tagsIndexName, post2); - - // Verify that the Indexer has the correct indices. - JSONAssert.assertEquals(""" - { - "t1": [ - "post-1" - ], - "t2": [ - "post-1" - ], - "t3": [ - "post-3" - ], - "t4": [ - "post-3" - ] - } - """, - JsonUtils.objectToJson(indexer.getIndices("tags")), - true); - - // Update post3 in the Indexer. - post3.getSpec().setTags(List.of("t4", "t5")); - indexer.update(tagsIndexName, post3); - - // Verify that the Indexer has the correct indices. - JSONAssert.assertEquals(""" - { - "t1": [ - "post-1" - ], - "t2": [ - "post-1" - ], - "t4": [ - "post-3" - ], - "t5": [ - "post-3" - ] - } - """, - JsonUtils.objectToJson(indexer.getIndices("tags")), - true); - } - - @Test - public void testLabelIndexer() throws JSONException { - // Create a new Indexer. - DefaultIndexer indexer = new DefaultIndexer<>(); - - // Define the IndexFunc for labels. - DefaultIndexer.IndexFunc labelIndexFunc = labelIndexFunc(); - - // Add the label IndexFunc to the Indexer. - String labelsIndexName = "labels"; - indexer.addIndexFunc(labelsIndexName, labelIndexFunc); - - // Create some posts with labels. - Post post1 = new Post(); - post1.setMetadata(new Metadata()); - post1.getMetadata().setName("post-1"); - post1.getMetadata().setLabels(Map.of("app", "myapp", "env", "prod")); - - Post post2 = new Post(); - post2.setMetadata(new Metadata()); - post2.getMetadata().setName("post-2"); - post2.getMetadata().setLabels(Map.of("app", "myapp", "env", "test")); - - Post post3 = new Post(); - post3.setMetadata(new Metadata()); - post3.getMetadata().setName("post-3"); - post3.getMetadata().setLabels(Map.of("app", "otherapp", "env", "prod")); - - // Add the posts to the Indexer. - indexer.add(labelsIndexName, post1); - indexer.add(labelsIndexName, post2); - indexer.add(labelsIndexName, post3); - - // Verify that the Indexer has the correct indices. - assertEquals( - Map.of( - "app=myapp", Set.of("post-1", "post-2"), - "app=otherapp", Set.of("post-3"), - "env=test", Set.of("post-2"), - "env=prod", Set.of("post-1", "post-3") - ), - indexer.getIndices("labels")); - - // Delete post2 from the Indexer. - indexer.delete(labelsIndexName, post2); - - // Verify that the Indexer has the correct indices. - JSONAssert.assertEquals(""" - { - "app=myapp": [ - "post-1" - ], - "env=prod": [ - "post-1", - "post-3" - ], - "app=otherapp": [ - "post-3" - ] - } - """, - JsonUtils.objectToJson(indexer.getIndices("labels")), - true); - - // Update post2 in the Indexer. - post2.getMetadata().setLabels(Map.of("l1", "v1", "l2", "v2")); - indexer.update(labelsIndexName, post2); - - // Verify that the Indexer has the correct indices. - JSONAssert.assertEquals(""" - { - "app=myapp": [ - "post-1" - ], - "env=prod": [ - "post-1", - "post-3" - ], - "app=otherapp": [ - "post-3" - ], - "l1=v1": [ - "post-2" - ], - "l2=v2": [ - "post-2" - ] - } - """, - JsonUtils.objectToJson(indexer.getIndices("labels")), - true); - - // Update post1 in the Indexer. - post1.getMetadata().setLabels(Map.of("l2", "v2", "l3", "v3")); - indexer.update(labelsIndexName, post1); - - // Verify that the Indexer has the correct indices. - JSONAssert.assertEquals(""" - { - "env=prod": [ - "post-3" - ], - "app=otherapp": [ - "post-3" - ], - "l1=v1": [ - "post-2" - ], - "l2=v2": [ - "post-1", - "post-2" - ], - "l3=v3": [ - "post-1" - ] - } - """, - JsonUtils.objectToJson(indexer.getIndices("labels")), - true); - } - - @Test - void multiIndexName() { - // Create a new Indexer. - DefaultIndexer indexer = new DefaultIndexer<>(); - - // Define the IndexFunc for labels. - String labelsIndexName = "labels"; - DefaultIndexer.IndexFunc labelIndexFunc = labelIndexFunc(); - indexer.addIndexFunc(labelsIndexName, labelIndexFunc); - - String tagsIndexName = "tags"; - indexer.addIndexFunc(tagsIndexName, post -> { - List tags = post.getSpec().getTags(); - return tags == null ? Set.of() : Set.copyOf(tags); - }); - - Post post1 = new Post(); - post1.setMetadata(new Metadata()); - post1.getMetadata().setName("post-1"); - post1.getMetadata().setLabels(Map.of("app", "myapp", "env", "prod")); - post1.setSpec(new Post.PostSpec()); - post1.getSpec().setTags(List.of("t1", "t2")); - - Post post2 = new Post(); - post2.setMetadata(new Metadata()); - post2.getMetadata().setName("post-2"); - post2.getMetadata().setLabels(Map.of("app", "myapp", "env", "test")); - post2.setSpec(new Post.PostSpec()); - post2.getSpec().setTags(List.of("t2", "t3")); - - indexer.add(labelsIndexName, post1); - indexer.add(tagsIndexName, post1); - - indexer.add(labelsIndexName, post2); - indexer.add(tagsIndexName, post2); - - assertThat(indexer.getByIndex(labelsIndexName, "app=myapp")) - .containsExactlyInAnyOrder("post-1", "post-2"); - assertThat(indexer.getByIndex(tagsIndexName, "t1")) - .containsExactlyInAnyOrder("post-1"); - - assertThat(indexer.getByIndex(labelsIndexName, "env=test")) - .containsExactlyInAnyOrder("post-2"); - assertThat(indexer.getByIndex(tagsIndexName, "t2")) - .containsExactlyInAnyOrder("post-1", "post-2"); - - post2.getSpec().setTags(List.of("t1", "t4")); - indexer.update(tagsIndexName, post2); - - assertThat(indexer.getByIndex(tagsIndexName, "t1")) - .containsExactlyInAnyOrder("post-1", "post-2"); - assertThat(indexer.getByIndex(tagsIndexName, "t2")) - .containsExactlyInAnyOrder("post-1"); - assertThat(indexer.getByIndex(tagsIndexName, "t4")) - .containsExactlyInAnyOrder("post-2"); - } - - private static DefaultIndexer.IndexFunc labelIndexFunc() { - return post -> { - Map labels = post.getMetadata().getLabels(); - Set indexKeys = new HashSet<>(); - if (labels != null) { - for (Map.Entry entry : labels.entrySet()) { - indexKeys.add(entry.getKey() + "=" + entry.getValue()); - } - } - return indexKeys; - }; - } - - @Test - void getByIndex() { - DefaultIndexer indexer = new DefaultIndexer<>(); - String tagsIndexName = "tags"; - indexer.addIndexFunc(tagsIndexName, post -> { - List tags = post.getSpec().getTags(); - return tags == null ? Set.of() : Set.copyOf(tags); - }); - - // Create some Post objects. - Post post1 = new Post(); - post1.setMetadata(new Metadata()); - post1.getMetadata().setName("post-1"); - post1.setSpec(new Post.PostSpec()); - post1.getSpec().setTags(List.of("t1", "t2")); - - Post post2 = new Post(); - post2.setMetadata(new Metadata()); - post2.getMetadata().setName("post-2"); - post2.setSpec(new Post.PostSpec()); - post2.getSpec().setTags(List.of("t2", "t3")); - - indexer.add(tagsIndexName, post1); - indexer.add(tagsIndexName, post2); - - assertThat(indexer.getByIndex(tagsIndexName, "t1")) - .containsExactlyInAnyOrder("post-1"); - assertThat(indexer.getByIndex(tagsIndexName, "t2")) - .containsExactlyInAnyOrder("post-1", "post-2"); - assertThat(indexer.getByIndex(tagsIndexName, "t3")) - .containsExactlyInAnyOrder("post-2"); - } - - @Test - void addButNotIndexFunc() { - // Create some Post objects. - Post post1 = new Post(); - post1.setMetadata(new Metadata()); - post1.getMetadata().setName("post-1"); - post1.setSpec(new Post.PostSpec()); - post1.getSpec().setTags(List.of("t1", "t2")); - - // Create a new Indexer that indexes Post objects by tags. - final DefaultIndexer indexer = new DefaultIndexer<>(); - assertThrows(IllegalArgumentException.class, () -> { - indexer.add("fake-index-name", post1); - }, "Index function not found for index name 'fake-index-name'"); - } -} diff --git a/application/src/test/java/run/halo/app/content/PostQueryTest.java b/application/src/test/java/run/halo/app/content/PostQueryTest.java index 54ec0fc94b2..d8b50bfc47a 100644 --- a/application/src/test/java/run/halo/app/content/PostQueryTest.java +++ b/application/src/test/java/run/halo/app/content/PostQueryTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import java.util.Collection; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -12,7 +13,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; -import run.halo.app.core.extension.content.Post; +import run.halo.app.extension.index.query.QueryIndexViewImpl; /** * Tests for {@link PostQuery}. @@ -32,61 +33,20 @@ void userScopedQueryTest() { .build(); PostQuery postQuery = new PostQuery(request, "faker"); - var spec = new Post.PostSpec(); - var post = new Post(); - post.setSpec(spec); - spec.setOwner("another-faker"); - assertThat(postQuery.toPredicate().test(post)).isFalse(); - - spec.setOwner("faker"); - assertThat(postQuery.toPredicate().test(post)).isTrue(); - } - - @Test - void toPredicate() { - MultiValueMap multiValueMap = new LinkedMultiValueMap<>(); - multiValueMap.put("category", List.of("category1", "category2")); - MockServerRequest request = MockServerRequest.builder() - .queryParams(multiValueMap) - .exchange(mock(ServerWebExchange.class)) - .build(); - PostQuery postQuery = new PostQuery(request); - - Post post = TestPost.postV1(); - post.getSpec().setTags(null); - post.getStatusOrDefault().setContributors(null); - post.getSpec().setCategories(List.of("category1")); - boolean test = postQuery.toPredicate().test(post); - assertThat(test).isTrue(); - - post.getSpec().setTags(List.of("tag1")); - test = postQuery.toPredicate().test(post); - assertThat(test).isTrue(); - - // Do not include tags - multiValueMap.put("tag", List.of("tag2")); - post.getSpec().setTags(List.of("tag1")); - post.getSpec().setCategories(null); - test = postQuery.toPredicate().test(post); - assertThat(test).isFalse(); - - multiValueMap.put("tag", List.of()); - multiValueMap.remove("category"); - request = MockServerRequest.builder() - .exchange(mock(ServerWebExchange.class)) - .queryParams(multiValueMap).build(); - postQuery = new PostQuery(request); - post.getSpec().setTags(List.of()); - test = postQuery.toPredicate().test(post); - assertThat(test).isTrue(); - - multiValueMap.put("labelSelector", List.of("hello")); - test = postQuery.toPredicate().test(post); - assertThat(test).isFalse(); - - post.getMetadata().setLabels(Map.of("hello", "world")); - test = postQuery.toPredicate().test(post); - assertThat(test).isTrue(); + var listOptions = postQuery.toListOptions(); + assertThat(listOptions).isNotNull(); + assertThat(listOptions.getFieldSelector()).isNotNull(); + var nameEntry = + (Collection>) List.of(Map.entry("metadata.name", "faker")); + var entry = (Collection>) List.of(Map.entry("faker", "faker")); + var indexView = + new QueryIndexViewImpl(Map.of("spec.owner", entry, "metadata.name", nameEntry)); + assertThat(listOptions.getFieldSelector().query().matches(indexView)) + .containsExactly("faker"); + + entry = List.of(Map.entry("another-faker", "user1")); + indexView = new QueryIndexViewImpl(Map.of("spec.owner", entry, "metadata.name", nameEntry)); + assertThat(listOptions.getFieldSelector().query().matches(indexView)).isEmpty(); } } diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java index edfd2854d86..f379b7c0901 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/CategoryReconcilerTest.java @@ -10,18 +10,19 @@ import java.time.Duration; import java.util.List; import java.util.Optional; -import org.json.JSONException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Sort; import run.halo.app.content.TestPost; import run.halo.app.content.permalinks.CategoryPermalinkPolicy; import run.halo.app.core.extension.content.Category; import run.halo.app.core.extension.content.Post; import run.halo.app.extension.ExtensionClient; +import run.halo.app.extension.ListOptions; import run.halo.app.extension.Metadata; import run.halo.app.extension.controller.Reconciler; @@ -43,7 +44,7 @@ class CategoryReconcilerTest { private CategoryReconciler categoryReconciler; @Test - void reconcileStatusPostForCategoryA() throws JSONException { + void reconcileStatusPostForCategoryA() { reconcileStatusPostPilling("category-A"); ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class); @@ -54,7 +55,7 @@ void reconcileStatusPostForCategoryA() throws JSONException { } @Test - void reconcileStatusPostForCategoryB() throws JSONException { + void reconcileStatusPostForCategoryB() { reconcileStatusPostPilling("category-B"); ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class); verify(client, times(3)).update(captor.capture()); @@ -64,7 +65,7 @@ void reconcileStatusPostForCategoryB() throws JSONException { } @Test - void reconcileStatusPostForCategoryC() throws JSONException { + void reconcileStatusPostForCategoryC() { reconcileStatusPostPilling("category-C"); ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class); verify(client, times(3)).update(captor.capture()); @@ -74,7 +75,7 @@ void reconcileStatusPostForCategoryC() throws JSONException { } @Test - void reconcileStatusPostForCategoryD() throws JSONException { + void reconcileStatusPostForCategoryD() { reconcileStatusPostPilling("category-D"); ArgumentCaptor captor = ArgumentCaptor.forClass(Category.class); verify(client, times(3)).update(captor.capture()); @@ -89,7 +90,7 @@ private void reconcileStatusPostPilling(String reconcileCategoryName) { .thenReturn(Optional.of(category)); }); - lenient().when(client.list(eq(Post.class), any(), any())) + lenient().when(client.listAll(eq(Post.class), any(ListOptions.class), any(Sort.class))) .thenReturn(posts()); lenient().when(client.list(eq(Category.class), any(), any())) .thenReturn(categories()); diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java index 18cad76cec7..be159714027 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/PostReconcilerTest.java @@ -85,7 +85,7 @@ void reconcile() { Snapshot snapshotV2 = TestPost.snapshotV2(); snapshotV1.getSpec().setContributors(Set.of("guqing")); snapshotV2.getSpec().setContributors(Set.of("guqing", "zhangsan")); - when(client.list(eq(Snapshot.class), any(), any())) + when(client.listAll(eq(Snapshot.class), any(), any())) .thenReturn(List.of(snapshotV1, snapshotV2)); ArgumentCaptor captor = ArgumentCaptor.forClass(Post.class); @@ -126,7 +126,7 @@ void reconcileExcerpt() { Snapshot snapshotV1 = TestPost.snapshotV1(); snapshotV1.getSpec().setContributors(Set.of("guqing")); - when(client.list(eq(Snapshot.class), any(), any())) + when(client.listAll(eq(Snapshot.class), any(), any())) .thenReturn(List.of(snapshotV1, snapshotV2)); ArgumentCaptor captor = ArgumentCaptor.forClass(Post.class); @@ -162,7 +162,7 @@ void reconcileLastModifyTimeWhenPostIsPublished() { when(client.fetch(eq(Snapshot.class), eq(post.getSpec().getReleaseSnapshot()))) .thenReturn(Optional.of(snapshotV2)); - when(client.list(eq(Snapshot.class), any(), any())) + when(client.listAll(eq(Snapshot.class), any(), any())) .thenReturn(List.of()); ArgumentCaptor captor = ArgumentCaptor.forClass(Post.class); @@ -191,7 +191,7 @@ void reconcileLastModifyTimeWhenPostIsNotPublished() { .rawType("markdown") .build())); - when(client.list(eq(Snapshot.class), any(), any())) + when(client.listAll(eq(Snapshot.class), any(), any())) .thenReturn(List.of()); ArgumentCaptor captor = ArgumentCaptor.forClass(Post.class); diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java index 6579c5374ac..9ee8f913975 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/SinglePageReconcilerTest.java @@ -95,7 +95,7 @@ void reconcile() { Snapshot snapshotV2 = TestPost.snapshotV2(); snapshotV1.getSpec().setContributors(Set.of("guqing")); snapshotV2.getSpec().setContributors(Set.of("guqing", "zhangsan")); - when(client.list(eq(Snapshot.class), any(), any())) + when(client.listAll(eq(Snapshot.class), any(), any())) .thenReturn(List.of(snapshotV1, snapshotV2)); when(externalUrlSupplier.get()).thenReturn(URI.create("")); @@ -156,7 +156,7 @@ void reconcileLastModifyTimeWhenPageIsPublished() { when(client.fetch(eq(Snapshot.class), eq(page.getSpec().getReleaseSnapshot()))) .thenReturn(Optional.of(snapshotV2)); - when(client.list(eq(Snapshot.class), any(), any())) + when(client.listAll(eq(Snapshot.class), any(), any())) .thenReturn(List.of()); ArgumentCaptor captor = ArgumentCaptor.forClass(SinglePage.class); @@ -186,7 +186,7 @@ void reconcileLastModifyTimeWhenPageIsNotPublished() { .build()) ); - when(client.list(eq(Snapshot.class), any(), any())) + when(client.listAll(eq(Snapshot.class), any(), any())) .thenReturn(List.of()); ArgumentCaptor captor = ArgumentCaptor.forClass(SinglePage.class); diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/TagReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/TagReconcilerTest.java index 9f9b52e8f39..38b1cff16fc 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/TagReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/TagReconcilerTest.java @@ -10,15 +10,14 @@ import java.time.Instant; import java.util.List; import java.util.Optional; -import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import run.halo.app.content.PostIndexInformer; import run.halo.app.content.permalinks.TagPermalinkPolicy; +import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.Tag; import run.halo.app.extension.ExtensionClient; import run.halo.app.extension.Metadata; @@ -37,9 +36,6 @@ class TagReconcilerTest { @Mock private TagPermalinkPolicy tagPermalinkPolicy; - @Mock - private PostIndexInformer postIndexInformer; - @InjectMocks private TagReconciler tagReconciler; @@ -48,8 +44,7 @@ void reconcile() { Tag tag = tag(); when(client.fetch(eq(Tag.class), eq("fake-tag"))) .thenReturn(Optional.of(tag)); - when(postIndexInformer.getByTagName(eq("fake-tag"))) - .thenReturn(Set.of()); + when(client.listAll(eq(Post.class), any(), any())).thenReturn(List.of()); when(tagPermalinkPolicy.permalink(any())) .thenAnswer(arg -> "/tags/" + tag.getSpec().getSlug()); ArgumentCaptor captor = ArgumentCaptor.forClass(Tag.class); @@ -85,8 +80,8 @@ void reconcileStatusPosts() { Tag tag = tag(); when(client.fetch(eq(Tag.class), eq("fake-tag"))) .thenReturn(Optional.of(tag)); - when(postIndexInformer.getByTagName(eq("fake-tag"))) - .thenReturn(Set.of("fake-post-1", "fake-post-3")); + when(client.listAll(eq(Post.class), any(), any())) + .thenReturn(List.of(createPost("fake-post-1"), createPost("fake-post-2"))); ArgumentCaptor captor = ArgumentCaptor.forClass(Tag.class); tagReconciler.reconcile(new TagReconciler.Request("fake-tag")); @@ -96,6 +91,14 @@ void reconcileStatusPosts() { assertThat(allValues.get(1).getStatusOrDefault().getVisiblePostCount()).isEqualTo(0); } + Post createPost(String name) { + var post = new Post(); + post.setMetadata(new Metadata()); + post.getMetadata().setName(name); + post.setSpec(new Post.PostSpec()); + return post; + } + Tag tag() { Tag tag = new Tag(); tag.setMetadata(new Metadata()); diff --git a/application/src/test/java/run/halo/app/theme/endpoint/CategoryQueryEndpointTest.java b/application/src/test/java/run/halo/app/theme/endpoint/CategoryQueryEndpointTest.java index a44c02778d2..0ef8f2eee62 100644 --- a/application/src/test/java/run/halo/app/theme/endpoint/CategoryQueryEndpointTest.java +++ b/application/src/test/java/run/halo/app/theme/endpoint/CategoryQueryEndpointTest.java @@ -21,6 +21,7 @@ import run.halo.app.extension.GroupVersion; 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.theme.finders.PostPublicQueryService; import run.halo.app.theme.finders.vo.ListedPostVo; @@ -84,7 +85,7 @@ void getByName() { @Test void listPostsByCategoryName() { ListResult listResult = new ListResult<>(List.of()); - when(postPublicQueryService.list(anyInt(), anyInt(), any(), any())) + when(postPublicQueryService.list(any(), any(PageRequest.class))) .thenReturn(Mono.just(listResult)); webTestClient.get() diff --git a/application/src/test/java/run/halo/app/theme/endpoint/PostQueryEndpointTest.java b/application/src/test/java/run/halo/app/theme/endpoint/PostQueryEndpointTest.java index 4b4404b3e00..f39f1396b31 100644 --- a/application/src/test/java/run/halo/app/theme/endpoint/PostQueryEndpointTest.java +++ b/application/src/test/java/run/halo/app/theme/endpoint/PostQueryEndpointTest.java @@ -2,7 +2,6 @@ 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.anyString; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -20,6 +19,7 @@ import run.halo.app.extension.GroupVersion; import run.halo.app.extension.ListResult; import run.halo.app.extension.Metadata; +import run.halo.app.extension.PageRequest; import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.PostPublicQueryService; import run.halo.app.theme.finders.vo.ListedPostVo; @@ -55,7 +55,7 @@ public void setUp() { @Test public void listPosts() { ListResult result = new ListResult<>(List.of()); - when(postPublicQueryService.list(anyInt(), anyInt(), any(), any())) + when(postPublicQueryService.list(any(), any(PageRequest.class))) .thenReturn(Mono.just(result)); webClient.get().uri("/posts") @@ -65,7 +65,7 @@ public void listPosts() { .expectBody() .jsonPath("$.items").isArray(); - verify(postPublicQueryService).list(anyInt(), anyInt(), any(), any()); + verify(postPublicQueryService).list(any(), any(PageRequest.class)); } @Test diff --git a/application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java b/application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java index a9b5d01ed10..405d5b6ddc2 100644 --- a/application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java +++ b/application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.when; import java.time.Instant; @@ -11,8 +10,6 @@ import java.util.List; import java.util.Map; import java.util.function.Predicate; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.logging.log4j.util.Strings; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -23,6 +20,7 @@ import run.halo.app.core.extension.content.Post; 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.metrics.CounterService; import run.halo.app.theme.finders.CategoryFinder; @@ -67,15 +65,6 @@ class PostFinderImplTest { @InjectMocks private PostFinderImpl postFinder; - @Test - void compare() { - List strings = posts().stream().sorted(PostFinderImpl.defaultComparator()) - .map(post -> post.getMetadata().getName()) - .toList(); - assertThat(strings).isEqualTo( - List.of("post-6", "post-2", "post-1", "post-5", "post-4", "post-3")); - } - @Test void predicate() { Predicate predicate = new DefaultQueryPostPredicateResolver().getPredicate().block(); @@ -93,7 +82,7 @@ void archives() { .map(ListedPostVo::from) .toList(); ListResult listResult = new ListResult<>(1, 10, 3, listedPostVos); - when(publicQueryService.list(anyInt(), anyInt(), any(), any())) + when(publicQueryService.list(any(), any(PageRequest.class))) .thenReturn(Mono.just(listResult)); ListResult archives = postFinder.archives(1, 10).block(); @@ -112,22 +101,6 @@ void archives() { assertThat(items.get(1).getMonths().get(0).getMonth()).isEqualTo("01"); } - @Test - void fixedSizeSlidingWindow() { - PostFinderImpl.FixedSizeSlidingWindow - window = new PostFinderImpl.FixedSizeSlidingWindow<>(3); - - List list = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - window.add(i); - list.add(Strings.join(window.elements(), ',')); - } - assertThat(list).isEqualTo( - List.of("0", "0,1", "0,1,2", "1,2,3", "2,3,4", "3,4,5", "4,5,6", "5,6,7", "6,7,8", - "7,8,9") - ); - } - @Test void postPreviousNextPair() { List postNames = new ArrayList<>(); @@ -136,28 +109,27 @@ void postPreviousNextPair() { } // post-0, post-1, post-2 - Pair previousNextPair = - PostFinderImpl.postPreviousNextPair(postNames, "post-0"); - assertThat(previousNextPair.getLeft()).isNull(); - assertThat(previousNextPair.getRight()).isEqualTo("post-1"); + var previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-0"); + assertThat(previousNextPair.prev()).isNull(); + assertThat(previousNextPair.next()).isEqualTo("post-1"); - previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-1"); - assertThat(previousNextPair.getLeft()).isEqualTo("post-0"); - assertThat(previousNextPair.getRight()).isEqualTo("post-2"); + previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-1"); + assertThat(previousNextPair.prev()).isEqualTo("post-0"); + assertThat(previousNextPair.next()).isEqualTo("post-2"); // post-1, post-2, post-3 - previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-2"); - assertThat(previousNextPair.getLeft()).isEqualTo("post-1"); - assertThat(previousNextPair.getRight()).isEqualTo("post-3"); + previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-2"); + assertThat(previousNextPair.prev()).isEqualTo("post-1"); + assertThat(previousNextPair.next()).isEqualTo("post-3"); // post-7, post-8, post-9 - previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-8"); - assertThat(previousNextPair.getLeft()).isEqualTo("post-7"); - assertThat(previousNextPair.getRight()).isEqualTo("post-9"); + previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-8"); + assertThat(previousNextPair.prev()).isEqualTo("post-7"); + assertThat(previousNextPair.next()).isEqualTo("post-9"); - previousNextPair = PostFinderImpl.postPreviousNextPair(postNames, "post-9"); - assertThat(previousNextPair.getLeft()).isEqualTo("post-8"); - assertThat(previousNextPair.getRight()).isNull(); + previousNextPair = PostFinderImpl.findPostNavigation(postNames, "post-9"); + assertThat(previousNextPair.prev()).isEqualTo("post-8"); + assertThat(previousNextPair.next()).isNull(); } List postsForArchives() {