From af785a1d4ea433ddad1aec8e89bfa4af1d866799 Mon Sep 17 00:00:00 2001 From: fit2-zhao Date: Sat, 9 Oct 2021 13:43:07 +0800 Subject: [PATCH] =?UTF-8?q?feat=20(=E6=8E=A5=E5=8F=A3=E5=AE=9A=E4=B9=89):?= =?UTF-8?q?=20=E6=93=8D=E4=BD=9C=E5=8E=86=E5=8F=B2=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../metersphere/log/aspect/MsLogAspect.java | 2 +- .../log/utils/ReflexObjectUtil.java | 52 +- .../log/utils/dff/ApiDefinitionDiffUtil.java | 266 +++++++++ .../log/utils/dff/CompatibilityFlags.java | 32 -- .../metersphere/log/utils/dff/Constants.java | 34 -- .../log/utils/dff/CopyingApplyProcessor.java | 16 - .../io/metersphere/log/utils/dff/Diff.java | 79 --- .../metersphere/log/utils/dff/DiffFlags.java | 82 --- .../log/utils/dff/InPlaceApplyProcessor.java | 164 ------ .../log/utils/dff/InternalUtils.java | 58 -- .../utils/dff/InvalidJsonPatchException.java | 27 - .../metersphere/log/utils/dff/JsonDiff.java | 512 ----------------- .../metersphere/log/utils/dff/JsonPatch.java | 143 ----- .../dff/JsonPatchApplicationException.java | 41 -- .../log/utils/dff/JsonPatchProcessor.java | 28 - .../log/utils/dff/JsonPointer.java | 346 ----------- .../dff/JsonPointerEvaluationException.java | 22 - .../metersphere/log/utils/dff/NodeType.java | 90 --- .../log/utils/dff/NoopProcessor.java | 37 -- .../metersphere/log/utils/dff/Operation.java | 66 --- .../log/utils/json/diff/ArrNode.java | 43 ++ .../log/utils/json/diff/DiffTest.java | 23 + .../log/utils/json/diff/GsonDiff.java | 17 + .../log/utils/json/diff/IncavaDiff.java | 500 ++++++++++++++++ .../log/utils/json/diff/IncavaEntry.java | 167 ++++++ .../log/utils/json/diff/Jackson2Diff.java | 11 + .../log/utils/json/diff/JacksonDiff.java | 11 + .../log/utils/json/diff/JsonDiff.java | 537 ++++++++++++++++++ .../metersphere/log/utils/json/diff/Leaf.java | 382 +++++++++++++ .../metersphere/log/utils/json/diff/Node.java | 46 ++ .../log/utils/json/diff/ObjNode.java | 42 ++ .../metersphere/log/utils/json/diff/Oper.java | 5 + .../metersphere/log/utils/json/diff/Root.java | 28 + .../log/utils/json/diff/Visitor.java | 21 + .../utils/json/diff/jsonwrap/JzonArray.java | 52 ++ .../utils/json/diff/jsonwrap/JzonElement.java | 35 ++ .../utils/json/diff/jsonwrap/JzonNull.java | 10 + .../utils/json/diff/jsonwrap/JzonObject.java | 60 ++ .../json/diff/jsonwrap/JzonPrimitive.java | 10 + .../log/utils/json/diff/jsonwrap/Wrapper.java | 38 ++ .../diff/jsonwrap/gson/GsonJsonArray.java | 75 +++ .../diff/jsonwrap/gson/GsonJsonElement.java | 65 +++ .../json/diff/jsonwrap/gson/GsonJsonNull.java | 20 + .../diff/jsonwrap/gson/GsonJsonObject.java | 98 ++++ .../diff/jsonwrap/gson/GsonJsonPrimitive.java | 14 + .../json/diff/jsonwrap/gson/GsonWrapper.java | 52 ++ .../jsonwrap/jackson/JacksonJsonArray.java | 48 ++ .../jsonwrap/jackson/JacksonJsonElement.java | 65 +++ .../jsonwrap/jackson/JacksonJsonNull.java | 20 + .../jsonwrap/jackson/JacksonJsonObject.java | 99 ++++ .../jackson/JacksonJsonPrimitive.java | 14 + .../diff/jsonwrap/jackson/JacksonWrapper.java | 65 +++ .../jackson/JacksonWrapperException.java | 22 + .../jsonwrap/jackson2/Jackson2JsonArray.java | 47 ++ .../jackson2/Jackson2JsonElement.java | 65 +++ .../jsonwrap/jackson2/Jackson2JsonNull.java | 20 + .../jsonwrap/jackson2/Jackson2JsonObject.java | 98 ++++ .../jackson2/Jackson2JsonPrimitive.java | 13 + .../jsonwrap/jackson2/Jackson2Wrapper.java | 64 +++ .../jackson2/Jackson2WrapperException.java | 22 + .../io/metersphere/log/vo/DetailColumn.java | 1 + .../log/vo/api/DefinitionReference.java | 8 + .../components/EditCompleteContainer.vue | 10 +- .../components/history/ChangeHistory.vue | 111 ++-- .../history/api/ApiDubboParameters.vue | 126 ++++ .../history/api/ApiHistoryDetail.vue | 218 +++++++ .../history/api/ApiHttpRequestParams.vue | 151 +++++ .../history/api/ApiJdbcParameters.vue | 100 ++++ .../history/api/ApiTcpParameters.vue | 187 ++++++ .../history/api/common/ApiKeyValueDetail.vue | 179 ++++++ .../history/api/json-view/ComparedEditor.vue | 89 +++ .../history/api/json-view/common.js | 32 ++ .../history/api/json-view/convert/convert.js | 202 +++++++ .../api/json-view/schema/editor/index.js | 7 + .../api/json-view/schema/editor/main.vue | 430 ++++++++++++++ .../api/json-view/schema/editor/type/array.js | 23 + .../json-view/schema/editor/type/boolean.js | 11 + .../json-view/schema/editor/type/integer.js | 40 ++ .../json-view/schema/editor/type/number.js | 32 ++ .../json-view/schema/editor/type/object.js | 19 + .../json-view/schema/editor/type/string.js | 43 ++ .../api/json-view/schema/editor/type/type.js | 17 + .../api/json-view/schema/editor/util.js | 10 + .../history/api/json-view/schema/index.js | 28 + .../components/history/tags/MsInputTag.vue | 194 +++++++ .../history/tags/TagsHistoryDetail.vue | 74 +++ frontend/src/business/main.js | 3 + 87 files changed, 5632 insertions(+), 1834 deletions(-) create mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/ApiDefinitionDiffUtil.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/CompatibilityFlags.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/Constants.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/CopyingApplyProcessor.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/Diff.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/DiffFlags.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/InPlaceApplyProcessor.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/InternalUtils.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/InvalidJsonPatchException.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/JsonDiff.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/JsonPatch.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/JsonPatchApplicationException.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/JsonPatchProcessor.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/JsonPointer.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/JsonPointerEvaluationException.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/NodeType.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/NoopProcessor.java delete mode 100644 backend/src/main/java/io/metersphere/log/utils/dff/Operation.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/ArrNode.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/DiffTest.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/GsonDiff.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/IncavaDiff.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/IncavaEntry.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/Jackson2Diff.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/JacksonDiff.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/JsonDiff.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/Leaf.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/Node.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/ObjNode.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/Oper.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/Root.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/Visitor.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonArray.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonElement.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonNull.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonObject.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonPrimitive.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/Wrapper.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonArray.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonElement.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonNull.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonObject.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonPrimitive.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonWrapper.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonArray.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonElement.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonNull.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonObject.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonPrimitive.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonWrapper.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonWrapperException.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonArray.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonElement.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonNull.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonObject.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonPrimitive.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2Wrapper.java create mode 100644 backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2WrapperException.java create mode 100644 frontend/src/business/components/history/api/ApiDubboParameters.vue create mode 100644 frontend/src/business/components/history/api/ApiHistoryDetail.vue create mode 100644 frontend/src/business/components/history/api/ApiHttpRequestParams.vue create mode 100644 frontend/src/business/components/history/api/ApiJdbcParameters.vue create mode 100644 frontend/src/business/components/history/api/ApiTcpParameters.vue create mode 100644 frontend/src/business/components/history/api/common/ApiKeyValueDetail.vue create mode 100644 frontend/src/business/components/history/api/json-view/ComparedEditor.vue create mode 100644 frontend/src/business/components/history/api/json-view/common.js create mode 100644 frontend/src/business/components/history/api/json-view/convert/convert.js create mode 100644 frontend/src/business/components/history/api/json-view/schema/editor/index.js create mode 100644 frontend/src/business/components/history/api/json-view/schema/editor/main.vue create mode 100644 frontend/src/business/components/history/api/json-view/schema/editor/type/array.js create mode 100644 frontend/src/business/components/history/api/json-view/schema/editor/type/boolean.js create mode 100644 frontend/src/business/components/history/api/json-view/schema/editor/type/integer.js create mode 100644 frontend/src/business/components/history/api/json-view/schema/editor/type/number.js create mode 100644 frontend/src/business/components/history/api/json-view/schema/editor/type/object.js create mode 100644 frontend/src/business/components/history/api/json-view/schema/editor/type/string.js create mode 100644 frontend/src/business/components/history/api/json-view/schema/editor/type/type.js create mode 100644 frontend/src/business/components/history/api/json-view/schema/editor/util.js create mode 100644 frontend/src/business/components/history/api/json-view/schema/index.js create mode 100644 frontend/src/business/components/history/tags/MsInputTag.vue create mode 100644 frontend/src/business/components/history/tags/TagsHistoryDetail.vue diff --git a/backend/src/main/java/io/metersphere/log/aspect/MsLogAspect.java b/backend/src/main/java/io/metersphere/log/aspect/MsLogAspect.java index 5a15807388..96b45012bc 100644 --- a/backend/src/main/java/io/metersphere/log/aspect/MsLogAspect.java +++ b/backend/src/main/java/io/metersphere/log/aspect/MsLogAspect.java @@ -201,7 +201,7 @@ public class MsLogAspect { } if (StringUtils.isNotEmpty(content) && StringUtils.isNotEmpty(msLog.beforeValue())) { OperatingLogDetails details = JSON.parseObject(content, OperatingLogDetails.class); - List columns = ReflexObjectUtil.compared(JSON.parseObject(msLog.beforeValue(), OperatingLogDetails.class), details); + List columns = ReflexObjectUtil.compared(JSON.parseObject(msLog.beforeValue(), OperatingLogDetails.class), details,msLog.module()); details.setColumns(columns); msOperLog.setOperContent(JSON.toJSONString(details)); msOperLog.setSourceId(details.getSourceId()); diff --git a/backend/src/main/java/io/metersphere/log/utils/ReflexObjectUtil.java b/backend/src/main/java/io/metersphere/log/utils/ReflexObjectUtil.java index 2fec6a1d36..c60d8c6a7d 100644 --- a/backend/src/main/java/io/metersphere/log/utils/ReflexObjectUtil.java +++ b/backend/src/main/java/io/metersphere/log/utils/ReflexObjectUtil.java @@ -7,6 +7,8 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; import io.metersphere.commons.utils.BeanUtils; import io.metersphere.commons.utils.LogUtil; +import io.metersphere.log.utils.dff.ApiDefinitionDiffUtil; +import io.metersphere.log.utils.json.diff.GsonDiff; import io.metersphere.log.vo.DetailColumn; import io.metersphere.log.vo.OperatingLogDetails; import io.metersphere.log.vo.StatusReference; @@ -76,6 +78,39 @@ public class ReflexObjectUtil { return columnList; } + public static List getColumns(Object obj) { + List columnList = new LinkedList<>(); + if (obj == null) { + return columnList; + } + // 得到类对象 + Class clazz = obj.getClass(); + // 得到类中的所有属性集合 + List fields = new LinkedList<>(); + // 遍历所有父类字节码对象 + while (clazz != null) { + // 获取字节码对象的属性对象数组 + Field[] declaredFields = clazz.getDeclaredFields(); + fields.add(declaredFields); + // 获得父类的字节码对象 + clazz = clazz.getSuperclass(); + } + for (Field[] fs : fields) { + for (int i = 0; i < fs.length; i++) { + Field f = fs[i]; + f.setAccessible(true); + try { + Object val = f.get(obj); + DetailColumn column = new DetailColumn(f.getName(), f.getName(), val, ""); + columnList.add(column); + } catch (Exception e) { + LogUtil.error(e); + } + } + } + return columnList; + } + public static boolean isJsonArray(String content) { try { JSONArray array = JSON.parseArray(content); @@ -96,7 +131,7 @@ public class ReflexObjectUtil { })); } - public static List compared(OperatingLogDetails obj, OperatingLogDetails newObj) { + public static List compared(OperatingLogDetails obj, OperatingLogDetails newObj, String module) { List comparedColumns = new LinkedList<>(); try { if (obj != null && newObj != null) { @@ -116,10 +151,23 @@ public class ReflexObjectUtil { if (StringUtils.isEmpty(JSON.toJSONString(originalColumns.get(i).getOriginalValue())) && StringUtils.isEmpty(JSON.toJSONString(newColumns.get(i).getOriginalValue()))) { continue; } - // 深度对比 DetailColumn column = new DetailColumn(); BeanUtils.copyBean(column, originalColumns.get(i)); column.setNewValue(newColumns.get(i).getOriginalValue()); + if (originalColumns.get(i).getColumnName().equals("tags")) { + GsonDiff diff = new GsonDiff(); + String oldTags = "{\"root\":" + originalColumns.get(i).getOriginalValue().toString() + "}"; + String newTags = "{\"root\":" + newColumns.get(i).getOriginalValue().toString() + "}"; + String diffStr = diff.diff(oldTags, newTags); + String diffValue = diff.apply(newTags, diffStr); + column.setDiffValue(diffValue); + } + // 深度对比 + else if (StringUtils.equals(module, "api_definition")) { + String newValue = newColumns.get(i).getOriginalValue().toString(); + String oldValue = column.getOriginalValue().toString(); + column.setDiffValue(ApiDefinitionDiffUtil.diff(newValue, oldValue)); + } comparedColumns.add(column); } } diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/ApiDefinitionDiffUtil.java b/backend/src/main/java/io/metersphere/log/utils/dff/ApiDefinitionDiffUtil.java new file mode 100644 index 0000000000..d68ec7352d --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/dff/ApiDefinitionDiffUtil.java @@ -0,0 +1,266 @@ +package io.metersphere.log.utils.dff; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import io.metersphere.api.dto.definition.request.sampler.MsDubboSampler; +import io.metersphere.api.dto.definition.request.sampler.MsHTTPSamplerProxy; +import io.metersphere.api.dto.definition.request.sampler.MsJDBCSampler; +import io.metersphere.api.dto.definition.request.sampler.MsTCPSampler; +import io.metersphere.log.utils.ReflexObjectUtil; +import io.metersphere.log.utils.json.diff.JacksonDiff; +import io.metersphere.log.utils.json.diff.JsonDiff; +import io.metersphere.log.vo.DetailColumn; +import io.metersphere.log.vo.OperatingLogDetails; +import io.metersphere.log.vo.api.DefinitionReference; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ApiDefinitionDiffUtil { + static final String JSON_START = "{\"root\":"; + static final String JSON_END = "}"; + + public static String diff(String newValue, String oldValue) { + try { + JSONObject bloBsNew = JSON.parseObject(newValue); + JSONObject bloBsOld = JSON.parseObject(oldValue); + if (bloBsNew == null || StringUtils.isEmpty(bloBsNew.getString("type"))) { + return null; + } + Map diffMap = new LinkedHashMap<>(); + diffMap.put("type", bloBsNew.getString("type")); + JsonDiff jsonDiff = new JacksonDiff(); + if (bloBsNew.getString("type").equals("TCPSampler")) { + MsTCPSampler tcpSamplerNew = bloBsNew.toJavaObject(MsTCPSampler.class); + MsTCPSampler tcpSamplerOld = bloBsOld.toJavaObject(MsTCPSampler.class); + diffTcp(tcpSamplerNew, tcpSamplerOld, jsonDiff, diffMap); + } else if (bloBsNew.getString("type").equals("HTTPSamplerProxy")) { + MsHTTPSamplerProxy httpSamplerProxyNew = bloBsNew.toJavaObject(MsHTTPSamplerProxy.class); + MsHTTPSamplerProxy httpSamplerProxyOld = bloBsOld.toJavaObject(MsHTTPSamplerProxy.class); + diffHttp(httpSamplerProxyNew, httpSamplerProxyOld, jsonDiff, diffMap); + } else if (bloBsNew.getString("type").equals("JDBCSampler")) { + MsJDBCSampler jdbcSamplerNew = bloBsNew.toJavaObject(MsJDBCSampler.class); + MsJDBCSampler jdbcSamplerOld = bloBsOld.toJavaObject(MsJDBCSampler.class); + diffJdbc(jdbcSamplerNew, jdbcSamplerOld, jsonDiff, diffMap); + } else { + MsDubboSampler dubboSamplerNew = bloBsNew.toJavaObject(MsDubboSampler.class); + MsDubboSampler dubboSamplerOld = bloBsOld.toJavaObject(MsDubboSampler.class); + diffDubbo(dubboSamplerNew, dubboSamplerOld, jsonDiff, diffMap); + } + return JSON.toJSONString(diffMap); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private static void diffHttp(MsHTTPSamplerProxy httpNew, MsHTTPSamplerProxy httpOld, JsonDiff jsonDiff, Map diffMap) { + // 请求头对比 old/new + if (CollectionUtils.isNotEmpty(httpNew.getHeaders())) { + httpNew.getHeaders().remove(httpNew.getHeaders().size() - 1); + httpOld.getHeaders().remove(httpOld.getHeaders().size() - 1); + } + String headerNew = JSON_START + JSON.toJSONString(httpNew.getHeaders()) + JSON_END; + String headerOld = JSON_START + JSON.toJSONString(httpOld.getHeaders()) + JSON_END; + if (!StringUtils.equals(headerNew, headerOld)) { + String patch = jsonDiff.diff(headerOld, headerNew); + String diffPatch = jsonDiff.apply(headerNew, patch); + if (StringUtils.isNotEmpty(diffPatch)) { + diffMap.put("header", diffPatch); + } + } + // 对比QUERY参数 + if (CollectionUtils.isNotEmpty(httpNew.getArguments())) { + httpNew.getArguments().remove(httpNew.getArguments().size() - 1); + httpOld.getArguments().remove(httpOld.getArguments().size() - 1); + } + String queryNew = JSON_START + JSON.toJSONString(httpNew.getArguments()) + JSON_END; + String queryOld = JSON_START + JSON.toJSONString(httpOld.getArguments()) + JSON_END; + if (!StringUtils.equals(queryNew, queryOld)) { + String patch = jsonDiff.diff(queryOld, queryNew); + String diff = jsonDiff.apply(queryNew, patch); + if (StringUtils.isNotEmpty(diff)) { + diffMap.put("query", diff); + } + } + // 对比REST参数 + if (CollectionUtils.isNotEmpty(httpNew.getRest())) { + httpNew.getRest().remove(httpNew.getRest().size() - 1); + httpOld.getRest().remove(httpOld.getRest().size() - 1); + } + String restNew = JSON_START + JSON.toJSONString(httpNew.getRest()) + JSON_END; + String restOld = JSON_START + JSON.toJSONString(httpOld.getRest()) + JSON_END; + if (!StringUtils.equals(restNew, restOld)) { + String patch = jsonDiff.diff(restOld, restNew); + String diff = jsonDiff.apply(restNew, patch); + if (StringUtils.isNotEmpty(diff)) { + diffMap.put("rest", diff); + } + } + // 对比BODY-JSON参数 + if (httpNew.getBody() != null) { + String bodyNew = JSON.toJSONString(httpNew.getBody()); + String bodyOld = JSON.toJSONString(httpOld.getBody()); + if (!StringUtils.equals(bodyNew, bodyOld)) { + String patch = jsonDiff.diff(bodyOld, bodyNew); + String diff = jsonDiff.apply(bodyNew, patch); + if (StringUtils.isNotEmpty(diff)) { + diffMap.put("body", diff); + } + } + // 对比BODY-FORM参数 + if (CollectionUtils.isNotEmpty(httpNew.getBody().getKvs())) { + httpNew.getBody().getKvs().remove(httpNew.getBody().getKvs().size() - 1); + httpOld.getBody().getKvs().remove(httpOld.getBody().getKvs().size() - 1); + } + String bodyFormNew = JSON_START + JSON.toJSONString(httpNew.getBody().getKvs()) + JSON_END; + String bodyFormOld = JSON_START + JSON.toJSONString(httpOld.getBody().getKvs()) + JSON_END; + if (!StringUtils.equals(bodyFormNew, bodyFormOld)) { + String patch = jsonDiff.diff(bodyFormOld, bodyFormNew); + String diff = jsonDiff.apply(bodyFormNew, patch); + if (StringUtils.isNotEmpty(diff)) { + diffMap.put("body_form", diff); + } + } + // 对比BODY-XML参数 + if (!StringUtils.equals(httpNew.getBody().getRaw(), httpOld.getBody().getRaw())) { + diffMap.put("body_raw_1", httpNew.getBody().getRaw()); + diffMap.put("body_raw_2", httpOld.getBody().getRaw()); + } + + } + } + + private static void diffTcp(MsTCPSampler tcpNew, MsTCPSampler tcpOld, JsonDiff jsonDiff, Map diffMap) { + // 对比请求参数 + if (CollectionUtils.isNotEmpty(tcpNew.getParameters())) { + tcpNew.getParameters().remove(tcpNew.getParameters().size() - 1); + tcpOld.getParameters().remove(tcpOld.getParameters().size() - 1); + } + String queryNew = JSON_START + JSON.toJSONString(tcpNew.getParameters()) + JSON_END; + String queryOld = JSON_START + JSON.toJSONString(tcpOld.getParameters()) + JSON_END; + if (!StringUtils.equals(queryNew, queryOld)) { + String patch = jsonDiff.diff(queryOld, queryNew); + String diff = jsonDiff.apply(queryNew, patch); + if (StringUtils.isNotEmpty(diff)) { + diffMap.put("query", diff); + } + } + // 对比BODY-JSON参数 + if (!StringUtils.equals(tcpNew.getJsonDataStruct(), tcpOld.getJsonDataStruct())) { + String patch = jsonDiff.diff(tcpOld.getJsonDataStruct(), tcpNew.getJsonDataStruct()); + String diff = jsonDiff.apply(tcpNew.getJsonDataStruct(), patch); + if (StringUtils.isNotEmpty(diff)) { + diffMap.put("body_json", diff); + } + } + // 对比BODY-XML参数 + String xmlNew = JSON_START + JSON.toJSONString(tcpNew.getXmlDataStruct()) + JSON_END; + String xmlOld = JSON_START + JSON.toJSONString(tcpOld.getXmlDataStruct()) + JSON_END; + if (!StringUtils.equals(xmlNew, xmlOld)) { + diffMap.put("body_xml_1", JSON.toJSONString(tcpNew.getXmlDataStruct())); + diffMap.put("body_xml_2", JSON.toJSONString(tcpOld.getXmlDataStruct())); + String patch = jsonDiff.diff(xmlOld, xmlNew); + String diffPatch = jsonDiff.apply(xmlNew, patch); + if (StringUtils.isNotEmpty(diffPatch)) { + diffMap.put("body_xml", diffPatch); + } + } + // 对比BODY-RAW参数 + if (!StringUtils.equals(tcpNew.getRawDataStruct(), tcpOld.getRawDataStruct())) { + diffMap.put("body_raw_1", tcpNew.getRawDataStruct()); + diffMap.put("body_raw_2", tcpOld.getRawDataStruct()); + } + // 对比pre参数 + if (tcpNew.getTcpPreProcessor() != null && !StringUtils.equals(tcpNew.getTcpPreProcessor().getScript(), tcpOld.getTcpPreProcessor().getScript())) { + diffMap.put("script_1", tcpNew.getTcpPreProcessor().getScript()); + diffMap.put("script_2", tcpOld.getTcpPreProcessor().getScript()); + } + } + + private static List getColumn(List columnsNew, List columnsOld) { + OperatingLogDetails detailsNew = new OperatingLogDetails(); + detailsNew.setColumns(columnsNew); + OperatingLogDetails detailsOld = new OperatingLogDetails(); + detailsOld.setColumns(columnsOld); + List diffColumns = ReflexObjectUtil.compared(detailsOld, detailsNew, ""); + return diffColumns; + } + + private static void diffJdbc(MsJDBCSampler jdbcNew, MsJDBCSampler jdbcOld, JsonDiff jsonDiff, Map diffMap) { + // 基础参数对比 + List columns = ReflexObjectUtil.getColumns(jdbcNew, DefinitionReference.jdbcColumns); + List columnsOld = ReflexObjectUtil.getColumns(jdbcOld, DefinitionReference.jdbcColumns); + List diffColumns = getColumn(columns, columnsOld); + if (CollectionUtils.isNotEmpty(diffColumns)) { + diffMap.put("base", JSON.toJSONString(diffColumns)); + } + // 自定义变量对比 + if (CollectionUtils.isNotEmpty(jdbcNew.getVariables())) { + jdbcNew.getVariables().remove(jdbcNew.getVariables().size() - 1); + jdbcOld.getVariables().remove(jdbcOld.getVariables().size() - 1); + } + String variablesNew = JSON_START + JSON.toJSONString(jdbcNew.getVariables()) + JSON_END; + String variablesOld = JSON_START + JSON.toJSONString(jdbcOld.getVariables()) + JSON_END; + if (!StringUtils.equals(variablesNew, variablesOld)) { + String patch = jsonDiff.diff(variablesOld, variablesNew); + String diffPatch = jsonDiff.apply(variablesNew, patch); + if (StringUtils.isNotEmpty(diffPatch)) { + diffMap.put("variables", diffPatch); + } + } + if (!StringUtils.equals(jdbcNew.getQuery(), jdbcOld.getQuery())) { + diffMap.put("query_1", jdbcNew.getQuery()); + diffMap.put("query_2", jdbcOld.getQuery()); + } + } + + private static void diffDubbo(MsDubboSampler dubboNew, MsDubboSampler dubboOld, JsonDiff jsonDiff, Map diffMap) { + // Config对比 + List diffColumns = getColumn(ReflexObjectUtil.getColumns(dubboNew.getConfigCenter()), ReflexObjectUtil.getColumns(dubboOld.getConfigCenter())); + if (CollectionUtils.isNotEmpty(diffColumns)) { + diffMap.put("config", JSON.toJSONString(diffColumns)); + } + // Registry对比 + List registryColumns = getColumn(ReflexObjectUtil.getColumns(dubboNew.getRegistryCenter()), ReflexObjectUtil.getColumns(dubboOld.getRegistryCenter())); + if (CollectionUtils.isNotEmpty(registryColumns)) { + diffMap.put("registry", JSON.toJSONString(registryColumns)); + } + // service对比 + List serviceColumns = getColumn(ReflexObjectUtil.getColumns(dubboNew.getConsumerAndService()), ReflexObjectUtil.getColumns(dubboOld.getConsumerAndService())); + if (CollectionUtils.isNotEmpty(serviceColumns)) { + diffMap.put("service", JSON.toJSONString(serviceColumns)); + } + // 对比Args参数 + if (CollectionUtils.isNotEmpty(dubboNew.getArgs())) { + dubboNew.getArgs().remove(dubboNew.getArgs().size() - 1); + dubboOld.getArgs().remove(dubboOld.getArgs().size() - 1); + } + String argsNew = JSON_START + JSON.toJSONString(dubboNew.getArgs()) + JSON_END; + String argsOld = JSON_START + JSON.toJSONString(dubboOld.getArgs()) + JSON_END; + if (!StringUtils.equals(argsNew, argsOld)) { + String patch = jsonDiff.diff(argsOld, argsNew); + String diffPatch = jsonDiff.apply(argsNew, patch); + if (StringUtils.isNotEmpty(diffPatch)) { + diffMap.put("args", diffPatch); + } + } + // 对比Attachment参数 + if (CollectionUtils.isNotEmpty(dubboNew.getAttachmentArgs())) { + dubboNew.getAttachmentArgs().remove(dubboNew.getAttachmentArgs().size() - 1); + dubboOld.getAttachmentArgs().remove(dubboOld.getAttachmentArgs().size() - 1); + } + String attachmentNew = JSON_START + JSON.toJSONString(dubboNew.getAttachmentArgs()) + JSON_END; + String attachmentOld = JSON_START + JSON.toJSONString(dubboOld.getAttachmentArgs()) + JSON_END; + if (!StringUtils.equals(attachmentNew, attachmentOld)) { + String patch = jsonDiff.diff(attachmentOld, attachmentNew); + String diffPatch = jsonDiff.apply(attachmentNew, patch); + if (StringUtils.isNotEmpty(diffPatch)) { + diffMap.put("attachment", diffPatch); + } + } + } +} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/CompatibilityFlags.java b/backend/src/main/java/io/metersphere/log/utils/dff/CompatibilityFlags.java deleted file mode 100644 index ec13f21ea8..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/CompatibilityFlags.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2016 flipkart.com zjsonpatch. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -package io.metersphere.log.utils.dff; - -import java.util.EnumSet; - -/** - * Created by tomerga on 04/09/2016. - */ -public enum CompatibilityFlags { - MISSING_VALUES_AS_NULLS, - REMOVE_NONE_EXISTING_ARRAY_ELEMENT, - ALLOW_MISSING_TARGET_OBJECT_ON_REPLACE; - - public static EnumSet defaults() { - return EnumSet.noneOf(CompatibilityFlags.class); - } -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/Constants.java b/backend/src/main/java/io/metersphere/log/utils/dff/Constants.java deleted file mode 100644 index c7b60936be..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/Constants.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016 flipkart.com zjsonpatch. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -package io.metersphere.log.utils.dff; - -/** - * Created with IntelliJ IDEA. - * User: gopi.vishwakarma - * Date: 10/07/15 - * Time: 10:35 AM - */ -final class Constants { - public static final String OP = "op"; - public static final String VALUE = "value"; - public static final String PATH = "path"; - public static final String FROM = "from"; - public static final String FROM_VALUE = "fromValue"; - - private Constants() {} - -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/CopyingApplyProcessor.java b/backend/src/main/java/io/metersphere/log/utils/dff/CopyingApplyProcessor.java deleted file mode 100644 index f9d13032e6..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/CopyingApplyProcessor.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.metersphere.log.utils.dff; - -import com.fasterxml.jackson.databind.JsonNode; - -import java.util.EnumSet; - -class CopyingApplyProcessor extends InPlaceApplyProcessor { - - CopyingApplyProcessor(JsonNode target) { - this(target, CompatibilityFlags.defaults()); - } - - CopyingApplyProcessor(JsonNode target, EnumSet flags) { - super(target.deepCopy(), flags); - } -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/Diff.java b/backend/src/main/java/io/metersphere/log/utils/dff/Diff.java deleted file mode 100644 index 9cf136f438..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/Diff.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2016 flipkart.com zjsonpatch. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.metersphere.log.utils.dff; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - */ -public class Diff { - private final Operation operation; - private final JsonPointer path; - private final JsonNode value; - private JsonPointer toPath; - private final JsonNode srcValue; - - Diff(Operation operation, JsonPointer path, JsonNode value) { - this.operation = operation; - this.path = path; - this.value = value; - this.srcValue = null; - } - - Diff(Operation operation, JsonPointer fromPath, JsonPointer toPath) { - this.operation = operation; - this.path = fromPath; - this.toPath = toPath; - this.value = null; - this.srcValue = null; - } - - Diff(Operation operation, JsonPointer path, JsonNode srcValue, JsonNode value) { - this.operation = operation; - this.path = path; - this.value = value; - this.srcValue = srcValue; - } - - public Operation getOperation() { - return operation; - } - - public JsonPointer getPath() { - return path; - } - - public JsonNode getValue() { - return value; - } - - public static Diff generateDiff(Operation replace, JsonPointer path, JsonNode target) { - return new Diff(replace, path, target); - } - - public static Diff generateDiff(Operation replace, JsonPointer path, JsonNode source, JsonNode target) { - return new Diff(replace, path, source, target); - } - - JsonPointer getToPath() { - return toPath; - } - - public JsonNode getSrcValue() { - return srcValue; - } -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/DiffFlags.java b/backend/src/main/java/io/metersphere/log/utils/dff/DiffFlags.java deleted file mode 100644 index dbc3217c01..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/DiffFlags.java +++ /dev/null @@ -1,82 +0,0 @@ -package io.metersphere.log.utils.dff; - -import java.util.EnumSet; - -public enum DiffFlags { - - /** - * This flag omits the value field on remove operations. - * This is a default flag. - */ - OMIT_VALUE_ON_REMOVE, - - /** - * This flag omits all {@link Operation#MOVE} operations, leaving only - * {@link Operation#ADD}, {@link Operation#REMOVE}, {@link Operation#REPLACE} - * and {@link Operation#COPY} operations. In other words, without this flag, - * {@link Operation#ADD} and {@link Operation#REMOVE} operations are not normalized - * into {@link Operation#MOVE} operations. - */ - OMIT_MOVE_OPERATION, - - /** - * This flag omits all {@link Operation#COPY} operations, leaving only - * {@link Operation#ADD}, {@link Operation#REMOVE}, {@link Operation#REPLACE} - * and {@link Operation#MOVE} operations. In other words, without this flag, - * {@link Operation#ADD} operations are not normalized into {@link Operation#COPY} - * operations. - */ - OMIT_COPY_OPERATION, - - /** - * This flag adds a fromValue field to all {@link Operation#REPLACE} operations. - * fromValue represents the the value replaced by a {@link Operation#REPLACE} - * operation, in other words, the original value. This can be useful for debugging - * output or custom processing of the diffs by downstream systems. - * Please note that this is a non-standard extension to RFC 6902 and will not affect - * how patches produced by this library are processed by this or other libraries. - * - * @since 0.4.1 - */ - ADD_ORIGINAL_VALUE_ON_REPLACE, - - /** - * This flag normalizes a {@link Operation#REPLACE} operation into its respective - * {@link Operation#REMOVE} and {@link Operation#ADD} operations. Although it adds - * a redundant step, this can be useful for auditing systems in which immutability - * is a requirement. - *

- * For the flag to work, {@link DiffFlags#ADD_ORIGINAL_VALUE_ON_REPLACE} has to be - * enabled as the new instructions in the patch need to grab the old fromValue - * {@code "op": "replace", "fromValue": "F1", "value": "F2" } - * The above instruction will be split into - * {@code "op":"remove", "value":"F1" } and {@code "op":"add", "value":"F2"} respectively. - *

- * Please note that this is a non-standard extension to RFC 6902 and will not affect - * how patches produced by this library are processed by this or other libraries. - * - * @since 0.4.11 - */ - ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE, - - /** - * This flag instructs the diff generator to emit {@link Operation#TEST} operations - * that validate the state of the source document before each mutation. This can be - * useful if you want to ensure data integrity prior to applying the patch. - * The resulting patches are standard per RFC 6902 and should be processed correctly - * by any compliant library; due to the associated space and performance costs, - * however, this isn't default behavior. - * - * @since 0.4.8 - */ - EMIT_TEST_OPERATIONS; - - - public static EnumSet defaults() { - return EnumSet.of(OMIT_VALUE_ON_REMOVE); - } - - public static EnumSet dontNormalizeOpIntoMoveAndCopy() { - return EnumSet.of(OMIT_MOVE_OPERATION, OMIT_COPY_OPERATION); - } -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/InPlaceApplyProcessor.java b/backend/src/main/java/io/metersphere/log/utils/dff/InPlaceApplyProcessor.java deleted file mode 100644 index 474b045a1b..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/InPlaceApplyProcessor.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2016 flipkart.com zjsonpatch. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.metersphere.log.utils.dff; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import java.util.EnumSet; - -class InPlaceApplyProcessor implements JsonPatchProcessor { - - private JsonNode target; - private EnumSet flags; - - InPlaceApplyProcessor(JsonNode target) { - this(target, CompatibilityFlags.defaults()); - } - - InPlaceApplyProcessor(JsonNode target, EnumSet flags) { - this.target = target; - this.flags = flags; - } - - public JsonNode result() { - return target; - } - - @Override - public void move(JsonPointer fromPath, JsonPointer toPath) throws JsonPointerEvaluationException { - JsonNode valueNode = fromPath.evaluate(target); - remove(fromPath); - set(toPath, valueNode, Operation.MOVE); - } - - @Override - public void copy(JsonPointer fromPath, JsonPointer toPath) throws JsonPointerEvaluationException { - JsonNode valueNode = fromPath.evaluate(target); - JsonNode valueToCopy = valueNode != null ? valueNode.deepCopy() : null; - set(toPath, valueToCopy, Operation.COPY); - } - - private static String show(JsonNode value) { - if (value == null || value.isNull()) - return "null"; - else if (value.isArray()) - return "array"; - else if (value.isObject()) - return "object"; - else - return "value " + value.toString(); // Caveat: numeric may differ from source (e.g. trailing zeros) - } - - @Override - public void test(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException { - JsonNode valueNode = path.evaluate(target); - if (!valueNode.equals(value)) - throw new JsonPatchApplicationException( - "Expected " + show(value) + " but found " + show(valueNode), Operation.TEST, path); - } - - @Override - public void add(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException { - set(path, value, Operation.ADD); - } - - @Override - public void replace(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException { - if (path.isRoot()) { - target = value; - return; - } - - JsonNode parentNode = path.getParent().evaluate(target); - JsonPointer.RefToken token = path.last(); - if (parentNode.isObject()) { - if (!flags.contains(CompatibilityFlags.ALLOW_MISSING_TARGET_OBJECT_ON_REPLACE) && - !parentNode.has(token.getField())) - throw new JsonPatchApplicationException( - "Missing field \"" + token.getField() + "\"", Operation.REPLACE, path.getParent()); - ((ObjectNode) parentNode).replace(token.getField(), value); - } else if (parentNode.isArray()) { - if (token.getIndex() >= parentNode.size()) - throw new JsonPatchApplicationException( - "Array index " + token.getIndex() + " out of bounds", Operation.REPLACE, path.getParent()); - ((ArrayNode) parentNode).set(token.getIndex(), value); - } else { - throw new JsonPatchApplicationException( - "Can't reference past scalar value", Operation.REPLACE, path.getParent()); - } - } - - @Override - public void remove(JsonPointer path) throws JsonPointerEvaluationException { - if (path.isRoot()) - throw new JsonPatchApplicationException("Cannot remove document root", Operation.REMOVE, path); - - JsonNode parentNode = path.getParent().evaluate(target); - JsonPointer.RefToken token = path.last(); - if (parentNode.isObject()) - ((ObjectNode) parentNode).remove(token.getField()); - else if (parentNode.isArray()) { - if (!flags.contains(CompatibilityFlags.REMOVE_NONE_EXISTING_ARRAY_ELEMENT) && - token.getIndex() >= parentNode.size()) - throw new JsonPatchApplicationException( - "Array index " + token.getIndex() + " out of bounds", Operation.REPLACE, path.getParent()); - ((ArrayNode) parentNode).remove(token.getIndex()); - } else { - throw new JsonPatchApplicationException( - "Cannot reference past scalar value", Operation.REPLACE, path.getParent()); - } - } - - - - private void set(JsonPointer path, JsonNode value, Operation forOp) throws JsonPointerEvaluationException { - if (path.isRoot()) - target = value; - else { - JsonNode parentNode = path.getParent().evaluate(target); - if (!parentNode.isContainerNode()) - throw new JsonPatchApplicationException("Cannot reference past scalar value", forOp, path.getParent()); - else if (parentNode.isArray()) - addToArray(path, value, parentNode); - else - addToObject(path, parentNode, value); - } - } - - private void addToObject(JsonPointer path, JsonNode node, JsonNode value) { - final ObjectNode target = (ObjectNode) node; - String key = path.last().getField(); - target.set(key, value); - } - - private void addToArray(JsonPointer path, JsonNode value, JsonNode parentNode) { - final ArrayNode target = (ArrayNode) parentNode; - int idx = path.last().getIndex(); - - if (idx == JsonPointer.LAST_INDEX) { - // see http://tools.ietf.org/html/rfc6902#section-4.1 - target.add(value); - } else { - if (idx > target.size()) - throw new JsonPatchApplicationException( - "Array index " + idx + " out of bounds", Operation.ADD, path.getParent()); - target.insert(idx, value); - } - } -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/InternalUtils.java b/backend/src/main/java/io/metersphere/log/utils/dff/InternalUtils.java deleted file mode 100644 index 2eda852d3e..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/InternalUtils.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.metersphere.log.utils.dff; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -class InternalUtils { - - static List toList(ArrayNode input) { - int size = input.size(); - List toReturn = new ArrayList(size); - for (int i = 0; i < size; i++) { - toReturn.add(input.get(i)); - } - return toReturn; - } - - static List longestCommonSubsequence(final List a, final List b) { - if (a == null || b == null) { - throw new NullPointerException("List must not be null for longestCommonSubsequence"); - } - - List toReturn = new LinkedList(); - - int aSize = a.size(); - int bSize = b.size(); - int temp[][] = new int[aSize + 1][bSize + 1]; - - for (int i = 1; i <= aSize; i++) { - for (int j = 1; j <= bSize; j++) { - if (i == 0 || j == 0) { - temp[i][j] = 0; - } else if (a.get(i - 1).equals(b.get(j - 1))) { - temp[i][j] = temp[i - 1][j - 1] + 1; - } else { - temp[i][j] = Math.max(temp[i][j - 1], temp[i - 1][j]); - } - } - } - int i = aSize, j = bSize; - while (i > 0 && j > 0) { - if (a.get(i - 1).equals(b.get(j - 1))) { - toReturn.add(a.get(i - 1)); - i--; - j--; - } else if (temp[i - 1][j] > temp[i][j - 1]) - i--; - else - j--; - } - Collections.reverse(toReturn); - return toReturn; - } -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/InvalidJsonPatchException.java b/backend/src/main/java/io/metersphere/log/utils/dff/InvalidJsonPatchException.java deleted file mode 100644 index a1df5cc64a..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/InvalidJsonPatchException.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2016 flipkart.com zjsonpatch. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -package io.metersphere.log.utils.dff; - -/** - * User: holograph - * Date: 03/08/16 - */ -public class InvalidJsonPatchException extends JsonPatchApplicationException { - public InvalidJsonPatchException(String message) { - super(message, null, null); - } -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/JsonDiff.java b/backend/src/main/java/io/metersphere/log/utils/dff/JsonDiff.java deleted file mode 100644 index 57f2b07ac9..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/JsonDiff.java +++ /dev/null @@ -1,512 +0,0 @@ -/* - * Copyright 2016 flipkart.com zjsonpatch. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.metersphere.log.utils.dff; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.apache.commons.collections4.ListUtils; - -import java.util.*; - -/** - * User: gopi.vishwakarma - * Date: 30/07/14 - */ -public final class JsonDiff { - - private final List diffs = new ArrayList(); - private final EnumSet flags; - - private JsonDiff(EnumSet flags) { - this.flags = flags.clone(); - } - - public static JsonNode asJson(final JsonNode source, final JsonNode target) { - return asJson(source, target, DiffFlags.defaults()); - } - - public static JsonNode asJson(final JsonNode source, final JsonNode target, EnumSet flags) { - JsonDiff diff = new JsonDiff(flags); - if (source == null && target != null) { - // return add node at root pointing to the target - diff.diffs.add(Diff.generateDiff(Operation.ADD, JsonPointer.ROOT, target)); - } - if (source != null && target == null) { - // return remove node at root pointing to the source - diff.diffs.add(Diff.generateDiff(Operation.REMOVE, JsonPointer.ROOT, source)); - } - if (source != null && target != null) { - diff.generateDiffs(JsonPointer.ROOT, source, target); - - if (!flags.contains(DiffFlags.OMIT_MOVE_OPERATION)) - // Merging remove & add to move operation - diff.introduceMoveOperation(); - - if (!flags.contains(DiffFlags.OMIT_COPY_OPERATION)) - // Introduce copy operation - diff.introduceCopyOperation(source, target); - - if (flags.contains(DiffFlags.ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE)) - // Split replace into remove and add instructions - diff.introduceExplicitRemoveAndAddOperation(); - } - return diff.getJsonNodes(); - } - - public static List jsonDiff(final JsonNode source, final JsonNode target) { - return diff(source, target); - } - - public static List diff(final JsonNode source, final JsonNode target) { - JsonDiff diff = new JsonDiff(DiffFlags.defaults()); - if (source == null && target != null) { - // return add node at root pointing to the target - diff.diffs.add(Diff.generateDiff(Operation.ADD, JsonPointer.ROOT, target)); - } - if (source != null && target == null) { - // return remove node at root pointing to the source - diff.diffs.add(Diff.generateDiff(Operation.REMOVE, JsonPointer.ROOT, source)); - } - if (source != null && target != null) { - diff.generateDiffs(JsonPointer.ROOT, source, target); - - if (!DiffFlags.defaults().contains(DiffFlags.OMIT_MOVE_OPERATION)) - // Merging remove & add to move operation - diff.introduceMoveOperation(); - - if (!DiffFlags.defaults().contains(DiffFlags.OMIT_COPY_OPERATION)) - // Introduce copy operation - diff.introduceCopyOperation(source, target); - - if (DiffFlags.defaults().contains(DiffFlags.ADD_EXPLICIT_REMOVE_ADD_ON_REPLACE)) - // Split replace into remove and add instructions - diff.introduceExplicitRemoveAndAddOperation(); - } - return diff.getDiffs(); - } - - private static JsonPointer getMatchingValuePath(Map unchangedValues, JsonNode value) { - return unchangedValues.get(value); - } - - private void introduceCopyOperation(JsonNode source, JsonNode target) { - Map unchangedValues = getUnchangedPart(source, target); - - for (int i = 0; i < diffs.size(); i++) { - Diff diff = diffs.get(i); - if (Operation.ADD != diff.getOperation()) continue; - - JsonPointer matchingValuePath = getMatchingValuePath(unchangedValues, diff.getValue()); - if (matchingValuePath != null && isAllowed(matchingValuePath, diff.getPath())) { - // Matching value found; replace add with copy - if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS)) { - // Prepend test node - diffs.add(i, new Diff(Operation.TEST, matchingValuePath, diff.getValue())); - i++; - } - diffs.set(i, new Diff(Operation.COPY, matchingValuePath, diff.getPath())); - } - } - } - - private static boolean isNumber(String str) { - int size = str.length(); - - for (int i = 0; i < size; i++) { - if (!Character.isDigit(str.charAt(i))) { - return false; - } - } - - return size > 0; - } - - // TODO this is quite unclear and needs some serious documentation - private static boolean isAllowed(JsonPointer source, JsonPointer destination) { - boolean isSame = source.equals(destination); - int i = 0; - int j = 0; - // Hack to fix broken COPY operation, need better handling here - while (i < source.size() && j < destination.size()) { - JsonPointer.RefToken srcValue = source.get(i); - JsonPointer.RefToken dstValue = destination.get(j); - String srcStr = srcValue.toString(); - String dstStr = dstValue.toString(); - if (isNumber(srcStr) && isNumber(dstStr)) { - - if (srcStr.compareTo(dstStr) > 0) { - return false; - } - } - i++; - j++; - - } - return !isSame; - } - - private static Map getUnchangedPart(JsonNode source, JsonNode target) { - Map unchangedValues = new HashMap(); - computeUnchangedValues(unchangedValues, JsonPointer.ROOT, source, target); - return unchangedValues; - } - - private static void computeUnchangedValues(Map unchangedValues, JsonPointer path, JsonNode source, JsonNode target) { - if (source.equals(target)) { - if (!unchangedValues.containsKey(target)) { - unchangedValues.put(target, path); - } - return; - } - - final NodeType firstType = NodeType.getNodeType(source); - final NodeType secondType = NodeType.getNodeType(target); - - if (firstType == secondType) { - switch (firstType) { - case OBJECT: - computeObject(unchangedValues, path, source, target); - break; - case ARRAY: - computeArray(unchangedValues, path, source, target); - break; - default: - /* nothing */ - } - } - } - - private static void computeArray(Map unchangedValues, JsonPointer path, JsonNode source, JsonNode target) { - final int size = Math.min(source.size(), target.size()); - - for (int i = 0; i < size; i++) { - JsonPointer currPath = path.append(i); - computeUnchangedValues(unchangedValues, currPath, source.get(i), target.get(i)); - } - } - - private static void computeObject(Map unchangedValues, JsonPointer path, JsonNode source, JsonNode target) { - final Iterator firstFields = source.fieldNames(); - while (firstFields.hasNext()) { - String name = firstFields.next(); - if (target.has(name)) { - JsonPointer currPath = path.append(name); - computeUnchangedValues(unchangedValues, currPath, source.get(name), target.get(name)); - } - } - } - - /** - * This method merge 2 diffs ( remove then add, or vice versa ) with same value into one Move operation, - * all the core logic resides here only - */ - private void introduceMoveOperation() { - for (int i = 0; i < diffs.size(); i++) { - Diff diff1 = diffs.get(i); - - // if not remove OR add, move to next diff - if (!(Operation.REMOVE == diff1.getOperation() || - Operation.ADD == diff1.getOperation())) { - continue; - } - - for (int j = i + 1; j < diffs.size(); j++) { - Diff diff2 = diffs.get(j); - if (!diff1.getValue().equals(diff2.getValue())) { - continue; - } - - Diff moveDiff = null; - if (Operation.REMOVE == diff1.getOperation() && - Operation.ADD == diff2.getOperation()) { - JsonPointer relativePath = computeRelativePath(diff2.getPath(), i + 1, j - 1, diffs); - moveDiff = new Diff(Operation.MOVE, diff1.getPath(), relativePath); - - } else if (Operation.ADD == diff1.getOperation() && - Operation.REMOVE == diff2.getOperation()) { - JsonPointer relativePath = computeRelativePath(diff2.getPath(), i, j - 1, diffs); // diff1's add should also be considered - moveDiff = new Diff(Operation.MOVE, relativePath, diff1.getPath()); - } - if (moveDiff != null) { - diffs.remove(j); - diffs.set(i, moveDiff); - break; - } - } - } - } - - /** - * This method splits a {@link Operation#REPLACE} operation within a diff into a {@link Operation#REMOVE} - * and {@link Operation#ADD} in order, respectively. - * Does nothing if {@link Operation#REPLACE} op does not contain a from value - */ - private void introduceExplicitRemoveAndAddOperation() { - List updatedDiffs = new ArrayList(); - for (Diff diff : diffs) { - if (!diff.getOperation().equals(Operation.REPLACE) || diff.getSrcValue() == null) { - updatedDiffs.add(diff); - continue; - } - //Split into two #REMOVE and #ADD - updatedDiffs.add(new Diff(Operation.REMOVE, diff.getPath(), diff.getSrcValue())); - updatedDiffs.add(new Diff(Operation.ADD, diff.getPath(), diff.getValue())); - } - diffs.clear(); - diffs.addAll(updatedDiffs); - } - - //Note : only to be used for arrays - //Finds the longest common Ancestor ending at Array - private static JsonPointer computeRelativePath(JsonPointer path, int startIdx, int endIdx, List diffs) { - List counters = new ArrayList(path.size()); - for (int i = 0; i < path.size(); i++) { - counters.add(0); - } - - for (int i = startIdx; i <= endIdx; i++) { - Diff diff = diffs.get(i); - //Adjust relative path according to #ADD and #Remove - if (Operation.ADD == diff.getOperation() || Operation.REMOVE == diff.getOperation()) { - updatePath(path, diff, counters); - } - } - return updatePathWithCounters(counters, path); - } - - private static JsonPointer updatePathWithCounters(List counters, JsonPointer path) { - List tokens = path.decompose(); - for (int i = 0; i < counters.size(); i++) { - int value = counters.get(i); - if (value != 0) { - int currValue = tokens.get(i).getIndex(); - tokens.set(i, new JsonPointer.RefToken(Integer.toString(currValue + value))); - } - } - return new JsonPointer(tokens); - } - - private static void updatePath(JsonPointer path, Diff pseudo, List counters) { - //find longest common prefix of both the paths - - if (pseudo.getPath().size() <= path.size()) { - int idx = -1; - for (int i = 0; i < pseudo.getPath().size() - 1; i++) { - if (pseudo.getPath().get(i).equals(path.get(i))) { - idx = i; - } else { - break; - } - } - if (idx == pseudo.getPath().size() - 2) { - if (pseudo.getPath().get(pseudo.getPath().size() - 1).isArrayIndex()) { - updateCounters(pseudo, pseudo.getPath().size() - 1, counters); - } - } - } - } - - private static void updateCounters(Diff pseudo, int idx, List counters) { - if (Operation.ADD == pseudo.getOperation()) { - counters.set(idx, counters.get(idx) - 1); - } else { - if (Operation.REMOVE == pseudo.getOperation()) { - counters.set(idx, counters.get(idx) + 1); - } - } - } - - private ArrayNode getJsonNodes() { - JsonNodeFactory FACTORY = JsonNodeFactory.instance; - final ArrayNode patch = FACTORY.arrayNode(); - for (Diff diff : diffs) { - ObjectNode jsonNode = getJsonNode(FACTORY, diff, flags); - patch.add(jsonNode); - } - return patch; - } - private List getDiffs() { - return diffs; - } - - private static ObjectNode getJsonNode(JsonNodeFactory FACTORY, Diff diff, EnumSet flags) { - ObjectNode jsonNode = FACTORY.objectNode(); - jsonNode.put(Constants.OP, diff.getOperation().rfcName()); - - switch (diff.getOperation()) { - case MOVE: - case COPY: - jsonNode.put(Constants.FROM, diff.getPath().toString()); // required {from} only in case of Move Operation - jsonNode.put(Constants.PATH, diff.getToPath().toString()); // destination Path - break; - - case REMOVE: - jsonNode.put(Constants.PATH, diff.getPath().toString()); - if (!flags.contains(DiffFlags.OMIT_VALUE_ON_REMOVE)) - jsonNode.set(Constants.VALUE, diff.getValue()); - break; - - case REPLACE: - if (flags.contains(DiffFlags.ADD_ORIGINAL_VALUE_ON_REPLACE)) { - jsonNode.set(Constants.FROM_VALUE, diff.getSrcValue()); - } - case ADD: - case TEST: - jsonNode.put(Constants.PATH, diff.getPath().toString()); - jsonNode.set(Constants.VALUE, diff.getValue()); - break; - - default: - // Safety net - throw new IllegalArgumentException("Unknown operation specified:" + diff.getOperation()); - } - - return jsonNode; - } - - private void generateDiffs(JsonPointer path, JsonNode source, JsonNode target) { - if (!source.equals(target)) { - final NodeType sourceType = NodeType.getNodeType(source); - final NodeType targetType = NodeType.getNodeType(target); - - if (sourceType == NodeType.ARRAY && targetType == NodeType.ARRAY) { - //both are arrays - compareArray(path, source, target); - } else if (sourceType == NodeType.OBJECT && targetType == NodeType.OBJECT) { - //both are json - compareObjects(path, source, target); - } else { - //can be replaced - if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS)) - diffs.add(new Diff(Operation.TEST, path, source)); - diffs.add(Diff.generateDiff(Operation.REPLACE, path, source, target)); - } - } - } - - private void compareArray(JsonPointer path, JsonNode source, JsonNode target) { - List lcs = getLCS(source, target); - int srcIdx = 0; - int targetIdx = 0; - int lcsIdx = 0; - int srcSize = source.size(); - int targetSize = target.size(); - int lcsSize = lcs.size(); - - int pos = 0; - while (lcsIdx < lcsSize) { - JsonNode lcsNode = lcs.get(lcsIdx); - JsonNode srcNode = source.get(srcIdx); - JsonNode targetNode = target.get(targetIdx); - - - if (lcsNode.equals(srcNode) && lcsNode.equals(targetNode)) { // Both are same as lcs node, nothing to do here - srcIdx++; - targetIdx++; - lcsIdx++; - pos++; - } else { - if (lcsNode.equals(srcNode)) { // src node is same as lcs, but not targetNode - //addition - JsonPointer currPath = path.append(pos); - diffs.add(Diff.generateDiff(Operation.ADD, currPath, targetNode)); - pos++; - targetIdx++; - } else if (lcsNode.equals(targetNode)) { //targetNode node is same as lcs, but not src - //removal, - JsonPointer currPath = path.append(pos); - if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS)) - diffs.add(new Diff(Operation.TEST, currPath, srcNode)); - diffs.add(Diff.generateDiff(Operation.REMOVE, currPath, srcNode)); - srcIdx++; - } else { - JsonPointer currPath = path.append(pos); - //both are unequal to lcs node - generateDiffs(currPath, srcNode, targetNode); - srcIdx++; - targetIdx++; - pos++; - } - } - } - - while ((srcIdx < srcSize) && (targetIdx < targetSize)) { - JsonNode srcNode = source.get(srcIdx); - JsonNode targetNode = target.get(targetIdx); - JsonPointer currPath = path.append(pos); - generateDiffs(currPath, srcNode, targetNode); - srcIdx++; - targetIdx++; - pos++; - } - pos = addRemaining(path, target, pos, targetIdx, targetSize); - removeRemaining(path, pos, srcIdx, srcSize, source); - } - - private void removeRemaining(JsonPointer path, int pos, int srcIdx, int srcSize, JsonNode source) { - while (srcIdx < srcSize) { - JsonPointer currPath = path.append(pos); - if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS)) - diffs.add(new Diff(Operation.TEST, currPath, source.get(srcIdx))); - diffs.add(Diff.generateDiff(Operation.REMOVE, currPath, source.get(srcIdx))); - srcIdx++; - } - } - - private int addRemaining(JsonPointer path, JsonNode target, int pos, int targetIdx, int targetSize) { - while (targetIdx < targetSize) { - JsonNode jsonNode = target.get(targetIdx); - JsonPointer currPath = path.append(pos); - diffs.add(Diff.generateDiff(Operation.ADD, currPath, jsonNode.deepCopy())); - pos++; - targetIdx++; - } - return pos; - } - - private void compareObjects(JsonPointer path, JsonNode source, JsonNode target) { - Iterator keysFromSrc = source.fieldNames(); - while (keysFromSrc.hasNext()) { - String key = keysFromSrc.next(); - if (!target.has(key)) { - //remove case - JsonPointer currPath = path.append(key); - if (flags.contains(DiffFlags.EMIT_TEST_OPERATIONS)) - diffs.add(new Diff(Operation.TEST, currPath, source.get(key))); - diffs.add(Diff.generateDiff(Operation.REMOVE, currPath, source.get(key))); - continue; - } - JsonPointer currPath = path.append(key); - generateDiffs(currPath, source.get(key), target.get(key)); - } - Iterator keysFromTarget = target.fieldNames(); - while (keysFromTarget.hasNext()) { - String key = keysFromTarget.next(); - if (!source.has(key)) { - //add case - JsonPointer currPath = path.append(key); - diffs.add(Diff.generateDiff(Operation.ADD, currPath, target.get(key))); - } - } - } - - private static List getLCS(final JsonNode first, final JsonNode second) { - return ListUtils.longestCommonSubsequence(InternalUtils.toList((ArrayNode) first), InternalUtils.toList((ArrayNode) second)); - } -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/JsonPatch.java b/backend/src/main/java/io/metersphere/log/utils/dff/JsonPatch.java deleted file mode 100644 index 28d40abda8..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/JsonPatch.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2016 flipkart.com zjsonpatch. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.metersphere.log.utils.dff; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.NullNode; - -import java.util.EnumSet; -import java.util.Iterator; - -/** - * User: gopi.vishwakarma - * Date: 31/07/14 - */ -public final class JsonPatch { - - private JsonPatch() { - } - - private static JsonNode getPatchAttr(JsonNode jsonNode, String attr) { - JsonNode child = jsonNode.get(attr); - if (child == null) - throw new InvalidJsonPatchException("Invalid JSON Patch payload (missing '" + attr + "' field)"); - return child; - } - - private static JsonNode getPatchAttrWithDefault(JsonNode jsonNode, String attr, JsonNode defaultValue) { - JsonNode child = jsonNode.get(attr); - if (child == null) - return defaultValue; - else - return child; - } - - private static void process(JsonNode patch, JsonPatchProcessor processor, EnumSet flags) - throws InvalidJsonPatchException { - - if (!patch.isArray()) - throw new InvalidJsonPatchException("Invalid JSON Patch payload (not an array)"); - Iterator operations = patch.iterator(); - while (operations.hasNext()) { - JsonNode jsonNode = operations.next(); - if (!jsonNode.isObject()) throw new InvalidJsonPatchException("Invalid JSON Patch payload (not an object)"); - Operation operation = Operation.fromRfcName(getPatchAttr(jsonNode, Constants.OP).textValue()); - JsonPointer path = JsonPointer.parse(getPatchAttr(jsonNode, Constants.PATH).textValue()); - - try { - switch (operation) { - case REMOVE: { - processor.remove(path); - break; - } - - case ADD: { - JsonNode value; - if (!flags.contains(CompatibilityFlags.MISSING_VALUES_AS_NULLS)) - value = getPatchAttr(jsonNode, Constants.VALUE); - else - value = getPatchAttrWithDefault(jsonNode, Constants.VALUE, NullNode.getInstance()); - processor.add(path, value.deepCopy()); - break; - } - - case REPLACE: { - JsonNode value; - if (!flags.contains(CompatibilityFlags.MISSING_VALUES_AS_NULLS)) - value = getPatchAttr(jsonNode, Constants.VALUE); - else - value = getPatchAttrWithDefault(jsonNode, Constants.VALUE, NullNode.getInstance()); - processor.replace(path, value.deepCopy()); - break; - } - - case MOVE: { - JsonPointer fromPath = JsonPointer.parse(getPatchAttr(jsonNode, Constants.FROM).textValue()); - processor.move(fromPath, path); - break; - } - - case COPY: { - JsonPointer fromPath = JsonPointer.parse(getPatchAttr(jsonNode, Constants.FROM).textValue()); - processor.copy(fromPath, path); - break; - } - - case TEST: { - JsonNode value; - if (!flags.contains(CompatibilityFlags.MISSING_VALUES_AS_NULLS)) - value = getPatchAttr(jsonNode, Constants.VALUE); - else - value = getPatchAttrWithDefault(jsonNode, Constants.VALUE, NullNode.getInstance()); - processor.test(path, value.deepCopy()); - break; - } - } - } - catch (JsonPointerEvaluationException e) { - throw new JsonPatchApplicationException(e.getMessage(), operation, e.getPath()); - } - } - } - - public static void validate(JsonNode patch, EnumSet flags) throws InvalidJsonPatchException { - process(patch, NoopProcessor.INSTANCE, flags); - } - - public static void validate(JsonNode patch) throws InvalidJsonPatchException { - validate(patch, CompatibilityFlags.defaults()); - } - - public static JsonNode apply(JsonNode patch, JsonNode source, EnumSet flags) throws JsonPatchApplicationException { - CopyingApplyProcessor processor = new CopyingApplyProcessor(source, flags); - process(patch, processor, flags); - return processor.result(); - } - - public static JsonNode apply(JsonNode patch, JsonNode source) throws JsonPatchApplicationException { - return apply(patch, source, CompatibilityFlags.defaults()); - } - - public static void applyInPlace(JsonNode patch, JsonNode source) { - applyInPlace(patch, source, CompatibilityFlags.defaults()); - } - - public static void applyInPlace(JsonNode patch, JsonNode source, EnumSet flags) { - InPlaceApplyProcessor processor = new InPlaceApplyProcessor(source, flags); - process(patch, processor, flags); - } -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/JsonPatchApplicationException.java b/backend/src/main/java/io/metersphere/log/utils/dff/JsonPatchApplicationException.java deleted file mode 100644 index 83efc7376f..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/JsonPatchApplicationException.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2016 flipkart.com zjsonpatch. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -package io.metersphere.log.utils.dff; - -/** - * User: holograph - * Date: 03/08/16 - */ -public class JsonPatchApplicationException extends RuntimeException { - Operation operation; - JsonPointer path; - - public JsonPatchApplicationException(String message, Operation operation, JsonPointer path) { - super(message); - this.operation = operation; - this.path = path; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - if (operation != null) sb.append('[').append(operation).append(" Operation] "); - sb.append(getMessage()); - if (path != null) sb.append(" at ").append(path.isRoot() ? "root" : path); - return sb.toString(); - } -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/JsonPatchProcessor.java b/backend/src/main/java/io/metersphere/log/utils/dff/JsonPatchProcessor.java deleted file mode 100644 index 4d2ac5098d..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/JsonPatchProcessor.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2016 flipkart.com zjsonpatch. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -package io.metersphere.log.utils.dff; - -import com.fasterxml.jackson.databind.JsonNode; - -interface JsonPatchProcessor { - void remove(JsonPointer path) throws JsonPointerEvaluationException; - void replace(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException; - void add(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException; - void move(JsonPointer fromPath, JsonPointer toPath) throws JsonPointerEvaluationException; - void copy(JsonPointer fromPath, JsonPointer toPath) throws JsonPointerEvaluationException; - void test(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException; -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/JsonPointer.java b/backend/src/main/java/io/metersphere/log/utils/dff/JsonPointer.java deleted file mode 100644 index 0b90078f7f..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/JsonPointer.java +++ /dev/null @@ -1,346 +0,0 @@ -package io.metersphere.log.utils.dff; - - -import com.fasterxml.jackson.databind.JsonNode; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Implements RFC 6901 (JSON Pointer) - * - *

For full details, please refer to RFC 6901. - * - *

Generally, a JSON Pointer is a string representation of a path into a JSON document. - * This class implements the RFC as closely as possible, and offers several helpers and - * utility methods on top of it: - * - *
- *      // Parse, build or render a JSON pointer
- *      String path = "/a/0/b/1";
- *      JsonPointer ptr1 = JsonPointer.{@link #parse}(path);
- *      JsonPointer ptr2 = JsonPointer.{@link #ROOT}.append("a").append(0).append("b").append(1);
- *      assert(ptr1.equals(ptr2));
- *      assert(path.equals(ptr1.toString()));
- *      assert(path.equals(ptr2.toString()));
- *
- *      // Evaluate a JSON pointer against a live document
- *      ObjectMapper om = new ObjectMapper();
- *      JsonNode doc = om.readTree("{\"foo\":[\"bar\", \"baz\"]}");
- *      JsonNode baz = JsonPointer.parse("/foo/1").{@link #evaluate(JsonNode) evaluate}(doc);
- *      assert(baz.textValue().equals("baz"));
- * 
- * - *

Instances of {@link JsonPointer} and its constituent {@link RefToken}s are immutable. - * - * @since 0.4.8 - */ -class JsonPointer { - private final RefToken[] tokens; - - /** A JSON pointer representing the root node of a JSON document */ - public final static JsonPointer ROOT = new JsonPointer(new RefToken[] {}); - - private JsonPointer(RefToken[] tokens) { - this.tokens = tokens; - } - - /** - * Constructs a new pointer from a list of reference tokens. - * - * @param tokens The list of reference tokens from which to construct the new pointer. This list is not modified. - */ - public JsonPointer(List tokens) { - this.tokens = tokens.toArray(new RefToken[0]); - } - - /** - * Parses a valid string representation of a JSON Pointer. - * - * @param path The string representation to be parsed. - * @return An instance of {@link JsonPointer} conforming to the specified string representation. - * @throws IllegalArgumentException The specified JSON Pointer is invalid. - */ - public static JsonPointer parse(String path) throws IllegalArgumentException { - StringBuilder reftoken = null; - List result = new ArrayList(); - - for (int i = 0; i < path.length(); ++i) { - char c = path.charAt(i); - - // Require leading slash - if (i == 0) { - if (c != '/') throw new IllegalArgumentException("Missing leading slash"); - reftoken = new StringBuilder(); - continue; - } - - switch (c) { - // Escape sequences - case '~': - switch (path.charAt(++i)) { - case '0': reftoken.append('~'); break; - case '1': reftoken.append('/'); break; - default: - throw new IllegalArgumentException("Invalid escape sequence ~" + path.charAt(i) + " at index " + i); - } - break; - - // New reftoken - case '/': - result.add(new RefToken(reftoken.toString())); - reftoken.setLength(0); - break; - - default: - reftoken.append(c); - break; - } - } - - if (reftoken == null) - return ROOT; - - result.add(RefToken.parse(reftoken.toString())); - return new JsonPointer(result); - } - - /** - * Indicates whether or not this instance points to the root of a JSON document. - * @return {@code true} if this pointer represents the root node, {@code false} otherwise. - */ - public boolean isRoot() { - return tokens.length == 0; - } - - /** - * Creates a new JSON pointer to the specified field of the object referenced by this instance. - * - * @param field The desired field name, or any valid JSON Pointer reference token - * @return The new {@link JsonPointer} instance. - */ - JsonPointer append(String field) { - RefToken[] newTokens = Arrays.copyOf(tokens, tokens.length + 1); - newTokens[tokens.length] = new RefToken(field); - return new JsonPointer(newTokens); - } - - /** - * Creates a new JSON pointer to an indexed element of the array referenced by this instance. - * - * @param index The desired index, or {@link #LAST_INDEX} to point past the end of the array. - * @return The new {@link JsonPointer} instance. - */ - JsonPointer append(int index) { - return append(Integer.toString(index)); - } - - /** Returns the number of reference tokens comprising this instance. */ - int size() { - return tokens.length; - } - - /** - * Returns a string representation of this instance - * - * @return - * An RFC 6901 compliant string - * representation of this JSON pointer. - */ - public String toString() { - StringBuilder sb = new StringBuilder(); - for (RefToken token : tokens) { - sb.append('/'); - sb.append(token); - } - return sb.toString(); - } - - /** - * Decomposes this JSON pointer into its reference tokens. - * - * @return A list of {@link RefToken}s. Modifications to this list do not affect this instance. - */ - public List decompose() { - return Arrays.asList(tokens.clone()); - } - - /** - * Retrieves the reference token at the specified index. - * - * @param index The desired reference token index. - * @return The specified instance of {@link RefToken}. - * @throws IndexOutOfBoundsException The specified index is illegal. - */ - public RefToken get(int index) throws IndexOutOfBoundsException { - if (index < 0 || index >= tokens.length) throw new IndexOutOfBoundsException("Illegal index: " + index); - return tokens[index]; - } - - /** - * Retrieves the last reference token for this JSON pointer. - * - * @return The last {@link RefToken} comprising this instance. - * @throws IllegalStateException Last cannot be called on {@link #ROOT root} pointers. - */ - public RefToken last() { - if (isRoot()) throw new IllegalStateException("Root pointers contain no reference tokens"); - return tokens[tokens.length - 1]; - } - - /** - * Creates a JSON pointer to the parent of the node represented by this instance. - * - * The parent of the {@link #ROOT root} pointer is the root pointer itself. - * - * @return A {@link JsonPointer} to the parent node. - */ - public JsonPointer getParent() { - return isRoot() ? this : new JsonPointer(Arrays.copyOf(tokens, tokens.length - 1)); - } - - private void error(int atToken, String message, JsonNode document) throws JsonPointerEvaluationException { - throw new JsonPointerEvaluationException( - message, - new JsonPointer(Arrays.copyOf(tokens, atToken)), - document); - } - - /** - * Takes a target document and resolves the node represented by this instance. - * - * The evaluation semantics are described in - * RFC 6901 sectino 4. - * - * @param document The target document against which to evaluate the JSON pointer. - * @return The {@link JsonNode} resolved by evaluating this JSON pointer. - * @throws JsonPointerEvaluationException The pointer could not be evaluated. - */ - public JsonNode evaluate(final JsonNode document) throws JsonPointerEvaluationException { - JsonNode current = document; - - for (int idx = 0; idx < tokens.length; ++idx) { - final RefToken token = tokens[idx]; - - if (current.isArray()) { - if (!token.isArrayIndex()) - error(idx, "Can't reference field \"" + token.getField() + "\" on array", document); - if (token.getIndex() == LAST_INDEX || token.getIndex() >= current.size()) - error(idx, "Array index " + token.toString() + " is out of bounds", document); - current = current.get(token.getIndex()); - } - else if (current.isObject()) { - if (!current.has(token.getField())) - error(idx,"Missing field \"" + token.getField() + "\"", document); - current = current.get(token.getField()); - } - else - error(idx, "Can't reference past scalar value", document); - } - - return current; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - JsonPointer that = (JsonPointer) o; - - // Probably incorrect - comparing Object[] arrays with Arrays.equals - return Arrays.equals(tokens, that.tokens); - } - - @Override - public int hashCode() { - return Arrays.hashCode(tokens); - } - - /** Represents a single JSON Pointer reference token. */ - static class RefToken { - private String decodedToken; - transient private Integer index = null; - - public RefToken(String decodedToken) { - if (decodedToken == null) throw new IllegalArgumentException("Token can't be null"); - this.decodedToken = decodedToken; - } - - private static final Pattern DECODED_TILDA_PATTERN = Pattern.compile("~0"); - private static final Pattern DECODED_SLASH_PATTERN = Pattern.compile("~1"); - - private static String decodePath(Object object) { - String path = object.toString(); // see http://tools.ietf.org/html/rfc6901#section-4 - path = DECODED_SLASH_PATTERN.matcher(path).replaceAll("/"); - return DECODED_TILDA_PATTERN.matcher(path).replaceAll("~"); - } - - private static final Pattern ENCODED_TILDA_PATTERN = Pattern.compile("~"); - private static final Pattern ENCODED_SLASH_PATTERN = Pattern.compile("/"); - - private static String encodePath(Object object) { - String path = object.toString(); // see http://tools.ietf.org/html/rfc6901#section-4 - path = ENCODED_TILDA_PATTERN.matcher(path).replaceAll("~0"); - return ENCODED_SLASH_PATTERN.matcher(path).replaceAll("~1"); - } - - private static final Pattern VALID_ARRAY_IND = Pattern.compile("-|0|(?:[1-9][0-9]*)"); - - public static RefToken parse(String rawToken) { - if (rawToken == null) throw new IllegalArgumentException("Token can't be null"); - return new RefToken(decodePath(rawToken)); - } - - public boolean isArrayIndex() { - if (index != null) return true; - Matcher matcher = VALID_ARRAY_IND.matcher(decodedToken); - if (matcher.matches()) { - index = matcher.group().equals("-") ? LAST_INDEX : Integer.parseInt(matcher.group()); - return true; - } - return false; - } - - public int getIndex() { - if (!isArrayIndex()) throw new IllegalStateException("Object operation on array target"); - return index; - } - - public String getField() { - return decodedToken; - } - - @Override - public String toString() { - return encodePath(decodedToken); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - RefToken refToken = (RefToken) o; - - return decodedToken.equals(refToken.decodedToken); - } - - @Override - public int hashCode() { - return decodedToken.hashCode(); - } - } - - /** - * Represents an array index pointing past the end of the array. - * - * Such an index is represented by the JSON pointer reference token "{@code -}"; see - * RFC 6901 section 4 for - * more details. - */ - final static int LAST_INDEX = Integer.MIN_VALUE; -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/JsonPointerEvaluationException.java b/backend/src/main/java/io/metersphere/log/utils/dff/JsonPointerEvaluationException.java deleted file mode 100644 index 2c17971ba5..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/JsonPointerEvaluationException.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.metersphere.log.utils.dff; - -import com.fasterxml.jackson.databind.JsonNode; - -public class JsonPointerEvaluationException extends Exception { - private final JsonPointer path; - private final JsonNode target; - - public JsonPointerEvaluationException(String message, JsonPointer path, JsonNode target) { - super(message); - this.path = path; - this.target = target; - } - - public JsonPointer getPath() { - return path; - } - - public JsonNode getTarget() { - return target; - } -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/NodeType.java b/backend/src/main/java/io/metersphere/log/utils/dff/NodeType.java deleted file mode 100644 index 03572bd7d3..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/NodeType.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2016 flipkart.com zjsonpatch. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.metersphere.log.utils.dff; - -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.JsonNode; - -import java.util.EnumMap; -import java.util.Map; - -enum NodeType { - /** - * Array nodes - */ - ARRAY("array"), - /** - * Boolean nodes - */ - BOOLEAN("boolean"), - /** - * Integer nodes - */ - INTEGER("integer"), - /** - * Number nodes (ie, decimal numbers) - */ - NULL("null"), - /** - * Object nodes - */ - NUMBER("number"), - /** - * Null nodes - */ - OBJECT("object"), - /** - * String nodes - */ - STRING("string"); - - /** - * The name for this type, as encountered in a JSON schema - */ - private final String name; - - private static final Map TOKEN_MAP - = new EnumMap(JsonToken.class); - - static { - TOKEN_MAP.put(JsonToken.START_ARRAY, ARRAY); - TOKEN_MAP.put(JsonToken.VALUE_TRUE, BOOLEAN); - TOKEN_MAP.put(JsonToken.VALUE_FALSE, BOOLEAN); - TOKEN_MAP.put(JsonToken.VALUE_NUMBER_INT, INTEGER); - TOKEN_MAP.put(JsonToken.VALUE_NUMBER_FLOAT, NUMBER); - TOKEN_MAP.put(JsonToken.VALUE_NULL, NULL); - TOKEN_MAP.put(JsonToken.START_OBJECT, OBJECT); - TOKEN_MAP.put(JsonToken.VALUE_STRING, STRING); - - } - - NodeType(final String name) { - this.name = name; - } - - @Override - public String toString() { - return name; - } - - public static NodeType getNodeType(final JsonNode node) { - final JsonToken token = node.asToken(); - final NodeType ret = TOKEN_MAP.get(token); - if (ret == null) throw new NullPointerException("unhandled token type " + token); - return ret; - } -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/NoopProcessor.java b/backend/src/main/java/io/metersphere/log/utils/dff/NoopProcessor.java deleted file mode 100644 index 6a82648fdf..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/NoopProcessor.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2016 flipkart.com zjsonpatch. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -package io.metersphere.log.utils.dff; - -import com.fasterxml.jackson.databind.JsonNode; - -/** - * A JSON patch processor that does nothing, intended for testing and validation. - */ -public class NoopProcessor implements JsonPatchProcessor { - static final NoopProcessor INSTANCE; - static { - INSTANCE = new NoopProcessor(); - } - - @Override public void remove(JsonPointer path) {} - @Override public void replace(JsonPointer path, JsonNode value) {} - @Override public void add(JsonPointer path, JsonNode value) {} - @Override public void move(JsonPointer fromPath, JsonPointer toPath) {} - @Override public void copy(JsonPointer fromPath, JsonPointer toPath) {} - @Override public void test(JsonPointer path, JsonNode value) {} - -} diff --git a/backend/src/main/java/io/metersphere/log/utils/dff/Operation.java b/backend/src/main/java/io/metersphere/log/utils/dff/Operation.java deleted file mode 100644 index 673c6b87db..0000000000 --- a/backend/src/main/java/io/metersphere/log/utils/dff/Operation.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2016 flipkart.com zjsonpatch. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -package io.metersphere.log.utils.dff; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * User: gopi.vishwakarma - * Date: 30/07/14 - */ -public enum Operation { - ADD("添加"), - REMOVE("移除"), - REPLACE("修改"), - MOVE("移动"), - COPY("复制"), - TEST("测试"); - - private final static Map OPS = createImmutableMap(); - - private static Map createImmutableMap() { - Map map = new HashMap(); - map.put(ADD.rfcName, ADD); - map.put(REMOVE.rfcName, REMOVE); - map.put(REPLACE.rfcName, REPLACE); - map.put(MOVE.rfcName, MOVE); - map.put(COPY.rfcName, COPY); - map.put(TEST.rfcName, TEST); - return Collections.unmodifiableMap(map); - } - - private String rfcName; - - Operation(String rfcName) { - this.rfcName = rfcName; - } - - public static Operation fromRfcName(String rfcName) throws InvalidJsonPatchException { - if (rfcName == null) throw new InvalidJsonPatchException("rfcName cannot be null"); - Operation op = OPS.get(rfcName.toLowerCase()); - if (op == null) throw new InvalidJsonPatchException("unknown / unsupported operation " + rfcName); - return op; - } - - public String rfcName() { - return this.rfcName; - } - - -} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/ArrNode.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/ArrNode.java new file mode 100644 index 0000000000..a60676c37f --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/ArrNode.java @@ -0,0 +1,43 @@ +package io.metersphere.log.utils.json.diff; + +class ArrNode extends Node { + + int index; + + ArrNode(Node parent, int index) { + super(parent); + this.index = index; + } + + @Override + void rehash(Node newParent) { + this.parent = newParent; + this.parentHashCode = newParent.hashCode; + int i = this.parentHashCode; + + i = i * 31 + ArrNode.class.hashCode(); + hashCode = i; + } + + @Override + int doHash(boolean indexed) { + + // this must either be the first node in which case passing + // false to lastArrNode must be correct, or it isn't + // in which case passing false is also correct. + int i = parent.doHash(indexed); + + i = i * 31 + ArrNode.class.hashCode(); + if (indexed) { + int adjusted = index; + i = i * 31 + adjusted; + } + return i; + + } + + @Override + public String toString() { + return "" + index; + } +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/DiffTest.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/DiffTest.java new file mode 100644 index 0000000000..e375a136f7 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/DiffTest.java @@ -0,0 +1,23 @@ +package io.metersphere.log.utils.json.diff; + +public class DiffTest { + public static void main(String[] args) { + GsonDiff diff = new GsonDiff(); + + String newValue= "{\n" + + " \"username\": \"zyy\",\n" + + " \"username2\": \"zyy\",\n" + + " \"password\": \"Calong@2015\"\n" + + "}"; + String oldValue = "{\n" + + " \"username\": \"zyy\",\n" + + " \"username1\": \"zyy\",\n" + + " \"password\": \"Calong@201512\"\n" + + "}"; + String d = diff.diff(oldValue,newValue); + + System.out.println(d); + + System.out.println(diff.apply(newValue,d)); + } +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/GsonDiff.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/GsonDiff.java new file mode 100644 index 0000000000..581174105f --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/GsonDiff.java @@ -0,0 +1,17 @@ +package io.metersphere.log.utils.json.diff; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.metersphere.log.utils.json.diff.jsonwrap.gson.GsonWrapper; + +public class GsonDiff extends JsonDiff { + + public GsonDiff() { + super(new GsonWrapper()); + } + + public JsonObject diff(JsonElement from, JsonElement to) throws IllegalArgumentException { + return (JsonObject) super.diff(from, to); + } +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/IncavaDiff.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/IncavaDiff.java new file mode 100644 index 0000000000..27c27ff076 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/IncavaDiff.java @@ -0,0 +1,500 @@ +package io.metersphere.log.utils.json.diff; + +import java.util.*; + +/* + Copyright (c) 2009, incava.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + + * Neither the name of incava.org nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Compares two lists, returning a list of the additions, changes, and deletions between them. A Comparator + * may be passed as an argument to the constructor, and will thus be used. If not provided, the initial value in the + * a ("from") list will be looked at to see if it supports the Comparable interface. If so, + * its equals and compareTo methods will be invoked on the instances in the "from" and "to" + * lists; otherwise, for speed, hash codes from the objects will be used instead for comparison. + * + *

+ * The file FileDiff.java shows an example usage of this class, in an application similar to the Unix "diff" program. + *

+ */ +public class IncavaDiff +{ + + /** + * The source list, AKA the "from" values. + */ + protected List a; + + /** + * The target list, AKA the "to" values. + */ + protected List b; + + /** + * The list of differences, as Difference instances. + */ + protected List diffs = new ArrayList(); + + /** + * The pending, uncommitted difference. + */ + private IncavaEntry pending; + + /** + * The comparator used, if any. + */ + private Comparator comparator; + + /** + * The thresholds. + */ + private TreeMap thresh; + + + /** + * Constructs the Diff object for the two arrays, using the given comparator. + */ + public IncavaDiff(Type[] a, Type[] b, Comparator comp) + { + this(Arrays.asList(a), Arrays.asList(b), comp); + } + + + /** + * Constructs the Diff object for the two arrays, using the default comparison mechanism between the objects, such + * as equals and compareTo. + */ + public IncavaDiff(Type[] a, Type[] b) + { + this(a, b, null); + } + + + /** + * Constructs the Diff object for the two lists, using the given comparator. + */ + public IncavaDiff(List a, List b, Comparator comp) + { + this.a = a; + this.b = b; + this.comparator = comp; + this.thresh = null; + } + + + /** + * Constructs the Diff object for the two lists, using the default comparison mechanism between the objects, such as + * equals and compareTo. + */ + public IncavaDiff(List a, List b) + { + this(a, b, null); + } + + + /** + * Runs diff and returns the results. + */ + public List diff() + { + traverseSequences(); + + // add the last difference, if pending: + if (pending != null) { + diffs.add(pending); + } + + return diffs; + } + + + /** + * Traverses the sequences, seeking the longest common subsequences, invoking the methods finishedA, + * finishedB, onANotB, and onBNotA. + */ + protected void traverseSequences() + { + Integer[] matches = getLongestCommonSubsequences(); + + int lastA = a.size() - 1; + int lastB = b.size() - 1; + int bi = 0; + int ai; + + int lastMatch = matches.length - 1; + + for (ai = 0; ai <= lastMatch; ++ai) { + Integer bLine = matches[ai]; + + if (bLine == null) { + onANotB(ai, bi); + } + else { + while (bi < bLine) { + onBNotA(ai, bi++); + } + + onMatch(ai, bi++); + } + } + + boolean calledFinishA = false; + boolean calledFinishB = false; + + while (ai <= lastA || bi <= lastB) { + + // last A? + if (ai == lastA + 1 && bi <= lastB) { + if (!calledFinishA && callFinishedA()) { + finishedA(lastA); + calledFinishA = true; + } + else { + while (bi <= lastB) { + onBNotA(ai, bi++); + } + } + } + + // last B? + if (bi == lastB + 1 && ai <= lastA) { + if (!calledFinishB && callFinishedB()) { + finishedB(lastB); + calledFinishB = true; + } + else { + while (ai <= lastA) { + onANotB(ai++, bi); + } + } + } + + if (ai <= lastA) { + onANotB(ai++, bi); + } + + if (bi <= lastB) { + onBNotA(ai, bi++); + } + } + } + + + /** + * Override and return true in order to have finishedA invoked at the last element in the + * a array. + */ + protected boolean callFinishedA() + { + return false; + } + + + /** + * Override and return true in order to have finishedB invoked at the last element in the + * b array. + */ + protected boolean callFinishedB() + { + return false; + } + + + /** + * Invoked at the last element in a, if callFinishedA returns true. + */ + protected void finishedA(int lastA) + { + } + + + /** + * Invoked at the last element in b, if callFinishedB returns true. + */ + protected void finishedB(int lastB) + { + } + + + /** + * Invoked for elements in a and not in b. + */ + protected void onANotB(int ai, int bi) + { + if (pending == null) { + pending = new IncavaEntry(ai, ai, bi, -1); + } + else { + pending.setDeleted(ai); + } + } + + + /** + * Invoked for elements in b and not in a. + */ + protected void onBNotA(int ai, int bi) + { + if (pending == null) { + pending = new IncavaEntry(ai, -1, bi, bi); + } + else { + pending.setAdded(bi); + } + } + + + /** + * Invoked for elements matching in a and b. + */ + protected void onMatch(int ai, int bi) + { + if (pending == null) { + // no current pending + } + else { + diffs.add(pending); + pending = null; + } + } + + + /** + * Compares the two objects, using the comparator provided with the constructor, if any. + */ + protected boolean equals(Type x, Type y) + { + return comparator == null ? x.equals(y) : comparator.compare(x, y) == 0; + } + + + /** + * Returns an array of the longest common subsequences. + */ + public Integer[] getLongestCommonSubsequences() + { + int aStart = 0; + int aEnd = a.size() - 1; + + int bStart = 0; + int bEnd = b.size() - 1; + + TreeMap matches = new TreeMap(); + + while (aStart <= aEnd && bStart <= bEnd && equals(a.get(aStart), b.get(bStart))) { + matches.put(aStart++, bStart++); + } + + while (aStart <= aEnd && bStart <= bEnd && equals(a.get(aEnd), b.get(bEnd))) { + matches.put(aEnd--, bEnd--); + } + + Map> bMatches = null; + if (comparator == null) { + if (a.size() > 0 && a.get(0) instanceof Comparable) { + // this uses the Comparable interface + bMatches = new TreeMap>(); + } + else { + // this just uses hashCode() + bMatches = new HashMap>(); + } + } + else { + // we don't really want them sorted, but this is the only Map + // implementation (as of JDK 1.4) that takes a comparator. + bMatches = new TreeMap>(comparator); + } + + for (int bi = bStart; bi <= bEnd; ++bi) { + Type element = b.get(bi); + Type key = element; + List positions = bMatches.get(key); + + if (positions == null) { + positions = new ArrayList(); + bMatches.put(key, positions); + } + + positions.add(bi); + } + + thresh = new TreeMap(); + Map links = new HashMap(); + + for (int i = aStart; i <= aEnd; ++i) { + Type aElement = a.get(i); + List positions = bMatches.get(aElement); + + if (positions != null) { + Integer k = 0; + ListIterator pit = positions.listIterator(positions.size()); + while (pit.hasPrevious()) { + Integer j = pit.previous(); + + k = insert(j, k); + + if (k == null) { + // nothing + } + else { + Object value = k > 0 ? links.get(k - 1) : null; + links.put(k, new Object[] { value, i, j }); + } + } + } + } + + if (thresh.size() > 0) { + Integer ti = thresh.lastKey(); + Object[] link = (Object[]) links.get(ti); + while (link != null) { + Integer x = (Integer) link[1]; + Integer y = (Integer) link[2]; + matches.put(x, y); + link = (Object[]) link[0]; + } + } + + int size = matches.size() == 0 ? 0 : 1 + matches.lastKey(); + Integer[] ary = new Integer[size]; + for (Integer idx : matches.keySet()) { + Integer val = matches.get(idx); + ary[idx] = val; + } + return ary; + } + + + /** + * Returns whether the integer is not zero (including if it is not null). + */ + protected static boolean isNonzero(Integer i) + { + return i != null && i != 0; + } + + + /** + * Returns whether the value in the map for the given index is greater than the given value. + */ + protected boolean isGreaterThan(Integer index, Integer val) + { + Integer lhs = thresh.get(index); + return lhs != null && val != null && lhs.compareTo(val) > 0; + } + + + /** + * Returns whether the value in the map for the given index is less than the given value. + */ + protected boolean isLessThan(Integer index, Integer val) + { + Integer lhs = thresh.get(index); + return lhs != null && (val == null || lhs.compareTo(val) < 0); + } + + + /** + * Returns the value for the greatest key in the map. + */ + protected Integer getLastValue() + { + return thresh.get(thresh.lastKey()); + } + + + /** + * Adds the given value to the "end" of the threshold map, that is, with the greatest index/key. + */ + protected void append(Integer value) + { + Integer addIdx = null; + if (thresh.size() == 0) { + addIdx = 0; + } + else { + Integer lastKey = thresh.lastKey(); + addIdx = lastKey + 1; + } + thresh.put(addIdx, value); + } + + + /** + * Inserts the given values into the threshold map. + */ + protected Integer insert(Integer j, Integer k) + { + if (isNonzero(k) && isGreaterThan(k, j) && isLessThan(k - 1, j)) { + thresh.put(k, j); + } + else { + int high = -1; + + if (isNonzero(k)) { + high = k; + } + else if (thresh.size() > 0) { + high = thresh.lastKey(); + } + + // off the end? + if (high == -1 || j.compareTo(getLastValue()) > 0) { + append(j); + k = high + 1; + } + else { + // binary search for insertion point: + int low = 0; + + while (low <= high) { + int index = (high + low) / 2; + Integer val = thresh.get(index); + int cmp = j.compareTo(val); + + if (cmp == 0) { + return null; + } + else if (cmp > 0) { + low = index + 1; + } + else { + high = index - 1; + } + } + + thresh.put(low, j); + k = low; + } + } + + return k; + } +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/IncavaEntry.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/IncavaEntry.java new file mode 100644 index 0000000000..e601774a56 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/IncavaEntry.java @@ -0,0 +1,167 @@ +package io.metersphere.log.utils.json.diff; + +/* + Copyright (c) 2009, incava.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + + * Neither the name of incava.org nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * Represents a difference, as used in Diff. A difference consists of two pairs of starting and ending + * points, each pair representing either the "from" or the "to" collection passed to Diff. If an ending + * point is -1, then the difference was either a deletion or an addition. For example, if getDeletedEnd() + * returns -1, then the difference represents an addition. + */ +public class IncavaEntry +{ + + public static final int NONE = -1; + + /** + * The point at which the deletion starts. + */ + private int delStart = NONE; + + /** + * The point at which the deletion ends. + */ + private int delEnd = NONE; + + /** + * The point at which the addition starts. + */ + private int addStart = NONE; + + /** + * The point at which the addition ends. + */ + private int addEnd = NONE; + + + /** + * Creates the difference for the given start and end points for the deletion and addition. + */ + public IncavaEntry(int delStart, int delEnd, int addStart, int addEnd) + { + this.delStart = delStart; + this.delEnd = delEnd; + this.addStart = addStart; + this.addEnd = addEnd; + } + + + /** + * The point at which the deletion starts, if any. A value equal to NONE means this is an addition. + */ + public int getDeletedStart() + { + return delStart; + } + + + /** + * The point at which the deletion ends, if any. A value equal to NONE means this is an addition. + */ + public int getDeletedEnd() + { + return delEnd; + } + + + /** + * The point at which the addition starts, if any. A value equal to NONE means this must be an + * addition. + */ + public int getAddedStart() + { + return addStart; + } + + + /** + * The point at which the addition ends, if any. A value equal to NONE means this must be an addition. + */ + public int getAddedEnd() + { + return addEnd; + } + + + /** + * Sets the point as deleted. The start and end points will be modified to include the given line. + */ + public void setDeleted(int line) + { + delStart = Math.min(line, delStart); + delEnd = Math.max(line, delEnd); + } + + + /** + * Sets the point as added. The start and end points will be modified to include the given line. + */ + public void setAdded(int line) + { + addStart = Math.min(line, addStart); + addEnd = Math.max(line, addEnd); + } + + + /** + * Compares this object to the other for equality. Both objects must be of type Difference, with the same starting + * and ending points. + */ + @Override + public boolean equals(Object obj) + { + if (obj instanceof IncavaEntry) { + IncavaEntry other = (IncavaEntry) obj; + + return (delStart == other.delStart && + delEnd == other.delEnd && + addStart == other.addStart && addEnd == other.addEnd); + } + else { + return false; + } + } + + + /** + * Returns a string representation of this difference. + */ + @Override + public String toString() + { + StringBuffer buf = new StringBuffer(); + buf.append("del: [" + delStart + ", " + delEnd + "]"); + buf.append(" "); + buf.append("add: [" + addStart + ", " + addEnd + "]"); + return buf.toString(); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/Jackson2Diff.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/Jackson2Diff.java new file mode 100644 index 0000000000..2812916257 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/Jackson2Diff.java @@ -0,0 +1,11 @@ +package io.metersphere.log.utils.json.diff; + +import io.metersphere.log.utils.json.diff.jsonwrap.jackson2.Jackson2Wrapper; + +public class Jackson2Diff extends JsonDiff { + + public Jackson2Diff() { + super(new Jackson2Wrapper()); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/JacksonDiff.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/JacksonDiff.java new file mode 100644 index 0000000000..88b3ca8db2 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/JacksonDiff.java @@ -0,0 +1,11 @@ +package io.metersphere.log.utils.json.diff; + +import io.metersphere.log.utils.json.diff.jsonwrap.jackson.JacksonWrapper; + +public class JacksonDiff extends JsonDiff { + + public JacksonDiff() { + super(new JacksonWrapper()); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/JsonDiff.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/JsonDiff.java new file mode 100644 index 0000000000..fb50b00d9c --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/JsonDiff.java @@ -0,0 +1,537 @@ +package io.metersphere.log.utils.json.diff; + +import com.google.gson.JsonPrimitive; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonArray; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonObject; +import io.metersphere.log.utils.json.diff.jsonwrap.Wrapper; +import io.metersphere.log.utils.json.diff.jsonwrap.gson.GsonJsonPrimitive; +import io.metersphere.log.utils.json.diff.jsonwrap.jackson.JacksonJsonObject; +import org.apache.commons.lang3.StringUtils; + +import java.util.*; +import java.util.Map.Entry; +import java.util.logging.Logger; + +/** + * Util for comparing two json-objects and create a new object with a set of instructions to transform the first to the second. + * + *

+ * Syntax for instructions: + * + *

+ * 
+ * {
+ *   "key":     "replaced",           // added or replacing key
+ *   "~key":    "replaced",           // added or replacing key (~ doesn't matter for primitive data types)
+ *   "key":     null,                 // added or replacing key with null.
+ *   "~key":    null,                 // added or replacing key with null (~ doesn't matter for null)
+ *   "-key":    0                     // key removed (value is ignored)
+ *   "key":     { "sub": "replaced" } // whole object "key" replaced
+ *   "~key":    { "sub": "merged" }   // key "sub" merged into object "key", rest of object untouched
+ *   "key":     [ "replaced" ]        // whole array added/replaced
+ *   "~key":    [ "replaced" ]        // whole array added/replaced (~ doesn't matter for whole array)
+ *   "key[4]":  { "sub": "replaced" } // object replacing element 4, rest of array untouched
+ *   "~key[4]": { "sub": "merged"}    // merging object at element 4, rest of array untouched
+ *   "key[+4]": { "sub": "array add"} // object inserted after 3 becoming the new 4 (current 4 pushed right)
+ *   "~key[+4]":{ "sub": "array add"} // object inserted after 3 becoming the new 4 (current 4 pushed right)
+ *   "-key[4]:  0                     // removing element 4 current 5 becoming new 4 (value is ignored)
+ * }
+ * 
+ * 
+ * + *

+ * Instruction order is merge, set, insert, delete. This is important when altering arrays, since insertions will affect the array index of subsequent delete instructions. + *

+ * + *

+ * When diffing, the object is expanded to a structure like this: Example: {a:[{b:1,c:2},{d:3}]} + * Becomes a list of: + *

    + *
  1. Leaf: obj + *
  2. Leaf: array 0 + *
  3. Leaf: obj + *
  4. Leaf: b: 1 + *
  5. Leaf: c: 2 + *
  6. Leaf: array 1 + *
  7. Leaf: obj + *
  8. Leaf: d: 3 + *
+ * + * @author Martin Algesten + */ +public class JsonDiff { + + static final String MOD = "~"; + static final String DIFF_ADD = "++"; + static final String DIFF_EDIT = "**"; + static final String DIFF_DEL = "--"; + + static class Instruction { + Oper oper; + int index; + String key; + + boolean isIndexed() { + return index > -1; + } + } + + static final Logger LOG = Logger.getLogger(JsonDiff.class.getName()); + + protected final Wrapper factory; + + final static Comparator> INSTRUCTIONS_COMPARATOR = new Comparator>() { + + @Override + public int compare(Entry o1, Entry o2) { + if (o1.getKey().startsWith(MOD) && !o2.getKey().startsWith(MOD)) { + return 1; + } else if (!o1.getKey().startsWith(MOD) && o2.getKey().startsWith(MOD)) { + return -1; + } + return o1.getKey().compareTo(o2.getKey()); + } + }; + + final static Comparator> OBJECT_KEY_COMPARATOR = new Comparator>() { + + @Override + public int compare(Entry o1, Entry o2) { + return o1.getKey().compareTo(o2.getKey()); + + } + }; + + @SuppressWarnings("rawtypes") + private Visitor visitor; + + JsonDiff(Wrapper factory) { + this.factory = factory; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + boolean accept(Leaf leaf, JzonArray instructions, JzonObject childPatch) { + JzonObject object = (JzonObject) factory.parse(leaf.val.toString()); + JzonObject patch = factory.createJsonObject(); + patch.add(MOD, instructions); + if (!childPatch.entrySet().isEmpty()) { + patch.entrySet().addAll((Collection) childPatch.entrySet()); + } + apply(object, patch); + return visitor.shouldCreatePatch(leaf.val.unwrap(), object.unwrap()); + } + + void apply(JzonElement origEl, JzonElement patchEl) throws IllegalArgumentException { + + JzonObject patch = (JzonObject) patchEl; + Set> memb = new TreeSet>(INSTRUCTIONS_COMPARATOR); + memb.addAll(patch.entrySet()); + for (Entry entry : memb) { + String key = entry.getKey(); + JzonElement value = entry.getValue(); + if (key.startsWith(MOD)) { + JzonElement partialInstructions = entry.getValue(); + if (!partialInstructions.isJsonArray()) { + throw new IllegalArgumentException(); + } + JzonArray array = (JzonArray) partialInstructions; + JzonElement applyTo; + if (key.equals(MOD)) { + applyTo = origEl; + } else if (origEl.isJsonArray()) { + int index = Integer.parseInt(key.substring(1)); + applyTo = ((JzonArray) origEl).get(index); + } else { + applyTo = ((JzonObject) origEl).get(key.substring(1)); + } + for (int i = 0; i < array.size(); i++) { + JzonElement partial = array.get(i); + if (!partial.isJsonObject()) { + throw new IllegalArgumentException(); + } + Entry childentry = ((JzonObject) partial).entrySet().iterator().next(); + String childKey = childentry.getKey(); + Instruction instruction = create(childKey); + boolean newAppliance = false; + if (instruction.isIndexed() && !applyTo.isJsonArray()) { + applyTo = factory.createJsonArray(); + newAppliance = true; + } else if (!instruction.isIndexed() && !applyTo.isJsonObject()) { + applyTo = factory.createJsonObject(); + newAppliance = true; + } + if (newAppliance) { + if (origEl.isJsonArray()) { + int index = Integer.parseInt(key); + ((JzonArray) origEl).insert(index, applyTo); + } else { + ((JzonObject) origEl).add(key.substring(1), applyTo); + } + } + applyPartial(applyTo, instruction, childentry.getValue()); + } + } else { + Instruction instruction = create(key); + if (instruction.oper == Oper.INSERT || instruction.oper == Oper.DELETE) { + applyPartial(origEl, instruction, value); + } else if (instruction.isIndexed()) { + if (!origEl.isJsonArray()) { + throw new IllegalArgumentException(); + } + if (value.isJsonPrimitive()) { + ((JzonArray) origEl).set(instruction.index, value); + } else { + if (((JzonArray) origEl).size() <= instruction.index) { + throw new IllegalArgumentException("Wrong index " + instruction.index + " for " + origEl); + } + JzonElement childEl = ((JzonArray) origEl).get(instruction.index); + apply(childEl, value); + } + } else if (origEl.isJsonObject()) { + if (value.isJsonPrimitive() || value.isJsonNull()) { + ((JzonObject) origEl).add(key, value); + } else { + JzonElement childEl = ((JzonObject) origEl).get(key); + apply(childEl, value); + } + } else { + throw new IllegalArgumentException(); + } + } + } + + } + + /** + * Patches the first argument with the second. Accepts two GSON JsonObject or (if jar is provided) a Jackson style ObjectNode. + * + * @param orig Object to patch. One of JsonObject or ObjectNode (if jar available). + * @param patch Object holding patch instructions. One of JsonObject or ObjectNode (if jar available). + * @throws IllegalArgumentException if the given arguments are not accepted. + */ + public void apply(Object orig, Object patch) { + + JzonElement origEl = factory.wrap(orig); + JzonElement patchEl = factory.wrap(patch); + + apply(origEl, patchEl); + + } + + /** + * Modifies the given original JSON object using the instructions provided and returns the result. Each argument is expected to be a JSON object {}. + * + * @param orig The original JSON object to modify. + * @param patch The set of instructions to use. + * @return The modified JSON object. + * @throws IllegalArgumentException if the given arguments are not accepted. + */ + public String apply(String orig, String patch) throws IllegalArgumentException { + + // by providing null as hint we default to GSON. + JzonElement origEl = factory.parse(orig); + JzonElement patchEl = factory.parse(patch); + + apply(origEl, patchEl); + + return origEl.toString(); + + } + + void applyPartial(JzonElement applyTo, Instruction instruction, JzonElement value) { + if (instruction.oper == Oper.DELETE) { + if (instruction.isIndexed()) { + if (((JzonArray) applyTo).size() < instruction.index) { + throw new IllegalArgumentException("Wrong index " + instruction.index + " for " + applyTo); + } + // 修改标记,增加IF内容 + if (value.isJsonPrimitive()) { + String valueJson = DIFF_DEL + (value != null && StringUtils.isNotEmpty(value.toString()) ? value.toString().substring(1, value.toString().length() - 1) : ""); + JsonPrimitive gsonJsonPrimitive = new JsonPrimitive(valueJson); + GsonJsonPrimitive primitive = new GsonJsonPrimitive(gsonJsonPrimitive); + ((JzonArray) applyTo).insert(instruction.index, primitive); + } else { + try { + JacksonJsonObject object = (JacksonJsonObject) value; + if (object != null && object.get("name") != null) { + object.add(DIFF_DEL + "name", object.get("name")); + } else { + object.add(DIFF_DEL + "name", new JacksonJsonObject(null)); + } + if (instruction.index > 0 && ((JzonArray) applyTo).size() == instruction.index) { + ((JzonArray) applyTo).insert(instruction.index, object); + } else { + ((JzonArray) applyTo).set(instruction.index, object); + } + } catch (Exception exception) { + ((JzonArray) applyTo).remove(instruction.index); + } + } + } else { + ((JzonObject) applyTo).add(DIFF_DEL + instruction.key, value); + } + } else if (instruction.oper == Oper.INSERT) { + if (instruction.isIndexed()) { + if (((JzonArray) applyTo).size() < instruction.index) { + throw new IllegalArgumentException("Wrong index " + instruction.index + " for " + applyTo); + } + // 修改标记,增加IF内容 + if (value.isJsonPrimitive()) { + String valueJson = DIFF_ADD + (value != null && StringUtils.isNotEmpty(value.toString()) ? value.toString().substring(1, value.toString().length() - 1) : ""); + JsonPrimitive gsonJsonPrimitive = new JsonPrimitive(valueJson); + GsonJsonPrimitive primitive = new GsonJsonPrimitive(gsonJsonPrimitive); + ((JzonArray) applyTo).set(instruction.index, primitive); + } else { + try { + JacksonJsonObject object = (JacksonJsonObject) value; + if (object != null && object.get("name") != null) { + object.add(DIFF_ADD + "name", object.get("name")); + } else { + object.add(DIFF_ADD + "name", new JacksonJsonObject(null)); + } + if (instruction.index > 0 && ((JzonArray) applyTo).size() == instruction.index) { + ((JzonArray) applyTo).set(instruction.index - 1, object); + } else { + ((JzonArray) applyTo).set(instruction.index, object); + } + } catch (Exception exception) { + ((JzonArray) applyTo).insert(instruction.index, value); + } + } + } else { + ((JzonObject) applyTo).remove(instruction.key); + ((JzonObject) applyTo).add(DIFF_ADD + instruction.key, value); + } + } else if (applyTo.isJsonArray()) { + if (((JzonArray) applyTo).size() <= instruction.index) { + throw new IllegalArgumentException("Wrong index " + instruction.index + " for " + applyTo); + } + ((JzonArray) applyTo).set(instruction.index, value); + } else { + ((JzonObject) applyTo).add(DIFF_EDIT + instruction.key, value); + } + } + + void checkIndex(JzonElement applyTo, int index) { + if (((JzonArray) applyTo).size() < index) { + throw new IllegalArgumentException(); + } + } + + Instruction create(String childKey) { + Instruction instruction = new Instruction(); + if (childKey.startsWith("-")) { + instruction.key = childKey.substring(1); + instruction.index = isIndexed(instruction.key); + instruction.oper = Oper.DELETE; + } else if (childKey.startsWith("+")) { + instruction.key = childKey.substring(1); + instruction.index = isIndexed(instruction.key); + instruction.oper = Oper.INSERT; + } else { + instruction.key = childKey; + instruction.index = isIndexed(instruction.key); + instruction.oper = Oper.SET; + } + return instruction; + } + + JzonObject diff(JzonElement fromEl, JzonElement toEl) { + + if (!fromEl.isJsonObject()) { + throw new IllegalArgumentException("From is not a json object"); + } + if (!toEl.isJsonObject()) { + throw new IllegalArgumentException("To is not a json object"); + } + + JzonObject from = (JzonObject) fromEl; + JzonObject to = (JzonObject) toEl; + + Root fromRoot = new Root(); + Root toRoot = new Root(); + + ArrayList fromLeaves = new ArrayList(); + ArrayList toLeaves = new ArrayList(); + + HashMap fromArrs = new HashMap(); + HashMap toArrs = new HashMap(); + + findLeaves(fromRoot, from, fromLeaves, fromArrs); + findLeaves(toRoot, to, toLeaves, toArrs); + + IncavaDiff idiff = new IncavaDiff(fromLeaves, toLeaves); + + List diff = idiff.diff(); + int delta = 0; + // be careful with direct use of indexOf: need instance equality, not equals! + for (IncavaEntry incavaEntry : diff) { + int deletes = Math.max(0, incavaEntry.getDeletedEnd() - incavaEntry.getDeletedStart() + 1); + int insertionIndex = (incavaEntry.getDeletedStart() > 0) ? incavaEntry.getDeletedStart() + delta - 1 : 0; + Leaf fromLeaf = (fromLeaves.size() > insertionIndex) ? fromLeaves.get(insertionIndex) : fromLeaves.get(fromLeaves.size() - 1); + for (int i = incavaEntry.getDeletedStart(); i < incavaEntry.getDeletedEnd() + 1; i++) { + // ensure not orphan + fromLeaf.recover(fromLeaves); + // proceed to delete + Leaf toLeaf = fromLeaves.get(i + delta); + fromLeaf.delete(toLeaf, null); + fromLeaf = toLeaf; + } + if (incavaEntry.getAddedEnd() < 0) { + continue; + } + fromLeaf = (fromLeaves.size() > insertionIndex) ? fromLeaves.get(insertionIndex) : fromLeaves.get(fromLeaves.size() - 1); + while (fromLeaf.oper == Oper.DELETE && insertionIndex > 0) { + // find a NOT deleted node for set / insertion - parent traversal will be done later + insertionIndex--; + fromLeaf = fromLeaves.get(insertionIndex); + } + for (int i = incavaEntry.getAddedStart(); i < incavaEntry.getAddedEnd() + 1; i++) { + // ensure not orphan + fromLeaf.recover(fromLeaves); + Leaf toLeaf = toLeaves.get(i); + if (deletes > 0) { + deletes--; + Leaf deleted = fromLeaves.get(incavaEntry.getDeletedStart() + delta + (i - incavaEntry.getAddedStart())); + deleted.recover(fromLeaves); + if (!fromLeaf.cancelDelete(deleted, toLeaf)) { + // couldn't cancel delete (different obj key): INSERT + fromLeaf.insert(toLeaf, null); + fromLeaves.add(insertionIndex + 1, toLeaf); + fromLeaf = toLeaf; + delta++; + } else { + // cancel delete: pure SET + fromLeaf = deleted; + } + } else { + // regular INSERT + fromLeaf.insert(toLeaf, null); + fromLeaves.add(insertionIndex + 1, toLeaf); + fromLeaf = toLeaf; + delta++; + } + insertionIndex++; + } + } + // recover all pending orphans: this could be easily optimized + int i = 0; + for (Leaf fromLeaf : fromLeaves) { + if (fromLeaf.isOrphan()) { + fromLeaf.recover(i, fromLeaves); + } + i++; + } + JzonObject patch = fromLeaves.iterator().next().patch(); + // prints the new structure + // fromLeaves.iterator().next().print(); + return patch; + + } + + /** + * Runs a diff using underlying JSON parser implementations. Accepts two GSON JsonObject or (if jar is provided) a Jackson style ObjectNode. The returned type + * is the same as the received. + * + * @param from Object to transform from. One of JsonObject or ObjectNode (if jar available). + * @param to Object to transform to. One of JsonObject or ObjectNode (if jar available). + * @return Object containing the instructions. The type will be the same as that passed in constructor. + * @throws IllegalArgumentException if the given arguments are not accepted. + */ + public Object diff(Object from, Object to) throws IllegalArgumentException { + + JzonElement fromEl = factory.wrap(from); + JzonElement toEl = factory.wrap(to); + + JzonObject diff = diff(fromEl, toEl); + + return diff.unwrap(); + } + + /** + * Runs a diff on the two given JSON objects given as string to produce another JSON object with instructions of how to transform the first argument to the second. Both from/to + * are expected to be objects {}. + * + * @param from The origin to transform + * @param to The desired result + * @return The set of instructions to go from to as a JSON object {}. + * @throws IllegalArgumentException if the given arguments are not accepted. + */ + public String diff(String from, String to) throws IllegalArgumentException { + + JzonElement fromEl = factory.parse(from); + JzonElement toEl = factory.parse(to); + + return diff(fromEl, toEl).toString(); + + } + + Leaf findLeaves(Node parent, JzonElement el, List leaves, HashMap arrs) { + + // create leaf for this part + Leaf leaf = new Leaf(parent, el); + leaf.factory = factory; + if (visitor != null) { + leaf.visitor = this; + } + leaves.add(leaf); + + if (el.isJsonObject()) { + + Set> memb = new TreeSet>(OBJECT_KEY_COMPARATOR); + memb.addAll(((JzonObject) el).entrySet()); + for (Entry e : memb) { + + ObjNode newParent = new ObjNode(parent, e.getKey()); + Leaf child = findLeaves(newParent, e.getValue(), leaves, arrs); + leaf.children.add(child); + } + + } else if (el.isJsonArray()) { + + JzonArray arr = (JzonArray) el; + for (int i = 0, n = arr.size(); i < n; i++) { + + ArrNode newParent = new ArrNode(parent, i); + + // this array saves a reference to all arrnodes + // which is used to adjust arr node indexes. + arrs.put(newParent.doHash(true), newParent); + + Leaf child = findLeaves(newParent, arr.get(i), leaves, arrs); + leaf.children.add(child); + } + + } + leaf.init(); + return leaf; + } + + /** + * @return the registered visitor if any + * @see Visitor + */ + public Visitor getVisitor() { + return visitor; + } + + int isIndexed(String childKey) { + try { + return Integer.parseInt(childKey); + } catch (NumberFormatException e) { + return -1; + } + } + + /** + * Registers a new visitor. + * + * @param visitor - visitor to register + * @see Visitor + */ + public void setVisitor(Visitor visitor) { + this.visitor = visitor; + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/Leaf.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/Leaf.java new file mode 100644 index 0000000000..f871664795 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/Leaf.java @@ -0,0 +1,382 @@ +package io.metersphere.log.utils.json.diff; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonArray; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonObject; +import io.metersphere.log.utils.json.diff.jsonwrap.Wrapper; + +import java.util.*; +import java.util.logging.Level; + +class Leaf implements Comparable { + + Wrapper factory; + + final Node parent; + JzonElement val; + Oper oper; + + List children = new LinkedList(); + List newStructure = new LinkedList(); + + Leaf(Node parent, JzonElement val) { + this.parent = parent; + this.val = val; + this.parent.leaf = this; + } + + boolean attach(Leaf leaf, Leaf at) { + Leaf attach = this; + int myIndex = (parent.parent == null) ? 0 : exactIndex(parent.parent.leaf.newStructure, this); + int atIndex = (at == this) ? 0 : exactIndex(newStructure, at) + 1; + while (leaf.oper != Oper.DELETE && attach.oper == Oper.DELETE && myIndex > 0) { + myIndex--; + attach = parent.parent.leaf.newStructure.get(myIndex); + atIndex = attach.newStructure.size(); + } + if (leaf.parent.parentHashCode == attach.parent.hashCode) { + // direct attachment + if (leaf.oper != Oper.DELETE && attach.oper == Oper.DELETE) { + return attach.parent.parent.leaf.attach(leaf, this); + } + if (leaf.oper == null && (leaf.val.isJsonPrimitive() || leaf.val.isJsonNull())) { + leaf.oper = Oper.SET; + } + attach.newStructure.add(atIndex, leaf); + leaf.rehash(attach); + if (JsonDiff.LOG.isLoggable(Level.FINE)) + JsonDiff.LOG.info("ATT " + leaf + " @" + this); + return true; + } + if (parent.parent == null) { + return false; + } + return parent.parent.leaf.attach(leaf, this); + } + + boolean cancelDelete(Leaf deleted, Leaf with) { + if (deleted.parent.hashCode == with.parent.hashCode) { + if (JsonDiff.LOG.isLoggable(Level.FINE)) + JsonDiff.LOG.info("SET " + deleted + " @" + with); + with.newStructure.clear(); + deleted.oper = Oper.SET; + //deleted.val = with.val; + Leaf newParent = deleted.parent.parent.leaf; + // recover deleted children (orphans) + for (Leaf orphan : deleted.children) { + orphan.parent.parent = deleted.parent; + deleted.newStructure.add(orphan); + } + deleted.rehash(newParent); + return true; + } + return false; + } + + void rehash(Leaf newParent) { + parent.rehash(newParent.parent); + for (Leaf child : newStructure) { + child.rehash(this); + } + } + + Leaf checkCancelation(Leaf possibleCancellation) { + for (Iterator iterator2 = newStructure.iterator(); iterator2.hasNext();) { + Leaf check = iterator2.next(); + if (check != possibleCancellation) { + if (!check.parent.getClass().equals(possibleCancellation.parent.getClass())) { + // type node change: cancellation + return check; + } + if (possibleCancellation.parent instanceof ObjNode && check.parent.hashCode == possibleCancellation.parent.hashCode) { + return check; + } + } + } + return null; + } + + void checkCancellations() { + for (Iterator it = newStructure.iterator(); it.hasNext();) { + Leaf child = it.next(); + if (child.oper == Oper.DELETE) { + Leaf cancelled = checkCancelation(child); + if (cancelled != null) { + if (cancelled.newStructure.isEmpty()) { + cancelled.oper = Oper.SET; + } + it.remove(); + } + } + } + } + + @Override + public int compareTo(Leaf o) { + return hashCode() - o.hashCode(); + } + + JsonDiff visitor; + + JzonArray createPatch(JzonObject patch) { + JzonArray instructions = factory.createJsonArray(); + if (oper != Oper.DELETE) { + checkCancellations(); + int i = 0, deletes = 0; + for (Iterator it = newStructure.iterator(); it.hasNext();) { + Leaf child = it.next(); + String key = child.parent.toString(); + String reIndexedKey = key; + if (child.parent instanceof ArrNode) { + ((ArrNode) child.parent).index = i - deletes; + reIndexedKey = child.parent.toString(); + } + JzonObject insert = factory.createJsonObject(); + boolean deeper = true; + if (child.oper == Oper.INSERT) { + insert.add("+" + reIndexedKey, child.val); + instructions.insert(instructions.size(), insert); + deeper = false; + } else if (child.oper == Oper.SET) { + insert.add(reIndexedKey, child.val); + instructions.insert(instructions.size(), insert); + deeper = false; + } else if (child.oper == Oper.DELETE) { + insert.add("-" + reIndexedKey, child.val); + instructions.insert(instructions.size(), insert); + deeper = false; + } + if (deeper) { + JzonObject childPatch = factory.createJsonObject(); + JzonArray childInstructions = child.createPatch(childPatch); + if (childInstructions.size() > 0) { + if (visitor != null && !child.val.isJsonPrimitive() && !visitor.accept(child, childInstructions, childPatch)) { + continue; + } + patch.add("~" + key, childInstructions); + } + if (!childPatch.entrySet().isEmpty()) { + patch.add(key, childPatch); + } + } + if (child.oper == Oper.DELETE) { + deletes++; + } + i++; + } + } else { + newStructure.clear(); + } + return instructions; + } + + void delete(Leaf leaf, Leaf at) { + if (JsonDiff.LOG.isLoggable(Level.FINE)) + JsonDiff.LOG.info("DELETE " + leaf + " @" + this); + leaf.oper = Oper.DELETE; + for (Leaf orphan : leaf.newStructure) { + orphan.parent.orphan(); + } + leaf.newStructure.clear(); + } + + @Override + public boolean equals(Object obj) { + return hashCode() == ((Leaf) obj).hashCode(); + } + + @Override + public int hashCode() { + int i = parent.hashCode; + if (val.isJsonArray()) { + // for arr and obj we must hash in a type qualifier + // since otherwise changes between these kinds of + // nodes will be considered equal + i = i * 31 + ArrNode.class.hashCode(); + } else if (val.isJsonObject()) { + i = i * 31 + ObjNode.class.hashCode(); + } else { + i = i * 31 + (val.isJsonPrimitive() || val.isJsonNull() ? val.hashCode() : 0); + } + return i; + } + + void init() { + this.parent.hashCode = this.parent.doHash(false); + this.parent.parentHashCode = (this.parent.parent == null) ? 0 : this.parent.parent.doHash(false); + this.newStructure.addAll(children); + } + + void insert(Leaf leaf, Leaf where) { + int hashCode = parent.hashCode; + int insCode = leaf.parent.parent.hashCode; + if (hashCode == 0 || insCode == hashCode) { + // eligible for insertion - check for sets after building the new graph + leaf.oper = Oper.INSERT; + leaf.parent.parent = parent; + leaf.newStructure.clear(); + if (where != null) { + int insertAt = exactIndex(newStructure, where) + 1; + newStructure.add(insertAt, leaf); + } else { + // direct insertion + newStructure.add(0, leaf); + } + if (JsonDiff.LOG.isLoggable(Level.FINE)) + JsonDiff.LOG.info("INSERTed " + leaf + " @" + this); + } else { + orphans(where); + parent.parent.leaf.insert(leaf, this); + } + } + + boolean isOrphan() { + return parent.hashCode != 0 && parent.isOrphan(); + } + + void orphans(Leaf where) { + List orphans = null; + int insertDeletionsIndex = 0; + if ((where == null && !newStructure.isEmpty()) || newStructure.size() == 1) { + orphans = newStructure; + } else if (newStructure.size() > 1) { + insertDeletionsIndex = exactIndex(newStructure, where) + 1; + orphans = newStructure.subList(insertDeletionsIndex, newStructure.size()); + } + if (orphans != null) { + List newOrphans = new ArrayList(); + for (Leaf orphan : orphans) { + if (orphan.oper != Oper.DELETE) { + orphan.parent.parent = null; + Node clone = orphan.parent.clone(); + Leaf leafClone = new Leaf(clone, orphan.val); + leafClone.visitor = visitor; + clone.leaf = leafClone; + leafClone.oper = Oper.DELETE; + newOrphans.add(leafClone); + } else { + newOrphans.add(orphan); + } + } + orphans.clear(); + newStructure.addAll(insertDeletionsIndex, newOrphans); + } + } + + JzonObject patch() { + checkCancellations(); + JzonObject patch = factory.createJsonObject(); + if (oper == Oper.INSERT) { + patch.add("+" + parent.toString(), val); + } else if (oper == Oper.SET) { + patch.add(parent.toString(), val); + } else if (oper == Oper.DELETE) { + patch.add("-" + parent.toString(), val); + } else { + JzonArray childInstructions = createPatch(patch); + if (childInstructions.size() > 0) { + patch.add("~", childInstructions); + } + } + return patch; + } + + void print() { + print(0); + } + + void print(int tab) { + for (Leaf lEntry : newStructure) { + for (int i = 0; i < tab; i++) { + System.out.print("\t"); + } + System.out.println(lEntry); + lEntry.print(tab + 1); + } + } + + protected static int exactIndex(Collection c, Leaf check) { + int i = -1; + for (Leaf l : c) { + i++; + if (l == check) { + return i; + } + } + return i; + } + + void recover(List fromLeaves) { + if (isOrphan()) { + int thisIndex = exactIndex(fromLeaves, this); + recover(thisIndex, fromLeaves); + } + if (parent.parent != null) { + parent.parent.leaf.recover(fromLeaves); + } + } + + void recover(int thisIndex, List fromLeaves) { + if (isOrphan()) { + Leaf newParent = null; + while (newParent == null || (oper != Oper.DELETE && newParent.oper == Oper.DELETE)) { + thisIndex--; + newParent = fromLeaves.get(thisIndex); + if (newParent.isOrphan()) { + newParent.recover(thisIndex, fromLeaves); + } + } + while (newParent.parent.parent != null && newParent.parent.hashCode != parent.parentHashCode) { + newParent = newParent.parent.parent.leaf; + if (newParent.isOrphan()) { + newParent.recover(fromLeaves); + } + } + if (newParent.oper == Oper.DELETE) { + return; + } + if (newParent.attach(this, null)) { + if (JsonDiff.LOG.isLoggable(Level.FINE)) + JsonDiff.LOG.info("RECOVERed " + this + " @" + newParent); + } else { + recover(thisIndex, fromLeaves); + } + } + } + + @Override + public String toString() { + + StringBuilder bld = new StringBuilder(newStructure.size() + "->"); + + bld.append("LEAF"); + if (parent != null && parent instanceof ArrNode) { + bld.append(parent.toString()); + } + bld.append("<"); + if (oper != null) { + bld.append(oper); + bld.append("_"); + } + if (val.isJsonPrimitive() || val.isJsonNull()) { + bld.append("{"); + bld.append(val); + bld.append("}"); + } else { + bld.append(parent.toString()); + bld.append(":"); + bld.append(val); + } + bld.append("_"); + bld.append(hashCode()); + if (parent.isOrphan()) { + bld.append("_ORPHAN"); + } + bld.append(">"); + bld.append("\n"); + + return bld.toString(); + + } + +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/Node.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/Node.java new file mode 100644 index 0000000000..14355ec181 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/Node.java @@ -0,0 +1,46 @@ +package io.metersphere.log.utils.json.diff; + +abstract class Node implements Cloneable { + + // keep the original hash code since we'll be unsetting the parent leaf + int hashCode, parentHashCode; + + Node parent; + Leaf leaf; + + Node(Node parent) { + this.parent = parent; + } + + @Override + protected Node clone() { + try { + return (Node) super.clone(); + } catch (Exception e) { + return null; + } + } + + abstract int doHash(boolean indexed); + + abstract void rehash(Node newParent); + + @Override + public int hashCode() { + return doHash(false); + } + + boolean isOrphan() { + return hashCode != 0 && parent == null; + } + + void orphan() { + parent = null; + if (leaf != null) { + for (Leaf c : leaf.newStructure) { + c.parent.orphan(); + } + leaf.newStructure.clear(); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/ObjNode.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/ObjNode.java new file mode 100644 index 0000000000..6049e9813e --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/ObjNode.java @@ -0,0 +1,42 @@ +package io.metersphere.log.utils.json.diff; + +class ObjNode extends Node { + + final String key; + + ObjNode(Node parent, String key) { + super(parent); + this.key = key; + } + + @Override + void rehash(Node newParent) { + this.parent = newParent; + this.parentHashCode = newParent.hashCode; + int i = this.parentHashCode; + + i = i * 31 + ObjNode.class.hashCode(); + i = i * 31 + key.hashCode(); + hashCode = i; + } + + + @Override + int doHash(boolean indexed) { + + // just pass through the arguments as is since + // it's the arr node that alters them. + int i = parent.doHash(indexed); + + i = i * 31 + ObjNode.class.hashCode(); + i = i * 31 + key.hashCode(); + return i; + + } + + @Override + public String toString() { + return key; + } + +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/Oper.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/Oper.java new file mode 100644 index 0000000000..9e8d2328ec --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/Oper.java @@ -0,0 +1,5 @@ +package io.metersphere.log.utils.json.diff; + +enum Oper { + INSERT, DELETE, SET +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/Root.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/Root.java new file mode 100644 index 0000000000..065c7bd065 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/Root.java @@ -0,0 +1,28 @@ +package io.metersphere.log.utils.json.diff; + +class Root extends Node { + + Root() { + super(null); + } + + @Override + protected Node clone() { + return this; + } + + @Override + int doHash(boolean indexed) { + return 0; + } + + @Override + public String toString() { + return "root"; + } + + @Override + void rehash(Node newParent) { + throw new IllegalStateException(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/Visitor.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/Visitor.java new file mode 100644 index 0000000000..c759da6b1f --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/Visitor.java @@ -0,0 +1,21 @@ +package io.metersphere.log.utils.json.diff; + + +/** + * Interface that allows filtering patch instructions. + * + * @since 2.0.0 + */ +public interface Visitor { + + /** + * Should a patch instruction be created for an element like to if its destiny is an element like to? + * + * @param from + * - from element + * @param to + * - to element + * @return if the instruction should be created + */ + boolean shouldCreatePatch(E from, E to); +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonArray.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonArray.java new file mode 100644 index 0000000000..426c94a8e9 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonArray.java @@ -0,0 +1,52 @@ +package io.metersphere.log.utils.json.diff.jsonwrap; + +/** + * Common abstaraction for a json array. + * + * @since 1.0.0 + */ +public interface JzonArray extends JzonElement { + + /** + * @return array size + */ + int size(); + + /** + * Returns element at given index. + * + * @param index + * - index to retreive element from + * @return element at given index + */ + JzonElement get(int index); + + /** + * Inserts element at given index. + * + * @param index + * - index to insert element at + * @param el + * - element to insert + */ + void insert(int index, JzonElement el); + + /** + * Sets element at given index. + * + * @param index + * - index to set element to + * @param el + * - element to set + */ + void set(int index, JzonElement el); + + /** + * Remove element at given index. + * + * @param index + * - index to remove element from + */ + void remove(int index); + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonElement.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonElement.java new file mode 100644 index 0000000000..cb66cc817b --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonElement.java @@ -0,0 +1,35 @@ +package io.metersphere.log.utils.json.diff.jsonwrap; + +/** + * Common abstraction for json elements. + * + * @since 1.0.0 + */ +public interface JzonElement { + + /** + * @return if this element is an object + */ + boolean isJsonObject(); + + /** + * @return if this element is an array + */ + boolean isJsonArray(); + + /** + * @return if this element is a primitive value + */ + boolean isJsonPrimitive(); + + /** + * @return if this element is null + */ + boolean isJsonNull(); + + /** + * @return the underlying implementation element + */ + Object unwrap(); + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonNull.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonNull.java new file mode 100644 index 0000000000..5c5fed74ef --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonNull.java @@ -0,0 +1,10 @@ +package io.metersphere.log.utils.json.diff.jsonwrap; + +/** + * Common abstraction for json null. + * + * @since 1.0.0 + */ +public interface JzonNull extends JzonElement { + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonObject.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonObject.java new file mode 100644 index 0000000000..50f9e0820a --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonObject.java @@ -0,0 +1,60 @@ +package io.metersphere.log.utils.json.diff.jsonwrap; + +import java.util.Collection; +import java.util.Map.Entry; + +/** + * Common abstraction for json objects. + * + * @since 1.0.0 + */ +public interface JzonObject extends JzonElement { + + /** + * Returns if this object has an element with the given key. + * + * @param key + * - the key to look for + * @return if this object has a key named liked that + */ + boolean has(String key); + + /** + * Adds an element with the given key. + * + * @param key + * - the key for the new element + */ + void add(String key, JzonElement prop); + + /** + * Adds an integer with the given key. + * + * @param key + * - the key for the new integer + */ + void addProperty(String key, int prop); + + /** + * @return entry set with key / value pairs + */ + Collection> entrySet(); + + /** + * Returns element at given key. + * + * @param key + * - key to look for + * @return the element at that key if any + */ + JzonElement get(String key); + + /** + * Removes an element with the given key. + * + * @param key + * - key to look for + */ + void remove(String key); + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonPrimitive.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonPrimitive.java new file mode 100644 index 0000000000..96dc1e92fc --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/JzonPrimitive.java @@ -0,0 +1,10 @@ +package io.metersphere.log.utils.json.diff.jsonwrap; + +/** + * Common abstraction for json primitive values. + * + * @since 1.0.0 + */ +public interface JzonPrimitive extends JzonElement { + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/Wrapper.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/Wrapper.java new file mode 100644 index 0000000000..511692d8c3 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/Wrapper.java @@ -0,0 +1,38 @@ +package io.metersphere.log.utils.json.diff.jsonwrap; + +/** + * Factory wrapper interface for multiple json implementations. + * + * @since 1.0.0 + */ +public interface Wrapper { + + /** + * Parses an element given a string. + * + * @param json + * - string + * @return parsed element + */ + JzonElement parse(String json); + + /** + * Wraps a given json element. + * + * @param o + * - element to wrap + * @return the wrapped element + */ + JzonElement wrap(Object o); + + /** + * @return a new implementation independent json object + */ + JzonObject createJsonObject(); + + /** + * @return a new implementation independent json array + */ + JzonArray createJsonArray(); + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonArray.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonArray.java new file mode 100644 index 0000000000..c63b000a2b --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonArray.java @@ -0,0 +1,75 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.gson; + +import java.lang.reflect.Field; +import java.util.ArrayList; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonArray; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; + +public class GsonJsonArray extends GsonJsonElement implements JzonArray { + + private final JsonArray wrapped; + + public GsonJsonArray(JsonArray wrapped) { + super(wrapped); + this.wrapped = wrapped; + } + + @Override + public int size() { + return wrapped.size(); + } + + @Override + public JzonElement get(int index) { + return GsonWrapper.wrap(wrapped.get(index)); + } + + @Override + public void insert(int index, JzonElement el) { + getElements(wrapped).add(index, (JsonElement) el.unwrap()); + } + + @Override + public void set(int index, JzonElement el) { + getElements(wrapped).set(index, (JsonElement) el.unwrap()); + } + + @Override + public void remove(int index) { + getElements(wrapped).remove(index); + } + + @Override + public String toString() { + return wrapped.toString(); + } + + private final static Field JsonArray_elements; + + static { + + try { + JsonArray_elements = JsonArray.class.getDeclaredField("elements"); + JsonArray_elements.setAccessible(true); + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + + @SuppressWarnings("unchecked") + private static ArrayList getElements(JsonArray arr) { + + try { + return (ArrayList) JsonArray_elements.get(arr); + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonElement.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonElement.java new file mode 100644 index 0000000000..37e964ef33 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonElement.java @@ -0,0 +1,65 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.gson; + +import com.google.gson.JsonElement; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; + + +public class GsonJsonElement implements JzonElement { + + final JsonElement wrapped; + + + protected GsonJsonElement(JsonElement wrapped) { + this.wrapped = wrapped; + } + + + @Override + public boolean isJsonObject() { + return wrapped.isJsonObject(); + } + + + @Override + public boolean isJsonArray() { + return wrapped.isJsonArray(); + } + + + @Override + public boolean isJsonPrimitive() { + return wrapped.isJsonPrimitive(); + } + + + @Override + public boolean isJsonNull() { + return wrapped.isJsonNull(); + } + + + @Override + public Object unwrap() { + return wrapped; + } + + + @Override + public String toString() { + return wrapped.toString(); + } + + + @Override + public boolean equals(Object obj) { + return wrapped.equals(obj); + } + + + @Override + public int hashCode() { + return wrapped.hashCode(); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonNull.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonNull.java new file mode 100644 index 0000000000..7b46c65c4c --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonNull.java @@ -0,0 +1,20 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.gson; + +import com.google.gson.JsonNull; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonNull; + + +public class GsonJsonNull extends GsonJsonElement implements JzonNull { + + static final JsonNull JNULL = new JsonNull(); + + + public final static GsonJsonNull INSTANCE = new GsonJsonNull(); + + + public GsonJsonNull() { + super(JNULL); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonObject.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonObject.java new file mode 100644 index 0000000000..f07c8b0cf3 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonObject.java @@ -0,0 +1,98 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.gson; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonObject; + + +public class GsonJsonObject extends GsonJsonElement implements JzonObject { + + private final JsonObject wrapped; + + + public GsonJsonObject(JsonObject wrapped) { + super(wrapped); + this.wrapped = wrapped; + } + + + @Override + public boolean has(String key) { + return wrapped.has(key); + } + + + @Override + public void add(String key, JzonElement prop) { + wrapped.add(key, (JsonElement) prop.unwrap()); + } + + + @Override + public void addProperty(String key, int prop) { + wrapped.addProperty(key, prop); + } + + + @Override + public Collection> entrySet() { + + Set> set = wrapped.entrySet(); + + HashSet> jset = new HashSet>(); + + for (final Entry e : set) { + + final JzonElement el = GsonWrapper.wrap(e.getValue()); + + jset.add(new Entry() { + + @Override + public String getKey() { + return e.getKey(); + } + + + @Override + public JzonElement getValue() { + return el; + } + + + @Override + public JzonElement setValue(JzonElement value) { + throw new UnsupportedOperationException(); + } + }); + } + + return jset; + + } + + + @Override + public JzonElement get(String key) { + return GsonWrapper.wrap(wrapped.get(key)); + } + + + @Override + public void remove(String key) { + wrapped.remove(key); + } + + + @Override + public String toString() { + return wrapped.toString(); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonPrimitive.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonPrimitive.java new file mode 100644 index 0000000000..5a3dc5c981 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonJsonPrimitive.java @@ -0,0 +1,14 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.gson; + +import com.google.gson.JsonPrimitive; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonPrimitive; + + +public class GsonJsonPrimitive extends GsonJsonElement implements JzonPrimitive { + + public GsonJsonPrimitive(JsonPrimitive wrapped) { + super(wrapped); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonWrapper.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonWrapper.java new file mode 100644 index 0000000000..641208005d --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/gson/GsonWrapper.java @@ -0,0 +1,52 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.gson; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonArray; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonObject; +import io.metersphere.log.utils.json.diff.jsonwrap.Wrapper; + +public class GsonWrapper implements Wrapper { + + private final static JsonParser JSON = new JsonParser(); + + public static JzonElement wrap(JsonElement el) { + if (el == null || el.isJsonNull()) { + return GsonJsonNull.INSTANCE; + } else if (el.isJsonArray()) { + return new GsonJsonArray((JsonArray) el); + } else if (el.isJsonObject()) { + return new GsonJsonObject((JsonObject) el); + } else if (el.isJsonPrimitive()) { + return new GsonJsonPrimitive((JsonPrimitive) el); + } else { + throw new IllegalStateException(); + } + } + + @Override + public JzonElement parse(String json) { + return wrap(JSON.parse(json)); + } + + @Override + public JzonElement wrap(Object o) { + return wrap((JsonElement) o); + } + + @Override + public JzonObject createJsonObject() { + return (JzonObject) wrap(new JsonObject()); + } + + @Override + public JzonArray createJsonArray() { + return (JzonArray) wrap(new JsonArray()); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonArray.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonArray.java new file mode 100644 index 0000000000..50cc6d3583 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonArray.java @@ -0,0 +1,48 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson; + +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.node.ArrayNode; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonArray; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; + +public class JacksonJsonArray extends JacksonJsonElement implements JzonArray { + + private final ArrayNode wrapped; + + public JacksonJsonArray(ArrayNode wrapped) { + super(wrapped); + this.wrapped = wrapped; + } + + @Override + public int size() { + return wrapped.size(); + } + + @Override + public JzonElement get(int index) { + return JacksonWrapper.wrap(wrapped.get(index)); + } + + @Override + public void insert(int index, JzonElement el) { + wrapped.insert(index, (JsonNode) el.unwrap()); + } + + @Override + public void set(int index, JzonElement el) { + wrapped.set(index, (JsonNode) el.unwrap()); + } + + @Override + public void remove(int index) { + wrapped.remove(index); + } + + @Override + public String toString() { + return wrapped.toString(); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonElement.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonElement.java new file mode 100644 index 0000000000..4eda8187ec --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonElement.java @@ -0,0 +1,65 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson; + +import org.codehaus.jackson.JsonNode; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; + + +public class JacksonJsonElement implements JzonElement { + + final JsonNode wrapped; + + + protected JacksonJsonElement(JsonNode wrapped) { + this.wrapped = wrapped; + } + + + @Override + public boolean isJsonObject() { + return wrapped.isObject(); + } + + + @Override + public boolean isJsonArray() { + return wrapped.isArray(); + } + + + @Override + public boolean isJsonPrimitive() { + return wrapped.isValueNode(); + } + + + @Override + public boolean isJsonNull() { + return wrapped.isNull(); + } + + + @Override + public Object unwrap() { + return wrapped; + } + + + @Override + public String toString() { + return wrapped.toString(); + } + + + @Override + public boolean equals(Object obj) { + return wrapped.equals(obj); + } + + + @Override + public int hashCode() { + return wrapped.hashCode(); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonNull.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonNull.java new file mode 100644 index 0000000000..0a29f1d4af --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonNull.java @@ -0,0 +1,20 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson; + +import org.codehaus.jackson.node.NullNode; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonNull; + + +public class JacksonJsonNull extends JacksonJsonElement implements JzonNull { + + static final NullNode JNULL = NullNode.getInstance(); + + + public final static JacksonJsonNull INSTANCE = new JacksonJsonNull(); + + + public JacksonJsonNull() { + super(JNULL); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonObject.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonObject.java new file mode 100644 index 0000000000..1931cf836d --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonObject.java @@ -0,0 +1,99 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map.Entry; + +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.node.ObjectNode; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonObject; + + +public class JacksonJsonObject extends JacksonJsonElement implements JzonObject { + + private final ObjectNode wrapped; + + + public JacksonJsonObject(ObjectNode wrapped) { + super(wrapped); + this.wrapped = wrapped; + } + + + @Override + public boolean has(String key) { + return wrapped.has(key); + } + + + @Override + public void add(String key, JzonElement prop) { + wrapped.put(key, (JsonNode) prop.unwrap()); + } + + + @Override + public void addProperty(String key, int prop) { + wrapped.put(key, prop); + } + + + @Override + public Collection> entrySet() { + + HashSet> jset = new HashSet>(); + + for (Iterator> i = wrapped.getFields(); i.hasNext();) { + + final Entry e = i.next(); + + final JzonElement el = JacksonWrapper.wrap(e.getValue()); + + jset.add(new Entry() { + + @Override + public String getKey() { + return e.getKey(); + } + + + @Override + public JzonElement getValue() { + return el; + } + + + @Override + public JzonElement setValue(JzonElement value) { + throw new UnsupportedOperationException(); + } + }); + } + + return jset; + + } + + + @Override + public JzonElement get(String key) { + return JacksonWrapper.wrap(wrapped.get(key)); + } + + + @Override + public void remove(String key) { + wrapped.remove(key); + } + + + @Override + public String toString() { + return wrapped.toString(); + } + + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonPrimitive.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonPrimitive.java new file mode 100644 index 0000000000..d061c2c818 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonJsonPrimitive.java @@ -0,0 +1,14 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson; + +import org.codehaus.jackson.node.ValueNode; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonPrimitive; + + +public class JacksonJsonPrimitive extends JacksonJsonElement implements JzonPrimitive { + + public JacksonJsonPrimitive(ValueNode wrapped) { + super(wrapped); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonWrapper.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonWrapper.java new file mode 100644 index 0000000000..cbd2789d8a --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonWrapper.java @@ -0,0 +1,65 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson; + +import java.io.IOException; + +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.JsonProcessingException; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.node.ArrayNode; +import org.codehaus.jackson.node.ObjectNode; +import org.codehaus.jackson.node.ValueNode; + +import io.metersphere.log.utils.json.diff.jsonwrap.JzonArray; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonObject; +import io.metersphere.log.utils.json.diff.jsonwrap.Wrapper; + +public class JacksonWrapper implements Wrapper { + + private final static ObjectMapper JSON = new ObjectMapper(); + + public static JzonElement wrap(JsonNode el) { + if (el == null || el.isNull()) { + return JacksonJsonNull.INSTANCE; + } else if (el.isArray()) { + return new JacksonJsonArray((ArrayNode) el); + } else if (el.isObject()) { + return new JacksonJsonObject((ObjectNode) el); + } else if (el.isValueNode()) { + return new JacksonJsonPrimitive((ValueNode) el); + } else { + throw new IllegalStateException(); + } + } + + @Override + public JzonElement parse(String json) { + try { + JsonParser parser = JSON.getJsonFactory().createJsonParser(json); + parser.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); + parser.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); + return wrap(parser.readValueAsTree()); + } catch (JsonProcessingException e) { + throw new JacksonWrapperException("Failed to parse JSON", e); + } catch (IOException e) { + throw new JacksonWrapperException("IOException parsing a String?", e); + } + } + + @Override + public JzonElement wrap(Object o) { + return wrap((JsonNode) o); + } + + @Override + public JzonObject createJsonObject() { + return (JzonObject) wrap(JSON.createObjectNode()); + } + + @Override + public JzonArray createJsonArray() { + return (JzonArray) wrap(JSON.createArrayNode()); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonWrapperException.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonWrapperException.java new file mode 100644 index 0000000000..c8a730fb89 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson/JacksonWrapperException.java @@ -0,0 +1,22 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson; + +@SuppressWarnings("serial") +public class JacksonWrapperException extends RuntimeException { + + public JacksonWrapperException() { + super(); + } + + public JacksonWrapperException(String message, Throwable cause) { + super(message, cause); + } + + public JacksonWrapperException(String message) { + super(message); + } + + public JacksonWrapperException(Throwable cause) { + super(cause); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonArray.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonArray.java new file mode 100644 index 0000000000..3fc1c29b18 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonArray.java @@ -0,0 +1,47 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson2; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonArray; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; + +public class Jackson2JsonArray extends Jackson2JsonElement implements JzonArray { + + private final ArrayNode wrapped; + + public Jackson2JsonArray(ArrayNode wrapped) { + super(wrapped); + this.wrapped = wrapped; + } + + @Override + public int size() { + return wrapped.size(); + } + + @Override + public JzonElement get(int index) { + return Jackson2Wrapper.wrap(wrapped.get(index)); + } + + @Override + public void insert(int index, JzonElement el) { + wrapped.insert(index, (JsonNode) el.unwrap()); + } + + @Override + public void set(int index, JzonElement el) { + wrapped.set(index, (JsonNode) el.unwrap()); + } + + @Override + public void remove(int index) { + wrapped.remove(index); + } + + @Override + public String toString() { + return wrapped.toString(); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonElement.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonElement.java new file mode 100644 index 0000000000..cacb8a240c --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonElement.java @@ -0,0 +1,65 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson2; + + +import com.fasterxml.jackson.databind.JsonNode; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; + + +public class Jackson2JsonElement implements JzonElement { + + final JsonNode wrapped; + + + protected Jackson2JsonElement(JsonNode wrapped) { + this.wrapped = wrapped; + } + + + @Override + public boolean isJsonObject() { + return wrapped.isObject(); + } + + + @Override + public boolean isJsonArray() { + return wrapped.isArray(); + } + + + @Override + public boolean isJsonPrimitive() { + return wrapped.isValueNode(); + } + + + @Override + public boolean isJsonNull() { + return wrapped.isNull(); + } + + + @Override + public Object unwrap() { + return wrapped; + } + + + @Override + public String toString() { + return wrapped.toString(); + } + + + @Override + public boolean equals(Object obj) { + return wrapped.equals(obj); + } + + + @Override + public int hashCode() { + return wrapped.hashCode(); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonNull.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonNull.java new file mode 100644 index 0000000000..66993d5316 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonNull.java @@ -0,0 +1,20 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson2; + + +import com.fasterxml.jackson.databind.node.NullNode; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonNull; + + +public class Jackson2JsonNull extends Jackson2JsonElement implements JzonNull { + + static final NullNode JNULL = NullNode.getInstance(); + + + public final static Jackson2JsonNull INSTANCE = new Jackson2JsonNull(); + + + public Jackson2JsonNull() { + super(JNULL); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonObject.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonObject.java new file mode 100644 index 0000000000..64c9db5a1e --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonObject.java @@ -0,0 +1,98 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson2; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonObject; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map.Entry; + + +public class Jackson2JsonObject extends Jackson2JsonElement implements JzonObject { + + private final ObjectNode wrapped; + + + public Jackson2JsonObject(ObjectNode wrapped) { + super(wrapped); + this.wrapped = wrapped; + } + + + @Override + public boolean has(String key) { + return wrapped.has(key); + } + + + @Override + public void add(String key, JzonElement prop) { + wrapped.put(key, (JsonNode) prop.unwrap()); + } + + + @Override + public void addProperty(String key, int prop) { + wrapped.put(key, prop); + } + + + @Override + public Collection> entrySet() { + + HashSet> jset = new HashSet>(); + + for (Iterator> i = wrapped.fields() ; i.hasNext();) { + + final Entry e = i.next(); + + final JzonElement el = Jackson2Wrapper.wrap(e.getValue()); + + jset.add(new Entry() { + + @Override + public String getKey() { + return e.getKey(); + } + + + @Override + public JzonElement getValue() { + return el; + } + + + @Override + public JzonElement setValue(JzonElement value) { + throw new UnsupportedOperationException(); + } + }); + } + + return jset; + + } + + + @Override + public JzonElement get(String key) { + return Jackson2Wrapper.wrap(wrapped.get(key)); + } + + + @Override + public void remove(String key) { + wrapped.remove(key); + } + + + @Override + public String toString() { + return wrapped.toString(); + } + + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonPrimitive.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonPrimitive.java new file mode 100644 index 0000000000..d347491be8 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2JsonPrimitive.java @@ -0,0 +1,13 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson2; + +import com.fasterxml.jackson.databind.node.ValueNode; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonPrimitive; + + +public class Jackson2JsonPrimitive extends Jackson2JsonElement implements JzonPrimitive { + + public Jackson2JsonPrimitive(ValueNode wrapped) { + super(wrapped); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2Wrapper.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2Wrapper.java new file mode 100644 index 0000000000..242e316d56 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2Wrapper.java @@ -0,0 +1,64 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson2; + +import com.fasterxml.jackson.core.JsonParser; +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.fasterxml.jackson.databind.node.ValueNode; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonArray; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonElement; +import io.metersphere.log.utils.json.diff.jsonwrap.JzonObject; +import io.metersphere.log.utils.json.diff.jsonwrap.Wrapper; + +import java.io.IOException; + +public class Jackson2Wrapper implements Wrapper { + + private final static ObjectMapper JSON = new ObjectMapper(); + + public static JzonElement wrap(JsonNode el) { + if (el == null || el.isNull()) { + return Jackson2JsonNull.INSTANCE; + } else if (el.isArray()) { + return new Jackson2JsonArray((ArrayNode) el); + } else if (el.isObject()) { + return new Jackson2JsonObject((ObjectNode) el); + } else if (el.isValueNode()) { + return new Jackson2JsonPrimitive((ValueNode) el); + } else { + throw new IllegalStateException(); + } + } + + @Override + public JzonElement parse(String json) { + try { + JsonParser parser = JSON.getJsonFactory().createJsonParser(json); + parser.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); + parser.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); + return wrap(parser.readValueAsTree()); + } catch (JsonProcessingException e) { + throw new Jackson2WrapperException("Failed to parse JSON", e); + } catch (IOException e) { + throw new Jackson2WrapperException("IOException parsing a String?", e); + } + } + + @Override + public JzonElement wrap(Object o) { + return wrap((JsonNode) o); + } + + @Override + public JzonObject createJsonObject() { + return (JzonObject) wrap(JSON.createObjectNode()); + } + + @Override + public JzonArray createJsonArray() { + return (JzonArray) wrap(JSON.createArrayNode()); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2WrapperException.java b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2WrapperException.java new file mode 100644 index 0000000000..3a60baf323 --- /dev/null +++ b/backend/src/main/java/io/metersphere/log/utils/json/diff/jsonwrap/jackson2/Jackson2WrapperException.java @@ -0,0 +1,22 @@ +package io.metersphere.log.utils.json.diff.jsonwrap.jackson2; + +@SuppressWarnings("serial") +public class Jackson2WrapperException extends RuntimeException { + + public Jackson2WrapperException() { + super(); + } + + public Jackson2WrapperException(String message, Throwable cause) { + super(message, cause); + } + + public Jackson2WrapperException(String message) { + super(message); + } + + public Jackson2WrapperException(Throwable cause) { + super(cause); + } + +} diff --git a/backend/src/main/java/io/metersphere/log/vo/DetailColumn.java b/backend/src/main/java/io/metersphere/log/vo/DetailColumn.java index 9009a74521..f612d9db4b 100644 --- a/backend/src/main/java/io/metersphere/log/vo/DetailColumn.java +++ b/backend/src/main/java/io/metersphere/log/vo/DetailColumn.java @@ -12,6 +12,7 @@ public class DetailColumn { private String columnName; private Object originalValue; private Object newValue; + private Object diffValue; public DetailColumn() { diff --git a/backend/src/main/java/io/metersphere/log/vo/api/DefinitionReference.java b/backend/src/main/java/io/metersphere/log/vo/api/DefinitionReference.java index 996e064a32..51554cef6e 100644 --- a/backend/src/main/java/io/metersphere/log/vo/api/DefinitionReference.java +++ b/backend/src/main/java/io/metersphere/log/vo/api/DefinitionReference.java @@ -6,10 +6,12 @@ import java.util.Map; public class DefinitionReference { public static Map definitionColumns = new LinkedHashMap<>(); public static Map caseColumns = new LinkedHashMap<>(); + public static Map jdbcColumns = new LinkedHashMap<>(); static { definitionColumns.clear(); caseColumns.clear(); + jdbcColumns.clear(); definitionColumns.put("name", "接口名称"); definitionColumns.put("createUser", "创建人"); definitionColumns.put("method", "请求类型"); @@ -39,5 +41,11 @@ public class DefinitionReference { // 深度对比字段 caseColumns.put("ms-dff-col", "request,tags"); + jdbcColumns.put("environmentId", "运行环境"); + jdbcColumns.put("dataSourceId", "数据源名称"); + jdbcColumns.put("queryTimeout", "超时时间"); + jdbcColumns.put("resultVariable", "存储结果"); + jdbcColumns.put("variableNames", "按列存储"); + } } \ No newline at end of file diff --git a/frontend/src/business/components/api/definition/components/EditCompleteContainer.vue b/frontend/src/business/components/api/definition/components/EditCompleteContainer.vue index edf9163e46..9d930aa566 100644 --- a/frontend/src/business/components/api/definition/components/EditCompleteContainer.vue +++ b/frontend/src/business/components/api/definition/components/EditCompleteContainer.vue @@ -203,14 +203,16 @@ export default { this.currentApi.request = JSON.parse(this.currentApi.request); } } - if (!this.currentApi.request.hashTree) { + if (this.currentApi && this.currentApi.request && !this.currentApi.request.hashTree) { this.currentApi.request.hashTree = []; } - if (this.currentApi.request.body && !this.currentApi.request.body.binary) { + if (this.currentApi && this.currentApi.request && this.currentApi.request.body && !this.currentApi.request.body.binary) { this.currentApi.request.body.binary = []; } - this.currentApi.request.clazzName = TYPE_TO_C.get(this.currentApi.request.type); - this.sort(this.currentApi.request.hashTree); + if (this.currentApi.request) { + this.currentApi.request.clazzName = TYPE_TO_C.get(this.currentApi.request.type); + this.sort(this.currentApi.request.hashTree); + } }, mockSetting() { let mockParam = {}; diff --git a/frontend/src/business/components/history/ChangeHistory.vue b/frontend/src/business/components/history/ChangeHistory.vue index ed068d454c..3581534597 100644 --- a/frontend/src/business/components/history/ChangeHistory.vue +++ b/frontend/src/business/components/history/ChangeHistory.vue @@ -21,10 +21,10 @@
- {{$t('operating_log.info')}} + {{ $t('operating_log.info') }}
-
{{ detail.originalValue ?detail.originalValue :"空值"}}
+
{{ detail.originalValue ? detail.originalValue : "空值" }}
@@ -35,10 +35,10 @@
- {{$t('operating_log.info')}} + {{ $t('operating_log.info') }}
-
{{ detail.newValue ? detail.newValue : "空值"}}
+
{{ detail.newValue ? detail.newValue : "空值" }}
@@ -46,62 +46,73 @@ - + + + diff --git a/frontend/src/business/components/history/api/ApiDubboParameters.vue b/frontend/src/business/components/history/api/ApiDubboParameters.vue new file mode 100644 index 0000000000..a4d07b0346 --- /dev/null +++ b/frontend/src/business/components/history/api/ApiDubboParameters.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/frontend/src/business/components/history/api/ApiHistoryDetail.vue b/frontend/src/business/components/history/api/ApiHistoryDetail.vue new file mode 100644 index 0000000000..1dab0f2537 --- /dev/null +++ b/frontend/src/business/components/history/api/ApiHistoryDetail.vue @@ -0,0 +1,218 @@ + + + + + diff --git a/frontend/src/business/components/history/api/ApiHttpRequestParams.vue b/frontend/src/business/components/history/api/ApiHttpRequestParams.vue new file mode 100644 index 0000000000..d9f4b0d4d1 --- /dev/null +++ b/frontend/src/business/components/history/api/ApiHttpRequestParams.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/frontend/src/business/components/history/api/ApiJdbcParameters.vue b/frontend/src/business/components/history/api/ApiJdbcParameters.vue new file mode 100644 index 0000000000..a4de08f517 --- /dev/null +++ b/frontend/src/business/components/history/api/ApiJdbcParameters.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/frontend/src/business/components/history/api/ApiTcpParameters.vue b/frontend/src/business/components/history/api/ApiTcpParameters.vue new file mode 100644 index 0000000000..a602718a46 --- /dev/null +++ b/frontend/src/business/components/history/api/ApiTcpParameters.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/frontend/src/business/components/history/api/common/ApiKeyValueDetail.vue b/frontend/src/business/components/history/api/common/ApiKeyValueDetail.vue new file mode 100644 index 0000000000..ac52c1f597 --- /dev/null +++ b/frontend/src/business/components/history/api/common/ApiKeyValueDetail.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/frontend/src/business/components/history/api/json-view/ComparedEditor.vue b/frontend/src/business/components/history/api/json-view/ComparedEditor.vue new file mode 100644 index 0000000000..a18cd4dd3e --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/ComparedEditor.vue @@ -0,0 +1,89 @@ + + + + diff --git a/frontend/src/business/components/history/api/json-view/common.js b/frontend/src/business/components/history/api/json-view/common.js new file mode 100644 index 0000000000..4a0cb1a129 --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/common.js @@ -0,0 +1,32 @@ +const Mock = require('mockjs'); +const jsf = require('json-schema-faker'); + +jsf.extend('mock', function () { + return { + mock: function (xx) { + if (xx && xx.startsWith("@")) { + return Mock.mock(xx); + } + return xx; + } + }; +}); + +const defaultOptions = { + failOnInvalidTypes: false, + failOnInvalidFormat: false +}; + +export const schemaToJson = (schema, options = {}) => { + Object.assign(options, defaultOptions); + + jsf.option(options); + let result; + try { + result = jsf.generate(schema); + } catch (err) { + result = err.message; + } + jsf.option(defaultOptions); + return result; +}; diff --git a/frontend/src/business/components/history/api/json-view/convert/convert.js b/frontend/src/business/components/history/api/json-view/convert/convert.js new file mode 100644 index 0000000000..7831e863b2 --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/convert/convert.js @@ -0,0 +1,202 @@ +const isBoolean = require("lodash.isboolean"); +const isEmpty = require("lodash.isempty"); +const isInteger = require("lodash.isinteger"); +const isNull = require("lodash.isnull"); +const isNumber = require("lodash.isnumber"); +const isObject = require("lodash.isobject"); +const isString = require("lodash.isstring"); +const {post} = require("@/common/js/ajax"); +const isArray = Array.isArray; + + +class Convert { + constructor() { + this._option = { + $id: "http://example.com/root.json", + $schema: "http://json-schema.org/draft-07/schema#", + } + this._object = null; + } + + /** + * 转换函数 + * @param {*} object 需要转换的对象 + * @param {*} ?option 可选参数,目前只有能设置 root 节点的 $id 和 $schema + */ + format(object, option = {}) { + // 数据校验,确保传入的的object只能是对象或数组 + if (!isObject(object)) { + throw new TypeError("传入参数只能是对象或数组"); + } + // 合并属性 + this._option = Object.assign(this._option, option); + // 需要转换的对象 + this._object = object; + let convertRes; + // 数组类型和对象类型结构不一样 + if (isArray(object)) { + convertRes = this._arrayToSchema(); + } else { + convertRes = this._objectToSchema(); + } + // 释放 + this._object = null; + return convertRes; + } + + /** + * 数组类型转换成JSONSCHEMA + */ + _arrayToSchema() { + // root节点基本信息 + let result = this._value2object(this._object, this._option.$id, "", true); + if (this._object.length > 0) { + let itemArr = []; + for (let index = 0; index < this._object.length; index++) { + // 创建items对象的基本信息 + let objectItem = this._object[index] + let item = this._value2object(objectItem, `#/items`, 'items'); + if (isObject(objectItem) && !isEmpty(objectItem)) { + // 递归遍历 + let objectItemSchema = this._json2schema(objectItem, `#/items`); + // 合并对象 + item = Object.assign(item, objectItemSchema); + } + itemArr.push(item); + } + result["items"] = itemArr; + } + return result + } + + /** + * 对象类型转换成JSONSCHEMA + */ + _objectToSchema() { + let baseResult = this._value2object(this._object, this._option.$id, "", true) + let objectSchema = this._json2schema(this._object) + baseResult = Object.assign(baseResult, objectSchema) + return baseResult + } + + /** + * 递归函数,转换object对象为json schmea 格式 + * @param {*} object 需要转换对象 + * @param {*} name $id值 + */ + _json2schema(object, name = "") { + // 如果递归值不是对象,那么return掉 + if (!isObject(object)) { + return; + } + // 处理当前路径$id + if (name === "" || name == undefined) { + name = "#" + } + let result = {}; + // 判断传入object是对象还是数组。 + if (isArray(object)) { + result.items = {}; + } else { + result.properties = {}; + } + // 遍历传入的对象 + for (const key in object) { + if (object.hasOwnProperty(key)) { + const element = object[key]; + // 如果只是undefined。跳过 + if (element === undefined) { + continue; + } + let $id = `${name}/properties/${key}` + // 判断当前 element 的值 是否也是对象,如果是就继续递归,不是就赋值给result + if (isObject(element)) { + // 创建当前属性的基本信息 + result["properties"][key] = this._value2object(element, $id, key) + if (isArray(element)) { + // 针对空数组和有值的数组做不同处理 + if (element.length > 0) { + // 是数组 + let itemArr = []; + for (let index = 0; index < element.length; index++) { + let elementItem = element[index]; + // 创建items对象的基本信息 + let item = this._value2object(elementItem, `${$id}/items`, key + 'items'); + // 判断第一项是否是对象,且对象属性不为空 + if (isObject(elementItem) && !isEmpty(elementItem)) { + // 新增的properties才合并进来 + item = Object.assign(item, this._json2schema(elementItem, `${$id}/items`)); + } + itemArr.push(item); + } + result["properties"][key]["items"] = itemArr; + } + } else { + // 不是数组,递归遍历获取,然后合并对象属性 + result["properties"][key] = Object.assign(result["properties"][key], this._json2schema(element, $id)); + } + } else { + // 一般属性直接获取基本信息 + result["properties"][key] = this._value2object(element, $id, key); + } + } + } + return result; + } + + /** + * 把json的值转换成对象类型 + * @param {*} value + * @param {*} $id + * @param {*} key + */ + _value2object(value, $id, key = '', root = false) { + let objectTemplate = { + $id: $id, + title: `The ${key} Schema`, + mock: { + "mock": value + }, + } + + // 判断是否为初始化root数据 + if (root) { + objectTemplate["$schema"] = this._option.$schema; + objectTemplate["title"] = `The Root Schema`; + objectTemplate["mock"] = undefined; + } + if (isBoolean(value)) { + objectTemplate.type = "boolean"; + } else if (isInteger(value)) { + objectTemplate.type = "integer"; + } else if (isNumber(value)) { + objectTemplate.type = "number"; + } else if (isString(value)) { + objectTemplate.type = "string"; + } else if (isNull(value)) { + objectTemplate.type = "null"; + } else if (isArray(value)) { + objectTemplate.type = "array"; + objectTemplate["mock"] = undefined; + } else if (isObject(value)) { + objectTemplate.type = "object" + objectTemplate["mock"] = undefined; + } + + return objectTemplate; + } + + /** + * 后台转换 + * @param callback + */ + schemaToJsonStr(schema, callback) { + post('/api/definition/preview', schema, (response) => { + if (callback) { + callback(response.data); + } + }); + } +} + +module.exports = Convert; diff --git a/frontend/src/business/components/history/api/json-view/schema/editor/index.js b/frontend/src/business/components/history/api/json-view/schema/editor/index.js new file mode 100644 index 0000000000..123863707a --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/schema/editor/index.js @@ -0,0 +1,7 @@ +import ComparedEditor from './main.vue' + +ComparedEditor.install = function (Vue) { + Vue.component(ComparedEditor.name, ComparedEditor) +} + +export default ComparedEditor diff --git a/frontend/src/business/components/history/api/json-view/schema/editor/main.vue b/frontend/src/business/components/history/api/json-view/schema/editor/main.vue new file mode 100644 index 0000000000..33f0485158 --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/schema/editor/main.vue @@ -0,0 +1,430 @@ + + + diff --git a/frontend/src/business/components/history/api/json-view/schema/editor/type/array.js b/frontend/src/business/components/history/api/json-view/schema/editor/type/array.js new file mode 100644 index 0000000000..d9d81ea86b --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/schema/editor/type/array.js @@ -0,0 +1,23 @@ +const value = { + description: null +} +const attr = { + description: { + name: '描述', + type: 'string' + }, + maxItems:{ + name: '最大元素个数', + type: 'integer' + }, + minItems:{ + name: '最小元素个数', + type: 'integer' + }, + uniqueItems:{ + name:'元素不可重复', + type: 'boolean' + } +} +const wrapper = {value, attr} +export default wrapper diff --git a/frontend/src/business/components/history/api/json-view/schema/editor/type/boolean.js b/frontend/src/business/components/history/api/json-view/schema/editor/type/boolean.js new file mode 100644 index 0000000000..be1bfab985 --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/schema/editor/type/boolean.js @@ -0,0 +1,11 @@ +const value = { + description: null +} +const attr = { + description: { + name: '描述', + type: 'string' + } +} +const wrapper = {value, attr} +export default wrapper \ No newline at end of file diff --git a/frontend/src/business/components/history/api/json-view/schema/editor/type/integer.js b/frontend/src/business/components/history/api/json-view/schema/editor/type/integer.js new file mode 100644 index 0000000000..a85cb61f6e --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/schema/editor/type/integer.js @@ -0,0 +1,40 @@ +const value = { + maximum: null, + minimum: null, + default: null, + enum: null, + description: null, +} +const attr = { + description: { + name: '描述', + type: 'string', + }, + maximum: { + name: '最大值', + type: 'integer' + }, + minimum: { + name: '最小值', + type: 'integer' + }, + exclusiveMaximum: { + name: '不包含最大值', + type: 'boolean' + }, + exclusiveMinimum: { + name: '不包含最小值', + type: 'boolean' + }, + default: { + name: '默认值', + type: 'integer', + }, + enum: { + name: '枚举值', + type: 'textarea', + description: "一行一个枚举值" + }, +} +const wrapper = {value, attr} +export default wrapper diff --git a/frontend/src/business/components/history/api/json-view/schema/editor/type/number.js b/frontend/src/business/components/history/api/json-view/schema/editor/type/number.js new file mode 100644 index 0000000000..a1c82a688c --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/schema/editor/type/number.js @@ -0,0 +1,32 @@ +const value = { + maximum: null, + minimum: null, + default: null, + enum: null, + description: null, +} +const attr = { + description: { + name: '描述', + type: 'string', + }, + maximum: { + name: '最大值', + type: 'number' + }, + minimum: { + name: '最小值', + type: 'number' + }, + default: { + name: '默认值', + type: 'string', + }, + enum: { + name: '枚举值', + type: 'textarea', + description: "一行一个枚举值" + }, +} +const wrapper = {value, attr} +export default wrapper diff --git a/frontend/src/business/components/history/api/json-view/schema/editor/type/object.js b/frontend/src/business/components/history/api/json-view/schema/editor/type/object.js new file mode 100644 index 0000000000..397ef7cfb3 --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/schema/editor/type/object.js @@ -0,0 +1,19 @@ +const value = { + description: null +} +const attr = { + description: { + name: '描述', + type: 'string', + }, + maxProperties:{ + name:'最大元素个数', + type:'integer' + }, + minProperties:{ + name:'最小元素个数', + type:'integer' + } +} +const wrapper = {value, attr} +export default wrapper diff --git a/frontend/src/business/components/history/api/json-view/schema/editor/type/string.js b/frontend/src/business/components/history/api/json-view/schema/editor/type/string.js new file mode 100644 index 0000000000..6b1266aa9d --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/schema/editor/type/string.js @@ -0,0 +1,43 @@ +const value = { + maxLength: null, + minLength: null, + default: null, + enum: null, + pattern: null, + format: null, + description: null, +} +const attr = { + maxLength: { + name: '最大字符数', + type: 'integer' + }, + minLength: { + name: '最小字符数', + type: 'integer' + }, + default: { + name: '默认值', + type: 'string', + }, + enum: { + name: '枚举值', + type: 'textarea', + description:"一行一个枚举值" + }, + pattern: { + name: '正则表达式', + type: 'string' + }, + format: { + name: '格式', + type: 'array', + enums: ['date', 'date-time', 'email', 'hostname', 'ipv4', 'ipv6', 'uri'] + }, + description: { + name: '描述', + type: 'string', + } +} +const wrapper = {value, attr} +export default wrapper diff --git a/frontend/src/business/components/history/api/json-view/schema/editor/type/type.js b/frontend/src/business/components/history/api/json-view/schema/editor/type/type.js new file mode 100644 index 0000000000..59a4e3b2c7 --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/schema/editor/type/type.js @@ -0,0 +1,17 @@ +import _object from './object' +import _string from './string' +import _array from './array' +import _boolean from './boolean' +import _integer from './integer' +import _number from './number' +const TYPE_NAME = ['string', 'number', 'integer','object', 'array', 'boolean'] + +const TYPE = { + 'object': _object, + 'string': _string, + 'array': _array, + 'boolean': _boolean, + 'integer': _integer, + 'number': _number +} +export {TYPE ,TYPE_NAME} diff --git a/frontend/src/business/components/history/api/json-view/schema/editor/util.js b/frontend/src/business/components/history/api/json-view/schema/editor/util.js new file mode 100644 index 0000000000..fc2fd33e02 --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/schema/editor/util.js @@ -0,0 +1,10 @@ +export function isNull(ele) { + if (typeof ele === 'undefined') { + return true; + } else if (ele === null) { + return true; + } else if (ele === '') { + return true; + } + return false; +} diff --git a/frontend/src/business/components/history/api/json-view/schema/index.js b/frontend/src/business/components/history/api/json-view/schema/index.js new file mode 100644 index 0000000000..a8eb912264 --- /dev/null +++ b/frontend/src/business/components/history/api/json-view/schema/index.js @@ -0,0 +1,28 @@ +import ComparedEditor from './editor/index' + +const components = [ + ComparedEditor +] + +// 定义 install 方法 +const install = function (Vue) { + if (install.installed) { + return; + } + install.installed = true; + // 遍历并注册全局组件 + components.map(component => { + Vue.component(component.name, component) + }) +} + +if (typeof window !== 'undefined' && window.Vue) { + install(window.Vue) +} + +export default { + // 导出的对象必须具备一个 install 方法 + install, + // 组件列表 + ...components +} diff --git a/frontend/src/business/components/history/tags/MsInputTag.vue b/frontend/src/business/components/history/tags/MsInputTag.vue new file mode 100644 index 0000000000..cf181efbc7 --- /dev/null +++ b/frontend/src/business/components/history/tags/MsInputTag.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/frontend/src/business/components/history/tags/TagsHistoryDetail.vue b/frontend/src/business/components/history/tags/TagsHistoryDetail.vue new file mode 100644 index 0000000000..5212017cad --- /dev/null +++ b/frontend/src/business/components/history/tags/TagsHistoryDetail.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/frontend/src/business/main.js b/frontend/src/business/main.js index 32c0d6bde4..0a3d62965f 100644 --- a/frontend/src/business/main.js +++ b/frontend/src/business/main.js @@ -20,6 +20,7 @@ import CKEditor from '@ckeditor/ckeditor5-vue'; import VueFab from 'vue-float-action-button' import {left2RightDrag, bottom2TopDrag, right2LeftDrag} from "../common/js/directive"; import JsonSchemaEditor from './components/common/json-schema/schema/index'; +import ComparedEditor from './components/history/api/json-view/schema/index'; import JSONPathPicker from 'vue-jsonpath-picker'; import VueClipboard from 'vue-clipboard2' import vueMinderEditor from 'vue-minder-editor-plus' @@ -32,6 +33,8 @@ Vue.use(mavonEditor) Vue.use(vueMinderEditor) Vue.use(JsonSchemaEditor); +Vue.use(ComparedEditor); + import VuePapaParse from 'vue-papa-parse' Vue.use(VuePapaParse) Vue.use(formCreate);