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 dependencies
pull/2144/head
guqing 2022-06-07 14:32:12 +08:00 committed by GitHub
parent 1024f71635
commit b3b13bc820
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 392 additions and 1 deletions

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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