Skip to content

Commit

Permalink
feat: add index mechanism for extension (#5121)
Browse files Browse the repository at this point in the history
#### What type of PR is this?
/kind feature
/area core
/milestone 2.12.x

#### What this PR does / why we need it:
新增自定义模型索引机制

默认为所有的自定义模型都添加了以下索引:
- metadata.name
- metadata.labels
- metadata.creationTimestamp
- metadata.deletionTimestamp

**how to test it?**
1. 测试应用的启动和停止
2. 测试 Reconciler 被正确执行,如创建文章发布文章,测试删除文章的某个 label 数据启动后能被 PostReconciler 恢复(即Reconciler 被正确执行)
3. 测试自定义模型自动生成的 list APIs
	1. 能根据 labels 正确过滤数据和分页
	2. 能根据 creationTimestamp 正确排序
	3. 测试插件启用后也能正确使用 list APIs 根据 labels 过滤数据和 creationTimestamp 排序
4. 能正确删除数据(则表示 GcReconciler 使用索引正确)
5. 测试在插件中为自定义模型注册索引
```java
public class DemoPlugin extension BasePlugin {
    private final SchemeManager schemeManager;

    public MomentsPlugin(PluginContext pluginContext, SchemeManager schemeManager) {
        super(pluginContext);
        this.schemeManager = schemeManager;
    }

    @OverRide
    public void start() {
        schemeManager.register(Moment.class, indexSpecs -> {
            indexSpecs.add(new IndexSpec()
                .setName("spec.tags")
                .setIndexFunc(multiValueAttribute(Moment.class, moment -> {
                    var tags = moment.getSpec().getTags();
                    return tags == null ? Set.of() : tags;
                }))
            );
            indexSpecs.add(new IndexSpec()
                .setName("spec.owner")
                .setIndexFunc(simpleAttribute(Moment.class,
                    moment -> moment.getSpec().getOwner())
                )
            );
            indexSpecs.add(new IndexSpec()
                .setName("spec.releaseTime")
                .setIndexFunc(simpleAttribute(Moment.class, moment -> {
                    var releaseTime = moment.getSpec().getReleaseTime();
                    return releaseTime == null ? null : releaseTime.toString();
                }))
            );

            indexSpecs.add(new IndexSpec()
                .setName("spec.visible")
                .setIndexFunc(simpleAttribute(Moment.class, moment -> {
                    var visible = moment.getSpec().getVisible();
                    return visible == null ? null : visible.toString();
                }))
            );
        });
    }

    @OverRide
    public void stop() {
        // unregister scheme 即可,不需要手动删除索引
    }
}
```
可以正确在自动生成的 list APIs 使用 fieldSelector 来过滤 `spec.slug` 和排序,可以自己添加其他的 indexSpec 测试
6. 测试唯一索引并添加重复数据,期望无法添加进去

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

#### Does this PR introduce a user-facing change?
```release-note
新增自定义模型索引机制
```
  • Loading branch information
guqing committed Jan 19, 2024
1 parent 3ebb45c commit 6a37df0
Show file tree
Hide file tree
Showing 115 changed files with 6,609 additions and 170 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package run.halo.app.extension;

import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.util.CollectionUtils;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.selector.LabelSelector;

@Getter
@RequiredArgsConstructor
@Builder(builderMethodName = "internalBuilder")
public class DefaultExtensionMatcher implements ExtensionMatcher {
private final ExtensionClient client;
private final GroupVersionKind gvk;
private final LabelSelector labelSelector;
private final FieldSelector fieldSelector;

public static DefaultExtensionMatcherBuilder builder(ExtensionClient client,
GroupVersionKind gvk) {
return internalBuilder().client(client).gvk(gvk);
}

/**
* Match the given extension with the current matcher.
*
* @param extension extension to match
* @return true if the extension matches the current matcher
*/
@Override
public boolean match(Extension extension) {
if (!gvk.equals(extension.groupVersionKind())) {
return false;
}
if (!hasFieldSelector() && !hasLabelSelector()) {
return true;
}
var listOptions = new ListOptions();
listOptions.setLabelSelector(labelSelector);
var fieldQuery = QueryFactory.all();
if (hasFieldSelector()) {
fieldQuery = QueryFactory.and(fieldQuery, fieldSelector.query());
}
listOptions.setFieldSelector(new FieldSelector(fieldQuery));
return client.indexedQueryEngine().retrieve(getGvk(),
listOptions, PageRequestImpl.ofSize(1)).getTotal() > 0;
}

boolean hasFieldSelector() {
return fieldSelector != null && fieldSelector.query() != null;
}

boolean hasLabelSelector() {
return labelSelector != null && !CollectionUtils.isEmpty(labelSelector.getMatchers());
}
}
9 changes: 9 additions & 0 deletions api/src/main/java/run/halo/app/extension/ExtensionClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import org.springframework.data.domain.Sort;
import run.halo.app.extension.index.IndexedQueryEngine;

/**
* ExtensionClient is an interface which contains some operations on Extension instead of
Expand Down Expand Up @@ -42,6 +44,11 @@ <E extends Extension> List<E> list(Class<E> type, Predicate<E> predicate,
<E extends Extension> ListResult<E> list(Class<E> type, Predicate<E> predicate,
Comparator<E> comparator, int page, int size);

<E extends Extension> List<E> listAll(Class<E> type, ListOptions options, Sort sort);

<E extends Extension> ListResult<E> listBy(Class<E> type, ListOptions options,
PageRequest page);

/**
* Fetches Extension by its type and name.
*
Expand Down Expand Up @@ -82,6 +89,8 @@ <E extends Extension> ListResult<E> list(Class<E> type, Predicate<E> predicate,
*/
<E extends Extension> void delete(E extension);

IndexedQueryEngine indexedQueryEngine();

void watch(Watcher watcher);

}
14 changes: 14 additions & 0 deletions api/src/main/java/run/halo/app/extension/ExtensionMatcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package run.halo.app.extension;

