From dac4eecea66f3a117ccfab62206459839c6a75bc Mon Sep 17 00:00:00 2001 From: John Niang Date: Sat, 12 Nov 2022 00:12:13 +0800 Subject: [PATCH] Implement full-text search of posts with Lucene default (#2675) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /area core /milestone 2.0 #### What this PR does / why we need it: This PR mainly implement full-text search of posts and provide extension point for other search engine. Meanwhile, I implement ExtensionGetter to get implemention(s) of extension point from system ConfigMap. But there still are something to do here: - [x] Udpate documents when posts are published or posts are becoming unpublic. - [x] Delete documents when posts are unpublished or deleted. Because I'm waiting for https://github.com/halo-dev/halo/pull/2659 got merged. I create two endpoints: 1. For full-text search of post ```bash curl -X 'GET' \ 'http://localhost:8090/apis/api.halo.run/v1alpha1/indices/post?keyword=halo&limit=10000&highlightPreTag=%3CB%3E&highlightPostTag=%3C%2FB%3E' \ -H 'accept: */*' ``` 1. For refreshing indices ```bash curl -X 'POST' \ 'http://localhost:8090/apis/api.console.halo.run/v1alpha1/indices/post' \ -H 'accept: */*' \ -d '' ``` #### Which issue(s) this PR fixes: Fixes #https://github.com/halo-dev/halo/issues/2637 #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note 提供文章全文搜索功能并支持搜索引擎扩展 ``` --- build.gradle | 6 + docs/full-text-search/README.md | 356 ++++++++++++++++++ docs/full-text-search/algolia.png | Bin 0 -> 35691 bytes docs/full-text-search/meilisearch.jpg | Bin 0 -> 91333 bytes .../halo/app/config/HaloConfiguration.java | 2 + .../run/halo/app/core/extension/Post.java | 11 +- .../core/extension/endpoint/PostEndpoint.java | 78 +++- .../halo/app/event/post/PostDeletedEvent.java | 17 + .../app/event/post/PostPublishedEvent.java | 18 + .../app/event/post/PostRecycledEvent.java | 17 + .../app/event/post/PostUnpublishedEvent.java | 18 + .../run/halo/app/extension/ListResult.java | 13 +- .../run/halo/app/extension/Unstructured.java | 22 +- .../app/infra/SchemeInitializedEvent.java | 11 + .../run/halo/app/infra/SchemeInitializer.java | 14 +- .../run/halo/app/infra/SystemSetting.java | 15 + .../app/infra/utils/GenericClassUtils.java | 48 +++ .../DefaultExtensionGetter.java | 67 ++++ .../extensionpoint/ExtensionGetter.java | 27 ++ .../run/halo/app/search/IndicesEndpoint.java | 46 +++ .../halo/app/search/IndicesInitializer.java | 36 ++ .../run/halo/app/search/IndicesService.java | 9 + .../halo/app/search/IndicesServiceImpl.java | 47 +++ .../java/run/halo/app/search/SearchParam.java | 63 ++++ .../run/halo/app/search/SearchResult.java | 13 + .../app/search/extension/SearchEngine.java | 39 ++ .../search/post/LucenePostSearchService.java | 196 ++++++++++ .../run/halo/app/search/post/PostDoc.java | 36 ++ .../app/search/post/PostEventListener.java | 76 ++++ .../run/halo/app/search/post/PostHit.java | 19 + .../app/search/post/PostSearchEndpoint.java | 69 ++++ .../app/search/post/PostSearchService.java | 17 + .../extensions/searchengine-lucene.yaml | 10 + .../run/halo/app/core/extension/PostTest.java | 31 ++ .../extension/endpoint/PostEndpointTest.java | 19 +- .../run/halo/app/infra/SystemSettingTest.java | 30 ++ .../security/SuperAdminInitializerTest.java | 3 +- 37 files changed, 1468 insertions(+), 31 deletions(-) create mode 100644 docs/full-text-search/README.md create mode 100644 docs/full-text-search/algolia.png create mode 100644 docs/full-text-search/meilisearch.jpg create mode 100644 src/main/java/run/halo/app/event/post/PostDeletedEvent.java create mode 100644 src/main/java/run/halo/app/event/post/PostPublishedEvent.java create mode 100644 src/main/java/run/halo/app/event/post/PostRecycledEvent.java create mode 100644 src/main/java/run/halo/app/event/post/PostUnpublishedEvent.java create mode 100644 src/main/java/run/halo/app/infra/SchemeInitializedEvent.java create mode 100644 src/main/java/run/halo/app/infra/utils/GenericClassUtils.java create mode 100644 src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java create mode 100644 src/main/java/run/halo/app/plugin/extensionpoint/ExtensionGetter.java create mode 100644 src/main/java/run/halo/app/search/IndicesEndpoint.java create mode 100644 src/main/java/run/halo/app/search/IndicesInitializer.java create mode 100644 src/main/java/run/halo/app/search/IndicesService.java create mode 100644 src/main/java/run/halo/app/search/IndicesServiceImpl.java create mode 100644 src/main/java/run/halo/app/search/SearchParam.java create mode 100644 src/main/java/run/halo/app/search/SearchResult.java create mode 100644 src/main/java/run/halo/app/search/extension/SearchEngine.java create mode 100644 src/main/java/run/halo/app/search/post/LucenePostSearchService.java create mode 100644 src/main/java/run/halo/app/search/post/PostDoc.java create mode 100644 src/main/java/run/halo/app/search/post/PostEventListener.java create mode 100644 src/main/java/run/halo/app/search/post/PostHit.java create mode 100644 src/main/java/run/halo/app/search/post/PostSearchEndpoint.java create mode 100644 src/main/java/run/halo/app/search/post/PostSearchService.java create mode 100644 src/main/resources/extensions/searchengine-lucene.yaml create mode 100644 src/test/java/run/halo/app/core/extension/PostTest.java create mode 100644 src/test/java/run/halo/app/infra/SystemSettingTest.java diff --git a/build.gradle b/build.gradle index b603f0e65..f8361b6d2 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,12 @@ dependencies { implementation 'org.openapi4j:openapi-schema-validator:1.0.7' implementation "net.bytebuddy:byte-buddy" + // Apache Lucene + implementation 'org.apache.lucene:lucene-core:9.4.1' + implementation 'org.apache.lucene:lucene-queryparser:9.4.1' + implementation 'org.apache.lucene:lucene-highlighter:9.4.1' + implementation 'cn.shenyanchao.ik-analyzer:ik-analyzer:9.0.0' + implementation "org.apache.commons:commons-lang3:$commonsLang3" implementation "io.seruco.encoding:base62:$base62" implementation "org.pf4j:pf4j:$pf4j" diff --git a/docs/full-text-search/README.md b/docs/full-text-search/README.md new file mode 100644 index 000000000..447d144f4 --- /dev/null +++ b/docs/full-text-search/README.md @@ -0,0 +1,356 @@ +# 在 Halo 中实践全文搜索 + +主题端需全文搜索接口用于模糊搜索文章,且对效率要求极高。已经有对应的 Issue +提出,可参考:。 + +实现全文搜索的本地方案最好的就是 Apache 旗下开源的 [Lucene](https://lucene.apache.org/) +,不过 [Hibernate Search](https://hibernate.org/search/) 也基于 Lucene 实现了全文搜索。Halo 2.0 的自定义模型并不是直接在 +Hibernate 上构建的,也就是说 Hibernate 在 Halo 2.0 只是一个可选项,故我们最终可能并不会采用 Hibernate Search,即使它有很多优势。 + +Halo 也可以学习 Hibernate 适配多种搜索引擎,如 Lucene、ElasticSearch、MeiliSearch 等。默认实现为 Lucene,对于用户来说,这种实现方式部署成本最低。 + +## 搜索接口设计 + +### 搜索参数 + +字段如下所示: + +- keyword: string. 关键字 +- sort: string[]. 搜索字段和排序方式 +- offset: number. 本次查询结果偏移数 +- limit: number. 本次查询的结果最大条数 + +例如: + +```bash +http://localhost:8090/apis/api.halo.run/v1alpha1/posts?keyword=halo&sort=title.asc&sort=publishTimestamp,desc&offset=20&limit=10 +``` + +### 搜索结果 + +```yaml +hits: + - name: halo01 + title: Halo 01 + permalink: /posts/halo01 + categories: + - a + - b + tags: + - c + - d + - name: halo02 + title: Halo 02 + permalink: /posts/halo02 + categories: + - a + - b + tags: + - c + - d +query: "halo" +total: 100 +limit: 20 +offset: 10 +processingTimeMills: 2 +``` + +#### 搜索结果分页问题 + +目前,大多数搜索引擎为了性能问题,并没有直接提供分页功能,或者不推荐分页。 + +请参考: + +- +- +- +- + +综合以上讨论,我们暂定不支持分页。不过允许设置单次查询的记录数(limit <= max_limit)。 + +#### 中文搜索优化 + +Lucene 默认的分析器,对中文的分词不够友好,我们需要借助外部依赖或者外部整理好的词库帮助我们更好的对中文句子分词,以便优化中文搜索结果。 + +以下是关于中文分析器的 Java 库: + +- +- +- +- +- + +### 搜索引擎样例 + +#### MeiliSearch + +```bash +curl 'http://localhost:7700/indexes/movies/search' \ + -H 'Accept: */*' \ + -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5' \ + -H 'Authorization: Bearer MASTER_KEY' \ + -H 'Connection: keep-alive' \ + -H 'Content-Type: application/json' \ + -H 'Cookie: logged_in=yes; adminer_permanent=; XSRF-TOKEN=75995791-980a-4f3e-81fb-2e199d8f3934' \ + -H 'Origin: http://localhost:7700' \ + -H 'Referer: http://localhost:7700/' \ + -H 'Sec-Fetch-Dest: empty' \ + -H 'Sec-Fetch-Mode: cors' \ + -H 'Sec-Fetch-Site: same-origin' \ + -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.26' \ + -H 'X-Meilisearch-Client: Meilisearch mini-dashboard (v0.2.2) ; Meilisearch instant-meilisearch (v0.8.2) ; Meilisearch JavaScript (v0.27.0)' \ + -H 'sec-ch-ua: "Microsoft Edge";v="107", "Chromium";v="107", "Not=A?Brand";v="24"' \ + -H 'sec-ch-ua-mobile: ?0' \ + -H 'sec-ch-ua-platform: "Windows"' \ + --data-raw '{"q":"halo","attributesToHighlight":["*"],"highlightPreTag":"","highlightPostTag":"","limit":21}' \ + --compressed +``` + +```json +{ + "hits": [ + { + "id": 108761, + "title": "I Am... Yours: An Intimate Performance at Wynn Las Vegas", + "overview": "Filmed at the Encore Theater at Wynn Las Vegas, this extraordinary concert features performances of over 30 songs from Beyoncé’s three multi-platinum solo releases, Destiny’s Child catalog and a few surprises. This amazing concert includes the #1 hits, “Single Ladies (Put A Ring On It),” “If I Were A Boy,” “Halo,” “Sweet Dreams” and showcases a gut-wrenching performance of “That’s Why You’re Beautiful.” Included on \"I AM... YOURS An Intimate Performance At Wynn Las Vegas,\" is a biographical storytelling woven between many songs and exclusive behind-the-scenes footage.", + "genres": ["Music", "Documentary"], + "poster": "https://image.tmdb.org/t/p/w500/j8n1XQNfw874Ka7SS3HQLCVNBxb.jpg", + "release_date": 1258934400, + "_formatted": { + "id": "108761", + "title": "I Am... Yours: An Intimate Performance at Wynn Las Vegas", + "overview": "Filmed at the Encore Theater at Wynn Las Vegas, this extraordinary concert features performances of over 30 songs from Beyoncé’s three multi-platinum solo releases, Destiny’s Child catalog and a few surprises. This amazing concert includes the #1 hits, “Single Ladies (Put A Ring On It),” “If I Were A Boy,” “Halo,” “Sweet Dreams” and showcases a gut-wrenching performance of “That’s Why You’re Beautiful.” Included on \"I AM... YOURS An Intimate Performance At Wynn Las Vegas,\" is a biographical storytelling woven between many songs and exclusive behind-the-scenes footage.", + "genres": ["Music", "Documentary"], + "poster": "https://image.tmdb.org/t/p/w500/j8n1XQNfw874Ka7SS3HQLCVNBxb.jpg", + "release_date": "1258934400" + } + } + ], + "estimatedTotalHits": 10, + "query": "halo", + "limit": 21, + "offset": 0, + "processingTimeMs": 2 +} +``` + +![MeiliSearch UI](./meilisearch.jpg) + +#### Algolia + +```bash +curl 'https://og53ly1oqh-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=Algolia%20for%20JavaScript%20(4.14.2)%3B%20Browser%20(lite)%3B%20docsearch%20(3.2.1)%3B%20docsearch-react%20(3.2.1)%3B%20docusaurus%20(2.1.0)&x-algolia-api-key=739f2a55c6d13d93af146c22a4885669&x-algolia-application-id=OG53LY1OQH' \ + -H 'Accept: */*' \ + -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5' \ + -H 'Connection: keep-alive' \ + -H 'Origin: https://docs.halo.run' \ + -H 'Referer: https://docs.halo.run/' \ + -H 'Sec-Fetch-Dest: empty' \ + -H 'Sec-Fetch-Mode: cors' \ + -H 'Sec-Fetch-Site: cross-site' \ + -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.26' \ + -H 'content-type: application/x-www-form-urlencoded' \ + -H 'sec-ch-ua: "Microsoft Edge";v="107", "Chromium";v="107", "Not=A?Brand";v="24"' \ + -H 'sec-ch-ua-mobile: ?0' \ + -H 'sec-ch-ua-platform: "Windows"' \ + --data-raw '{"requests":[{"query":"halo","indexName":"docs","params":"attributesToRetrieve=%5B%22hierarchy.lvl0%22%2C%22hierarchy.lvl1%22%2C%22hierarchy.lvl2%22%2C%22hierarchy.lvl3%22%2C%22hierarchy.lvl4%22%2C%22hierarchy.lvl5%22%2C%22hierarchy.lvl6%22%2C%22content%22%2C%22type%22%2C%22url%22%5D&attributesToSnippet=%5B%22hierarchy.lvl1%3A5%22%2C%22hierarchy.lvl2%3A5%22%2C%22hierarchy.lvl3%3A5%22%2C%22hierarchy.lvl4%3A5%22%2C%22hierarchy.lvl5%3A5%22%2C%22hierarchy.lvl6%3A5%22%2C%22content%3A5%22%5D&snippetEllipsisText=%E2%80%A6&highlightPreTag=%3Cmark%3E&highlightPostTag=%3C%2Fmark%3E&hitsPerPage=20&facetFilters=%5B%22language%3Azh-Hans%22%2C%5B%22docusaurus_tag%3Adefault%22%2C%22docusaurus_tag%3Adocs-default-1.6%22%5D%5D"}]}' \ + --compressed +``` + +```json +{ + "results": [ + { + "hits": [ + { + "content": null, + "hierarchy": { + "lvl0": "Documentation", + "lvl1": "使用 Docker Compose 部署 Halo", + "lvl2": "更新容器组 ​", + "lvl3": null, + "lvl4": null, + "lvl5": null, + "lvl6": null + }, + "type": "lvl2", + "url": "https://docs.halo.run/getting-started/install/other/docker-compose/#更新容器组", + "objectID": "4ccfa93009143feb6e423274a4944496267beea8", + "_snippetResult": { + "hierarchy": { + "lvl1": { + "value": "… Docker Compose 部署 Halo", + "matchLevel": "full" + }, + "lvl2": { + "value": "更新容器组 ​", + "matchLevel": "none" + } + } + }, + "_highlightResult": { + "hierarchy": { + "lvl0": { + "value": "Documentation", + "matchLevel": "none", + "matchedWords": [] + }, + "lvl1": { + "value": "使用 Docker Compose 部署 Halo", + "matchLevel": "full", + "fullyHighlighted": false, + "matchedWords": ["halo"] + }, + "lvl2": { + "value": "更新容器组 ​", + "matchLevel": "none", + "matchedWords": [] + } + }, + "hierarchy_camel": [ + { + "lvl0": { + "value": "Documentation", + "matchLevel": "none", + "matchedWords": [] + }, + "lvl1": { + "value": "使用 Docker Compose 部署 Halo", + "matchLevel": "full", + "fullyHighlighted": false, + "matchedWords": ["halo"] + }, + "lvl2": { + "value": "更新容器组 ​", + "matchLevel": "none", + "matchedWords": [] + } + } + ] + } + } + ], + "nbHits": 113, + "page": 0, + "nbPages": 6, + "hitsPerPage": 20, + "exhaustiveNbHits": true, + "exhaustiveTypo": true, + "exhaustive": { + "nbHits": true, + "typo": true + }, + "query": "halo", + "params": "query=halo&attributesToRetrieve=%5B%22hierarchy.lvl0%22%2C%22hierarchy.lvl1%22%2C%22hierarchy.lvl2%22%2C%22hierarchy.lvl3%22%2C%22hierarchy.lvl4%22%2C%22hierarchy.lvl5%22%2C%22hierarchy.lvl6%22%2C%22content%22%2C%22type%22%2C%22url%22%5D&attributesToSnippet=%5B%22hierarchy.lvl1%3A5%22%2C%22hierarchy.lvl2%3A5%22%2C%22hierarchy.lvl3%3A5%22%2C%22hierarchy.lvl4%3A5%22%2C%22hierarchy.lvl5%3A5%22%2C%22hierarchy.lvl6%3A5%22%2C%22content%3A5%22%5D&snippetEllipsisText=%E2%80%A6&highlightPreTag=%3Cmark%3E&highlightPostTag=%3C%2Fmark%3E&hitsPerPage=20&facetFilters=%5B%22language%3Azh-Hans%22%2C%5B%22docusaurus_tag%3Adefault%22%2C%22docusaurus_tag%3Adocs-default-1.6%22%5D%5D", + "index": "docs", + "renderingContent": {}, + "processingTimeMS": 1, + "processingTimingsMS": { + "total": 1 + } + } + ] +} +``` + +![Algolia UI](./algolia.png) + +#### Wiki + +```bash +curl 'https://wiki.fit2cloud.com/rest/api/search?cql=siteSearch%20~%20%22halo%22%20AND%20type%20in%20(%22space%22%2C%22user%22%2C%22com.atlassian.confluence.extra.team-calendars%3Acalendar-content-type%22%2C%22attachment%22%2C%22page%22%2C%22com.atlassian.confluence.extra.team-calendars%3Aspace-calendars-view-content-type%22%2C%22blogpost%22)&start=20&limit=20&excerpt=highlight&expand=space.icon&includeArchivedSpaces=false&src=next.ui.search' \ + -H 'authority: wiki.fit2cloud.com' \ + -H 'accept: */*' \ + -H 'accept-language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-TW;q=0.5' \ + -H 'cache-control: no-cache, no-store, must-revalidate' \ + -H 'cookie: _ga=GA1.2.1720479041.1657188862; seraph.confluence=89915546%3A6fc1394f8d537ffa08fb679e6e4dd64993448051; mywork.tab.tasks=false; JSESSIONID=5347D8618AC5883DE9B702E77152170D' \ + -H 'expires: 0' \ + -H 'pragma: no-cache' \ + -H 'referer: https://wiki.fit2cloud.com/' \ + -H 'sec-ch-ua: "Microsoft Edge";v="107", "Chromium";v="107", "Not=A?Brand";v="24"' \ + -H 'sec-ch-ua-mobile: ?0' \ + -H 'sec-ch-ua-platform: "Windows"' \ + -H 'sec-fetch-dest: empty' \ + -H 'sec-fetch-mode: cors' \ + -H 'sec-fetch-site: same-origin' \ + -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.26' \ + --compressed +``` + +```json +{ + "results": [ + { + "content": { + "id": "76722", + "type": "page", + "status": "current", + "title": "2.3 测试 - 接口", + "restrictions": {}, + "_links": { + "webui": "/pages/viewpage.action?pageId=721", + "tinyui": "/x/8K_SB", + "self": "https://wiki.halo.run/rest/api/content/76720" + }, + "_expandable": { + "container": "", + "metadata": "", + "extensions": "", + "operations": "", + "children": "", + "history": "/rest/api/content/7670/history", + "ancestors": "", + "body": "", + "version": "", + "descendants": "", + "space": "/rest/api/space/IT" + } + }, + "title": "2.3 接口 - 接口", + "excerpt": "另存为新用例", + "url": "/pages/viewpage.action?pageId=7672", + "resultGlobalContainer": { + "title": "IT 客户", + "displayUrl": "/display/IT" + }, + "entityType": "content", + "iconCssClass": "aui-icon content-type-page", + "lastModified": "2022-05-11T22:40:53.000+08:00", + "friendlyLastModified": "五月 11, 2022", + "timestamp": 1652280053000 + } + ], + "start": 20, + "limit": 20, + "size": 20, + "totalSize": 70, + "cqlQuery": "siteSearch ~ \"halo\" AND type in (\"space\",\"user\",\"com.atlassian.confluence.extra.team-calendars:calendar-content-type\",\"attachment\",\"page\",\"com.atlassian.confluence.extra.team-calendars:space-calendars-view-content-type\",\"blogpost\")", + "searchDuration": 36, + "_links": { + "base": "https://wiki.halo.run", + "context": "" + } +} +``` + +### FAQ + +#### 是否需要统一参数和响应体结构? + +以下是关于统一参数和响应体结构的优缺点分析: + +优点: + +- 主题端搜索结果 UI 更加一致,不会因为使用不同搜索引擎导致 UI 上的变动 + +缺点: + +- 无法完全发挥出对应的搜索引擎的实力。比如某个搜索引擎有很实用的功能,而某些搜索引擎没有。 +- Halo Core 需要适配不同的搜索引擎,比较繁琐 + +#### 是否需要提供扩展点集成其他搜索引擎? + +既然 Lucene 非常强大,且暂时已经能够满足我们的要求,我们为什么还需要集成其他搜索引擎呢? + +- Lucene 目前是作为 Halo 的依赖使用的,也就意味着只支持 Halo 单实例部署,阻碍未来 Halo 无状态化的趋势。 +- 相反,其他搜索引擎(例如 Solr、MeiliSearch、ElasticSearch 等)都可以独立部署,Halo 只需要利用对应的 SDK 和搜索引擎沟通即可,无论 Halo 是否是多实例部署。 diff --git a/docs/full-text-search/algolia.png b/docs/full-text-search/algolia.png new file mode 100644 index 0000000000000000000000000000000000000000..d16964292b37d82c79f79ac6c9208853c46a60a3 GIT binary patch literal 35691 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 YZT8(CO!Q(khJYVA8D;5GxbeIH0%>Ey`Tzg` literal 0 HcmV?d00001 diff --git a/docs/full-text-search/meilisearch.jpg b/docs/full-text-search/meilisearch.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f594677718bca2563b753d965deea3ace922000b GIT binary patch literal 91333 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 literal 0 HcmV?d00001 diff --git a/src/main/java/run/halo/app/config/HaloConfiguration.java b/src/main/java/run/halo/app/config/HaloConfiguration.java index e7cbabc91..fc67d21af 100644 --- a/src/main/java/run/halo/app/config/HaloConfiguration.java +++ b/src/main/java/run/halo/app/config/HaloConfiguration.java @@ -4,8 +4,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; @Configuration(proxyBeanMethods = false) +@EnableAsync public class HaloConfiguration { @Bean diff --git a/src/main/java/run/halo/app/core/extension/Post.java b/src/main/java/run/halo/app/core/extension/Post.java index 810de45c7..59346b7cf 100644 --- a/src/main/java/run/halo/app/core/extension/Post.java +++ b/src/main/java/run/halo/app/core/extension/Post.java @@ -1,5 +1,7 @@ package run.halo.app.core.extension; +import static java.lang.Boolean.parseBoolean; + import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import java.time.Instant; @@ -13,6 +15,7 @@ import lombok.ToString; import run.halo.app.extension.AbstractExtension; import run.halo.app.extension.ExtensionUtil; import run.halo.app.extension.GVK; +import run.halo.app.extension.MetadataOperator; import run.halo.app.infra.Condition; /** @@ -62,8 +65,12 @@ public class Post extends AbstractExtension { @JsonIgnore public boolean isPublished() { - Map labels = getMetadata().getLabels(); - return labels != null && labels.getOrDefault(PUBLISHED_LABEL, "false").equals("true"); + return isPublished(this.getMetadata()); + } + + public static boolean isPublished(MetadataOperator metadata) { + var labels = metadata.getLabels(); + return labels != null && parseBoolean(labels.getOrDefault(PUBLISHED_LABEL, "false")); } @Data diff --git a/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java b/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java index e21316d58..46133025e 100644 --- a/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java +++ b/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java @@ -6,20 +6,27 @@ import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder; import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder; import io.swagger.v3.oas.annotations.enums.ParameterIn; +import java.time.Duration; import lombok.AllArgsConstructor; import org.springdoc.core.fn.builders.schema.Builder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; import run.halo.app.content.ListedPost; import run.halo.app.content.PostQuery; import run.halo.app.content.PostRequest; import run.halo.app.content.PostService; import run.halo.app.core.extension.Post; +import run.halo.app.event.post.PostPublishedEvent; +import run.halo.app.event.post.PostRecycledEvent; +import run.halo.app.event.post.PostUnpublishedEvent; import run.halo.app.extension.ListResult; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.router.QueryParamBuildUtil; @@ -37,6 +44,8 @@ public class PostEndpoint implements CustomEndpoint { private final PostService postService; private final ReactiveExtensionClient client; + private final ApplicationEventPublisher eventPublisher; + @Override public RouterFunction endpoint() { final var tag = "api.console.halo.run/v1alpha1/Post"; @@ -91,9 +100,29 @@ public class PostEndpoint implements CustomEndpoint { .in(ParameterIn.PATH) .required(true) .implementation(String.class)) + .parameter(parameterBuilder().name("headSnapshot") + .description("Head snapshot name of content.") + .in(ParameterIn.QUERY) + .required(false)) .response(responseBuilder() .implementation(Post.class)) ) + .PUT("posts/{name}/unpublish", this::unpublishPost, + builder -> builder.operationId("UnpublishPost") + .description("Publish a post.") + .tag(tag) + .parameter(parameterBuilder().name("name") + .in(ParameterIn.PATH) + .required(true)) + .response(responseBuilder() + .implementation(Post.class))) + .PUT("posts/{name}/recycle", this::recyclePost, + builder -> builder.operationId("RecyclePost") + .description("Recycle a post.") + .tag(tag) + .parameter(parameterBuilder().name("name") + .in(ParameterIn.PATH) + .required(true))) .build(); } @@ -110,15 +139,54 @@ public class PostEndpoint implements CustomEndpoint { } Mono publishPost(ServerRequest request) { - String name = request.pathVariable("name"); - return client.fetch(Post.class, name) - .flatMap(post -> { - Post.PostSpec spec = post.getSpec(); + var name = request.pathVariable("name"); + return client.get(Post.class, name) + .doOnNext(post -> { + var spec = post.getSpec(); + request.queryParam("headSnapshot").ifPresent(spec::setHeadSnapshot); spec.setPublish(true); + // TODO Provide release snapshot query param to control spec.setReleaseSnapshot(spec.getHeadSnapshot()); - return client.update(post); }) + .flatMap(client::update) + .retryWhen(Retry.backoff(3, Duration.ofMillis(100)) + .filter(t -> t instanceof OptimisticLockingFailureException)) .flatMap(post -> postService.publishPost(post.getMetadata().getName())) + // TODO Fire published event in reconciler in the future + .doOnNext(post -> eventPublisher.publishEvent( + new PostPublishedEvent(this, post.getMetadata().getName()))) + .flatMap(post -> ServerResponse.ok().bodyValue(post)); + } + + private Mono unpublishPost(ServerRequest request) { + var name = request.pathVariable("name"); + return client.get(Post.class, name) + .doOnNext(post -> { + var spec = post.getSpec(); + spec.setPublish(false); + }) + .flatMap(client::update) + .retryWhen(Retry.backoff(3, Duration.ofMillis(100)) + .filter(t -> t instanceof OptimisticLockingFailureException)) + // TODO Fire unpublished event in reconciler in the future + .doOnNext(post -> eventPublisher.publishEvent( + new PostUnpublishedEvent(this, post.getMetadata().getName()))) + .flatMap(post -> ServerResponse.ok().bodyValue(post)); + } + + private Mono recyclePost(ServerRequest request) { + var name = request.pathVariable("name"); + return client.get(Post.class, name) + .doOnNext(post -> { + var spec = post.getSpec(); + spec.setDeleted(true); + }) + .flatMap(client::update) + .retryWhen(Retry.backoff(3, Duration.ofMillis(100)) + .filter(t -> t instanceof OptimisticLockingFailureException)) + // TODO Fire recycled event in reconciler in the future + .doOnNext(post -> eventPublisher.publishEvent( + new PostRecycledEvent(this, post.getMetadata().getName()))) .flatMap(post -> ServerResponse.ok().bodyValue(post)); } diff --git a/src/main/java/run/halo/app/event/post/PostDeletedEvent.java b/src/main/java/run/halo/app/event/post/PostDeletedEvent.java new file mode 100644 index 000000000..ade689021 --- /dev/null +++ b/src/main/java/run/halo/app/event/post/PostDeletedEvent.java @@ -0,0 +1,17 @@ +package run.halo.app.event.post; + +import org.springframework.context.ApplicationEvent; + +public class PostDeletedEvent extends ApplicationEvent { + + private final String postName; + + public PostDeletedEvent(Object source, String postName) { + super(source); + this.postName = postName; + } + + public String getPostName() { + return postName; + } +} diff --git a/src/main/java/run/halo/app/event/post/PostPublishedEvent.java b/src/main/java/run/halo/app/event/post/PostPublishedEvent.java new file mode 100644 index 000000000..9eec770bf --- /dev/null +++ b/src/main/java/run/halo/app/event/post/PostPublishedEvent.java @@ -0,0 +1,18 @@ +package run.halo.app.event.post; + +import org.springframework.context.ApplicationEvent; + +public class PostPublishedEvent extends ApplicationEvent { + + private final String postName; + + public PostPublishedEvent(Object source, String postName) { + super(source); + this.postName = postName; + } + + public String getPostName() { + return postName; + } + +} diff --git a/src/main/java/run/halo/app/event/post/PostRecycledEvent.java b/src/main/java/run/halo/app/event/post/PostRecycledEvent.java new file mode 100644 index 000000000..c3b8f4fd8 --- /dev/null +++ b/src/main/java/run/halo/app/event/post/PostRecycledEvent.java @@ -0,0 +1,17 @@ +package run.halo.app.event.post; + +import org.springframework.context.ApplicationEvent; + +public class PostRecycledEvent extends ApplicationEvent { + + private final String postName; + + public PostRecycledEvent(Object source, String postName) { + super(source); + this.postName = postName; + } + + public String getPostName() { + return postName; + } +} diff --git a/src/main/java/run/halo/app/event/post/PostUnpublishedEvent.java b/src/main/java/run/halo/app/event/post/PostUnpublishedEvent.java new file mode 100644 index 000000000..5d40db25f --- /dev/null +++ b/src/main/java/run/halo/app/event/post/PostUnpublishedEvent.java @@ -0,0 +1,18 @@ +package run.halo.app.event.post; + +import org.springframework.context.ApplicationEvent; + +public class PostUnpublishedEvent extends ApplicationEvent { + + private final String postName; + + public PostUnpublishedEvent(Object source, String postName) { + super(source); + this.postName = postName; + } + + public String getPostName() { + return postName; + } + +} diff --git a/src/main/java/run/halo/app/extension/ListResult.java b/src/main/java/run/halo/app/extension/ListResult.java index ffd68c606..520235143 100644 --- a/src/main/java/run/halo/app/extension/ListResult.java +++ b/src/main/java/run/halo/app/extension/ListResult.java @@ -1,5 +1,7 @@ package run.halo.app.extension; +import static run.halo.app.infra.utils.GenericClassUtils.generateConcreteClass; + import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; @@ -120,15 +122,8 @@ public class ListResult implements Streamable { * @return generic ListResult class. */ public static Class generateGenericClass(Class type) { - var generic = - TypeDescription.Generic.Builder.parameterizedType(ListResult.class, type) - .build(); - return new ByteBuddy() - .subclass(generic) - .name(type.getSimpleName() + "List") - .make() - .load(ListResult.class.getClassLoader()) - .getLoaded(); + return generateConcreteClass(ListResult.class, type, + () -> type.getSimpleName() + "List"); } public static ListResult emptyResult() { diff --git a/src/main/java/run/halo/app/extension/Unstructured.java b/src/main/java/run/halo/app/extension/Unstructured.java index 5719926ae..32b5f9730 100644 --- a/src/main/java/run/halo/app/extension/Unstructured.java +++ b/src/main/java/run/halo/app/extension/Unstructured.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.time.Instant; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -45,6 +46,10 @@ public class Unstructured implements Extension { this.data = data; } + public Map getData() { + return Collections.unmodifiableMap(data); + } + @Override public String getApiVersion() { return (String) data.get("apiVersion"); @@ -161,7 +166,7 @@ public class Unstructured implements Extension { data.put("metadata", metadataMap); } - static Optional getNestedValue(Map map, String... fields) { + public static Optional getNestedValue(Map map, String... fields) { if (fields == null || fields.length == 0) { return Optional.of(map); } @@ -177,11 +182,11 @@ public class Unstructured implements Extension { } @SuppressWarnings("unchecked") - static Optional> getNestedStringList(Map map, String... fields) { + public static Optional> getNestedStringList(Map map, String... fields) { return getNestedValue(map, fields).map(value -> (List) value); } - static Optional> getNestedStringSet(Map map, String... fields) { + public static Optional> getNestedStringSet(Map map, String... fields) { return getNestedValue(map, fields).map(value -> { if (value instanceof Collection collection) { return new LinkedHashSet<>(collection); @@ -192,7 +197,7 @@ public class Unstructured implements Extension { } @SuppressWarnings("unchecked") - static void setNestedValue(Map map, Object value, String... fields) { + public static void setNestedValue(Map map, Object value, String... fields) { if (fields == null || fields.length == 0) { // do nothing when no fields provided return; @@ -205,12 +210,13 @@ public class Unstructured implements Extension { }); } - static Optional getNestedMap(Map map, String... fields) { + public static Optional getNestedMap(Map map, String... fields) { return getNestedValue(map, fields).map(value -> (Map) value); } @SuppressWarnings("unchecked") - static Optional> getNestedStringStringMap(Map map, String... fields) { + public static Optional> getNestedStringStringMap(Map map, + String... fields) { return getNestedValue(map, fields) .map(labelsObj -> { var labels = (Map) labelsObj; @@ -220,7 +226,7 @@ public class Unstructured implements Extension { }); } - static Optional getNestedInstant(Map map, String... fields) { + public static Optional getNestedInstant(Map map, String... fields) { return getNestedValue(map, fields) .map(instantValue -> { if (instantValue instanceof Instant instant) { @@ -231,7 +237,7 @@ public class Unstructured implements Extension { } - static Optional getNestedLong(Map map, String... fields) { + public static Optional getNestedLong(Map map, String... fields) { return getNestedValue(map, fields) .map(longObj -> { if (longObj instanceof Long l) { diff --git a/src/main/java/run/halo/app/infra/SchemeInitializedEvent.java b/src/main/java/run/halo/app/infra/SchemeInitializedEvent.java new file mode 100644 index 000000000..647aa518c --- /dev/null +++ b/src/main/java/run/halo/app/infra/SchemeInitializedEvent.java @@ -0,0 +1,11 @@ +package run.halo.app.infra; + +import org.springframework.context.ApplicationEvent; + +public class SchemeInitializedEvent extends ApplicationEvent { + + public SchemeInitializedEvent(Object source) { + super(source); + } + +} diff --git a/src/main/java/run/halo/app/infra/SchemeInitializer.java b/src/main/java/run/halo/app/infra/SchemeInitializer.java index 8f1d60526..97104b567 100644 --- a/src/main/java/run/halo/app/infra/SchemeInitializer.java +++ b/src/main/java/run/halo/app/infra/SchemeInitializer.java @@ -1,6 +1,7 @@ package run.halo.app.infra; import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationListener; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; @@ -27,6 +28,7 @@ import run.halo.app.core.extension.attachment.Policy; import run.halo.app.core.extension.attachment.PolicyTemplate; import run.halo.app.extension.ConfigMap; import run.halo.app.extension.SchemeManager; +import run.halo.app.search.extension.SearchEngine; import run.halo.app.security.authentication.pat.PersonalAccessToken; @Component @@ -34,15 +36,23 @@ public class SchemeInitializer implements ApplicationListener> { + + public static final ExtensionPointEnabled EMPTY = new ExtensionPointEnabled(); + + public static final String GROUP = "extensionPointEnabled"; + + } + } diff --git a/src/main/java/run/halo/app/infra/utils/GenericClassUtils.java b/src/main/java/run/halo/app/infra/utils/GenericClassUtils.java new file mode 100644 index 000000000..d79eac2b8 --- /dev/null +++ b/src/main/java/run/halo/app/infra/utils/GenericClassUtils.java @@ -0,0 +1,48 @@ +package run.halo.app.infra.utils; + +import static net.bytebuddy.description.type.TypeDescription.Generic.Builder.parameterizedType; + +import java.io.IOException; +import java.util.function.Supplier; +import net.bytebuddy.ByteBuddy; +import reactor.core.Exceptions; + +public enum GenericClassUtils { + ; + + /** + * Generate concrete class of generic class. e.g.: {@code List} + * + * @param rawClass is generic class, like {@code List.class} + * @param parameterType is parameter type of generic class + * @param parameter type + * @return generated class + */ + public static Class generateConcreteClass(Class rawClass, Class parameterType) { + return generateConcreteClass(rawClass, parameterType, () -> + parameterType.getSimpleName() + rawClass.getSimpleName()); + } + + /** + * Generate concrete class of generic class. e.g.: {@code List} + * + * @param rawClass is generic class, like {@code List.class} + * @param parameterType is parameter type of generic class + * @param nameGenerator is generated class name + * @param parameter type + * @return generated class + */ + public static Class generateConcreteClass(Class rawClass, Class parameterType, + Supplier nameGenerator) { + var concreteType = parameterizedType(rawClass, parameterType).build(); + try (var unloaded = new ByteBuddy() + .subclass(concreteType) + .name(nameGenerator.get()) + .make()) { + return unloaded.load(rawClass.getClassLoader()).getLoaded(); + } catch (IOException e) { + // Should never happen + throw Exceptions.propagate(e); + } + } +} diff --git a/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java b/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java new file mode 100644 index 000000000..9a41dbbfc --- /dev/null +++ b/src/main/java/run/halo/app/plugin/extensionpoint/DefaultExtensionGetter.java @@ -0,0 +1,67 @@ +package run.halo.app.plugin.extensionpoint; + +import java.util.Set; +import org.pf4j.ExtensionPoint; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.infra.SystemSetting.ExtensionPointEnabled; +import run.halo.app.plugin.HaloPluginManager; + +@Component +public class DefaultExtensionGetter implements ExtensionGetter { + + private final SystemConfigurableEnvironmentFetcher systemConfigFetcher; + + private final HaloPluginManager pluginManager; + + private final ApplicationContext applicationContext; + + public DefaultExtensionGetter(SystemConfigurableEnvironmentFetcher systemConfigFetcher, + HaloPluginManager pluginManager, ApplicationContext applicationContext) { + this.systemConfigFetcher = systemConfigFetcher; + this.pluginManager = pluginManager; + this.applicationContext = applicationContext; + } + + @Override + public Mono getEnabledExtension(Class extensionPoint) { + return systemConfigFetcher.fetch(ExtensionPointEnabled.GROUP, ExtensionPointEnabled.class) + .switchIfEmpty(Mono.just(ExtensionPointEnabled.EMPTY)) + .mapNotNull(enabled -> { + var implClassNames = enabled.getOrDefault(extensionPoint.getName(), Set.of()); + return pluginManager.getExtensions(extensionPoint) + .stream() + .filter(impl -> implClassNames.contains(impl.getClass().getName())) + .findFirst() + // Fallback to local implementation of the extension point. + // This will happen when no proper configuration is found. + .orElseGet(() -> + applicationContext.getBeanProvider(extensionPoint).getIfAvailable()); + }); + } + + @Override + public Flux getEnabledExtensions(Class extensionPoint) { + return systemConfigFetcher.fetch(ExtensionPointEnabled.GROUP, ExtensionPointEnabled.class) + .switchIfEmpty(Mono.just(ExtensionPointEnabled.EMPTY)) + .flatMapMany(enabled -> { + var implClassNames = enabled.getOrDefault(extensionPoint.getName(), Set.of()); + var extensions = pluginManager.getExtensions(extensionPoint) + .stream() + .filter(impl -> implClassNames.contains(impl.getClass().getName())) + .toList(); + if (extensions.isEmpty()) { + extensions = applicationContext.getBeanProvider(extensionPoint) + .orderedStream() + // we only fetch one implementation here + .limit(1) + .toList(); + } + return Flux.fromIterable(extensions); + }); + } + +} diff --git a/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionGetter.java b/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionGetter.java new file mode 100644 index 000000000..9cfff8931 --- /dev/null +++ b/src/main/java/run/halo/app/plugin/extensionpoint/ExtensionGetter.java @@ -0,0 +1,27 @@ +package run.halo.app.plugin.extensionpoint; + +import org.pf4j.ExtensionPoint; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface ExtensionGetter { + + /** + * Get only one enabled extension from system configuration. + * + * @param extensionPoint is extension point class. + * @return implementation of the corresponding extension point. If no configuration is found, + * we will use the default implementation from application context instead. + */ + Mono getEnabledExtension(Class extensionPoint); + + /** + * Get enabled extension list from system configuration. + * + * @param extensionPoint is extension point class. + * @return implementations of the corresponding extension point. If no configuration is found, + * we will use the default implementation from application context instead. + */ + Flux getEnabledExtensions(Class extensionPoint); + +} diff --git a/src/main/java/run/halo/app/search/IndicesEndpoint.java b/src/main/java/run/halo/app/search/IndicesEndpoint.java new file mode 100644 index 000000000..e8696b8dd --- /dev/null +++ b/src/main/java/run/halo/app/search/IndicesEndpoint.java @@ -0,0 +1,46 @@ +package run.halo.app.search; + +import lombok.extern.slf4j.Slf4j; +import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.endpoint.CustomEndpoint; +import run.halo.app.extension.GroupVersion; + +@Component +@Slf4j +public class IndicesEndpoint implements CustomEndpoint { + + private final IndicesService indicesService; + + private static final String API_VERSION = "api.console.halo.run/v1alpha1"; + + public IndicesEndpoint(IndicesService indicesService) { + this.indicesService = indicesService; + } + + @Override + public RouterFunction endpoint() { + final var tag = API_VERSION + "/Indices"; + return SpringdocRouteBuilder.route() + .POST("indices/post", this::rebuildPostIndices, + builder -> builder.operationId("BuildPostIndices") + .tag(tag) + .description("Build or rebuild post indices for full text search")) + .build(); + } + + private Mono rebuildPostIndices(ServerRequest request) { + return indicesService.rebuildPostIndices() + .then(Mono.defer(() -> ServerResponse.ok().bodyValue("Rebuild post indices"))); + } + + @Override + public GroupVersion groupVersion() { + return GroupVersion.parseAPIVersion(API_VERSION); + } + +} diff --git a/src/main/java/run/halo/app/search/IndicesInitializer.java b/src/main/java/run/halo/app/search/IndicesInitializer.java new file mode 100644 index 000000000..89e6dd6a4 --- /dev/null +++ b/src/main/java/run/halo/app/search/IndicesInitializer.java @@ -0,0 +1,36 @@ +package run.halo.app.search; + +import java.util.concurrent.CountDownLatch; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.util.StopWatch; +import run.halo.app.infra.SchemeInitializedEvent; + +@Slf4j +@Component +public class IndicesInitializer { + + private final IndicesService indicesService; + + public IndicesInitializer(IndicesService indicesService) { + this.indicesService = indicesService; + } + + @Async + @EventListener(SchemeInitializedEvent.class) + public void whenSchemeInitialized(SchemeInitializedEvent event) throws InterruptedException { + var latch = new CountDownLatch(1); + log.info("Initialize post indices..."); + var watch = new StopWatch("PostIndicesWatch"); + watch.start("rebuild"); + indicesService.rebuildPostIndices() + .doFinally(signalType -> latch.countDown()) + .subscribe(); + latch.await(); + watch.stop(); + log.info("Initialized post indices. Usage: {}", watch); + } + +} diff --git a/src/main/java/run/halo/app/search/IndicesService.java b/src/main/java/run/halo/app/search/IndicesService.java new file mode 100644 index 000000000..40cfdf674 --- /dev/null +++ b/src/main/java/run/halo/app/search/IndicesService.java @@ -0,0 +1,9 @@ +package run.halo.app.search; + +import reactor.core.publisher.Mono; + +public interface IndicesService { + + Mono rebuildPostIndices(); + +} diff --git a/src/main/java/run/halo/app/search/IndicesServiceImpl.java b/src/main/java/run/halo/app/search/IndicesServiceImpl.java new file mode 100644 index 000000000..21b91ce9b --- /dev/null +++ b/src/main/java/run/halo/app/search/IndicesServiceImpl.java @@ -0,0 +1,47 @@ +package run.halo.app.search; + +import org.springframework.stereotype.Service; +import reactor.core.Exceptions; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import run.halo.app.core.extension.Post; +import run.halo.app.plugin.extensionpoint.ExtensionGetter; +import run.halo.app.search.post.PostDoc; +import run.halo.app.search.post.PostSearchService; +import run.halo.app.theme.finders.PostFinder; + +@Service +public class IndicesServiceImpl implements IndicesService { + private final ExtensionGetter extensionGetter; + + private final PostFinder postFinder; + + public IndicesServiceImpl(ExtensionGetter extensionGetter, PostFinder postFinder) { + this.extensionGetter = extensionGetter; + this.postFinder = postFinder; + } + + @Override + public Mono rebuildPostIndices() { + return extensionGetter.getEnabledExtension(PostSearchService.class) + // TODO Optimize listing posts with non-blocking. + .flatMap(searchService -> Flux.fromStream(() -> postFinder.list(0, 0) + .stream() + .filter(post -> Post.isPublished(post.getMetadata())) + .peek(post -> postFinder.content(post.getMetadata().getName())) + .map(PostDoc::from)) + .subscribeOn(Schedulers.boundedElastic()) + .limitRate(100) + .buffer(100) + .doOnNext(postDocs -> { + try { + searchService.addDocuments(postDocs); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + }) + .then() + ); + } +} diff --git a/src/main/java/run/halo/app/search/SearchParam.java b/src/main/java/run/halo/app/search/SearchParam.java new file mode 100644 index 000000000..c7ba3640a --- /dev/null +++ b/src/main/java/run/halo/app/search/SearchParam.java @@ -0,0 +1,63 @@ +package run.halo.app.search; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebInputException; + +public class SearchParam { + + private static final int DEFAULT_LIMIT = 10; + private static final String DEFAULT_HIGHLIGHT_PRE_TAG = ""; + private static final String DEFAULT_HIGHLIGHT_POST_TAG = ""; + + private final MultiValueMap query; + + public SearchParam(MultiValueMap query) { + this.query = query; + } + + @Schema(name = "keyword", required = true) + public String getKeyword() { + var keyword = query.getFirst("keyword"); + if (!StringUtils.hasText(keyword)) { + throw new ServerWebInputException("keyword is required"); + } + return keyword; + } + + @Schema(name = "limit", defaultValue = "100", maximum = "1000") + public int getLimit() { + var limitString = query.getFirst("limit"); + int limit = 0; + if (StringUtils.hasText(limitString)) { + try { + limit = Integer.parseInt(limitString); + } catch (NumberFormatException nfe) { + throw new ServerWebInputException("Failed to get "); + } + } + if (limit <= 0) { + limit = DEFAULT_LIMIT; + } + return limit; + } + + @Schema(name = "highlightPreTag", defaultValue = DEFAULT_HIGHLIGHT_PRE_TAG) + public String getHighlightPreTag() { + var highlightPreTag = query.getFirst("highlightPreTag"); + if (!StringUtils.hasText(highlightPreTag)) { + highlightPreTag = DEFAULT_HIGHLIGHT_PRE_TAG; + } + return highlightPreTag; + } + + @Schema(name = "highlightPostTag", defaultValue = DEFAULT_HIGHLIGHT_POST_TAG) + public String getHighlightPostTag() { + var highlightPostTag = query.getFirst("highlightPostTag"); + if (!StringUtils.hasText(highlightPostTag)) { + highlightPostTag = DEFAULT_HIGHLIGHT_POST_TAG; + } + return highlightPostTag; + } +} diff --git a/src/main/java/run/halo/app/search/SearchResult.java b/src/main/java/run/halo/app/search/SearchResult.java new file mode 100644 index 000000000..8fadd14cd --- /dev/null +++ b/src/main/java/run/halo/app/search/SearchResult.java @@ -0,0 +1,13 @@ +package run.halo.app.search; + +import java.util.List; +import lombok.Data; + +@Data +public class SearchResult { + private List hits; + private String keyword; + private Long total; + private int limit; + private long processingTimeMillis; +} diff --git a/src/main/java/run/halo/app/search/extension/SearchEngine.java b/src/main/java/run/halo/app/search/extension/SearchEngine.java new file mode 100644 index 000000000..0834b0940 --- /dev/null +++ b/src/main/java/run/halo/app/search/extension/SearchEngine.java @@ -0,0 +1,39 @@ +package run.halo.app.search.extension; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import run.halo.app.extension.AbstractExtension; +import run.halo.app.extension.GVK; +import run.halo.app.extension.Ref; + +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +@GVK(group = "plugin.halo.run", version = "v1alpha1", kind = "SearchEngine", + plural = "searchengines", singular = "searchengine") +public class SearchEngine extends AbstractExtension { + + @Schema(required = true) + private SearchEngineSpec spec; + + @Data + public static class SearchEngineSpec { + + private String logo; + + private String website; + + @Schema(required = true) + private String displayName; + + private String description; + + private Ref settingRef; + + private String postSearchImpl; + + } + +} diff --git a/src/main/java/run/halo/app/search/post/LucenePostSearchService.java b/src/main/java/run/halo/app/search/post/LucenePostSearchService.java new file mode 100644 index 000000000..e582f6dd5 --- /dev/null +++ b/src/main/java/run/halo/app/search/post/LucenePostSearchService.java @@ -0,0 +1,196 @@ +package run.halo.app.search.post; + +import static org.apache.commons.lang3.StringUtils.stripToEmpty; +import static org.apache.lucene.document.Field.Store.NO; +import static org.apache.lucene.document.Field.Store.YES; +import static org.apache.lucene.index.IndexWriterConfig.OpenMode.APPEND; +import static org.apache.lucene.index.IndexWriterConfig.OpenMode.CREATE_OR_APPEND; + +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.LongPoint; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.document.StringField; +import org.apache.lucene.document.TextField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.highlight.Highlighter; +import org.apache.lucene.search.highlight.InvalidTokenOffsetsException; +import org.apache.lucene.search.highlight.QueryScorer; +import org.apache.lucene.search.highlight.SimpleFragmenter; +import org.apache.lucene.search.highlight.SimpleHTMLFormatter; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.jsoup.Jsoup; +import org.jsoup.safety.Safelist; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.stereotype.Service; +import org.springframework.util.StopWatch; +import org.wltea.analyzer.lucene.IKAnalyzer; +import reactor.core.Exceptions; +import run.halo.app.infra.properties.HaloProperties; +import run.halo.app.search.SearchParam; +import run.halo.app.search.SearchResult; + +@Service +@Slf4j +public class LucenePostSearchService implements PostSearchService, DisposableBean { + + public static final int MAX_FRAGMENT_SIZE = 100; + + private final Analyzer analyzer; + + private final Directory postIndexDir; + + public LucenePostSearchService(HaloProperties haloProperties) + throws IOException { + analyzer = new IKAnalyzer(); + var postIdxPath = haloProperties.getWorkDir().resolve("indices/posts"); + postIndexDir = FSDirectory.open(postIdxPath); + } + + @Override + public SearchResult search(SearchParam param) throws Exception { + var dirReader = DirectoryReader.open(postIndexDir); + var searcher = new IndexSearcher(dirReader); + var keyword = param.getKeyword(); + var watch = new StopWatch("SearchWatch"); + watch.start("search for " + keyword); + var query = buildQuery(keyword); + var topDocs = searcher.search(query, param.getLimit(), Sort.RELEVANCE); + watch.stop(); + var highlighter = new Highlighter( + new SimpleHTMLFormatter(param.getHighlightPreTag(), param.getHighlightPostTag()), + new QueryScorer(query)); + highlighter.setTextFragmenter(new SimpleFragmenter(MAX_FRAGMENT_SIZE)); + + var hits = new ArrayList(topDocs.scoreDocs.length); + for (var scoreDoc : topDocs.scoreDocs) { + hits.add(convert(searcher.doc(scoreDoc.doc), highlighter)); + } + + var result = new SearchResult(); + result.setHits(hits); + result.setTotal(topDocs.totalHits.value); + result.setKeyword(param.getKeyword()); + result.setLimit(param.getLimit()); + result.setProcessingTimeMillis(watch.getTotalTimeMillis()); + return result; + } + + @Override + public void addDocuments(List posts) throws IOException { + var writeConfig = new IndexWriterConfig(analyzer); + writeConfig.setOpenMode(CREATE_OR_APPEND); + try (var writer = new IndexWriter(postIndexDir, writeConfig)) { + posts.forEach(post -> { + var doc = this.convert(post); + try { + var seqNum = + writer.updateDocument(new Term(PostDoc.ID_FIELD, post.getName()), doc); + if (log.isDebugEnabled()) { + log.debug("Updated document({}) with sequence number {} returned", + post.getName(), seqNum); + } + } catch (IOException e) { + throw Exceptions.propagate(e); + } + }); + } + } + + @Override + public void removeDocuments(Set postNames) throws IOException { + var writeConfig = new IndexWriterConfig(analyzer); + writeConfig.setOpenMode(APPEND); + try (var writer = new IndexWriter(postIndexDir, writeConfig)) { + var terms = postNames.stream() + .map(postName -> new Term(PostDoc.ID_FIELD, postName)) + .toArray(Term[]::new); + long seqNum = writer.deleteDocuments(terms); + log.debug("Deleted documents({}) with sequence number {}", terms.length, seqNum); + } + } + + @Override + public void destroy() throws Exception { + analyzer.close(); + postIndexDir.close(); + } + + + private Query buildQuery(String keyword) { + keyword = stripToEmpty(keyword).toLowerCase(); + if (log.isDebugEnabled()) { + log.debug("Trying to search for keyword: {}", keyword); + } + return new FuzzyQuery(new Term("searchable", keyword)); + } + + private Document convert(PostDoc post) { + var doc = new Document(); + doc.add(new StringField("name", post.getName(), YES)); + doc.add(new StoredField("title", post.getTitle())); + + var content = Jsoup.clean(stripToEmpty(post.getExcerpt()) + stripToEmpty(post.getContent()), + Safelist.none()); + + doc.add(new StoredField("content", content)); + doc.add(new TextField("searchable", post.getTitle() + content, NO)); + + long publishTimestamp = post.getPublishTimestamp().toEpochMilli(); + doc.add(new LongPoint("publishTimestamp", publishTimestamp)); + doc.add(new StoredField("publishTimestamp", publishTimestamp)); + doc.add(new StoredField("permalink", post.getPermalink())); + return doc; + } + + private PostHit convert(Document doc, Highlighter highlighter) + throws IOException, InvalidTokenOffsetsException { + var post = new PostHit(); + post.setName(doc.get("name")); + + var title = getHighlightedText(doc, "title", highlighter, MAX_FRAGMENT_SIZE); + post.setTitle(title); + + var content = getHighlightedText(doc, "content", highlighter, MAX_FRAGMENT_SIZE); + post.setContent(content); + + var publishTimestamp = doc.getField("publishTimestamp").numericValue().longValue(); + post.setPublishTimestamp(Instant.ofEpochMilli(publishTimestamp)); + post.setPermalink(doc.get("permalink")); + return post; + } + + private String getHighlightedText(Document doc, String field, Highlighter highlighter, + int maxLength) + throws InvalidTokenOffsetsException, IOException { + try { + var highlightedText = highlighter.getBestFragment(analyzer, field, doc.get(field)); + if (highlightedText != null) { + return highlightedText; + } + } catch (IllegalArgumentException iae) { + // TODO we have to ignore the error currently due to no solution about the error. + if (!"boost must be a positive float, got -1.0".equals(iae.getMessage())) { + throw iae; + } + } + // handle if there is not highlighted text + var fieldValue = doc.get(field); + return StringUtils.substring(fieldValue, 0, maxLength); + } +} diff --git a/src/main/java/run/halo/app/search/post/PostDoc.java b/src/main/java/run/halo/app/search/post/PostDoc.java new file mode 100644 index 000000000..867234373 --- /dev/null +++ b/src/main/java/run/halo/app/search/post/PostDoc.java @@ -0,0 +1,36 @@ +package run.halo.app.search.post; + +import java.time.Instant; +import lombok.Data; +import run.halo.app.theme.finders.vo.PostVo; + +@Data +public class PostDoc { + + public static final String ID_FIELD = "name"; + + private String name; + + private String title; + + private String excerpt; + + private String content; + + private Instant publishTimestamp; + + private String permalink; + + // TODO Move this static method to other place. + public static PostDoc from(PostVo postVo) { + var post = new PostDoc(); + post.setName(postVo.getMetadata().getName()); + post.setTitle(postVo.getSpec().getTitle()); + post.setExcerpt(postVo.getStatus().getExcerpt()); + post.setPublishTimestamp(postVo.getSpec().getPublishTime()); + post.setContent(postVo.getContent().getContent()); + post.setPermalink(postVo.getStatus().getPermalink()); + return post; + } + +} diff --git a/src/main/java/run/halo/app/search/post/PostEventListener.java b/src/main/java/run/halo/app/search/post/PostEventListener.java new file mode 100644 index 000000000..439a1b964 --- /dev/null +++ b/src/main/java/run/halo/app/search/post/PostEventListener.java @@ -0,0 +1,76 @@ +package run.halo.app.search.post; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import reactor.core.Exceptions; +import run.halo.app.event.post.PostPublishedEvent; +import run.halo.app.event.post.PostRecycledEvent; +import run.halo.app.event.post.PostUnpublishedEvent; +import run.halo.app.plugin.extensionpoint.ExtensionGetter; +import run.halo.app.theme.finders.PostFinder; + +@Component +public class PostEventListener { + + private final ExtensionGetter extensionGetter; + + private final PostFinder postFinder; + + public PostEventListener(ExtensionGetter extensionGetter, + PostFinder postFinder) { + this.extensionGetter = extensionGetter; + this.postFinder = postFinder; + } + + @Async + @EventListener(PostPublishedEvent.class) + public void handlePostPublished(PostPublishedEvent publishedEvent) throws InterruptedException { + var postVo = postFinder.getByName(publishedEvent.getPostName()); + var postDoc = PostDoc.from(postVo); + + var latch = new CountDownLatch(1); + extensionGetter.getEnabledExtension(PostSearchService.class) + .doOnNext(searchService -> { + try { + searchService.addDocuments(List.of(postDoc)); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + }) + .doFinally(signalType -> latch.countDown()) + .subscribe(); + latch.await(); + } + + @Async + @EventListener(PostUnpublishedEvent.class) + public void handlePostUnpublished(PostUnpublishedEvent unpublishedEvent) + throws InterruptedException { + deletePostDoc(unpublishedEvent.getPostName()); + } + + @Async + @EventListener(PostRecycledEvent.class) + public void handlePostRecycled(PostRecycledEvent recycledEvent) throws InterruptedException { + deletePostDoc(recycledEvent.getPostName()); + } + + void deletePostDoc(String postName) throws InterruptedException { + var latch = new CountDownLatch(1); + extensionGetter.getEnabledExtension(PostSearchService.class) + .doOnNext(searchService -> { + try { + searchService.removeDocuments(Set.of(postName)); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + }) + .doFinally(signalType -> latch.countDown()) + .subscribe(); + latch.await(); + } +} diff --git a/src/main/java/run/halo/app/search/post/PostHit.java b/src/main/java/run/halo/app/search/post/PostHit.java new file mode 100644 index 000000000..04c9da111 --- /dev/null +++ b/src/main/java/run/halo/app/search/post/PostHit.java @@ -0,0 +1,19 @@ +package run.halo.app.search.post; + +import java.time.Instant; +import lombok.Data; + +@Data +public class PostHit { + + private String name; + + private String title; + + private String content; + + private Instant publishTimestamp; + + private String permalink; + +} diff --git a/src/main/java/run/halo/app/search/post/PostSearchEndpoint.java b/src/main/java/run/halo/app/search/post/PostSearchEndpoint.java new file mode 100644 index 000000000..ed98eb0c3 --- /dev/null +++ b/src/main/java/run/halo/app/search/post/PostSearchEndpoint.java @@ -0,0 +1,69 @@ +package run.halo.app.search.post; + +import static run.halo.app.extension.router.QueryParamBuildUtil.buildParametersFromType; +import static run.halo.app.infra.utils.GenericClassUtils.generateConcreteClass; + +import org.springdoc.core.fn.builders.apiresponse.Builder; +import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.Exceptions; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.endpoint.CustomEndpoint; +import run.halo.app.extension.GroupVersion; +import run.halo.app.plugin.extensionpoint.ExtensionGetter; +import run.halo.app.search.SearchParam; +import run.halo.app.search.SearchResult; + +@Component +public class PostSearchEndpoint implements CustomEndpoint { + + private static final String API_VERSION = "api.halo.run/v1alpha1"; + + private final ExtensionGetter extensionGetter; + + public PostSearchEndpoint(ExtensionGetter extensionGetter) { + this.extensionGetter = extensionGetter; + } + + @Override + public RouterFunction endpoint() { + final var tag = API_VERSION + "/Post"; + return SpringdocRouteBuilder.route() + .GET("indices/post", this::search, + builder -> { + builder.operationId("SearchPost") + .tag(tag) + .description("Search posts with fuzzy query") + .response(Builder.responseBuilder().implementation( + generateConcreteClass(SearchResult.class, PostHit.class, + () -> "PostHits"))); + buildParametersFromType(builder, SearchParam.class); + } + ) + .build(); + } + + private Mono search(ServerRequest request) { + return Mono.fromSupplier( + () -> new SearchParam(request.queryParams())) + .flatMap(param -> extensionGetter.getEnabledExtension(PostSearchService.class) + .switchIfEmpty(Mono.error(() -> + new RuntimeException("Please enable any post search service before searching"))) + .map(searchService -> { + try { + return searchService.search(param); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + })) + .flatMap(result -> ServerResponse.ok().bodyValue(result)); + } + + @Override + public GroupVersion groupVersion() { + return GroupVersion.parseAPIVersion(API_VERSION); + } +} diff --git a/src/main/java/run/halo/app/search/post/PostSearchService.java b/src/main/java/run/halo/app/search/post/PostSearchService.java new file mode 100644 index 000000000..37f5b4c1f --- /dev/null +++ b/src/main/java/run/halo/app/search/post/PostSearchService.java @@ -0,0 +1,17 @@ +package run.halo.app.search.post; + +import java.util.List; +import java.util.Set; +import org.pf4j.ExtensionPoint; +import run.halo.app.search.SearchParam; +import run.halo.app.search.SearchResult; + +public interface PostSearchService extends ExtensionPoint { + + SearchResult search(SearchParam searchParam) throws Exception; + + void addDocuments(List posts) throws Exception; + + void removeDocuments(Set postNames) throws Exception; + +} diff --git a/src/main/resources/extensions/searchengine-lucene.yaml b/src/main/resources/extensions/searchengine-lucene.yaml new file mode 100644 index 000000000..b27bf9d4c --- /dev/null +++ b/src/main/resources/extensions/searchengine-lucene.yaml @@ -0,0 +1,10 @@ +apiVersion: plugin.halo.run/v1alpha1 +kind: SearchEngine +metadata: + name: lucene +spec: + logo: https://lucene.apache.org/theme/images/lucene/lucene_logo_green_300.png + website: https://lucene.apache.org/ + displayName: Lucene + description: Apache Lucene is a high-performance, full-featured search engine library written entirely in Java. It is a technology suitable for nearly any application that requires structured search, full-text search, faceting, nearest-neighbor search across high-dimensionality vectors, spell correction or query suggestions. + postSearchImpl: run.halo.app.search.post.LucenePostSearchService diff --git a/src/test/java/run/halo/app/core/extension/PostTest.java b/src/test/java/run/halo/app/core/extension/PostTest.java new file mode 100644 index 000000000..3a8055608 --- /dev/null +++ b/src/test/java/run/halo/app/core/extension/PostTest.java @@ -0,0 +1,31 @@ +package run.halo.app.core.extension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.function.Function; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import run.halo.app.extension.MetadataOperator; + +class PostTest { + + @Test + void staticIsPublishedTest() { + var test = (Function, Boolean>) (labels) -> { + var metadata = Mockito.mock(MetadataOperator.class); + when(metadata.getLabels()).thenReturn(labels); + return Post.isPublished(metadata); + }; + assertEquals(false, test.apply(Map.of())); + assertEquals(false, test.apply(Map.of("content.halo.run/published", "false"))); + assertEquals(false, test.apply(Map.of("content.halo.run/published", "False"))); + assertEquals(false, test.apply(Map.of("content.halo.run/published", "0"))); + assertEquals(false, test.apply(Map.of("content.halo.run/published", "1"))); + assertEquals(false, test.apply(Map.of("content.halo.run/published", "T"))); + assertEquals(false, test.apply(Map.of("content.halo.run/published", ""))); + assertEquals(true, test.apply(Map.of("content.halo.run/published", "true"))); + assertEquals(true, test.apply(Map.of("content.halo.run/published", "True"))); + } +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/core/extension/endpoint/PostEndpointTest.java b/src/test/java/run/halo/app/core/extension/endpoint/PostEndpointTest.java index 8ccc9bc92..d4686cef1 100644 --- a/src/test/java/run/halo/app/core/extension/endpoint/PostEndpointTest.java +++ b/src/test/java/run/halo/app/core/extension/endpoint/PostEndpointTest.java @@ -3,6 +3,8 @@ package run.halo.app.core.extension.endpoint; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; import org.junit.jupiter.api.BeforeEach; @@ -11,12 +13,14 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Mono; import run.halo.app.content.PostRequest; import run.halo.app.content.PostService; import run.halo.app.content.TestPost; import run.halo.app.core.extension.Post; +import run.halo.app.event.post.PostPublishedEvent; import run.halo.app.extension.ReactiveExtensionClient; /** @@ -27,15 +31,19 @@ import run.halo.app.extension.ReactiveExtensionClient; */ @ExtendWith(MockitoExtension.class) class PostEndpointTest { + @Mock - private PostService postService; + PostService postService; @Mock - private ReactiveExtensionClient client; + ReactiveExtensionClient client; + + @Mock + ApplicationEventPublisher eventPublisher; @InjectMocks - private PostEndpoint postEndpoint; + PostEndpoint postEndpoint; - private WebTestClient webTestClient; + WebTestClient webTestClient; @BeforeEach void setUp() { @@ -75,9 +83,10 @@ class PostEndpointTest { void publishPost() { Post post = TestPost.postV1(); when(postService.publishPost(any())).thenReturn(Mono.just(post)); - when(client.fetch(eq(Post.class), eq(post.getMetadata().getName()))) + when(client.get(eq(Post.class), eq(post.getMetadata().getName()))) .thenReturn(Mono.just(post)); when(client.update(any())).thenReturn(Mono.just(post)); + doNothing().when(eventPublisher).publishEvent(isA(PostPublishedEvent.class)); webTestClient.put() .uri("/posts/post-A/publish") diff --git a/src/test/java/run/halo/app/infra/SystemSettingTest.java b/src/test/java/run/halo/app/infra/SystemSettingTest.java new file mode 100644 index 000000000..c54ff65aa --- /dev/null +++ b/src/test/java/run/halo/app/infra/SystemSettingTest.java @@ -0,0 +1,30 @@ +package run.halo.app.infra; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import run.halo.app.infra.SystemSetting.ExtensionPointEnabled; +import run.halo.app.infra.utils.JsonUtils; + +class SystemSettingTest { + + @Nested + class ExtensionPointEnabledTest { + + @Test + void deserializeTest() { + var json = """ + { + "run.halo.app.search.post.PostSearchService": [ + "run.halo.app.search.post.LucenePostSearchService" + ] + } + """; + + var enabled = JsonUtils.jsonToObject(json, ExtensionPointEnabled.class); + assertTrue(enabled.containsKey("run.halo.app.search.post.PostSearchService")); + } + } + +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/security/SuperAdminInitializerTest.java b/src/test/java/run/halo/app/security/SuperAdminInitializerTest.java index 2a3be4f30..9d12ced3b 100644 --- a/src/test/java/run/halo/app/security/SuperAdminInitializerTest.java +++ b/src/test/java/run/halo/app/security/SuperAdminInitializerTest.java @@ -19,7 +19,8 @@ import run.halo.app.extension.ReactiveExtensionClient; @SpringBootTest(properties = {"halo.security.initializer.disabled=false", "halo.security.initializer.super-admin-username=fake-admin", - "halo.security.initializer.super-admin-password=fake-password"}) + "halo.security.initializer.super-admin-password=fake-password", + "halo.required-extension-disabled=true"}) @AutoConfigureWebTestClient @AutoConfigureTestDatabase class SuperAdminInitializerTest {