feat(项目设置): 项目环境增加DTO

This commit is contained in:
wxg0103 2023-08-22 11:15:30 +08:00 committed by 刘瑞斌
parent 509804dc06
commit 43ca83399e
53 changed files with 1908 additions and 2 deletions

View File

@ -0,0 +1,71 @@
package io.metersphere.sdk.constants;
import java.util.ArrayList;
import java.util.List;
public class ElementConstants {
public static final String HASH_TREE = "hashTree";
public static final String CLAZZ_NAME = "clazzName";
public static final String SCENARIO = "scenario";
public static final String SCENARIO_UPPER = "SCENARIO";
public static final String HTTP_SAMPLER = "HTTPSamplerProxy";
public static final String TCP_SAMPLER = "TCPSampler";
public static final String DUBBO_SAMPLER = "DubboSampler";
public static final String JDBC_SAMPLER = "JDBCSampler";
public static final String JSR223 = "JSR223Processor";
public static final String JSR223_PRE = "JSR223PreProcessor";
public static final String JSR223_POST = "JSR223PostProcessor";
public static final String JDBC_PRE = "JDBCPreProcessor";
public static final String JDBC_POST = "JDBCPostProcessor";
public static final String JMETER_ELE = "JmeterElement";
public static final String TEST_PLAN = "TestPlan";
public static final String THREAD_GROUP = "ThreadGroup";
public static final String DNS_CACHE = "DNSCacheManager";
public static final String DEBUG_SAMPLER = "DebugSampler";
public static final String AUTH_MANAGER = "AuthManager";
public static final String ABS_SAMPLER = "AbstractSampler";
public static final String IF_CONTROLLER = "IfController";
public static final String TRANSACTION_CONTROLLER = "TransactionController";
public static final String LOOP_CONTROLLER = "LoopController";
public static final String CONSTANT_TIMER = "ConstantTimer";
public static final String ASSERTIONS = "Assertions";
public static final String EXTRACT = "Extract";
public static final String STEP_CREATED = "Created";
public static final String INDEX = "index";
public static final String ID = "id";
public static final String REF_ENABLE = "refEnable";
public final static List<String> REQUESTS = new ArrayList<String>() {{
this.add(ElementConstants.HTTP_SAMPLER);
this.add(ElementConstants.DUBBO_SAMPLER);
this.add(ElementConstants.JDBC_SAMPLER);
this.add(ElementConstants.TCP_SAMPLER);
this.add(ElementConstants.JSR223);
this.add(ElementConstants.JSR223_PRE);
this.add(ElementConstants.JSR223_POST);
this.add(ElementConstants.JDBC_PRE);
this.add(ElementConstants.JDBC_POST);
this.add(ElementConstants.JMETER_ELE);
this.add(ElementConstants.TEST_PLAN);
this.add(ElementConstants.THREAD_GROUP);
this.add(ElementConstants.DNS_CACHE);
this.add(ElementConstants.DEBUG_SAMPLER);
this.add(ElementConstants.AUTH_MANAGER);
this.add(ElementConstants.ABS_SAMPLER);
}};
public static final String SCRIPT = "script";
public static final String BEANSHELL = "beanshell";
public static final String IS_REF = "isRef";
public static final String FILE_ID = "fileId";
public static final String RESOURCE_ID = "resourceId";
public static final String FILENAME = "filename";
public static final String COVER = "COVER";
public static final String MS_KEYSTORE_FILE_PATH = "MS-KEYSTORE-FILE-PATH";
public static final String MS_KEYSTORE_FILE_PASSWORD = "MS-KEYSTORE-FILE-PASSWORD";
public static final String VIRTUAL_STEPS = "VIRTUAL_STEPS";
public static final String REF = "$ref";
public static final String TYPE = "type";
}

View File

@ -151,4 +151,18 @@ public class PermissionConstants {
public static final String PROJECT_APPLICATION_API_UPDATE = "PROJECT_APPLICATION_API:UPDATE"; public static final String PROJECT_APPLICATION_API_UPDATE = "PROJECT_APPLICATION_API:UPDATE";
/*------ end: PROJECT_APPLICATION ------*/ /*------ end: PROJECT_APPLICATION ------*/
public static final String PROJECT_BASE_INFO_READ = "PROJECT_BASE_INFO:READ"; public static final String PROJECT_BASE_INFO_READ = "PROJECT_BASE_INFO:READ";
/**
* 项目环境权限
*/
/*------ start: PROJECT_ENVIRONMENT ------*/
public static final String PROJECT_ENVIRONMENT_READ = "PROJECT_ENVIRONMENT:READ";
public static final String PROJECT_ENVIRONMENT_READ_ADD = "PROJECT_ENVIRONMENT:READ+ADD";
public static final String PROJECT_ENVIRONMENT_READ_UPDATE = "PROJECT_ENVIRONMENT:READ+UPDATE";
public static final String PROJECT_ENVIRONMENT_READ_DELETE = "PROJECT_ENVIRONMENT:READ+DELETE";
public static final String PROJECT_ENVIRONMENT_READ_IMPORT = "PROJECT_ENVIRONMENT:READ+IMPORT";
public static final String PROJECT_ENVIRONMENT_READ_EXPORT = "PROJECT_ENVIRONMENT:READ+EXPORT";
/*------ end: PROJECT_ENVIRONMENT ------*/
} }

View File

@ -0,0 +1,12 @@
package io.metersphere.sdk.constants;
public class RequestTypeConstants {
public static final String HTTP = "HTTP";
public static final String DUBBO = "DUBBO";
public static final String SQL = "SQL";
public static final String TCP = "TCP";
}

View File

@ -1,5 +1,5 @@
package io.metersphere.sdk.constants; package io.metersphere.sdk.constants;
public enum StorageConstants { public enum StorageConstants {
MINIO, GIT MINIO, GIT, FILE_REF
} }

View File

@ -0,0 +1,5 @@
package io.metersphere.sdk.constants;
public enum VariableTypeConstants {
CONSTANT, LIST, JSON
}

View File

@ -0,0 +1,116 @@
package io.metersphere.sdk.controller.environment;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.domain.Environment;
import io.metersphere.sdk.dto.environment.EnvironmentConfigRequest;
import io.metersphere.sdk.dto.environment.dataSource.DataSource;
import io.metersphere.sdk.service.PluginLoadService;
import io.metersphere.sdk.service.environment.EnvironmentService;
import io.metersphere.sdk.util.SessionUtils;
import io.metersphere.validation.groups.Created;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.sql.Driver;
import java.sql.DriverManager;
import java.util.List;
import java.util.Map;
import java.util.Properties;
@RestController
@RequestMapping(value = "/project/environment")
@Tag(name = "项目管理-环境")
public class EnvironmentController {
@Resource
private PluginLoadService pluginLoadService;
@Resource
private EnvironmentService environmentService;
@GetMapping("/list/{projectId}")
@Operation(summary = "项目管理-环境-环境目录-列表")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ)
public List<Environment> list(@PathVariable String projectId) {
return environmentService.list(projectId);
}
@GetMapping("/get/{environmentId}")
@Operation(summary = "项目管理-环境-环境目录-详情")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ)
public EnvironmentConfigRequest get(@PathVariable String environmentId) {
return environmentService.get(environmentId);
}
@PostMapping("/add")
@Operation(summary = "项目管理-环境-环境目录-新增")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ_ADD)
public EnvironmentConfigRequest add(@Validated({Created.class}) @RequestBody EnvironmentConfigRequest environmentConfigRequest,
@RequestPart(value = "files", required = false) List<MultipartFile> sslFiles) {
return environmentService.add(environmentConfigRequest, SessionUtils.getUserId(), sslFiles);
}
@PostMapping("/update")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ_UPDATE)
@Operation(summary = "项目管理-环境-环境目录-修改")
public void update() {
}
@PostMapping("/delete/{id}")
@Operation(summary = "项目管理-环境-环境目录-删除")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ_DELETE)
public void delete(@PathVariable String id) {
environmentService.delete(id);
}
@PostMapping("/database/validate")
@Operation(summary = "项目管理-环境-数据库配置-校验")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ)
public void validate(@RequestBody DataSource databaseConfig) {
try {
if (StringUtils.isNotBlank(databaseConfig.getDriverId())) {
ClassLoader classLoader = pluginLoadService.getClassLoader(databaseConfig.getDriverId());
Driver driver = (Driver) classLoader.loadClass(databaseConfig.getDriver()).newInstance();
Properties properties = new Properties();
properties.setProperty("user", databaseConfig.getUsername());
properties.setProperty("password", databaseConfig.getPassword());
driver.connect(databaseConfig.getDbUrl(), properties);
} else {
DriverManager.getConnection(databaseConfig.getDbUrl(), databaseConfig.getUsername(), databaseConfig.getPassword());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@GetMapping("/database/driver-options/{organizationId}")
@Operation(summary = "项目管理-环境-数据库配置-数据库驱动选项")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ)
public Map<String, String> driverOptions(@PathVariable String organizationId) {
return environmentService.getDriverOptions(organizationId);
}
@PostMapping(value = "/import", consumes = {"multipart/form-data"})
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ_IMPORT)
@Operation(summary = "项目管理-环境-环境目录-导入")
public void create(@RequestPart(value = "file") MultipartFile file) {
environmentService.create(file, SessionUtils.getUserId(), SessionUtils.getCurrentProjectId());
}
@PostMapping("/export")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ_EXPORT)
@Operation(summary = "项目管理-环境-环境目录-导出")
public List<EnvironmentConfigRequest> export(@RequestBody List<String> environmentIds) {
return environmentService.export(environmentIds);
}
}

