diff --git a/case-server/pom.xml b/case-server/pom.xml
index 6711ec4..2ba59b4 100644
--- a/case-server/pom.xml
+++ b/case-server/pom.xml
@@ -151,6 +151,13 @@
httpclient
4.5.5
+
+
+
+ com.flipkart.zjsonpatch
+ zjsonpatch
+ 0.4.11
+
diff --git a/case-server/sql/case-server.sql b/case-server/sql/case-server.sql
index 3546c0a..0701a3d 100644
--- a/case-server/sql/case-server.sql
+++ b/case-server/sql/case-server.sql
@@ -115,3 +115,8 @@ INSERT INTO `authority` (id,authority_name,authority_desc,authority_content) VAL
INSERT INTO `authority` (id,authority_name,authority_desc,authority_content) VALUES (2, 'ROLE_ADMIN', '管理员', '/api/dir/list,/api/backup/**,/api/record/**,/api/file/**,/api/user/**,/api/case/**');
INSERT INTO `authority` (id,authority_name,authority_desc,authority_content) VALUES (3, 'ROLE_SA', '超级管理员','/api/**');
+# 添加2个字段,记录每次保存的增量修改内容
+
+ALTER TABLE `case_manager`.`case_backup`
+ADD COLUMN `json_patch` longtext NULL COMMENT '本次修改内容的 json-patch 。如果是冲突,保存的是存在冲突无法应用的 patch ',
+ADD COLUMN `is_conflict` tinyint(1) NULL COMMENT '是否为冲突保存的副本';
\ No newline at end of file
diff --git a/case-server/src/main/java/com/xiaoju/framework/constants/enums/ApplyPatchFlagEnum.java b/case-server/src/main/java/com/xiaoju/framework/constants/enums/ApplyPatchFlagEnum.java
new file mode 100644
index 0000000..0b97a22
--- /dev/null
+++ b/case-server/src/main/java/com/xiaoju/framework/constants/enums/ApplyPatchFlagEnum.java
@@ -0,0 +1,16 @@
+package com.xiaoju.framework.constants.enums;
+
+public enum ApplyPatchFlagEnum {
+
+ /**
+ * 忽略对 order replace 操作时出现的冲突
+ * 在测试任务编辑时,因为展示的脑图是完整脑图的子集,各个字段的 order 值和完整版会不一样
+ * 所以一旦改动节点顺序,replace 原始的 order 值基本都会是错的,引起冲突
+ */
+ IGNORE_REPLACE_ORDER_CONFLICT,
+
+ /**
+ * 忽略对节点展开属性变更导致的冲突。这个冲突不影响实际用例数据,只影响展示效果
+ */
+ IGNORE_EXPAND_STATE_CONFLICT
+}
diff --git a/case-server/src/main/java/com/xiaoju/framework/constants/enums/StatusCode.java b/case-server/src/main/java/com/xiaoju/framework/constants/enums/StatusCode.java
index 1ae8a1f..b7ef260 100644
--- a/case-server/src/main/java/com/xiaoju/framework/constants/enums/StatusCode.java
+++ b/case-server/src/main/java/com/xiaoju/framework/constants/enums/StatusCode.java
@@ -23,6 +23,7 @@ public enum StatusCode implements Status {
WS_UNKNOWN_ERROR(100010, "websocket访问异常"),
AUTHORITY_ERROR(100011, "权限认证错误"),
ASPECT_ERROR(100012, "权限内部处理错误"),
+ SAVE_CONFLICT(20002, "本次修改内容和数据库最新版本存在冲突"),
// 内部异常
INTERNAL_ERROR(10400, "内部参数校验或逻辑出错"),
diff --git a/case-server/src/main/java/com/xiaoju/framework/controller/BackupController.java b/case-server/src/main/java/com/xiaoju/framework/controller/BackupController.java
index a49be8d..72dda67 100644
--- a/case-server/src/main/java/com/xiaoju/framework/controller/BackupController.java
+++ b/case-server/src/main/java/com/xiaoju/framework/controller/BackupController.java
@@ -43,6 +43,20 @@ public Response> getBackupByCaseId(@RequestParam @NotNull(messa
return Response.success(caseBackupService.getBackupByCaseId(caseId, beginTime, endTime));
}
+ /**
+ * 获取单个备份记录
+ * @param backupId 备份记录对应的 id
+ * @return
+ */
+ @GetMapping(value = "/getBackupById")
+ public Response getBackupById(@RequestParam @NotNull(message = "备份记录id为空") Long backupId) {
+ CaseBackup caseBackup = caseBackupService.getBackupById(backupId);
+ if (caseBackup == null) {
+ return Response.build(StatusCode.NOT_FOUND_ENTITY, String.format("未找到备份记录id为 %d 的备份记录", backupId));
+ }
+ return Response.success(caseBackup);
+ }
+
/**
* 删除某个用例所有的备份记录
*
diff --git a/case-server/src/main/java/com/xiaoju/framework/controller/CaseController.java b/case-server/src/main/java/com/xiaoju/framework/controller/CaseController.java
index 234f987..8fbd125 100644
--- a/case-server/src/main/java/com/xiaoju/framework/controller/CaseController.java
+++ b/case-server/src/main/java/com/xiaoju/framework/controller/CaseController.java
@@ -191,7 +191,8 @@ public Response> updateWsCase(@RequestBody WsSaveReq req) {
caseService.wsSave(req);
return Response.success();
} catch (CaseServerException e) {
- throw new CaseServerException(e.getLocalizedMessage(), e.getStatus());
+ LOGGER.error("[Case Update]Update test case failed. params={}.", req.toString(), e);
+ return Response.build(e.getStatus().getStatus(), e.getMessage());
} catch (Exception e) {
e.printStackTrace();
LOGGER.error("[Case Update]Update test case failed. params={} e={} ", req.toString(), e.getMessage());
diff --git a/case-server/src/main/java/com/xiaoju/framework/entity/dto/ApplyPatchResultDto.java b/case-server/src/main/java/com/xiaoju/framework/entity/dto/ApplyPatchResultDto.java
new file mode 100644
index 0000000..fee507c
--- /dev/null
+++ b/case-server/src/main/java/com/xiaoju/framework/entity/dto/ApplyPatchResultDto.java
@@ -0,0 +1,24 @@
+package com.xiaoju.framework.entity.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class ApplyPatchResultDto {
+
+ /**
+ * 应用了不冲突patch后的json
+ */
+ String jsonAfterPatch;
+
+ /**
+ * 存在冲突无法应用的 patch
+ */
+ List conflictPatch;
+
+ /**
+ * 无冲突已应用的 patch
+ */
+ List applyPatch;
+}
diff --git a/case-server/src/main/java/com/xiaoju/framework/entity/persistent/CaseBackup.java b/case-server/src/main/java/com/xiaoju/framework/entity/persistent/CaseBackup.java
index 00249f3..fb86e7a 100644
--- a/case-server/src/main/java/com/xiaoju/framework/entity/persistent/CaseBackup.java
+++ b/case-server/src/main/java/com/xiaoju/framework/entity/persistent/CaseBackup.java
@@ -21,4 +21,13 @@ public class CaseBackup {
private String recordContent;
private String extra;
private Integer isDelete;
+ /**
+ * 本次变更应用的 patch 内容。如果是冲突副本,则此处记录的是冲突无法应用的 patch 内容
+ */
+ private String jsonPatch;
+
+ /**
+ * 是否为保存冲突时记录的副本
+ */
+ private Boolean isConflict;
}
diff --git a/case-server/src/main/java/com/xiaoju/framework/entity/request/ws/WsSaveReq.java b/case-server/src/main/java/com/xiaoju/framework/entity/request/ws/WsSaveReq.java
index b236610..4709cbc 100644
--- a/case-server/src/main/java/com/xiaoju/framework/entity/request/ws/WsSaveReq.java
+++ b/case-server/src/main/java/com/xiaoju/framework/entity/request/ws/WsSaveReq.java
@@ -19,6 +19,11 @@ public class WsSaveReq implements ParamValidate {
*/
private String caseContent;
+ /**
+ * 改动前的内容。可能是 record 的,也可能是 testcase 的。若提供,会进行增量保存。若不提供,进行全量保存
+ */
+ private String baseCaseContent;
+
private Long id;
private String modifier;
@@ -29,6 +34,11 @@ public class WsSaveReq implements ParamValidate {
*/
private Long recordId;
+ /**
+ * 保存理由。若不为空,会作为历史记录的 title
+ */
+ private String saveReason;
+
@Override
public void validate() {
if (StringUtils.isEmpty(caseContent)) {
diff --git a/case-server/src/main/java/com/xiaoju/framework/mapper/CaseBackupMapper.java b/case-server/src/main/java/com/xiaoju/framework/mapper/CaseBackupMapper.java
index d209933..e5e1b34 100644
--- a/case-server/src/main/java/com/xiaoju/framework/mapper/CaseBackupMapper.java
+++ b/case-server/src/main/java/com/xiaoju/framework/mapper/CaseBackupMapper.java
@@ -19,7 +19,7 @@
public interface CaseBackupMapper {
/**
- * 获取一份用例下所有的用例备份记录
+ * 获取一份用例下所有的用例备份记录。
*
* @param caseId 用例id
* @param beginTime 开始时间
@@ -27,8 +27,8 @@ public interface CaseBackupMapper {
* @return 所有备份记录
*/
List selectByCaseId(@Param("caseId") Long caseId,
- @Param("beginTime") Date beginTime,
- @Param("endTime") Date endTime);
+ @Param("beginTime") Date beginTime,
+ @Param("endTime") Date endTime);
/**
* 删除一批备份记录
@@ -46,4 +46,11 @@ List selectByCaseId(@Param("caseId") Long caseId,
* @return int
*/
int insert(CaseBackup caseBackup);
+
+ /**
+ * 根据备份记录id获取单条备份记录
+ * @param id 备份记录id
+ * @return
+ */
+ CaseBackup selectOne(Long id);
}
diff --git a/case-server/src/main/java/com/xiaoju/framework/service/CaseBackupService.java b/case-server/src/main/java/com/xiaoju/framework/service/CaseBackupService.java
index 2474998..e5df121 100644
--- a/case-server/src/main/java/com/xiaoju/framework/service/CaseBackupService.java
+++ b/case-server/src/main/java/com/xiaoju/framework/service/CaseBackupService.java
@@ -38,4 +38,11 @@ public interface CaseBackupService {
* @return int
*/
int deleteBackup(Long caseId);
+
+ /**
+ * 根据 id 获取备份记录
+ * @param backupId 备份记录id
+ * @return
+ */
+ CaseBackup getBackupById(Long backupId);
}
diff --git a/case-server/src/main/java/com/xiaoju/framework/service/CaseService.java b/case-server/src/main/java/com/xiaoju/framework/service/CaseService.java
index 2457cd2..97d2033 100644
--- a/case-server/src/main/java/com/xiaoju/framework/service/CaseService.java
+++ b/case-server/src/main/java/com/xiaoju/framework/service/CaseService.java
@@ -93,5 +93,5 @@ public interface CaseService {
*
* @param req 请求体
*/
- void wsSave(WsSaveReq req);
+ String wsSave(WsSaveReq req);
}
diff --git a/case-server/src/main/java/com/xiaoju/framework/service/impl/CaseBackupServiceImpl.java b/case-server/src/main/java/com/xiaoju/framework/service/impl/CaseBackupServiceImpl.java
index f9e7d50..f77c5b6 100644
--- a/case-server/src/main/java/com/xiaoju/framework/service/impl/CaseBackupServiceImpl.java
+++ b/case-server/src/main/java/com/xiaoju/framework/service/impl/CaseBackupServiceImpl.java
@@ -1,15 +1,21 @@
package com.xiaoju.framework.service.impl;
+import com.xiaoju.framework.constants.enums.StatusCode;
+import com.xiaoju.framework.entity.exception.CaseServerException;
import com.xiaoju.framework.entity.persistent.CaseBackup;
import com.xiaoju.framework.mapper.CaseBackupMapper;
import com.xiaoju.framework.service.CaseBackupService;
+import com.xiaoju.framework.util.MinderJsonPatchUtil;
import com.xiaoju.framework.util.TimeUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
+import java.io.IOException;
import java.util.Date;
import java.util.List;
@@ -70,4 +76,28 @@ private Date transferTime(String time) {
}
return TimeUtil.transferStrToDateInSecond(time);
}
+
+ @Override
+ public CaseBackup getBackupById(Long backupId) {
+ CaseBackup caseBackup = caseBackupMapper.selectOne(backupId);
+
+ if (StringUtils.isEmpty(caseBackup.getJsonPatch())) {
+ // 老的数据没有 json-patch 记录,直接返回即可
+ return caseBackup;
+ }
+
+ try {
+ LOGGER.info("patch 内容: " + caseBackup.getJsonPatch());
+ caseBackup.setCaseContent(MinderJsonPatchUtil.markJsonPatchOnMinderContent(
+ caseBackup.getJsonPatch(),
+ caseBackup.getCaseContent()));
+ } catch (IOException | IllegalArgumentException e) {
+ throw new CaseServerException("添加标记出错,存储的 json-patch 的格式不正确", e, StatusCode.DATA_FORMAT_ERROR);
+ } catch (Exception e) {
+ // 其他异常,一般是标记失败造成。先记录一个 warning 即可,然后返回的内容就不做标记直接返回
+ LOGGER.warn("patch标记失败,backupId 为 {} ", caseBackup.getId(), e);
+ }
+
+ return caseBackup;
+ }
}
diff --git a/case-server/src/main/java/com/xiaoju/framework/service/impl/CaseServiceImpl.java b/case-server/src/main/java/com/xiaoju/framework/service/impl/CaseServiceImpl.java
index 53e2075..01c82c6 100644
--- a/case-server/src/main/java/com/xiaoju/framework/service/impl/CaseServiceImpl.java
+++ b/case-server/src/main/java/com/xiaoju/framework/service/impl/CaseServiceImpl.java
@@ -1,11 +1,13 @@
package com.xiaoju.framework.service.impl;
import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.xiaoju.framework.constants.SystemConstant;
import com.xiaoju.framework.constants.enums.StatusCode;
+import com.xiaoju.framework.entity.dto.ApplyPatchResultDto;
import com.xiaoju.framework.entity.dto.DirNodeDto;
import com.xiaoju.framework.entity.dto.RecordNumDto;
import com.xiaoju.framework.entity.dto.RecordWsDto;
@@ -35,20 +37,28 @@
import com.xiaoju.framework.service.CaseService;
import com.xiaoju.framework.service.DirService;
import com.xiaoju.framework.service.RecordService;
+import com.xiaoju.framework.util.MinderJsonPatchUtil;
import com.xiaoju.framework.util.TimeUtil;
import com.xiaoju.framework.util.TreeUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
+import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
+import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import static com.xiaoju.framework.constants.SystemConstant.COMMA;
import static com.xiaoju.framework.constants.SystemConstant.IS_DELETE;
+import static com.xiaoju.framework.constants.enums.ApplyPatchFlagEnum.IGNORE_EXPAND_STATE_CONFLICT;
+import static com.xiaoju.framework.constants.enums.ApplyPatchFlagEnum.IGNORE_REPLACE_ORDER_CONFLICT;
+import static com.xiaoju.framework.util.MinderJsonPatchUtil.cleanAllBackground;
/**
* 用例实现类
@@ -59,6 +69,8 @@
@Service
public class CaseServiceImpl implements CaseService {
+ private static final Logger LOGGER = LoggerFactory.getLogger(CaseServiceImpl.class);
+
@Resource
private BizMapper bizMapper;
@@ -239,62 +251,135 @@ public CaseGeneralInfoResp getCaseGeneralInfo(Long caseId) {
}
@Override
- public void wsSave(WsSaveReq req) {
-// List editors = WebSocket.getEditingUser(String.valueOf(req.getId()),
-// StringUtils.isEmpty(req.getRecordId())?"undefined":String.valueOf(req.getRecordId()));
-// if (editors.size() < 1) {
-// throw new CaseServerException("用例ws链接已经断开,当前保存可能丢失,请刷新页面重建ws链接。", StatusCode.WS_UNKNOWN_ERROR);
-// }
-
- CaseBackup caseBackup = new CaseBackup();
+ public String wsSave(WsSaveReq req) {
+ // TODO: 这个方法逻辑有点太复杂了,需要再拆开一些 private 的子方法
+ String recordInfo = "";
+ String conflictMessage = "";
+ String saveExecRecordMessage = "";
+ List applyPatch = new ArrayList<>();
+ String returnMessage = "";
+
// 这里触发保存record
- if (!StringUtils.isEmpty(req.getRecordId())) {
+ if (!ObjectUtils.isEmpty(req.getRecordId())) {
RecordWsDto dto = recordService.getWsRecord(req.getRecordId());
- // 看看是不是有重合的执行人
- List names = Arrays.stream(dto.getExecutors().split(COMMA)).filter(e->!StringUtils.isEmpty(e)).collect(Collectors.toList());
- long count = names.stream().filter(e -> e.equals(req.getModifier())).count();
- String executors;
- if (count > 0) {
- // 有重合,不管了
- executors = dto.getExecutors();
- } else {
- // 没重合往后面塞一个
- names.add(req.getModifier());
- executors = String.join(",", names);
+ saveRecord(req, dto);
+
+ ExecRecord record = recordMapper.selectOne(req.getRecordId());
+ // 简单记录下信息,后续 backup 记录用到
+ recordInfo = "|任务名称:" + record.getTitle() + ",任务id:" + record.getId();
+
+ saveExecRecordMessage = "用例执行结果保存成功。";
+
+ if (!StringUtils.isEmpty(req.getBaseCaseContent())) {
+ // 后续要进行用例的增量保存,所以把 baseCaseContent 和 caseContent 里面的 progress 都去掉
+ req.setBaseCaseContent(MinderJsonPatchUtil.cleanAllProgress(req.getBaseCaseContent()));
+ req.setCaseContent(MinderJsonPatchUtil.cleanAllProgress(req.getCaseContent()));
}
+ }
+ // 统一保存用例的变更
+ TestCase testCase = caseMapper.selectOne(req.getId());
- JSONObject jsonObject = TreeUtil.parse(req.getCaseContent());
- ExecRecord record = new ExecRecord();
- record.setId(req.getRecordId());
- record.setCaseId(req.getId());
- record.setModifier(req.getModifier());
- record.setGmtModified(new Date(System.currentTimeMillis()));
- record.setCaseContent(jsonObject.getJSONObject("progress").toJSONString());
- record.setFailCount(jsonObject.getInteger("failCount"));
- record.setBlockCount(jsonObject.getInteger("blockCount"));
- record.setIgnoreCount(jsonObject.getInteger("ignoreCount"));
- record.setPassCount(jsonObject.getInteger("passCount"));
- record.setTotalCount(jsonObject.getInteger("totalCount"));
- record.setSuccessCount(jsonObject.getInteger("successCount"));
- record.setExecutors(executors);
- recordService.modifyRecord(record);
- caseBackup.setCaseId(req.getRecordId());
- caseBackup.setRecordContent(req.getCaseContent());
- caseBackup.setCaseContent("");
- } else {
- // 这里触发保存testcase
- TestCase testCase = caseMapper.selectOne(req.getId());
- testCase.setCaseContent(req.getCaseContent());
- testCase.setModifier(req.getModifier());
- caseMapper.update(testCase);
- caseBackup.setCaseId(req.getId());
- caseBackup.setCaseContent(req.getCaseContent());
- caseBackup.setRecordContent("");
+ if (testCase.getCaseContent().equals(req.getCaseContent())) {
+ return saveExecRecordMessage + "检测到现有内容和数据库一致,无需保存";
+ }
+
+
+ if (StringUtils.isEmpty(req.getBaseCaseContent())) {
+ if (ObjectUtils.isEmpty(req.getRecordId())) {
+ // 老的模式,直接全量保存,和旧逻辑保持一致。仅作为服务端已更新,前端未更新期间避免无法保存时用
+ LOGGER.warn("检测到仍在使用直接覆盖的方式进行用例保存。保存者:{}", req.getModifier());
+ testCase.setCaseContent(req.getCaseContent());
+ // 保存到数据库的用例中,不应该带有任何 background 信息,避免影响预览时标记变更内容
+ testCase.setCaseContent(cleanAllBackground(testCase.getCaseContent()));
+ testCase.setModifier(req.getModifier());
+ caseMapper.update(testCase);
+
+ // 保存成功,也存一个新的备份
+ CaseBackup caseBackup = new CaseBackup();
+ caseBackup.setCaseId(testCase.getId());
+ caseBackup.setTitle(req.getSaveReason() + recordInfo);
+ caseBackup.setCreator(testCase.getModifier());
+ caseBackup.setGmtCreated(new Date());
+ caseBackup.setCaseContent(testCase.getCaseContent());
+ caseBackupService.insertBackup(caseBackup);
+
+ returnMessage = saveExecRecordMessage + "用例集改动全量保存成功";
+ }
+ } else {
+ // 前端可返回 base 版本 json 和改动后版本 json 时,使用增量保存
+ try {
+ String allPatch = MinderJsonPatchUtil.getContentPatch(req.getBaseCaseContent(), req.getCaseContent());
+ LOGGER.info("需要应用的 patch: {}", allPatch);
+
+ if (JSONArray.parseArray(allPatch).isEmpty()) {
+ return saveExecRecordMessage + "检测到本次用例内容没有内容变更,无需保存";
+ }
+
+ // 忽略调整 order 相关冲突及展开状态修改相关冲突
+ ApplyPatchResultDto applyPatchResultDto = MinderJsonPatchUtil.batchApplyPatch(allPatch,
+ testCase.getCaseContent(), EnumSet.of(IGNORE_REPLACE_ORDER_CONFLICT, IGNORE_EXPAND_STATE_CONFLICT));
+ String caseContentAfterPatch = applyPatchResultDto.getJsonAfterPatch();
+ List conflictPatches = applyPatchResultDto.getConflictPatch();
+ applyPatch = applyPatchResultDto.getApplyPatch();
+
+ // 能应用上的要保存一下
+ testCase.setCaseContent(caseContentAfterPatch);
+
+ if (!conflictPatches.isEmpty()) {
+ LOGGER.warn("在用例id {} 上应用增量保存,发现部分改动无法应用。无法应用的改动为:{}", req.getId(),
+ JSONObject.toJSONString(conflictPatches));
+ // 存在冲突,先单独保存一次备份,避免数据丢失
+ CaseBackup conflictCaseBackup = new CaseBackup();
+ conflictCaseBackup.setCaseId(req.getId());
+ conflictCaseBackup.setTitle("合并冲突自动保存副本" + recordInfo);
+ conflictCaseBackup.setCreator(req.getModifier());
+ conflictCaseBackup.setGmtCreated(new Date());
+ conflictCaseBackup.setCaseContent(req.getCaseContent());
+ // 备份保存,也保存一下冲突的 patch 内容,便于后面生成相关指示信息
+ conflictCaseBackup.setIsConflict(true);
+ conflictCaseBackup.setJsonPatch(convertPatchListToJsonArray(conflictPatches));
+
+ CaseBackup dbCaseBackup = caseBackupService.insertBackup(conflictCaseBackup);
+
+ // 请勿改动,前端根据这个来识别本次改动结果信息
+ String backupMsg = "backupId=" + dbCaseBackup.getId();
+ conflictMessage = saveExecRecordMessage + "用例改动增量保存失败,部分内容修改内容和数据库最新版本存在冲突,请手动处理。" + backupMsg;
+ }
+
+ // 看有没有应用成功变更的,有的话更新下数据库内容并存备份,没有的话不用保存
+ if (!applyPatch.isEmpty()) {
+ // 保存到数据库的用例中,不应该带有任何 background 信息,避免影响预览时标记变更内容
+ testCase.setCaseContent(cleanAllBackground(testCase.getCaseContent()));
+ testCase.setModifier(req.getModifier());
+ caseMapper.update(testCase);
+
+ // 保存成功,也存一个新的备份
+ CaseBackup caseBackup = new CaseBackup();
+ caseBackup.setCaseId(testCase.getId());
+ caseBackup.setTitle(req.getSaveReason() + recordInfo);
+ caseBackup.setCreator(testCase.getModifier());
+ caseBackup.setGmtCreated(new Date());
+ caseBackup.setCaseContent(testCase.getCaseContent());
+ // 备份保存,也保存一下本次 patch 的内容,便于看每次改动内容时进行识别
+ caseBackup.setIsConflict(false);
+ caseBackup.setJsonPatch(convertPatchListToJsonArray(applyPatch));
+ caseBackupService.insertBackup(caseBackup);
+ }
+
+ // 如果前面有冲突,需要返回 exception
+ if (!StringUtils.isEmpty(conflictMessage)) {
+ throw new CaseServerException(conflictMessage, StatusCode.SAVE_CONFLICT);
+ }
+
+ returnMessage = saveExecRecordMessage + "用例集改动保存成功";
+ } catch (IOException e) {
+ throw new CaseServerException("解析需增量保存的 json 失败,请确认提交的信息格式正确", e, StatusCode.DATA_FORMAT_ERROR);
+ }
}
- caseBackup.setCreator(req.getModifier());
- caseBackup.setExtra("");
- caseBackupService.insertBackup(caseBackup);
+
+ return returnMessage;
+
}
/**
@@ -472,4 +557,47 @@ public void updateBiz(TestCase testCase, DirNodeDto tree) {
biz.setGmtModified(new Date());
bizMapper.update(biz);
}
+
+ private String convertPatchListToJsonArray(List patchList) {
+ JSONArray patchJsonArray = new JSONArray();
+
+ for (String patch : patchList) {
+ // 也校验一下是否为有效的 json 格式。如果不是,直接报错吧。
+ patchJsonArray.add(JSONObject.parse(patch));
+ }
+
+ return patchJsonArray.toJSONString();
+ }
+
+ // 原来保存测试记录的逻辑
+ private void saveRecord(WsSaveReq req, RecordWsDto dto) {
+ // 看看是不是有重合的执行人
+ List names = Arrays.stream(dto.getExecutors().split(COMMA)).filter(e->!StringUtils.isEmpty(e)).collect(Collectors.toList());
+ long count = names.stream().filter(e -> e.equals(req.getModifier())).count();
+ String executors;
+ if (count > 0) {
+ // 有重合,不管了
+ executors = dto.getExecutors();
+ } else {
+ // 没重合往后面塞一个
+ names.add(req.getModifier());
+ executors = String.join(",", names);
+ }
+
+ JSONObject jsonObject = TreeUtil.parse(req.getCaseContent());
+ ExecRecord record = new ExecRecord();
+ record.setId(req.getRecordId());
+ record.setCaseId(req.getId());
+ record.setModifier(req.getModifier());
+ record.setGmtModified(new Date(System.currentTimeMillis()));
+ record.setCaseContent(jsonObject.getJSONObject("progress").toJSONString());
+ record.setFailCount(jsonObject.getInteger("failCount"));
+ record.setBlockCount(jsonObject.getInteger("blockCount"));
+ record.setIgnoreCount(jsonObject.getInteger("ignoreCount"));
+ record.setPassCount(jsonObject.getInteger("passCount"));
+ record.setTotalCount(jsonObject.getInteger("totalCount"));
+ record.setSuccessCount(jsonObject.getInteger("successCount"));
+ record.setExecutors(executors);
+ recordService.modifyRecord(record);
+ }
}
diff --git a/case-server/src/main/java/com/xiaoju/framework/util/MinderJsonPatchUtil.java b/case-server/src/main/java/com/xiaoju/framework/util/MinderJsonPatchUtil.java
new file mode 100644
index 0000000..685ff03
--- /dev/null
+++ b/case-server/src/main/java/com/xiaoju/framework/util/MinderJsonPatchUtil.java
@@ -0,0 +1,604 @@
+package com.xiaoju.framework.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.parser.Feature;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.flipkart.zjsonpatch.JsonDiff;
+import com.flipkart.zjsonpatch.JsonPatch;
+import com.flipkart.zjsonpatch.JsonPatchApplicationException;
+import com.xiaoju.framework.constants.enums.ApplyPatchFlagEnum;
+import com.xiaoju.framework.entity.dto.ApplyPatchResultDto;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+import static com.flipkart.zjsonpatch.DiffFlags.*;
+import static com.xiaoju.framework.constants.enums.ApplyPatchFlagEnum.IGNORE_EXPAND_STATE_CONFLICT;
+import static com.xiaoju.framework.constants.enums.ApplyPatchFlagEnum.IGNORE_REPLACE_ORDER_CONFLICT;
+
+public class MinderJsonPatchUtil {
+ // TODO: 目前混用了 fastjson 和 jackson ,后面需要优化下,把 fastjson 依赖干掉
+
+
+ /**
+ * 获取两个 Json 之间的差异,以 json patch 格式返回
+ * @param baseContent 改动前 json
+ * @param targetContent 改动后 json
+ * @return json patch 格式的 patch json
+ * @throws IOException json 解析错误时,抛出此异常
+ */
+ public static String getContentPatch(String baseContent, String targetContent) throws IOException {
+ ObjectMapper mapper = new ObjectMapper();
+
+ String convertedBaseContent = convertChildrenArrayToObject(baseContent);
+ String convertedTargetContent = convertChildrenArrayToObject(targetContent);
+
+ JsonNode base = mapper.readTree(convertedBaseContent);
+ JsonNode result = mapper.readTree(convertedTargetContent);
+
+ // OMIT_COPY_OPERATION: 每个节点的 id 都是不一样的,界面上的 copy 到 json-patch 应该是 add ,不应该出现 copy 操作。
+ // ADD_ORIGINAL_VALUE_ON_REPLACE: replace 中加一个 fromValue 表达原来的值
+ // OMIT_MOVE_OPERATION: 所有 move 操作,都还是维持原来 add + remove 的状态,避免一些类似 priority 属性值的一增一减被认为是 move 。
+ // 去掉了默认自带的 OMIT_VALUE_ON_REMOVE ,这样所有 remove 会在 value 字段中带上原始值
+ JsonNode originPatch = JsonDiff.asJson(base, result,
+ EnumSet.of(OMIT_COPY_OPERATION, ADD_ORIGINAL_VALUE_ON_REPLACE, OMIT_MOVE_OPERATION));
+
+ // 借助去掉 order 的内容,正确生成 move 操作
+ JsonNode baseWithoutOrder = mapper.readTree(convertChildrenArrayToObject(baseContent, false));
+ JsonNode targetWithoutOrder = mapper.readTree(convertChildrenArrayToObject(targetContent, false));
+
+ List allFromPath = new ArrayList<>();
+ List allToPath = new ArrayList<>();
+ List allMoveOprations = new ArrayList<>();
+
+ // 需要生成 move 操作,去掉原有 flags 里面的忽略 move 标记
+ JsonNode noOrderPatch = JsonDiff.asJson(baseWithoutOrder, targetWithoutOrder,
+ EnumSet.of(OMIT_COPY_OPERATION, ADD_ORIGINAL_VALUE_ON_REPLACE));
+ for (JsonNode oneNoOrderPatch: noOrderPatch) {
+ if ("move".equals(oneNoOrderPatch.get("op").asText())) {
+ allFromPath.add(oneNoOrderPatch.get("from").asText());
+ allToPath.add(oneNoOrderPatch.get("path").asText());
+ allMoveOprations.add(oneNoOrderPatch);
+ }
+ }
+
+ ArrayNode finalPatch = mapper.createArrayNode();
+ // 先把所有 move 加进这个最终的 patch 中
+ for (JsonNode movePatch : allMoveOprations) {
+ finalPatch.add(movePatch);
+ }
+
+ for (JsonNode onePatch : originPatch) {
+ // 和 move 匹配的 add 中,根节点 order 字段需要变为 replace 存下来,避免丢失顺序
+ if ("add".equals(onePatch.get("op").asText()) && allToPath.contains(onePatch.get("path").asText())) {
+ // 获取 add 中 value 第一层的 order 值。此时 value 实际是移动的整体 object ,order 就在第一层
+ int newOrder = onePatch.get("value").get("order").asInt();
+ ObjectNode replaceOrderPatch = mapper.createObjectNode();
+ replaceOrderPatch.put("op", "replace");
+ replaceOrderPatch.put("path", onePatch.get("path").asText() + "/order");
+ replaceOrderPatch.put("value", newOrder);
+ // 这种情况下就不用管 replace 的原来值是什么了,所以不设定 fromValue
+ finalPatch.add(replaceOrderPatch);
+
+ // 这个 add 的作用已经被 move + replace 达成了,所以不需要记录这个 add
+ continue;
+ }
+
+ // move 的源节点删除操作,需要忽略,因为 move 已经起到相应的作用了
+ if ("remove".equals(onePatch.get("op").asText()) && allFromPath.contains(onePatch.get("path").asText())) {
+ continue;
+ }
+
+ // 如果 order 没变,那不去除 order 的 patch 有可能也有 move 。这个时候这个 move 需要去掉,避免重复
+ if ("move".equals(onePatch.get("op").asText()) && allMoveOprations.contains(onePatch)) {
+ continue;
+ }
+
+ // 其他不需要调整的,直接加进去就可以了
+ finalPatch.add(onePatch);
+ }
+
+ // 整体的 replace 和 remove 加上 test
+ finalPatch = addTestToAllReplaceAndRemove(finalPatch);
+
+ return mapper.writeValueAsString(finalPatch);
+ }
+
+ /**
+ * 给所有 replace 或 remove 的 patch ,能校验原始值的,都加上 test
+ * @param allPatch ArrayNode 形式的所有 patch 内容
+ * @return 添加完 test 后的所有 patch 内容
+ */
+ private static ArrayNode addTestToAllReplaceAndRemove(ArrayNode allPatch) throws IOException {
+ ObjectMapper mapper = new ObjectMapper();
+ ArrayNode result = mapper.createArrayNode();
+
+ for (JsonNode onePatch : allPatch) {
+ // 实际应用 patch 时,不会管 replace 本身的 fromValue 字段。得手动前面加一个 test 的校验应用前的原内容是否一致,并在外面再用一个 array 包起来。
+ // 即 [.., {op: replace, fromValue: .., path: .., value: ..}] 改为 [.., [{op: test, path: .., value: }, {op: replace, path: .., value: }]]
+ // 如果没有 fromValue 字段,那无法校验,直接按原来样子记录即可
+ if ("replace".equals(onePatch.get("op").asText()) && onePatch.get("fromValue") != null) {
+ ArrayNode testAndReplaceArray = mapper.createArrayNode();
+ ObjectNode testPatch = mapper.createObjectNode();
+ testPatch.put("op", "test");
+ testPatch.put("path", onePatch.get("path").asText());
+ testPatch.set("value", onePatch.get("fromValue"));
+
+ testAndReplaceArray.add(testPatch);
+ testAndReplaceArray.add(onePatch);
+
+ result.add(testAndReplaceArray);
+ continue;
+ }
+
+ // remove 同理,有 value 的前面都加一个 test
+ // 特别注意:在测试任务中删除时,很容易因为 order 不一致导致 test 不通过,无法删除。因此删除前校验,应该只校验删除前的本节点 data 及 childrenObject 内容
+ if ("remove".equals(onePatch.get("op").asText()) && onePatch.get("value") != null) {
+ ArrayNode testAndRemoveArray = mapper.createArrayNode();
+ if (isNodePath(onePatch.get("path").asText())) {
+ // 移除的是节点,只需要校验本级别节点的 data 及 childrenObject 全部内容,不要校验 order
+ JsonNode originValue = onePatch.get("value");
+
+ ObjectNode testPatchForData = mapper.createObjectNode();
+ testPatchForData.put("op", "test");
+ testPatchForData.put("path", onePatch.get("path").asText() + "/data");
+ testPatchForData.set("value", originValue.get("data"));
+
+ ObjectNode testPatchForChildrenObject = mapper.createObjectNode();
+ testPatchForChildrenObject.put("op", "test");
+ testPatchForChildrenObject.put("path", onePatch.get("path").asText() + "/childrenObject");
+ testPatchForChildrenObject.set("value", originValue.get("childrenObject"));
+
+ testAndRemoveArray.add(testPatchForData);
+ testAndRemoveArray.add(testPatchForChildrenObject);
+ } else {
+ // 移除的不是节点,正常验证全部内容即可
+ ObjectNode testPatch = mapper.createObjectNode();
+
+ testPatch.put("op", "test");
+ testPatch.put("path", onePatch.get("path").asText());
+ testPatch.set("value", onePatch.get("value"));
+
+ testAndRemoveArray.add(testPatch);
+ }
+ testAndRemoveArray.add(onePatch);
+ result.add(testAndRemoveArray);
+ continue;
+ }
+
+ result.add(onePatch);
+ }
+
+ return result;
+ }
+
+ /**
+ * 详见 batchApplyPatch(String patch, String baseContent, EnumSet flags) 方法。此方法使用的默认 EnumSet 为空。
+ * @param patch patch json
+ * @param baseContent 需要应用到的 json
+ * @return ApplyPatchResultDto 对象,包含应用后的 json 、应用成功的 patch 和跳过的 patch
+ * @throws IOException json 解析错误时,抛出此异常
+ */
+ public static ApplyPatchResultDto batchApplyPatch(String patch, String baseContent) throws IOException {
+ return batchApplyPatch(patch, baseContent, EnumSet.noneOf(ApplyPatchFlagEnum.class));
+ }
+
+ /**
+ * 逐个应用 patch 到目标 json 中,并自动跳过无法应用的 patch 。
+ * @param patch patch json
+ * @param baseContent 需要应用到的 json
+ * @param flags EnumSet,每个元素为 ApplyPatchFlagEnum 枚举值。用于指代应用 patch 过程中一些特殊操作
+ * @return ApplyPatchResultDto 对象,包含应用后的 json 、应用成功的 patch 和跳过的 patch
+ * @throws IOException json 解析错误时,抛出此异常
+ */
+ public static ApplyPatchResultDto batchApplyPatch(String patch, String baseContent, EnumSet flags) throws IOException {
+ baseContent = convertChildrenArrayToObject(baseContent);
+
+ ApplyPatchResultDto applyPatchResultDto = new ApplyPatchResultDto();
+
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode patchJson = mapper.readTree(patch);
+ JsonNode afterPatchJson = mapper.readTree(baseContent);
+ List conflictPatch = new ArrayList<>();
+ List applyPatch = new ArrayList<>();
+
+ for (JsonNode onePatchOperation : patchJson) {
+ try {
+ if (onePatchOperation.isArray()) {
+ afterPatchJson = JsonPatch.apply(onePatchOperation, afterPatchJson);
+ } else { // 外面包一个 array
+ afterPatchJson = JsonPatch.apply(mapper.createArrayNode().add(onePatchOperation), afterPatchJson);
+ }
+ applyPatch.add(mapper.writeValueAsString(onePatchOperation));
+ } catch (JsonPatchApplicationException e) {
+ // 检查是否是对 order 的操作。如果是,那就忽略这个冲突
+ if (flags.contains(IGNORE_REPLACE_ORDER_CONFLICT) &&
+ onePatchOperation.isArray() &&
+ onePatchOperation.get(0).get("path").asText().endsWith("/order")) {
+ continue;
+ }
+ // 忽略展开/不展开节点类的改动冲突
+ if (flags.contains(IGNORE_EXPAND_STATE_CONFLICT) &&
+ onePatchOperation.isArray() &&
+ onePatchOperation.get(0).get("path").asText().endsWith("/expandState")
+ ){
+ continue;
+ }
+ conflictPatch.add(mapper.writeValueAsString(onePatchOperation));
+ }
+ }
+
+ String afterPatch = mapper.writeValueAsString(afterPatchJson);
+ afterPatch = convertChildrenObjectToArray(afterPatch);
+
+ applyPatchResultDto.setJsonAfterPatch(afterPatch);
+ applyPatchResultDto.setConflictPatch(conflictPatch);
+ applyPatchResultDto.setApplyPatch(applyPatch);
+
+ return applyPatchResultDto;
+ }
+
+ /**
+ * 清空用例 json 中包含的所有 progress 属性,即清空里面包含的测试结果
+ * @param
+ * @return
+ */
+ public static String cleanAllProgress(String caseContent) {
+ JSONObject caseContentJson = JSON.parseObject(caseContent);
+ JSONObject rootData = caseContentJson.getJSONObject("root");
+
+ removeNodeSpecificField(rootData, "progress");
+
+ // 把旧数据直接删掉,换成新数据
+ caseContentJson.remove("root");
+ caseContentJson.put("root", rootData);
+
+ return JSON.toJSONString(caseContentJson);
+ }
+
+ /**
+ * 清空用例 json 中包含的所有 background 属性,避免影响后续历史记录预览结果
+ * @param
+ * @return
+ */
+ public static String cleanAllBackground(String caseContent) {
+ JSONObject caseContentJson = JSON.parseObject(caseContent);
+ JSONObject rootData = caseContentJson.getJSONObject("root");
+
+ removeNodeSpecificField(rootData, "background");
+
+ // 把旧数据直接删掉,换成新数据
+ caseContentJson.remove("root");
+ caseContentJson.put("root", rootData);
+
+ return JSON.toJSONString(caseContentJson);
+ }
+
+ /**
+ * 把 children 从 array 改为 object (array中每个元素外面多加一个 key ,key 的值为元素中的 data.id ),解决 json-pointer 针对数组用下标定位,会不准确问题
+ * 示例:
+ * 转换前: {"root": {"data": {"id": "nodeA"}, "children": [{"data": {"id": "nodeAa"}, "children": []}, {"data": {"id": "nodeAb"}, "children": []}]}}
+ * 转换后: {"root": {"data": {"id": "nodeA"}, "childrenObject": {"nodeAa": {"data": {"id": "nodeAa"}, "childrenObject": {}, "order": 0}}, {"nodeAb": {"data": {"id": "nodeAb"}, "childrenObject": {}, "order": 1}}}}
+ * @param caseContent 完整用例 json ,需包含 root 节点数据
+ * @return 转换后 children 都不是 array 的新完整用例 json
+ */
+ public static String convertChildrenArrayToObject(String caseContent) {
+ return convertChildrenArrayToObject(caseContent, true);
+ }
+
+ /**
+ * 把 children 重新从 object 改为 array ,变回原来脑图的格式,用于实际存储到数据库
+ * 示例:
+ * 转换前: {"root": {"data": {"id": "nodeA"}, "childrenObject": {"nodeAa": {"data": {"id": "nodeAa"}, "childrenObject": {}, "order": 0}}, {"nodeAb": {"data": {"id": "nodeAb"}, "childrenObject": {}, "order": 1}}}}
+ * 转换后: {"root": {"data": {"id": "nodeA"}, "children": [{"data": {"id": "nodeAa"}, "children": []}, {"data": {"id": "nodeAb"}, "children": []}]}}
+ * @param convertedCaseContent 转换过的完整用例 json
+ * @return 转换会原来脑图格式的完整 json
+ */
+ public static String convertChildrenObjectToArray(String convertedCaseContent) {
+ return convertChildrenObjectToArray(convertedCaseContent, true);
+ }
+
+ /**
+ * 根据 jsonPatch 内容,在脑图中标记变更。以节点为单位,增加的加绿色背景,删除的加红色背景,修改的加蓝色背景。
+ * 特别注意,移动节点(move)因为实际节点 id 未有变化,所以也会被标记为修改
+ *
+ * @param minderContent
+ * @param jsonPatch
+ * @return
+ */
+ public static String markJsonPatchOnMinderContent(String jsonPatch, String minderContent) throws IOException, IllegalArgumentException {
+ String green = "#67c23a";
+ String blue = "#409eff";
+ String red = "#f56c6c";
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ // 因为 jsonPatch 是针对已经把 children 数组变为对象的 json 格式,所以要先转换下
+ ObjectNode convertedMinderContentJson = objectMapper.readTree(convertChildrenArrayToObject(minderContent)).deepCopy();
+
+ ArrayNode jsonPatchArray = (ArrayNode) objectMapper.readTree(jsonPatch);
+
+ for (JsonNode onePatch : jsonPatchArray) {
+ JsonNode operation;
+ if (onePatch.isArray() && onePatch.size() <= 3 && onePatch.size() >= 2) {
+ // 只可能是 replace 或 remove 的。前面多加了 test ,会是一个带有2个或3个元素的 array 。且最后一个才是 replace 或 remove
+ operation = onePatch.get(onePatch.size()-1);
+ if (!("replace".equals(operation.get("op").asText()) || "remove".equals(operation.get("op").asText()))) {
+ throw new IllegalArgumentException(String.format("此单个 patch 格式不正常," +
+ "正常格式在多元素 array 的最后一个,应该是 replace 或 remove 操作" +
+ "不符合的 patch 内容: %s",
+ objectMapper.writeValueAsString(onePatch)));
+ }
+ } else if (onePatch.isObject()) {
+ operation = onePatch;
+ } else {
+ // 目前不会生成不符合这两种格式的 patch ,抛异常
+ throw new IllegalArgumentException(String.format("此单个 patch 格式不正常,正常格式应该是2到3个元素的array或单个object" +
+ "请确认 patch 内容是通过此工具类提供的获取 patch 方法生成。不符合的 patch 内容: %s",
+ objectMapper.writeValueAsString(onePatch)));
+ }
+
+ // 先判定是否为整个节点的内容变更
+ if (isNodePath(operation.get("path").asText())) {
+ // 节点级别,只支持 add 、 remove 、move 。因为 replace 只改值不改key,不可能在节点级别产生 replace 操作
+ switch (operation.get("op").asText()) {
+ case "add":
+ addAddNodeMark(convertedMinderContentJson, operation, green);
+ break;
+ case "move":
+ addMoveNodeMark(convertedMinderContentJson, operation, blue);
+ break;
+ case "remove":
+ addRemoveNodeMark(convertedMinderContentJson, operation, red);
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("此单个 patch 格式不正常," +
+ "正常的节点级别 patch ,op 应该是 add、move、remove 其中一个" +
+ "不符合的 patch 内容: %s",
+ objectMapper.writeValueAsString(operation)));
+ }
+ } else {
+ // 非节点级别变更,都将它标记为 修改内容 即可。不应该出现 move 节点属性的动作
+ switch (operation.get("op").asText()) {
+ case "add":
+ addAddAttrMark(convertedMinderContentJson, operation, blue);
+ break;
+ case "replace":
+ addReplaceAttrMark(convertedMinderContentJson, operation, blue);
+ break;
+ case "remove":
+ addRemoveAttrMark(convertedMinderContentJson, operation, blue);
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("此单个 patch 格式不正常," +
+ "正常的非节点级别 patch ,op 应该是 add、replace、remove 四个其中一个" +
+ "不符合的 patch 内容: %s",
+ objectMapper.writeValueAsString(operation)));
+ }
+ }
+ }
+
+ return convertChildrenObjectToArray(objectMapper.writeValueAsString(convertedMinderContentJson));
+ }
+
+ private static void addReplaceAttrMark(JsonNode convertedMinderContentJson, JsonNode replacePatch, String backgroundValue) {
+ // replace 的意味着节点 id 没变,只会影响单个节点,直接给这个节点标记即可
+ // 因为改动内容是单个属性,所以直接给这个 patch 上一级的节点,把 background 属性改为蓝色即可
+ String replacePathText = replacePatch.get("path").asText();
+ // 改动的属性有可能是在 data 字段中的,也可能是手动添加的 order
+ String replaceNodePathText;
+ if (replacePathText.endsWith("order")) {
+ replaceNodePathText = replacePathText.substring(0, replacePathText.lastIndexOf("/order"));
+ } else {
+ replaceNodePathText = replacePathText.substring(0, replacePathText.lastIndexOf("/data"));
+ }
+
+ ObjectNode replaceNode = (ObjectNode) convertedMinderContentJson.at(replaceNodePathText);
+ ((ObjectNode) replaceNode.get("data")).put("background", backgroundValue);
+ }
+
+ private static void addRemoveAttrMark(JsonNode convertedMinderContentJson, JsonNode removePatch, String backgroundValue) {
+ String removePath = removePatch.get("path").asText();
+
+ // remove 属性,直接给这个节点做标记即可
+ String removeAttrNodePath = removePath.substring(0, removePath.lastIndexOf("/data"));
+ JsonNode removeAttrNode = convertedMinderContentJson.at(removeAttrNodePath);
+ ((ObjectNode) removeAttrNode.get("data")).put("background", backgroundValue);
+ }
+
+ private static void addRemoveNodeMark(JsonNode convertedMinderContentJson, JsonNode removePatch, String backgroundValue) throws JsonProcessingException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ String removePath = removePatch.get("path").asText();
+
+ // remove 节点,有可能存在两种场景:应用成功、应用失败。成功的话,节点会不存在。需要根据现在新版内容判定是成功还是失败。
+ JsonNode removeNode = convertedMinderContentJson.at(removePath);
+ if (removeNode.isMissingNode()) {
+ // 属于应用成功的,需要从 value 里把删掉的内容重新加回来,并给每个子节点都标记红色
+ removeNode = removePatch.get("value");
+ if (removeNode == null) {
+ throw new IllegalArgumentException(String.format("此单个 patch 格式不正常," +
+ "正常格式 remove 应该带有 value 字段,表示被删内容原始值" +
+ "不符合的 patch 内容: %s",
+ objectMapper.writeValueAsString(removePatch)));
+ }
+ String removePathText = removePatch.get("path").asText();
+ String removeNodeParentNodePath = removePathText.substring(0, removePathText.lastIndexOf('/'));
+ ObjectNode removeNodeParentObject = (ObjectNode) convertedMinderContentJson.at(removeNodeParentNodePath);
+ removeNodeParentObject.set(removeNode.get("data").get("id").asText(), removeNode);
+
+ addBackgroundOnAllNodes((ObjectNode) removeNode, backgroundValue);
+ } else {
+ // 属于应用失败,存在冲突的 patch 。直接标记颜色即可
+ addBackgroundOnAllNodes((ObjectNode) removeNode, backgroundValue);
+ }
+ }
+
+ private static void addAddAttrMark(JsonNode convertedMinderContentJson, JsonNode addPatch, String backgroundValue) {
+ // add 有可能是新增子节点,也可能是新增属性。
+ String addPatchPath = addPatch.get("path").asText();
+
+ // 增加属性的
+ String addPatchNodePath = addPatchPath.substring(0, addPatchPath.lastIndexOf("/data"));
+ JsonNode addAttrNode = convertedMinderContentJson.at(addPatchNodePath);
+ ((ObjectNode) addAttrNode.get("data")).put("background", backgroundValue);
+ }
+
+ private static void addAddNodeMark(JsonNode convertedMinderContentJson, JsonNode addPatch, String backgroundValue) {
+ // 递归给本节点及所有子节点加标记
+ JsonNode addNode = convertedMinderContentJson.at(addPatch.get("path").asText());
+ addBackgroundOnAllNodes((ObjectNode) addNode, backgroundValue);
+ }
+
+
+ private static void addMoveNodeMark(JsonNode convertedMinderContentJson, JsonNode movePatch, String backgroundValue) {
+ // 不管是否冲突,备份里都会有移动后的数据。找到 move 后的 object ,直接递归加标记即可
+ JsonNode moveNode = convertedMinderContentJson.at(movePatch.get("path").asText());
+ addBackgroundOnAllNodes((ObjectNode) moveNode, backgroundValue);
+ }
+
+ private static boolean isNodePath(String jsonPatchPath) {
+ String[] keys = jsonPatchPath.split("/");
+ return "childrenObject".equals(keys[keys.length - 2]);
+ }
+
+
+ // 递归给此节点及下面所有子节点都加上指定的 background 属性值
+ private static void addBackgroundOnAllNodes(ObjectNode rootNode, String backgroundValue) {
+ // 先给当前节点的 data ,加 background
+ ((ObjectNode) rootNode.get("data")).put("background", backgroundValue);
+
+ // 再给当前节点的 childrenObject ,进行递归
+ for (JsonNode childObject : rootNode.get("childrenObject")) {
+ addBackgroundOnAllNodes((ObjectNode) childObject, backgroundValue);
+ }
+ }
+
+
+ private static String convertChildrenArrayToObject(String caseContent, Boolean withOrder) {
+ JSONObject caseContentJson = JSON.parseObject(caseContent);
+ JSONObject rootData = caseContentJson.getJSONObject("root");
+
+ rootData.put("childrenObject", convertArrayToObject(rootData.getJSONArray("children"), withOrder));
+
+ // 把旧数据直接删掉,换成新数据
+ rootData.remove("children");
+
+ return JSON.toJSONString(caseContentJson);
+ }
+
+ private static String convertChildrenObjectToArray(String convertedCaseContent, Boolean withOrder) {
+ JSONObject caseContentJson = JSON.parseObject(convertedCaseContent, Feature.OrderedField);
+ JSONObject rootData = caseContentJson.getJSONObject("root");
+
+ rootData.put("children", convertObjectToArray(rootData.getJSONObject("childrenObject"), withOrder));
+
+ // 把旧数据直接删掉,换成新数据
+ rootData.remove("childrenObject");
+
+ return JSON.toJSONString(caseContentJson);
+ }
+
+ // 递归把每个 object 改回 array ,去掉 object 中第一层的 key
+ private static JSONArray convertObjectToArray(JSONObject childrenObject, Boolean withOrder) {
+ JSONArray childrenArray = new JSONArray();
+ List keyMoved = new ArrayList<>();
+
+ // object 中每个子元素,重新放回到 array 中
+ for (int i=0; i
- id, case_id, title, creator, gmt_created,case_content, record_content,extra,is_delete
+ id, case_id, title, creator, gmt_created,case_content, record_content,extra, is_delete, json_patch, is_conflict