feat(用例管理): 新增xmind导入

This commit is contained in:
guoyuqi 2024-08-08 10:20:38 +08:00 committed by Craftsman
parent 192a82fff5
commit 8307bf3020
17 changed files with 843 additions and 94 deletions

View File

@ -155,6 +155,18 @@
</exclusions>
</dependency>
<!-- jsonNode xml 格式转换 -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>${jackson-dataformat-xml.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson-core.version}</version> <!-- 替换为你的版本 -->
</dependency>
<!-- minio -->
<dependency>
<groupId>io.minio</groupId>

View File

@ -3,7 +3,6 @@ package io.metersphere.sdk.util;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
@ -11,10 +10,13 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.*;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
@ -206,90 +208,17 @@ public class XMLUtils {
return result.toString();
}
public static String delXmlHeader(String xml) {
int begin = xml.indexOf("?>");
if (begin != -1) {
if (begin + 2 >= xml.length()) {
return null;
}
xml = xml.substring(begin + 2);
} // <?xml version="1.0" encoding="utf-8"?> 若存在则去除
String rgex = ">";
Pattern pattern = Pattern.compile(rgex);
Matcher m = pattern.matcher(xml);
xml = m.replaceAll("> ");
rgex = "\\s*</";
pattern = Pattern.compile(rgex);
m = pattern.matcher(xml);
xml = m.replaceAll(" </");
return xml;
}
// 传入完整的 xml 文本转换成 json 对象
public static JsonNode xmlConvertJson(String xml) {
if (StringUtils.isBlank(xml)) return null;
xml = delXmlHeader(xml);
if (xml == null) return null;
// 创建一个XmlMapper对象
ObjectMapper xmlMapper = new XmlMapper();
// 将XML字符串转换为JsonNode对象
try {
if (stringToDocument(xml) == null) {
LogUtils.error("xml内容转换失败");
return null;
}
} catch (Exception e) {
return xmlMapper.readTree(xml.getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
Element node = null;
try {
node = stringToDocument(xml).getRootElement();
} catch (Exception e) {
throw new RuntimeException(e);
}
return getJsonObjectByDC(node);
}
private static JsonNode getJsonObjectByDC(Element node) {
ObjectNode result = objectMapper.createObjectNode();;
List<Element> listElement = node.elements();// 所有一级子节点的list
if (!listElement.isEmpty()) {
List<JsonNode> list = new LinkedList<>();
for (Element e : listElement) {// 遍历所有一级子节点
JsonNode jsonObject = getJsonObjectByDC(e);
//加xml标签上的属性 eg: <field length="2" scale="0" type="string">RB</field>
//这里添加 length scale type
if (!e.attributes().isEmpty()) {
ObjectNode attributeJson = objectMapper.createObjectNode();;
for (Attribute attribute : e.attributes()) {
try {
attributeJson.putIfAbsent(attribute.getName(), objectMapper.readTree(attribute.getValue()));
} catch (JsonProcessingException ex) {
throw new RuntimeException(ex);
}
}
ObjectNode jsonObjectNode = (ObjectNode) jsonObject;
jsonObjectNode.putIfAbsent("attribute", attributeJson);
}
list.add(jsonObject);
}
if (list.size() == 1) {
result.putIfAbsent(node.getName(), list.get(0));
} else {
try {
String s = objectMapper.writeValueAsString(list);
result.putIfAbsent(node.getName(), objectMapper.readTree(s));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
} else {
if (!StringUtils.isAllBlank(node.getName(), node.getText())) {
try {
result.putIfAbsent(node.getName(), objectMapper.readTree(node.getText()));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
return result;
}
}

View File

@ -41,6 +41,32 @@
<artifactId>metersphere-provider</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb-api.revision}</version>
<scope>provided</scope><!-- only required for compilation with Java 11 or higher, but only used by Java 7 or below -->
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>${jaxb-impl.revision}</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>${jaxb-core.revision}</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>${activation.revision}</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>${jaxen.revision}</version>
</dependency>
</dependencies>
<build>

View File

@ -230,6 +230,15 @@ public class FunctionalCaseController {
return functionalCaseFileService.preCheckExcel(request, file);
}
@PostMapping("/pre-check/xmind")
@Operation(summary = "用例管理-功能用例-xmind导入检查")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_IMPORT)
@CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project")
public FunctionalCaseImportResponse preCheckXMind(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) {
SessionUser user = SessionUtils.getUser();
return functionalCaseFileService.preCheckXMind(request,user, file);
}
@PostMapping("/import/excel")
@Operation(summary = "用例管理-功能用例-excel导入")
@ -240,6 +249,15 @@ public class FunctionalCaseController {
return functionalCaseFileService.importExcel(request, user, file);
}
@PostMapping("/import/xmind")
@Operation(summary = "用例管理-功能用例-xmind导入")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ_IMPORT)
@CheckOwner(resourceId = "#request.getProjectId()", resourceType = "project")
public FunctionalCaseImportResponse importXMind(@RequestPart("request") FunctionalCaseImportRequest request, @RequestPart(value = "file", required = false) MultipartFile file) {
SessionUser user = SessionUtils.getUser();
return functionalCaseFileService.importXMind(request, user, file);
}
@PostMapping("/operation-history")
@Operation(summary = "用例管理-功能用例-变更历史")
@RequiresPermissions(PermissionConstants.FUNCTIONAL_CASE_READ)

View File

@ -27,6 +27,7 @@ import io.metersphere.functional.mapper.ExtFunctionalCaseCommentMapper;
import io.metersphere.functional.request.FunctionalCaseExportRequest;
import io.metersphere.functional.request.FunctionalCaseImportRequest;
import io.metersphere.functional.socket.ExportWebSocketHandler;
import io.metersphere.functional.xmind.parser.XMindCaseParser;
import io.metersphere.plan.domain.TestPlanCaseExecuteHistory;
import io.metersphere.project.domain.Project;
import io.metersphere.project.mapper.ExtBaseProjectVersionMapper;
@ -44,6 +45,7 @@ import io.metersphere.system.dto.sdk.BaseTreeNode;
import io.metersphere.system.dto.sdk.SessionUser;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import io.metersphere.system.dto.sdk.TemplateDTO;
import io.metersphere.system.excel.domain.ExcelErrData;
import io.metersphere.system.excel.utils.EasyExcelExporter;
import io.metersphere.system.manager.ExportTaskManager;
import io.metersphere.system.mapper.SystemParameterMapper;
@ -288,8 +290,40 @@ public class FunctionalCaseFileService {
public List<TemplateCustomFieldDTO> getCustomFields(String projectId) {
TemplateDTO defaultTemplateDTO = projectTemplateService.getDefaultTemplateDTO(projectId, TemplateScene.FUNCTIONAL.name());
List<TemplateCustomFieldDTO> customFields = Optional.ofNullable(defaultTemplateDTO.getCustomFields()).orElse(new ArrayList<>());
return customFields;
return Optional.ofNullable(defaultTemplateDTO.getCustomFields()).orElse(new ArrayList<>());
}
/**
* 导入前校验xmind格式
*
* @return FunctionalCaseImportResponse
*/
public FunctionalCaseImportResponse preCheckXMind(FunctionalCaseImportRequest request,SessionUser user, MultipartFile multipartFile) {
if (multipartFile == null) {
throw new MSException(Translator.get("file_cannot_be_null"));
}
try {
List<ExcelErrData<FunctionalCaseExcelData>> errList;
FunctionalCaseImportResponse response = new FunctionalCaseImportResponse();
//设置默认版本
if (StringUtils.isEmpty(request.getVersionId())) {
request.setVersionId(extBaseProjectVersionMapper.getDefaultVersion(request.getProjectId()));
}
Long nextPos = functionalCaseService.getNextOrder(request.getProjectId());
Long lasePos = nextPos + ((long) ServiceUtils.POS_STEP * Integer.parseInt(request.getCount()));
//获取当前项目默认模板的自定义字段
List<TemplateCustomFieldDTO> customFields = getCustomFields(request.getProjectId());
XMindCaseParser xMindParser = new XMindCaseParser(request, customFields, user,lasePos);
errList = xMindParser.parse(multipartFile);
xMindParser.clear();
response.setErrorMessages(errList);
response.setSuccessCount(xMindParser.getList().size()+xMindParser.getUpdateList().size());
response.setFailCount(errList.size());
return response;
} catch (Exception e) {
LogUtils.error("checkImportExcel error", e);
throw new MSException(Translator.get("check_import_excel_error"));
}
}
@ -332,6 +366,43 @@ public class FunctionalCaseFileService {
}
}
public FunctionalCaseImportResponse importXMind(FunctionalCaseImportRequest request, SessionUser user, MultipartFile multipartFile) {
if (multipartFile == null) {
throw new MSException(Translator.get("file_cannot_be_null"));
}
try {
List<ExcelErrData<FunctionalCaseExcelData>> errList;
FunctionalCaseImportResponse response = new FunctionalCaseImportResponse();
//设置默认版本
if (StringUtils.isEmpty(request.getVersionId())) {
request.setVersionId(extBaseProjectVersionMapper.getDefaultVersion(request.getProjectId()));
}
Long nextPos = functionalCaseService.getNextOrder(request.getProjectId());
Long lasePos = nextPos + ((long) ServiceUtils.POS_STEP * Integer.parseInt(request.getCount()));
//获取当前项目默认模板的自定义字段
List<TemplateCustomFieldDTO> customFields = getCustomFields(request.getProjectId());
XMindCaseParser xmindParser = new XMindCaseParser(request, customFields, user,lasePos);
errList = xmindParser.parse(multipartFile);
if (CollectionUtils.isEmpty(xmindParser.getList())
&& CollectionUtils.isEmpty(xmindParser.getUpdateList())) {
if (errList == null) {
errList = new ArrayList<>();
}
ExcelErrData excelErrData = new ExcelErrData(1, Translator.get("upload_fail") + "" + Translator.get("upload_content_is_null"));
errList.add(excelErrData);
}
xmindParser.saveData();
xmindParser.clear();
response.setErrorMessages(errList);
response.setSuccessCount(xmindParser.getList().size()+xmindParser.getUpdateList().size());
response.setFailCount(errList.size());
return response;
} catch (Exception e) {
LogUtils.error("checkImportExcel error", e);
throw new MSException(Translator.get("check_import_excel_error"));
}
}
public void export(String userId, FunctionalCaseExportRequest request) {
try {
ExportTaskExample exportTaskExample = new ExportTaskExample();

View File

@ -1273,11 +1273,11 @@ public class FunctionalCaseService {
}
private void setCustomFieldValue(Object value, FunctionalCaseCustomField caseCustomField) {
if (StringUtils.equalsIgnoreCase(value.toString(), "[]") || value instanceof List) {
if (value !=null && (StringUtils.equalsIgnoreCase(value.toString(), "[]") || value instanceof List)) {
//数组类型
caseCustomField.setValue(JSON.toJSONString(value));
} else {
caseCustomField.setValue(value.toString());
caseCustomField.setValue(value == null ? StringUtils.EMPTY : value.toString());
}
}

View File

@ -0,0 +1,477 @@
package io.metersphere.functional.xmind.parser;
import io.metersphere.functional.dto.FunctionalCaseStepDTO;
import io.metersphere.functional.excel.domain.FunctionalCaseExcelData;
import io.metersphere.functional.excel.exception.CustomFieldValidateException;
import io.metersphere.functional.excel.validate.AbstractCustomFieldValidator;
import io.metersphere.functional.excel.validate.CustomFieldValidatorFactory;
import io.metersphere.functional.request.FunctionalCaseImportRequest;
import io.metersphere.functional.service.FunctionalCaseModuleService;
import io.metersphere.functional.service.FunctionalCaseService;
import io.metersphere.functional.xmind.pojo.Attached;
import io.metersphere.functional.xmind.pojo.JsonRootBean;
import io.metersphere.functional.xmind.utils.DetailUtil;
import io.metersphere.sdk.util.CommonBeanFactory;
import io.metersphere.sdk.util.JSON;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.dto.sdk.BaseTreeNode;
import io.metersphere.system.dto.sdk.SessionUser;
import io.metersphere.system.dto.sdk.TemplateCustomFieldDTO;
import io.metersphere.system.excel.domain.ExcelErrData;
import lombok.Getter;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* 数据转换
* 1 解析Xmind文件 XmindParser.parseObject
* 2 解析后的JSON this.parse 转成测试用例
*/
public class XMindCaseParser {
Map<String, TemplateCustomFieldDTO> customFieldsMap;
@Getter
protected List<FunctionalCaseExcelData> list = new ArrayList<>();
@Getter
protected List<FunctionalCaseExcelData> updateList = new ArrayList<>();
private FunctionalCaseService functionalCaseService;
private FunctionalCaseImportRequest request;
/**
* 所有的模块集合
*/
private List<BaseTreeNode> moduleTree;
private SessionUser user;
private Map<String, String> pathMap = new HashMap<>();
protected static final int TAGS_COUNT = 15;
protected static final int TAG_LENGTH = 64;
protected static final int STEP_LENGTH = 1000;
private AtomicLong lastPos;
private HashMap<String, AbstractCustomFieldValidator> customFieldValidatorMap;
/**
* 过程校验记录
*/
private DetailUtil process;
public XMindCaseParser(FunctionalCaseImportRequest request, List<TemplateCustomFieldDTO> customFields, SessionUser user, Long pos) {
//当前项目模板的自定义字段
this.request = request;
customFieldsMap = customFields.stream().collect(Collectors.toMap(TemplateCustomFieldDTO::getFieldName, i -> i));
functionalCaseService = CommonBeanFactory.getBean(FunctionalCaseService.class);
moduleTree = CommonBeanFactory.getBean(FunctionalCaseModuleService.class).getTree(request.getProjectId());
customFieldValidatorMap = CustomFieldValidatorFactory.getValidatorMap();
lastPos = new AtomicLong(pos);
this.user = user;
process = new DetailUtil();
}
private final List<String> priorityList = Arrays.asList("P0", "P1", "P2", "P3");
private static final String ID = "(?:id:|id)";
private static final String CASE = "(?:CASE-|case-)";
private static final String PREREQUISITE = "(?:" + Translator.get("xmind_prerequisite") + ":|" + Translator.get("xmind_prerequisite") + ")";
private static final String STEP = "(?:" + Translator.get("xmind_step") + ":|" + Translator.get("xmind_step") + ")";
private static final String STEP_DESCRIPTION = "(?:" + Translator.get("xmind_stepDescription") + ":|" + Translator.get("xmind_stepDescription") + ")";
private static final String TEXT_DESCRIPTION = "(?:" + Translator.get("xmind_textDescription") + ":|" + Translator.get("xmind_textDescription") + ")";
private static final String EXPECTED_RESULT = "(?:" + Translator.get("xmind_expectedResult") + ":|" + Translator.get("xmind_expectedResult") + ")";
private static final String DESCRIPTION = "(?:" + Translator.get("xmind_description") + ":|" + Translator.get("xmind_description") + ")";
private static final String TAGS = "(?:" + Translator.get("xmind_tags") + ":|" + Translator.get("xmind_tags") + ")";
public void clear() {
list.clear();
updateList.clear();
pathMap.clear();
moduleTree = new ArrayList<>();
customFieldValidatorMap = new HashMap<>();
lastPos = null;
}
private boolean isAvailable(String str, String regex) {
if (StringUtils.isEmpty(str) || StringUtils.isEmpty(regex)) {
return false;
}
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
Matcher result = pattern.matcher(str);
return result.find();
}
private String replace(String str, String regex) {
if (StringUtils.isEmpty(str) || StringUtils.isEmpty(regex)) {
return str;
}
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
Matcher result = pattern.matcher(str);
str = result.replaceAll(StringUtils.EMPTY);
return str;
}
/**
* 递归处理案例数据
*/
private void recursion(Attached parent, int level, List<Attached> attachedList) {
for (Attached item : attachedList) {
if (isAvailable(item.getTitle(), CASE)) {
item.setParent(parent);
// 格式化一个用例
this.formatTestCase(item.getTitle(), parent.getPath(), item.getChildren() != null ? item.getChildren().getAttached() : null);
} else {
String nodePath = parent.getPath().trim() + "/" + item.getTitle().trim();
item.setPath(nodePath);
item.setParent(parent);
if (item.getChildren() != null && CollectionUtils.isNotEmpty(item.getChildren().getAttached())) {
recursion(item, level + 1, item.getChildren().getAttached());
}
}
}
}
/**
* 验证用例的合规性
*/
public boolean validate(FunctionalCaseExcelData data) {
//模块校验
boolean validate;
validate = validateModule(data);
if (!validate) {
return false;
}
//校验自定义字段
validate = validateCustomField(data);
if (!validate) {
return false;
}
//校验id
validate = validateIdExist(data);
if (!validate) {
return false;
}
//标签长度校验
validate = validateTags(data);
return validate;
}
/**
* 校验标签长度 个数
*/
private boolean validateTags(FunctionalCaseExcelData data) {
AtomicBoolean validate = new AtomicBoolean(true);
List<String> tags = functionalCaseService.handleImportTags(data.getTags());
if (tags.size() > TAGS_COUNT) {
process.add(data.getName(), Translator.get("tags_count"));
return false;
}
tags.forEach(tag -> {
if (tag.length() > TAG_LENGTH) {
process.add(data.getName(), Translator.get("tag_length"));
validate.set(false);
}
});
return validate.get();
}
/**
* 校验模块
*/
private boolean validateModule(FunctionalCaseExcelData data) {
boolean validate = true;
String module = data.getModule();
if (StringUtils.isNotEmpty(module)) {
String[] nodes = module.split("/");
//模块名不能为空
for (int i = 0; i < nodes.length; i++) {
if (i != 0 && StringUtils.equals(nodes[i].trim(), StringUtils.EMPTY)) {
validate = false;
break;
}
}
//增加字数校验每一层不能超过100个字
for (String nodeStr : nodes) {
if (StringUtils.isNotEmpty(nodeStr)) {
if (nodeStr.trim().length() > 100) {
validate = false;
break;
}
}
}
}
return validate;
}
/**
* 校验Excel中是否有ID
* 是否覆盖
* 1.覆盖id存在则更新id不存在则新增
* 2.不覆盖id存在不处理id不存在新增
*/
@Nullable
private boolean validateIdExist(FunctionalCaseExcelData data) {
boolean validate = true;
//当前读取的数据有ID
if (StringUtils.isNotEmpty(data.getNum())) {
Integer num = -1;
try {
num = Integer.parseInt(data.getNum());
} catch (Exception e) {
validate = false;
process.add(data.getName(), Translator.get("id_not_rightful") + "[" + data.getNum() + "]");
}
if (num < 0) {
validate = false;
process.add(data.getName(), Translator.get("id_not_rightful") + "[" + data.getNum() + "]");
}
}
return validate;
}
/**
* 校验自定义字段并记录错误提示
* 如果填写的是自定义字段的选项值则转换成ID保存
*/
private boolean validateCustomField(FunctionalCaseExcelData data) {
boolean validate = true;
Map<String, Object> customData = data.getCustomData();
for (String fieldName : customData.keySet()) {
Object value = customData.get(fieldName);
TemplateCustomFieldDTO templateCustomFieldDTO = customFieldsMap.get(fieldName);
if (templateCustomFieldDTO == null) {
continue;
}
AbstractCustomFieldValidator customFieldValidator = customFieldValidatorMap.get(templateCustomFieldDTO.getType());
try {
customFieldValidator.validate(templateCustomFieldDTO, value.toString());
if (customFieldValidator.isKVOption) {
// 这里如果填的是选项值替换成选项ID保存
customData.put(fieldName, customFieldValidator.parse2Key(value.toString(), templateCustomFieldDTO));
}
} catch (CustomFieldValidateException e) {
validate = false;
process.add(data.getName(), e.getMessage());
}
}
return validate;
}
/**
* 处理新增数据集合还是更新数据集合
*
* @param functionalCaseExcelData
*/
private void handleId(FunctionalCaseExcelData functionalCaseExcelData) {
if (StringUtils.isNotEmpty(functionalCaseExcelData.getNum())) {
String checkResult = functionalCaseService.checkNumExist(functionalCaseExcelData.getNum(), request.getProjectId());
if (StringUtils.isNotEmpty(checkResult)) {
if (request.isCover()) {
//如果是覆盖那么有id的需要更新
functionalCaseExcelData.setNum(checkResult);
updateList.add(functionalCaseExcelData);
}
} else {
list.add(functionalCaseExcelData);
}
} else {
list.add(functionalCaseExcelData);
}
}
/**
* 格式化一个用例
*/
private void formatTestCase(String title, String nodePath, List<Attached> attacheds) {
FunctionalCaseExcelData testCase = new FunctionalCaseExcelData();
String tc = title.replace("", ":");
String[] tcArrs = tc.split(":");
if (tcArrs.length <= 1) {
process.add(Translator.get("test_case_name") + Translator.get("incorrect_format"), title);
return;
}
// 用例名称
String name = title.replace(tcArrs[0] + "", StringUtils.EMPTY).replace(tcArrs[0] + ":", StringUtils.EMPTY);
testCase.setName(name);
nodePath = nodePath.trim();
if (!nodePath.startsWith("/")) {
nodePath = "/" + nodePath;
}
if (nodePath.endsWith("/")) {
nodePath = nodePath.substring(0, nodePath.length() - 1);
}
testCase.setModule(nodePath);
// 用例等级和用例性质处理
if (tcArrs[0].contains("-")) {
for (String item : tcArrs[0].split("-")) {
if (item.toUpperCase().startsWith("P")) {
Map<String, Object> customData = new LinkedHashMap<>();
// 用例等级和用例性质处理
if (!priorityList.contains(item.toUpperCase())) {
process.add(title, Translator.get("test_case_priority") + Translator.get("incorrect_format"));
customData.put("priority", "P0");
} else {
customData.put("priority", item.toUpperCase());
}
testCase.setCustomData(customData);
}
}
}
// 用例id blobs tags, 自定义字段处理
StringBuilder customId = new StringBuilder();
if (attacheds != null && !attacheds.isEmpty()) {
attacheds.forEach(item -> {
//id
if (isAvailable(item.getTitle(), ID)) {
customId.append(replace(item.getTitle(), ID));
testCase.setNum(customId.toString());
} else if (isAvailable(item.getTitle(), PREREQUISITE)) {
testCase.setPrerequisite(replace(item.getTitle(), PREREQUISITE));
} else if (isAvailable(item.getTitle(), TEXT_DESCRIPTION)) {
testCase.setTextDescription(replace(item.getTitle(), TEXT_DESCRIPTION));
} else if (isAvailable(item.getTitle(), DESCRIPTION)) {
testCase.setTextDescription(replace(item.getTitle(), DESCRIPTION));
} else if (isAvailable(item.getTitle(), TAGS)) {
String tag = replace(item.getTitle(), TAGS);
String[] tagArr = tag.split("\\|");
if (CollectionUtils.isNotEmpty(Arrays.asList(tagArr))) {
String tags = StringUtils.joinWith(",", Arrays.asList(tagArr));
testCase.setTags(tags);
}
} else if (isAvailable(item.getTitle(), STEP_DESCRIPTION)) {
if (item.getChildren() != null) {
testCase.setSteps(this.getSteps(item.getChildren().getAttached(), title));
}
} else {
//自定义字段
String[] customFiled = item.getTitle().split("(?::|)");
Map<String, Object> stringObjectMap = testCase.getCustomData();
if (customFiled.length > 1) {
TemplateCustomFieldDTO templateCustomFieldDTO = customFieldsMap.get(customFiled[0]);
if (templateCustomFieldDTO == null) {
stringObjectMap.put(customFiled[0], customFiled[1]);
}
} else {
TemplateCustomFieldDTO templateCustomFieldDTO = customFieldsMap.get(customFiled[0]);
if (templateCustomFieldDTO == null) {
stringObjectMap.put(customFiled[0], StringUtils.EMPTY);
}
}
testCase.setCustomData(stringObjectMap);
}
});
}
boolean validate = validate(testCase);
if (validate) {
handleId(testCase);
}
}
/**
* 执行保存数据
*/
public void saveData() {
if (CollectionUtils.isNotEmpty(list)) {
functionalCaseService.saveImportData(list, request, moduleTree, customFieldsMap, pathMap, user, lastPos);
}
if (CollectionUtils.isNotEmpty(updateList)) {
functionalCaseService.updateImportData(updateList, request, moduleTree, customFieldsMap, pathMap, user);
}
}
/**
* 获取步骤数据
*/
private String getSteps(List<Attached> attacheds, String caseName) {
List<FunctionalCaseStepDTO> functionalCaseStepDTOS = new ArrayList<>();
if (!attacheds.isEmpty()) {
for (int i = 0; i < attacheds.size(); i++) {
// 保持插入顺序判断用例是否有相同的steps
FunctionalCaseStepDTO functionalCaseStepDTO = new FunctionalCaseStepDTO();
functionalCaseStepDTO.setId(UUID.randomUUID().toString());
functionalCaseStepDTO.setNum(i + 1);
if (isAvailable(attacheds.get(i).getTitle(), STEP)) {
String stepDesc = attacheds.get(i).getTitle().replace("", ":");
String[] stepDescArrs = stepDesc.split(":");
functionalCaseStepDTO.setDesc(StringUtils.isNotBlank(stepDescArrs[1]) ? stepDescArrs[1] : StringUtils.EMPTY);
} else {
functionalCaseStepDTO.setDesc(StringUtils.EMPTY);
}
if (functionalCaseStepDTO.getDesc().length() > STEP_LENGTH) {
process.add(caseName, Translator.get("step_length"));
return JSON.toJSONString(functionalCaseStepDTOS);
}
if (attacheds.get(i) != null && attacheds.get(i).getChildren() != null && attacheds.get(i).getChildren().getAttached() != null) {
String title = attacheds.get(i).getChildren().getAttached().get(0).getTitle();
if (isAvailable(title, EXPECTED_RESULT)) {
String stepDesc = title.replace("", ":");
String[] stepDescArrs = stepDesc.split(":");
functionalCaseStepDTO.setResult(StringUtils.isNotBlank(stepDescArrs[1]) ? stepDescArrs[1] : StringUtils.EMPTY);
} else {
functionalCaseStepDTO.setResult(StringUtils.EMPTY);
}
}
if (functionalCaseStepDTO.getResult().length() > STEP_LENGTH) {
process.add(caseName, Translator.get("result_length"));
return JSON.toJSONString(functionalCaseStepDTOS);
}
functionalCaseStepDTOS.add(functionalCaseStepDTO);
}
} else {
// 保持插入顺序判断用例是否有相同的steps
FunctionalCaseStepDTO functionalCaseStepDTO = new FunctionalCaseStepDTO();
functionalCaseStepDTO.setId(UUID.randomUUID().toString());
functionalCaseStepDTO.setNum(1);
functionalCaseStepDTO.setDesc(StringUtils.EMPTY);
functionalCaseStepDTO.setResult(StringUtils.EMPTY);
functionalCaseStepDTOS.add(functionalCaseStepDTO);
}
return JSON.toJSONString(functionalCaseStepDTOS);
}
/**
* 导入思维导图处理
*/
public List<ExcelErrData<FunctionalCaseExcelData>> parse(MultipartFile multipartFile) {
try {
// 获取思维导图内容
List<JsonRootBean> roots = XMindParser.parseObject(multipartFile);
for (JsonRootBean root : roots) {
if (root != null && root.getRootTopic() != null && root.getRootTopic().getChildren() != null) {
// 判断是模块还是用例
for (Attached item : root.getRootTopic().getChildren().getAttached()) {
// 用例
if (isAvailable(item.getTitle(), CASE)) {
return process.parse(replace(item.getTitle(), CASE) + "" + Translator.get("test_case_create_module_fail"));
} else {
String modulePath = item.getTitle();
item.setPath(modulePath);
if (item.getChildren() != null && !item.getChildren().getAttached().isEmpty()) {
// 递归处理案例数据
recursion(item, 1, item.getChildren().getAttached());
}
}
}
}
}
} catch (Exception ex) {
LogUtils.info(ex.getMessage());
return process.parse(ex.getMessage());
}
return process.parse();
}
}

View File

@ -1,6 +1,7 @@
package io.metersphere.functional.xmind.parser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.metersphere.sdk.util.LogUtils;
import io.metersphere.sdk.util.XMLUtils;
import org.apache.commons.lang3.StringUtils;
@ -73,8 +74,8 @@ public class XMindLegacy {
String res = sheet.asXML();
// 将xml转为json
JsonNode xmlJSONObj = XMLUtils.xmlConvertJson(res);
JsonNode jsonNode = xmlJSONObj.get("sheet");
sheets.add(jsonNode.toString());
ObjectMapper objectMapper = new ObjectMapper();
sheets.add(objectMapper.writeValueAsString(xmlJSONObj));
}
// 设置缩进
return sheets;

View File

@ -66,7 +66,7 @@ public class XMindParser {
List<JsonRootBean> jsonRootBeans = new ArrayList<>();
if (contents != null) {
for (String content : contents) {
caseCount += content.split("(case-:)").length;
caseCount += content.split("(?:case-|CASE-)").length;
JsonRootBean jsonRootBean = JSON.parseObject(content, JsonRootBean.class);
jsonRootBeans.add(jsonRootBean);
}

View File

@ -1,14 +1,16 @@
package io.metersphere.functional.xmind.pojo;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* XMind 节点对象
*/
@Data
@Setter
@Getter
public class Attached {
private String id;

View File

@ -0,0 +1,70 @@
package io.metersphere.functional.xmind.utils;
import io.metersphere.functional.excel.domain.FunctionalCaseExcelData;
import io.metersphere.sdk.util.Translator;
import io.metersphere.system.excel.domain.ExcelErrData;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Getter
@Setter
public class DetailUtil implements Serializable {
private Map<String, StringBuilder> process;
public DetailUtil() {
process = new LinkedHashMap<>();
}
public void add(String type, String msContent) {
if (process.containsKey(type)) {
process.get(type).append(msContent + "");
} else {
process.put(type, new StringBuilder(msContent + ""));
}
}
public List<ExcelErrData<FunctionalCaseExcelData>> parse(String content) {
List<ExcelErrData<FunctionalCaseExcelData>> errList = new ArrayList<>();
ExcelErrData excelErrData = new ExcelErrData(1, content);
errList.add(excelErrData);
return errList;
}
public List<ExcelErrData<FunctionalCaseExcelData>> parse() {
List<ExcelErrData<FunctionalCaseExcelData>> errList = new ArrayList<>();
List<String> result = new ArrayList<>();
process.entrySet().parallelStream().reduce(result, (first, second) -> {
first.add(second.getKey() + "" + second.getValue());
return first;
}, (first, second) -> {
if (first == second) {
return first;
}
first.addAll(second);
return first;
});
int emptyName = 0;
for (int i = 0; i < result.size(); i++) {
if (result.get(i).contains(Translator.get("test_case_name") + Translator.get("incorrect_format"))) {
emptyName +=1;
} else {
ExcelErrData excelErrData = new ExcelErrData(i, result.get(i));
errList.add(excelErrData);
}
}
if (emptyName>0) {
ExcelErrData excelErrData = new ExcelErrData(result.size(), "部分用例名称为空,校验失败;");
errList.add(excelErrData);
}
return errList;
}
}

View File

@ -1,9 +1,6 @@
package io.metersphere.functional.controller;
import io.metersphere.functional.domain.FunctionalCase;
import io.metersphere.functional.domain.FunctionalCaseAttachment;
import io.metersphere.functional.domain.FunctionalCaseAttachmentExample;
import io.metersphere.functional.domain.FunctionalCaseCustomField;
import io.metersphere.functional.domain.*;
import io.metersphere.functional.dto.CaseCustomFieldDTO;
import io.metersphere.functional.dto.FunctionalCaseAttachmentDTO;
import io.metersphere.functional.dto.FunctionalCasePageDTO;
@ -11,6 +8,7 @@ import io.metersphere.functional.dto.response.FunctionalCaseImportResponse;
import io.metersphere.functional.excel.domain.FunctionalCaseHeader;
import io.metersphere.functional.mapper.FunctionalCaseAttachmentMapper;
import io.metersphere.functional.mapper.FunctionalCaseCustomFieldMapper;
import io.metersphere.functional.mapper.FunctionalCaseMapper;
import io.metersphere.functional.request.*;
import io.metersphere.functional.result.CaseManagementResultCode;
import io.metersphere.functional.utils.FileBaseUtils;
@ -84,7 +82,9 @@ public class FunctionalCaseControllerTests extends BaseTest {
public static final String FUNCTIONAL_CASE_POS_URL = "/functional/case/edit/pos";
public static final String DOWNLOAD_EXCEL_TEMPLATE_URL = "/functional/case/download/excel/template/";
public static final String CHECK_EXCEL_URL = "/functional/case/pre-check/excel";
public static final String CHECK_XMIND_URL = "/functional/case/pre-check/xmind";
public static final String IMPORT_EXCEL_URL = "/functional/case/import/excel";
public static final String IMPORT_XMIND_URL = "/functional/case/import/xmind";
public static final String OPERATION_HISTORY_URL = "/functional/case/operation-history";
public static final String EXPORT_EXCEL_URL = "/functional/case/export/excel";
public static final String DOWNLOAD_XMIND_TEMPLATE_URL = "/functional/case/download/xmind/template/";
@ -109,6 +109,9 @@ public class FunctionalCaseControllerTests extends BaseTest {
@Resource
private FunctionalCaseAttachmentMapper functionalCaseAttachmentMapper;
@Resource
private FunctionalCaseMapper functionalCaseMapper;
protected static String functionalCaseId;
@ -774,6 +777,136 @@ public class FunctionalCaseControllerTests extends BaseTest {
this.requestMultipart(IMPORT_EXCEL_URL, paramMap);
}
@Test
@Order(19)
public void testImportXmind() throws Exception {
String filePath = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/1xml.xmind")).getPath();
MockMultipartFile file = new MockMultipartFile("file", "11.xmind", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath));
FunctionalCaseImportRequest request = new FunctionalCaseImportRequest();
request.setCover(true);
request.setProjectId("100001100001");
request.setCount("1");
LinkedMultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("file", file);
MvcResult functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(IMPORT_XMIND_URL, paramMap);
String functionalCaseImportResponseData = functionalCaseMvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder functionalCaseResultHolder = JSON.parseObject(functionalCaseImportResponseData, ResultHolder.class);
FunctionalCaseImportResponse functionalCaseImportResponse = JSON.parseObject(JSON.toJSONString(functionalCaseResultHolder.getData()), FunctionalCaseImportResponse.class);
Assertions.assertNotNull(functionalCaseImportResponse);
System.out.println(JSON.toJSONString(functionalCaseImportResponse));
FunctionalCaseExample functionalCaseExample = new FunctionalCaseExample();
functionalCaseExample.createCriteria().andNameEqualTo("用例名称");
List<FunctionalCase> functionalCases = functionalCaseMapper.selectByExample(functionalCaseExample);
System.out.println(JSON.toJSONString(functionalCases));
String filePath5 = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/2module.xmind")).getPath();
MockMultipartFile file5 = new MockMultipartFile("file", "15.xmind", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath5));
paramMap = new LinkedMultiValueMap<>();
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("file", file5);
functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(IMPORT_XMIND_URL, paramMap);
functionalCaseImportResponseData = functionalCaseMvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
functionalCaseResultHolder = JSON.parseObject(functionalCaseImportResponseData, ResultHolder.class);
functionalCaseImportResponse = JSON.parseObject(JSON.toJSONString(functionalCaseResultHolder.getData()), FunctionalCaseImportResponse.class);
Assertions.assertNotNull(functionalCaseImportResponse);
System.out.println(JSON.toJSONString(functionalCaseImportResponse));
functionalCaseExample = new FunctionalCaseExample();
functionalCaseExample.createCriteria().andNameEqualTo("用例名称");
functionalCases = functionalCaseMapper.selectByExample(functionalCaseExample);
System.out.println(JSON.toJSONString(functionalCases));
String filePath1 = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/3erro.xmind")).getPath();
MockMultipartFile file1 = new MockMultipartFile("file", "14.xmind", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath1));
paramMap = new LinkedMultiValueMap<>();
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("file", file1);
functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(IMPORT_XMIND_URL, paramMap);
functionalCaseImportResponseData = functionalCaseMvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
functionalCaseResultHolder = JSON.parseObject(functionalCaseImportResponseData, ResultHolder.class);
functionalCaseImportResponse = JSON.parseObject(JSON.toJSONString(functionalCaseResultHolder.getData()), FunctionalCaseImportResponse.class);
Assertions.assertNotNull(functionalCaseImportResponse);
System.out.println(JSON.toJSONString(functionalCaseImportResponse));
String filePath4 = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/empty.xmind")).getPath();
MockMultipartFile file2 = new MockMultipartFile("file", "18.xmind", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath4));
paramMap = new LinkedMultiValueMap<>();
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("file", file2);
functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(IMPORT_XMIND_URL, paramMap);
functionalCaseImportResponseData = functionalCaseMvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
functionalCaseResultHolder = JSON.parseObject(functionalCaseImportResponseData, ResultHolder.class);
functionalCaseImportResponse = JSON.parseObject(JSON.toJSONString(functionalCaseResultHolder.getData()), FunctionalCaseImportResponse.class);
Assertions.assertNotNull(functionalCaseImportResponse);
System.out.println(JSON.toJSONString(functionalCaseImportResponse));
}
@Test
@Order(20)
public void testPreCheckImportXmind() throws Exception {
FunctionalCaseImportRequest request = new FunctionalCaseImportRequest();
request.setCover(true);
request.setProjectId("100001100001");
request.setCount("1");
LinkedMultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
String filePath = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/1xml.xmind")).getPath();
MockMultipartFile file = new MockMultipartFile("file", "11.xmind", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath));
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("file", file);
MvcResult functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(CHECK_XMIND_URL, paramMap);
String functionalCaseImportResponseData = functionalCaseMvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
ResultHolder functionalCaseResultHolder = JSON.parseObject(functionalCaseImportResponseData, ResultHolder.class);
FunctionalCaseImportResponse functionalCaseImportResponse = JSON.parseObject(JSON.toJSONString(functionalCaseResultHolder.getData()), FunctionalCaseImportResponse.class);
Assertions.assertNotNull(functionalCaseImportResponse);
System.out.println(JSON.toJSONString(functionalCaseImportResponse));
String filePath5 = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/2module.xmind")).getPath();
MockMultipartFile file5 = new MockMultipartFile("file", "15.xmind", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath5));
paramMap = new LinkedMultiValueMap<>();
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("file", file5);
functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(CHECK_XMIND_URL, paramMap);
functionalCaseImportResponseData = functionalCaseMvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
functionalCaseResultHolder = JSON.parseObject(functionalCaseImportResponseData, ResultHolder.class);
functionalCaseImportResponse = JSON.parseObject(JSON.toJSONString(functionalCaseResultHolder.getData()), FunctionalCaseImportResponse.class);
Assertions.assertNotNull(functionalCaseImportResponse);
System.out.println(JSON.toJSONString(functionalCaseImportResponse));
String filePath1 = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/3erro.xmind")).getPath();
MockMultipartFile file1 = new MockMultipartFile("file", "14.xmind", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath1));
paramMap = new LinkedMultiValueMap<>();
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("file", file1);
functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(CHECK_XMIND_URL, paramMap);
functionalCaseImportResponseData = functionalCaseMvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
functionalCaseResultHolder = JSON.parseObject(functionalCaseImportResponseData, ResultHolder.class);
functionalCaseImportResponse = JSON.parseObject(JSON.toJSONString(functionalCaseResultHolder.getData()), FunctionalCaseImportResponse.class);
Assertions.assertNotNull(functionalCaseImportResponse);
System.out.println(JSON.toJSONString(functionalCaseImportResponse.getErrorMessages()));
String filePath4 = Objects.requireNonNull(this.getClass().getClassLoader().getResource("file/empty.xmind")).getPath();
MockMultipartFile file2 = new MockMultipartFile("file", "18.xmind", MediaType.APPLICATION_OCTET_STREAM_VALUE, FileBaseUtils.getFileBytes(filePath4));
paramMap = new LinkedMultiValueMap<>();
paramMap.add("request", JSON.toJSONString(request));
paramMap.add("file", file2);
functionalCaseMvcResult = this.requestMultipartWithOkAndReturn(CHECK_XMIND_URL, paramMap);
functionalCaseImportResponseData = functionalCaseMvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
functionalCaseResultHolder = JSON.parseObject(functionalCaseImportResponseData, ResultHolder.class);
functionalCaseImportResponse = JSON.parseObject(JSON.toJSONString(functionalCaseResultHolder.getData()), FunctionalCaseImportResponse.class);
Assertions.assertNotNull(functionalCaseImportResponse);
System.out.println(JSON.toJSONString(functionalCaseImportResponse));
}
@Test
@Order(22)
public void operationHistoryList() throws Exception {

10
pom.xml
View File

@ -85,6 +85,16 @@
<commons-jexl3.version>3.3</commons-jexl3.version>
<revision>3.x</revision>
<monitoring-engine.revision>3.0</monitoring-engine.revision>
<!-- 用于解析xml -->
<jaxb-api.revision>2.4.0-b180830.0359</jaxb-api.revision>
<jaxb-impl.revision>2.3.0</jaxb-impl.revision>
<jaxb-core.revision>2.3.0</jaxb-core.revision>
<activation.revision>1.1.1</activation.revision>
<jaxen.revision>1.2.0</jaxen.revision>
<jackson-dataformat-xml.version>2.17.2</jackson-dataformat-xml.version>
<jackson-core.version>2.17.2</jackson-core.version>
</properties>
<modules>