View File

@ -0,0 +1,46 @@
package io.metersphere.sdk.controller.environment;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.dto.environment.GlobalParamsRequest;
import io.metersphere.sdk.service.environment.GlobalParamsService;
import io.metersphere.sdk.util.SessionUtils;
import io.metersphere.validation.groups.Created;
import io.metersphere.validation.groups.Updated;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/project/global/params")
@Tag(name = "项目管理-环境-全局参数")
public class GlobalParamsController {
@Resource
private GlobalParamsService globalParamsService;
@PostMapping("/add")
@Operation(summary = "项目管理-环境-全局参数-新增")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ_ADD)
public GlobalParamsRequest add(@Validated({Created.class}) @RequestBody GlobalParamsRequest globalParamsRequest) {
return globalParamsService.add(globalParamsRequest, SessionUtils.getUserId());
}
@PostMapping("/update")
@Operation(summary = "项目管理-环境-全局参数-修改")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ_UPDATE)
public GlobalParamsRequest update(@Validated({Updated.class}) @RequestBody GlobalParamsRequest globalParamsRequest) {
return globalParamsService.update(globalParamsRequest, SessionUtils.getUserId());
}
@GetMapping("/get/{projectId}")
@Operation(summary = "项目管理-环境-全局参数-详情")
@RequiresPermissions(PermissionConstants.PROJECT_ENVIRONMENT_READ)
public GlobalParamsRequest get(@PathVariable String projectId) {
return globalParamsService.get(projectId);
}
}

View File

@ -0,0 +1,25 @@
package io.metersphere.sdk.dto.environment;
import io.metersphere.sdk.constants.StorageConstants;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
@Data
public class BodyFile {
private String id;
private String name;
// LOCAL 引用(FILE_REF) / GIT
private String storage;
private String fileId;
private String projectId;
private String fileType;
// 正常/已删除
private String status;
// 调试附件处理
private String refResourceId;
public boolean isRef() {
return StringUtils.equals(storage, StorageConstants.FILE_REF.name()) && StringUtils.isNotEmpty(fileId);
}
}

View File

@ -0,0 +1,64 @@
package io.metersphere.sdk.dto.environment;
import io.metersphere.sdk.dto.environment.assertions.EnvironmentAssertions;
import io.metersphere.sdk.dto.environment.authConfig.AuthConfig;
import io.metersphere.sdk.dto.environment.common.CommonParams;
import io.metersphere.sdk.dto.environment.dataSource.DataSource;
import io.metersphere.sdk.dto.environment.hostConfig.HostConfig;
import io.metersphere.sdk.dto.environment.httpConfig.HttpConfig;
import io.metersphere.sdk.dto.environment.script.post.EnvironmentPostScript;
import io.metersphere.sdk.dto.environment.script.pre.EnvironmentPreScript;
import io.metersphere.sdk.dto.environment.ssl.KeyStoreConfig;
import io.metersphere.sdk.dto.environment.tcpConfig.TCPConfig;
import io.metersphere.sdk.dto.environment.variables.CommonVariables;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Data
public class EnvironmentConfig implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "公共参数 请求超时时间、响应超时时间")
private CommonParams commonParams;
@Schema(description = "环境变量")
private List<CommonVariables> commonVariables;
@Schema(description = "HTTP配置")
private List<HttpConfig> httpConfig;
@Schema(description = "数据库配置")
private List<DataSource> dataSources;
@Schema(description = "Host配置")
private HostConfig hostConfig;
@Schema(description = "TCP配置")
private TCPConfig tcpConfig;
@Schema(description = "认证配置")
private AuthConfig authConfig;
@Schema(description = "SSL配置")
private KeyStoreConfig sslConfig;
@Schema(description = "全局前置脚本")
private EnvironmentPreScript preScript;
@Schema(description = "全局后置脚本")
private EnvironmentPostScript postScript;
@Schema(description = "全局断言")
private EnvironmentAssertions assertions;
public EnvironmentConfig() {
this.commonParams = new CommonParams();
this.commonVariables = new ArrayList<>();
this.httpConfig = new ArrayList<>();
this.dataSources = new ArrayList<>();
this.hostConfig = new HostConfig();
this.tcpConfig = new TCPConfig();
this.authConfig = new AuthConfig();
this.preScript = new EnvironmentPreScript();
this.postScript = new EnvironmentPostScript();
this.assertions = new EnvironmentAssertions();
this.sslConfig = new KeyStoreConfig();
}
}

View File

@ -0,0 +1,32 @@
package io.metersphere.sdk.dto.environment;
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.Size;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class EnvironmentConfigRequest implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "ID")
@NotBlank(message = "{project_parameters.id.not_blank}", groups = {Updated.class})
@Size(min = 1, max = 50, message = "{project_parameters.id.length_range}", groups = {Updated.class})
private String id;
@Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{project_parameters.project_id.not_blank}", groups = {Created.class, Updated.class})
@Size(min = 1, max = 50, message = "{project_parameters.project_id.length_range}", groups = {Created.class, Updated.class})
private String projectId;
@Schema(description = "环境名称")
@NotBlank(message = "{project_parameters.project_id.not_blank}", groups = {Created.class, Updated.class})
private String name;
@Schema(description = "环境配置")
private EnvironmentConfig config;
}

View File

