feat(系统设置): 插件管理接口实现

--task=1012390 --user=陈建星 系统设置-系统-插件管理-后台 https://www.tapd.cn/55049933/s/1401201
This commit is contained in:
jianxing 2023-08-02 18:14:03 +08:00 committed by fit2-zhao
parent 05d5dec709
commit 7e4182549a
55 changed files with 1291 additions and 367 deletions

View File

@ -2,8 +2,10 @@ package io.metersphere.listener;
import io.metersphere.api.event.APIEventSource;
import io.metersphere.plan.listener.ExecEventListener;
import io.metersphere.sdk.service.PluginLoadService;
import io.metersphere.sdk.util.CommonBeanFactory;
import io.metersphere.sdk.util.LogUtils;
import jakarta.annotation.Resource;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@ -11,6 +13,9 @@ import org.springframework.stereotype.Component;
@Component
public class AppStartListener implements ApplicationRunner {
@Resource
private PluginLoadService pluginLoadService;
@Override
public void run(ApplicationArguments args) throws Exception {
LogUtils.info("================= 应用启动 =================");
@ -32,5 +37,8 @@ public class AppStartListener implements ApplicationRunner {
// 触发事件
apiEventSource.fireEvent("API", "Event after removing the listener test.");
//loadEventSource.fireEvent("LOAD","Event after removing the listener.");
// 加载插件
pluginLoadService.loadPlugins();
}
}

View File

@ -9,26 +9,30 @@ import java.util.Arrays;
import lombok.Data;
@Data
public class PluginFrontScript implements Serializable {
public class PluginScript implements Serializable {
@Schema(title = "插件的ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{plugin_front_script.plugin_id.not_blank}", groups = {Created.class})
@Size(min = 1, max = 50, message = "{plugin_front_script.plugin_id.length_range}", groups = {Created.class, Updated.class})
@NotBlank(message = "{plugin_script.plugin_id.not_blank}", groups = {Created.class})
@Size(min = 1, max = 50, message = "{plugin_script.plugin_id.length_range}", groups = {Created.class, Updated.class})
private String pluginId;
@Schema(title = "插件中对应表单配置的ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{plugin_front_script.script_id.not_blank}", groups = {Created.class})
@Size(min = 1, max = 50, message = "{plugin_front_script.script_id.length_range}", groups = {Created.class, Updated.class})
@NotBlank(message = "{plugin_script.script_id.not_blank}", groups = {Created.class})
@Size(min = 1, max = 50, message = "{plugin_script.script_id.length_range}", groups = {Created.class, Updated.class})
private String scriptId;
@Schema(title = "插件中对应表单配置的名称")
private String name;
@Schema(title = "脚本内容")
private String script;
private byte[] script;
private static final long serialVersionUID = 1L;
public enum Column {
pluginId("plugin_id", "pluginId", "VARCHAR", false),
scriptId("script_id", "scriptId", "VARCHAR", false),
script("script", "script", "LONGVARCHAR", false);
name("name", "name", "VARCHAR", true),
script("script", "script", "LONGVARBINARY", false);
private static final String BEGINNING_DELIMITER = "`";

View File

@ -3,14 +3,14 @@ package io.metersphere.system.domain;
import java.util.ArrayList;
import java.util.List;
public class PluginFrontScriptExample {
public class PluginScriptExample {
protected String orderByClause;
protected boolean distinct;
protected List<Criteria> oredCriteria;
public PluginFrontScriptExample() {
public PluginScriptExample() {
oredCriteria = new ArrayList<Criteria>();
}
@ -243,6 +243,76 @@ public class PluginFrontScriptExample {
addCriterion("script_id not between", value1, value2, "scriptId");
return (Criteria) this;
}
public Criteria andNameIsNull() {
addCriterion("`name` is null");
return (Criteria) this;
}
public Criteria andNameIsNotNull() {
addCriterion("`name` is not null");
return (Criteria) this;
}
public Criteria andNameEqualTo(String value) {
addCriterion("`name` =", value, "name");
return (Criteria) this;
}
public Criteria andNameNotEqualTo(String value) {
addCriterion("`name` <>", value, "name");
return (Criteria) this;
}
public Criteria andNameGreaterThan(String value) {
addCriterion("`name` >", value, "name");
return (Criteria) this;
}
public Criteria andNameGreaterThanOrEqualTo(String value) {
addCriterion("`name` >=", value, "name");
return (Criteria) this;
}
public Criteria andNameLessThan(String value) {
addCriterion("`name` <", value, "name");
return (Criteria) this;
}
public Criteria andNameLessThanOrEqualTo(String value) {
addCriterion("`name` <=", value, "name");
return (Criteria) this;
}
public Criteria andNameLike(String value) {
addCriterion("`name` like", value, "name");
return (Criteria) this;
}
public Criteria andNameNotLike(String value) {
addCriterion("`name` not like", value, "name");
return (Criteria) this;
}
public Criteria andNameIn(List<String> values) {
addCriterion("`name` in", values, "name");
return (Criteria) this;
}
public Criteria andNameNotIn(List<String> values) {
addCriterion("`name` not in", values, "name");
return (Criteria) this;
}
public Criteria andNameBetween(String value1, String value2) {
addCriterion("`name` between", value1, value2, "name");
return (Criteria) this;
}
public Criteria andNameNotBetween(String value1, String value2) {
addCriterion("`name` not between", value1, value2, "name");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {

View File

@ -1,38 +0,0 @@
package io.metersphere.system.mapper;
import io.metersphere.system.domain.PluginFrontScript;
import io.metersphere.system.domain.PluginFrontScriptExample;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface PluginFrontScriptMapper {
long countByExample(PluginFrontScriptExample example);
int deleteByExample(PluginFrontScriptExample example);
int deleteByPrimaryKey(@Param("pluginId") String pluginId, @Param("scriptId") String scriptId);
int insert(PluginFrontScript record);
int insertSelective(PluginFrontScript record);
List<PluginFrontScript> selectByExampleWithBLOBs(PluginFrontScriptExample example);
List<PluginFrontScript> selectByExample(PluginFrontScriptExample example);
PluginFrontScript selectByPrimaryKey(@Param("pluginId") String pluginId, @Param("scriptId") String scriptId);
int updateByExampleSelective(@Param("record") PluginFrontScript record, @Param("example") PluginFrontScriptExample example);
int updateByExampleWithBLOBs(@Param("record") PluginFrontScript record, @Param("example") PluginFrontScriptExample example);
int updateByExample(@Param("record") PluginFrontScript record, @Param("example") PluginFrontScriptExample example);
int updateByPrimaryKeySelective(PluginFrontScript record);
int updateByPrimaryKeyWithBLOBs(PluginFrontScript record);
int batchInsert(@Param("list") List<PluginFrontScript> list);
int batchInsertSelective(@Param("list") List<PluginFrontScript> list, @Param("selective") PluginFrontScript.Column ... selective);
}

View File

@ -0,0 +1,40 @@
package io.metersphere.system.mapper;
import io.metersphere.system.domain.PluginScript;
import io.metersphere.system.domain.PluginScriptExample;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface PluginScriptMapper {
long countByExample(PluginScriptExample example);
int deleteByExample(PluginScriptExample example);
int deleteByPrimaryKey(@Param("pluginId") String pluginId, @Param("scriptId") String scriptId);
int insert(PluginScript record);
int insertSelective(PluginScript record);
List<PluginScript> selectByExampleWithBLOBs(PluginScriptExample example);
List<PluginScript> selectByExample(PluginScriptExample example);
PluginScript selectByPrimaryKey(@Param("pluginId") String pluginId, @Param("scriptId") String scriptId);
int updateByExampleSelective(@Param("record") PluginScript record, @Param("example") PluginScriptExample example);
int updateByExampleWithBLOBs(@Param("record") PluginScript record, @Param("example") PluginScriptExample example);
int updateByExample(@Param("record") PluginScript record, @Param("example") PluginScriptExample example);
int updateByPrimaryKeySelective(PluginScript record);
int updateByPrimaryKeyWithBLOBs(PluginScript record);
int updateByPrimaryKey(PluginScript record);
int batchInsert(@Param("list") List<PluginScript> list);
int batchInsertSelective(@Param("list") List<PluginScript> list, @Param("selective") PluginScript.Column ... selective);
}

View File

@ -1,12 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.metersphere.system.mapper.PluginFrontScriptMapper">
<resultMap id="BaseResultMap" type="io.metersphere.system.domain.PluginFrontScript">
<mapper namespace="io.metersphere.system.mapper.PluginScriptMapper">
<resultMap id="BaseResultMap" type="io.metersphere.system.domain.PluginScript">
<id column="plugin_id" jdbcType="VARCHAR" property="pluginId" />
<id column="script_id" jdbcType="VARCHAR" property="scriptId" />
<result column="name" jdbcType="VARCHAR" property="name" />
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="io.metersphere.system.domain.PluginFrontScript">
<result column="script" jdbcType="LONGVARCHAR" property="script" />
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="io.metersphere.system.domain.PluginScript">
<result column="script" jdbcType="LONGVARBINARY" property="script" />
</resultMap>
<sql id="Example_Where_Clause">
<where>
@ -67,12 +68,12 @@
</where>
</sql>
<sql id="Base_Column_List">
plugin_id, script_id
plugin_id, script_id, `name`
</sql>
<sql id="Blob_Column_List">
script
</sql>
<select id="selectByExampleWithBLOBs" parameterType="io.metersphere.system.domain.PluginFrontScriptExample" resultMap="ResultMapWithBLOBs">
<select id="selectByExampleWithBLOBs" parameterType="io.metersphere.system.domain.PluginScriptExample" resultMap="ResultMapWithBLOBs">
select
<if test="distinct">
distinct
@ -80,7 +81,7 @@
<include refid="Base_Column_List" />
,
<include refid="Blob_Column_List" />
from plugin_front_script
from plugin_script
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
@ -88,13 +89,13 @@
order by ${orderByClause}
</if>
</select>
<select id="selectByExample" parameterType="io.metersphere.system.domain.PluginFrontScriptExample" resultMap="BaseResultMap">
<select id="selectByExample" parameterType="io.metersphere.system.domain.PluginScriptExample" resultMap="BaseResultMap">
select
<if test="distinct">
distinct
</if>
<include refid="Base_Column_List" />
from plugin_front_script
from plugin_script
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
@ -107,29 +108,29 @@
<include refid="Base_Column_List" />
,
<include refid="Blob_Column_List" />
from plugin_front_script
from plugin_script
where plugin_id = #{pluginId,jdbcType=VARCHAR}
and script_id = #{scriptId,jdbcType=VARCHAR}
</select>
<delete id="deleteByPrimaryKey" parameterType="map">
delete from plugin_front_script
delete from plugin_script
where plugin_id = #{pluginId,jdbcType=VARCHAR}
and script_id = #{scriptId,jdbcType=VARCHAR}
</delete>
<delete id="deleteByExample" parameterType="io.metersphere.system.domain.PluginFrontScriptExample">
delete from plugin_front_script
<delete id="deleteByExample" parameterType="io.metersphere.system.domain.PluginScriptExample">
delete from plugin_script
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
</delete>
<insert id="insert" parameterType="io.metersphere.system.domain.PluginFrontScript">
insert into plugin_front_script (plugin_id, script_id, script
)
values (#{pluginId,jdbcType=VARCHAR}, #{scriptId,jdbcType=VARCHAR}, #{script,jdbcType=LONGVARCHAR}
)
<insert id="insert" parameterType="io.metersphere.system.domain.PluginScript">
insert into plugin_script (plugin_id, script_id, `name`,
script)
values (#{pluginId,jdbcType=VARCHAR}, #{scriptId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR},
#{script,jdbcType=LONGVARBINARY})
</insert>
<insert id="insertSelective" parameterType="io.metersphere.system.domain.PluginFrontScript">
insert into plugin_front_script
<insert id="insertSelective" parameterType="io.metersphere.system.domain.PluginScript">
insert into plugin_script
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="pluginId != null">
plugin_id,
@ -137,6 +138,9 @@
<if test="scriptId != null">
script_id,
</if>
<if test="name != null">
`name`,
</if>
<if test="script != null">
script,
</if>
@ -148,19 +152,22 @@
<if test="scriptId != null">
#{scriptId,jdbcType=VARCHAR},
</if>
<if test="name != null">
#{name,jdbcType=VARCHAR},
</if>
<if test="script != null">
#{script,jdbcType=LONGVARCHAR},
#{script,jdbcType=LONGVARBINARY},
</if>
</trim>
</insert>
<select id="countByExample" parameterType="io.metersphere.system.domain.PluginFrontScriptExample" resultType="java.lang.Long">
select count(*) from plugin_front_script
<select id="countByExample" parameterType="io.metersphere.system.domain.PluginScriptExample" resultType="java.lang.Long">
select count(*) from plugin_script
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
</select>
<update id="updateByExampleSelective" parameterType="map">
update plugin_front_script
update plugin_script
<set>
<if test="record.pluginId != null">
plugin_id = #{record.pluginId,jdbcType=VARCHAR},
@ -168,8 +175,11 @@
<if test="record.scriptId != null">
script_id = #{record.scriptId,jdbcType=VARCHAR},
</if>
<if test="record.name != null">
`name` = #{record.name,jdbcType=VARCHAR},
</if>
<if test="record.script != null">
script = #{record.script,jdbcType=LONGVARCHAR},
script = #{record.script,jdbcType=LONGVARBINARY},
</if>
</set>
<if test="_parameter != null">
@ -177,49 +187,61 @@
</if>
</update>
<update id="updateByExampleWithBLOBs" parameterType="map">
update plugin_front_script
update plugin_script
set plugin_id = #{record.pluginId,jdbcType=VARCHAR},
script_id = #{record.scriptId,jdbcType=VARCHAR},
script = #{record.script,jdbcType=LONGVARCHAR}
`name` = #{record.name,jdbcType=VARCHAR},
script = #{record.script,jdbcType=LONGVARBINARY}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
</update>
<update id="updateByExample" parameterType="map">
update plugin_front_script
update plugin_script
set plugin_id = #{record.pluginId,jdbcType=VARCHAR},
script_id = #{record.scriptId,jdbcType=VARCHAR}
script_id = #{record.scriptId,jdbcType=VARCHAR},
`name` = #{record.name,jdbcType=VARCHAR}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
</update>
<update id="updateByPrimaryKeySelective" parameterType="io.metersphere.system.domain.PluginFrontScript">
update plugin_front_script
<update id="updateByPrimaryKeySelective" parameterType="io.metersphere.system.domain.PluginScript">
update plugin_script
<set>
<if test="name != null">
`name` = #{name,jdbcType=VARCHAR},
</if>
<if test="script != null">
script = #{script,jdbcType=LONGVARCHAR},
script = #{script,jdbcType=LONGVARBINARY},
</if>
</set>
where plugin_id = #{pluginId,jdbcType=VARCHAR}
and script_id = #{scriptId,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKeyWithBLOBs" parameterType="io.metersphere.system.domain.PluginFrontScript">
update plugin_front_script
set script = #{script,jdbcType=LONGVARCHAR}
<update id="updateByPrimaryKeyWithBLOBs" parameterType="io.metersphere.system.domain.PluginScript">
update plugin_script
set `name` = #{name,jdbcType=VARCHAR},
script = #{script,jdbcType=LONGVARBINARY}
where plugin_id = #{pluginId,jdbcType=VARCHAR}
and script_id = #{scriptId,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKey" parameterType="io.metersphere.system.domain.PluginScript">
update plugin_script
set `name` = #{name,jdbcType=VARCHAR}
where plugin_id = #{pluginId,jdbcType=VARCHAR}
and script_id = #{scriptId,jdbcType=VARCHAR}
</update>
<insert id="batchInsert" parameterType="map">
insert into plugin_front_script
(plugin_id, script_id, script)
insert into plugin_script
(plugin_id, script_id, `name`, script)
values
<foreach collection="list" item="item" separator=",">
(#{item.pluginId,jdbcType=VARCHAR}, #{item.scriptId,jdbcType=VARCHAR}, #{item.script,jdbcType=LONGVARCHAR}
)
(#{item.pluginId,jdbcType=VARCHAR}, #{item.scriptId,jdbcType=VARCHAR}, #{item.name,jdbcType=VARCHAR},
#{item.script,jdbcType=LONGVARBINARY})
</foreach>
</insert>
<insert id="batchInsertSelective" parameterType="map">
insert into plugin_front_script (
insert into plugin_script (
<foreach collection="selective" item="column" separator=",">
${column.escapedColumnName}
</foreach>
@ -234,8 +256,11 @@
<if test="'script_id'.toString() == column.value">
#{item.scriptId,jdbcType=VARCHAR}
</if>
<if test="'name'.toString() == column.value">
#{item.name,jdbcType=VARCHAR}
</if>
<if test="'script'.toString() == column.value">
#{item.script,jdbcType=LONGVARCHAR}
#{item.script,jdbcType=LONGVARBINARY}
</if>
</foreach>
)

View File

@ -157,11 +157,12 @@ CREATE TABLE IF NOT EXISTS plugin
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT = '插件';
CREATE TABLE IF NOT EXISTS plugin_front_script
CREATE TABLE IF NOT EXISTS plugin_script
(
`plugin_id` VARCHAR(50) NOT NULL COMMENT '插件的ID' ,
`script_id` VARCHAR(50) NOT NULL COMMENT '插件中对应表单配置的ID' ,
`script` TEXT COMMENT '脚本内容' ,
`name` VARCHAR(255) COMMENT '插件中对应表单配置的名称' ,
`script` LONGBLOB COMMENT '脚本内容' ,
PRIMARY KEY (plugin_id,script_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4

View File

@ -4,10 +4,6 @@ import io.metersphere.plugin.sdk.api.AbstractMsPlugin;
public abstract class AbstractApiPlugin extends AbstractMsPlugin {
private static final String API_PLUGIN_TYPE = "API";
public AbstractApiPlugin(ClassLoader pluginClassLoader) {
super(pluginClassLoader);
}
@Override
public String getType() {
return API_PLUGIN_TYPE;

View File

@ -4,10 +4,6 @@ import io.metersphere.plugin.sdk.api.AbstractMsPlugin;
public abstract class AbstractPlatformPlugin extends AbstractMsPlugin {
private static final String PLATFORM_PLUGIN_TYPE = "PLATFORM";
public AbstractPlatformPlugin(ClassLoader pluginClassLoader) {
super(pluginClassLoader);
}
@Override
public String getType() {
return PLATFORM_PLUGIN_TYPE;

View File

@ -1,75 +1,16 @@
package io.metersphere.plugin.sdk.api;
import io.metersphere.plugin.sdk.util.PluginLogUtils;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractMsPlugin implements MsPlugin {
private static final String SCRIPT_DIR = "script";
private ClassLoader pluginClassLoader;
public AbstractMsPlugin(ClassLoader pluginClassLoader) {
this.pluginClassLoader = pluginClassLoader;
}
protected InputStream readResource(String name) {
return pluginClassLoader.getResourceAsStream(name);
}
/**
* @return 返回该加载前端配置文件的目录默认是 script
* @return 返回默认的前端配置文件的目录
* 可以重写定制
*/
protected String getScriptDir() {
return SCRIPT_DIR;
}
/**
* @return 返回 resources下的 script 下的 json 文件
*/
@Override
public List<String> getFrontendScript() {
List<String> scriptList = new ArrayList<>();
String scriptDirName = getScriptDir();
URL scriptDir = pluginClassLoader.getResource(scriptDirName);
if (scriptDir != null) {
File resourceDir = new File(scriptDir.getFile());
List<String> filePaths = getFilePaths(resourceDir);
for (String filePath : filePaths) {
InputStream in = readResource(scriptDirName + "/" + filePath);
try {
if (in != null) {
scriptList.add(IOUtils.toString(in));
}
} catch (IOException e) {
PluginLogUtils.error(e);
}
}
}
return scriptList;
}
private static List<String> getFilePaths(File directory) {
List<String> filePaths = new ArrayList<>();
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
filePaths.addAll(getFilePaths(file));
} else {
filePaths.add(file.getAbsolutePath());
}
}
}
return filePaths;
public String getScriptDir() {
return SCRIPT_DIR;
}
@Override
@ -79,6 +20,6 @@ public abstract class AbstractMsPlugin implements MsPlugin {
@Override
public String getPluginId() {
return getName().toLowerCase() + "-" + getVersion();
return getKey().toLowerCase() + "-" + getVersion();
}
}

View File

@ -1,7 +1,5 @@
package io.metersphere.plugin.sdk.api;
import java.util.List;
/**
* 插件的基本信息
*
@ -37,14 +35,14 @@ public interface MsPlugin {
*/
String getPluginId();
/**
* @return 返回前端渲染需要的数据
* 默认会返回 resources下的 script 下的 json 文件
*/
List<String> getFrontendScript();
/**
* @return 返回插件的版本
*/
String getVersion();
/**
* @return 返回该加载前端配置文件的目录默认是 script
* 可以重写定制
*/
String getScriptDir();
}

View File

@ -20,6 +20,8 @@ public class SystemInterceptor {
configList.add(new MybatisInterceptorConfig(NoviceStatistics.class, "dataOption", CompressUtils.class, "zip", "unzip"));
configList.add(new MybatisInterceptorConfig(ServiceIntegration.class, "configuration", CompressUtils.class, "zip", "unzip"));
configList.add(new MybatisInterceptorConfig(UserExtend.class, "platformInfo", CompressUtils.class, "zip", "unzip"));
configList.add(new MybatisInterceptorConfig(PluginScript.class, "script", CompressUtils.class, "zip", "unzip"));
configList.add(new MybatisInterceptorConfig(PluginScript.class, "script", CompressUtils.class, "zip", "unzip"));
return configList;
}

View File

@ -0,0 +1,6 @@
package io.metersphere.sdk.constants;
public class KafkaPluginTopicType {
public static final String ADD = "ADD";
public static final String DELETE = "DELETE";
}

View File

@ -0,0 +1,5 @@
package io.metersphere.sdk.constants;
public class KafkaTopicConstants {
public static final String PLUGIN = "PLUGIN";
}

View File

@ -1,5 +1,5 @@
package io.metersphere.sdk.constants;
public enum PluginScenarioType {
PAI, PLATFORM
API, PLATFORM
}

View File

@ -1,20 +0,0 @@
package io.metersphere.sdk.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class OrganizationOptionDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(title = "组织ID")
private String id;
@Schema(title = "组织编号")
private Long num;
@Schema(title = "组织名称")
private String name;
}

View File

@ -11,6 +11,10 @@ public class MSException extends RuntimeException {
super(message);
}
public MSException(Throwable t) {
super(t);
}
public MSException(IResultCode errorCode) {
super(StringUtils.EMPTY);
this.errorCode = errorCode;
@ -26,6 +30,10 @@ public class MSException extends RuntimeException {
this.errorCode = errorCode;
}
public MSException(String message, Throwable t) {
super(message, t);
}
public IResultCode getErrorCode() {
return errorCode;
}

View File

@ -2,6 +2,9 @@ package io.metersphere.sdk.file;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.List;
public interface FileRepository {
/**
* 保存文件
@ -11,7 +14,7 @@ public interface FileRepository {
* @return
* @throws Exception
*/
public String saveFile(MultipartFile file, FileRequest request) throws Exception;
String saveFile(MultipartFile file, FileRequest request) throws Exception;
/**
* 保存文件
@ -21,7 +24,7 @@ public interface FileRepository {
* @return
* @throws Exception
*/
public String saveFile(byte[] bytes, FileRequest request) throws Exception;
String saveFile(byte[] bytes, FileRequest request) throws Exception;
/**
* 删除文件
@ -29,7 +32,7 @@ public interface FileRepository {
* @param request
* @throws Exception
*/
public void delete(FileRequest request) throws Exception;
void delete(FileRequest request) throws Exception;
/**
* 删除文件夹
@ -37,7 +40,7 @@ public interface FileRepository {
* @param request
* @throws Exception
*/
public void deleteFolder(FileRequest request) throws Exception;
void deleteFolder(FileRequest request) throws Exception;
/**
@ -47,7 +50,16 @@ public interface FileRepository {
* @return
* @throws Exception
*/
public byte[] getFile(FileRequest request) throws Exception;
byte[] getFile(FileRequest request) throws Exception;
/**
* 获取文件字输入流
*
* @param request
* @return
* @throws Exception
*/
InputStream getFileAsStream(FileRequest request) throws Exception;
/**
* 流式处理方式通过逐块地下载文件
@ -56,6 +68,14 @@ public interface FileRepository {
* @param localPath
* @throws Exception
*/
public void downloadFile(FileRequest request, String localPath) throws Exception;
void downloadFile(FileRequest request, String localPath) throws Exception;
/**
* 获取指定文件夹下的文件名列表
*
* @param request
* @throws Exception
*/
List<String> getFolderFileNames(FileRequest request) throws Exception;
}

View File

@ -24,8 +24,8 @@ public class MinioRepository implements FileRepository {
// 文件存储路径
return StringUtils.join(
request.getProjectId(),
File.separator,
StringUtils.isNotBlank(request.getResourceId()) ? request.getResourceId() + File.separator : StringUtils.EMPTY,
"/",
StringUtils.isNotBlank(request.getResourceId()) ? request.getResourceId() + "/" : StringUtils.EMPTY,
request.getFileName());
}
@ -68,6 +68,11 @@ public class MinioRepository implements FileRepository {
removeObjects(MinioConfig.BUCKET, filePath);
}
@Override
public List<String> getFolderFileNames(FileRequest request) throws Exception {
return listObjects(MinioConfig.BUCKET, getPath(request));
}
private boolean removeObject(String bucketName, String objectName) throws Exception {
client.removeObject(RemoveObjectArgs.builder()
.bucket(bucketName) // 存储桶
@ -128,6 +133,7 @@ public class MinioRepository implements FileRepository {
}
}
@Override
public InputStream getFileAsStream(FileRequest request) throws Exception {
String fileName = getPath(request);
return client.getObject(GetObjectArgs.builder()

View File

@ -49,7 +49,7 @@ public class OperationLogModule {
public static final String UI_AUTOMATION = "UI_AUTOMATION";
public static final String UI_AUTOMATION_REPORT = "UI_AUTOMATION_REPORT";
public static final String UI_AUTOMATION_SCHEDULE = "UI_AUTOMATION_SCHEDULE";
public static final String PLUGIN_MANAGE = "PLUGIN_MANAGE";
public static final String SYSTEM_PLUGIN = "SYSTEM_PLUGIN";
public static final String SYSTEM_PROJECT = "SYSTEM_PROJECT";
public static final String SYSTEM_PROJECT_MEMBER = "SYSTEM_PROJECT_MEMBER";
}

View File

@ -1,6 +1,7 @@
package io.metersphere.sdk.plugin.loader;
import io.metersphere.sdk.plugin.storage.StorageStrategy;
import io.metersphere.sdk.util.LogUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.*;
@ -25,12 +26,12 @@ public class PluginClassLoader extends ClassLoader {
/**
* 保存加载失败的类之后重试
*/
protected Map<String, byteArrayWrapper> loadErrorMap = new HashMap<>();
protected Map<String, ByteArrayWrapper> loadErrorMap = new HashMap<>();
private class byteArrayWrapper {
private class ByteArrayWrapper {
private byte[] values;
public byteArrayWrapper(byte[] values) {
public ByteArrayWrapper(byte[] values) {
this.values = values;
}
@ -49,14 +50,21 @@ public class PluginClassLoader extends ClassLoader {
*/
protected StorageStrategy storageStrategy;
protected boolean isNeedUploadFile;
public PluginClassLoader() {
// 将父加载器设置成当前的类加载器目的是由父加载器加载接口实现类由该加载器加载
super(PluginClassLoader.class.getClassLoader());
}
public PluginClassLoader(StorageStrategy storageStrategy) {
this(storageStrategy, true);
}
public PluginClassLoader(StorageStrategy storageStrategy, boolean isNeedUploadFile) {
this();
this.storageStrategy = storageStrategy;
this.isNeedUploadFile = isNeedUploadFile;
}
public StorageStrategy getStorageStrategy() {
@ -106,7 +114,7 @@ public class PluginClassLoader extends ClassLoader {
*
* @param in
*/
public void loadJar(InputStream in) throws IOException {
public void loadJar(InputStream in) throws Exception {
if (in != null) {
try (JarInputStream jis = new JarInputStream(in)) {
JarEntry je;
@ -132,8 +140,8 @@ public class PluginClassLoader extends ClassLoader {
JarEntry je = en.nextElement();
try (InputStream in = jar.getInputStream(je)) {
loadJar(in, je);
} catch (IOException e) {
// LogUtils.error(e);
} catch (Exception e) {
LogUtils.error(e);
}
}
reloadErrorClazz();
@ -146,7 +154,7 @@ public class PluginClassLoader extends ClassLoader {
* @param je
* @throws IOException
*/
protected void loadJar(InputStream in, JarEntry je) throws IOException {
protected void loadJar(InputStream in, JarEntry je) throws Exception {
je.getName();
String name = je.getName();
if (name.endsWith(".class")) {
@ -167,13 +175,13 @@ public class PluginClassLoader extends ClassLoader {
Class<?> clazz = defineClass(className, bytes, 0, bytes.length);
clazzSet.add(clazz);
} catch (NoClassDefFoundError e) {
loadErrorMap.put(className, new byteArrayWrapper(bytes));
loadErrorMap.put(className, new ByteArrayWrapper(bytes));
} catch (Throwable e) {
// LogUtils.error(e);
LogUtils.error(e);
}
} else if (!name.endsWith("/")) {
// 非目录即静态资源
if (storageStrategy != null) {
if (storageStrategy != null && isNeedUploadFile) {
storageStrategy.store(name, in);
}
}
@ -190,13 +198,13 @@ public class PluginClassLoader extends ClassLoader {
while (iterator.hasNext()) {
String className = iterator.next();
try {
// LogUtils.info("reload class: " + className);
LogUtils.info("reload class: " + className);
byte[] bytes = loadErrorMap.get(className).getValues();
Class<?> clazz = defineClass(className, bytes, 0, bytes.length);
clazzSet.add(clazz);
iterator.remove();
} catch (Throwable e) {
// LogUtils.error(e);
LogUtils.error(e);
}
}
}
@ -212,8 +220,8 @@ public class PluginClassLoader extends ClassLoader {
if (null != storageStrategy) {
try {
return storageStrategy.get(name);
} catch (IOException e) {
// LogUtils.error(e, logger);
} catch (Exception e) {
LogUtils.error(e);
return null;
}
}

View File

@ -55,15 +55,15 @@ public class PluginManager {
*
* @param in
*/
public PluginManager loadJar(String pluginId, InputStream in, StorageStrategy storageStrategy) throws IOException {
PluginClassLoader pluginClassLoader = new PluginClassLoader(storageStrategy);
public PluginManager loadJar(String pluginId, InputStream in, StorageStrategy storageStrategy, boolean isNeedUploadFile) throws Exception {
PluginClassLoader pluginClassLoader = new PluginClassLoader(storageStrategy, isNeedUploadFile);
classLoaderMap.put(pluginId, pluginClassLoader);
pluginClassLoader.loadJar(in);
return this;
}
public PluginManager loadJar(String pluginId, InputStream in) throws IOException {
return this.loadJar(pluginId, in, null);
public PluginManager loadJar(String pluginId, InputStream in, boolean isNeedUploadFile) throws Exception {
return this.loadJar(pluginId, in, null, isNeedUploadFile);
}
/**
@ -87,6 +87,9 @@ public class PluginManager {
public <T> T getImplInstance(String pluginId, Class<T> superClazz) {
try {
Class<T> clazz = getImplClass(pluginId, superClazz);
if (clazz == null) {
throw new MSException("未找到插件实现类");
}
return clazz.getConstructor().newInstance();
} catch (InvocationTargetException e) {
LogUtils.error(e);

View File

@ -1,35 +0,0 @@
package io.metersphere.sdk.plugin.storage;
import java.io.IOException;
import java.io.InputStream;
/**
* jar包静态资源存储策略存储在 Minio
*/
public class MinioStorageStrategy implements StorageStrategy {
private String pluginId;
public static final String DIR_PATH = "system/plugin";
public MinioStorageStrategy(String pluginId) {
this.pluginId = pluginId;
}
@Override
public String store(String name, InputStream in) throws IOException {
// todo 上传到 minio
return null;
}
@Override
public InputStream get(String name) {
// todo 获取文件
return null;
}
@Override
public void delete() throws IOException {
// todo 删除文件
}
}

View File

@ -0,0 +1,64 @@
package io.metersphere.sdk.plugin.storage;
import io.metersphere.sdk.file.FileCenter;
import io.metersphere.sdk.file.FileRepository;
import io.metersphere.sdk.file.FileRequest;
import org.apache.commons.lang3.StringUtils;
import java.io.InputStream;
import java.util.List;
/**
* jar包静态资源存储策略
* @author jianxing
*/
public class MsStorageStrategy implements StorageStrategy {
private final FileRepository fileRepository;
private final String pluginId;
public static final String DIR_PATH = "system/plugin";
public MsStorageStrategy(String pluginId) {
this.pluginId = pluginId;
fileRepository = FileCenter.getDefaultRepository();
}
@Override
public String store(String name, InputStream in) throws Exception {
FileRequest request = getFileRequest(name);
return fileRepository.saveFile(in.readAllBytes(), request);
}
@Override
public InputStream get(String name) throws Exception {
FileRequest request = getFileRequest(name);
return fileRepository.getFileAsStream(request);
}
@Override
public List<String> getFolderFileNames(String dirName) throws Exception {
FileRequest request = getFileRequest(dirName);
List<String> fileNames = fileRepository.getFolderFileNames(request);
return fileNames.stream().map(s -> s.replace(getPluginDir(), StringUtils.EMPTY)).toList();
}
@Override
public void delete() throws Exception {
FileRequest request = new FileRequest();
request.setProjectId(getPluginDir());
fileRepository.deleteFolder(request);
}
private FileRequest getFileRequest(String name) {
FileRequest request = new FileRequest();
request.setProjectId(getPluginDir());
request.setFileName(name);
return request;
}
private String getPluginDir() {
return DIR_PATH + "/" + this.pluginId;
}
}

View File

@ -2,9 +2,11 @@ package io.metersphere.sdk.plugin.storage;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* jar包图片前端配置文件等静态资源存储策略
* @author jianxing
*/
public interface StorageStrategy {
@ -15,7 +17,7 @@ public interface StorageStrategy {
* @return
* @throws IOException
*/
String store(String name, InputStream in) throws IOException;
String store(String name, InputStream in) throws Exception;
/**
* 获取文件
@ -23,12 +25,19 @@ public interface StorageStrategy {
* @return
* @throws IOException
*/
InputStream get(String path) throws IOException;
InputStream get(String path) throws Exception;
/**
* 获取指定文件夹下的文件名列表
*
* @param dirName
* @throws Exception
*/
List<String> getFolderFileNames(String dirName) throws Exception;
/**
* 删除文件
* @throws IOException
*/
void delete() throws IOException;
void delete() throws Exception;
}

View File

@ -0,0 +1,190 @@
package io.metersphere.sdk.service;
import io.metersphere.plugin.sdk.api.MsPlugin;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.plugin.loader.PluginClassLoader;
import io.metersphere.sdk.plugin.loader.PluginManager;
import io.metersphere.sdk.plugin.storage.MsStorageStrategy;
import io.metersphere.sdk.plugin.storage.StorageStrategy;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.domain.Plugin;
import io.metersphere.system.domain.PluginExample;
import io.metersphere.system.mapper.PluginMapper;
import jakarta.annotation.Resource;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* @author jianxing
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class PluginLoadService {
private final PluginManager pluginManager = new PluginManager();
@Resource
private PluginMapper pluginMapper;
/**
* 上传插件到 minio
*/
public void uploadPlugin(String id, MultipartFile file) {
try {
getStorageStrategy(id).store(file.getOriginalFilename(), file.getInputStream());
} catch (Exception e) {
LogUtils.error(e);
throw new MSException("文件上传异常", e);
}
}
/**
* @return 返回前端渲染需要的数据
* 默认会返回 resources下的 script 下的 json 文件
*/
public List<String> getFrontendScripts(String pluginId) {
MsPlugin msPluginInstance = getMsPluginInstance(pluginId);
String scriptDir = msPluginInstance.getScriptDir();
StorageStrategy storageStrategy = pluginManager.getClassLoader(pluginId).getStorageStrategy();
try {
// 查询脚本文件名
List<String> folderFileNames = storageStrategy.getFolderFileNames(scriptDir);
// 获取脚本内容
List<String> scripts = new ArrayList<>(folderFileNames.size());
for (String folderFileName : folderFileNames) {
InputStream in = storageStrategy.get(folderFileName);
if (in == null) {
continue;
}
scripts.add(IOUtil.toString(storageStrategy.get(folderFileName)));
}
return scripts;
} catch (Exception e) {
LogUtils.error(e);
throw new MSException("获取脚本异常", e);
}
}
private static StorageStrategy getStorageStrategy(String id) {
return new MsStorageStrategy(id);
}
public void loadPlugin(String id, MultipartFile file) {
// 加载 jar
InputStream inputStream;
try {
inputStream = file.getInputStream();
} catch (IOException e) {
LogUtils.error(e);
throw new MSException("获取文件输入流异常", e);
}
loadPlugin(id, inputStream, true);
}
public void loadPlugin(String pluginId, String fileName) {
PluginClassLoader classLoader = pluginManager.getClassLoader(pluginId);
if (classLoader != null) {
return;
}
// 加载 jar
InputStream inputStream;
try {
inputStream = classLoader.getStorageStrategy().get(fileName);
} catch (Exception e) {
LogUtils.error(e);
throw new MSException("下载文件异常", e);
}
loadPlugin(pluginId, inputStream, false);
}
public void loadPlugin(String id, InputStream inputStream, boolean isNeedUploadFile) {
if (inputStream == null) {
return;
}
loadPlugin(id, inputStream, new MsStorageStrategy(id), isNeedUploadFile);
}
/**
* 加载插件
*
* @param id 插件ID
* @param inputStream 输入流
* @param storageStrategy 静态文件及jar包存储策略
*/
public void loadPlugin(String id, InputStream inputStream, StorageStrategy storageStrategy, boolean isNeedUploadFile) {
if (inputStream == null || pluginManager.getClassLoader(id) != null) {
return;
}
// 加载 jar
try {
pluginManager.loadJar(id, inputStream,
storageStrategy == null ? getStorageStrategy(id) : storageStrategy, isNeedUploadFile);
} catch (Exception e) {
LogUtils.error(e);
throw new MSException("加载插件异常", e);
}
}
/**
* 项目启动时加载插件
*/
public synchronized void loadPlugins() {
List<Plugin> plugins = pluginMapper.selectByExample(new PluginExample());
plugins.forEach(plugin -> {
String id = plugin.getId();
StorageStrategy storageStrategy = getStorageStrategy(id);
try {
InputStream inputStream = storageStrategy.get(plugin.getFileName());
loadPlugin(id, inputStream, storageStrategy, false);
} catch (Exception e) {
LogUtils.error("初始化插件异常" + plugin.getFileName(), e);
}
});
}
/**
* 卸载插件
*/
public void unloadPlugin(String pluginId) {
pluginManager.deletePlugin(pluginId);
}
/**
* 删除插件
*/
public void deletePlugin(String pluginId) {
// 删除文件
PluginClassLoader classLoader = pluginManager.getClassLoader(pluginId);
try {
if (classLoader != null) {
classLoader.getStorageStrategy().delete();
}
} catch (Exception e) {
LogUtils.error(e);
throw new MSException("删除插件异常 ", e);
}
unloadPlugin(pluginId);
}
public MsPlugin getMsPluginInstance(String id) {
return pluginManager.getImplInstance(id, MsPlugin.class);
}
public boolean hasPluginKey(String currentPluginId, String pluginKey) {
for (String pluginId : pluginManager.getClassLoaderMap().keySet()) {
MsPlugin msPlugin = getMsPluginInstance(pluginId);
if (!StringUtils.equals(currentPluginId, pluginId) && StringUtils.equals(msPlugin.getKey(), pluginKey)) {
return true;
}
}
return false;
}
}

View File

@ -168,7 +168,10 @@ permission.system_plugin.read=READ
permission.system_plugin.add=CREATE
permission.system_plugin.edit=UPDATE
permission.system_plugin.delete=DELETE
plugin.exist=plugin name or filename already exists
plugin.type.exist=plugin type already exists
plugin.script.exist=duplicate script id
plugin.script.format=malformed script

View File

@ -166,4 +166,8 @@ permission.system_plugin.name=插件
permission.system_plugin.read=查看插件
permission.system_plugin.add=创建插件
permission.system_plugin.edit=更新插件
permission.system_plugin.delete=删除插件
permission.system_plugin.delete=删除插件
plugin.exist=插件名称或文件名已存在
plugin.type.exist=插件类型已存在
plugin.script.exist=脚本id重复
plugin.script.format=脚本格式错误

View File

@ -167,4 +167,8 @@ permission.system_plugin.read=查看插件
permission.system_plugin.add=創建插件
permission.system_plugin.edit=更新插件
permission.system_plugin.delete=刪除插件
plugin.exist=插件名稱或文件名已存在
plugin.type.exist=插件類型已存在
plugin.script.exist=腳本id重複
plugin.script.format=腳本格式錯誤

View File

@ -163,6 +163,12 @@ public abstract class BaseTest {
.andExpect(status().isOk());
}
protected ResultActions requestMultipart(String url, MultiValueMap<String, Object> paramMap, Object... uriVariables) throws Exception {
MockHttpServletRequestBuilder requestBuilder = getMultipartRequestBuilder(url, paramMap, uriVariables);
return mockMvc.perform(requestBuilder)
.andExpect(content().contentType(MediaType.APPLICATION_JSON));
}
protected MvcResult requestMultipartWithOkAndReturn(String url, MultiValueMap<String, Object> paramMap, Object... uriVariables) throws Exception {
return this.requestMultipartWithOk(url, paramMap, uriVariables).andReturn();
}

View File

@ -8,6 +8,7 @@ import io.metersphere.sdk.util.SessionUtils;
import io.metersphere.system.domain.Plugin;
import io.metersphere.system.dto.PluginDTO;
import io.metersphere.system.request.PluginUpdateRequest;
import io.metersphere.system.service.PluginLogService;
import io.metersphere.system.service.PluginService;
import io.metersphere.validation.groups.Created;
import io.metersphere.validation.groups.Updated;
@ -40,17 +41,10 @@ public class PluginController {
return pluginService.list();
}
@GetMapping("/get/{id}")
@Operation(summary = "获取插件详情")
@RequiresPermissions(PermissionConstants.SYSTEM_PLUGIN_READ)
public PluginDTO get(@PathVariable String id) {
return pluginService.get(id);
}
@PostMapping("/add")
@Operation(summary = "创建插件")
@RequiresPermissions(PermissionConstants.SYSTEM_PLUGIN_ADD)
@Log(type = OperationLogType.UPDATE, expression = "#msClass.addLog(#request)", msClass = PluginService.class)
@Log(type = OperationLogType.UPDATE, expression = "#msClass.addLog(#request)", msClass = PluginLogService.class)
public Plugin add(@Validated({Created.class}) @RequestPart(value = "request") PluginUpdateRequest request,
@RequestPart(value = "file") MultipartFile file) {
request.setCreateUser(SessionUtils.getUserId());
@ -60,26 +54,25 @@ public class PluginController {
@PostMapping("/update")
@Operation(summary = "更新插件")
@RequiresPermissions(PermissionConstants.SYSTEM_PLUGIN_UPDATE)
@Log(type = OperationLogType.ADD, expression = "#msClass.updateLog(#request)", msClass = PluginService.class)
public Plugin update(@Validated({Updated.class}) @RequestPart(value = "request") PluginUpdateRequest request,
@RequestPart(value = "file", required = false) MultipartFile file) {
@Log(type = OperationLogType.ADD, expression = "#msClass.updateLog(#request)", msClass = PluginLogService.class)
public Plugin update(@Validated({Updated.class}) @RequestBody PluginUpdateRequest request) {
Plugin plugin = new Plugin();
BeanUtils.copyBean(plugin, request);
return pluginService.update(request, file);
return pluginService.update(request);
}
@GetMapping("/delete/{id}")
@Operation(summary = "删除插件")
@RequiresPermissions(PermissionConstants.SYSTEM_PLUGIN_DELETE)
@Log(type = OperationLogType.DELETE, expression = "#msClass.deleteLog(#id)", msClass = PluginService.class)
public String delete(@PathVariable String id) {
return pluginService.delete(id);
@Log(type = OperationLogType.DELETE, expression = "#msClass.deleteLog(#id)", msClass = PluginLogService.class)
public void delete(@PathVariable String id) {
pluginService.delete(id);
}
@GetMapping("/script/get/{pluginId}/{scriptId}")
@Operation(summary = "获取插件对应表单的脚本内容")
@RequiresPermissions(PermissionConstants.SYSTEM_PLUGIN_READ)
public String getScript(@PathVariable String pluginId, String scriptId) {
public String getScript(@PathVariable String pluginId, @PathVariable String scriptId) {
return pluginService.getScript(pluginId, scriptId);
}
}

View File

@ -16,7 +16,11 @@ public enum SystemResultCode implements IResultCode {
/**
* 获取/编辑组织自定义用户组如果非组织自定义用户组会返回该响应码
*/
NO_ORG_USER_ROLE_PERMISSION(101007, "organization_user_role_permission_error");
NO_ORG_USER_ROLE_PERMISSION(101007, "organization_user_role_permission_error"),
PLUGIN_EXIST(101008, "plugin.exist"),
PLUGIN_TYPE_EXIST(101009, "plugin.type.exist"),
PLUGIN_SCRIPT_EXIST(101010, "plugin.script.exist"),
PLUGIN_SCRIPT_FORMAT(101011, "plugin.script.format");
private final int code;
private final String message;

View File

@ -1,6 +1,6 @@
package io.metersphere.system.dto;
import io.metersphere.sdk.dto.OrganizationOptionDTO;
import io.metersphere.sdk.dto.OptionDTO;
import io.metersphere.system.domain.Plugin;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -15,14 +15,8 @@ public class PluginDTO extends Plugin implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(title = "插件前端表单配置项列表")
private List<PluginDTO.PluginForm> pluginForms;
private List<OptionDTO> pluginForms;
@Schema(title = "关联的组织列表")
private List<OrganizationOptionDTO> organizations;
@Data
class PluginForm {
private String id;
private String name;
}
private List<OptionDTO> organizations;
}

View File

@ -0,0 +1,40 @@
package io.metersphere.system.listener;
import io.metersphere.sdk.constants.KafkaPluginTopicType;
import io.metersphere.sdk.constants.KafkaTopicConstants;
import io.metersphere.sdk.service.PluginLoadService;
import io.metersphere.sdk.util.LogUtils;
import jakarta.annotation.Resource;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class PluginListener {
public static final String PLUGIN_CONSUMER = "plugin_consumer";
@Resource
private PluginLoadService pluginLoadService;
// groupId 必须是每个实例唯一
@KafkaListener(id = PLUGIN_CONSUMER, topics = KafkaTopicConstants.PLUGIN, groupId = PLUGIN_CONSUMER + "_" + "${random.uuid}")
public void handlePluginChange(ConsumerRecord<?, String> record) {
LogUtils.info("Service consume platform_plugin message: " + record);
String[] info = record.value().split(":");
String operate = info[0];
String pluginId = info[1];
switch (operate) {
case KafkaPluginTopicType.ADD:
String pluginName = info[2];
pluginLoadService.loadPlugin(pluginId, pluginName);
break;
case KafkaPluginTopicType.DELETE:
pluginLoadService.unloadPlugin(pluginId);
break;
default:
break;
}
}
}

View File

@ -1,5 +1,6 @@
package io.metersphere.system.mapper;
import io.metersphere.sdk.dto.OptionDTO;
import io.metersphere.system.domain.User;
import io.metersphere.system.dto.OrgUserExtend;
import io.metersphere.system.dto.OrganizationDTO;
@ -75,4 +76,6 @@ public interface ExtOrganizationMapper {
* @return 组织列表数据
*/
List<OrganizationProjectOptionsDTO> selectOrganizationOptions();
List<OptionDTO> getOptionsByIds(@Param("ids") List<String> ids);
}

View File

@ -77,6 +77,12 @@
<select id="selectOrganizationOptions" resultType="io.metersphere.system.dto.OrganizationProjectOptionsDTO">
select id, name from organization order by create_time desc
</select>
<select id="getOptionsByIds" resultType="io.metersphere.sdk.dto.OptionDTO">
select id, name from organization where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<sql id="queryWhereCondition">
<where>

View File

@ -0,0 +1,9 @@
package io.metersphere.system.mapper;
import io.metersphere.system.dto.PluginDTO;
import java.util.List;
public interface ExtPluginMapper {
List<PluginDTO> getPlugins();
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.metersphere.system.mapper.ExtPluginMapper">
<select id="getPlugins" resultType="io.metersphere.system.dto.PluginDTO">
SELECT
<include refid="io.metersphere.system.mapper.PluginMapper.Base_Column_List"/>
FROM
plugin
</select>
</mapper>

View File

@ -0,0 +1,10 @@
package io.metersphere.system.mapper;
import io.metersphere.system.domain.PluginScript;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface ExtPluginScriptMapper {
List<PluginScript> getOptionByPluginIds(@Param("pluginIds") List<String> pluginIds);
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.metersphere.system.mapper.ExtPluginScriptMapper">
<select id="getOptionByPluginIds" resultType="io.metersphere.system.domain.PluginScript">
select plugin_id, script_id, name from plugin_script where plugin_id in
<foreach collection="pluginIds" item="pluginId" open="(" separator="," close=")">
#{pluginId}
</foreach>
</select>
</mapper>

View File

@ -13,7 +13,7 @@ import java.util.List;
public class PluginUpdateRequest {
@Schema(title = "ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{plugin.id.not_blank}", groups = {Updated.class})
@Size(min = 1, max = 50, message = "{plugin.id.length_range}", groups = {Created.class, Updated.class})
@Size(min = 1, max = 50, message = "{plugin.id.length_range}", groups = {Updated.class})
private String id;
@Schema(title = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED)

View File

@ -25,6 +25,7 @@ import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -62,6 +63,9 @@ public class OrganizationService {
private SystemProjectService systemProjectService;
@Resource
private UserRolePermissionMapper userRolePermissionMapper;
@Resource
@Lazy
private PluginOrganizationService pluginOrganizationService;
private static final String ADD_MEMBER_PATH = "/system/organization/add-member";
private static final String REMOVE_MEMBER_PATH = "/system/organization/remove-member";
@ -119,6 +123,9 @@ public class OrganizationService {
userRolePermissionMapper.deleteByExample(userRolePermissionExample);
}
// 删除组织和插件的关联关系
pluginOrganizationService.deleteByOrgId(organizationId);
// TODO: 删除环境组, 删除定时任务
// 删除组织
organizationMapper.deleteByPrimaryKey(organizationId);
@ -723,4 +730,11 @@ public class OrganizationService {
dto.setModifiedValue(JSON.toJSONBytes(modifiedValue));
logs.add(dto);
}
public List<OptionDTO> getOptionsByIds(List<String> orgIds) {
if (CollectionUtils.isEmpty(orgIds)) {
return new ArrayList<>(0);
}
return extOrganizationMapper.getOptionsByIds(orgIds);
}
}

View File

@ -0,0 +1,68 @@
package io.metersphere.system.service;
import io.metersphere.system.domain.Plugin;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import io.metersphere.sdk.constants.OperationLogConstants;
import io.metersphere.sdk.log.constants.OperationLogModule;
import io.metersphere.sdk.log.constants.OperationLogType;
import io.metersphere.sdk.dto.LogDTO;
import io.metersphere.sdk.util.JSON;
import org.springframework.transaction.annotation.Transactional;
import io.metersphere.system.request.PluginUpdateRequest;
/**
* @author jianxing
* @date : 2023-8-3
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class PluginLogService {
@Resource
private PluginService pluginService;
public LogDTO addLog(PluginUpdateRequest request) {
LogDTO dto = new LogDTO(
OperationLogConstants.SYSTEM,
OperationLogConstants.SYSTEM,
null,
null,
OperationLogType.ADD.name(),
OperationLogModule.SYSTEM_PLUGIN,
request.getName());
dto.setOriginalValue(JSON.toJSONBytes(request));
return dto;
}
public LogDTO updateLog(PluginUpdateRequest request) {
Plugin plugin = pluginService.get(request.getId());
LogDTO dto = new LogDTO(
OperationLogConstants.SYSTEM,
OperationLogConstants.SYSTEM,
plugin.getId(),
null,
OperationLogType.UPDATE.name(),
OperationLogModule.SYSTEM_PLUGIN,
plugin.getName());
dto.setOriginalValue(JSON.toJSONBytes(plugin));
return dto;
}
public LogDTO deleteLog(String id) {
Plugin plugin = pluginService.get(id);
if (plugin == null) {
return null;
}
LogDTO dto = new LogDTO(
OperationLogConstants.SYSTEM,
OperationLogConstants.SYSTEM,
plugin.getId(),
null,
OperationLogType.DELETE.name(),
OperationLogModule.SYSTEM_PLUGIN,
plugin.getName());
dto.setOriginalValue(JSON.toJSONBytes(plugin));
return dto;
}
}

View File

@ -0,0 +1,89 @@
package io.metersphere.system.service;
import io.metersphere.sdk.dto.OptionDTO;
import io.metersphere.system.domain.PluginOrganization;
import io.metersphere.system.domain.PluginOrganizationExample;
import io.metersphere.system.mapper.PluginOrganizationMapper;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Transactional(rollbackFor = Exception.class)
public class PluginOrganizationService {
@Resource
private PluginOrganizationMapper pluginOrganizationMapper;
@Resource
private OrganizationService organizationService;
public void add(String pluginId, List<String> orgIds) {
if (CollectionUtils.isEmpty(orgIds)) {
return;
}
List<PluginOrganization> pluginOrganizations = new ArrayList<>(orgIds.size());
for (String orgId : orgIds) {
PluginOrganization pluginOrganization = new PluginOrganization();
pluginOrganization.setPluginId(pluginId);
pluginOrganization.setOrganizationId(orgId);
pluginOrganizations.add(pluginOrganization);
}
pluginOrganizationMapper.batchInsert(pluginOrganizations);
}
public void deleteByPluginId(String pluginId) {
PluginOrganizationExample example = new PluginOrganizationExample();
example.createCriteria().andPluginIdEqualTo(pluginId);
pluginOrganizationMapper.deleteByExample(example);
}
public void deleteByOrgId(String orgId) {
PluginOrganizationExample example = new PluginOrganizationExample();
example.createCriteria().andOrganizationIdEqualTo(orgId);
pluginOrganizationMapper.deleteByExample(example);
}
public void update(String pluginId, List<String> organizationIds) {
if (organizationIds == null) {
// 如果参数没填则不更新
return;
}
// 先删除关联关系
deleteByPluginId(pluginId);
// 重新添加关联关系
add(pluginId, organizationIds);
}
public Map<String, List<OptionDTO>> getOrgMap(List<String> pluginIds) {
if (CollectionUtils.isEmpty(pluginIds)) {
return Collections.emptyMap();
}
// 查询插件和组织的关联关系
PluginOrganizationExample example = new PluginOrganizationExample();
example.createCriteria().andPluginIdIn(pluginIds);
List<PluginOrganization> pluginOrganizations = pluginOrganizationMapper.selectByExample(example);
// 查询组织信息
List<String> orgIds = pluginOrganizations.stream().map(PluginOrganization::getOrganizationId).toList();
List<OptionDTO> orgList = organizationService.getOptionsByIds(orgIds);
Map<String, OptionDTO> orgInfoMap = orgList.stream().collect(Collectors.toMap(OptionDTO::getId, i -> i));
// 组装成 map
Map<String, List<OptionDTO>> orgMap = new HashMap<>();
for (PluginOrganization pluginOrganization : pluginOrganizations) {
String pluginId = pluginOrganization.getPluginId();
String orgId = pluginOrganization.getOrganizationId();
OptionDTO orgInfo = orgInfoMap.get(orgId);
if (orgInfo == null) {
continue;
}
orgMap.computeIfAbsent(pluginId, k -> new ArrayList<>())
.add(orgInfo);
}
return orgMap;
}
}

View File

@ -0,0 +1,84 @@
package io.metersphere.system.service;
import io.metersphere.sdk.dto.OptionDTO;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.JSON;
import io.metersphere.system.domain.PluginScript;
import io.metersphere.system.domain.PluginScriptExample;
import io.metersphere.system.mapper.ExtPluginScriptMapper;
import io.metersphere.system.mapper.PluginScriptMapper;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_SCRIPT_EXIST;
import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_SCRIPT_FORMAT;
@Service
@Transactional(rollbackFor = Exception.class)
public class PluginScriptService {
@Resource
private PluginScriptMapper pluginScriptMapper;
@Resource
private ExtPluginScriptMapper extPluginScriptMapper;
public void add(String pluginId, List<String> frontendScript) {
Set<String> ids = new HashSet<>();
List<PluginScript> pluginScripts = new ArrayList<>(frontendScript.size());
for (String script : frontendScript) {
PluginScript pluginScript = new PluginScript();
OptionDTO scriptInfo;
try {
scriptInfo = JSON.parseObject(script, OptionDTO.class);
} catch (Exception e) {
throw new MSException(PLUGIN_SCRIPT_FORMAT);
}
// ID 判重
if (ids.contains(scriptInfo.getId())) {
throw new MSException(PLUGIN_SCRIPT_EXIST);
}
ids.add(scriptInfo.getId());
pluginScript.setPluginId(pluginId);
pluginScript.setScriptId(
StringUtils.isBlank(scriptInfo.getId()) ? UUID.randomUUID().toString() : scriptInfo.getId()
);
pluginScript.setName(scriptInfo.getName());
pluginScript.setScript(script.getBytes());
pluginScripts.add(pluginScript);
}
pluginScriptMapper.batchInsert(pluginScripts);
}
public void deleteByPluginId(String pluginId) {
PluginScriptExample example = new PluginScriptExample();
example.createCriteria().andPluginIdEqualTo(pluginId);
pluginScriptMapper.deleteByExample(example);
}
public String get(String pluginId, String scriptId) {
PluginScript frontScript = pluginScriptMapper.selectByPrimaryKey(pluginId, scriptId);
return frontScript == null ? null : new String(frontScript.getScript());
}
public Map<String, List<OptionDTO>> getScripteMap(List<String> pluginIds) {
if (CollectionUtils.isEmpty(pluginIds)) {
return Collections.emptyMap();
}
List<PluginScript> scripts = extPluginScriptMapper.getOptionByPluginIds(pluginIds);
Map<String, List<OptionDTO>> scriptMap = new HashMap<>();
for (PluginScript script : scripts) {
List<OptionDTO> scriptList = scriptMap.computeIfAbsent(script.getPluginId(), k -> new ArrayList<>());
OptionDTO optionDTO = new OptionDTO();
optionDTO.setId(script.getScriptId());
optionDTO.setName(script.getName());
scriptList.add(optionDTO);
}
return scriptMap;
}
}

View File

@ -1,23 +1,34 @@
package io.metersphere.system.service;
import io.metersphere.sdk.constants.PluginScenarioType;
import io.metersphere.plugin.sdk.api.MsPlugin;
import io.metersphere.sdk.constants.KafkaPluginTopicType;
import io.metersphere.sdk.constants.KafkaTopicConstants;
import io.metersphere.sdk.dto.OptionDTO;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.service.PluginLoadService;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.system.domain.Plugin;
import io.metersphere.system.domain.PluginFrontScript;
import io.metersphere.system.domain.PluginExample;
import io.metersphere.system.dto.PluginDTO;
import io.metersphere.system.mapper.PluginFrontScriptMapper;
import io.metersphere.system.mapper.ExtPluginMapper;
import io.metersphere.system.mapper.PluginMapper;
import io.metersphere.system.request.PluginUpdateRequest;
import jakarta.annotation.Resource;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_EXIST;
import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_TYPE_EXIST;
/**
* @author jianxing
* @date : 2023-7-13
@ -29,50 +40,157 @@ public class PluginService {
@Resource
private PluginMapper pluginMapper;
@Resource
private PluginFrontScriptMapper pluginFrontScriptMapper;
private ExtPluginMapper extPluginMapper;
@Resource
private PluginScriptService pluginScriptService;
@Resource
private PluginOrganizationService pluginOrganizationService;
@Resource
private PluginLoadService pluginLoadService;
@Resource
private KafkaTemplate<String, String> kafkaTemplate;
public List<PluginDTO> list() {
return new ArrayList<>();
List<PluginDTO> plugins = extPluginMapper.getPlugins();
List<String> pluginIds = plugins.stream().map(Plugin::getId).toList();
Map<String, List<OptionDTO>> scripteMap = pluginScriptService.getScripteMap(pluginIds);
Map<String, List<OptionDTO>> orgMap = pluginOrganizationService.getOrgMap(pluginIds);
plugins.forEach(plugin -> {
plugin.setPluginForms(scripteMap.get(plugin.getId()));
plugin.setOrganizations(orgMap.get(plugin.getId()));
});
return plugins;
}
public PluginDTO get(String id) {
Plugin plugin = pluginMapper.selectByPrimaryKey(id);
PluginDTO pluginDTO = new PluginDTO();
BeanUtils.copyBean(plugin, pluginDTO);
return pluginDTO;
public Plugin get(String id) {
return pluginMapper.selectByPrimaryKey(id);
}
public Plugin add(PluginUpdateRequest request, MultipartFile file) {
String id = UUID.randomUUID().toString();
Plugin plugin = new Plugin();
BeanUtils.copyBean(plugin, request);
plugin.setId(UUID.randomUUID().toString());
plugin.setPluginId(UUID.randomUUID().toString());
plugin.setFileName(file.getName());
plugin.setId(id);
plugin.setFileName(file.getOriginalFilename());
plugin.setCreateTime(System.currentTimeMillis());
plugin.setUpdateTime(System.currentTimeMillis());
plugin.setXpack(false);
plugin.setScenario(PluginScenarioType.PAI.name());
pluginMapper.insert(plugin);
// 校验重名
checkPluginAddExist(plugin);
try {
// 加载插件
pluginLoadService.loadPlugin(id, file);
// 上传插件
pluginLoadService.uploadPlugin(id, file);
// 获取插件前端配置脚本
List<String> frontendScript = pluginLoadService.getFrontendScripts(id);
MsPlugin msPlugin = pluginLoadService.getMsPluginInstance(id);
plugin.setScenario(msPlugin.getType());
plugin.setXpack(msPlugin.isXpack());
plugin.setPluginId(msPlugin.getPluginId());
// 校验插件类型是否重复
checkPluginKeyExist(id, msPlugin.getKey());
// 保存插件脚本
pluginScriptService.add(id, frontendScript);
// 保存插件和组织的关联关系
if (!request.getGlobal()) {
pluginOrganizationService.add(id, request.getOrganizationIds());
}
pluginMapper.insert(plugin);
// 通知其他节点加载插件
notifiedPluginAdd(id, plugin.getFileName());
} catch (Exception e) {
// 删除插件
pluginLoadService.deletePlugin(id);
throw e;
}
return plugin;
}
public Plugin update(PluginUpdateRequest request, MultipartFile file) {
private void checkPluginKeyExist(String pluginId, String pluginKey) {
if (pluginLoadService.hasPluginKey(pluginId, pluginKey)) {
throw new MSException(PLUGIN_TYPE_EXIST);
}
}
private void checkPluginAddExist(Plugin plugin) {
PluginExample example = new PluginExample();
example.createCriteria()
.andNameEqualTo(plugin.getName());
PluginExample.Criteria criteria = example.createCriteria()
.andFileNameEqualTo(plugin.getFileName());
example.or(criteria);
if (CollectionUtils.isNotEmpty(pluginMapper.selectByExample(example))) {
throw new MSException(PLUGIN_EXIST);
}
}
/**
* 通知其他节点加载插件
* 这里需要传一下 fileName事务未提交查询不到文件名
* @param pluginId
* @param fileName
*/
public void notifiedPluginAdd(String pluginId, String fileName) {
// 初始化项目默认节点
kafkaTemplate.send(KafkaTopicConstants.PLUGIN, String.format("%s:%s:%s", KafkaPluginTopicType.ADD, pluginId, fileName));
}
/**
* 通知其他节点卸载插件
* @param pluginId
*/
public void notifiedPluginDelete(String pluginId) {
// 初始化项目默认节点
kafkaTemplate.send(KafkaTopicConstants.PLUGIN, String.format("%s:%s", KafkaPluginTopicType.DELETE, pluginId));
}
public Plugin update(PluginUpdateRequest request) {
request.setCreateUser(null);
Plugin plugin = new Plugin();
BeanUtils.copyBean(plugin, request);
plugin.setCreateTime(null);
plugin.setUpdateTime(null);
plugin.setCreateUser(null);
// 校验重名
checkPluginUpdateExist(plugin);
pluginMapper.updateByPrimaryKeySelective(plugin);
if (request.getGlobal()) {
// 全局插件删除和组织的关联关系
request.setOrganizationIds(new ArrayList<>(0));
}
pluginOrganizationService.update(plugin.getId(), request.getOrganizationIds());
return plugin;
}
public String delete(String id) {
private void checkPluginUpdateExist(Plugin plugin) {
PluginExample example = new PluginExample();
example.createCriteria()
.andIdNotEqualTo(plugin.getId())
.andNameEqualTo(plugin.getName());
if (CollectionUtils.isNotEmpty(pluginMapper.selectByExample(example))) {
throw new MSException(PLUGIN_EXIST);
}
}
public void delete(String id) {
pluginMapper.deleteByPrimaryKey(id);
return id;
// 删除插件脚本
pluginScriptService.deleteByPluginId(id);
// 删除和组织的关联关系
pluginOrganizationService.deleteByPluginId(id);
// 删除和卸载插件
pluginLoadService.deletePlugin(id);
notifiedPluginDelete(id);
}
public String getScript(String pluginId, String scriptId) {
PluginFrontScript frontScript = pluginFrontScriptMapper.selectByPrimaryKey(pluginId, scriptId);
return frontScript == null ? null : frontScript.getScript();
return pluginScriptService.get(pluginId, scriptId);
}
}

View File

@ -78,7 +78,7 @@
<table tableName="operating_log"/>
<table tableName="operating_log_resource"/>
<table tableName="plugin"/>
<table tableName="plugin_front_script"/>
<table tableName="plugin_script"/>
<table tableName="plugin_organization"/>
<table tableName="schedule"/>
<table tableName="service_integration"/>

View File

@ -2,20 +2,31 @@ package io.metersphere.system.controller;
import io.metersphere.sdk.base.BaseTest;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.system.domain.Plugin;
import io.metersphere.sdk.constants.PluginScenarioType;
import io.metersphere.sdk.dto.OptionDTO;
import io.metersphere.sdk.log.constants.OperationLogType;
import io.metersphere.sdk.util.JSON;
import io.metersphere.system.controller.param.PluginUpdateRequestDefinition;
import io.metersphere.system.domain.*;
import io.metersphere.system.dto.OrganizationDTO;
import io.metersphere.system.dto.PluginDTO;
import io.metersphere.system.mapper.PluginMapper;
import io.metersphere.system.mapper.PluginOrganizationMapper;
import io.metersphere.system.mapper.PluginScriptMapper;
import io.metersphere.system.request.PluginUpdateRequest;
import io.metersphere.system.service.OrganizationService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.MultiValueMap;
import java.io.File;
import java.util.*;
import static io.metersphere.system.controller.result.SystemResultCode.*;
/**
* @author jianxing
@ -29,99 +40,240 @@ public class PluginControllerTests extends BaseTest {
private static final String SCRIPT_GET = "script/get/{0}/{1}";
@Resource
private PluginMapper pluginMapper;
@Resource
private PluginOrganizationMapper pluginOrganizationMapper;
@Resource
private PluginScriptMapper pluginScriptMapper;
@Resource
private OrganizationService organizationService;
private static Plugin addPlugin;
private static Plugin anotherAddPlugin;
@Override
protected String getBasePath() {
return BASE_PATH;
}
@Test
public void list() throws Exception {
// @@请求成功
this.requestGetWithOk(DEFAULT_LIST)
.andReturn();
// List<Plugin> pluginList = getResultDataArray(mvcResult, Plugin.class);
// todo 校验数据是否正确
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_PLUGIN_READ, DEFAULT_LIST);
@Order(0)
public void listEmpty() throws Exception {
// @@没有数据是校验是否成功
this.requestGetWithOkAndReturn(DEFAULT_LIST);
}
@Test
@Order(0)
@Order(1)
public void add() throws Exception {
// @@请求成功
PluginUpdateRequest request = new PluginUpdateRequest();
OrganizationDTO org = organizationService.getDefault();
File jarFile = new File(
this.getClass().getClassLoader().getResource("file/metersphere-mqtt-plugin-3.x.jar")
.getPath()
);
File anotherJarFile = new File(
this.getClass().getClassLoader().getResource("file/metersphere-jira-plugin-3.x.jar")
.getPath()
);
request.setName("test");
request.setDescription("test desc");
MultiValueMap<String, Object> multiValueMap = getDefaultMultiPartParam(request,
new File("src/test/resources/application.properties"));
request.setGlobal(false);
request.setEnable(false);
request.setOrganizationIds(Arrays.asList(org.getId()));
MultiValueMap<String, Object> multiValueMap = getDefaultMultiPartParam(request, jarFile);
MvcResult mvcResult = this.requestMultipartWithOkAndReturn(DEFAULT_ADD, multiValueMap);
// 校验数据是否正确
Plugin resultData = getResultData(mvcResult, Plugin.class);
Plugin plugin = pluginMapper.selectByPrimaryKey(resultData.getId());
Assertions.assertEquals(plugin.getName(), request.getName());
Assertions.assertEquals(plugin.getDescription(), request.getDescription());
Assertions.assertEquals(plugin.getEnable(), request.getEnable());
Assertions.assertEquals(plugin.getGlobal(), request.getGlobal());
Assertions.assertEquals(plugin.getXpack(), false);
Assertions.assertEquals(plugin.getFileName(), jarFile.getName());
Assertions.assertEquals(plugin.getScenario(), PluginScenarioType.API.name());
Assertions.assertEquals(Arrays.asList(org.getId()), getOrgIdsByPlugId(plugin.getId()));
Assertions.assertEquals(Arrays.asList("connect", "disconnect", "pub", "sub"), getScriptIdsByPlugId(plugin.getId()));
addPlugin = plugin;
// @@重名校验异常
// 校验插件名称重名
assertErrorCode(this.requestMultipart(DEFAULT_ADD,
getDefaultMultiPartParam(request, anotherJarFile)), PLUGIN_EXIST);
// 校验文件名重名
request.setName("test1");
assertErrorCode(this.requestMultipart(DEFAULT_ADD,
getDefaultMultiPartParam(request, jarFile)), PLUGIN_EXIST);
// 校验插件 key 重复
File typeRepeatFile = new File(
this.getClass().getClassLoader().getResource("file/metersphere-mqtt-plugin-repeat-key.jar")
.getPath()
);
assertErrorCode(this.requestMultipart(DEFAULT_ADD,
getDefaultMultiPartParam(request, typeRepeatFile)), PLUGIN_TYPE_EXIST);
// @@校验插件脚本解析失败
File scriptParseFile = new File(
this.getClass().getClassLoader().getResource("file/metersphere-plugin-script-parse-error.jar")
.getPath()
);
assertErrorCode(this.requestMultipart(DEFAULT_ADD,
getDefaultMultiPartParam(request, scriptParseFile)), PLUGIN_SCRIPT_FORMAT);
// @@校验插件脚本ID重复
File scriptIdRepeatFile = new File(
this.getClass().getClassLoader().getResource("file/metersphere-plugin-script-id-repeat-error.jar")
.getPath()
);
assertErrorCode(this.requestMultipart(DEFAULT_ADD,
getDefaultMultiPartParam(request, scriptIdRepeatFile)), PLUGIN_SCRIPT_EXIST);
request.setGlobal(true);
request.setEnable(true);
request.setName("test2");
MvcResult antoherMvcResult = this.requestMultipartWithOkAndReturn(DEFAULT_ADD,
getDefaultMultiPartParam(request, anotherJarFile));
// 校验 global tru e时organizationIds 为空
Plugin antoherPlugin = pluginMapper.selectByPrimaryKey(getResultData(antoherMvcResult, Plugin.class).getId());
Assertions.assertEquals(antoherPlugin.getEnable(), request.getEnable());
Assertions.assertEquals(antoherPlugin.getGlobal(), request.getGlobal());
Assertions.assertEquals(new ArrayList<>(0), getOrgIdsByPlugId(antoherPlugin.getId()));
anotherAddPlugin = antoherPlugin;
this.addPlugin = plugin;
// todo 校验请求成功数据
// @@校验日志
// checkLog(this.addPlugin.getId(), OperationLogType.ADD);
// @@异常参数校验
// createdGroupParamValidateTest(PluginUpdateRequestDefinition.class, ADD);
checkLog(this.addPlugin.getId(), OperationLogType.ADD);
// @@校验权限
requestMultipartPermissionTest(PermissionConstants.SYSTEM_PLUGIN_ADD, DEFAULT_ADD, multiValueMap);
}
@Test
@Order(1)
public void get() throws Exception {
// @@请求成功
this.requestGetWithOk(DEFAULT_GET, this.addPlugin.getId())
.andReturn();
// Plugin plugin = getResultData(mvcResult, Plugin.class);
// todo 校验数据是否正确
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_PLUGIN_READ, DEFAULT_GET, this.addPlugin.getId());
}
@Test
@Order(2)
public void getScript() throws Exception {
// @@请求成功
this.requestGetWithOk(SCRIPT_GET, this.addPlugin.getId(), "script id")
.andReturn();
// Plugin plugin = getResultData(mvcResult, Plugin.class);
// todo 校验数据是否正确
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_PLUGIN_READ, SCRIPT_GET, this.addPlugin.getId(), "script id");
}
@Test
public void update() throws Exception {
// @@请求成功
PluginUpdateRequest request = new PluginUpdateRequest();
OrganizationDTO org = organizationService.getDefault();
request.setId(addPlugin.getId());
request.setName("test update");
request.setCreateUser("test update user");
request.setDescription("test update desc");
request.setEnable(true);
request.setGlobal(true);
request.setOrganizationIds(Arrays.asList(org.getId()));
MultiValueMap<String, Object> multiValueMap = getDefaultMultiPartParam(request,
new File("src/test/resources/application.properties"));
this.requestMultipartWithOk(DEFAULT_UPDATE, multiValueMap);
this.requestPostWithOk(DEFAULT_UPDATE, request);
// 校验请求成功数据
// Plugin plugin = pluginMapper.selectByPrimaryKey(request.getId());
// todo 校验请求成功数据
Plugin plugin = pluginMapper.selectByPrimaryKey(request.getId());
Assertions.assertEquals(plugin.getName(), request.getName());
Assertions.assertEquals(plugin.getDescription(), request.getDescription());
Assertions.assertEquals(plugin.getEnable(), request.getEnable());
Assertions.assertEquals(plugin.getGlobal(), request.getGlobal());
Assertions.assertEquals(plugin.getXpack(), false);
// 校验 global true organizationIds 为空
Assertions.assertEquals(new ArrayList<>(0), getOrgIdsByPlugId(plugin.getId()));
// 这些数据不能修改
Assertions.assertEquals(plugin.getFileName(), addPlugin.getFileName());
Assertions.assertEquals(plugin.getScenario(), addPlugin.getScenario());
Assertions.assertEquals(plugin.getCreateUser(), addPlugin.getCreateUser());
// 校验 global false organizationIds 数据
request.setGlobal(false);
this.requestPostWithOk(DEFAULT_UPDATE, request);
Assertions.assertEquals(Arrays.asList(org.getId()), getOrgIdsByPlugId(plugin.getId()));
// 校验组织为null不修改关联关系
request.setOrganizationIds(null);
this.requestPostWithOk(DEFAULT_UPDATE, request);
Assertions.assertEquals(Arrays.asList(org.getId()), getOrgIdsByPlugId(plugin.getId()));
// @@重名校验异常
request.setName(anotherAddPlugin.getName());
assertErrorCode( this.requestPost(DEFAULT_UPDATE, request), PLUGIN_EXIST);
// @@校验日志
// checkLog(request.getId(), OperationLogType.UPDATE);
checkLog(request.getId(), OperationLogType.UPDATE);
// @@异常参数校验
// updatedGroupParamValidateTest(PluginUpdateRequestDefinition.class, UPDATE);
updatedGroupParamValidateTest(PluginUpdateRequestDefinition.class, DEFAULT_UPDATE);
// @@校验权限
requestMultipartPermissionTest(PermissionConstants.SYSTEM_PLUGIN_UPDATE, DEFAULT_UPDATE, multiValueMap);
requestPostPermissionTest(PermissionConstants.SYSTEM_PLUGIN_UPDATE, DEFAULT_UPDATE, request);
}
@Test
@Order(3)
public void getScript() throws Exception {
// @@请求成功
MvcResult mvcResult = this.requestGetWithOk(SCRIPT_GET, this.addPlugin.getId(), "connect").andReturn();
// 校验数据是否正确
Assertions.assertTrue(StringUtils.isNotBlank(getResultData(mvcResult, String.class)));
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_PLUGIN_READ, SCRIPT_GET, this.addPlugin.getId(), "connect");
}
@Test
@Order(4)
public void list() throws Exception {
// @@请求成功
MvcResult mvcResult = this.requestGetWithOkAndReturn(DEFAULT_LIST);
// 校验数据是否正确
List<PluginDTO> pluginList = getResultDataArray(mvcResult, PluginDTO.class);
Assertions.assertEquals(2, pluginList.size());
for (PluginDTO pluginDTO : pluginList) {
Plugin comparePlugin = null;
if (StringUtils.equals(pluginDTO.getId(), addPlugin.getId())) {
comparePlugin = pluginMapper.selectByPrimaryKey(addPlugin.getId());
} else if (StringUtils.equals(pluginDTO.getId(), anotherAddPlugin.getId())) {
comparePlugin = pluginMapper.selectByPrimaryKey(anotherAddPlugin.getId());
}
Plugin plugin = JSON.parseObject(JSON.toJSONString(pluginDTO), Plugin.class);
List<String> scriptIds = pluginDTO.getPluginForms().stream().map(OptionDTO::getId).toList();
Assertions.assertEquals(plugin, comparePlugin);
Assertions.assertEquals(scriptIds, getScriptIdsByPlugId(plugin.getId()));
List<OptionDTO> orgList = Optional.ofNullable(pluginDTO.getOrganizations()).orElse(new ArrayList<>(0));
Assertions.assertEquals(orgList.stream().map(OptionDTO::getId).toList(), getOrgIdsByPlugId(plugin.getId()));
}
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_PLUGIN_READ, DEFAULT_LIST);
}
@Test
@Order(5)
public void delete() throws Exception {
// @@请求成功
this.requestGetWithOk(DEFAULT_DELETE, addPlugin.getId());
// todo 校验请求成功数据
// 校验请求成功数据
Plugin plugin = pluginMapper.selectByPrimaryKey(addPlugin.getId());
Assertions.assertNull(plugin);
Assertions.assertEquals(new ArrayList<>(0), getOrgIdsByPlugId(addPlugin.getId()));
Assertions.assertEquals(new ArrayList<>(0), getScriptIdsByPlugId(addPlugin.getId()));
// @@校验日志
// checkLog(addPlugin.getId(), OperationLogType.DELETE);
checkLog(addPlugin.getId(), OperationLogType.DELETE);
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_PLUGIN_DELETE, DEFAULT_DELETE, addPlugin.getId());
}
private List<String> getOrgIdsByPlugId(String pluginId) {
PluginOrganizationExample example = new PluginOrganizationExample();
example.createCriteria().andPluginIdEqualTo(pluginId);
return pluginOrganizationMapper.selectByExample(example)
.stream()
.map(PluginOrganization::getOrganizationId)
.toList();
}
private List<String> getScriptIdsByPlugId(String pluginId) {
PluginScriptExample example = new PluginScriptExample();
example.createCriteria().andPluginIdEqualTo(pluginId);
return pluginScriptMapper.selectByExample(example)
.stream()
.map(PluginScript::getScriptId)
.toList();
}
}

View File

@ -9,7 +9,7 @@ import lombok.Data;
@Data
public class PluginUpdateRequestDefinition {
@NotBlank(message = "{plugin.id.not_blank}", groups = {Updated.class})
@Size(min = 1, max = 50, message = "{plugin.id.length_range}", groups = {Created.class, Updated.class})
@Size(min = 1, max = 50, message = "{plugin.id.length_range}", groups = {Updated.class})
private String id;
@NotBlank(groups = {Created.class})
@ -18,8 +18,4 @@ public class PluginUpdateRequestDefinition {
@Size(min = 1, max = 500, groups = {Created.class, Updated.class})
private String description;
@NotBlank(groups = {Created.class})
@Size(min = 1, max = 50, groups = {Created.class, Updated.class})
private String scenario;
}

View File

@ -8,6 +8,6 @@ embedded.mysql.collation=utf8mb4_general_ci
# redis
embedded.redis.enabled=true
# kafka
embedded.kafka.enabled=false
embedded.kafka.enabled=true
# minio
embedded.minio.enabled=true