From 7e4182549a336d5555e23af6f9787b4a5681c210 Mon Sep 17 00:00:00 2001 From: jianxing Date: Wed, 2 Aug 2023 18:14:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=B3=BB=E7=BB=9F=E8=AE=BE=E7=BD=AE):=20?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E7=AE=A1=E7=90=86=E6=8E=A5=E5=8F=A3=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --task=1012390 --user=陈建星 系统设置-系统-插件管理-后台 https://www.tapd.cn/55049933/s/1401201 --- .../listener/AppStartListener.java | 8 + ...uginFrontScript.java => PluginScript.java} | 18 +- ...tExample.java => PluginScriptExample.java} | 74 ++++- .../mapper/PluginFrontScriptMapper.java | 38 --- .../system/mapper/PluginScriptMapper.java | 40 +++ ...criptMapper.xml => PluginScriptMapper.xml} | 107 ++++--- .../3.0.0/ddl/V3.0.0_11__system_setting.sql | 5 +- .../plugin/api/api/AbstractApiPlugin.java | 4 - .../platform/api/AbstractPlatformPlugin.java | 4 - .../plugin/sdk/api/AbstractMsPlugin.java | 67 +---- .../metersphere/plugin/sdk/api/MsPlugin.java | 14 +- .../config/interceptor/SystemInterceptor.java | 2 + .../sdk/constants/KafkaPluginTopicType.java | 6 + .../sdk/constants/KafkaTopicConstants.java | 5 + .../sdk/constants/PluginScenarioType.java | 2 +- .../sdk/dto/OrganizationOptionDTO.java | 20 -- .../sdk/exception/MSException.java | 8 + .../metersphere/sdk/file/FileRepository.java | 32 ++- .../metersphere/sdk/file/MinioRepository.java | 10 +- .../sdk/log/constants/OperationLogModule.java | 2 +- .../sdk/plugin/loader/PluginClassLoader.java | 36 ++- .../sdk/plugin/loader/PluginManager.java | 11 +- .../plugin/storage/MinioStorageStrategy.java | 35 --- .../sdk/plugin/storage/MsStorageStrategy.java | 64 +++++ .../sdk/plugin/storage/StorageStrategy.java | 15 +- .../sdk/service/PluginLoadService.java | 190 +++++++++++++ .../resources/i18n/system_en_US.properties | 5 +- .../resources/i18n/system_zh_CN.properties | 6 +- .../resources/i18n/system_zh_TW.properties | 4 + .../io/metersphere/sdk/base/BaseTest.java | 6 + .../system/controller/PluginController.java | 25 +- .../controller/result/SystemResultCode.java | 6 +- .../io/metersphere/system/dto/PluginDTO.java | 12 +- .../system/listener/PluginListener.java | 40 +++ .../system/mapper/ExtOrganizationMapper.java | 3 + .../system/mapper/ExtOrganizationMapper.xml | 6 + .../system/mapper/ExtPluginMapper.java | 9 + .../system/mapper/ExtPluginMapper.xml | 10 + .../system/mapper/ExtPluginScriptMapper.java | 10 + .../system/mapper/ExtPluginScriptMapper.xml | 10 + .../system/request/PluginUpdateRequest.java | 2 +- .../system/service/OrganizationService.java | 14 + .../system/service/PluginLogService.java | 68 +++++ .../service/PluginOrganizationService.java | 89 ++++++ .../system/service/PluginScriptService.java | 84 ++++++ .../system/service/PluginService.java | 162 +++++++++-- .../main/resources/systemGeneratorConfig.xml | 2 +- .../controller/PluginControllerTests.java | 260 ++++++++++++++---- .../param/PluginUpdateRequestDefinition.java | 6 +- .../src/test/resources/bootstrap.properties | 2 +- .../file/metersphere-jira-plugin-3.x.jar | Bin 0 -> 4035 bytes .../file/metersphere-mqtt-plugin-3.x.jar | Bin 0 -> 6706 bytes .../metersphere-mqtt-plugin-repeat-key.jar | Bin 0 -> 6706 bytes ...ersphere-plugin-script-id-repeat-error.jar | Bin 0 -> 6707 bytes .../metersphere-plugin-script-parse-error.jar | Bin 0 -> 6710 bytes 55 files changed, 1291 insertions(+), 367 deletions(-) rename backend/framework/domain/src/main/java/io/metersphere/system/domain/{PluginFrontScript.java => PluginScript.java} (79%) rename backend/framework/domain/src/main/java/io/metersphere/system/domain/{PluginFrontScriptExample.java => PluginScriptExample.java} (80%) delete mode 100644 backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.java create mode 100644 backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.java rename backend/framework/domain/src/main/java/io/metersphere/system/mapper/{PluginFrontScriptMapper.xml => PluginScriptMapper.xml} (74%) create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaPluginTopicType.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaTopicConstants.java delete mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/OrganizationOptionDTO.java delete mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MinioStorageStrategy.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MsStorageStrategy.java create mode 100644 backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java create mode 100644 backend/services/system-setting/src/main/java/io/metersphere/system/listener/PluginListener.java create mode 100644 backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.java create mode 100644 backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.xml create mode 100644 backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.java create mode 100644 backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.xml create mode 100644 backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginLogService.java create mode 100644 backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginOrganizationService.java create mode 100644 backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginScriptService.java create mode 100644 backend/services/system-setting/src/test/resources/file/metersphere-jira-plugin-3.x.jar create mode 100644 backend/services/system-setting/src/test/resources/file/metersphere-mqtt-plugin-3.x.jar create mode 100644 backend/services/system-setting/src/test/resources/file/metersphere-mqtt-plugin-repeat-key.jar create mode 100644 backend/services/system-setting/src/test/resources/file/metersphere-plugin-script-id-repeat-error.jar create mode 100644 backend/services/system-setting/src/test/resources/file/metersphere-plugin-script-parse-error.jar diff --git a/backend/app/src/main/java/io/metersphere/listener/AppStartListener.java b/backend/app/src/main/java/io/metersphere/listener/AppStartListener.java index 991f50d545..1b999337ce 100644 --- a/backend/app/src/main/java/io/metersphere/listener/AppStartListener.java +++ b/backend/app/src/main/java/io/metersphere/listener/AppStartListener.java @@ -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(); } } diff --git a/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginFrontScript.java b/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginScript.java similarity index 79% rename from backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginFrontScript.java rename to backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginScript.java index 6101cd698d..2641defa96 100644 --- a/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginFrontScript.java +++ b/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginScript.java @@ -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 = "`"; diff --git a/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginFrontScriptExample.java b/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginScriptExample.java similarity index 80% rename from backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginFrontScriptExample.java rename to backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginScriptExample.java index 3ab4ebfa04..bcd0df4dfd 100644 --- a/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginFrontScriptExample.java +++ b/backend/framework/domain/src/main/java/io/metersphere/system/domain/PluginScriptExample.java @@ -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 oredCriteria; - public PluginFrontScriptExample() { + public PluginScriptExample() { oredCriteria = new ArrayList(); } @@ -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 values) { + addCriterion("`name` in", values, "name"); + return (Criteria) this; + } + + public Criteria andNameNotIn(List 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 { diff --git a/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.java b/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.java deleted file mode 100644 index 80199f3db0..0000000000 --- a/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.java +++ /dev/null @@ -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 selectByExampleWithBLOBs(PluginFrontScriptExample example); - - List 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 list); - - int batchInsertSelective(@Param("list") List list, @Param("selective") PluginFrontScript.Column ... selective); -} \ No newline at end of file diff --git a/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.java b/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.java new file mode 100644 index 0000000000..0b3fe73413 --- /dev/null +++ b/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.java @@ -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 selectByExampleWithBLOBs(PluginScriptExample example); + + List 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 list); + + int batchInsertSelective(@Param("list") List list, @Param("selective") PluginScript.Column ... selective); +} \ No newline at end of file diff --git a/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.xml b/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.xml similarity index 74% rename from backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.xml rename to backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.xml index 050e7d88bf..113298d515 100644 --- a/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginFrontScriptMapper.xml +++ b/backend/framework/domain/src/main/java/io/metersphere/system/mapper/PluginScriptMapper.xml @@ -1,12 +1,13 @@ - - + + + - - + + @@ -67,12 +68,12 @@ - plugin_id, script_id + plugin_id, script_id, `name` script - select distinct @@ -80,7 +81,7 @@ , - from plugin_front_script + from plugin_script @@ -88,13 +89,13 @@ order by ${orderByClause} - select distinct - from plugin_front_script + from plugin_script @@ -107,29 +108,29 @@ , - from plugin_front_script + from plugin_script where plugin_id = #{pluginId,jdbcType=VARCHAR} and script_id = #{scriptId,jdbcType=VARCHAR} - delete from plugin_front_script + delete from plugin_script where plugin_id = #{pluginId,jdbcType=VARCHAR} and script_id = #{scriptId,jdbcType=VARCHAR} - - delete from plugin_front_script + + delete from plugin_script - - insert into plugin_front_script (plugin_id, script_id, script - ) - values (#{pluginId,jdbcType=VARCHAR}, #{scriptId,jdbcType=VARCHAR}, #{script,jdbcType=LONGVARCHAR} - ) + + insert into plugin_script (plugin_id, script_id, `name`, + script) + values (#{pluginId,jdbcType=VARCHAR}, #{scriptId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, + #{script,jdbcType=LONGVARBINARY}) - - insert into plugin_front_script + + insert into plugin_script plugin_id, @@ -137,6 +138,9 @@ script_id, + + `name`, + script, @@ -148,19 +152,22 @@ #{scriptId,jdbcType=VARCHAR}, + + #{name,jdbcType=VARCHAR}, + - #{script,jdbcType=LONGVARCHAR}, + #{script,jdbcType=LONGVARBINARY}, - + select count(*) from plugin_script - update plugin_front_script + update plugin_script plugin_id = #{record.pluginId,jdbcType=VARCHAR}, @@ -168,8 +175,11 @@ script_id = #{record.scriptId,jdbcType=VARCHAR}, + + `name` = #{record.name,jdbcType=VARCHAR}, + - script = #{record.script,jdbcType=LONGVARCHAR}, + script = #{record.script,jdbcType=LONGVARBINARY}, @@ -177,49 +187,61 @@ - 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} - 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} - - update plugin_front_script + + update plugin_script + + `name` = #{name,jdbcType=VARCHAR}, + - script = #{script,jdbcType=LONGVARCHAR}, + script = #{script,jdbcType=LONGVARBINARY}, where plugin_id = #{pluginId,jdbcType=VARCHAR} and script_id = #{scriptId,jdbcType=VARCHAR} - - update plugin_front_script - set script = #{script,jdbcType=LONGVARCHAR} + + 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 plugin_script + set `name` = #{name,jdbcType=VARCHAR} where plugin_id = #{pluginId,jdbcType=VARCHAR} and script_id = #{scriptId,jdbcType=VARCHAR} - insert into plugin_front_script - (plugin_id, script_id, script) + insert into plugin_script + (plugin_id, script_id, `name`, script) values - (#{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}) - insert into plugin_front_script ( + insert into plugin_script ( ${column.escapedColumnName} @@ -234,8 +256,11 @@ #{item.scriptId,jdbcType=VARCHAR} + + #{item.name,jdbcType=VARCHAR} + - #{item.script,jdbcType=LONGVARCHAR} + #{item.script,jdbcType=LONGVARBINARY} ) diff --git a/backend/framework/domain/src/main/resources/migration/3.0.0/ddl/V3.0.0_11__system_setting.sql b/backend/framework/domain/src/main/resources/migration/3.0.0/ddl/V3.0.0_11__system_setting.sql index 088d24abda..8a696a56f3 100644 --- a/backend/framework/domain/src/main/resources/migration/3.0.0/ddl/V3.0.0_11__system_setting.sql +++ b/backend/framework/domain/src/main/resources/migration/3.0.0/ddl/V3.0.0_11__system_setting.sql @@ -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 diff --git a/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiPlugin.java b/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiPlugin.java index dd91f94789..9675aa8061 100644 --- a/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiPlugin.java +++ b/backend/framework/plugin/metersphere-api-plugin-sdk/src/main/java/io/metersphere/plugin/api/api/AbstractApiPlugin.java @@ -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; diff --git a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java index 9fb815786a..dcead1c8d8 100644 --- a/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java +++ b/backend/framework/plugin/metersphere-platform-plugin-sdk/src/main/java/io/metersphere/plugin/platform/api/AbstractPlatformPlugin.java @@ -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; diff --git a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/AbstractMsPlugin.java b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/AbstractMsPlugin.java index 2bc3d1e629..73c5b021b5 100644 --- a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/AbstractMsPlugin.java +++ b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/AbstractMsPlugin.java @@ -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 getFrontendScript() { - List scriptList = new ArrayList<>(); - String scriptDirName = getScriptDir(); - URL scriptDir = pluginClassLoader.getResource(scriptDirName); - if (scriptDir != null) { - File resourceDir = new File(scriptDir.getFile()); - List 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 getFilePaths(File directory) { - List 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(); } } diff --git a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/MsPlugin.java b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/MsPlugin.java index 2f815f138a..623ab6b834 100644 --- a/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/MsPlugin.java +++ b/backend/framework/plugin/metersphere-plugin-sdk/src/main/java/io/metersphere/plugin/sdk/api/MsPlugin.java @@ -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 getFrontendScript(); - /** * @return 返回插件的版本 */ String getVersion(); + + /** + * @return 返回该加载前端配置文件的目录,默认是 script + * 可以重写定制 + */ + String getScriptDir(); } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/interceptor/SystemInterceptor.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/interceptor/SystemInterceptor.java index f213aca5ea..ee645145c0 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/interceptor/SystemInterceptor.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/config/interceptor/SystemInterceptor.java @@ -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; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaPluginTopicType.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaPluginTopicType.java new file mode 100644 index 0000000000..6cc787ab87 --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaPluginTopicType.java @@ -0,0 +1,6 @@ +package io.metersphere.sdk.constants; + +public class KafkaPluginTopicType { + public static final String ADD = "ADD"; + public static final String DELETE = "DELETE"; +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaTopicConstants.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaTopicConstants.java new file mode 100644 index 0000000000..5f170654ce --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/KafkaTopicConstants.java @@ -0,0 +1,5 @@ +package io.metersphere.sdk.constants; + +public class KafkaTopicConstants { + public static final String PLUGIN = "PLUGIN"; +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PluginScenarioType.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PluginScenarioType.java index 5f5b96ec66..a29c33e86c 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PluginScenarioType.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/constants/PluginScenarioType.java @@ -1,5 +1,5 @@ package io.metersphere.sdk.constants; public enum PluginScenarioType { - PAI, PLATFORM + API, PLATFORM } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/OrganizationOptionDTO.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/OrganizationOptionDTO.java deleted file mode 100644 index f455173175..0000000000 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/dto/OrganizationOptionDTO.java +++ /dev/null @@ -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; -} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/exception/MSException.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/exception/MSException.java index 9e8b8ea7a1..a851176682 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/exception/MSException.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/exception/MSException.java @@ -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; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileRepository.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileRepository.java index 40bdcd8e57..d354accf7c 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileRepository.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/FileRepository.java @@ -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 getFolderFileNames(FileRequest request) throws Exception; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/MinioRepository.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/MinioRepository.java index 4568ce9667..9c45ca4bd6 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/MinioRepository.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/file/MinioRepository.java @@ -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 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() diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/log/constants/OperationLogModule.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/log/constants/OperationLogModule.java index 515c350dda..5404922ae8 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/log/constants/OperationLogModule.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/log/constants/OperationLogModule.java @@ -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"; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginClassLoader.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginClassLoader.java index 5fe152cc36..dad09a64f3 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginClassLoader.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginClassLoader.java @@ -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 loadErrorMap = new HashMap<>(); + protected Map 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; } } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java index f48c922ba7..7bd388c59b 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/loader/PluginManager.java @@ -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 getImplInstance(String pluginId, Class superClazz) { try { Class clazz = getImplClass(pluginId, superClazz); + if (clazz == null) { + throw new MSException("未找到插件实现类"); + } return clazz.getConstructor().newInstance(); } catch (InvocationTargetException e) { LogUtils.error(e); diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MinioStorageStrategy.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MinioStorageStrategy.java deleted file mode 100644 index 0af2304682..0000000000 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MinioStorageStrategy.java +++ /dev/null @@ -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 删除文件 - } -} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MsStorageStrategy.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MsStorageStrategy.java new file mode 100644 index 0000000000..959b949afb --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/MsStorageStrategy.java @@ -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 getFolderFileNames(String dirName) throws Exception { + FileRequest request = getFileRequest(dirName); + List 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; + } +} diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/StorageStrategy.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/StorageStrategy.java index c5794566ab..5ce880f51f 100644 --- a/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/StorageStrategy.java +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/plugin/storage/StorageStrategy.java @@ -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 getFolderFileNames(String dirName) throws Exception; /** * 删除文件 * @throws IOException */ - void delete() throws IOException; + void delete() throws Exception; } diff --git a/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java new file mode 100644 index 0000000000..4ddd2cb06c --- /dev/null +++ b/backend/framework/sdk/src/main/java/io/metersphere/sdk/service/PluginLoadService.java @@ -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 getFrontendScripts(String pluginId) { + MsPlugin msPluginInstance = getMsPluginInstance(pluginId); + String scriptDir = msPluginInstance.getScriptDir(); + StorageStrategy storageStrategy = pluginManager.getClassLoader(pluginId).getStorageStrategy(); + try { + // 查询脚本文件名 + List folderFileNames = storageStrategy.getFolderFileNames(scriptDir); + // 获取脚本内容 + List 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 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; + } +} diff --git a/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties b/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties index 850cf551aa..bbc4393b1f 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_en_US.properties @@ -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 diff --git a/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties b/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties index 23c3354db8..778f0a1231 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_zh_CN.properties @@ -166,4 +166,8 @@ permission.system_plugin.name=插件 permission.system_plugin.read=查看插件 permission.system_plugin.add=创建插件 permission.system_plugin.edit=更新插件 -permission.system_plugin.delete=删除插件 \ No newline at end of file +permission.system_plugin.delete=删除插件 +plugin.exist=插件名称或文件名已存在 +plugin.type.exist=插件类型已存在 +plugin.script.exist=脚本id重复 +plugin.script.format=脚本格式错误 \ No newline at end of file diff --git a/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties b/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties index 84352711ce..8c480cfd83 100644 --- a/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties +++ b/backend/framework/sdk/src/main/resources/i18n/system_zh_TW.properties @@ -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=腳本格式錯誤 diff --git a/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/BaseTest.java b/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/BaseTest.java index 3c470c1620..e081484c5a 100644 --- a/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/BaseTest.java +++ b/backend/framework/sdk/src/test/java/io/metersphere/sdk/base/BaseTest.java @@ -163,6 +163,12 @@ public abstract class BaseTest { .andExpect(status().isOk()); } + protected ResultActions requestMultipart(String url, MultiValueMap 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 paramMap, Object... uriVariables) throws Exception { return this.requestMultipartWithOk(url, paramMap, uriVariables).andReturn(); } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PluginController.java b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PluginController.java index 3ae0397a52..2a44ca6cef 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PluginController.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/PluginController.java @@ -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); } } \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/result/SystemResultCode.java b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/result/SystemResultCode.java index 8fa0f5eb8d..5984e57ae7 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/controller/result/SystemResultCode.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/controller/result/SystemResultCode.java @@ -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; diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/PluginDTO.java b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/PluginDTO.java index 2d09828c05..dac3490618 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/dto/PluginDTO.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/dto/PluginDTO.java @@ -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 pluginForms; + private List pluginForms; @Schema(title = "关联的组织列表") - private List organizations; - - @Data - class PluginForm { - private String id; - private String name; - } + private List organizations; } \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/listener/PluginListener.java b/backend/services/system-setting/src/main/java/io/metersphere/system/listener/PluginListener.java new file mode 100644 index 0000000000..39aa408f0a --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/listener/PluginListener.java @@ -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 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; + } + } +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.java b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.java index ea69f6326f..333ed2638d 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.java @@ -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 selectOrganizationOptions(); + + List getOptionsByIds(@Param("ids") List ids); } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.xml b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.xml index a0f440daa9..47eea4af03 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.xml +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtOrganizationMapper.xml @@ -77,6 +77,12 @@ + diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.java b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.java new file mode 100644 index 0000000000..1a03b9e565 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.java @@ -0,0 +1,9 @@ +package io.metersphere.system.mapper; + +import io.metersphere.system.dto.PluginDTO; + +import java.util.List; + +public interface ExtPluginMapper { + List getPlugins(); +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.xml b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.xml new file mode 100644 index 0000000000..4a1bb3b2a4 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginMapper.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.java b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.java new file mode 100644 index 0000000000..f3816dc9ca --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.java @@ -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 getOptionByPluginIds(@Param("pluginIds") List pluginIds); +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.xml b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.xml new file mode 100644 index 0000000000..3fa43737a4 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/mapper/ExtPluginScriptMapper.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/request/PluginUpdateRequest.java b/backend/services/system-setting/src/main/java/io/metersphere/system/request/PluginUpdateRequest.java index 55f2538a59..e1442113bf 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/request/PluginUpdateRequest.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/request/PluginUpdateRequest.java @@ -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) diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/OrganizationService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/OrganizationService.java index 18bedb7999..05e8d176dc 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/service/OrganizationService.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/OrganizationService.java @@ -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 getOptionsByIds(List orgIds) { + if (CollectionUtils.isEmpty(orgIds)) { + return new ArrayList<>(0); + } + return extOrganizationMapper.getOptionsByIds(orgIds); + } } diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginLogService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginLogService.java new file mode 100644 index 0000000000..1049ea7be5 --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginLogService.java @@ -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; + } +} \ No newline at end of file diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginOrganizationService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginOrganizationService.java new file mode 100644 index 0000000000..979d147d6e --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginOrganizationService.java @@ -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 orgIds) { + if (CollectionUtils.isEmpty(orgIds)) { + return; + } + List 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 organizationIds) { + if (organizationIds == null) { + // 如果参数没填,则不更新 + return; + } + // 先删除关联关系 + deleteByPluginId(pluginId); + // 重新添加关联关系 + add(pluginId, organizationIds); + } + + public Map> getOrgMap(List pluginIds) { + if (CollectionUtils.isEmpty(pluginIds)) { + return Collections.emptyMap(); + } + // 查询插件和组织的关联关系 + PluginOrganizationExample example = new PluginOrganizationExample(); + example.createCriteria().andPluginIdIn(pluginIds); + List pluginOrganizations = pluginOrganizationMapper.selectByExample(example); + + // 查询组织信息 + List orgIds = pluginOrganizations.stream().map(PluginOrganization::getOrganizationId).toList(); + List orgList = organizationService.getOptionsByIds(orgIds); + Map orgInfoMap = orgList.stream().collect(Collectors.toMap(OptionDTO::getId, i -> i)); + + // 组装成 map + Map> 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; + } +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginScriptService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginScriptService.java new file mode 100644 index 0000000000..a49e1a029f --- /dev/null +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginScriptService.java @@ -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 frontendScript) { + Set ids = new HashSet<>(); + List 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> getScripteMap(List pluginIds) { + if (CollectionUtils.isEmpty(pluginIds)) { + return Collections.emptyMap(); + } + List scripts = extPluginScriptMapper.getOptionByPluginIds(pluginIds); + Map> scriptMap = new HashMap<>(); + for (PluginScript script : scripts) { + List 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; + + } +} diff --git a/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginService.java b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginService.java index a7f9e9ba65..1b8a3b8902 100644 --- a/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginService.java +++ b/backend/services/system-setting/src/main/java/io/metersphere/system/service/PluginService.java @@ -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 kafkaTemplate; public List list() { - return new ArrayList<>(); + List plugins = extPluginMapper.getPlugins(); + List pluginIds = plugins.stream().map(Plugin::getId).toList(); + Map> scripteMap = pluginScriptService.getScripteMap(pluginIds); + Map> 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 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); } } \ No newline at end of file diff --git a/backend/services/system-setting/src/main/resources/systemGeneratorConfig.xml b/backend/services/system-setting/src/main/resources/systemGeneratorConfig.xml index 088b64a1ca..fad011c7b6 100644 --- a/backend/services/system-setting/src/main/resources/systemGeneratorConfig.xml +++ b/backend/services/system-setting/src/main/resources/systemGeneratorConfig.xml @@ -78,7 +78,7 @@
-
+
diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/PluginControllerTests.java b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/PluginControllerTests.java index 8d5844b572..ed83729c15 100644 --- a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/PluginControllerTests.java +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/PluginControllerTests.java @@ -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 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 multiValueMap = getDefaultMultiPartParam(request, - new File("src/test/resources/application.properties")); + request.setGlobal(false); + request.setEnable(false); + request.setOrganizationIds(Arrays.asList(org.getId())); + MultiValueMap 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 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 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 scriptIds = pluginDTO.getPluginForms().stream().map(OptionDTO::getId).toList(); + Assertions.assertEquals(plugin, comparePlugin); + Assertions.assertEquals(scriptIds, getScriptIdsByPlugId(plugin.getId())); + List 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 getOrgIdsByPlugId(String pluginId) { + PluginOrganizationExample example = new PluginOrganizationExample(); + example.createCriteria().andPluginIdEqualTo(pluginId); + return pluginOrganizationMapper.selectByExample(example) + .stream() + .map(PluginOrganization::getOrganizationId) + .toList(); + } + + private List getScriptIdsByPlugId(String pluginId) { + PluginScriptExample example = new PluginScriptExample(); + example.createCriteria().andPluginIdEqualTo(pluginId); + return pluginScriptMapper.selectByExample(example) + .stream() + .map(PluginScript::getScriptId) + .toList(); + } } \ No newline at end of file diff --git a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/PluginUpdateRequestDefinition.java b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/PluginUpdateRequestDefinition.java index 9467acaef1..f3e886bd79 100644 --- a/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/PluginUpdateRequestDefinition.java +++ b/backend/services/system-setting/src/test/java/io/metersphere/system/controller/param/PluginUpdateRequestDefinition.java @@ -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; } diff --git a/backend/services/system-setting/src/test/resources/bootstrap.properties b/backend/services/system-setting/src/test/resources/bootstrap.properties index 62e94d254a..df85de4cef 100644 --- a/backend/services/system-setting/src/test/resources/bootstrap.properties +++ b/backend/services/system-setting/src/test/resources/bootstrap.properties @@ -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 \ No newline at end of file diff --git a/backend/services/system-setting/src/test/resources/file/metersphere-jira-plugin-3.x.jar b/backend/services/system-setting/src/test/resources/file/metersphere-jira-plugin-3.x.jar new file mode 100644 index 0000000000000000000000000000000000000000..f8b4cb2f6fa331dd107b8691cdc89814cd810311 GIT binary patch literal 4035 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 literal 0 HcmV?d00001 diff --git a/backend/services/system-setting/src/test/resources/file/metersphere-mqtt-plugin-3.x.jar b/backend/services/system-setting/src/test/resources/file/metersphere-mqtt-plugin-3.x.jar new file mode 100644 index 0000000000000000000000000000000000000000..75049c42ff464e0ab30cbb638e3913beb62b0e5b GIT binary patch literal 6706 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 g>4v_~>;IyuLl> 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 literal 0 HcmV?d00001 diff --git a/backend/services/system-setting/src/test/resources/file/metersphere-plugin-script-parse-error.jar b/backend/services/system-setting/src/test/resources/file/metersphere-plugin-script-parse-error.jar new file mode 100644 index 0000000000000000000000000000000000000000..7c9cdb194067d4e061b0ae6a6f04385a92337f2a GIT binary patch literal 6710 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 DXqR3Z literal 0 HcmV?d00001