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
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class BasePlugin extends Plugin {
|
||||
public class BasePlugin extends Plugin {
|
||||
|
||||
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