feat(系统设置): 服务集成接口实现

--task=1012656 --user=陈建星 系统设置-组织-服务集成-后台 https://www.tapd.cn/55049933/s/1401952
This commit is contained in:
jianxing 2023-08-10 18:31:32 +08:00 committed by jianxing
parent 0120db4fc6
commit 8627f7b68f
37 changed files with 1071 additions and 291 deletions

View File

@ -18,6 +18,20 @@
<artifactId>metersphere-plugin-sdk</artifactId>
<version>${revision}</version>
</dependency>
<!-- 默认的http请求工具 restTemplate -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
</dependencies>
<properties>

View File

@ -0,0 +1,25 @@
package io.metersphere.plugin.platform.api;
import io.metersphere.plugin.platform.dto.PlatformRequest;
import io.metersphere.plugin.sdk.util.JSON;
import io.metersphere.plugin.sdk.util.MSPluginException;
import org.apache.commons.lang3.StringUtils;
public abstract class AbstractPlatform implements Platform {
protected PlatformRequest request;
public AbstractPlatform(PlatformRequest request) {
this.request = request;
}
public <T> T getIntegrationConfig(String integrationConfig, Class<T> clazz) {
if (StringUtils.isBlank(integrationConfig)) {
throw new MSPluginException("服务集成配置为空");
}
return JSON.parseObject(integrationConfig, clazz);
}
public String getPluginId() {
return request.getPluginId();
}
}

View File

@ -3,9 +3,48 @@ package io.metersphere.plugin.platform.api;
import io.metersphere.plugin.sdk.api.AbstractMsPlugin;
public abstract class AbstractPlatformPlugin extends AbstractMsPlugin {
private static final String PLATFORM_PLUGIN_TYPE = "PLATFORM";
private static final String DEFAULT_PLATFORM_PLUGIN_TYPE = "PLATFORM";
private static final String DEFAULT_INTEGRATION_SCRIPT_ID = "integration";
private static final String DEFAULT_PROJECT_SCRIPT_ID = "project";
private static final String DEFAULT_ACCOUNT_SCRIPT_ID = "account";
@Override
public String getType() {
return PLATFORM_PLUGIN_TYPE;
return DEFAULT_PLATFORM_PLUGIN_TYPE;
}
/**
* 返回插件的描述信息
* @return
*/
public abstract String getDescription();
/**
* 返回插件的logo路径比如/static/jira.png
* @return
*/
public abstract String getLogo();
/**
* 返回服务集成脚本的ID默认为 integration
* @return
*/
public String getIntegrationScriptId() {
return DEFAULT_INTEGRATION_SCRIPT_ID;
}
/**
* 返回项目配置脚本的ID默认为 project
* @return
*/
public String getProjectScriptId() {
return DEFAULT_PROJECT_SCRIPT_ID;
}
/**
* 返回个人账号脚本的ID默认为 account
* @return
*/
public String getAccountScriptId() {
return DEFAULT_ACCOUNT_SCRIPT_ID;
}
}

View File

@ -0,0 +1,108 @@
package io.metersphere.plugin.platform.api;
import io.metersphere.plugin.platform.utils.EnvProxySelector;
import io.metersphere.plugin.platform.utils.PluginCodingUtils;
import io.metersphere.plugin.sdk.util.JSON;
import io.metersphere.plugin.sdk.util.MSPluginException;
import io.metersphere.plugin.sdk.util.PluginLogUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.ssl.TrustStrategy;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.SSLContext;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
public abstract class BaseClient {
protected RestTemplate restTemplate;
public BaseClient() {
try {
TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(null, acceptingTrustStrategy)
.build();
SSLConnectionSocketFactory csf = SSLConnectionSocketFactoryBuilder
.create()
.setSslContext(sslContext)
.build();
CloseableHttpClient httpClient = HttpClients.custom()
// 可以支持设置系统代理
.setRoutePlanner(new SystemDefaultRoutePlanner(new EnvProxySelector()))
// 忽略 https
.setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(csf)
.build())
.build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
restTemplate = new RestTemplate(requestFactory);
} catch (Exception e) {
PluginLogUtils.error(e);
}
}
protected HttpHeaders getBasicHttpHeaders(String userName, String passWd) {
String authKey = PluginCodingUtils.base64Encoding(userName + ":" + passWd);
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(authKey);
headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
return headers;
}
protected HttpHeaders getBearHttpHeaders(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
return headers;
}
protected String getResult(ResponseEntity<String> response) {
int statusCodeValue = response.getStatusCodeValue();
PluginLogUtils.info("responseCode: " + statusCodeValue);
if (statusCodeValue >= 400) {
throw new MSPluginException(response.getBody());
}
PluginLogUtils.info("result: " + response.getBody());
return response.getBody();
}
protected Object getResultForList(Class clazz, ResponseEntity<String> response) {
return Arrays.asList(JSON.parseArray(getResult(response), clazz).toArray());
}
protected Object getResultForObject(Class clazz, ResponseEntity<String> response) {
return JSON.parseObject(getResult(response), clazz);
}
public void validateProxyUrl(String url, String... path) {
try {
if (!StringUtils.containsAny(new URI(url).getPath(), path)) {
// 只允许访问图片
throw new MSPluginException("illegal path");
}
} catch (URISyntaxException e) {
PluginLogUtils.error(e);
throw new MSPluginException("illegal path");
}
}
}

View File

@ -1,147 +1,14 @@
package io.metersphere.plugin.platform.api;
import io.metersphere.plugin.platform.dto.*;
import java.util.List;
/**
* 平台对接相关业务
* 平台对接相关业务接口
* @author jianxing.chen
*/
public interface Platform {
/**
* 获取平台相关需求
* 功能用例关联需求时调用
* @param projectConfig 项目设置表单值
* @return 需求列表
*/
List<DemandDTO> getDemands(String projectConfig);
/**
* 创建缺陷并封装 MS 返回
* 创建缺陷时调用
* @param bugsRequest bugRequest
* @return MS 缺陷
*/
MsBugDTO addBug(PlatformBugUpdateRequest bugsRequest);
/**
* 项目设置和缺陷表单中调用接口获取下拉框选项
* 配置文件的表单中选项值配置了 optionMethod 则调用获取表单的选项值
* @return 返回下拉列表
*/
List<SelectOption> getFormOptions(GetOptionRequest request);
/**
* 更新缺陷
* 编辑缺陷时调用
* @param request
* @return MS 缺陷
*/
MsBugDTO updateBug(PlatformBugUpdateRequest request);
/**
* 删除缺陷平台缺陷
* 删除缺陷时调用
* @param id 平台的缺陷 ID
*/
void deleteBug(String id);
/**
* 校验服务集成配置
* 服务集成点击校验时调用
*/
void validateIntegrationConfig();
/**
* 校验项目配置
* 项目设置成点击校验项目 key 时调用
*/
void validateProjectConfig(String projectConfig);
/**
* 校验用户配置配置
* 用户信息校验第三方信息时调用
*/
void validateUserConfig(String userConfig);
/**
* 支持附件上传
* 编辑缺陷上传附件是会调用判断是否支持附件上传
* 如果支持会调用 syncBugsAttachment 上传缺陷到第三方平台
*/
boolean isAttachmentUploadSupport();
/**
* 同步缺陷最新变更
* 开源用户点击同步缺陷时调用
*/
SyncBugResult syncBugs(SyncBugRequest request);
/**
* 同步项目下所有的缺陷
* 企业版用户会调用同步缺陷
*/
void syncAllBugs(SyncAllBugRequest request);
/**
* 获取附件内容
* 同步缺陷中同步附件时会调用
* @param fileKey 文件关键字
*/
byte[] getAttachmentContent(String fileKey);
/**
* 获取第三方平台缺陷的自定义字段
* 需要 PluginMetaInfo isThirdPartTemplateSupport 返回 true
* @return
*/
List<PlatformCustomFieldItemDTO> getThirdPartCustomField(String projectConfig);
/**
* Get请求的代理
* 目前使用场景富文本框中如果有图片是存储在第三方平台MS 通过 url 访问
* 这时如果第三方平台需要登入才能访问到静态资源可以通过将富文本框图片内容构造如下格式访问
* ![name](/resource/md/get/path?platform=Jira?project_id=&organization_id=&path=)
* @param path
* @return
*/
Object proxyForGet(String path, Class responseEntityClazz);
/**
* 同步 MS 缺陷附件到第三方平台
* isAttachmentUploadSupport 返回为 true 同步和创建缺陷时会调用
*/
void syncBugsAttachment(SyncBugAttachmentRequest request);
/**
* 获取第三方平台的状态列表
* 缺陷列表和编辑缺陷时会调用
* @return
*/
List<PlatformStatusDTO> getStatusList(String projectConfig);
/**
* 获取第三方平台的状态转移列表
* 即编辑缺陷时的可选状态
* 默认会调用 getStatusList可重写覆盖
* @param bugId
* @return
*/
List<PlatformStatusDTO> getTransitions(String projectConfig, String bugId);
/**
* 用例关联需求时调用
* 可在第三方平台添加用例和需求的关联关系
* @param request
*/
void handleDemandUpdate(DemandUpdateRequest request);
/**
* 用例批量关联需求时调用
* 可在第三方平台添加用例和需求的关联关系
* @param request
*/
void handleDemandUpdateBatch(DemandUpdateRequest request);
}

