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 + select + + from case_backup + where id = #{backupId,jdbcType=BIGINT} and is_delete = 0 + + \ No newline at end of file diff --git a/case-server/src/test/java/com/xiaoju/framework/util/MinderJsonPatchUtilTest.java b/case-server/src/test/java/com/xiaoju/framework/util/MinderJsonPatchUtilTest.java new file mode 100644 index 0000000..05f42e7 --- /dev/null +++ b/case-server/src/test/java/com/xiaoju/framework/util/MinderJsonPatchUtilTest.java @@ -0,0 +1,409 @@ +package com.xiaoju.framework.util; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +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.xiaoju.framework.entity.dto.ApplyPatchResultDto; +import org.apache.commons.io.IOUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.EnumSet; +import java.util.Objects; + +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 MinderJsonPatchUtilTest { + + + private String jsonNoNode; + private String jsonAddNodeA; + private String jsonAddNodeB; + private String jsonAddNodeCBaseNodeB; + private String jsonAddNodeDBaseNodeA; + private String jsonAddABCD; + private String jsonAddPriorityOnABaseAD; + + private String jsonChangeNodeAToAA; + private String jsonChangeNodeAToAAA; + private String jsonMoveBBaseAOnABCD; + private String jsonMoveBBaseABeforeDOnABCD; + + private String jsonNodeACollapse; + private String jsonNodeAExpand; + + private ObjectMapper objectMapper = new ObjectMapper(); + + + @Before + public void initResources() throws Exception { + jsonNoNode = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonNoNode.json")), "utf-8"); + jsonAddNodeA = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonAddNodeA.json")), "utf-8"); + jsonAddNodeB = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonAddNodeB.json")), "utf-8");; + jsonAddNodeCBaseNodeB = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonAddNodeCBaseNodeB.json")), "utf-8"); + jsonAddNodeDBaseNodeA = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonAddNodeDBaseNodeA.json")), "utf-8"); + jsonAddABCD = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonAddABCD.json")), "utf-8"); + jsonAddPriorityOnABaseAD = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonAddPriorityOnABaseAD.json")), "utf-8"); + + jsonChangeNodeAToAA = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonChangeNodeAToAA.json")), "utf-8"); + jsonChangeNodeAToAAA = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonChangeNodeAToAAA.json")), "utf-8"); + + jsonMoveBBaseAOnABCD = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonMoveBBaseAOnABCD.json")), "utf-8"); + jsonMoveBBaseABeforeDOnABCD = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonMoveBBaseABeforeDOnABCD.json")), "utf-8"); + + jsonNodeACollapse = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonNodeACollapse.json")), "utf-8"); + jsonNodeAExpand = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonNodeAExpand.json")), "utf-8"); + + + } + + // 测试增量合并相关功能 + @Test + public void testGetDiffAndPatchBetweenTwoVersion() throws IOException { + String patch = MinderJsonPatchUtil.getContentPatch(jsonNoNode, jsonAddNodeA); + + ApplyPatchResultDto applyPatchResultDto = MinderJsonPatchUtil.batchApplyPatch(patch, jsonNoNode); + Assert.assertEquals(JSONObject.parseObject(jsonAddNodeA), JSONObject.parseObject(applyPatchResultDto.getJsonAfterPatch())); + } + + @Test + public void testConflict() throws JsonProcessingException, IOException { + // 最原始的 json 是 jsonAddNodeB + String baseJson = jsonAddNodeB; + + // 首先,有用户基于 b 添加子节点 c + String addNodeC = MinderJsonPatchUtil.getContentPatch(baseJson, jsonAddNodeCBaseNodeB); + System.out.println("第一个 patch 内容: " + addNodeC); + + // 同时,另一个用户把 b 删除了 + String deleteB = MinderJsonPatchUtil.getContentPatch(baseJson, jsonNoNode); + System.out.println("第二个 patch 内容: " + deleteB); + + // 删除的先保存 + ApplyPatchResultDto applyPatchResultDto = MinderJsonPatchUtil.batchApplyPatch(deleteB, jsonAddNodeB); + String firstSave = applyPatchResultDto.getJsonAfterPatch(); + + // 添加的再保存,会因为冲突被跳过 + applyPatchResultDto = MinderJsonPatchUtil.batchApplyPatch(addNodeC, firstSave); + Assert.assertEquals(JSONObject.parseObject(firstSave), JSONObject.parseObject(applyPatchResultDto.getJsonAfterPatch())); + Assert.assertNotEquals(0, applyPatchResultDto.getConflictPatch().size()); + System.out.println("产生冲突的 patch 操作" + applyPatchResultDto.getConflictPatch()); + } + + @Test // 测试在获取 patch 和应用 patch 时,整个 json 的 children 从 array 改为用 object ,是否可解决下标不准确问题 + public void testApplyPatchOnArray() throws IOException { + String baseJson = jsonNoNode; + + // 用户a开始编辑, 添加了 A + String addA = MinderJsonPatchUtil.getContentPatch(baseJson, jsonAddNodeA); + String step1 = MinderJsonPatchUtil.batchApplyPatch(addA, baseJson).getJsonAfterPatch(); + + // 用户b开始编辑,添加了 B + String addB = MinderJsonPatchUtil.getContentPatch(baseJson, jsonAddNodeB); + String step2 = MinderJsonPatchUtil.batchApplyPatch(addB, step1).getJsonAfterPatch(); + + // 用户 a 继续基于 A 添加 D + String addDbaseA = MinderJsonPatchUtil.getContentPatch(jsonAddNodeA, jsonAddNodeDBaseNodeA); + String step3 = MinderJsonPatchUtil.batchApplyPatch(addDbaseA, step2).getJsonAfterPatch(); + + // 用户 b 继续基于 B 添加 C + String addCbaseB = MinderJsonPatchUtil.getContentPatch(jsonAddNodeB, jsonAddNodeCBaseNodeB); + String step4 = MinderJsonPatchUtil.batchApplyPatch(addCbaseB, step3).getJsonAfterPatch(); + + // 确认最后的 C 是加到了 B 而非 A 上。直接用原版 json 应用增量,会因为下标为0加到了 A 后面。 + assertJsonObjectEquals(step4, jsonAddABCD); + } + + @Test + public void testConvertAllDataToId() { + String converted = MinderJsonPatchUtil.convertChildrenArrayToObject(jsonAddABCD); + String reverted = MinderJsonPatchUtil.convertChildrenObjectToArray(converted); + + System.out.println("childrenArray 转为 jsonObject 后 " + converted); + System.out.println("childrenObject 转回 childrenArray 后" + reverted); + + assertJsonObjectEquals(jsonAddABCD, reverted); + } + + @Test // 测试当转换时遇到 order 比 childrenObject 的子元素个数大时,元素依然能被转换,不被丢失或者遗漏转换 + public void testConvertChildrenObjectToChildrenArrayWithOrderBiggerThanChildrenObjectKeysetSize() throws IOException { + String jsonWithChildrenObject = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonWithChildrenObject.json")), "utf-8"); + String jsonWithChildrenArray = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonWithChildrenArray.json")), "utf-8"); + + assertJsonObjectEquals(jsonWithChildrenArray, MinderJsonPatchUtil.convertChildrenObjectToArray(jsonWithChildrenObject)); + } + + @Test + public void testReplaceWouldCheckBaseData() throws IOException { + // 用户 a 把 a 的 text 属性从 A 改为了 AA + String replaceAtoAA = MinderJsonPatchUtil.getContentPatch(jsonAddNodeA, jsonChangeNodeAToAA); + String latestContent = MinderJsonPatchUtil.batchApplyPatch(replaceAtoAA, jsonAddNodeA).getJsonAfterPatch(); + + // 另一个用户,把 a 的 text 从 A 改为了 AAA + String replaceAto3A = MinderJsonPatchUtil.getContentPatch(jsonAddNodeA, jsonChangeNodeAToAAA); + ApplyPatchResultDto applyPatchResultDto = MinderJsonPatchUtil.batchApplyPatch(replaceAto3A, latestContent); + + // 此时应该会发现有冲突,因为第二个用户的变更基于的原始值不对 + Assert.assertNotEquals(0, applyPatchResultDto.getConflictPatch().size()); + assertJsonArrayEquals("[{'op':'test','path':'/root/childrenObject/cby3dozxagw0/data/text','value':'A'},{'op':'replace','fromValue':'A','path':'/root/childrenObject/cby3dozxagw0/data/text','value':'AAA'}]", applyPatchResultDto.getConflictPatch().get(0)); + + // 用户更新了内容,从 a 的 text 从 AA 改为 AAA + String replace2Ato3A = MinderJsonPatchUtil.getContentPatch(jsonChangeNodeAToAA, jsonChangeNodeAToAAA); + applyPatchResultDto = MinderJsonPatchUtil.batchApplyPatch(replace2Ato3A, latestContent); + + // 这次可以正常应用了 + Assert.assertEquals(0, applyPatchResultDto.getConflictPatch().size()); + } + + @Test + public void testRemoveWouldCheckBaseDataInRemoveNotNode() throws IOException { + // 用户 a 给 A 删除了优先级 + String baseJson = jsonAddPriorityOnABaseAD; + String deleteExpandStateInA = MinderJsonPatchUtil.getContentPatch(baseJson, jsonAddNodeDBaseNodeA); + + // 可以删除成功 + String latestContent = MinderJsonPatchUtil.batchApplyPatch(deleteExpandStateInA, baseJson).getJsonAfterPatch(); + assertJsonObjectEquals(jsonAddNodeDBaseNodeA, latestContent); + } + + @Test + public void testRemoveWouldCheckBaseDataInRemoveNodeChildrenObjectConflict() throws IOException { + // 用户 a 给 A 添加了 D 节点 + String baseJson = jsonAddNodeA; + + String addDtoA = MinderJsonPatchUtil.getContentPatch(baseJson, jsonAddNodeDBaseNodeA); + String latestContent = MinderJsonPatchUtil.batchApplyPatch(addDtoA, baseJson).getJsonAfterPatch(); + + assertJsonObjectEquals(jsonAddNodeDBaseNodeA, latestContent); + + // 另一个用户,把 A 删掉了,此时他还没看到 D 节点 + String deleteA = MinderJsonPatchUtil.getContentPatch(baseJson, jsonNoNode); + ApplyPatchResultDto applyPatchResultDto = MinderJsonPatchUtil.batchApplyPatch(deleteA, latestContent); + + // 应该会存在冲突 + Assert.assertNotEquals(0, applyPatchResultDto.getConflictPatch().size()); + assertJsonArrayEquals("[{'op':'test','path':'/root/childrenObject/cby3dozxagw0/data','value':{'created':1623140763953,'id':'cby3dozxagw0','text':'A'}},{'op':'test','path':'/root/childrenObject/cby3dozxagw0/childrenObject','value':{}},{'op':'remove','path':'/root/childrenObject/cby3dozxagw0','value':{'data':{'created':1623140763953,'id':'cby3dozxagw0','text':'A'},'childrenObject':{},'order':0}}]", + applyPatchResultDto.getConflictPatch().get(0)); + + // 用户更新了内容,再删除 A + String deleteAwithD = MinderJsonPatchUtil.getContentPatch(latestContent, jsonNoNode); + applyPatchResultDto = MinderJsonPatchUtil.batchApplyPatch(deleteAwithD, latestContent); + latestContent = applyPatchResultDto.getJsonAfterPatch(); + + // 不再有冲突 + Assert.assertEquals(0, applyPatchResultDto.getConflictPatch().size()); + assertJsonObjectEquals(jsonNoNode, latestContent); + } + + @Test // 删除的是节点,校验时只校验 data 和 childrenObject ,忽略 order 的校验。主要是针对在测试任务中,order 不一定和全集一致的情况。 + public void testRemoveWouldCheckBaseDataInRemoveNodeOrderConflict() throws IOException { + // 用户在子集中只看到 B 节点和其子节点 C ,进行了删除 + String deleteB = MinderJsonPatchUtil.getContentPatch(jsonAddNodeCBaseNodeB, jsonNoNode); + + // 实际全集里,B 是在 A 同级的下一个的。应用变更时应该成功 + ApplyPatchResultDto applyPatchResultDto = MinderJsonPatchUtil.batchApplyPatch(deleteB, jsonAddABCD); + + // 此时,不应该出现冲突 + Assert.assertEquals(0, applyPatchResultDto.getConflictPatch().size()); + // 删除后,只剩下 AD 节点 + assertJsonObjectEquals(jsonAddNodeDBaseNodeA, applyPatchResultDto.getJsonAfterPatch()); + } + + @Test + public void testMoveNodeShouldNotBeRemoveAndAdd() throws IOException { + String moveBBaseAOnABCDPatch = MinderJsonPatchUtil.getContentPatch(jsonAddABCD, jsonMoveBBaseAOnABCD); + + // 确认只生成了 move 操作。后面的 order 是为了修复测试任务 order 不一定准确的问题的,不影响本身 move 合并 + assertJsonArrayEquals("[{'op':'move','path':'/root/childrenObject/cby3dozxagw0/childrenObject/cby3h1dtg4g0','from':'/root/childrenObject/cby3h1dtg4g0'},{'op':'replace','path':'/root/childrenObject/cby3dozxagw0/childrenObject/cby3h1dtg4g0/order','value':1}]", + moveBBaseAOnABCDPatch); + + // 确认 move 操作应用后的结果,和原来生成 patch 的结果完全一致 + ApplyPatchResultDto afterPatch = MinderJsonPatchUtil.batchApplyPatch(moveBBaseAOnABCDPatch, jsonAddABCD); + assertJsonObjectEquals(jsonMoveBBaseAOnABCD, afterPatch.getJsonAfterPatch()); + } + + @Test + public void testMoveAndChangeOrder() throws IOException { + String moveBBaseAAfterDOnABCDPatch = MinderJsonPatchUtil.getContentPatch(jsonAddABCD, jsonMoveBBaseABeforeDOnABCD); + + // 确认有把 B 的 order 从 1 变为 0 的操作 + System.out.println("moveBBaseAAfterDOnABCDPatch: " + moveBBaseAAfterDOnABCDPatch); + + // 确认操作后的结果,和原来生成的 patch 结果完全一致 + ApplyPatchResultDto afterPatch = MinderJsonPatchUtil.batchApplyPatch(moveBBaseAAfterDOnABCDPatch, jsonAddABCD); + assertJsonObjectEquals(jsonMoveBBaseABeforeDOnABCD, afterPatch.getJsonAfterPatch()); + } + + @Test + public void testIgnoreOrderConflict() throws IOException { + // 场景:根节点下第一级别,依次有 A、B、E 三个节点。经过筛选,只展示了 A、E 节点,这时候往 A、E 节点中间插入 F ,应该不出现冲突,且确认 F 插入成功,位置确实在 A、E 之间 + String json1LevelABE = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/json1LevelABE.json")), "utf-8"); + String json1LevelAE = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/json1LevelAE.json")), "utf-8"); + String json1LevelAFE = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/json1LevelAFE.json")), "utf-8"); + + // 在测试任务中,只看到 AE ,并在 A和E中间插入了 F + String addFBetweenAE = MinderJsonPatchUtil.getContentPatch(json1LevelAE, json1LevelAFE); + + // 但实际完整的用例已有 ABE ,所以插入 F 理论上会引起冲突,需要加忽略 order 冲突的 flag + ApplyPatchResultDto applyPatchResultDto = MinderJsonPatchUtil.batchApplyPatch(addFBetweenAE, json1LevelABE, EnumSet.of(IGNORE_REPLACE_ORDER_CONFLICT)); + + // 确认没产生冲突,忽略了 order replace 的冲突 + Assert.assertEquals(0, applyPatchResultDto.getConflictPatch().size()); + + // 确认最终 F 被加到了 A、E 之间 + String latestContent = applyPatchResultDto.getJsonAfterPatch(); + JsonNode latestContentObject = objectMapper.readTree(latestContent); + ArrayNode nodesUnderRoot = (ArrayNode) latestContentObject.get("root").get("children"); + assertJsonArrayEquals("[{'data':{'created':1623140763953,'id':'cby3dozxagw0','text':'A'},'children':[]},{'data':{'created':1623141026007,'id':'cby3h1dtg4g0','text':'B'},'children':[]},{'data':{'created':1623141026022,'id':'cby3h1dtg422','text':'F'},'children':[]},{'data':{'created':1623141026011,'id':'cby3h1dtg411','text':'E'},'children':[]}]\n", + objectMapper.writeValueAsString(nodesUnderRoot)); + } + + @Test + public void testIgnoreExpandStateConflict() throws IOException { + // 场景:可能在浏览过程中无意收起了其他的节点有改动过的节点,造成冲突。此类冲突应该忽略,不应该反馈出来 + + // 某用户把节点展开了 + String latestConetent = jsonNodeAExpand; + + // 另一个用户,打开时节点是非展开的,手动将其展开 + String expandPatch = MinderJsonPatchUtil.getContentPatch(jsonNodeACollapse, jsonNodeAExpand); + + // 确认不会产生冲突 + Assert.assertEquals(0, MinderJsonPatchUtil.batchApplyPatch(expandPatch, latestConetent, EnumSet.of(IGNORE_EXPAND_STATE_CONFLICT)).getConflictPatch().size()); + } + + @Test + public void testMarkAddNode() throws IOException { + // 新增(需递归给增加的内容添加绿色) + String addNodeAD = MinderJsonPatchUtil.getContentPatch(jsonNoNode, jsonAddNodeDBaseNodeA); + String markAdd = MinderJsonPatchUtil.markJsonPatchOnMinderContent(addNodeAD, jsonAddNodeDBaseNodeA); + + System.out.println("markedAdd: " + markAdd); + String addPath = objectMapper.readTree(addNodeAD).get(0).get("path").asText(); + JsonNode markAddJson = objectMapper.readTree(MinderJsonPatchUtil.convertChildrenArrayToObject(markAdd)); + JsonNode addNode = markAddJson.at(addPath); + assertJsonObjectEquals("{'data':{'created':1623140763953,'background':'#67c23a','id':'cby3dozxagw0','text':'A'},'childrenObject':{'cby3h1dtg4ff':{'data':{'created':1623141026009,'background':'#67c23a','id':'cby3h1dtg4ff','text':'D'},'childrenObject':{},'order':0}},'order':0}", + objectMapper.writeValueAsString(addNode)); + } + @Test + public void testMarkAddAttr() throws IOException { + // 新增有可能只是属性的新增(比如优先级、自定义标签),此时标记为修改节点,并只标记此节点。 + String addAttributeOnA = MinderJsonPatchUtil.getContentPatch(jsonAddNodeDBaseNodeA, jsonAddPriorityOnABaseAD); + String markAddAttribute = MinderJsonPatchUtil.markJsonPatchOnMinderContent(addAttributeOnA, jsonAddPriorityOnABaseAD); + + System.out.println("markAddAttribute " + markAddAttribute); + String addAttributePath = objectMapper.readTree(addAttributeOnA).get(0).get("path").asText(); + String addAttributeNodePath = addAttributePath.substring(0, addAttributePath.lastIndexOf("/data")); + JsonNode markAddAttributeJson = objectMapper.readTree(MinderJsonPatchUtil.convertChildrenArrayToObject(markAddAttribute)); + JsonNode addAttributeNode = markAddAttributeJson.at(addAttributeNodePath); + assertJsonObjectEquals("{'data':{'created':1623140763953,'background':'#409eff','id':'cby3dozxagw0','text':'A','priority':1},'childrenObject':{'cby3h1dtg4ff':{'data':{'created':1623141026009,'id':'cby3h1dtg4ff','text':'D'},'childrenObject':{},'order':0}},'order':0}", + objectMapper.writeValueAsString(addAttributeNode)); + } + + @Test + public void testMarkRemoveNodeNotInConflict() throws IOException { + // 删除1:应用成功的场景(实际新内容已经没有被删除的节点了) + String deleteNodeAD = MinderJsonPatchUtil.getContentPatch(jsonAddNodeDBaseNodeA, jsonNoNode); + String markRemove = MinderJsonPatchUtil.markJsonPatchOnMinderContent(deleteNodeAD, jsonNoNode); + + System.out.println("markedRemove: " + markRemove); + // remove 的前面会加 test 操作,所以要取最后一个 + JsonNode deleteNodeADPatch = objectMapper.readTree(deleteNodeAD).get(0); + String removePath = deleteNodeADPatch.get(deleteNodeADPatch.size()-1).get("path").asText(); + JsonNode markRemoveJson = objectMapper.readTree(MinderJsonPatchUtil.convertChildrenArrayToObject(markRemove)); + JsonNode removeNode = markRemoveJson.at(removePath); + // 把被删除的节点内容补充回来了 + Assert.assertFalse(removeNode.isMissingNode()); + assertJsonObjectEquals("{'data':{'created':1623140763953,'background':'#f56c6c','id':'cby3dozxagw0','text':'A'},'childrenObject':{'cby3h1dtg4ff':{'data':{'created':1623141026009,'background':'#f56c6c','id':'cby3h1dtg4ff','text':'D'},'childrenObject':{},'order':0}},'order':0}", + objectMapper.writeValueAsString(removeNode)); + + } + + @Test + public void testMarkRemoveNodeInConflict() throws IOException { + // 删除2:冲突的场景(实际新内容里还是有被删除节点) + String deleteNodeAD = MinderJsonPatchUtil.getContentPatch(jsonAddNodeDBaseNodeA, jsonNoNode); + JsonNode deleteNodeADPatch = objectMapper.readTree(deleteNodeAD).get(0); + String removePath = deleteNodeADPatch.get(deleteNodeADPatch.size() - 1).get("path").asText(); + String markRemoveInConflict = MinderJsonPatchUtil.markJsonPatchOnMinderContent(deleteNodeAD, jsonAddNodeDBaseNodeA); + + System.out.println("markRemoveInConflict: " + markRemoveInConflict); + JsonNode markRemoveInConflictJson = objectMapper.readTree(MinderJsonPatchUtil.convertChildrenArrayToObject(markRemoveInConflict)); + JsonNode removeInConflictNode = markRemoveInConflictJson.at(removePath); + Assert.assertFalse(removeInConflictNode.isMissingNode()); + assertJsonObjectEquals("{'data':{'created':1623140763953,'background':'#f56c6c','id':'cby3dozxagw0','text':'A'},'childrenObject':{'cby3h1dtg4ff':{'data':{'created':1623141026009,'background':'#f56c6c','id':'cby3h1dtg4ff','text':'D'},'childrenObject':{},'order':0}},'order':0}", + objectMapper.writeValueAsString(removeInConflictNode)); + } + + @Test + public void testMarkRemoveAttr() throws IOException { + String deletePriorityOnA = MinderJsonPatchUtil.getContentPatch(jsonAddPriorityOnABaseAD, jsonAddNodeDBaseNodeA); + String markRemoveAttr = MinderJsonPatchUtil.markJsonPatchOnMinderContent(deletePriorityOnA, jsonAddABCD); + + JsonNode markRemoveAttrJson = objectMapper.readTree(MinderJsonPatchUtil.convertChildrenArrayToObject(markRemoveAttr)); + String removeAttrPath = objectMapper.readTree(deletePriorityOnA).get(0).get(1).get("path").asText(); + String removeAttrNodePath = removeAttrPath.substring(0, removeAttrPath.lastIndexOf("/data")); + JsonNode removeAttrNode = markRemoveAttrJson.at(removeAttrNodePath); + Assert.assertFalse(removeAttrNode.isMissingNode()); + assertJsonObjectEquals("{'data':{'created':1623140763953,'background':'#409eff','id':'cby3dozxagw0','text':'A'},'childrenObject':{'cby3h1dtg4ff':{'data':{'created':1623141026009,'id':'cby3h1dtg4ff','text':'D'},'childrenObject':{},'order':0}},'order':0}", + objectMapper.writeValueAsString(removeAttrNode)); + } + + @Test + public void testMarkReplaceAttr() throws IOException { + // 修改: replace (只针对单节点进行) + String replaceAAtoA = MinderJsonPatchUtil.getContentPatch(jsonChangeNodeAToAA, jsonAddNodeA); + String markReplace = MinderJsonPatchUtil.markJsonPatchOnMinderContent(replaceAAtoA, jsonAddNodeDBaseNodeA); + + System.out.println("markReplace: " + markReplace); + JsonNode markReplaceJson = objectMapper.readTree(MinderJsonPatchUtil.convertChildrenArrayToObject(markReplace)); + // replace 的前面会加 test 操作,所以 get(0) 后要再 get(1) + String replaceKeyPath = objectMapper.readTree(replaceAAtoA).get(0).get(1).get("path").asText(); + String replaceNodePath = replaceKeyPath.substring(0, replaceKeyPath.lastIndexOf("/data")); + JsonNode replaceNode = markReplaceJson.at(replaceNodePath); + // replace 只会标记修改当前的节点,不会动子节点 + assertJsonObjectEquals("{'data':{'created':1623140763953,'background':'#409eff','id':'cby3dozxagw0','text':'A'},'childrenObject':{'cby3h1dtg4ff':{'data':{'created':1623141026009,'id':'cby3h1dtg4ff','text':'D'},'childrenObject':{},'order':0}},'order':0}", + objectMapper.writeValueAsString(replaceNode)); + + } + + @Test + public void testMarkMoveNode() throws IOException { + // 修改: move (需递归给 move 结果节点添加蓝色) + String moveBBaseAOnABCDPatch = MinderJsonPatchUtil.getContentPatch(jsonAddABCD, jsonMoveBBaseAOnABCD); + String markMove = MinderJsonPatchUtil.markJsonPatchOnMinderContent(moveBBaseAOnABCDPatch, jsonMoveBBaseABeforeDOnABCD); + + System.out.println("markMove: " + markMove); + JsonNode markMoveJson = objectMapper.readTree(MinderJsonPatchUtil.convertChildrenArrayToObject(markMove)); + String movePath = objectMapper.readTree(moveBBaseAOnABCDPatch).get(0).get("path").asText(); + JsonNode moveNode = markMoveJson.at(movePath); + assertJsonObjectEquals("{'data':{'created':1623141026007,'background':'#409eff','id':'cby3h1dtg4g0','text':'B'},'childrenObject':{'cby3h1dtg4g0':{'data':{'created':1623141026007,'background':'#409eff','id':'cby3h1dtg4g0','text':'C'},'childrenObject':{},'order':0}},'order':0}", + objectMapper.writeValueAsString(moveNode)); + } + + @Test + public void testCleanAllProgress() throws IOException { + String jsonWithProgress = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonWithProgress.json")), "utf-8"); + String jsonWithNoProgress = IOUtils.toString(Objects.requireNonNull(getClass().getClassLoader().getResource("MinderJsonPatchUtilTest/jsonWithNoProgress.json")), "utf-8"); + + String jsonAfterCleanProgress = MinderJsonPatchUtil.cleanAllProgress(jsonWithProgress); + + Assert.assertEquals(JSONObject.parseObject(jsonAfterCleanProgress), JSON.parseObject(jsonWithNoProgress)); + } + + + private void assertJsonObjectEquals(String expected, String actual) { + Assert.assertEquals(JSONObject.parseObject(expected), JSONObject.parseObject(actual)); + } + + private void assertJsonArrayEquals(String expected, String actual) { + Assert.assertEquals(JSONArray.parseArray(expected), JSONArray.parseArray(actual)); + } +} diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/json1LevelABE.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/json1LevelABE.json new file mode 100644 index 0000000..bca48da --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/json1LevelABE.json @@ -0,0 +1,41 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "A" + }, + "children": [ + ] + }, + { + "data": { + "id": "cby3h1dtg4g0", + "created": 1623141026007, + "text": "B" + }, + "children": [ + ] + }, + { + "data": { + "id": "cby3h1dtg411", + "created": 1623141026011, + "text": "E" + }, + "children": [ + ] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/json1LevelAE.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/json1LevelAE.json new file mode 100644 index 0000000..49a422f --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/json1LevelAE.json @@ -0,0 +1,32 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "A" + }, + "children": [ + ] + }, + { + "data": { + "id": "cby3h1dtg411", + "created": 1623141026011, + "text": "E" + }, + "children": [ + ] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/json1LevelAFE.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/json1LevelAFE.json new file mode 100644 index 0000000..a7808a6 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/json1LevelAFE.json @@ -0,0 +1,41 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "A" + }, + "children": [ + ] + }, + { + "data": { + "id": "cby3h1dtg422", + "created": 1623141026022, + "text": "F" + }, + "children": [ + ] + }, + { + "data": { + "id": "cby3h1dtg411", + "created": 1623141026011, + "text": "E" + }, + "children": [ + ] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddABCD.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddABCD.json new file mode 100644 index 0000000..5fde049 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddABCD.json @@ -0,0 +1,48 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "A" + }, + "children": [ + { + "data": { + "id": "cby3h1dtg4ff", + "created": 1623141026009, + "text": "D" + }, + "children": [] + } + ] + }, + { + "data": { + "id": "cby3h1dtg4g0", + "created": 1623141026007, + "text": "B" + }, + "children": [ + { + "data": { + "id": "cby3h1dtg4g0", + "created": 1623141026007, + "text": "C" + }, + "children": [] + } + ] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddNodeA.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddNodeA.json new file mode 100644 index 0000000..755e322 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddNodeA.json @@ -0,0 +1,22 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "A" + }, + "children": [] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddNodeB.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddNodeB.json new file mode 100644 index 0000000..efd64d3 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddNodeB.json @@ -0,0 +1,22 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3h1dtg4g0", + "created": 1623141026007, + "text": "B" + }, + "children": [] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddNodeCBaseNodeB.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddNodeCBaseNodeB.json new file mode 100644 index 0000000..9947284 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddNodeCBaseNodeB.json @@ -0,0 +1,31 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3h1dtg4g0", + "created": 1623141026007, + "text": "B" + }, + "children": [ + { + "data": { + "id": "cby3h1dtg4g0", + "created": 1623141026007, + "text": "C" + }, + "children": [] + } + ] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddNodeDBaseNodeA.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddNodeDBaseNodeA.json new file mode 100644 index 0000000..4fa6537 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddNodeDBaseNodeA.json @@ -0,0 +1,31 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "A" + }, + "children": [ + { + "data": { + "id": "cby3h1dtg4ff", + "created": 1623141026009, + "text": "D" + }, + "children": [] + } + ] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddPriorityOnABaseAD.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddPriorityOnABaseAD.json new file mode 100644 index 0000000..9996d0f --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonAddPriorityOnABaseAD.json @@ -0,0 +1,32 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "A", + "priority": 1, + }, + "children": [ + { + "data": { + "id": "cby3h1dtg4ff", + "created": 1623141026009, + "text": "D" + }, + "children": [] + } + ] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonChangeNodeAToAA.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonChangeNodeAToAA.json new file mode 100644 index 0000000..1aaf79a --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonChangeNodeAToAA.json @@ -0,0 +1,22 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "AA" + }, + "children": [] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonChangeNodeAToAAA.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonChangeNodeAToAAA.json new file mode 100644 index 0000000..f216fd4 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonChangeNodeAToAAA.json @@ -0,0 +1,22 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "AAA" + }, + "children": [] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonMoveBBaseABeforeDOnABCD.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonMoveBBaseABeforeDOnABCD.json new file mode 100644 index 0000000..4d336cd --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonMoveBBaseABeforeDOnABCD.json @@ -0,0 +1,48 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "A" + }, + "children": [ + { + "data": { + "id": "cby3h1dtg4g0", + "created": 1623141026007, + "text": "B" + }, + "children": [ + { + "data": { + "id": "cby3h1dtg4g0", + "created": 1623141026007, + "text": "C" + }, + "children": [] + } + ] + }, + { + "data": { + "id": "cby3h1dtg4ff", + "created": 1623141026009, + "text": "D" + }, + "children": [] + } + ] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonMoveBBaseAOnABCD.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonMoveBBaseAOnABCD.json new file mode 100644 index 0000000..ac9bc57 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonMoveBBaseAOnABCD.json @@ -0,0 +1,48 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "A" + }, + "children": [ + { + "data": { + "id": "cby3h1dtg4ff", + "created": 1623141026009, + "text": "D" + }, + "children": [] + }, + { + "data": { + "id": "cby3h1dtg4g0", + "created": 1623141026007, + "text": "B" + }, + "children": [ + { + "data": { + "id": "cby3h1dtg4g0", + "created": 1623141026007, + "text": "C" + }, + "children": [] + } + ] + } + ] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonNoNode.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonNoNode.json new file mode 100644 index 0000000..ac201aa --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonNoNode.json @@ -0,0 +1,13 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonNodeACollapse.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonNodeACollapse.json new file mode 100644 index 0000000..d61f0f8 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonNodeACollapse.json @@ -0,0 +1,23 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "A", + "expandState": "collapse" + }, + "children": [] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonNodeAExpand.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonNodeAExpand.json new file mode 100644 index 0000000..7848307 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonNodeAExpand.json @@ -0,0 +1,23 @@ +{ + "root": { + "data": { + "id": "bv8nxhi3c800", + "created": 1562059643204, + "text": "中心主题" + }, + "children": [ + { + "data": { + "id": "cby3dozxagw0", + "created": 1623140763953, + "text": "A", + "expandState": "expand" + }, + "children": [] + } + ] + }, + "template": "default", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonWithChildrenArray.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonWithChildrenArray.json new file mode 100644 index 0000000..8ff7cda --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonWithChildrenArray.json @@ -0,0 +1,111 @@ +{ + "template": "default", + "root": { + "data": { + "created": 1562059643204, + "id": "bv8nxhi3c800", + "text": "增量保存测试2" + }, + "children": [{ + "data": { + "expandState": "expand", + "resource": ["恒捷"], + "created": 1623332369626, + "id": "cbzzarfg4lc0", + "text": "恒捷编辑的" + }, + "children": [{ + "data": { + "resource": ["标签1", "恒捷"], + "created": 1623727000977, + "id": "cc3v6mkha2g0", + "text": "测试改动节点内容1", + "priority": 2 + }, + "children": [] + }, { + "data": { + "created": 1623727119617, + "id": "cc3v852kol40", + "text": "测试冲突用" + }, + "children": [{ + "data": { + "created": 1623727217697, + "id": "cc3v9e4mfgg0", + "text": "不冲突" + }, + "children": [] + }] + }] + }, { + "data": { + "expandState": "expand", + "resource": ["管理员"], + "created": 1623332380387, + "id": "cbzzawdetqo0", + "text": "管理员编辑的" + }, + "children": [{ + "data": { + "created": 1623332415676, + "id": "cbzzbcl10i80", + "text": "第一个模块" + }, + "children": [{ + "data": { + "created": 1623332415676, + "id": "cbzzbcl12vk0", + "text": "第1.2个模块" + }, + "children": [] + }] + }, { + "data": { + "created": 1623332415677, + "id": "cbzzbcl17h40", + "text": "第二个模块" + }, + "children": [{ + "data": { + "created": 1623332912093, + "id": "cbzzhomurvs0", + "text": "管理员在这随便改" + }, + "children": [] + }] + }, { + "data": { + "expandState": "expand", + "created": 1623332926596, + "id": "cbzzhvap80w0", + "text": "好像增量有点慢?" + }, + "children": [{ + "data": { + "created": 1623332971073, + "id": "cbzzifq9q0o0", + "text": "去掉大节点,看增量快不快?" + }, + "children": [] + }] + }, { + "data": { + "created": 1623722956260, + "id": "cc3tr0g8ulk0", + "text": "我也加一个" + }, + "children": [] + }, { + "data": { + "created": 1623723200621, + "id": "cc3tu4pj30o0", + "text": "我也再加" + }, + "children": [] + }] + }] + }, + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonWithChildrenObject.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonWithChildrenObject.json new file mode 100644 index 0000000..72d5991 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonWithChildrenObject.json @@ -0,0 +1,144 @@ +{ + "template": "default", + "root": { + "data": { + "created": 1562059643204, + "id": "bv8nxhi3c800", + "text": "增量保存测试2" + }, + "childrenObject": { + "cbzzarfg4lc0": { + "data": { + "expandState": "expand", + "resource": ["恒捷"], + "created": 1623332369626, + "id": "cbzzarfg4lc0", + "text": "恒捷编辑的" + }, + "childrenObject": { + "cc3v6mkha2g0": { + "data": { + "resource": ["标签1", "恒捷"], + "created": 1623727000977, + "id": "cc3v6mkha2g0", + "text": "测试改动节点内容1", + "priority": 2 + }, + "childrenObject": {}, + "order": 0 + }, + "cc3v852kol40": { + "data": { + "created": 1623727119617, + "id": "cc3v852kol40", + "text": "测试冲突用" + }, + "childrenObject": { + "cc3v9e4mfgg0": { + "data": { + "created": 1623727217697, + "id": "cc3v9e4mfgg0", + "text": "不冲突" + }, + "childrenObject": {}, + "order": 1 + } + }, + "order": 1 + } + }, + "order": 0 + }, + "cbzzawdetqo0": { + "data": { + "expandState": "expand", + "resource": ["管理员"], + "created": 1623332380387, + "id": "cbzzawdetqo0", + "text": "管理员编辑的" + }, + "childrenObject": { + "cbzzbcl17h40": { + "data": { + "created": 1623332415677, + "id": "cbzzbcl17h40", + "text": "第二个模块" + }, + "childrenObject": { + "cbzzhomurvs0": { + "data": { + "created": 1623332912093, + "id": "cbzzhomurvs0", + "text": "管理员在这随便改" + }, + "childrenObject": {}, + "order": 0 + } + }, + "order": 1 + }, + "cc3tr0g8ulk0": { + "data": { + "created": 1623722956260, + "id": "cc3tr0g8ulk0", + "text": "我也加一个" + }, + "childrenObject": {}, + "order": 3 + }, + "cc3tu4pj30o0": { + "data": { + "created": 1623723200621, + "id": "cc3tu4pj30o0", + "text": "我也再加" + }, + "childrenObject": {}, + "order": 4 + }, + "cbzzbcl10i80": { + "data": { + "created": 1623332415676, + "id": "cbzzbcl10i80", + "text": "第一个模块" + }, + "childrenObject": { + "cbzzbcl12vk0": { + "data": { + "created": 1623332415676, + "id": "cbzzbcl12vk0", + "text": "第1.2个模块" + }, + "childrenObject": {}, + "order": 0 + } + }, + "order": 0 + }, + "cbzzhvap80w0": { + "data": { + "expandState": "expand", + "created": 1623332926596, + "id": "cbzzhvap80w0", + "text": "好像增量有点慢?" + }, + "childrenObject": { + "cbzzifq9q0o0": { + "data": { + "created": 1623332971073, + "id": "cbzzifq9q0o0", + "text": "去掉大节点,看增量快不快?" + }, + "childrenObject": {}, + "order": 0 + } + }, + "order": 2 + } + }, + "order": 1 + } + } + }, + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonWithNoProgress.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonWithNoProgress.json new file mode 100644 index 0000000..58092a6 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonWithNoProgress.json @@ -0,0 +1,72 @@ +{ + "root": { + "data": { + "expandState": "expand", + "created": 1562059643204, + "id": "bv8nxhi3c800", + "text": "示例用例" + }, + "children": [{ + "data": { + "expandState": "expand", + "created": 1616069354364, + "id": "ca0grwa2aww0", + "text": "优先级" + }, + "children": [{ + "data": { + "expandState": "expand", + "created": 1608689668493, + "id": "c7zsw7e4hp40", + "text": "优先级P0", + "priority": 1 + }, + "children": [] + }, { + "data": { + "created": 1616069399281, + "id": "ca0gsgwwtt40", + "text": "优先级P1", + "priority": 2 + }, + "children": [] + }, { + "data": { + "created": 1616069411231, + "id": "ca0gsmejqy80", + "text": "优先级P2", + "priority": 3 + }, + "children": [] + }] + }, { + "data": { + "resource": ["自定义标签"], + "created": 1616069364633, + "id": "ca0gs0zwi940", + "text": "自定义标签" + }, + "children": [] + }, { + "data": { + "note": "我是备注", + "created": 1616069378505, + "id": "ca0gs7db36w0", + "text": "备注" + }, + "children": [] + }, { + "data": { + "hyperlink": "http://baidu.com", + "created": 1616069422262, + "id": "ca0gsrgywog0", + "text": "链接", + "hyperlinkTitle": "百度链接" + }, + "children": [] + }] + }, + "template": "structure", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file diff --git a/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonWithProgress.json b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonWithProgress.json new file mode 100644 index 0000000..c030a24 --- /dev/null +++ b/case-server/src/test/resources/MinderJsonPatchUtilTest/jsonWithProgress.json @@ -0,0 +1,77 @@ +{ + "root": { + "data": { + "expandState": "expand", + "created": 1562059643204, + "id": "bv8nxhi3c800", + "text": "示例用例" + }, + "children": [{ + "data": { + "expandState": "expand", + "created": 1616069354364, + "id": "ca0grwa2aww0", + "text": "优先级", + "progress": 9 + }, + "children": [{ + "data": { + "expandState": "expand", + "created": 1608689668493, + "progress": 9, + "id": "c7zsw7e4hp40", + "text": "优先级P0", + "priority": 1 + }, + "children": [] + }, { + "data": { + "created": 1616069399281, + "progress": 1, + "id": "ca0gsgwwtt40", + "text": "优先级P1", + "priority": 2 + }, + "children": [] + }, { + "data": { + "created": 1616069411231, + "progress": 5, + "id": "ca0gsmejqy80", + "text": "优先级P2", + "priority": 3 + }, + "children": [] + }] + }, { + "data": { + "resource": ["自定义标签"], + "created": 1616069364633, + "id": "ca0gs0zwi940", + "text": "自定义标签", + "progress": 1 + }, + "children": [] + }, { + "data": { + "note": "我是备注", + "created": 1616069378505, + "id": "ca0gs7db36w0", + "text": "备注" + }, + "children": [] + }, { + "data": { + "hyperlink": "http://baidu.com", + "created": 1616069422262, + "id": "ca0gsrgywog0", + "text": "链接", + "hyperlinkTitle": "百度链接" + }, + "children": [] + }] + }, + "template": "structure", + "theme": "fresh-blue", + "version": "1.4.43" +} \ No newline at end of file