feat(测试跟踪): 支持思维导图导入用例

This commit is contained in:
fit2-zhao 2020-09-16 18:47:04 +08:00
parent d71969b2fa
commit 76b09b4751
22 changed files with 1048 additions and 142 deletions

View File

@ -297,6 +297,31 @@
<version>0.15.2</version> <version>0.15.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.20</version>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<!--xpath不加这个依赖会报错-->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20171018</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -2,9 +2,10 @@ package io.metersphere.base.mapper;
import io.metersphere.base.domain.TestCaseNode; import io.metersphere.base.domain.TestCaseNode;
import io.metersphere.base.domain.TestCaseNodeExample; import io.metersphere.base.domain.TestCaseNodeExample;
import java.util.List;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface TestCaseNodeMapper { public interface TestCaseNodeMapper {
long countByExample(TestCaseNodeExample example); long countByExample(TestCaseNodeExample example);
@ -14,6 +15,9 @@ public interface TestCaseNodeMapper {
int insert(TestCaseNode record); int insert(TestCaseNode record);
int insertBatch(@Param("records") List<TestCaseNode> records);
int insertSelective(TestCaseNode record); int insertSelective(TestCaseNode record);
List<TestCaseNode> selectByExample(TestCaseNodeExample example); List<TestCaseNode> selectByExample(TestCaseNodeExample example);

View File

@ -101,14 +101,28 @@
<include refid="Example_Where_Clause" /> <include refid="Example_Where_Clause" />
</if> </if>
</delete> </delete>
<insert id="insert" parameterType="io.metersphere.base.domain.TestCaseNode">
<insert id="insertBatch" parameterType="io.metersphere.base.domain.TestCaseNode">
insert into test_case_node (id, project_id, name, insert into test_case_node (id, project_id, name,
parent_id, level, create_time, parent_id, level, create_time,
update_time) update_time)
values (#{id,jdbcType=VARCHAR}, #{projectId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, values
#{parentId,jdbcType=VARCHAR}, #{level,jdbcType=INTEGER}, #{createTime,jdbcType=BIGINT}, <foreach collection="records" item="emp" separator=",">
(#{emp.id,jdbcType=VARCHAR}, #{emp.projectId,jdbcType=VARCHAR}, #{emp.name,jdbcType=VARCHAR},
#{emp.parentId,jdbcType=VARCHAR}, #{emp.level,jdbcType=INTEGER}, #{emp.createTime,jdbcType=BIGINT},
#{emp.updateTime,jdbcType=BIGINT})
</foreach>
</insert>
<insert id="insert" parameterType="io.metersphere.base.domain.TestCaseNode">
insert into test_case_node (id, project_id, name,
parent_id, level, create_time,
update_time)
values (#{id,jdbcType=VARCHAR}, #{projectId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR},
#{parentId,jdbcType=VARCHAR}, #{level,jdbcType=INTEGER}, #{createTime,jdbcType=BIGINT},
#{updateTime,jdbcType=BIGINT}) #{updateTime,jdbcType=BIGINT})
</insert> </insert>
<insert id="insertSelective" parameterType="io.metersphere.base.domain.TestCaseNode"> <insert id="insertSelective" parameterType="io.metersphere.base.domain.TestCaseNode">
insert into test_case_node insert into test_case_node
<trim prefix="(" suffix=")" suffixOverrides=","> <trim prefix="(" suffix=")" suffixOverrides=",">

View File

@ -99,10 +99,10 @@ public class TestCaseController {
return testCaseService.deleteTestCase(testCaseId); return testCaseService.deleteTestCase(testCaseId);
} }
@PostMapping("/import/{projectId}") @PostMapping("/import/{projectId}/{userId}")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public ExcelResponse testCaseImport(MultipartFile file, @PathVariable String projectId) throws NoSuchFieldException { public ExcelResponse testCaseImport(MultipartFile file, @PathVariable String projectId,@PathVariable String userId) throws NoSuchFieldException {
return testCaseService.testCaseImport(file, projectId); return testCaseService.testCaseImport(file, projectId,userId);
} }
@GetMapping("/export/template") @GetMapping("/export/template")
@ -110,6 +110,11 @@ public class TestCaseController {
public void testCaseTemplateExport(HttpServletResponse response) { public void testCaseTemplateExport(HttpServletResponse response) {
testCaseService.testCaseTemplateExport(response); testCaseService.testCaseTemplateExport(response);
} }
@GetMapping("/export/xmindTemplate")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public void xmindTemplate(HttpServletResponse response) {
testCaseService.testCaseXmindTemplateExport(response);
}
@PostMapping("/export/testcase") @PostMapping("/export/testcase")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR) @RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)

View File

@ -27,6 +27,7 @@ import io.metersphere.i18n.Translator;
import io.metersphere.track.dto.TestCaseDTO; import io.metersphere.track.dto.TestCaseDTO;
import io.metersphere.track.request.testcase.QueryTestCaseRequest; import io.metersphere.track.request.testcase.QueryTestCaseRequest;
import io.metersphere.track.request.testcase.TestCaseBatchRequest; import io.metersphere.track.request.testcase.TestCaseBatchRequest;
import io.metersphere.xmind.XmindToTestCaseParser;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSession;
@ -38,6 +39,8 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -67,9 +70,6 @@ public class TestCaseService {
@Resource @Resource
TestCaseNodeService testCaseNodeService; TestCaseNodeService testCaseNodeService;
@Resource
UserMapper userMapper;
@Resource @Resource
UserRoleMapper userRoleMapper; UserRoleMapper userRoleMapper;
@ -236,10 +236,10 @@ public class TestCaseService {
return projectMapper.selectByPrimaryKey(testCaseWithBLOBs.getProjectId()); return projectMapper.selectByPrimaryKey(testCaseWithBLOBs.getProjectId());
} }
public ExcelResponse testCaseImport(MultipartFile file, String projectId) {
public ExcelResponse testCaseImport(MultipartFile multipartFile, String projectId, String userId) {
ExcelResponse excelResponse = new ExcelResponse(); ExcelResponse excelResponse = new ExcelResponse();
String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId(); String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId();
QueryTestCaseRequest queryTestCaseRequest = new QueryTestCaseRequest(); QueryTestCaseRequest queryTestCaseRequest = new QueryTestCaseRequest();
queryTestCaseRequest.setProjectId(projectId); queryTestCaseRequest.setProjectId(projectId);
@ -247,25 +247,44 @@ public class TestCaseService {
Set<String> testCaseNames = testCases.stream() Set<String> testCaseNames = testCases.stream()
.map(TestCase::getName) .map(TestCase::getName)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
UserRoleExample userRoleExample = new UserRoleExample();
userRoleExample.createCriteria()
.andRoleIdIn(Arrays.asList(RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER))
.andSourceIdEqualTo(currentWorkspaceId);
Set<String> userIds = userRoleMapper.selectByExample(userRoleExample).stream().map(UserRole::getUserId).collect(Collectors.toSet());
EasyExcelListener easyExcelListener = null;
List<ExcelErrData<TestCaseExcelData>> errList = null; List<ExcelErrData<TestCaseExcelData>> errList = null;
try {
easyExcelListener = new TestCaseDataListener(this, projectId, testCaseNames, userIds); if (multipartFile.getOriginalFilename().endsWith(".xmind")) {
EasyExcelFactory.read(file.getInputStream(), TestCaseExcelData.class, easyExcelListener).sheet().doRead(); try {
errList = easyExcelListener.getErrList(); errList = new ArrayList<>();
} catch (Exception e) { String processLog = new XmindToTestCaseParser(this, userId, projectId, testCaseNames).importXmind(multipartFile);
LogUtil.error(e.getMessage(), e); if (!StringUtils.isEmpty(processLog)) {
MSException.throwException(e.getMessage()); excelResponse.setSuccess(false);
} finally { ExcelErrData excelErrData = new ExcelErrData(null, 1, processLog);
easyExcelListener.close(); errList.add(excelErrData);
excelResponse.setErrList(errList);
} else {
excelResponse.setSuccess(true);
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
UserRoleExample userRoleExample = new UserRoleExample();
userRoleExample.createCriteria()
.andRoleIdIn(Arrays.asList(RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER))
.andSourceIdEqualTo(currentWorkspaceId);
Set<String> userIds = userRoleMapper.selectByExample(userRoleExample).stream().map(UserRole::getUserId).collect(Collectors.toSet());
EasyExcelListener easyExcelListener = null;
try {
easyExcelListener = new TestCaseDataListener(this, projectId, testCaseNames, userIds);
EasyExcelFactory.read(multipartFile.getInputStream(), TestCaseExcelData.class, easyExcelListener).sheet().doRead();
errList = easyExcelListener.getErrList();
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
MSException.throwException(e.getMessage());
} finally {
easyExcelListener.close();
}
} }
//如果包含错误信息就导出错误信息 //如果包含错误信息就导出错误信息
if (!errList.isEmpty()) { if (!errList.isEmpty()) {
@ -309,6 +328,35 @@ public class TestCaseService {
} }
} }
public static void download(HttpServletResponse res) throws IOException {
// 发送给客户端的数据
OutputStream outputStream = res.getOutputStream();
byte[] buff = new byte[1024];
// 读取filename
String filePath = ClassLoader.getSystemResource("template/testcase.xmind").getPath();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File(filePath)));) {
int i = bis.read(buff);
while (i != -1) {
outputStream.write(buff, 0, buff.length);
outputStream.flush();
i = bis.read(buff);
}
} catch (Exception ex) {
LogUtil.error(ex.getMessage());
}
}
public void testCaseXmindTemplateExport(HttpServletResponse response) {
try {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode("思维导图用例模版", "UTF-8") + ".xmind");
download(response);
} catch (Exception ex) {
}
}
private List<TestCaseExcelData> generateExportTemplate() { private List<TestCaseExcelData> generateExportTemplate() {
List<TestCaseExcelData> list = new ArrayList<>(); List<TestCaseExcelData> list = new ArrayList<>();
StringBuilder path = new StringBuilder(""); StringBuilder path = new StringBuilder("");
@ -406,18 +454,18 @@ public class TestCaseService {
} else if (t.getMethod().equals("auto") && t.getType().equals("api")) { } else if (t.getMethod().equals("auto") && t.getType().equals("api")) {
data.setStepDesc(""); data.setStepDesc("");
data.setStepResult(""); data.setStepResult("");
if(t.getTestId().equals("other")){ if (t.getTestId().equals("other")) {
data.setRemark(t.getOtherTestName()); data.setRemark(t.getOtherTestName());
}else{ } else {
data.setRemark(t.getApiName()); data.setRemark(t.getApiName());
} }
} else if (t.getMethod().equals("auto") && t.getType().equals("performance")) { } else if (t.getMethod().equals("auto") && t.getType().equals("performance")) {
data.setStepDesc(""); data.setStepDesc("");
data.setStepResult(""); data.setStepResult("");
if(t.getTestId().equals("other")){ if (t.getTestId().equals("other")) {
data.setRemark(t.getOtherTestName()); data.setRemark(t.getOtherTestName());
}else{ } else {
data.setRemark(t.getPerformName()); data.setRemark(t.getPerformName());
} }
} }

View File

@ -0,0 +1,248 @@
package io.metersphere.xmind;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.base.domain.TestCaseWithBLOBs;
import io.metersphere.commons.constants.TestCaseConstants;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.excel.domain.TestCaseExcelData;
import io.metersphere.i18n.Translator;
import io.metersphere.track.service.TestCaseService;
import io.metersphere.xmind.parser.XmindParser;
import io.metersphere.xmind.parser.domain.Attached;
import io.metersphere.xmind.parser.domain.JsonRootBean;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.*;
/**
* 数据转换
*/
public class XmindToTestCaseParser {
private TestCaseService testCaseService;
private String maintainer;
private String projectId;
private StringBuffer process; // 过程校验记录
private Set<String> testCaseNames;
public XmindToTestCaseParser(TestCaseService testCaseService, String userId, String projectId, Set<String> testCaseNames) {
this.testCaseService = testCaseService;
this.maintainer = userId;
this.projectId = projectId;
this.testCaseNames = testCaseNames;
testCaseWithBLOBs = new LinkedList<>();
xmindDataList = new ArrayList<>();
process = new StringBuffer();
}
// 案例详情
private List<TestCaseWithBLOBs> testCaseWithBLOBs;
// 用于重复对比
protected List<TestCaseExcelData> xmindDataList;
// 递归处理案例数据
private void makeXmind(StringBuffer processBuffer, String nodeId, int level, String nodePath, List<Attached> attacheds) {
for (Attached item : attacheds) {
if (!StringUtils.isEmpty(item.getTitle()) && item.getTitle().startsWith("tc")) { // 用例
this.newTestCase(item.getTitle(), nodePath, item.getChildren() != null ? item.getChildren().getAttached() : null);
} else {
nodePath = nodePath + "/" + item.getTitle();
if (item.getChildren() != null && !item.getChildren().getAttached().isEmpty())
makeXmind(processBuffer, nodeId, level + 1, nodePath, item.getChildren().getAttached());
}
}
}
// 获取步骤数据
public String getSteps(List<Attached> attacheds) {
JSONArray jsonArray = new JSONArray();
for (int i = 0; i < attacheds.size(); i++) {
// 保持插入顺序判断用例是否有相同的steps
JSONObject step = new JSONObject(true);
step.put("num", i + 1);
step.put("desc", attacheds.get(i).getTitle());
if (attacheds.get(i).getChildren() != null && !attacheds.get(i).getChildren().getAttached().isEmpty()) {
step.put("result", attacheds.get(i).getChildren().getAttached().get(0).getTitle());
}
jsonArray.add(step);
}
return jsonArray.toJSONString();
}
private void newTestCase(String title, String nodePath, List<Attached> attacheds) {
TestCaseWithBLOBs testCase = new TestCaseWithBLOBs();
testCase.setProjectId(projectId);
testCase.setMaintainer(maintainer);
testCase.setPriority("P0");
testCase.setMethod("manual");
testCase.setType("functional");
String tc = title.replace("", ":");
String tcArr[] = tc.split(":");
if (tcArr.length != 2) {
process.append(Translator.get("test_case_name") + "" + title + "" + Translator.get("incorrect_format"));
}
// 用例名称
testCase.setName(tcArr[1].replace("tc:|tc", ""));
if (!nodePath.startsWith("/")) {
nodePath = "/" + nodePath;
}
if (nodePath.endsWith("/")) {
nodePath = nodePath.substring(0, nodePath.length() - 1);
}
testCase.setNodePath(nodePath);
// 用例等级和用例性质处理
if (tcArr[0].indexOf("-") != -1) {
String otArr[] = tcArr[0].split("-");
for (int i = 0; i < otArr.length; i++) {
if (otArr[i].startsWith("P") || otArr[i].startsWith("p")) {
testCase.setPriority(otArr[i]);
} else if (otArr[i].endsWith("功能测试")) {
testCase.setType("functional");
} else if (otArr[i].endsWith("性能测试")) {
testCase.setType("performance");
} else if (otArr[i].endsWith("接口测试")) {
testCase.setType("api");
}
}
}
// 测试步骤处理
if (attacheds != null && !attacheds.isEmpty()) {
List<Attached> steps = new LinkedList<>();
attacheds.forEach(item -> {
if (item.getTitle().startsWith("pc")) {
testCase.setPrerequisite(item.getTitle().replaceAll("(?:pc:|pc)", ""));
} else if (item.getTitle().startsWith("rc")) {
testCase.setRemark(item.getTitle().replaceAll("(?:rc:|rc)", ""));
} else {
steps.add(item);
}
});
if (!steps.isEmpty()) {
testCase.setSteps(this.getSteps(steps));
}
}
TestCaseExcelData compartData = new TestCaseExcelData();
BeanUtils.copyBean(compartData, testCase);
if (xmindDataList.contains(compartData)) {
process.append(Translator.get("test_case_already_exists_excel") + "" + testCase.getName() + "; ");
} else if (validate(testCase)) {
testCase.setId(UUID.randomUUID().toString());
testCase.setCreateTime(System.currentTimeMillis());
testCase.setUpdateTime(System.currentTimeMillis());
testCaseWithBLOBs.add(testCase);
}
xmindDataList.add(compartData);
}
//获取流文件
private static void inputStreamToFile(InputStream ins, File file) {
try (OutputStream os = new FileOutputStream(file);) {
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
} catch (Exception e) {
LogUtil.error(e.getMessage());
}
}
/**
* MultipartFile File
*
* @param file
* @throws Exception
*/
private File multipartFileToFile(MultipartFile file) throws Exception {
if (file != null && file.getSize() > 0) {
try (InputStream ins = file.getInputStream();) {
File toFile = new File(file.getOriginalFilename());
inputStreamToFile(ins, toFile);
return toFile;
}
}
return null;
}
public boolean validate(TestCaseWithBLOBs data) {
String nodePath = data.getNodePath();
StringBuilder stringBuilder = new StringBuilder();
if (nodePath != null) {
String[] nodes = nodePath.split("/");
if (nodes.length > TestCaseConstants.MAX_NODE_DEPTH + 1) {
stringBuilder.append(Translator.get("test_case_node_level_tip") +
TestCaseConstants.MAX_NODE_DEPTH + Translator.get("test_case_node_level") + "; ");
}
for (int i = 0; i < nodes.length; i++) {
if (i != 0 && org.apache.commons.lang3.StringUtils.equals(nodes[i].trim(), "")) {
stringBuilder.append(Translator.get("module_not_null") + "; ");
break;
}
}
}
if (org.apache.commons.lang3.StringUtils.equals(data.getType(), TestCaseConstants.Type.Functional.getValue()) && org.apache.commons.lang3.StringUtils.equals(data.getMethod(), TestCaseConstants.Method.Auto.getValue())) {
stringBuilder.append(Translator.get("functional_method_tip") + "; ");
}
if (testCaseNames.contains(data.getName())) {
boolean dbExist = testCaseService.exist(data);
boolean excelExist = false;
if (dbExist) {
// db exist
stringBuilder.append(Translator.get("test_case_already_exists_excel") + "" + data.getName() + "; ");
}
} else {
testCaseNames.add(data.getName());
}
if (!StringUtils.isEmpty(stringBuilder.toString())) {
process.append(stringBuilder.toString());
return false;
}
return true;
}
public String importXmind(MultipartFile multipartFile) {
StringBuffer processBuffer = new StringBuffer();
try {
File file = multipartFileToFile(multipartFile);
JsonRootBean root = XmindParser.parseObject(file);
if (root != null && root.getRootTopic() != null && root.getRootTopic().getChildren() != null) {
// 判断是模块还是用例
root.getRootTopic().getChildren().getAttached().forEach(item -> {
if (!StringUtils.isEmpty(item.getTitle()) && item.getTitle().startsWith("tc")) { // 用例
this.newTestCase(item.getTitle(), "", item.getChildren() != null ? item.getChildren().getAttached() : null);
} else {
item.setPath(item.getTitle());
if (item.getChildren() != null && !item.getChildren().getAttached().isEmpty())
makeXmind(processBuffer, null, 1, item.getPath(), item.getChildren().getAttached());
}
});
}
if (StringUtils.isEmpty(process.toString()) && !testCaseWithBLOBs.isEmpty()) {
testCaseService.saveImportData(testCaseWithBLOBs, projectId);
}
} catch (Exception ex) {
processBuffer.append(Translator.get("incorrect_format"));
LogUtil.error(ex.getMessage());
ex.printStackTrace();
} finally {
testCaseWithBLOBs.clear();
}
return process.toString();
}
}

View File

@ -0,0 +1,79 @@
package io.metersphere.xmind.parser;
import org.dom4j.*;
import org.json.JSONObject;
import org.json.XML;
import java.io.IOException;
import java.util.List;
public class XmindLegacy {
/**
* 返回content.xml和comments.xml合并后的json
*
* @param xmlContent
* @param xmlComments
* @return
* @throws IOException
* @throws DocumentException
*/
public static String getContent(String xmlContent, String xmlComments) throws IOException, DocumentException {
// 删除content.xml里面不能识别的字符串
xmlContent = xmlContent.replace("xmlns=\"urn:xmind:xmap:xmlns:content:2.0\"", "");
xmlContent = xmlContent.replace("xmlns:fo=\"http://www.w3.org/1999/XSL/Format\"", "");
// 删除<topic>节点
xmlContent = xmlContent.replace("<topics type=\"attached\">", "");
xmlContent = xmlContent.replace("</topics>", "");
// 去除title中svg:width属性
xmlContent = xmlContent.replaceAll("<title svg:width=\"[0-9]*\">", "<title>");
Document document = DocumentHelper.parseText(xmlContent);// 读取XML文件,获得document对象
Element root = document.getRootElement();
List<Node> topics = root.selectNodes("//topic");
if (xmlComments != null) {
// 删除comments.xml里面不能识别的字符串
xmlComments = xmlComments.replace("xmlns=\"urn:xmind:xmap:xmlns:comments:2.0\"", "");
// 添加评论到content中
Document commentDocument = DocumentHelper.parseText(xmlComments);
List<Node> commentsList = commentDocument.selectNodes("//comment");
for (Node topic : topics) {
for (Node commentNode : commentsList) {
Element commentElement = (Element) commentNode;
Element topicElement = (Element) topic;
if (topicElement.attribute("id").getValue()
.equals(commentElement.attribute("object-id").getValue())) {
Element comment = topicElement.addElement("comments");
comment.addAttribute("creationTime", commentElement.attribute("time").getValue());
comment.addAttribute("author", commentElement.attribute("author").getValue());
comment.addAttribute("content", commentElement.element("content").getText());
}
}
}
}
// 第一个topic转换为json中的rootTopic
Node rootTopic = root.selectSingleNode("/xmap-content/sheet/topic");
rootTopic.setName("rootTopic");
// 将xml中topic节点转换为attached节点
List<Node> topicList = rootTopic.selectNodes("//topic");
for (Node node : topicList) {
node.setName("attached");
}
// 选取第一个sheet
Element sheet = root.elements("sheet").get(0);
String res = sheet.asXML();
// 将xml转为json
JSONObject xmlJSONObj = XML.toJSONObject(res);
JSONObject jsonObject = xmlJSONObj.getJSONObject("sheet");
// 设置缩进
return jsonObject.toString(4);
}
}

View File

@ -0,0 +1,116 @@
package io.metersphere.xmind.parser;
import com.alibaba.fastjson.JSON;
import io.metersphere.xmind.parser.domain.JsonRootBean;
import org.apache.commons.compress.archivers.ArchiveException;
import org.dom4j.DocumentException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @Description 解析主体
*/
public class XmindParser {
public static final String xmindZenJson = "content.json";
public static final String xmindLegacyContent = "content.xml";
public static final String xmindLegacyComments = "comments.xml";
/**
* 解析脑图文件返回content整合后的内容
*
* @param file
* @return
* @throws IOException
* @throws ArchiveException
* @throws DocumentException
*/
public static String parseJson(File file) throws IOException, ArchiveException, DocumentException {
String res = ZipUtils.extract(file);
String content = null;
if (isXmindZen(res, file)) {
content = getXmindZenContent(file, res);
} else {
content = getXmindLegacyContent(file, res);
}
// 删除生成的文件夹
File dir = new File(res);
boolean flag = deleteDir(dir);
if (flag) {
// do something
}
JsonRootBean jsonRootBean = JSON.parseObject(content, JsonRootBean.class);
return (JSON.toJSONString(jsonRootBean, false));
}
public static JsonRootBean parseObject(File file) throws DocumentException, ArchiveException, IOException {
String content = parseJson(file);
JsonRootBean jsonRootBean = JSON.parseObject(content, JsonRootBean.class);
return jsonRootBean;
}
public static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
// 递归删除目录中的子目录下
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空可以删除
return dir.delete();
}
/**
* @return
*/
public static String getXmindZenContent(File file, String extractFileDir)
throws IOException, ArchiveException {
List<String> keys = new ArrayList<>();
keys.add(xmindZenJson);
Map<String, String> map = ZipUtils.getContents(keys, file, extractFileDir);
String content = map.get(xmindZenJson);
content = XmindZen.getContent(content);
return content;
}
/**
* @return
*/
public static String getXmindLegacyContent(File file, String extractFileDir)
throws IOException, ArchiveException, DocumentException {
List<String> keys = new ArrayList<>();
keys.add(xmindLegacyContent);
keys.add(xmindLegacyComments);
Map<String, String> map = ZipUtils.getContents(keys, file, extractFileDir);
String contentXml = map.get(xmindLegacyContent);
String commentsXml = map.get(xmindLegacyComments);
String xmlContent = XmindLegacy.getContent(contentXml, commentsXml);
return xmlContent;
}
private static boolean isXmindZen(String res, File file) throws IOException, ArchiveException {
// 解压
File parent = new File(res);
if (parent.isDirectory()) {
String[] files = parent.list(new ZipUtils.FileFilter());
for (int i = 0; i < Objects.requireNonNull(files).length; i++) {
if (files[i].equals(xmindZenJson)) {
return true;
}
}
}
return false;
}
}

View File

@ -0,0 +1,65 @@
package io.metersphere.xmind.parser;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.dom4j.DocumentException;
import java.io.IOException;
public class XmindZen {
/**
* @param jsonContent
* @return
* @throws IOException
* @throws DocumentException
*/
public static String getContent(String jsonContent) {
JSONObject jsonObject = JSONArray.parseArray(jsonContent).getJSONObject(0);
JSONObject rootTopic = jsonObject.getJSONObject("rootTopic");
transferNotes(rootTopic);
JSONObject children = rootTopic.getJSONObject("children");
recursionChildren(children);
return jsonObject.toString();
}
/**
* 递归转换children
*
* @param children
*/
private static void recursionChildren(JSONObject children) {
if (children == null) {
return;
}
JSONArray attachedArray = children.getJSONArray("attached");
if (attachedArray == null) {
return;
}
for (Object attached : attachedArray) {
JSONObject attachedObject = (JSONObject) attached;
transferNotes(attachedObject);
JSONObject childrenObject = attachedObject.getJSONObject("children");
if (childrenObject == null) {
continue;
}
recursionChildren(childrenObject);
}
}
private static void transferNotes(JSONObject object) {
JSONObject notes = object.getJSONObject("notes");
if (notes == null) {
return;
}
JSONObject plain = notes.getJSONObject("plain");
if (plain != null) {
String content = plain.getString("content");
notes.remove("plain");
notes.put("content", content);
} else {
notes.put("content", null);
}
}
}

View File

@ -0,0 +1,87 @@
package io.metersphere.xmind.parser;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.examples.Expander;
import java.io.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @Description zip解压工具
*/
public class ZipUtils {
private static final String currentPath = System.getProperty("user.dir");
/**
* 找到压缩文件中匹配的子文件返回的为 getContents("comments.xml, unzip
*
* @param subFileNames
* @param file
*/
public static Map<String, String> getContents(List<String> subFileNames, File file, String extractFileDir)
throws IOException, ArchiveException {
String destFilePath = extractFileDir;
Map<String, String> map = new HashMap<>();
File destFile = new File(destFilePath);
if (destFile.isDirectory()) {
String[] res = destFile.list(new FileFilter());
for (int i = 0; i < Objects.requireNonNull(res).length; i++) {
if (subFileNames.contains(res[i])) {
String s = destFilePath + File.separator + res[i];
String content = getFileContent(s);
map.put(res[i], content);
}
}
}
return map;
}
/**
* 返回解压后的文件夹名字
*
* @return
* @throws IOException
* @throws ArchiveException
*/
public static String extract(File file) throws IOException, ArchiveException {
Expander expander = new Expander();
String destFileName = currentPath + File.separator + "XMind" + System.currentTimeMillis(); // 目标文件夹名字
expander.expand(file, new File(destFileName));
return destFileName;
}
// 这是一个内部类过滤器,策略模式
static class FileFilter implements FilenameFilter {
@Override
public boolean accept(File dir, String name) {
// String的 endsWith(String str)方法 筛选出以str结尾的字符串
if (name.endsWith(".xml") || name.endsWith(".json")) {
return true;
}
return false;
}
}
public static String getFileContent(String fileName) throws IOException {
File file;
try {
file = new File(fileName);
} catch (Exception e) {
throw new RuntimeException("找不到该文件");
}
FileReader fileReader = new FileReader(file);
BufferedReader bufferedReder = new BufferedReader(fileReader);
StringBuilder stringBuffer = new StringBuilder();
while (bufferedReder.ready()) {
stringBuffer.append(bufferedReder.readLine());
}
// 打开的文件需关闭在unix下可以删除否则在windows下不能删除file.delete())
bufferedReder.close();
fileReader.close();
return stringBuffer.toString();
}
}

View File

@ -0,0 +1,18 @@
package io.metersphere.xmind.parser.domain;
import lombok.Data;
import java.util.List;
@Data
public class Attached {
private String id;
private String title;
private Notes notes;
private String path;
private List<Comments> comments;
private Children children;
}

View File

@ -0,0 +1,12 @@
package io.metersphere.xmind.parser.domain;
import lombok.Data;
import java.util.List;
@Data
public class Children {
private List<Attached> attached;
}

View File

@ -0,0 +1,12 @@
package io.metersphere.xmind.parser.domain;
import lombok.Data;
@Data
public class Comments {
private long creationTime;
private String author;
private String content;
}

View File

@ -0,0 +1,12 @@
package io.metersphere.xmind.parser.domain;
import lombok.Data;
@Data
public class JsonRootBean {
private String id;
private String title;
private RootTopic rootTopic;
}

View File

@ -0,0 +1,10 @@
package io.metersphere.xmind.parser.domain;
import lombok.Data;
@Data
public class Notes {
private String content;
}

View File

@ -0,0 +1,16 @@
package io.metersphere.xmind.parser.domain;
import lombok.Data;
import java.util.List;
@Data
public class RootTopic {
private String id;
private String title;
private Notes notes;
private List<Comments> comments;
private Children children;
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -1,123 +1,235 @@
<template> <template>
<el-dialog class="testcase-import" :title="$t('test_track.case.import.case_import')" :visible.sync="dialogVisible" <el-dialog class="testcase-import" :title="$t('test_track.case.import.case_import')" :visible.sync="dialogVisible"
@close="close"> @close="close">
<el-row> <el-tabs v-model="activeName" simple>
<el-link type="primary" class="download-template" <el-tab-pane :label="$t('test_track.case.import.excel_title')" name="excelImport">
@click="downloadTemplate"
>{{$t('test_track.case.import.download_template')}}</el-link></el-row>
<el-row>
<el-upload
v-loading="result.loading"
:element-loading-text="$t('test_track.case.import.importing')"
element-loading-spinner="el-icon-loading"
class="upload-demo"
multiple
:limit="1"
action=""
:on-exceed="handleExceed"
:beforeUpload="uploadValidate"
:on-error="handleError"
:show-file-list="false"
:http-request="upload"
:file-list="fileList">
<template v-slot:trigger>
<el-button size="mini" type="success" plain>{{$t('test_track.case.import.click_upload')}}</el-button>
</template>
<template v-slot:tip>
<div class="el-upload__tip">{{$t('test_track.case.import.upload_limit')}}</div>
</template>
</el-upload>
</el-row>
<el-row> <el-row>
<ul> <el-link type="primary" class="download-template"
<li v-for="errFile in errList" :key="errFile.rowNum"> @click="downloadTemplate"
{{errFile.errMsg}} >{{$t('test_track.case.import.download_template')}}
</li> </el-link>
</ul> </el-row>
</el-row> <el-row>
<el-upload
v-loading="result.loading"
:element-loading-text="$t('test_track.case.import.importing')"
element-loading-spinner="el-icon-loading"
class="upload-demo"
multiple
:limit="1"
action=""
:on-exceed="handleExceed"
:beforeUpload="uploadValidate"
:on-error="handleError"
:show-file-list="false"
:http-request="upload"
:file-list="fileList">
<template v-slot:trigger>
<el-button size="mini" type="success" plain>{{$t('test_track.case.import.click_upload')}}</el-button>
</template>
<template v-slot:tip>
<div class="el-upload__tip">{{$t('test_track.case.import.upload_limit')}}</div>
</template>
</el-upload>
</el-row>
</el-dialog>
<el-row>
<ul>
<li v-for="errFile in errList" :key="errFile.rowNum">
{{errFile.errMsg}}
</li>
</ul>
</el-row>
</el-tab-pane>
<!-- Xmind 导入 -->
<el-tab-pane :label="$t('test_track.case.import.xmind_title')" name="xmindImport" style="border: 0px">
<el-row class="import-row">
<div class="el-step__icon is-text" style="background-color: #C9E6F8;border-color: #C9E6F8;margin-right: 10px">
<div class="el-step__icon-inner">1</div>
</div>
<label class="ms-license-label">{{$t('test_track.case.import.import_desc')}}</label>
</el-row>
<el-row class="import-row">
<el-card :body-style="{ padding: '0px' }">
<img src="../../../../../assets/xmind.jpg" width="700px" height="250px"
class="image">
</el-card>
</el-row>
<el-row class="import-row">
<div class="el-step__icon is-text"
style="background-color: #C9E6F8;border-color: #C9E6F8;margin-right: 10px ">
<div class="el-step__icon-inner">2</div>
</div>
<label class="ms-license-label">{{$t('test_track.case.import.import_file')}}</label>
</el-row>
<el-row class="import-row">
<el-link type="primary" class="download-template"
@click="downloadXmindTemplate"
>{{$t('test_track.case.import.download_template')}}
</el-link>
</el-row>
<el-row class="import-row">
<el-upload
v-loading="result.loading"
:element-loading-text="$t('test_track.case.import.importing')"
element-loading-spinner="el-icon-loading"
class="upload-demo"
multiple
:limit="1"
action=""
:on-exceed="handleExceed"
:beforeUpload="uploadValidateXmind"
:on-error="handleError"
:show-file-list="false"
:http-request="uploadXmind"
:file-list="fileList">
<template v-slot:trigger>
<el-button size="mini" type="success" plain>{{$t('test_track.case.import.click_upload')}}</el-button>
</template>
<template v-slot:tip>
<div class="el-upload__tip">{{$t('test_track.case.import.upload_xmind')}}</div>
</template>
</el-upload>
</el-row>
<el-row>
<ul>
<li v-for="errFile in xmindErrList" :key="errFile.rowNum">
{{errFile.errMsg}}
</li>
</ul>
</el-row>
</el-tab-pane>
</el-tabs>
</el-dialog>
</template> </template>
<script> <script>
import ElUploadList from "element-ui/packages/upload/src/upload-list"; import ElUploadList from "element-ui/packages/upload/src/upload-list";
import MsTableButton from '../../../../components/common/components/MsTableButton'; import MsTableButton from '../../../../components/common/components/MsTableButton';
import {listenGoBack, removeGoBackListener} from "../../../../../common/js/utils"; import {listenGoBack, removeGoBackListener} from "../../../../../common/js/utils";
import {TokenKey, WORKSPACE_ID} from '../../../../../common/js/constants';
export default { export default {
name: "TestCaseImport", name: "TestCaseImport",
components: {ElUploadList, MsTableButton}, components: {ElUploadList, MsTableButton},
data() { data() {
return { return {
result: {}, result: {},
dialogVisible: false, activeName: 'excelImport',
fileList: [], dialogVisible: false,
errList: [], fileList: [],
isLoading: false errList: [],
} xmindErrList: [],
isLoading: false
}
},
props: {
projectId: {
type: String
}
},
methods: {
handleExceed(files, fileList) {
this.$warning(this.$t('test_track.case.import.upload_limit_count'));
}, },
props: { uploadValidate(file) {
projectId: { let suffix = file.name.substring(file.name.lastIndexOf('.') + 1);
type: String if (suffix != 'xls' && suffix != 'xlsx') {
this.$warning(this.$t('test_track.case.import.upload_limit_format'));
return false;
} }
},
methods: {
handleExceed(files, fileList) {
this.$warning(this.$t('test_track.case.import.upload_limit_count'));
},
uploadValidate(file) {
let suffix = file.name.substring(file.name.lastIndexOf('.') + 1);
if (suffix != 'xls' && suffix != 'xlsx' && suffix != 'xmind') {
this.$warning(this.$t('test_track.case.import.upload_limit_format'));
return false;
}
if (file.size / 1024 / 1024 > 20) { if (file.size / 1024 / 1024 > 20) {
this.$warning(this.$t('test_track.case.import.upload_limit_size')); this.$warning(this.$t('test_track.case.import.upload_limit_size'));
return false; return false;
}
this.isLoading = true;
this.errList = [];
this.xmindErrList = [];
return true;
},
uploadValidateXmind(file) {
let suffix = file.name.substring(file.name.lastIndexOf('.') + 1);
if (suffix != 'xmind') {
this.$warning(this.$t('test_track.case.import.upload_xmind_format'));
return false;
}
if (file.size / 1024 / 1024 > 20) {
this.$warning(this.$t('test_track.case.import.upload_limit_size'));
return false;
}
this.isLoading = true;
this.errList = [];
this.xmindErrList = [];
return true;
},
handleError(err, file, fileList) {
this.isLoading = false;
this.$error(err.message);
},
open() {
listenGoBack(this.close);
this.dialogVisible = true;
},
close() {
removeGoBackListener(this.close);
this.dialogVisible = false;
this.fileList = [];
this.errList = [];
this.xmindErrList = [];
},
downloadTemplate() {
this.$fileDownload('/test/case/export/template');
},
downloadXmindTemplate() {
this.$fileDownload('/test/case/export/xmindTemplate');
},
upload(file) {
this.isLoading = false;
this.fileList.push(file.file);
let user = JSON.parse(localStorage.getItem(TokenKey));
this.result = this.$fileUpload('/test/case/import/' + this.projectId + '/' + user.id, file.file, null, {}, response => {
let res = response.data;
if (res.success) {
this.$success(this.$t('test_track.case.import.success'));
this.dialogVisible = false;
this.$emit("refresh");
} else {
this.errList = res.errList;
} }
this.isLoading = true;
this.errList = [];
return true;
},
handleError(err, file, fileList) {
this.isLoading = false;
this.$error(err.message);
},
open() {
listenGoBack(this.close);
this.dialogVisible = true;
},
close() {
removeGoBackListener(this.close);
this.dialogVisible = false;
this.fileList = []; this.fileList = [];
this.errList = []; }, erro => {
}, this.fileList = [];
downloadTemplate() { });
this.$fileDownload('/test/case/export/template'); },
}, uploadXmind(file) {
upload(file) { this.isLoading = false;
this.isLoading = false; this.fileList.push(file.file);
this.fileList.push(file.file); let user = JSON.parse(localStorage.getItem(TokenKey));
this.result = this.$fileUpload('/test/case/import/' + this.projectId, file.file, null, {}, response => {
let res = response.data; this.result = this.$fileUpload('/test/case/import/' + this.projectId + '/' + user.id, file.file, null, {}, response => {
if (res.success) { let res = response.data;
this.$success(this.$t('test_track.case.import.success')); if (res.success) {
this.dialogVisible = false; this.$success(this.$t('test_track.case.import.success'));
this.$emit("refresh"); this.dialogVisible = false;
} else { this.$emit("refresh");
this.errList = res.errList; } else {
} this.xmindErrList = res.errList;
this.fileList = []; }
}, erro => { this.fileList = [];
this.fileList = []; }, erro => {
}); this.fileList = [];
} });
} }
} }
}
</script> </script>
<style> <style>
@ -130,8 +242,12 @@
padding-bottom: 10px; padding-bottom: 10px;
} }
.import-row {
padding-top: 20px;
}
.testcase-import >>> .el-dialog { .testcase-import >>> .el-dialog {
width: 400px; width: 700px;
} }

View File

@ -653,13 +653,19 @@ export default {
case_import: "Import test case", case_import: "Import test case",
download_template: "Download template", download_template: "Download template",
click_upload: "Upload", click_upload: "Upload",
upload_limit: "Only XLS/XLSX files can be uploaded, and no more than 20M", upload_limit: "Only XLS/XLSX/XMIND files can be uploaded, and no more than 20M",
upload_xmind_format: "Upload files can only be .xmind format",
upload_xmind: "Only xmind files can be uploaded, and no more than 500",
upload_limit_count: "Only one file can be uploaded at a time", upload_limit_count: "Only one file can be uploaded at a time",
upload_limit_format: "Upload files can only be XLS, XLSX format!", upload_limit_format: "Upload files can only be XLS, XLSX format!",
upload_limit_size: "Upload file size cannot exceed 20MB!", upload_limit_size: "Upload file size cannot exceed 20MB!",
upload_limit_other_size: "Upload file size cannot exceed", upload_limit_other_size: "Upload file size cannot exceed",
success: "Import success", success: "Import success",
importing: "Importing...", importing: "Importing...",
excel_title: "Excel ",
xmind_title: "Xmind",
import_desc: "Import instructions",
import_file: "upload files",
}, },
export: { export: {
export: "Export cases" export: "Export cases"

View File

@ -657,12 +657,18 @@ export default {
download_template: "下载模版", download_template: "下载模版",
click_upload: "点击上传", click_upload: "点击上传",
upload_limit: "只能上传xls/xlsx文件且不超过20M", upload_limit: "只能上传xls/xlsx文件且不超过20M",
upload_xmind: "支持文件类型:.xmind一次至多导入500 条用例",
upload_xmind_format: "上传文件只能是 .xmind 格式",
upload_limit_other_size: "上传文件大小不能超过", upload_limit_other_size: "上传文件大小不能超过",
upload_limit_count: "一次只能上传一个文件", upload_limit_count: "一次只能上传一个文件",
upload_limit_format: "上传文件只能是 xls、xlsx格式!", upload_limit_format: "上传文件只能是 xls、xlsx格式!",
upload_limit_size: "上传文件大小不能超过 20MB!", upload_limit_size: "上传文件大小不能超过 20MB!",
success: "导入成功!", success: "导入成功!",
importing: "导入中...", importing: "导入中...",
excel_title: "表格文件",
xmind_title: "思维导图",
import_desc: "导入说明",
import_file: "上传文件",
}, },
export: { export: {
export: "导出用例" export: "导出用例"

View File

@ -651,13 +651,20 @@ export default {
case_import: "導入測試用例", case_import: "導入測試用例",
download_template: "下載模版", download_template: "下載模版",
click_upload: "點擊上傳", click_upload: "點擊上傳",
upload_limit: "只能上傳xls/xlsx文件,且不超過20M", upload_limit: "只能上傳xls/xlsx/xmind文件,且不超過20M",
upload_xmind: "支持文件類型:.xmind壹次至多導入500 條用例",
upload_xmind_format: "上傳文件只能是 .xmind 格式",
upload_limit_count: "一次只能上傳一個文件", upload_limit_count: "一次只能上傳一個文件",
upload_limit_format: "上傳文件只能是 xls、xlsx格式!", upload_limit_format: "上傳文件只能是 xls、xlsx格式!",
upload_limit_size: "上傳文件大小不能超過 20MB!", upload_limit_size: "上傳文件大小不能超過 20MB!",
upload_limit_other_size: "上傳文件大小不能超過", upload_limit_other_size: "上傳文件大小不能超過",
success: "導入成功!", success: "導入成功!",
importing: "導入中...", importing: "導入中...",
excel_title: "表格文件",
xmind_title: "思維導圖",
import_desc: "導入說明",
import_file: "上傳文件",
}, },
export: { export: {
export: "導出用例" export: "導出用例"