View File

@ -8,5 +8,6 @@ import lombok.Setter;
public class PlatformRequest {
private String integrationConfig;
private String organizationId;
private String userPlatformInfo;
private String userAccountConfig;
private String pluginId;
}

View File

@ -0,0 +1,45 @@
package io.metersphere.plugin.platform.utils;
import io.metersphere.plugin.sdk.util.PluginLogUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.net.*;
import java.util.Arrays;
import java.util.List;
public class EnvProxySelector extends ProxySelector {
ProxySelector defaultProxySelector = ProxySelector.getDefault();
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
PluginLogUtils.error("connectFailed : " + uri);
}
/**
* 获取环境变量http代理配置
*
* @param uri
* @return
*/
@Override
public List<Proxy> select(URI uri) {
String httpProxy = System.getenv("http_proxy");
String httpsProxy = System.getenv("https_proxy");
URI proxy = null;
try {
if (StringUtils.equalsIgnoreCase(uri.getScheme(), "https")
&& StringUtils.isNotBlank(httpsProxy)) {
proxy = new URI(httpsProxy);
} else if (StringUtils.isNotBlank(httpProxy)) {
proxy = new URI(httpProxy);
}
if (proxy != null) {
return Arrays.asList(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxy.getHost(), proxy.getPort())));
}
} catch (URISyntaxException e) {
PluginLogUtils.error(e);
}
return defaultProxySelector.select(uri);
}
}

View File

@ -0,0 +1,69 @@
package io.metersphere.plugin.platform.utils;
import io.metersphere.plugin.sdk.util.PluginLogUtils;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Method;
public class PluginBeanUtils {
public static <T> T copyBean(T target, Object source) {
try {
org.springframework.beans.BeanUtils.copyProperties(source, target);
return target;
} catch (Exception e) {
throw new RuntimeException("Failed to copy object: ", e);
}
}
public static <T> T copyBean(T target, Object source, String... ignoreProperties) {
try {
org.springframework.beans.BeanUtils.copyProperties(source, target, ignoreProperties);
return target;
} catch (Exception e) {
throw new RuntimeException("Failed to copy object: ", e);
}
}
public static Object getFieldValueByName(String fieldName, Object bean) {
try {
if (StringUtils.isBlank(fieldName)) {
return null;
}
String firstLetter = fieldName.substring(0, 1).toUpperCase();
String getter = "get" + firstLetter + fieldName.substring(1);
Method method = bean.getClass().getMethod(getter);
return method.invoke(bean);
} catch (Exception e) {
PluginLogUtils.error("failed to getFieldValueByName. ", e);
return null;
}
}
public static void setFieldValueByName(Object bean, String fieldName, Object value, Class<?> type) {
try {
if (StringUtils.isBlank(fieldName)) {
return;
}
String firstLetter = fieldName.substring(0, 1).toUpperCase();
String setter = "set" + firstLetter + fieldName.substring(1);
Method method = bean.getClass().getMethod(setter, type);
method.invoke(bean, value);
} catch (Exception e) {
PluginLogUtils.error("failed to setFieldValueByName. ", e);
}
}
public static Method getMethod(Object bean, String fieldName, Class<?> type) {
try {
if (StringUtils.isBlank(fieldName)) {
return null;
}
String firstLetter = fieldName.substring(0, 1).toUpperCase();
String setter = "set" + firstLetter + fieldName.substring(1);
return bean.getClass().getMethod(setter, type);
} catch (Exception e) {
return null;
}
}
}

View File

@ -0,0 +1,168 @@
package io.metersphere.plugin.platform.utils;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/**
* 加密解密工具
*
* @author kun.mo
*/
public class PluginCodingUtils {
private static final String UTF_8 = StandardCharsets.UTF_8.name();
private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
/**
* MD5加密
*
* @param src 要加密的串
* @return 加密后的字符串
*/
public static String md5(String src) {
return md5(src, UTF_8);
}
/**
* MD5加密
*
* @param src 要加密的串
* @param charset 加密字符集
* @return 加密后的字符串
*/
public static String md5(String src, String charset) {
try {
byte[] strTemp = StringUtils.isEmpty(charset) ? src.getBytes() : src.getBytes(charset);
MessageDigest mdTemp = MessageDigest.getInstance("MD5");
mdTemp.update(strTemp);
byte[] md = mdTemp.digest();
int j = md.length;
char[] str = new char[j * 2];
int k = 0;
for (byte byte0 : md) {
str[k++] = HEX_DIGITS[byte0 >>> 4 & 0xf];
str[k++] = HEX_DIGITS[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
throw new RuntimeException("MD5 encrypt error:", e);
}
}
/**
* BASE64解密
*
* @param src 待解密的字符串
* @return 解密后的字符串
*/
public static String base64Decoding(String src) {
byte[] b;
String result = null;
if (src != null) {
try {
b = Base64.decodeBase64(src);
result = new String(b, UTF_8);
} catch (Exception e) {
throw new RuntimeException("BASE64 decoding error:", e);
}
}
return result;
}
/**
* BASE64加密
*
* @param src 待加密的字符串
* @return 加密后的字符串
*/
public static String base64Encoding(String src) {
String result = null;
if (src != null) {
try {
result = Base64.encodeBase64String(src.getBytes(UTF_8));
} catch (Exception e) {
throw new RuntimeException("BASE64 encoding error:", e);
}
}
return result;
}
/**
* AES加密
*
* @param src 待加密字符串
* @param secretKey 密钥
* @param iv 向量
* @return 加密后字符串
*/
public static String aesEncrypt(String src, String secretKey, String iv) {
if (StringUtils.isBlank(secretKey)) {
throw new RuntimeException("secretKey is empty");
}
try {
byte[] raw = secretKey.getBytes(UTF_8);
SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "AES");
// "算法/模式/补码方式" ECB
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv1 = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv1);
byte[] encrypted = cipher.doFinal(src.getBytes(UTF_8));
return Base64.encodeBase64String(encrypted);
} catch (Exception e) {
throw new RuntimeException("AES encrypt error:", e);
}
}
/**
* AES 解密
*
* @param src 待解密字符串
* @param secretKey 密钥
* @param iv 向量
* @return 解密后字符串
*/
public static String aesDecrypt(String src, String secretKey, String iv) {
if (StringUtils.isBlank(secretKey)) {
throw new RuntimeException("secretKey is empty");
}
try {
byte[] raw = secretKey.getBytes(UTF_8);
SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv1 = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, iv1);
byte[] encrypted1 = Base64.decodeBase64(src);
byte[] original = cipher.doFinal(encrypted1);
return new String(original, UTF_8);
} catch (BadPaddingException | IllegalBlockSizeException e) {
// 解密的原字符串为非加密字符串则直接返回原字符串
return src;
} catch (Exception e) {
throw new RuntimeException("decrypt errorplease check parameters", e);
}
}
public static String secretKey() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(128);
SecretKey secretKey = keyGen.generateKey();
return Base64.encodeBase64String(secretKey.getEncoded());
} catch (Exception e) {
throw new RuntimeException("generate secretKey error", e);
}
}
}