import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.selector.LabelSelector;

public interface ExtensionMatcher {
GroupVersionKind getGvk();

LabelSelector getLabelSelector();

FieldSelector getFieldSelector();

boolean match(Extension extension);
}
13 changes: 13 additions & 0 deletions api/src/main/java/run/halo/app/extension/ListOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package run.halo.app.extension;

import lombok.Data;
import lombok.experimental.Accessors;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.selector.LabelSelector;

@Data
@Accessors(chain = true)
public class ListOptions {
private LabelSelector labelSelector;
private FieldSelector fieldSelector;
}
65 changes: 65 additions & 0 deletions api/src/main/java/run/halo/app/extension/PageRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package run.halo.app.extension;

import org.springframework.data.domain.Sort;
import org.springframework.util.Assert;

/**
* <p>{@link PageRequest} is an interface for pagination information.</p>
* <p>Page number starts from 1.</p>
* <p>if page size is 0, it means no pagination and all results will be returned.</p>
*
* @author guqing
* @see PageRequestImpl
* @since 2.12.0
*/
public interface PageRequest {
int getPageNumber();

int getPageSize();

PageRequest previous();

PageRequest next();

/**
* Returns the previous {@link PageRequest} or the first {@link PageRequest} if the current one
* already is the first one.
*
* @return a new {@link org.springframework.data.domain.PageRequest} with
* {@link #getPageNumber()} - 1 as {@link #getPageNumber()}
*/
PageRequest previousOrFirst();

/**
* Returns the {@link PageRequest} requesting the first page.
*
* @return a new {@link org.springframework.data.domain.PageRequest} with
* {@link #getPageNumber()} = 1 as {@link #getPageNumber()}
*/
PageRequest first();

/**
* Creates a new {@link PageRequest} with {@code pageNumber} applied.
*
* @param pageNumber 1-based page index.
* @return a new {@link org.springframework.data.domain.PageRequest}
*/
PageRequest withPage(int pageNumber);

PageRequestImpl withSort(Sort sort);

boolean hasPrevious();

Sort getSort();

/**
* Returns the current {@link Sort} or the given one if the current one is unsorted.
*
* @param sort must not be {@literal null}.
* @return the current {@link Sort} or the given one if the current one is unsorted.
*/
default Sort getSortOr(Sort sort) {
Assert.notNull(sort, "Fallback Sort must not be null");
return getSort().isSorted() ? getSort() : sort;
}
}
86 changes: 86 additions & 0 deletions api/src/main/java/run/halo/app/extension/PageRequestImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package run.halo.app.extension;

import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;

import org.springframework.data.domain.Sort;
import org.springframework.util.Assert;