@ -0,0 +1,21 @@
package io.metersphere.sdk.dto.environment;
import io.metersphere.sdk.dto.environment.variables.CommonVariables;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
@Data
public class GlobalParamsDTO implements Serializable {
@Schema(description = "请求头")
private List<KeyValue> headers;
@Schema(description = "全局变量")
private List<CommonVariables> commonVariables;
@Serial
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,32 @@
package io.metersphere.sdk.dto.environment;
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.Size;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class GlobalParamsRequest implements Serializable {
@Schema(description = "ID")
@NotBlank(message = "{project_parameters.id.not_blank}", groups = {Updated.class})
@Size(min = 1, max = 50, message = "{project_parameters.id.length_range}", groups = {Updated.class})
private String id;
@Schema(description = "项目ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "{project_parameters.project_id.not_blank}", groups = {Created.class, Updated.class})
@Size(min = 1, max = 50, message = "{project_parameters.project_id.length_range}", groups = {Created.class, Updated.class})
private String projectId;
@Schema(description = "全局参数")
private GlobalParamsDTO globalParams;
@Serial
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,16 @@
package io.metersphere.sdk.dto.environment;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class KeyValue {
@Schema(description = "参数名")
private String name;
@Schema(description = "参数值")
private String value;
@Schema(description = "是否启用")
private Boolean enable = true;
}

View File

@ -0,0 +1,20 @@
package io.metersphere.sdk.dto.environment.assertions;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class EnvAssertionDuration extends EnvAssertionType {
@Schema(description = "响应时间")
private long value;
public EnvAssertionDuration() {
setType(DURATION);
}
public boolean isValid() {
return value > 0 && isEnable();
}
}

View File

@ -0,0 +1,33 @@
package io.metersphere.sdk.dto.environment.assertions;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.StringUtils;
@EqualsAndHashCode(callSuper = true)
@Data
public class EnvAssertionJSR223 extends EnvAssertionType {
@Schema(description = "变量名")
private String variable;
@Schema(description = "操作符")
private String operator;
@Schema(description = "")
private String value;
@Schema(description = "脚本名称")
private String name;
@Schema(description = "脚本内容")
private String script;
@Schema(description = "脚本语言")
private String scriptLanguage;
@Schema(description = "是否启用jsr223")
private Boolean jsrEnable;
public EnvAssertionJSR223() {
setType(JSR223);
}
public boolean isValid() {
return StringUtils.isNotBlank(script) && StringUtils.isNotBlank(scriptLanguage) && isEnable();
}
}

View File

@ -0,0 +1,27 @@
package io.metersphere.sdk.dto.environment.assertions;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.StringUtils;
@EqualsAndHashCode(callSuper = true)
@Data
public class EnvAssertionJsonPath extends EnvAssertionType {
@Schema(description = "期望值")
private String expect;
@Schema(description = "表达式")
private String expression;
@Schema(description = "描述")
private String description;
@Schema(description = "option 包含/不包含/正则/等于/不等于/大于/小于")
private String option = "REGEX";
public EnvAssertionJsonPath() {
setType(JSON_PATH);
}
public boolean isValid() {
return StringUtils.isNotBlank(expression) && isEnable();
}
}

View File

@ -0,0 +1,27 @@
package io.metersphere.sdk.dto.environment.assertions;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.StringUtils;
@EqualsAndHashCode(callSuper = true)
@Data
public class EnvAssertionRegex extends EnvAssertionType {
@Schema(description = "断言对象 Response Headers/Response Code/Response Data")
private String subject;
@Schema(description = "表达式")
private String expression;
@Schema(description = "描述")
private String description;
@Schema(description = "忽略状态")
private Boolean assumeSuccess = false;
public EnvAssertionRegex() {
setType(EnvAssertionType.REGEX);
}
public boolean isValid() {
return StringUtils.isNotBlank(subject) && StringUtils.isNotBlank(expression) && isEnable();
}
}

View File

@ -0,0 +1,18 @@
package io.metersphere.sdk.dto.environment.assertions;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class EnvAssertionType {
public final static String REGEX = "Regex";
public final static String DURATION = "Duration";
public final static String JSON_PATH = "JSONPath";
public final static String JSR223 = "JSR223";
public final static String TEXT = "Text";
public final static String XPATH2 = "XPath2";
@Schema(description = "是否启用")
private boolean enable = true;
private String type;
}

View File

@ -0,0 +1,21 @@
package io.metersphere.sdk.dto.environment.assertions;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.StringUtils;
@EqualsAndHashCode(callSuper = true)
@Data
public class EnvAssertionXPath extends EnvAssertionType {
@Schema(description = "表达式")
private String expression;
public EnvAssertionXPath() {
setType(XPATH2);
}
public boolean isValid() {
return StringUtils.isNotBlank(expression) && isEnable();
}
}

View File

@ -0,0 +1,31 @@
package io.metersphere.sdk.dto.environment.assertions;
import io.metersphere.sdk.dto.environment.assertions.document.MsAssertionDocument;
import io.metersphere.sdk.dto.environment.httpConfig.ApplicationModule;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class EnvironmentAssertions {
@Schema(description = "应用模块")
private ApplicationModule module;
@Schema(description = "xpath类型 xml/html")
private String xpathType;
@Schema(description = "正则断言")
private List<EnvAssertionRegex> regex;
@Schema(description = "jsonPath断言")
private List<EnvAssertionJsonPath> jsonPath;
@Schema(description = "jsr223断言")
private List<EnvAssertionJSR223> jsr223;
@Schema(description = "xpath断言")
private List<EnvAssertionXPath> xpath;
@Schema(description = "响应时间断言")
private EnvAssertionDuration duration;
@Schema(description = "文档断言")
private MsAssertionDocument document;
}

View File

@ -0,0 +1,25 @@
package io.metersphere.sdk.dto.environment.assertions.document;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class Document {
@Schema(description = "JSON跟随API 这里的是id")
private String jsonFollowAPI;
@Schema(description = "XML跟随API")
private String xmlFollowAPI;
@Schema(description = "JSON内容")
private List<DocumentElement> json;
@Schema(description = "XML内容")
private List<DocumentElement> xml;
@Schema(description = "是否包含")
private Boolean include = false;
@Schema(description = "是否包含类型")
private Boolean typeVerification = false;
}

View File

@ -0,0 +1,36 @@
package io.metersphere.sdk.dto.environment.assertions.document;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class DocumentElement {
@Schema(description = "id")
private String id;
@Schema(description = "名称")
private String name;
@Schema(description = "包含")
private Boolean include = false;
@Schema(description = "状态")
private String status;
@Schema(description = "类型校验")
private boolean typeVerification;
@Schema(description = "类型 object/array/string/number/boolean/integer")
private String type;
@Schema(description = "组id")
private String groupId;
@Schema(description = "行数")
private int rowspan;
@Schema(description = "校验组内元素")
private boolean arrayVerification;
@Schema(description = "内容校验 none/value_eq/value_not_eq/value_in/length_eq/length_not_eq/length_gt/length_lt/regular")
private String contentVerification;
@Schema(description = "预期结果")
private Object expectedOutcome;
@Schema(description = "子级")
private List<DocumentElement> children;
}

View File

@ -0,0 +1,14 @@
package io.metersphere.sdk.dto.environment.assertions.document;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class MsAssertionDocument {
@Schema(description = "是否启用")
private Boolean enable = true;
@Schema(description = "断言类型 JSON/XML")
private String type;
@Schema(description = "数据")
private Document data;
}

View File

@ -0,0 +1,21 @@
package io.metersphere.sdk.dto.environment.authConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class AuthConfig implements Serializable {
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
private String password;
@Schema(description = "认证方式 No Auth、Basic Auth、Digest Auth")
private String verification;
@Serial
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,17 @@
package io.metersphere.sdk.dto.environment.common;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class CommonParams implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "链接超时时间")
private Long requestTimeout;
@Schema(description = "响应超时时间")
private Long responseTimeout;
}

View File

@ -0,0 +1,31 @@
package io.metersphere.sdk.dto.environment.dataSource;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class DataSource implements Serializable {
@Schema(description = "id")
private String id;
@Schema(description = "数据源名称")
private String dataSource;
@Schema(description = "数据驱动")
private String driver;
@Schema(description = "数据驱动id")
private String driverId;
@Schema(description = "数据库连接url")
private String dbUrl;
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
private String password;
@Schema(description = "最大连接数")
private Long poolMax;
@Schema(description = "超时时间")
private Long timeout;
@Serial
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,22 @@
package io.metersphere.sdk.dto.environment.hostConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class Host implements Serializable {
@Schema(description = "IP")
private String ip;
@Schema(description = "域名")
private String domain;
@Schema(description = "描述")
private String description;
@Schema(description = "id")
private String uuid;
@Serial
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,20 @@
package io.metersphere.sdk.dto.environment.hostConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.apache.hc.core5.net.Host;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
@Data
public class HostConfig implements Serializable {
@Schema(description = "启用Host")
private Boolean enable;
@Schema(description = "Host列表")
private List<Host> hosts;
@Serial
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,12 @@
package io.metersphere.sdk.dto.environment.httpConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class ApplicationModule {
@Schema(description = "接口测试")
private Boolean apiTest = true;
@Schema(description = "UI测试")
private Boolean uiTest = false;
}

View File

@ -0,0 +1,35 @@
package io.metersphere.sdk.dto.environment.httpConfig;
import io.metersphere.sdk.dto.environment.KeyValue;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
@Data
public class HttpConfig implements Serializable {
@Schema(description = "环境域名")
private String socket;
@Schema(description = "domain")
private String domain;
@Schema(description = "协议")
private String protocol = "https";
@Schema(description = "应用模块接口测试、ui测试")
private List<ApplicationModule> applicationModule;
@Schema(description = "启用条件 NONE/MODULE/PATH")
private String type;
@Schema(description = "启用条件为PATH时需要填写的路径/ 如果是模块时需要时模块的id")
private List<KeyValue> details;
@Schema(description = "请求头")
private List<KeyValue> headers;
@Schema(description = "浏览器 选项为chrome/firefox")
private String browser;
private String description;
@Serial
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,39 @@
package io.metersphere.sdk.dto.environment.script;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Data
public class ApiScript {
@Schema(description = "环境级")
private ScriptContent envJSR223PostScript;
@Schema(description = "场景级")
private ScenarioPostScript scenarioJSR223PostScript;
@Schema(description = "步骤级")
private StepPostScript stepJSR223PostScript;
}
@Data
@EqualsAndHashCode(callSuper = true)
class ScenarioPostScript extends ScriptContent {
@Schema(description = "关联场景结果 true: 是/false: 否")
private Boolean associateScenarioResults = false;
}
@Data
@EqualsAndHashCode(callSuper = true)
class StepPostScript extends ScriptContent{
@Schema(description = "忽略请求")
private List<String> filterRequestPostScript;
@Schema(description = "脚本执行顺序 true:先执行 false:后执行")
private Boolean preScriptExecBefore = true;
@Schema(description = "脚本内容")
private ScriptContent scriptContent;
}

View File

@ -0,0 +1,17 @@
package io.metersphere.sdk.dto.environment.script;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class ScriptContent {
@Schema(description = "脚本内容")
private String script;
@Schema(description = "脚本语言")
private String scriptLanguage;
@Schema(description = "是否是jsr223")
private Boolean jsrEnable;
}

View File

@ -0,0 +1,16 @@
package io.metersphere.sdk.dto.environment.script.post;
import io.metersphere.sdk.dto.environment.script.ApiScript;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class EnvironmentPostScript {
@Schema(description = "接口测试")
private ApiScript apiPostScript;
@Schema(description = "UI测试")
private UiPostScript uiPostScript;
}

View File

@ -0,0 +1,18 @@
package io.metersphere.sdk.dto.environment.script.post;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class UiPostScript {
@Schema(description = "脚本执行顺序 true:先执行 false:后执行")
private Boolean postScriptExecBefore = true;
@Schema(description = "脚本类型 true:同步 false:异步")
private Boolean jsrType = true;
@Schema(description = "设置变量 true:有返回值 false:无返回值")
private Boolean jsrSetVariable = true;
@Schema(description = "变量名称")
private String variableName;
@Schema(description = "脚本内容")
private Boolean value;
}

View File

@ -0,0 +1,15 @@
package io.metersphere.sdk.dto.environment.script.pre;
import io.metersphere.sdk.dto.environment.script.ApiScript;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class EnvironmentPreScript {
@Schema(description = "接口测试")
private ApiScript apiPreScript;
@Schema(description = "UI测试")
private UiPreScript uiPreScript;
}

View File

@ -0,0 +1,21 @@
package io.metersphere.sdk.dto.environment.script.pre;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class UiPreScript {
@Schema(description = "脚本执行顺序 true:先执行 false:后执行")
private Boolean preScriptExecBefore = true;
@Schema(description = "脚本类型 true:同步 false:异步")
private Boolean jsrType = true;
@Schema(description = "设置变量 true:有返回值 false:无返回值")
private Boolean jsrSetVariable = true;
@Schema(description = "变量名称")
private String variableName;
@Schema(description = "脚本内容")
private Boolean value;
}

View File

@ -0,0 +1,41 @@
package io.metersphere.sdk.dto.environment.ssl;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.stream.Collectors;
@Data
public class KeyStoreConfig {
@Schema(description = "证书条目")
private List<KeyStoreEntry> entry;
@Schema(description = "证书文件")
private List<KeyStoreFile> files;
public String getDefaultAlias() {
if (CollectionUtils.isNotEmpty(entry)) {
List<KeyStoreEntry> entryList = this.entry.stream().filter(KeyStoreEntry::isDefault).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(entryList)) {
if (StringUtils.isNotEmpty(entryList.get(0).getNewAsName())) {
return entryList.get(0).getNewAsName();
} else {
return entryList.get(0).getOriginalAsName();
}
}
}
return null;
}
public String getAlias(String asName) {
if (CollectionUtils.isNotEmpty(entry)) {
List<KeyStoreEntry> entryList = this.entry.stream().filter(ks -> StringUtils.equals(asName, ks.getNewAsName())).collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(entryList) && CollectionUtils.isNotEmpty(files) && files.size() == 1) {
return entryList.get(0).getOriginalAsName();
}
}
return asName;
}
}

View File

@ -0,0 +1,24 @@
package io.metersphere.sdk.dto.environment.ssl;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class KeyStoreEntry {
@Schema(description = "证书条目id")
private String id;
@Schema(description = "原有别名")
private String originalAsName;
@Schema(description = "新别名")
private String newAsName;
@Schema(description = "证书类型")
private String type;
@Schema(description = "证书密码")
private String password;
@Schema(description = "证书来源")
private String sourceName;
@Schema(description = "证书来源id")
private String sourceId;
@Schema(description = "是否默认")
private boolean isDefault;
}

View File

@ -0,0 +1,23 @@
package io.metersphere.sdk.dto.environment.ssl;
import io.metersphere.sdk.dto.environment.BodyFile;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class KeyStoreFile {
@Schema(description = "证书id")
private String id;
@Schema(description = "证书名称")
private String name;
@Schema(description = "证书类型")
private String type;
@Schema(description = "修改时间")
private String updateTime;
@Schema(description = "证书密码")
private String password;
@Schema(description = "证书文件")
private BodyFile file;
}

View File

@ -0,0 +1,18 @@
package io.metersphere.sdk.dto.environment.ssl;
import lombok.Data;
@Data
public class MsKeyStore {
private String id;
private String password;
private String path;
public MsKeyStore() {
}
public MsKeyStore(String path, String password) {
this.password = password;
this.path = path;
}
}

View File

@ -0,0 +1,38 @@
package io.metersphere.sdk.dto.environment.tcpConfig;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class TCPConfig implements Serializable {
@Schema(description = "TCPClient 选项为TCPClientImpl、BinaryTCPClientImpl、LengthPrefixedBinaryTCPClientImpl")
private String className;
@Schema(description = "服务器名或IP")
private String server;
@Schema(description = "端口")
private int port = 0;
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
private String password;
@Schema(description = "连接超时")
private String connectTimeout;
@Schema(description = "响应超时")
private String timeout;
@Schema(description = "So Linger")
private String soLinger;
@Schema(description = "Re-use connection")
private Boolean reUseConnection;
@Schema(description = "设置无延迟")
private Boolean nodelay;
@Schema(description = "Close Connection")
private Boolean closeConnection;
@Schema(description = "行尾(EOL)字节值")
private String eolByte;
@Serial
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,45 @@
package io.metersphere.sdk.dto.environment.variables;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.metersphere.sdk.constants.VariableTypeConstants;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
@Data
public class CommonVariables implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "id")
private String id;
@Schema(description = "变量名")
private String name;
@Schema(description = "变量类型 CONSTANT LIST JSON")
private String type = VariableTypeConstants.CONSTANT.name();
@Schema(description = "变量值")
private String value;
@Schema(description = "状态")
private Boolean enable = true;
@Schema(description = "描述")
private String description;
@JsonIgnore
public boolean isConstantValid() {
return StringUtils.isEmpty(this.type) || (StringUtils.equals("text", this.type) && StringUtils.isNotEmpty(name)) || (StringUtils.equals(this.type, VariableTypeConstants.CONSTANT.name()) && StringUtils.isNotEmpty(name));
}
@JsonIgnore
public boolean isListValid() {
return StringUtils.equals(this.type, VariableTypeConstants.LIST.name()) && StringUtils.isNotEmpty(name) && StringUtils.isNotEmpty(value) && value.indexOf(",") != -1;
}
@JsonIgnore
public boolean isJsonValid() {
return StringUtils.equals(this.type, VariableTypeConstants.JSON.name()) && StringUtils.isNotEmpty(name);
}
}

View File

@ -166,6 +166,11 @@ public class PluginLoadService {
pluginManager.deletePlugin(pluginId); pluginManager.deletePlugin(pluginId);
} }
public PluginClassLoader getClassLoader(String pluginId) {
return pluginManager.getClassLoader(pluginId);
}
/** /**
* 删除插件 * 删除插件
*/ */

View File

@ -0,0 +1,214 @@
package io.metersphere.sdk.service.environment;
import io.metersphere.sdk.constants.PluginScenarioType;
import io.metersphere.sdk.domain.Environment;
import io.metersphere.sdk.domain.EnvironmentBlob;
import io.metersphere.sdk.domain.EnvironmentBlobExample;
import io.metersphere.sdk.domain.EnvironmentExample;
import io.metersphere.sdk.dto.environment.EnvironmentConfig;
import io.metersphere.sdk.dto.environment.EnvironmentConfigRequest;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.file.FileRequest;
import io.metersphere.sdk.file.MinioRepository;
import io.metersphere.sdk.mapper.EnvironmentBlobMapper;
import io.metersphere.sdk.mapper.EnvironmentMapper;
import io.metersphere.sdk.service.PluginLoadService;
import io.metersphere.sdk.uid.UUID;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.domain.Plugin;
import io.metersphere.system.domain.PluginExample;
import io.metersphere.system.domain.PluginOrganization;
import io.metersphere.system.domain.PluginOrganizationExample;
import io.metersphere.system.mapper.PluginMapper;
import io.metersphere.system.mapper.PluginOrganizationMapper;
import jakarta.annotation.Resource;
import jakarta.transaction.Transactional;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.sql.Driver;
import java.util.*;
@Service
@Transactional
public class EnvironmentService {
@Resource
private PluginMapper pluginMapper;
@Resource
private PluginLoadService pluginLoadService;
@Resource
private PluginOrganizationMapper pluginOrganizationMapper;
@Resource
private EnvironmentMapper environmentMapper;
@Resource
private EnvironmentBlobMapper environmentBlobMapper;
@Resource
private MinioRepository minioRepository;
public Map<String, String> getDriverOptions(String organizationId) {
Map<String, String> pluginDriverClassNames = new HashMap<>();
PluginExample example = new PluginExample();
example.createCriteria().andScenarioEqualTo(PluginScenarioType.JDBC_DRIVER.name()).andEnableEqualTo(true);
List<Plugin> plugins = pluginMapper.selectByExample(example);
plugins.forEach(plugin -> {
if (BooleanUtils.isTrue(plugin.getGlobal())) {
pluginDriverClassNames.put(plugin.getId(), pluginLoadService.getImplClass(plugin.getId(), Driver.class).getName());
} else {
//判断组织id
if (StringUtils.isNotBlank(organizationId)) {
//判断组织id是否在插件组织id中
PluginOrganizationExample pluginOrganizationExample = new PluginOrganizationExample();
pluginOrganizationExample.createCriteria().andPluginIdEqualTo(plugin.getId()).andOrganizationIdEqualTo(organizationId);
List<PluginOrganization> pluginOrganizations = pluginOrganizationMapper.selectByExample(pluginOrganizationExample);
if (pluginOrganizations.size() > 0) {
pluginDriverClassNames.put(plugin.getId(), pluginLoadService.getImplClass(plugin.getId(), Driver.class).getName());
}
}
}
});
// 已经内置了 mysql 依赖
pluginDriverClassNames.put(StringUtils.EMPTY, "com.mysql.jdbc.Driver");
return pluginDriverClassNames;
}
public void delete(String id) {
environmentMapper.deleteByPrimaryKey(id);
environmentBlobMapper.deleteByPrimaryKey(id);
}
public EnvironmentConfigRequest add(EnvironmentConfigRequest environmentConfigRequest, String userId, List<MultipartFile> sslFiles) {
Environment environment = new Environment();
environment.setId(UUID.randomUUID().toString());
environment.setCreateUser(userId);
environment.setName(environmentConfigRequest.getName());
checkEnvironmentExist(environment);
environment.setCreateTime(System.currentTimeMillis());
environment.setProjectId(environmentConfigRequest.getProjectId());
environmentMapper.insert(environment);
EnvironmentBlob environmentBlob = new EnvironmentBlob();
environmentBlob.setId(environment.getId());
String config = JSON.toJSONString(environmentConfigRequest.getConfig());
environmentBlob.setConfig(config.getBytes());
environmentBlobMapper.insert(environmentBlob);
if (CollectionUtils.isNotEmpty(sslFiles)) {
sslFiles.forEach(sslFile -> {
FileRequest fileRequest = new FileRequest();
fileRequest.setFileName(sslFile.getOriginalFilename());
fileRequest.setProjectId(environment.getProjectId());
fileRequest.setResourceId(environment.getId());
try {
minioRepository.saveFile(sslFile, fileRequest);
} catch (Exception e) {
LogUtils.info("上传ssl文件失败: 文件名称:" + sslFile.getOriginalFilename(), e);
}
});
}
return environmentConfigRequest;
}
public List<Environment> list(String projectId) {
EnvironmentExample example = new EnvironmentExample();
example.createCriteria().andProjectIdEqualTo(projectId);
example.setOrderByClause("update_time desc");
return environmentMapper.selectByExample(example);
}
public EnvironmentConfigRequest get(String environmentId) {
EnvironmentConfigRequest environmentConfigRequest = new EnvironmentConfigRequest();
Environment environment = environmentMapper.selectByPrimaryKey(environmentId);
EnvironmentBlob environmentBlob = environmentBlobMapper.selectByPrimaryKey(environmentId);
environmentConfigRequest.setProjectId(environment.getProjectId());
environmentConfigRequest.setName(environment.getName());
environmentConfigRequest.setId(environment.getId());
if (environmentBlob == null) {
return environmentConfigRequest;
}
environmentConfigRequest.setConfig(JSON.parseObject(Arrays.toString(environmentBlob.getConfig()), EnvironmentConfig.class));
return environmentConfigRequest;
}
public List<EnvironmentConfigRequest> export(List<String> environmentIds) {
if (CollectionUtils.isNotEmpty(environmentIds)) {
// 查询环境
EnvironmentExample environmentExample = new EnvironmentExample();
environmentExample.createCriteria().andIdIn(environmentIds);
List<Environment> environments = environmentMapper.selectByExample(environmentExample);
Map<String, Environment> environmentMap = new HashMap<>();
environments.forEach(environment -> environmentMap.put(environment.getId(), environment));
// 查询环境配置
EnvironmentBlobExample environmentBlobExample = new EnvironmentBlobExample();
environmentBlobExample.createCriteria().andIdIn(environmentIds);
List<EnvironmentBlob> environmentBlobs = environmentBlobMapper.selectByExample(environmentBlobExample);
Map<String, EnvironmentBlob> environmentBlobMap = new HashMap<>();
environmentBlobs.forEach(environmentBlob -> environmentBlobMap.put(environmentBlob.getId(), environmentBlob));
List<EnvironmentConfigRequest> environmentConfigRequests = new ArrayList<>();
environmentIds.forEach(environmentId -> {
EnvironmentConfigRequest environmentConfigRequest = new EnvironmentConfigRequest();
Environment environment = environmentMap.get(environmentId);
EnvironmentBlob environmentBlob = environmentBlobMap.get(environmentId);
environmentConfigRequest.setProjectId(environment.getProjectId());
environmentConfigRequest.setName(environment.getName());
environmentConfigRequest.setId(environment.getId());
if (environmentBlob != null) {
environmentConfigRequest.setConfig(JSON.parseObject(Arrays.toString(environmentBlob.getConfig()), EnvironmentConfig.class));
}
environmentConfigRequests.add(environmentConfigRequest);
});
return environmentConfigRequests;
} else {
return new ArrayList<>();
}
}
public void checkEnvironmentExist(Environment environment) {
if (environment.getName() != null) {
EnvironmentExample environmentExample = new EnvironmentExample();
environmentExample.createCriteria().andNameEqualTo(environment.getName()).andProjectIdEqualTo(environment.getProjectId()).andIdNotEqualTo(environment.getId());
if (environmentMapper.selectByExample(environmentExample).size() > 0) {
throw new MSException(Translator.get("api_test_environment_already_exists"));
}
}
}
public void create(MultipartFile file, String userId, String currentProjectId) {
if (file != null) {
try {
InputStream inputStream = file.getInputStream();
// 读取文件内容
String content = new String(inputStream.readAllBytes());
inputStream.close();
// 拿到的参数是一个list
List<EnvironmentConfigRequest> environmentConfigRequests = JSON.parseArray(content, EnvironmentConfigRequest.class);
if (CollectionUtils.isNotEmpty(environmentConfigRequests)) {
environmentConfigRequests.forEach(environmentConfigRequest -> {
Environment environment = new Environment();
environment.setId(UUID.randomUUID().toString());
environment.setCreateUser(userId);
environment.setName(environmentConfigRequest.getName());
checkEnvironmentExist(environment);
environment.setCreateTime(System.currentTimeMillis());
environment.setProjectId(currentProjectId);
environmentMapper.insert(environment);
EnvironmentBlob environmentBlob = new EnvironmentBlob();
environmentBlob.setId(environment.getId());
String config = JSON.toJSONString(environmentConfigRequest.getConfig());
environmentBlob.setConfig(config.getBytes());
environmentBlobMapper.insert(environmentBlob);
});
}
} catch (Exception e) {
LogUtils.error("获取文件输入流异常", e);
throw new RuntimeException("获取文件输入流异常", e);
}
}
}
}

View File

@ -0,0 +1,100 @@
package io.metersphere.sdk.service.environment;
import io.metersphere.project.mapper.ProjectMapper;
import io.metersphere.sdk.domain.ProjectParameters;
import io.metersphere.sdk.domain.ProjectParametersExample;
import io.metersphere.sdk.dto.environment.GlobalParamsDTO;
import io.metersphere.sdk.dto.environment.GlobalParamsRequest;
import io.metersphere.sdk.exception.MSException;
import io.metersphere.sdk.mapper.ProjectParametersMapper;
import io.metersphere.sdk.uid.UUID;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.Translator;
import jakarta.annotation.Resource;
import jakarta.transaction.Transactional;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Transactional
public class GlobalParamsService {
@Resource
private ProjectParametersMapper projectParametersMapper;
@Resource
private ProjectMapper projectMapper;
public GlobalParamsRequest add(GlobalParamsRequest globalParamsRequest, String userId) {
ProjectParameters projectParameters = new ProjectParameters();
projectParameters.setProjectId(globalParamsRequest.getProjectId());
checkExist(globalParamsRequest.getProjectId());
checkProjectExist(globalParamsRequest.getProjectId());
projectParameters.setId(UUID.randomUUID().toString());
projectParameters.setCreateUser(userId);
projectParameters.setUpdateUser(userId);
projectParameters.setCreateTime(System.currentTimeMillis());
projectParameters.setUpdateTime(System.currentTimeMillis());
String params = JSON.toJSONString(globalParamsRequest.getGlobalParams());
projectParameters.setParameters(params.getBytes());
projectParametersMapper.insert(projectParameters);
globalParamsRequest.setId(projectParameters.getId());
return globalParamsRequest;
}
public GlobalParamsRequest update(GlobalParamsRequest globalParamsRequest, String userId) {
ProjectParameters projectParameters = new ProjectParameters();
projectParameters.setProjectId(globalParamsRequest.getProjectId());
checkDataExist(globalParamsRequest.getProjectId());
checkProjectExist(globalParamsRequest.getProjectId());
projectParameters.setId(globalParamsRequest.getId());
projectParameters.setUpdateUser(userId);
projectParameters.setUpdateTime(System.currentTimeMillis());
String params = JSON.toJSONString(globalParamsRequest.getGlobalParams());
projectParameters.setParameters(params.getBytes());
projectParametersMapper.updateByPrimaryKeySelective(projectParameters);
return globalParamsRequest;
}
private void checkDataExist(String projectId) {
ProjectParametersExample example = new ProjectParametersExample();
example.createCriteria().andProjectIdEqualTo(projectId);
List<ProjectParameters> projectParameters = projectParametersMapper.selectByExample(example);
if (projectParameters.isEmpty()) {
throw new MSException(Translator.get("global_parameters_is_not_exist"));
}
}
public GlobalParamsRequest get(String projectId) {
ProjectParametersExample example = new ProjectParametersExample();
example.createCriteria().andProjectIdEqualTo(projectId);
List<ProjectParameters> projectParametersList = projectParametersMapper.selectByExampleWithBLOBs(example);
if (CollectionUtils.isNotEmpty(projectParametersList)) {
GlobalParamsRequest globalParamsRequest = new GlobalParamsRequest();
globalParamsRequest.setProjectId(projectId);
globalParamsRequest.setId(projectParametersList.get(0).getId());
globalParamsRequest.setGlobalParams(JSON.parseObject(new String(projectParametersList.get(0).getParameters()), GlobalParamsDTO.class));
return globalParamsRequest;
} else {
return null;
}
}
private void checkExist(String projectId) {
ProjectParametersExample example = new ProjectParametersExample();
example.createCriteria().andProjectIdEqualTo(projectId);
List<ProjectParameters> projectParameters = projectParametersMapper.selectByExample(example);
if (!projectParameters.isEmpty()) {
throw new MSException(Translator.get("global_parameters_already_exist"));
}
}
private void checkProjectExist(String projectId) {
if (projectMapper.selectByPrimaryKey(projectId) == null) {
throw new MSException(Translator.get("project_is_not_exist"));
}
}
}

View File

@ -448,3 +448,6 @@ template_scene_illegal_error=Scene is illegal
custom_field.functional_priority=Priority custom_field.functional_priority=Priority
template.functional_default=Default template.functional_default=Default
global_parameters_already_exist=Global parameters already exist
global_parameters_is_not_exist=Global parameters is not exist

View File

@ -445,3 +445,6 @@ template_scene_illegal_error=使用场景不合法
# 内置的模板或字段 # 内置的模板或字段
custom_field.functional_priority=优先级 custom_field.functional_priority=优先级
template.functional_default=默认模板 template.functional_default=默认模板
global_parameters_already_exist=全局参数已存在
global_parameters_is_not_exist=全局参数不存在

View File

@ -444,3 +444,6 @@ template_scene_illegal_error=使用場景不合法
# 内置的模板或字段 # 内置的模板或字段
custom_field.functional_priority=優先級 custom_field.functional_priority=優先級
template.functional_default=默認模板 template.functional_default=默認模板
global_parameters_already_exist=全局參數已存在
global_parameters_is_not_exist=全局參數不存在

View File

@ -30,7 +30,7 @@ public class ProjectController {
} }
@GetMapping("/list/options/{organizationId}") @GetMapping("/list/options/{organizationId}")
@Operation(summary = "根据项目ID获取所有有权限的项目") @Operation(summary = "根据组织ID获取所有有权限的项目")
@RequiresPermissions(PermissionConstants.PROJECT_BASE_INFO_READ) @RequiresPermissions(PermissionConstants.PROJECT_BASE_INFO_READ)
public List<Project> getUserProject(@PathVariable String organizationId) { public List<Project> getUserProject(@PathVariable String organizationId) {
return projectService.getUserProject(organizationId, SessionUtils.getUserId()); return projectService.getUserProject(organizationId, SessionUtils.getUserId());

View File

@ -0,0 +1,40 @@
package io.metersphere.project.service;
import io.metersphere.sdk.domain.EnvironmentExample;
import io.metersphere.sdk.domain.ProjectParametersExample;
import io.metersphere.sdk.mapper.EnvironmentBlobMapper;
import io.metersphere.sdk.mapper.EnvironmentMapper;
import io.metersphere.sdk.mapper.ProjectParametersMapper;
import io.metersphere.sdk.service.CleanupProjectResourceService;
import io.metersphere.sdk.util.LogUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
@Component
public class CleanupEnvironmentResourceService implements CleanupProjectResourceService {
@Resource
private EnvironmentMapper environmentMapper;
@Resource
private EnvironmentBlobMapper environmentBlobMapper;
@Resource
private ProjectParametersMapper projectParametersMapper;
@Override
public void deleteResources(String projectId) {
EnvironmentExample environmentExample = new EnvironmentExample();
environmentExample.createCriteria().andProjectIdEqualTo(projectId);
environmentMapper.deleteByExample(environmentExample);
environmentBlobMapper.deleteByPrimaryKey(projectId);
ProjectParametersExample projectExample = new ProjectParametersExample();
projectExample.createCriteria().andProjectIdEqualTo(projectId);
projectParametersMapper.deleteByExample(projectExample);
LogUtils.info("删除当前项目[" + projectId + "]相关环境资源");
}
@Override
public void cleanReportResources(String projectId) {
}
}

View File

@ -0,0 +1,35 @@
package io.metersphere.project.controller;
import io.metersphere.project.service.CleanupEnvironmentResourceService;
import io.metersphere.sdk.invoker.ProjectServiceInvoker;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@AutoConfigureMockMvc
public class CleanupEnvironmentTests {
private final ProjectServiceInvoker serviceInvoker;
@Resource
private CleanupEnvironmentResourceService cleanupEnvironmentResourceService;
@Autowired
public CleanupEnvironmentTests(ProjectServiceInvoker serviceInvoker) {
this.serviceInvoker = serviceInvoker;
}
@Test
@Order(1)
public void testCleanupResource() throws Exception {
serviceInvoker.invokeServices("test");
cleanupEnvironmentResourceService.deleteResources("test");
cleanupEnvironmentResourceService.cleanReportResources("test");
}
}

View File

@ -0,0 +1,15 @@
package io.metersphere.project.controller;
import io.metersphere.sdk.base.BaseTest;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class EnvironmentControllerTests extends BaseTest {
}

View File

@ -0,0 +1,309 @@
package io.metersphere.project.controller;
import io.metersphere.sdk.base.BaseTest;
import io.metersphere.sdk.constants.PermissionConstants;
import io.metersphere.sdk.constants.SessionConstants;
import io.metersphere.sdk.constants.VariableTypeConstants;
import io.metersphere.sdk.controller.handler.ResultHolder;
import io.metersphere.sdk.domain.ProjectParameters;
import io.metersphere.sdk.domain.ProjectParametersExample;
import io.metersphere.sdk.dto.environment.GlobalParamsDTO;
import io.metersphere.sdk.dto.environment.GlobalParamsRequest;
import io.metersphere.sdk.dto.environment.KeyValue;
import io.metersphere.sdk.dto.environment.variables.CommonVariables;
import io.metersphere.sdk.mapper.ProjectParametersMapper;
import io.metersphere.sdk.util.JSON;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.*;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.jdbc.SqlConfig;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class GlobalParamsControllerTests extends BaseTest {
@Resource
private MockMvc mockMvc;
private static final String prefix = "/project/global/params";
private static final String add = prefix + "/add";
private static final String get = prefix + "/get/";
private static final String update = prefix + "/update";
private static final ResultMatcher BAD_REQUEST_MATCHER = status().isBadRequest();
private static final ResultMatcher ERROR_REQUEST_MATCHER = status().is5xxServerError();
@Resource
private ProjectParametersMapper projectParametersMapper;
public static <T> T parseObjectFromMvcResult(MvcResult mvcResult, Class<T> parseClass) {
try {
String returnData = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder resultHolder = JSON.parseObject(returnData, ResultHolder.class);
//返回请求正常
Assertions.assertNotNull(resultHolder);
return JSON.parseObject(JSON.toJSONString(resultHolder.getData()), parseClass);
} catch (Exception ignore) {
}
return null;
}
private MvcResult responseGet(String url) throws Exception {
return mockMvc.perform(MockMvcRequestBuilders.get(url)
.header(SessionConstants.HEADER_TOKEN, sessionId)
.header(SessionConstants.CSRF_TOKEN, csrfToken)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON)).andReturn();
}
private MvcResult responsePost(String url, Object param) throws Exception {
return mockMvc.perform(MockMvcRequestBuilders.post(url)
.header(SessionConstants.HEADER_TOKEN, sessionId)
.header(SessionConstants.CSRF_TOKEN, csrfToken)
.content(JSON.toJSONString(param))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON)).andReturn();
}
private void requestPost(String url, Object param, ResultMatcher resultMatcher) throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post(url)
.header(SessionConstants.HEADER_TOKEN, sessionId)
.header(SessionConstants.CSRF_TOKEN, csrfToken)
.content(JSON.toJSONString(param))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(resultMatcher)
.andExpect(content().contentType(MediaType.APPLICATION_JSON));
}
//根据需要多长的list 生成不同的List<EnvVariables> envVariables
private List<CommonVariables> getEnvVariables(int length) {
List<CommonVariables> commonVariables = new ArrayList<>();
for (int i = 0; i < length; i++) {
CommonVariables envVariable = new CommonVariables();
envVariable.setName("name" + i);
envVariable.setValue("value" + i);
envVariable.setDescription("desc" + i);
envVariable.setType(VariableTypeConstants.CONSTANT.name());
commonVariables.add(envVariable);
}
return commonVariables;
}
//根据需要多长的list 生成不同的List<KeyValue> headers
private List<KeyValue> getHeaders(int length) {
List<KeyValue> headers = new ArrayList<>();
for (int i = 0; i < length; i++) {
KeyValue header = new KeyValue();
header.setName("key" + i);
header.setValue("value" + i);
headers.add(header);
}
return headers;
}
@Test
@Order(1)
@Sql(scripts = {"/dml/init_project.sql"},
config = @SqlConfig(encoding = "utf-8", transactionMode = SqlConfig.TransactionMode.ISOLATED),
executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
public void testAddSuccess() throws Exception {
//添加全局参数 有headers 有envVariables
GlobalParamsRequest request = new GlobalParamsRequest();
request.setProjectId("projectId1");
GlobalParamsDTO globalParamsDTO = new GlobalParamsDTO();
globalParamsDTO.setHeaders(getHeaders(1));
globalParamsDTO.setCommonVariables(getEnvVariables(1));
request.setGlobalParams(globalParamsDTO);
MvcResult mvcResult = this.responsePost(add, request);
GlobalParamsRequest globalParamsRequest = parseObjectFromMvcResult(mvcResult, GlobalParamsRequest.class);
Assertions.assertNotNull(globalParamsRequest);
ProjectParameters projectParameters = projectParametersMapper.selectByPrimaryKey(globalParamsRequest.getId());
Assertions.assertNotNull(projectParameters);
Assertions.assertEquals("projectId1", projectParameters.getProjectId());
Assertions.assertEquals(1, JSON.parseObject(new String(projectParameters.getParameters()), GlobalParamsDTO.class).getHeaders().size());
Assertions.assertEquals(1, JSON.parseObject(new String(projectParameters.getParameters()), GlobalParamsDTO.class).getCommonVariables().size());
//添加全局参数 有headers 无envVariables
request = new GlobalParamsRequest();
request.setProjectId("projectId2");
globalParamsDTO = new GlobalParamsDTO();
globalParamsDTO.setHeaders(getHeaders(1));
globalParamsDTO.setCommonVariables(new ArrayList<>());
request.setGlobalParams(globalParamsDTO);
mvcResult = this.responsePost(add, request);
globalParamsRequest = parseObjectFromMvcResult(mvcResult, GlobalParamsRequest.class);
Assertions.assertNotNull(globalParamsRequest);
projectParameters = projectParametersMapper.selectByPrimaryKey(globalParamsRequest.getId());
Assertions.assertNotNull(projectParameters);
Assertions.assertEquals("projectId2", projectParameters.getProjectId());
Assertions.assertEquals(1, JSON.parseObject(new String(projectParameters.getParameters()), GlobalParamsDTO.class).getHeaders().size());
Assertions.assertEquals(0, JSON.parseObject(new String(projectParameters.getParameters()), GlobalParamsDTO.class).getCommonVariables().size());
//添加全局参数 无headers 有envVariables
request = new GlobalParamsRequest();
request.setProjectId("projectId3");
globalParamsDTO = new GlobalParamsDTO();
globalParamsDTO.setHeaders(new ArrayList<>());
globalParamsDTO.setCommonVariables(getEnvVariables(1));
request.setGlobalParams(globalParamsDTO);
mvcResult = this.responsePost(add, request);
globalParamsRequest = parseObjectFromMvcResult(mvcResult, GlobalParamsRequest.class);
Assertions.assertNotNull(globalParamsRequest);
projectParameters = projectParametersMapper.selectByPrimaryKey(globalParamsRequest.getId());
Assertions.assertNotNull(projectParameters);
Assertions.assertEquals("projectId3", projectParameters.getProjectId());
Assertions.assertEquals(0, JSON.parseObject(new String(projectParameters.getParameters()), GlobalParamsDTO.class).getHeaders().size());
Assertions.assertEquals(1, JSON.parseObject(new String(projectParameters.getParameters()), GlobalParamsDTO.class).getCommonVariables().size());
//添加全局参数 无headers 无envVariables
request = new GlobalParamsRequest();
request.setProjectId("projectId4");
globalParamsDTO = new GlobalParamsDTO();
globalParamsDTO.setHeaders(new ArrayList<>());
globalParamsDTO.setCommonVariables(new ArrayList<>());
request.setGlobalParams(globalParamsDTO);
mvcResult = this.responsePost(add, request);
globalParamsRequest = parseObjectFromMvcResult(mvcResult, GlobalParamsRequest.class);
Assertions.assertNotNull(globalParamsRequest);
projectParameters = projectParametersMapper.selectByPrimaryKey(globalParamsRequest.getId());
Assertions.assertNotNull(projectParameters);
Assertions.assertEquals("projectId4", projectParameters.getProjectId());
Assertions.assertEquals(0, JSON.parseObject(new String(projectParameters.getParameters()), GlobalParamsDTO.class).getHeaders().size());
Assertions.assertEquals(0, JSON.parseObject(new String(projectParameters.getParameters()), GlobalParamsDTO.class).getCommonVariables().size());
request = new GlobalParamsRequest();
request.setProjectId("projectId5");
request.setGlobalParams(new GlobalParamsDTO());
mvcResult = this.responsePost(add, request);
globalParamsRequest = parseObjectFromMvcResult(mvcResult, GlobalParamsRequest.class);
Assertions.assertNotNull(globalParamsRequest);
projectParameters = projectParametersMapper.selectByPrimaryKey(globalParamsRequest.getId());
Assertions.assertNotNull(projectParameters);
Assertions.assertEquals("projectId5", projectParameters.getProjectId());
Assertions.assertNull(JSON.parseObject(new String(projectParameters.getParameters()), GlobalParamsDTO.class).getHeaders());
Assertions.assertNull(JSON.parseObject(new String(projectParameters.getParameters()), GlobalParamsDTO.class).getCommonVariables());
//校验权限
request = new GlobalParamsRequest();
request.setProjectId("100001100001");
//权限校验
requestPostPermissionTest(PermissionConstants.PROJECT_ENVIRONMENT_READ_ADD, add, request);
}
@Test
@Order(2)
public void testAddError() throws Exception {
// 添加全局参数 无projectId
GlobalParamsRequest request = new GlobalParamsRequest();
this.requestPost(add, request, BAD_REQUEST_MATCHER);
// 添加全局参数 projectId不存在
request = new GlobalParamsRequest();
request.setProjectId("projectId1111");
this.requestPost(add, request, ERROR_REQUEST_MATCHER);
// 添加全局参数 已存在
request = new GlobalParamsRequest();
request.setProjectId("projectId1");
this.requestPost(add, request, ERROR_REQUEST_MATCHER);
}
@Test
@Order(3)
public void testUpdateSuccess() throws Exception {
//修改全局参数 有headers 有envVariables
ProjectParametersExample example = new ProjectParametersExample();
example.createCriteria().andProjectIdEqualTo("projectId1");
List<ProjectParameters> projectParametersList = projectParametersMapper.selectByExample(example);
GlobalParamsRequest request = new GlobalParamsRequest();
request.setProjectId("projectId1");
request.setId(projectParametersList.get(0).getId());
GlobalParamsDTO globalParamsDTO = new GlobalParamsDTO();
globalParamsDTO.setHeaders(getHeaders(2));
globalParamsDTO.setCommonVariables(getEnvVariables(2));
request.setGlobalParams(globalParamsDTO);
MvcResult mvcResult = this.responsePost(update, request);
GlobalParamsRequest globalParamsRequest = parseObjectFromMvcResult(mvcResult, GlobalParamsRequest.class);
Assertions.assertNotNull(globalParamsRequest);
ProjectParameters projectParameters = projectParametersMapper.selectByPrimaryKey(globalParamsRequest.getId());
Assertions.assertNotNull(projectParameters);
Assertions.assertEquals("projectId1", projectParameters.getProjectId());
Assertions.assertEquals(2, JSON.parseObject(new String(projectParameters.getParameters()), GlobalParamsDTO.class).getHeaders().size());
Assertions.assertEquals(2, JSON.parseObject(new String(projectParameters.getParameters()), GlobalParamsDTO.class).getCommonVariables().size());
//校验权限
request = new GlobalParamsRequest();
example = new ProjectParametersExample();
example.createCriteria().andProjectIdEqualTo("100001100001");
projectParametersList = projectParametersMapper.selectByExample(example);
request.setProjectId("100001100001");
request.setId(projectParametersList.get(0).getId());
//权限校验
requestPostPermissionTest(PermissionConstants.PROJECT_ENVIRONMENT_READ_UPDATE, update, request);
}
@Test
@Order(4)
public void testUpdateError() throws Exception {
// 修改全局参数 无projectId
GlobalParamsRequest request = new GlobalParamsRequest();
request.setProjectId(null);
request.setId("id");
this.requestPost(update, request, BAD_REQUEST_MATCHER);
// 修改全局参数 projectId不存在
request = new GlobalParamsRequest();
request.setId("id");
request.setProjectId("projectId1111");
this.requestPost(update, request, ERROR_REQUEST_MATCHER);
ProjectParametersExample example = new ProjectParametersExample();
example.createCriteria().andProjectIdEqualTo("projectId2");
projectParametersMapper.deleteByExample(example);
// 修改全局参数 全局参数不存在
request = new GlobalParamsRequest();
request.setId("id");
request.setProjectId("projectId2");
this.requestPost(update, request, ERROR_REQUEST_MATCHER);
}
@Test
@Order(5)
public void testGetSuccess() throws Exception {
//获取全局参数
MvcResult mvcResult = this.responseGet(get + "projectId1");
GlobalParamsRequest globalParamsRequest = parseObjectFromMvcResult(mvcResult, GlobalParamsRequest.class);
Assertions.assertNotNull(globalParamsRequest);
Assertions.assertEquals("projectId1", globalParamsRequest.getProjectId());
Assertions.assertEquals(2, globalParamsRequest.getGlobalParams().getHeaders().size());
Assertions.assertEquals(2, globalParamsRequest.getGlobalParams().getCommonVariables().size());
//权限校验
requestGetPermissionTest(PermissionConstants.PROJECT_ENVIRONMENT_READ, get + "100001100001");
//获取全局参数 全局参数不存在
mvcResult = this.responseGet(get + "projectId2");
globalParamsRequest = parseObjectFromMvcResult(mvcResult, GlobalParamsRequest.class);
Assertions.assertNull(globalParamsRequest);
}
}