View File

@ -0,0 +1,23 @@
package io.metersphere.plugin.sdk.util;
/**
* 插件异常类
* @author jianxing
*/
public class MSPluginException extends RuntimeException {
public MSPluginException() {
super();
}
public MSPluginException(String message) {
super(message);
}
public MSPluginException(String message, Throwable cause) {
super(message, cause);
}
public MSPluginException(Throwable cause) {
super(cause);
}
}

View File

@ -8,7 +8,6 @@
<groupId>io.metersphere</groupId>
<artifactId>framework</artifactId>
<version>${revision}</version>
</parent>
<groupId>io.metersphere</groupId>
@ -25,29 +24,4 @@
<module>metersphere-api-plugin-sdk</module>
<module>metersphere-platform-plugin-sdk</module>
</modules>
<dependencies>
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_core</artifactId>
<version>${jmeter.version}</version>
</dependency>
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>jorphan</artifactId>
<version>${jmeter.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -3,6 +3,7 @@ package io.metersphere.sdk.controller.handler;
import io.metersphere.sdk.controller.handler.result.IResultCode;
import io.metersphere.sdk.controller.handler.result.MsHttpResultCode;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.util.Translator;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.shiro.ShiroException;
@ -76,11 +77,11 @@ public class RestControllerExceptionHandler {
if (errorCode instanceof MsHttpResultCode) {
// 如果是 MsHttpResultCode则设置响应的状态码取状态码的后三位
return ResponseEntity.status(errorCode.getCode() % 1000)
.body(ResultHolder.error(errorCode.getCode(), errorCode.getMessage()));
.body(ResultHolder.error(errorCode.getCode(), Translator.get(errorCode.getMessage(), errorCode.getMessage())));
} else {
// 响应码返回 500设置业务状态码
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ResultHolder.error(errorCode.getCode(), errorCode.getMessage(), e.getMessage()));
.body(ResultHolder.error(errorCode.getCode(), Translator.get(errorCode.getMessage(), errorCode.getMessage()), e.getMessage()));
}
}

View File

