mirror of https://github.com/halo-dev/halo
feat: add yaml descriptor finder for plugin (#2132)
* feat: add yaml plugin descriptor finder * refactor: json assert * refactor: plugin descriptor finder * refactor: yaml plguin finder * refactor: plugin dependenciespull/2144/head
parent
1024f71635
commit
b3b13bc820
|
@ -12,7 +12,7 @@ import org.pf4j.PluginWrapper;
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class BasePlugin extends Plugin {
|
public class BasePlugin extends Plugin {
|
||||||
|
|
||||||
private PluginApplicationContext applicationContext;
|
private PluginApplicationContext applicationContext;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.ToString;
|
||||||
|
import run.halo.app.extension.AbstractExtension;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom resource for Plugin.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class Plugin extends AbstractExtension {
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private PluginSpec spec;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class PluginSpec {
|
||||||
|
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
private String author;
|
||||||
|
|
||||||
|
private String logo;
|
||||||
|
|
||||||
|
private Map<String, String> pluginDependencies = new HashMap<>(4);
|
||||||
|
|
||||||
|
private String homepage;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private String license;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SemVer format.
|
||||||
|
*/
|
||||||
|
private String requires = "*";
|
||||||
|
|
||||||
|
private String pluginClass = BasePlugin.class.getName();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.pf4j.DefaultPluginDescriptor;
|
||||||
|
import org.pf4j.PluginDependency;
|
||||||
|
import org.pf4j.PluginDescriptor;
|
||||||
|
import org.pf4j.PluginDescriptorFinder;
|
||||||
|
import org.pf4j.util.FileUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a plugin descriptor for a plugin path.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @see DefaultPluginDescriptor
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class YamlPluginDescriptorFinder implements PluginDescriptorFinder {
|
||||||
|
|
||||||
|
private final YamlPluginFinder yamlPluginFinder;
|
||||||
|
|
||||||
|
public YamlPluginDescriptorFinder() {
|
||||||
|
yamlPluginFinder = new YamlPluginFinder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isApplicable(Path pluginPath) {
|
||||||
|
return Files.exists(pluginPath)
|
||||||
|
&& (Files.isDirectory(pluginPath)
|
||||||
|
|| FileUtils.isJarFile(pluginPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PluginDescriptor find(Path pluginPath) {
|
||||||
|
Plugin plugin = yamlPluginFinder.find(pluginPath);
|
||||||
|
return convert(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultPluginDescriptor convert(Plugin plugin) {
|
||||||
|
String pluginId = plugin.getMetadata().getName();
|
||||||
|
Plugin.PluginSpec spec = plugin.getSpec();
|
||||||
|
DefaultPluginDescriptor defaultPluginDescriptor =
|
||||||
|
new DefaultPluginDescriptor(pluginId,
|
||||||
|
spec.getDescription(),
|
||||||
|
spec.getPluginClass(),
|
||||||
|
spec.getVersion(),
|
||||||
|
spec.getRequires(),
|
||||||
|
spec.getAuthor(),
|
||||||
|
spec.getLicense());
|
||||||
|
// add dependencies
|
||||||
|
spec.getPluginDependencies().forEach((pluginDepName, versionRequire) -> {
|
||||||
|
PluginDependency dependency =
|
||||||
|
new PluginDependency(String.format("%s@%s", pluginDepName, versionRequire));
|
||||||
|
defaultPluginDescriptor.addDependency(dependency);
|
||||||
|
});
|
||||||
|
return defaultPluginDescriptor;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.pf4j.PluginRuntimeException;
|
||||||
|
import org.pf4j.util.FileUtils;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import run.halo.app.extension.Unstructured;
|
||||||
|
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Reading plugin descriptor data from plugin.yaml.</p>
|
||||||
|
* Example:
|
||||||
|
* <pre>
|
||||||
|
* apiVersion: v1alpha1
|
||||||
|
* kind: Plugin
|
||||||
|
* metadata:
|
||||||
|
* name: plugin-1
|
||||||
|
* labels:
|
||||||
|
* extensions.guqing.xyz/category: attachment
|
||||||
|
* spec:
|
||||||
|
* # 'version' is a valid semantic version string (see semver.org).
|
||||||
|
* version: 0.0.1
|
||||||
|
* requires: ">=2.0.0"
|
||||||
|
* author: guqing
|
||||||
|
* logo: example.com/logo.png
|
||||||
|
* pluginClass: xyz.guqing.plugin.potatoes.PotatoesApp
|
||||||
|
* pluginDependencies:
|
||||||
|
* "plugin-2": 1.0.0
|
||||||
|
* # 'homepage' usually links to the GitHub repository of the plugin
|
||||||
|
* homepage: example.com
|
||||||
|
* # 'displayName' explains what the plugin does in only a few words
|
||||||
|
* displayName: "a name to show"
|
||||||
|
* description: "Tell me more about this plugin."
|
||||||
|
* license: MIT
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class YamlPluginFinder {
|
||||||
|
public static final String DEFAULT_PROPERTIES_FILE_NAME = "plugin.yaml";
|
||||||
|
|
||||||
|
private final String propertiesFileName;
|
||||||
|
|
||||||
|
public YamlPluginFinder() {
|
||||||
|
this(DEFAULT_PROPERTIES_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public YamlPluginFinder(String propertiesFileName) {
|
||||||
|
this.propertiesFileName = propertiesFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Plugin find(Path pluginPath) {
|
||||||
|
return readPluginDescriptor(pluginPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Plugin readPluginDescriptor(Path pluginPath) {
|
||||||
|
Path propertiesPath = getManifestPath(pluginPath, propertiesFileName);
|
||||||
|
if (propertiesPath == null) {
|
||||||
|
throw new PluginRuntimeException("Cannot find the plugin manifest path");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Lookup plugin descriptor in '{}'", propertiesPath);
|
||||||
|
if (Files.notExists(propertiesPath)) {
|
||||||
|
throw new PluginRuntimeException("Cannot find '{}' path", propertiesPath);
|
||||||
|
}
|
||||||
|
YamlUnstructuredLoader yamlUnstructuredLoader =
|
||||||
|
new YamlUnstructuredLoader(new FileSystemResource(propertiesPath));
|
||||||
|
List<Unstructured> unstructuredList = yamlUnstructuredLoader.load();
|
||||||
|
if (unstructuredList.size() != 1) {
|
||||||
|
throw new PluginRuntimeException("Unable to find plugin descriptor file '{}'",
|
||||||
|
propertiesFileName);
|
||||||
|
}
|
||||||
|
Unstructured unstructured = unstructuredList.get(0);
|
||||||
|
return Unstructured.OBJECT_MAPPER.convertValue(unstructured,
|
||||||
|
Plugin.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Path getManifestPath(Path pluginPath, String propertiesFileName) {
|
||||||
|
if (Files.isDirectory(pluginPath)) {
|
||||||
|
return pluginPath.resolve(Paths.get(propertiesFileName));
|
||||||
|
} else {
|
||||||
|
// it's a jar file
|
||||||
|
try {
|
||||||
|
return FileUtils.getPath(pluginPath, propertiesFileName);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new PluginRuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.pf4j.PluginDescriptor;
|
||||||
|
import org.skyscreamer.jsonassert.JSONAssert;
|
||||||
|
import org.springframework.util.ResourceUtils;
|
||||||
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link YamlPluginDescriptorFinder}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
class YamlPluginDescriptorFinderTest {
|
||||||
|
|
||||||
|
private YamlPluginDescriptorFinder yamlPluginDescriptorFinder;
|
||||||
|
private Path testPath;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws FileNotFoundException {
|
||||||
|
yamlPluginDescriptorFinder = new YamlPluginDescriptorFinder();
|
||||||
|
File file = ResourceUtils.getFile("classpath:plugin/plugin.yaml");
|
||||||
|
testPath = file.toPath().getParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isApplicable() throws IOException {
|
||||||
|
// File not exists
|
||||||
|
boolean applicable =
|
||||||
|
yamlPluginDescriptorFinder.isApplicable(Path.of("/some/path/test.jar"));
|
||||||
|
assertThat(applicable).isFalse();
|
||||||
|
|
||||||
|
// jar file is applicable
|
||||||
|
Path tempJarFile = Files.createTempFile("test", ".jar");
|
||||||
|
applicable =
|
||||||
|
yamlPluginDescriptorFinder.isApplicable(tempJarFile);
|
||||||
|
assertThat(applicable).isTrue();
|
||||||
|
|
||||||
|
// zip file is not applicable
|
||||||
|
Path tempZipFile = Files.createTempFile("test", ".zip");
|
||||||
|
applicable =
|
||||||
|
yamlPluginDescriptorFinder.isApplicable(tempZipFile);
|
||||||
|
assertThat(applicable).isFalse();
|
||||||
|
|
||||||
|
// directory is applicable
|
||||||
|
applicable =
|
||||||
|
yamlPluginDescriptorFinder.isApplicable(tempJarFile.getParent());
|
||||||
|
assertThat(applicable).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void find() throws JsonProcessingException, JSONException {
|
||||||
|
PluginDescriptor pluginDescriptor = yamlPluginDescriptorFinder.find(testPath);
|
||||||
|
String actual = JsonUtils.objectToJson(pluginDescriptor);
|
||||||
|
JSONAssert.assertEquals("""
|
||||||
|
{
|
||||||
|
"pluginId": "plugin-1",
|
||||||
|
"pluginDescription": "Tell me more about this plugin.",
|
||||||
|
"pluginClass": "run.halo.app.plugin.BasePlugin",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"requires": ">=2.0.0",
|
||||||
|
"provider": "guqing",
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"pluginId": "banana",
|
||||||
|
"pluginVersionSupport": "0.0.1",
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
actual,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.pf4j.PluginRuntimeException;
|
||||||
|
import org.skyscreamer.jsonassert.JSONAssert;
|
||||||
|
import org.springframework.util.ResourceUtils;
|
||||||
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link YamlPluginDescriptorFinder}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
class YamlPluginFinderTest {
|
||||||
|
private YamlPluginFinder pluginFinder;
|
||||||
|
private Path testPath;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws FileNotFoundException {
|
||||||
|
pluginFinder = new YamlPluginFinder();
|
||||||
|
File file = ResourceUtils.getFile("classpath:plugin/plugin.yaml");
|
||||||
|
testPath = file.toPath().getParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findTest() throws JsonProcessingException, JSONException {
|
||||||
|
Plugin plugin = pluginFinder.find(testPath);
|
||||||
|
assertThat(plugin).isNotNull();
|
||||||
|
JSONAssert.assertEquals("""
|
||||||
|
{
|
||||||
|
"spec": {
|
||||||
|
"displayName": "a name to show",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"author": "guqing",
|
||||||
|
"logo": "https://guqing.xyz/avatar",
|
||||||
|
"pluginDependencies": {
|
||||||
|
"banana": "0.0.1"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/guqing/halo-plugin-1",
|
||||||
|
"description": "Tell me more about this plugin.",
|
||||||
|
"license": "MIT",
|
||||||
|
"requires": ">=2.0.0",
|
||||||
|
"pluginClass": "run.halo.app.plugin.BasePlugin"
|
||||||
|
},
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Plugin",
|
||||||
|
"metadata": {
|
||||||
|
"name": "plugin-1",
|
||||||
|
"labels": null,
|
||||||
|
"annotations": null,
|
||||||
|
"version": null,
|
||||||
|
"creationTimestamp": null,
|
||||||
|
"deletionTimestamp": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
JsonUtils.objectToJson(plugin),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findFailedWhenFileNotFound() {
|
||||||
|
Path test = Paths.get("/tmp");
|
||||||
|
assertThatThrownBy(() -> {
|
||||||
|
pluginFinder.find(test);
|
||||||
|
}).isInstanceOf(PluginRuntimeException.class)
|
||||||
|
.hasMessage("Cannot find '/tmp/plugin.yaml' path");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Plugin
|
||||||
|
metadata:
|
||||||
|
name: plugin-1
|
||||||
|
spec:
|
||||||
|
# 'version' is a valid semantic version string (see semver.org).
|
||||||
|
version: 0.0.1
|
||||||
|
requires: ">=2.0.0"
|
||||||
|
author: guqing
|
||||||
|
logo: https://guqing.xyz/avatar
|
||||||
|
pluginDependencies:
|
||||||
|
"banana": "0.0.1"
|
||||||
|
homepage: https://github.com/guqing/halo-plugin-1
|
||||||
|
displayName: "a name to show"
|
||||||
|
description: "Tell me more about this plugin."
|
||||||
|
license: MIT
|
Loading…
Reference in New Issue