public class PageRequestImpl implements PageRequest {

private final int pageNumber;
private final int pageSize;
private final Sort sort;

public PageRequestImpl(int pageNumber, int pageSize, Sort sort) {
Assert.notNull(sort, "Sort must not be null");
Assert.isTrue(pageNumber >= 0, "Page index must not be less than zero!");
Assert.isTrue(pageSize >= 0, "Page size must not be less than one!");
this.pageNumber = pageNumber;
this.pageSize = pageSize;
this.sort = sort;
}

public static PageRequestImpl of(int pageNumber, int pageSize) {
return of(pageNumber, pageSize, Sort.unsorted());
}

public static PageRequestImpl of(int pageNumber, int pageSize, Sort sort) {
return new PageRequestImpl(pageNumber, pageSize, sort);
}

public static PageRequestImpl ofSize(int pageSize) {
return PageRequestImpl.of(1, pageSize);
}

@Override
public int getPageNumber() {
return pageNumber;
}

@Override
public int getPageSize() {
return pageSize;
}

@Override
public PageRequest previous() {
return getPageNumber() == 0 ? this
: new PageRequestImpl(getPageNumber() - 1, getPageSize(), getSort());
}

@Override
public Sort getSort() {
return sort;
}

@Override
public PageRequest next() {
return new PageRequestImpl(getPageNumber() + 1, getPageSize(), getSort());
}

@Override
public PageRequest previousOrFirst() {
return hasPrevious() ? previous() : first();
}

@Override
public PageRequest first() {
return new PageRequestImpl(1, getPageSize(), getSort());
}

@Override
public PageRequest withPage(int pageNumber) {
return new PageRequestImpl(pageNumber, getPageSize(), getSort());
}

@Override
public PageRequestImpl withSort(Sort sort) {
return new PageRequestImpl(getPageNumber(), getPageSize(),
defaultIfNull(sort, Sort.unsorted()));
}

@Override
public boolean hasPrevious() {
return pageNumber > 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import java.util.Comparator;
import java.util.function.Predicate;
import org.springframework.data.domain.Sort;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.extension.index.IndexedQueryEngine;

/**
* ExtensionClient is an interface which contains some operations on Extension instead of
Expand Down Expand Up @@ -39,6 +41,11 @@ <E extends Extension> Flux<E> list(Class<E> type, Predicate<E> predicate,
<E extends Extension> Mono<ListResult<E>> list(Class<E> type, Predicate<E> predicate,
Comparator<E> comparator, int page, int size);

<E extends Extension> Flux<E> listAll(Class<E> type, ListOptions options, Sort sort);

<E extends Extension> Mono<ListResult<E>> listBy(Class<E> type, ListOptions options,
PageRequest pageable);

/**
* Fetches Extension by its type and name.
*
Expand Down Expand Up @@ -80,6 +87,8 @@ <E extends Extension> Mono<ListResult<E>> list(Class<E> type, Predicate<E> predi
*/
<E extends Extension> Mono<E> delete(E extension);

IndexedQueryEngine indexedQueryEngine();

void watch(Watcher watcher);

}
7 changes: 7 additions & 0 deletions api/src/main/java/run/halo/app/extension/SchemeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import org.springframework.lang.NonNull;
import run.halo.app.extension.exception.SchemeNotFoundException;
import run.halo.app.extension.index.IndexSpecs;

public interface SchemeManager {

void register(@NonNull Scheme scheme);

void register(@NonNull Scheme scheme, Consumer<IndexSpecs> specsConsumer);

/**
* Registers an Extension using its type.
*
Expand All @@ -20,6 +24,9 @@ default <T extends Extension> void register(Class<T> type) {
register(Scheme.buildFromType(type));
}

default <T extends Extension> void register(Class<T> type, Consumer<IndexSpecs> specsConsumer) {
register(Scheme.buildFromType(type), specsConsumer);
}

void unregister(@NonNull Scheme scheme);

Expand Down
5 changes: 5 additions & 0 deletions api/src/main/java/run/halo/app/extension/Watcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import reactor.core.Disposable;
import run.halo.app.extension.controller.Reconciler;

public interface Watcher extends Disposable {

default void onAdd(Reconciler.Request request) {
// Do nothing here, just for sync all on start.
}

default void onAdd(Extension extension) {
// Do nothing here
}
Expand Down
Loading

0 comments on commit 6a37df0

Please sign in to comment.