@ -13,7 +13,10 @@ public enum CommonResultCode implements IResultCode {
* 调用获取全局用户组接口如果操作的是内置的用户组会返回该响应码
*/
INTERNAL_USER_ROLE_PERMISSION(101003, "internal_user_role_permission_error"),
USER_ROLE_RELATION_REMOVE_ADMIN_USER_PERMISSION(100004, "user_role_relation_remove_admin_user_permission_error");
USER_ROLE_RELATION_REMOVE_ADMIN_USER_PERMISSION(100004, "user_role_relation_remove_admin_user_permission_error"),
FILE_NAME_ILLEGAL(100005, "file_name_illegal_error"),
PLUGIN_ENABLE(100006, "plugin_enable_error"),
PLUGIN_PERMISSION(100007, "plugin_permission_error");
private int code;
private String message;

View File

@ -9,10 +9,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.*;
/**
* @author jianxing.chen
@ -24,6 +21,14 @@ public class PluginManager {
*/
protected Map<String, PluginClassLoader> classLoaderMap = new HashMap<>();
/**
* 缓存查找过的类
* 内层 map
* key 未接口的类
* value 为实现类
*/
protected Map<String, Map<Class, Class>> implClassCache = new HashMap<>();
public PluginClassLoader getClassLoader(String pluginId) {
return classLoaderMap.get(pluginId);
}
@ -48,6 +53,7 @@ public class PluginManager {
public void deletePlugin(String id) {
classLoaderMap.remove(id);
implClassCache.remove(id);
}
/**
@ -70,40 +76,52 @@ public class PluginManager {
* 获取接口的单一实现类
*/
public <T> Class<T> getImplClass(String pluginId, Class<T> superClazz) {
PluginClassLoader classLoader = classLoaderMap.get(pluginId);
PluginClassLoader classLoader = getPluginClassLoader(pluginId);
Map<Class, Class> classes = implClassCache.get(pluginId);
if (classes == null) {
classes = new HashMap<>();
implClassCache.put(pluginId, classes);
}
if (classes.get(superClazz) != null) {
return classes.get(superClazz);
}
LinkedHashSet<Class<T>> result = new LinkedHashSet<>();
Set<Class> clazzSet = classLoader.getClazzSet();
for (Class item : clazzSet) {
if (isImplClazz(superClazz, item) && !result.contains(item)) {
classes.put(superClazz, item);
return item;
}
}
return null;
}
private PluginClassLoader getPluginClassLoader(String pluginId) {
PluginClassLoader classLoader = classLoaderMap.get(pluginId);
if (classLoader == null) {
throw new MSException("插件未加载");
}
return classLoader;
}
/**
* 获取指定接口最后一次加载的实现类实例
*/
public <T> T getImplInstance(String pluginId, Class<T> superClazz) {
try {
Class<T> clazz = getImplClass(pluginId, superClazz);
if (clazz == null) {
throw new MSException("未找到插件实现类");
}
return clazz.getConstructor().newInstance();
} catch (InvocationTargetException e) {
LogUtils.error(e);
throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE, e.getTargetException().getMessage());
} catch (Exception e) {
LogUtils.error(e);
throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE, e.getMessage());
}
return this.getImplInstance(pluginId, superClazz, null);
}
public <T> T getImplInstance(String pluginId, Class<T> superClazz, Object param) {
try {
Class<T> clazz = getImplClass(pluginId, superClazz);
return clazz.getConstructor(param.getClass()).newInstance(param);
if (clazz == null) {
throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE);
}
if (param == null) {
return clazz.getConstructor().newInstance();
} else {
return clazz.getConstructor(param.getClass()).newInstance(param);
}
} catch (InvocationTargetException e) {
LogUtils.error(e.getTargetException());
throw new MSException(CommonResultCode.PLUGIN_GET_INSTANCE, e.getTargetException().getMessage());

View File

@ -0,0 +1,108 @@
package io.metersphere.sdk.service;
import io.metersphere.plugin.platform.api.Platform;
import io.metersphere.plugin.platform.dto.PlatformRequest;
import io.metersphere.sdk.constants.PluginScenarioType;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.system.domain.*;
import io.metersphere.system.mapper.PluginMapper;
import io.metersphere.system.mapper.PluginOrganizationMapper;
import io.metersphere.system.mapper.ServiceIntegrationMapper;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static io.metersphere.sdk.controller.handler.result.CommonResultCode.PLUGIN_ENABLE;
import static io.metersphere.sdk.controller.handler.result.CommonResultCode.PLUGIN_PERMISSION;
@Service
@Transactional(rollbackFor = Exception.class)
public class PlatformPluginService {
@Resource
private PluginLoadService pluginLoadService;
@Resource
private ServiceIntegrationMapper serviceIntegrationMapper;
@Resource
private PluginMapper pluginMapper;
@Resource
private PluginOrganizationMapper pluginOrganizationMapper;
/**
* 获取平台实例
*
* @param pluginId
* @param orgId
* @param integrationConfig
* @return
*/
public Platform getPlatform(String pluginId, String orgId, String integrationConfig) {
if (StringUtils.isNotBlank(orgId)) {
// 服务集成的测试链接不需要校验插件开启和状态
Plugin plugin = pluginMapper.selectByPrimaryKey(pluginId);
checkPluginEnable(plugin);
checkPluginPermission(pluginId, orgId, plugin);
}
PlatformRequest pluginRequest = new PlatformRequest();
pluginRequest.setIntegrationConfig(integrationConfig);
pluginRequest.setOrganizationId(orgId);
return pluginLoadService.getImplInstance(pluginId, Platform.class, pluginRequest);
}
public Platform getPlatform(String pluginId, String orgId) {
// 这里会校验插件是否存在
pluginLoadService.getMsPluginInstance(pluginId);
ServiceIntegration serviceIntegration = getServiceIntegrationByPluginId(pluginId);
return getPlatform(pluginId, orgId, new String(serviceIntegration.getConfiguration()));
}
/**
* 校验该组织是否能访问插件
* @param pluginId
* @param orgId
* @param plugin
*/
private void checkPluginPermission(String pluginId, String orgId, Plugin plugin) {
if (plugin.getGlobal()) {
return;
}
PluginOrganizationExample example = new PluginOrganizationExample();
example.createCriteria()
.andOrganizationIdEqualTo(orgId)
.andPluginIdEqualTo(pluginId);
List<PluginOrganization> pluginOrganizations = pluginOrganizationMapper.selectByExample(example);
for (PluginOrganization pluginOrganization : pluginOrganizations) {
if (StringUtils.equals(pluginOrganization.getOrganizationId(), orgId)) {
return;
}
}
throw new MSException(PLUGIN_PERMISSION);
}
/**
* 校验插件是否启用
* @param plugin
*/
private static void checkPluginEnable(Plugin plugin) {
if (BooleanUtils.isFalse(plugin.getEnable())) {
throw new MSException(PLUGIN_ENABLE);
}
}
private ServiceIntegration getServiceIntegrationByPluginId(String pluginId) {
ServiceIntegrationExample example = new ServiceIntegrationExample();
example.createCriteria().andPluginIdEqualTo(pluginId);
return serviceIntegrationMapper.selectByExampleWithBLOBs(example).get(0);
}
public List<Plugin> getPlatformPlugins() {
PluginExample example = new PluginExample();
example.createCriteria()
.andScenarioEqualTo(PluginScenarioType.PLATFORM.name());
return pluginMapper.selectByExample(example);
}
}

View File

@ -1,15 +1,19 @@
package io.metersphere.sdk.service;
import io.metersphere.plugin.platform.api.AbstractPlatformPlugin;
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.JSON;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.system.domain.Plugin;
import io.metersphere.system.domain.PluginExample;
import io.metersphere.system.domain.PluginScript;
import io.metersphere.system.mapper.PluginMapper;
import io.metersphere.system.mapper.PluginScriptMapper;
import jakarta.annotation.Resource;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
@ -21,6 +25,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author jianxing
@ -33,6 +39,8 @@ public class PluginLoadService {
@Resource
private PluginMapper pluginMapper;
@Resource
private PluginScriptMapper pluginScriptMapper;
/**
* 上传插件到 minio
@ -178,6 +186,28 @@ public class PluginLoadService {
return pluginManager.getImplInstance(id, MsPlugin.class);
}
public <T> T getImplInstance(String pluginId, Class<T> superClazz, Object param) {
return pluginManager.getImplInstance(pluginId, superClazz, param);
}
public <T> T getImplInstance(String pluginId, Class<T> superClazz) {
return pluginManager.getImplInstance(pluginId, superClazz);
}
public List<AbstractPlatformPlugin> getPlatformPluginInstanceList() {
return getImplInstanceList(AbstractPlatformPlugin.class);
}
public AbstractPlatformPlugin getPlatformPluginInstance(String pluginId) {
return getImplInstance(pluginId, AbstractPlatformPlugin.class);
}
public <T> List<T> getImplInstanceList(Class<T> clazz, Object... initArgs) {
return pluginManager.getClassLoaderMap().keySet().stream()
.map(pluginId -> pluginManager.getImplInstance(pluginId, clazz, initArgs)
).collect(Collectors.toList());
}
public boolean hasPluginKey(String currentPluginId, String pluginKey) {
for (String pluginId : pluginManager.getClassLoaderMap().keySet()) {
MsPlugin msPlugin = getMsPluginInstance(pluginId);
@ -187,4 +217,17 @@ public class PluginLoadService {
}
return false;
}
public InputStream getResourceAsStream(String pluginId, String name) {
return pluginManager.getClassLoaderMap().get(pluginId).getResourceAsStream(name);
}
public Map getPluginScriptConfig(String pluginId, String scriptId) {
PluginScript pluginScript = pluginScriptMapper.selectByPrimaryKey(pluginId, scriptId);
return JSON.parseMap(new String(pluginScript.getScript()));
}
public Object getPluginScriptContent(String pluginId, String scriptId) {
return getPluginScriptConfig(pluginId, scriptId).get("script");
}
}

View File

@ -55,7 +55,7 @@ public class FilterChainUtils {
filterChainDefinitionMap.put("/websocket/**", "csrf");
// 获取插件中的图片
filterChainDefinitionMap.put("/platform/plugin/image/**", "anon");
filterChainDefinitionMap.put("/plugin/image/**", "anon");
return filterChainDefinitionMap;
}

View File

@ -427,4 +427,8 @@ permission.add=Create
permission.edit=Update
permission.delete=Delete
permission.import=Import
permission.recover=Recover
permission.recover=Recover
file_name_illegal_error=File name is invalid
plugin_enable_error=Plugin is not enabled
plugin_permission_error=No access to this plugin

View File

@ -426,4 +426,8 @@ permission.add=创建
permission.edit=修改
permission.delete=删除
permission.import=导入
permission.recover=恢复
permission.recover=恢复
file_name_illegal_error=文件名不合法
plugin_enable_error=插件未启用
plugin_permission_error=没有该插件的访问权限

View File

@ -426,4 +426,8 @@ permission.add=創建
permission.edit=修改
permission.delete=刪除
permission.import=導入
permission.recover=恢復
permission.recover=恢復
file_name_illegal_error=文件名不合法
plugin_enable_error=插件未啟用
plugin_permission_error=沒有該插件的訪問權限

View File

@ -175,6 +175,7 @@ service_integration.plugin_id.length_range=pluginId length must be between {min}
service_integration.organization_id.not_blank=organizationId cannot be empty
service_integration.organization_id.length_range=organizationId length must be between {min} and {max}
service_integration_exist_error=Service integration configuration already exists
service_integration.configuration.not_blank=Service integration configuration cannot be empty
# permission
permission.system_plugin.name=Plugin
permission.system_organization_project.name=Organization Project

View File

@ -160,20 +160,21 @@ plugin.file_name.not_blank=文件名不能为空
plugin.file_name.length_range=文件名长度必须在{min}和{max}之间
plugin.create_user.not_blank=创建人不能为空
plugin.create_user.length_range=创建人长度必须在{min}和{max}之间
plugin.scenario.not_blank=插件使用场景PAI/PLATFORM不能为空
plugin.scenario.length_range=插件使用场景PAI/PLATFORM长度必须在{min}和{max}之间
plugin.scenario.not_blank=插件使用场景API/PLATFORM不能为空
plugin.scenario.length_range=插件使用场景API/PLATFORM长度必须在{min}和{max}之间
plugin.exist=插件名称或文件名已存在
plugin.type.exist=插件类型已存在
plugin.script.exist=脚本id重复
plugin.script.format=脚本格式错误
# serviceIntegration
service_integration.id.not_blank=ID不能
service_integration.id.length_range=ID長度必須在{min}和{max}之间
service_integration.plugin_id.not_blank=插件的ID不能
service_integration.plugin_id.length_range=插件的ID長度必須在{min}和{max}之间
service_integration.organization_id.not_blank=组织ID不能
service_integration.organization_id.length_range=组织ID長度必須在{min}和{max}之间
service_integration.id.not_blank=ID不能
service_integration.id.length_range=ID长度必须在{min}和{max}之间
service_integration.plugin_id.not_blank=插件的ID不能
service_integration.plugin_id.length_range=插件的ID长度必须在{min}和{max}之间
service_integration.organization_id.not_blank=组织ID不能
service_integration.organization_id.length_range=组织ID长度必须在{min}和{max}之间
service_integration_exist_error=服务集成配置已存在
service_integration.configuration.not_blank=服务集成配置不能為空
# permission
permission.system_plugin.name=插件
permission.system_organization_project.name=组织与项目

View File

@ -174,6 +174,7 @@ service_integration.plugin_id.length_range=插件的ID长度必须在{min}和{ma
service_integration.organization_id.not_blank=组织ID不能为空
service_integration.organization_id.length_range=组织ID长度必须在{min}和{max}之间
service_integration_exist_error=服務集成配置已存在
service_integration.configuration.not_blank=服务集成配置不能為空
# permission
permission.system_plugin.name=插件
permission.system_organization_project.name=組織與項目

View File

@ -40,6 +40,8 @@ import org.springframework.util.MultiValueMap;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
@ -232,20 +234,24 @@ public abstract class BaseTest {
}
protected <T> T getResultData(MvcResult mvcResult, Class<T> clazz) throws Exception {
Object data = JSON.parseMap(mvcResult.getResponse().getContentAsString()).get("data");
Object data = parseResponse(mvcResult).get("data");
return JSON.parseObject(JSON.toJSONString(data), clazz);
}
protected <T> T getResultMessageDetail(MvcResult mvcResult, Class<T> clazz) throws Exception {
Object data = JSON.parseMap(mvcResult.getResponse().getContentAsString()).get("messageDetail");
Object data = parseResponse(mvcResult).get("messageDetail");
return JSON.parseObject(JSON.toJSONString(data), clazz);
}
protected <T> List<T> getResultDataArray(MvcResult mvcResult, Class<T> clazz) throws Exception {
Object data = JSON.parseMap(mvcResult.getResponse().getContentAsString()).get("data");
Object data = parseResponse(mvcResult).get("data");
return JSON.parseArray(JSON.toJSONString(data), clazz);
}
private static Map parseResponse(MvcResult mvcResult) throws UnsupportedEncodingException {
return JSON.parseMap(mvcResult.getResponse().getContentAsString(Charset.defaultCharset()));
}
/**
* 校验错误响应码
*/
@ -258,7 +264,7 @@ public abstract class BaseTest {
}
protected <T> Pager<List<T>> getPageResult(MvcResult mvcResult, Class<T> clazz) throws Exception {
Map<String, Object> pagerResult = (Map<String, Object>) JSON.parseMap(mvcResult.getResponse().getContentAsString()).get("data");
Map<String, Object> pagerResult = (Map<String, Object>) parseResponse(mvcResult).get("data");
List<T> list = JSON.parseArray(JSON.toJSONString(pagerResult.get("list")), clazz);
Pager pager = new Pager();
pager.setPageSize(Long.valueOf(pagerResult.get("pageSize").toString()));

View File

@ -2,7 +2,7 @@ package io.metersphere.sdk.base.param;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.*;
/**
* @author jianxing
@ -14,6 +14,15 @@ public class NotEmptyParamGenerator extends ParamGenerator {
*/
@Override
public Object invalidGenerate(Annotation annotation, Field field) {
return new ArrayList<>(0);
if (field.getType().equals(List.class)) {
return new ArrayList<>(0);
}
if (field.getType().equals(Set.class)) {
return new HashSet<>(0);
}
if (field.getType().equals(Map.class)) {
return new HashMap<>(0);
}
throw new RuntimeException("不支持该类型");
}
}

View File

@ -22,6 +22,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
/**
@ -86,7 +87,7 @@ public class PluginController {
@Schema(description = "图片路径", requiredMode = Schema.RequiredMode.REQUIRED)
@RequestParam("imagePath")
String imagePath,
HttpServletResponse response) {
// todo
HttpServletResponse response) throws IOException {
pluginService.getPluginImg(pluginId, imagePath, response);
}
}

View File

@ -14,12 +14,13 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.NotEmpty;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author : jianxing
@ -32,6 +33,7 @@ public class ServiceIntegrationController {
@Resource
private ServiceIntegrationService serviceIntegrationService;
@GetMapping("/list/{organizationId}")
@Operation(summary = "获取服务集成列表")
@RequiresPermissions(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_READ)
@ -59,24 +61,27 @@ public class ServiceIntegrationController {
@Operation(summary = "删除服务集成")
@RequiresPermissions(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_DELETE)
@Log(type = OperationLogType.DELETE, expression = "#msClass.deleteLog(#id)", msClass = ServiceIntegrationLogService.class)
public String delete(@PathVariable String id) {
return serviceIntegrationService.delete(id);
public void delete(@PathVariable String id) {
serviceIntegrationService.delete(id);
}
@PostMapping("/validate")
@PostMapping("/validate/{pluginId}")
@Operation(summary = "校验服务集成信息")
@RequiresPermissions(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE)
public boolean validate(@Validated({Updated.class}) @RequestBody
@Schema(description = "配置的表单键值对", requiredMode = Schema.RequiredMode.REQUIRED)
Map<String, String> serviceIntegrationInfo) {
return serviceIntegrationService.validate(serviceIntegrationInfo);
public void validate(@PathVariable String pluginId,
@Validated({Updated.class})
@RequestBody
@NotEmpty
@Schema(description = "配置的表单键值对", requiredMode = Schema.RequiredMode.REQUIRED)
HashMap<String, String> serviceIntegrationInfo) {
serviceIntegrationService.validate(pluginId, serviceIntegrationInfo);
}
@GetMapping("/validate/{id}")
@Operation(summary = "校验服务集成信息")
@RequiresPermissions(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE)
public boolean validate(@PathVariable String id) {
return serviceIntegrationService.validate(id);
public void validate(@PathVariable String id) {
serviceIntegrationService.validate(id);
}
@GetMapping("/script/{pluginId}")

View File

@ -20,7 +20,7 @@ public class ServiceIntegrationDTO implements Serializable {
private String title;
@Schema(description = "插件描述")
private String describe;
private String description;
@Schema(description = "是否启用")
private Boolean enable;

View File

@ -28,7 +28,7 @@ public class PluginUpdateRequest {
private Boolean global;
@Schema(description = "插件描述")
@Size(min = 1, max = 500, message = "{plugin.scenario.length_range}", groups = {Created.class, Updated.class})
@Size(max = 500, message = "{plugin.scenario.length_range}", groups = {Created.class, Updated.class})
private String description;
@Schema(hidden = true)

View File

@ -1,12 +1,14 @@
package io.metersphere.system.request;
import io.metersphere.validation.groups.Created;
import io.metersphere.validation.groups.Updated;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serializable;
import java.util.Map;
@ -17,7 +19,7 @@ public class ServiceIntegrationUpdateRequest implements Serializable {
@Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{service_integration.id.not_blank}", groups = {Updated.class})
@Size(min = 1, max = 50, message = "{service_integration.id.length_range}", groups = {Created.class, Updated.class})
@Size(min = 1, max = 50, message = "{service_integration.id.length_range}", groups = {Updated.class})
private String id;
@Schema(description = "插件的ID", requiredMode = Schema.RequiredMode.REQUIRED)
@ -34,6 +36,6 @@ public class ServiceIntegrationUpdateRequest implements Serializable {
private String organizationId;
@Schema(description = "配置的表单键值对", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "{service_integration.configuration.not_blank}", groups = {Created.class})
private Map<String, String> configuration;
@NotEmpty(message = "{service_integration.configuration.not_blank}", groups = {Created.class})
private Map<String, Object> configuration;
}

View File

@ -4,6 +4,7 @@ package io.metersphere.system.service;
import io.metersphere.plugin.sdk.api.MsPlugin;
import io.metersphere.sdk.constants.KafkaPluginTopicType;
import io.metersphere.sdk.constants.KafkaTopicConstants;
import io.metersphere.sdk.controller.handler.result.CommonResultCode;
import io.metersphere.sdk.dto.OptionDTO;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.service.PluginLoadService;
@ -15,6 +16,7 @@ import io.metersphere.system.mapper.ExtPluginMapper;
import io.metersphere.system.mapper.PluginMapper;
import io.metersphere.system.request.PluginUpdateRequest;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
@ -23,10 +25,10 @@ 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 java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.*;
import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_EXIST;
import static io.metersphere.system.controller.result.SystemResultCode.PLUGIN_TYPE_EXIST;
@ -138,8 +140,9 @@ public class PluginService {
}
/**
* 通知其他节点加载插件
* 这里需要传一下 fileName事务未提交查询不到文件名
* 通知其他节点加载插件
* 这里需要传一下 fileName事务未提交查询不到文件名
*
* @param pluginId
* @param fileName
*/
@ -149,7 +152,8 @@ public class PluginService {
}
/**
* 通知其他节点卸载插件
* 通知其他节点卸载插件
*
* @param pluginId
*/
public void notifiedPluginDelete(String pluginId) {
@ -201,4 +205,34 @@ public class PluginService {
public String getScript(String pluginId, String scriptId) {
return pluginScriptService.get(pluginId, scriptId);
}
public void getPluginImg(String pluginId, String filePath, HttpServletResponse response) throws IOException {
validateImageFileName(filePath);
InputStream inputStream = pluginLoadService.getResourceAsStream(pluginId, filePath);
writeImage(filePath, inputStream, response);
}
public void validateImageFileName(String filename) {
Set<String> imgSuffix = new HashSet<>() {{
add("jpg");
add("png");
add("gif");
add("jpeg");
}};
if (!imgSuffix.contains(StringUtils.substringAfterLast(filename, "."))) {
throw new MSException(CommonResultCode.FILE_NAME_ILLEGAL);
}
}
public void writeImage(String filePath, InputStream in, HttpServletResponse response) throws IOException {
response.setContentType("image/" + StringUtils.substringAfterLast(filePath, "."));
try (OutputStream out = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
response.getOutputStream().write(buffer, 0, bytesRead);
}
out.flush();
}
}
}

View File

@ -1,8 +1,13 @@
package io.metersphere.system.service;
import io.metersphere.plugin.platform.api.AbstractPlatformPlugin;
import io.metersphere.plugin.platform.api.Platform;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.service.PlatformPluginService;
import io.metersphere.sdk.service.PluginLoadService;
import io.metersphere.sdk.util.BeanUtils;
import io.metersphere.sdk.util.JSON;
import io.metersphere.system.domain.Plugin;
import io.metersphere.system.domain.ServiceIntegration;
import io.metersphere.system.domain.ServiceIntegrationExample;
import io.metersphere.system.dto.ServiceIntegrationDTO;
@ -10,13 +15,14 @@ import io.metersphere.system.mapper.ServiceIntegrationMapper;
import io.metersphere.system.request.ServiceIntegrationUpdateRequest;
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.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import static io.metersphere.system.controller.result.SystemResultCode.SERVICE_INTEGRATION_EXIST;
@ -27,12 +33,48 @@ import static io.metersphere.system.controller.result.SystemResultCode.SERVICE_I
@Service
@Transactional(rollbackFor = Exception.class)
public class ServiceIntegrationService {
@Resource
private ServiceIntegrationMapper serviceIntegrationMapper;
@Resource
private PluginLoadService pluginLoadService;
@Resource
private PlatformPluginService platformPluginService;
public static final String PLUGIN_IMAGE_GET_PATH = "/plugin/image/%s?imagePath=%s";
public List<ServiceIntegrationDTO> list(String organizationId) {
return Arrays.asList(new ServiceIntegrationDTO());
// 查询服务集成已配置数据
Map<String, ServiceIntegration> serviceIntegrationMap = getServiceIntegrationByOrgId(organizationId).stream()
.collect(Collectors.toMap(ServiceIntegration::getPluginId, i -> i));
List<Plugin> plugins = platformPluginService.getPlatformPlugins();
return plugins.stream().map(plugin -> {
AbstractPlatformPlugin msPluginInstance = pluginLoadService.getPlatformPluginInstance(plugin.getId());
// 获取插件基础信息
ServiceIntegrationDTO serviceIntegrationDTO = new ServiceIntegrationDTO();
serviceIntegrationDTO.setTitle(msPluginInstance.getName());
serviceIntegrationDTO.setEnable(false);
serviceIntegrationDTO.setConfig(false);
serviceIntegrationDTO.setDescription(msPluginInstance.getDescription());
serviceIntegrationDTO.setLogo(String.format(PLUGIN_IMAGE_GET_PATH, plugin.getId(), msPluginInstance.getLogo()));
serviceIntegrationDTO.setPluginId(plugin.getId());
ServiceIntegration serviceIntegration = serviceIntegrationMap.get(plugin.getId());
if (serviceIntegration != null) {
serviceIntegrationDTO.setConfiguration(JSON.parseMap(new String(serviceIntegration.getConfiguration())));
serviceIntegrationDTO.setId(serviceIntegration.getId());
serviceIntegrationDTO.setEnable(serviceIntegration.getEnable());
serviceIntegrationDTO.setConfig(true);
serviceIntegrationDTO.setOrganizationId(serviceIntegration.getOrganizationId());
}
return serviceIntegrationDTO;
}).toList();
}
private List<ServiceIntegration> getServiceIntegrationByOrgId(String organizationId) {
ServiceIntegrationExample example = new ServiceIntegrationExample();
example.createCriteria()
.andOrganizationIdEqualTo(organizationId);
return serviceIntegrationMapper.selectByExampleWithBLOBs(example);
}
public ServiceIntegration add(ServiceIntegrationUpdateRequest request) {
@ -47,6 +89,8 @@ public class ServiceIntegrationService {
public ServiceIntegration update(ServiceIntegrationUpdateRequest request) {
ServiceIntegration serviceIntegration = new ServiceIntegration();
// 组织不能修改
serviceIntegration.setOrganizationId(null);
BeanUtils.copyBean(serviceIntegration, request);
if (request.getConfiguration() != null) {
serviceIntegration.setConfiguration(JSON.toJSONBytes(request.getConfiguration()));
@ -55,9 +99,8 @@ public class ServiceIntegrationService {
return serviceIntegration;
}
public String delete(String id) {
public void delete(String id) {
serviceIntegrationMapper.deleteByPrimaryKey(id);
return id;
}
private void checkAddExist(ServiceIntegration serviceIntegration) {
@ -70,13 +113,15 @@ public class ServiceIntegrationService {
}
}
public boolean validate(Map<String, String> serviceIntegrationInfo) {
return serviceIntegrationInfo == null;
public void validate(String pluginId, Map<String, String> serviceIntegrationInfo) {
Platform platform = platformPluginService.getPlatform(pluginId, StringUtils.EMPTY, JSON.toJSONString(serviceIntegrationInfo));
platform.validateIntegrationConfig();
}
public boolean validate(String id) {
public void validate(String id) {
ServiceIntegration serviceIntegration = serviceIntegrationMapper.selectByPrimaryKey(id);
return serviceIntegration == null;
Platform platform = platformPluginService.getPlatform(serviceIntegration.getPluginId(), StringUtils.EMPTY);
platform.validateIntegrationConfig();
}
public ServiceIntegration get(String id) {
@ -84,6 +129,7 @@ public class ServiceIntegrationService {
}
public Object getPluginScript(String pluginId) {
return new ServiceIntegration();
AbstractPlatformPlugin platformPlugin = pluginLoadService.getImplInstance(pluginId, AbstractPlatformPlugin.class);
return pluginLoadService.getPluginScriptContent(pluginId, platformPlugin.getIntegrationScriptId());
}
}

View File

@ -29,6 +29,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static io.metersphere.sdk.controller.handler.result.CommonResultCode.FILE_NAME_ILLEGAL;
import static io.metersphere.system.controller.result.SystemResultCode.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -260,8 +261,10 @@ public class PluginControllerTests extends BaseTest {
@Order(5)
public void getPluginImg() throws Exception {
// @@请求成功
mockMvc.perform(getRequestBuilder(PLUGIN_IMAGE, "pluginId", "/static/jira.jpg"))
mockMvc.perform(getRequestBuilder(PLUGIN_IMAGE, anotherAddPlugin.getId(), "/static/jira.jpg"))
.andExpect(status().isOk());
assertErrorCode( this.requestGet(PLUGIN_IMAGE, anotherAddPlugin.getId(), "/static/jira.doc"), FILE_NAME_ILLEGAL);
}
@Test
@ -274,6 +277,7 @@ public class PluginControllerTests extends BaseTest {
Assertions.assertNull(plugin);
Assertions.assertEquals(new ArrayList<>(0), getOrgIdsByPlugId(addPlugin.getId()));
Assertions.assertEquals(new ArrayList<>(0), getScriptIdsByPlugId(addPlugin.getId()));
this.requestGetWithOk(DEFAULT_DELETE, anotherAddPlugin.getId());
// @@校验日志
checkLog(addPlugin.getId(), OperationLogType.DELETE);

View File

@ -1,22 +1,44 @@
package io.metersphere.system.controller;
import io.metersphere.plugin.platform.api.AbstractPlatformPlugin;
import io.metersphere.plugin.sdk.util.JSON;
import io.metersphere.sdk.base.BaseTest;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.log.constants.OperationLogType;
import io.metersphere.sdk.service.PluginLoadService;
import io.metersphere.system.controller.param.ServiceIntegrationUpdateRequestDefinition;
import io.metersphere.system.domain.Organization;
import io.metersphere.system.domain.Plugin;
import io.metersphere.system.domain.ServiceIntegration;
import io.metersphere.system.dto.ServiceIntegrationDTO;
import io.metersphere.system.mapper.ServiceIntegrationMapper;
import io.metersphere.system.request.PluginUpdateRequest;
import io.metersphere.system.request.ServiceIntegrationUpdateRequest;
import io.metersphere.system.service.OrganizationService;
import io.metersphere.system.service.PluginService;
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 lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.*;
import org.mockserver.client.MockServerClient;
import org.mockserver.model.Header;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MvcResult;
import java.util.HashMap;
import java.io.File;
import java.io.FileInputStream;
import java.util.List;
import java.util.Map;
import static io.metersphere.sdk.constants.InternalUserRole.ADMIN;
import static io.metersphere.system.controller.result.SystemResultCode.SERVICE_INTEGRATION_EXIST;
import static io.metersphere.system.service.ServiceIntegrationService.PLUGIN_IMAGE_GET_PATH;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
/**
* @author jianxing
@ -29,41 +51,68 @@ public class ServiceIntegrationControllerTests extends BaseTest {
private static final String BASE_PATH = "/service/integration/";
private static final String LIST = "/list/{0}";
private static final String VALIDATE_GET = "/validate/{0}";
private static final String VALIDATE_POST = "/validate";
private static final String VALIDATE_POST = "/validate/{0}";
private static final String SCRIPT_GET = "/script/{0}";
private static ServiceIntegration addServiceIntegration;
private static Plugin plugin;
private static Organization defaultOrg;
@Resource
private ServiceIntegrationMapper serviceIntegrationMapper;
private static ServiceIntegration addServiceIntegration;
@Resource
private OrganizationService organizationService;
@Resource
private PluginLoadService pluginLoadService;
@Resource
private PluginService pluginService;
@Resource
private MockServerClient mockServerClient;
@Value("${embedded.mockserver.host}")
private String mockServerHost;
@Value("${embedded.mockserver.port}")
private int mockServerHostPort;
@Override
protected String getBasePath() {
return BASE_PATH;
}
@Test
public void list() throws Exception {
// @@请求成功
this.requestGetWithOkAndReturn(LIST, "orgId");
// List<ServiceIntegration> serviceIntegrationList = getResultDataArray(mvcResult, ServiceIntegration.class);
// todo 校验数据是否正确
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_READ, LIST,"orgId");
@Order(0)
public void listEmpty() throws Exception {
defaultOrg = organizationService.getDefault();
// @@请求成功, 校验空数据请求是否正常
this.requestGetWithOkAndReturn(LIST, defaultOrg.getId());
}
@Test
@Order(0)
@Order(1)
public void add() throws Exception {
plugin = addPlugin();
JiraIntegrationConfig integrationConfig = new JiraIntegrationConfig();
Map<String, Object> integrationConfigMap = JSON.parseMap(JSON.toJSONString(integrationConfig));
// @@请求成功
ServiceIntegrationUpdateRequest request = new ServiceIntegrationUpdateRequest();
request.setEnable(false);
request.setPluginId("pluginId");
request.setConfiguration(new HashMap<>());
request.setOrganizationId("orgId");
// todo 填充数据
request.setPluginId(plugin.getId());
request.setConfiguration(integrationConfigMap);
request.setOrganizationId(defaultOrg.getId());
MvcResult mvcResult = this.requestPostWithOkAndReturn(DEFAULT_ADD, request);
// 校验请求成功数据
ServiceIntegration resultData = getResultData(mvcResult, ServiceIntegration.class);
ServiceIntegration serviceIntegration = serviceIntegrationMapper.selectByPrimaryKey(resultData.getId());
this.addServiceIntegration= serviceIntegration;
// todo 校验请求成功数据
Assertions.assertEquals(JSON.parseMap(new String(serviceIntegration.getConfiguration())), integrationConfigMap);
Assertions.assertEquals(serviceIntegration.getEnable(), request.getEnable());
Assertions.assertEquals(serviceIntegration.getPluginId(), request.getPluginId());
Assertions.assertEquals(serviceIntegration.getOrganizationId(), request.getOrganizationId());
this.addServiceIntegration = serviceIntegration;
// @@重名校验异常
assertErrorCode(this.requestPost(DEFAULT_ADD, request), SERVICE_INTEGRATION_EXIST);
// @@校验日志
checkLog(this.addServiceIntegration.getId(), OperationLogType.ADD);
// @@异常参数校验
@ -73,20 +122,32 @@ public class ServiceIntegrationControllerTests extends BaseTest {
}
@Test
@Order(1)
@Order(2)
public void update() throws Exception {
JiraIntegrationConfig integrationConfig = new JiraIntegrationConfig();
integrationConfig.setAddress(String.format("http://%s:%s", mockServerHost, mockServerHostPort));
Map<String, Object> integrationConfigMap = JSON.parseMap(JSON.toJSONString(integrationConfig));
// @@请求成功
ServiceIntegrationUpdateRequest request = new ServiceIntegrationUpdateRequest();
request.setId(addServiceIntegration.getId());
request.setEnable(false);
request.setPluginId("pluginId");
request.setConfiguration(new HashMap<>());
request.setOrganizationId("orgId");
// todo 填充数据
request.setPluginId(plugin.getId());
request.setConfiguration(integrationConfigMap);
request.setOrganizationId(defaultOrg.getId());
this.requestPostWithOk(DEFAULT_UPDATE, request);
// 校验请求成功数据
// ServiceIntegration serviceIntegration = serviceIntegrationMapper.selectByPrimaryKey(request.getId());
// todo 校验请求成功数据
ServiceIntegration serviceIntegration = serviceIntegrationMapper.selectByPrimaryKey(request.getId());
Assertions.assertEquals(JSON.parseMap(new String(serviceIntegration.getConfiguration())), integrationConfigMap);
Assertions.assertEquals(serviceIntegration.getEnable(), request.getEnable());
Assertions.assertEquals(serviceIntegration.getPluginId(), request.getPluginId());
// 验证组织修改无效
Assertions.assertEquals(serviceIntegration.getOrganizationId(), defaultOrg.getId());
// 提升覆盖率
request.setConfiguration(null);
this.requestPostWithOk(DEFAULT_UPDATE, request);
// @@校验日志
checkLog(request.getId(), OperationLogType.UPDATE);
// @@异常参数校验
@ -95,41 +156,132 @@ public class ServiceIntegrationControllerTests extends BaseTest {
requestPostPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE, DEFAULT_UPDATE, request);
}
@Test
@Order(3)
public void list() throws Exception {
// @@请求成功
MvcResult mvcResult = this.requestGetWithOkAndReturn(LIST, defaultOrg.getId());
// 校验请求成功数据
List<ServiceIntegrationDTO> serviceIntegrationList = getResultDataArray(mvcResult, ServiceIntegrationDTO.class);
ServiceIntegrationDTO serviceIntegrationDTO = serviceIntegrationList.get(0);
ServiceIntegration serviceIntegration = serviceIntegrationMapper.selectByPrimaryKey(serviceIntegrationDTO.getId());
Assertions.assertEquals(JSON.parseMap(new String(serviceIntegration.getConfiguration())),
serviceIntegrationDTO.getConfiguration());
Assertions.assertEquals(serviceIntegration.getEnable(), serviceIntegrationDTO.getEnable());
Assertions.assertEquals(serviceIntegration.getPluginId(), serviceIntegrationDTO.getPluginId());
AbstractPlatformPlugin msPluginInstance = pluginLoadService.getPlatformPluginInstance(plugin.getId());
Assertions.assertEquals(serviceIntegrationDTO.getDescription(), msPluginInstance.getDescription());
Assertions.assertEquals(serviceIntegrationDTO.getOrganizationId(), defaultOrg.getId());
Assertions.assertEquals(serviceIntegrationDTO.getTitle(), msPluginInstance.getName());
Assertions.assertEquals(serviceIntegrationDTO.getConfig(), true);
Assertions.assertEquals(serviceIntegrationDTO.getLogo(),
String.format(PLUGIN_IMAGE_GET_PATH, plugin.getId(), msPluginInstance.getLogo()));
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_READ, LIST, defaultOrg.getId());
}
@Test
@Order(4)
public void validateGet() throws Exception {
mockServerClient
.when(
request()
.withMethod("GET")
.withPath("/rest/api/2/myself"))
.respond(
response()
.withStatusCode(200)
.withHeaders(
new Header("Content-Type", "application/json; charset=utf-8"),
new Header("Cache-Control", "public, max-age=86400"))
.withBody("{\"self\"")
);
// @@请求成功
this.requestGetWithOk(VALIDATE_GET, addServiceIntegration.getId());
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE, VALIDATE_GET, addServiceIntegration.getId());
}
@Test
@Order(5)
public void validatePost() throws Exception {
JiraIntegrationConfig integrationConfig = new JiraIntegrationConfig();
integrationConfig.setAddress(String.format("http://%s:%s", mockServerHost, mockServerHostPort));
Map<String, Object> integrationConfigMap = JSON.parseMap(JSON.toJSONString(integrationConfig));
// @@请求成功
this.requestPostWithOk(VALIDATE_POST, integrationConfigMap, plugin.getId());
// @@校验权限
requestPostPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE, VALIDATE_POST, integrationConfigMap, plugin.getId());
}
@Test
@Order(6)
public void getPluginScript() throws Exception {
// @@请求成功
MvcResult mvcResult = this.requestGetWithOkAndReturn(SCRIPT_GET, plugin.getId());
// 校验请求成功数据
Assertions.assertTrue(StringUtils.isNotBlank(mvcResult.getResponse().getContentAsString()));
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_READ, SCRIPT_GET, plugin.getId());
}
@Test
public void delete() throws Exception {
// @@请求成功
this.requestGetWithOk(DEFAULT_DELETE, addServiceIntegration.getId());
// todo 校验请求成功数据
// 校验请求成功数据
ServiceIntegration serviceIntegration = serviceIntegrationMapper.selectByPrimaryKey(addServiceIntegration.getId());
Assertions.assertNull(serviceIntegration);
// 清理插件
deletePlugin();
// @@校验日志
checkLog(addServiceIntegration.getId(), OperationLogType.DELETE);
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_DELETE, DEFAULT_DELETE, addServiceIntegration.getId());
}
@Test
public void validateGet() throws Exception {
// @@请求成功
this.requestGetWithOk(VALIDATE_GET, addServiceIntegration.getId());
// todo 校验请求成功数据
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE, VALIDATE_GET, addServiceIntegration.getId());
/**
* 添加插件供测试使用
*
* @return
* @throws Exception
*/
public Plugin addPlugin() throws Exception {
PluginUpdateRequest request = new PluginUpdateRequest();
File jarFile = new File(
this.getClass().getClassLoader().getResource("file/metersphere-jira-plugin-3.x.jar")
.getPath()
);
FileInputStream inputStream = new FileInputStream(jarFile);
MockMultipartFile mockMultipartFile = new MockMultipartFile(jarFile.getName(), inputStream);
request.setName("测试插件");
request.setGlobal(true);
request.setEnable(true);
request.setCreateUser(ADMIN.name());
return pluginService.add(request, mockMultipartFile);
}
@Test
public void validatePost() throws Exception {
// @@请求成功
this.requestPostWithOk(VALIDATE_POST, new HashMap<>());
// todo 校验请求成功数据
// @@校验权限
requestPostPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_UPDATE, VALIDATE_POST, new HashMap<>());
/**
* 删除插件
* @throws Exception
*/
public void deletePlugin() throws Exception {
this.requestGetWithOk(DEFAULT_DELETE, plugin.getId());
}
@Test
public void getPluginScript() throws Exception {
// @@请求成功
this.requestGetWithOk(SCRIPT_GET, "pluginId");
// todo 校验请求成功数据
// @@校验权限
requestGetPermissionTest(PermissionConstants.SYSTEM_SERVICE_INTEGRATION_READ, SCRIPT_GET, "pluginId");
@Getter
@Setter
public class JiraIntegrationConfig {
private String account;
private String password;
private String token;
private String authType;
private String address;
private String version;
}
}

View File

@ -16,6 +16,6 @@ public class PluginUpdateRequestDefinition {
@Size(min = 1, max = 255, groups = {Created.class, Updated.class})
private String name;
@Size(min = 1, max = 500, groups = {Created.class, Updated.class})
@Size(max = 500, groups = {Created.class, Updated.class})
private String description;
}

View File

@ -14,7 +14,7 @@ import java.util.Map;
public class ServiceIntegrationUpdateRequestDefinition {
@Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(groups = {Updated.class})
@Size(min = 1, max = 50, groups = {Created.class, Updated.class})
@Size(min = 1, max = 50, groups = {Updated.class})
private String id;
@Schema(description = "插件的ID", requiredMode = Schema.RequiredMode.REQUIRED)