Refactor project structure for a better development (#3552)

#### What type of PR is this?

/kind cleanup
/area core

#### What this PR does / why we need it:

This PR totally refactor project structure for a better plugin development. Now we can maintain and publish api and platform modules at Halo application side, which will be references by plugins.

Currently, we can execute command `./gradlew clean publish` to publish api and platform modules into **local** Maven repository, so that we can refer these dependencies (`run.halo.tools.platform:plugin:2.4.0-SNAPSHOT` and `run.halo.app:api:2.4.0-SNAPSHOT`) in plugin projects. 

I will make another pull request to publish api library and platforms into Maven central repository.

**Modules explanation**:
- API module contains common classes which might be used by plugins.
- Plugin Platform module contains dependency declarations of other plugin API modules.
- Application Platform module contains dependency declarations application module might uses.

If we want to build application only(exclude check and jar), we have to execute the command below:

```bash
./gradlew clean :application:build -x :application:check -x :application:jar
```

The executable Jar will be generated at folder `application/build/libs/`.

If we want to build a Docker image, we could execute the command below:

```bash
docker build -t johnniang/halo:project-structure .

# Test the Docker image
docker run -it --rm -p8090:8090 johnniang/halo:project-structure
```

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

Fixes https://github.com/halo-dev/halo/issues/2730

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?

```release-note
重构项目结构
```
pull/3506/head^2
John Niang 2023-03-23 16:02:33 +08:00 committed by GitHub
parent 7ca5270238
commit c400c85922
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
715 changed files with 596 additions and 466 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
console
.github
.git

12
.gitignore vendored
View File

@ -5,8 +5,8 @@ logs/
### Gradle
.gradle
/build/
/out/
build/
out/
!gradle/wrapper/gradle-wrapper.jar
bin/
@ -71,7 +71,7 @@ application-local.yaml
application-local.properties
### Zip file for test
!src/test/resources/themes/*.zip
!src/main/resources/themes/*.zip
src/main/resources/console/
src/main/resources/presets/
!application/src/test/resources/themes/*.zip
!application/src/main/resources/themes/*.zip
application/src/main/resources/console/
application/src/main/resources/presets/

View File

@ -1,6 +1,6 @@
FROM eclipse-temurin:17-jre as builder
WORKDIR application
ARG JAR_FILE=build/libs/halo-*.jar
ARG JAR_FILE=application/build/libs/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

81
api/build.gradle Normal file
View File

@ -0,0 +1,81 @@
plugins {
id 'java-library'
id 'maven-publish'
id "io.freefair.lombok" version "8.0.0-rc2"
}
group = 'run.halo.app'
description = 'API of halo project, connecting by other projects.'
repositories {
mavenCentral()
}
dependencies {
api platform(project(':platform:application'))
api 'org.springframework.boot:spring-boot-starter-actuator'
api 'org.springframework.boot:spring-boot-starter-data-jpa'
api 'org.springframework.boot:spring-boot-starter-mail'
api 'org.springframework.boot:spring-boot-starter-thymeleaf'
api 'org.springframework.boot:spring-boot-starter-webflux'
api 'org.springframework.boot:spring-boot-starter-validation'
api 'org.springframework.boot:spring-boot-starter-data-r2dbc'
// Spring Security
api 'org.springframework.boot:spring-boot-starter-security'
api 'org.springframework.security:spring-security-oauth2-jose'
api 'org.springframework.security:spring-security-oauth2-client'
api 'org.springframework.security:spring-security-oauth2-resource-server'
api "org.springdoc:springdoc-openapi-starter-webflux-ui"
api 'org.openapi4j:openapi-schema-validator'
api "net.bytebuddy:byte-buddy"
// Apache Lucene
api "org.apache.lucene:lucene-core"
api "org.apache.lucene:lucene-queryparser"
api "org.apache.lucene:lucene-highlighter"
api "org.apache.lucene:lucene-backward-codecs"
api 'cn.shenyanchao.ik-analyzer:ik-analyzer'
api "org.apache.commons:commons-lang3"
api "io.seruco.encoding:base62"
api "org.pf4j:pf4j"
api "com.google.guava:guava"
api "org.jsoup:jsoup"
api "io.github.java-diff-utils:java-diff-utils"
api "org.springframework.integration:spring-integration-core"
api "com.github.java-json-tools:json-patch"
api "org.thymeleaf.extras:thymeleaf-extras-springsecurity6"
runtimeOnly 'io.r2dbc:r2dbc-h2'
runtimeOnly 'org.postgresql:postgresql'
runtimeOnly 'org.postgresql:r2dbc-postgresql'
runtimeOnly 'org.mariadb:r2dbc-mariadb'
runtimeOnly 'com.github.jasync-sql:jasync-r2dbc-mysql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'io.projectreactor:reactor-test'
}
java {
withSourcesJar()
withJavadocJar()
}
tasks.named('test') {
useJUnitPlatform()
}
publishing {
publications {
library(MavenPublication) {
from components.java
}
}
repositories {
mavenLocal()
}
}

View File

@ -2,7 +2,7 @@ package run.halo.app.content.comment;
import org.pf4j.ExtensionPoint;
import reactor.core.publisher.Mono;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.Extension;
import run.halo.app.extension.Ref;
/**
@ -11,7 +11,7 @@ import run.halo.app.extension.Ref;
* @author guqing
* @since 2.0.0
*/
public interface CommentSubject<T extends AbstractExtension> extends ExtensionPoint {
public interface CommentSubject<T extends Extension> extends ExtensionPoint {
Mono<T> get(String name);

View File

@ -0,0 +1,41 @@
package run.halo.app.core.extension;
import lombok.Data;
import lombok.EqualsAndHashCode;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.Metadata;
/**
* A counter for number of requests by extension resource name.
*
* @author guqing
* @since 2.0.0
*/
@Data
@GVK(group = "metrics.halo.run", version = "v1alpha1", kind = "Counter", plural = "counters",
singular = "counter")
@EqualsAndHashCode(callSuper = true)
public class Counter extends AbstractExtension {
private Integer visit;
private Integer upvote;
private Integer downvote;
private Integer totalComment;
private Integer approvedComment;
public static Counter emptyCounter(String name) {
Counter counter = new Counter();
counter.setMetadata(new Metadata());
counter.getMetadata().setName(name);
counter.setUpvote(0);
counter.setTotalComment(0);
counter.setApprovedComment(0);
counter.setVisit(0);
return counter;
}
}

View File

@ -13,10 +13,10 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
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.GroupVersionKind;
import run.halo.app.extension.MetadataOperator;
import run.halo.app.extension.MetadataUtil;
import run.halo.app.infra.ConditionList;
/**
@ -255,11 +255,6 @@ public class Post extends AbstractExtension {
return this;
}
/**
* Build compact post.
*
* @return a compact post
*/
public CompactPost build() {
CompactPost compactPost = new CompactPost();
compactPost.setName(name);
@ -271,7 +266,7 @@ public class Post extends AbstractExtension {
}
public static void changePublishedState(Post post, boolean value) {
Map<String, String> labels = ExtensionUtil.nullSafeLabels(post);
Map<String, String> labels = MetadataUtil.nullSafeLabels(post);
labels.put(PUBLISHED_LABEL, String.valueOf(value));
}
}

View File

@ -9,9 +9,9 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
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.GroupVersionKind;
import run.halo.app.extension.MetadataUtil;
/**
* <p>Single page extension.</p>
@ -112,7 +112,7 @@ public class SinglePage extends AbstractExtension {
}
public static void changePublishedState(SinglePage page, boolean value) {
Map<String, String> labels = ExtensionUtil.nullSafeLabels(page);
Map<String, String> labels = MetadataUtil.nullSafeLabels(page);
labels.put(PUBLISHED_LABEL, String.valueOf(value));
}
}

View File

@ -1,6 +1,5 @@
package run.halo.app.core.extension.content;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.LinkedHashSet;
@ -8,10 +7,7 @@ import java.util.Set;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import run.halo.app.content.ContentWrapper;
import run.halo.app.content.PatchUtils;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import run.halo.app.extension.Ref;
@ -69,27 +65,4 @@ public class Snapshot extends AbstractExtension {
contributors.add(name);
}
@JsonIgnore
public ContentWrapper applyPatch(Snapshot baseSnapshot) {
Assert.notNull(baseSnapshot, "The baseSnapshot must not be null.");
String baseSnapshotName = baseSnapshot.getMetadata().getName();
if (StringUtils.equals(getMetadata().getName(), baseSnapshotName)) {
return ContentWrapper.builder()
.snapshotName(this.getMetadata().getName())
.raw(this.spec.rawPatch)
.content(this.spec.contentPatch)
.rawType(this.spec.rawType)
.build();
}
String patchedContent =
PatchUtils.applyPatch(baseSnapshot.getSpec().getContentPatch(), this.spec.contentPatch);
String patchedRaw =
PatchUtils.applyPatch(baseSnapshot.getSpec().getRawPatch(), this.spec.rawPatch);
return ContentWrapper.builder()
.snapshotName(this.getMetadata().getName())
.raw(patchedRaw)
.content(patchedContent)
.rawType(this.spec.rawType)
.build();
}
}

View File

@ -8,20 +8,21 @@ import java.util.function.Predicate;
/**
* ExtensionClient is an interface which contains some operations on Extension instead of
* ExtensionStore.
* <br/><br/>
* Please note that this client can only use in non-reactive environment. If you want to
* use Extension client in reactive environment, please use {@link ReactiveExtensionClient} instead.
*
* @author johnniang
* @apiNote Please note that this client can only use in non-reactive environment. If you want to
* use Extension client in reactive environment, please use {@link ReactiveExtensionClient} instead.
*/
public interface ExtensionClient {
/**
* Lists Extensions by Extension type, filter and sorter.
*
* @param type is the class type of Extension.
* @param predicate filters the reEnqueue.
* @param type is the class type of Extension.
* @param predicate filters the reEnqueue.
* @param comparator sorts the reEnqueue.
* @param <E> is Extension type.
* @param <E> is Extension type.
* @return all filtered and sorted Extensions.
*/
<E extends Extension> List<E> list(Class<E> type, Predicate<E> predicate,
@ -30,12 +31,12 @@ public interface ExtensionClient {
/**
* Lists Extensions by Extension type, filter, sorter and page info.
*
* @param type is the class type of Extension.
* @param predicate filters the reEnqueue.
* @param type is the class type of Extension.
* @param predicate filters the reEnqueue.
* @param comparator sorts the reEnqueue.
* @param page is page number which starts from 0.
* @param size is page size.
* @param <E> is Extension type.
* @param page is page number which starts from 0.
* @param size is page size.
* @param <E> is Extension type.
* @return a list of Extensions.
*/
<E extends Extension> ListResult<E> list(Class<E> type, Predicate<E> predicate,
@ -46,7 +47,7 @@ public interface ExtensionClient {
*
* @param type is Extension type.
* @param name is Extension name.
* @param <E> is Extension type.
* @param <E> is Extension type.
* @return an optional Extension.
*/
<E extends Extension> Optional<E> fetch(Class<E> type, String name);
@ -58,8 +59,8 @@ public interface ExtensionClient {
* Creates an Extension.
*
* @param extension is fresh Extension to be created. Please make sure the Extension name does
* not exist.
* @param <E> is Extension type.
* not exist.
* @param <E> is Extension type.
*/
<E extends Extension> void create(E extension);
@ -67,8 +68,8 @@ public interface ExtensionClient {
* Updates an Extension.
*
* @param extension is an Extension to be updated. Please make sure the resource version is
* latest.
* @param <E> is Extension type.
* latest.
* @param <E> is Extension type.
*/
<E extends Extension> void update(E extension);
@ -76,8 +77,8 @@ public interface ExtensionClient {
* Deletes an Extension.
*
* @param extension is an Extension to be deleted. Please make sure the resource version is
* latest.
* @param <E> is Extension type.
* latest.
* @param <E> is Extension type.
*/
<E extends Extension> void delete(E extension);

View File

@ -1,22 +1,21 @@
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;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;
import lombok.Data;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.type.TypeDescription;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
import run.halo.app.infra.utils.GenericClassUtils;
@Data
public class ListResult<T> implements Streamable<T> {
public class ListResult<T> implements Iterable<T>, Supplier<Stream<T>> {
@Schema(description = "Page number, starts from 1. If not set or equal to 0, it means no "
+ "pagination.", required = true)
@ -83,12 +82,6 @@ public class ListResult<T> implements Streamable<T> {
return items.iterator();
}
@Override
@JsonIgnore
public boolean isEmpty() {
return Streamable.super.isEmpty();
}
@Schema(description = "Indicates total pages.", required = true)
@JsonProperty("totalPages")
public long getTotalPages() {
@ -122,7 +115,7 @@ public class ListResult<T> implements Streamable<T> {
* @return generic ListResult class.
*/
public static <T> Class<?> generateGenericClass(Class<T> type) {
return generateConcreteClass(ListResult.class, type,
return GenericClassUtils.generateConcreteClass(ListResult.class, type,
() -> type.getSimpleName() + "List");
}
@ -146,4 +139,9 @@ public class ListResult<T> implements Streamable<T> {
}
return listSort;
}
@Override
public Stream<T> get() {
return items.stream();
}
}

View File

@ -3,44 +3,9 @@ package run.halo.app.extension;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Extension utilities.
*
* @author johnniang
*/
public final class ExtensionUtil {
private ExtensionUtil() {
}
/**
* Builds the name prefix of ExtensionStore.
*
* @param scheme is scheme of an Extension.
* @return name prefix of ExtensionStore.
*/
public static String buildStoreNamePrefix(Scheme scheme) {
// rule of key: /registry/[group]/plural-name/extension-name
StringBuilder builder = new StringBuilder("/registry/");
if (StringUtils.hasText(scheme.groupVersionKind().group())) {
builder.append(scheme.groupVersionKind().group()).append('/');
}
builder.append(scheme.plural());
return builder.toString();
}
/**
* Builds full name of ExtensionStore.
*
* @param scheme is scheme of an Extension.
* @param name the exact name of Extension.
* @return full name of ExtensionStore.
*/
public static String buildStoreName(Scheme scheme, String name) {
return buildStoreNamePrefix(scheme) + "/" + name;
}
public enum MetadataUtil {
;
/**
* Gets extension metadata labels null safe.

View File

@ -1,10 +1,9 @@
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 net.bytebuddy.description.type.TypeDescription;
import reactor.core.Exceptions;
public enum GenericClassUtils {
@ -34,7 +33,8 @@ public enum GenericClassUtils {
*/
public static <T> Class<?> generateConcreteClass(Class<?> rawClass, Class<T> parameterType,
Supplier<String> nameGenerator) {
var concreteType = parameterizedType(rawClass, parameterType).build();
var concreteType =
TypeDescription.Generic.Builder.parameterizedType(rawClass, parameterType).build();
try (var unloaded = new ByteBuddy()
.subclass(concreteType)
.name(nameGenerator.get())

View File

@ -52,10 +52,11 @@ public class PathUtils {
/**
* Combine paths based on the passed in path segments parameters.
* <br/><br/>
* This method doesn't work for Windows system currently.
*
* @param pathSegments Path segments to be combined
* @return the combined path
* @apiNote This method doesn't work for Windows system currently.
*/
public static String combinePath(String... pathSegments) {
StringBuilder sb = new StringBuilder();

View File

@ -17,6 +17,7 @@ public class BasePlugin extends Plugin {
public BasePlugin(PluginWrapper wrapper) {
super(wrapper);
log.info("Initialized plugin {}", wrapper.getPluginId());
}
private PluginManager getPluginManager() {

View File

@ -0,0 +1,15 @@
package run.halo.app.plugin;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Map;
import java.util.Optional;
public interface SettingFetcher {
<T> Optional<T> fetch(String group, Class<T> clazz);
JsonNode get(String group);
Map<String, JsonNode> getValues();
}

View File

@ -2,7 +2,6 @@ package run.halo.app.search.post;
import java.time.Instant;
import org.springframework.util.Assert;
import run.halo.app.theme.finders.vo.PostVo;
public record PostDoc(String name,
String title,
@ -20,15 +19,4 @@ public record PostDoc(String name,
Assert.notNull(publishTimestamp, "PublishTimestamp must not be null");
}
// TODO Move this static method to other place.
public static PostDoc from(PostVo postVo) {
return new PostDoc(
postVo.getMetadata().getName(),
postVo.getSpec().getTitle(),
postVo.getStatus().getExcerpt(),
postVo.getContent().getContent(),
postVo.getSpec().getPublishTime(),
postVo.getStatus().getPermalink()
);
}
}

View File

@ -7,7 +7,6 @@ import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static run.halo.app.extension.FakeExtension.createFake;
import java.util.List;
import java.util.function.Predicate;
@ -46,7 +45,7 @@ class RequestSynchronizerTest {
@Test
void shouldStartCorrectlyWhenSyncingAllOnStart() {
when(client.list(same(FakeExtension.class), any(), any())).thenReturn(
List.of(createFake("fake-01"), createFake("fake-02")));
List.of(FakeExtension.createFake("fake-01"), FakeExtension.createFake("fake-02")));
synchronizer.start();

98
application/build.gradle Normal file
View File

@ -0,0 +1,98 @@
plugins {
id 'org.springframework.boot' version '3.0.4'
id 'io.spring.dependency-management' version '1.1.0'
id "com.gorylenko.gradle-git-properties" version "2.3.2"
id "checkstyle"
id 'java'
id 'jacoco'
id "de.undercouch.download" version "5.3.1"
id "io.freefair.lombok" version "8.0.0-rc2"
}
group = "run.halo.app"
sourceCompatibility = JavaVersion.VERSION_17
checkstyle {
toolVersion = "9.3"
showViolations = false
ignoreFailures = false
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
mavenLocal()
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
springBoot {
buildInfo()
}
bootJar {
manifest {
attributes "Implementation-Title": "Halo Application",
"Implementation-Version": archiveVersion
}
}
tasks.named('jar') {
enabled = false
}
ext {
commonsLang3 = "3.12.0"
base62 = "0.1.3"
pf4j = '3.9.0'
javaDiffUtils = "4.12"
guava = "31.1-jre"
jsoup = '1.15.3'
jsonPatch = "1.13"
springDocOpenAPI = "2.0.2"
lucene = "9.5.0"
}
dependencies {
implementation project(':api')
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
annotationProcessor "org.springframework:spring-context-indexer"
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'io.projectreactor:reactor-test'
}
tasks.named('test') {
useJUnitPlatform()
finalizedBy jacocoTestReport
}
tasks.named('jacocoTestReport') {
reports {
xml.required = true
html.required = false
}
}
tasks.register('downloadPluginPresets', Download) {
doFirst {
delete 'src/main/resources/presets/plugins'
}
src([
'https://github.com/halo-sigs/plugin-comment-widget/releases/download/v1.3.0/plugin-comment-widget-1.3.0.jar',
'https://github.com/halo-sigs/plugin-search-widget/releases/download/v1.0.0/plugin-search-widget-1.0.0.jar',
'https://github.com/halo-sigs/plugin-sitemap/releases/download/v1.0.1/plugin-sitemap-1.0.1.jar',
'https://github.com/halo-sigs/plugin-feed/releases/download/v1.1.0-beta.1/plugin-feed-1.1.0-beta.1.jar'
])
dest 'src/main/resources/presets/plugins'
}

Some files were not shown because too many files have changed in this diff Show More