feat (接口定义): 操作历史重构

This commit is contained in:
fit2-zhao 2021-10-09 13:43:07 +08:00 committed by fit2-zhao
parent ba08a5578c
commit af785a1d4e
87 changed files with 5632 additions and 1834 deletions

View File

@ -201,7 +201,7 @@ public class MsLogAspect {
}
if (StringUtils.isNotEmpty(content) && StringUtils.isNotEmpty(msLog.beforeValue())) {
OperatingLogDetails details = JSON.parseObject(content, OperatingLogDetails.class);
List<DetailColumn> columns = ReflexObjectUtil.compared(JSON.parseObject(msLog.beforeValue(), OperatingLogDetails.class), details);
List<DetailColumn> columns = ReflexObjectUtil.compared(JSON.parseObject(msLog.beforeValue(), OperatingLogDetails.class), details,msLog.module());
details.setColumns(columns);
msOperLog.setOperContent(JSON.toJSONString(details));
msOperLog.setSourceId(details.getSourceId());

View File

@ -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<DetailColumn> getColumns(Object obj) {
List<DetailColumn> columnList = new LinkedList<>();
if (obj == null) {
return columnList;
}
// 得到类对象
Class clazz = obj.getClass();
// 得到类中的所有属性集合
List<Field[]> 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<DetailColumn> compared(OperatingLogDetails obj, OperatingLogDetails newObj) {
public static List<DetailColumn> compared(OperatingLogDetails obj, OperatingLogDetails newObj, String module) {
List<DetailColumn> 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);
}
}

View File

@ -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<String, String> 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<String, String> 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<String, String> 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<DetailColumn> getColumn(List<DetailColumn> columnsNew, List<DetailColumn> columnsOld) {
OperatingLogDetails detailsNew = new OperatingLogDetails();
detailsNew.setColumns(columnsNew);
OperatingLogDetails detailsOld = new OperatingLogDetails();
detailsOld.setColumns(columnsOld);
List<DetailColumn> diffColumns = ReflexObjectUtil.compared(detailsOld, detailsNew, "");
return diffColumns;
}
private static void diffJdbc(MsJDBCSampler jdbcNew, MsJDBCSampler jdbcOld, JsonDiff jsonDiff, Map<String, String> diffMap) {
// 基础参数对比
List<DetailColumn> columns = ReflexObjectUtil.getColumns(jdbcNew, DefinitionReference.jdbcColumns);
List<DetailColumn> columnsOld = ReflexObjectUtil.getColumns(jdbcOld, DefinitionReference.jdbcColumns);
List<DetailColumn> 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<String, String> diffMap) {
// Config对比
List<DetailColumn> diffColumns = getColumn(ReflexObjectUtil.getColumns(dubboNew.getConfigCenter()), ReflexObjectUtil.getColumns(dubboOld.getConfigCenter()));
if (CollectionUtils.isNotEmpty(diffColumns)) {
diffMap.put("config", JSON.toJSONString(diffColumns));
}
// Registry对比
List<DetailColumn> registryColumns = getColumn(ReflexObjectUtil.getColumns(dubboNew.getRegistryCenter()), ReflexObjectUtil.getColumns(dubboOld.getRegistryCenter()));
if (CollectionUtils.isNotEmpty(registryColumns)) {
diffMap.put("registry", JSON.toJSONString(registryColumns));
}
// service对比
List<DetailColumn> 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);
}
}
}
}

View File

@ -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<CompatibilityFlags> defaults() {
return EnumSet.noneOf(CompatibilityFlags.class);
}
}

View File

@ -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() {}
}

View File

@ -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<CompatibilityFlags> flags) {
super(target.deepCopy(), flags);
}
}

View File

@ -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;
}
}

View File

@ -1,82 +0,0 @@
package io.metersphere.log.utils.dff;
import java.util.EnumSet;
public enum DiffFlags {
/**
* This flag omits the <i>value</i> 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 <i>fromValue</i> field to all {@link Operation#REPLACE} operations.
* <i>fromValue</i> 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.
* <p>
* 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 <i>fromValue</i>
* {@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.
* <p>
* 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<DiffFlags> defaults() {
return EnumSet.of(OMIT_VALUE_ON_REMOVE);
}
public static EnumSet<DiffFlags> dontNormalizeOpIntoMoveAndCopy() {
return EnumSet.of(OMIT_MOVE_OPERATION, OMIT_COPY_OPERATION);
}
}

View File

@ -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<CompatibilityFlags> flags;
InPlaceApplyProcessor(JsonNode target) {
this(target, CompatibilityFlags.defaults());
}
InPlaceApplyProcessor(JsonNode target, EnumSet<CompatibilityFlags> 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);
}
}
}

View File

@ -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<JsonNode> toList(ArrayNode input) {
int size = input.size();
List<JsonNode> toReturn = new ArrayList<JsonNode>(size);
for (int i = 0; i < size; i++) {
toReturn.add(input.get(i));
}
return toReturn;
}
static List<JsonNode> longestCommonSubsequence(final List<JsonNode> a, final List<JsonNode> b) {
if (a == null || b == null) {
throw new NullPointerException("List must not be null for longestCommonSubsequence");
}
List<JsonNode> toReturn = new LinkedList<JsonNode>();
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;
}
}

View File

@ -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);
}
}

View File

@ -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<Diff> diffs = new ArrayList<Diff>();
private final EnumSet<DiffFlags> flags;
private JsonDiff(EnumSet<DiffFlags> 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<DiffFlags> 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<Diff> jsonDiff(final JsonNode source, final JsonNode target) {
return diff(source, target);
}
public static List<Diff> 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<JsonNode, JsonPointer> unchangedValues, JsonNode value) {
return unchangedValues.get(value);
}
private void introduceCopyOperation(JsonNode source, JsonNode target) {
Map<JsonNode, JsonPointer> 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<JsonNode, JsonPointer> getUnchangedPart(JsonNode source, JsonNode target) {
Map<JsonNode, JsonPointer> unchangedValues = new HashMap<JsonNode, JsonPointer>();
computeUnchangedValues(unchangedValues, JsonPointer.ROOT, source, target);
return unchangedValues;
}
private static void computeUnchangedValues(Map<JsonNode, JsonPointer> 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<JsonNode, JsonPointer> 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<JsonNode, JsonPointer> unchangedValues, JsonPointer path, JsonNode source, JsonNode target) {
final Iterator<String> 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<Diff> updatedDiffs = new ArrayList<Diff>();
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<Diff> diffs) {
List<Integer> counters = new ArrayList<Integer>(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<Integer> counters, JsonPointer path) {
List<JsonPointer.RefToken> 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<Integer> 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<Integer> 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<Diff> getDiffs() {
return diffs;
}
private static ObjectNode getJsonNode(JsonNodeFactory FACTORY, Diff diff, EnumSet<DiffFlags> 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<JsonNode> 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<String> 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<String> 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<JsonNode> getLCS(final JsonNode first, final JsonNode second) {
return ListUtils.longestCommonSubsequence(InternalUtils.toList((ArrayNode) first), InternalUtils.toList((ArrayNode) second));
}
}

View File

@ -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<CompatibilityFlags> flags)
throws InvalidJsonPatchException {
if (!patch.isArray())
throw new InvalidJsonPatchException("Invalid JSON Patch payload (not an array)");
Iterator<JsonNode> 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<CompatibilityFlags> 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<CompatibilityFlags> 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<CompatibilityFlags> flags) {
InPlaceApplyProcessor processor = new InPlaceApplyProcessor(source, flags);
process(patch, processor, flags);
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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)
*
* <p>For full details, please refer to <a href="https://tools.ietf.org/html/rfc6901">RFC 6901</a>.
*
* <p></p>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:
*
* <pre>
* // 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"));
* </pre>
*
* <p>Instances of {@link JsonPointer} and its constituent {@link RefToken}s are <b>immutable</b>.
*
* @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<RefToken> 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<RefToken> result = new ArrayList<RefToken>();
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 <a href="https://tools.ietf.org/html/rfc6901#section-5">RFC 6901 compliant</a> 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<RefToken> 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
* <a href="https://tools.ietf.org/html/rfc6901#section-4">RFC 6901 sectino 4</a>.
*
* @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
* <a href="https://tools.ietf.org/html/rfc6901#section-4">RFC 6901 section 4</a> for
* more details.
*/
final static int LAST_INDEX = Integer.MIN_VALUE;
}

View File

@ -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;
}
}

View File

@ -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<JsonToken, NodeType> TOKEN_MAP
= new EnumMap<JsonToken, NodeType>(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;
}
}

View File

@ -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) {}
}

View File

@ -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<String, Operation> OPS = createImmutableMap();
private static Map<String, Operation> createImmutableMap() {
Map<String, Operation> map = new HashMap<String, Operation>();
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;
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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 <code>Comparator</code>
* may be passed as an argument to the constructor, and will thus be used. If not provided, the initial value in the
* <code>a</code> ("from") list will be looked at to see if it supports the <code>Comparable</code> interface. If so,
* its <code>equals</code> and <code>compareTo</code> 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.
*
* <p>
* The file FileDiff.java shows an example usage of this class, in an application similar to the Unix "diff" program.
* </p>
*/
public class IncavaDiff<Type>
{
/**
* The source list, AKA the "from" values.
*/
protected List<Type> a;
/**
* The target list, AKA the "to" values.
*/
protected List<Type> b;
/**
* The list of differences, as <code>Difference</code> instances.
*/
protected List<IncavaEntry> diffs = new ArrayList<IncavaEntry>();
/**
* The pending, uncommitted difference.
*/
private IncavaEntry pending;
/**
* The comparator used, if any.
*/
private Comparator<Type> comparator;
/**
* The thresholds.
*/
private TreeMap<Integer, Integer> thresh;
/**
* Constructs the Diff object for the two arrays, using the given comparator.
*/
public IncavaDiff(Type[] a, Type[] b, Comparator<Type> 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 <code>equals</code> and <code>compareTo</code>.
*/
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<Type> a, List<Type> b, Comparator<Type> 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
* <code>equals</code> and <code>compareTo</code>.
*/
public IncavaDiff(List<Type> a, List<Type> b)
{
this(a, b, null);
}
/**
* Runs diff and returns the results.
*/
public List<IncavaEntry> 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 <code>finishedA</code>,
* <code>finishedB</code>, <code>onANotB</code>, and <code>onBNotA</code>.
*/
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 <code>finishedA</code> invoked at the last element in the
* <code>a</code> array.
*/
protected boolean callFinishedA()
{
return false;
}
/**
* Override and return true in order to have <code>finishedB</code> invoked at the last element in the
* <code>b</code> array.
*/
protected boolean callFinishedB()
{
return false;
}
/**
* Invoked at the last element in <code>a</code>, if <code>callFinishedA</code> returns true.
*/
protected void finishedA(int lastA)
{
}
/**
* Invoked at the last element in <code>b</code>, if <code>callFinishedB</code> returns true.
*/
protected void finishedB(int lastB)
{
}
/**
* Invoked for elements in <code>a</code> and not in <code>b</code>.
*/
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 <code>b</code> and not in <code>a</code>.
*/
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 <code>a</code> and <code>b</code>.
*/
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<Integer, Integer> matches = new TreeMap<Integer, Integer>();
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<Type, List<Integer>> bMatches = null;
if (comparator == null) {
if (a.size() > 0 && a.get(0) instanceof Comparable) {
// this uses the Comparable interface
bMatches = new TreeMap<Type, List<Integer>>();
}
else {
// this just uses hashCode()
bMatches = new HashMap<Type, List<Integer>>();
}
}
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<Type, List<Integer>>(comparator);
}
for (int bi = bStart; bi <= bEnd; ++bi) {
Type element = b.get(bi);
Type key = element;
List<Integer> positions = bMatches.get(key);
if (positions == null) {
positions = new ArrayList<Integer>();
bMatches.put(key, positions);
}
positions.add(bi);
}
thresh = new TreeMap<Integer, Integer>();
Map<Integer, Object[]> links = new HashMap<Integer, Object[]>();
for (int i = aStart; i <= aEnd; ++i) {
Type aElement = a.get(i);
List<Integer> positions = bMatches.get(aElement);
if (positions != null) {
Integer k = 0;
ListIterator<Integer> 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;
}
}

View File

@ -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 <code>Diff</code>. A difference consists of two pairs of starting and ending
* points, each pair representing either the "from" or the "to" collection passed to <code>Diff</code>. If an ending
* point is -1, then the difference was either a deletion or an addition. For example, if <code>getDeletedEnd()</code>
* 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 <code>NONE</code> means this is an addition.
*/
public int getDeletedStart()
{
return delStart;
}
/**
* The point at which the deletion ends, if any. A value equal to <code>NONE</code> means this is an addition.
*/
public int getDeletedEnd()
{
return delEnd;
}
/**
* The point at which the addition starts, if any. A value equal to <code>NONE</code> means this must be an
* addition.
*/
public int getAddedStart()
{
return addStart;
}
/**
* The point at which the addition ends, if any. A value equal to <code>NONE</code> 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();
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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.
*
* <p>
* Syntax for instructions:
*
* <pre>
* <code>
* {
* "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)
* }
* </code>
* </pre>
*
* <p>
* Instruction order is merge, set, insert, delete. This is important when altering arrays, since insertions will affect the array index of subsequent delete instructions.
* </p>
*
* <p>
* When diffing, the object is expanded to a structure like this: <code>Example: {a:[{b:1,c:2},{d:3}]}</code>
* Becomes a list of:
* <ol>
* <li>Leaf: obj
* <li>Leaf: array 0
* <li>Leaf: obj
* <li>Leaf: b: 1
* <li>Leaf: c: 2
* <li>Leaf: array 1
* <li>Leaf: obj
* <li>Leaf: d: 3
* </ol>
*
* @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<Entry<String, JzonElement>> INSTRUCTIONS_COMPARATOR = new Comparator<Entry<String, JzonElement>>() {
@Override
public int compare(Entry<String, JzonElement> o1, Entry<String, JzonElement> 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<Entry<String, JzonElement>> OBJECT_KEY_COMPARATOR = new Comparator<Entry<String, JzonElement>>() {
@Override
public int compare(Entry<String, JzonElement> o1, Entry<String, JzonElement> 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<Entry<String, JzonElement>> memb = new TreeSet<Entry<String, JzonElement>>(INSTRUCTIONS_COMPARATOR);
memb.addAll(patch.entrySet());
for (Entry<String, JzonElement> 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<String, JzonElement> 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<Leaf> fromLeaves = new ArrayList<Leaf>();
ArrayList<Leaf> toLeaves = new ArrayList<Leaf>();
HashMap<Integer, ArrNode> fromArrs = new HashMap<Integer, ArrNode>();
HashMap<Integer, ArrNode> toArrs = new HashMap<Integer, ArrNode>();
findLeaves(fromRoot, from, fromLeaves, fromArrs);
findLeaves(toRoot, to, toLeaves, toArrs);
IncavaDiff<Leaf> idiff = new IncavaDiff<Leaf>(fromLeaves, toLeaves);
List<IncavaEntry> 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<Leaf> leaves, HashMap<Integer, ArrNode> 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<Entry<String, JzonElement>> memb = new TreeSet<Entry<String, JzonElement>>(OBJECT_KEY_COMPARATOR);
memb.addAll(((JzonObject) el).entrySet());
for (Entry<String, JzonElement> 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;
}
}

View File

@ -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<Leaf> {
Wrapper factory;
final Node parent;
JzonElement val;
Oper oper;
List<Leaf> children = new LinkedList<Leaf>();
List<Leaf> newStructure = new LinkedList<Leaf>();
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<Leaf> 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<Leaf> 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<Leaf> 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<Leaf> 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<Leaf> newOrphans = new ArrayList<Leaf>();
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<Leaf> c, Leaf check) {
int i = -1;
for (Leaf l : c) {
i++;
if (l == check) {
return i;
}
}
return i;
}
void recover(List<Leaf> 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<Leaf> 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();
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,5 @@
package io.metersphere.log.utils.json.diff;
enum Oper {
INSERT, DELETE, SET
}

View File

@ -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();
}
}

View File

@ -0,0 +1,21 @@
package io.metersphere.log.utils.json.diff;
/**
* Interface that allows filtering patch instructions.
*
* @since 2.0.0
*/
public interface Visitor<E> {
/**
* Should a patch instruction be created for an element like <code>to</code> if its destiny is an element like <code>to</code>?
*
* @param from
* - from element
* @param to
* - to element
* @return if the instruction should be created
*/
boolean shouldCreatePatch(E from, E to);
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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 {
}

View File

@ -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<? extends Entry<String, JzonElement>> 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);
}

View File

@ -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 {
}

View File

@ -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();
}

View File

@ -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<JsonElement> getElements(JsonArray arr) {
try {
return (ArrayList<JsonElement>) JsonArray_elements.get(arr);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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<? extends Entry<String, JzonElement>> entrySet() {
Set<Entry<String, JsonElement>> set = wrapped.entrySet();
HashSet<Entry<String, JzonElement>> jset = new HashSet<Entry<String, JzonElement>>();
for (final Entry<String, JsonElement> e : set) {
final JzonElement el = GsonWrapper.wrap(e.getValue());
jset.add(new Entry<String, JzonElement>() {
@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();
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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<? extends Entry<String, JzonElement>> entrySet() {
HashSet<Entry<String, JzonElement>> jset = new HashSet<Entry<String, JzonElement>>();
for (Iterator<Entry<String, JsonNode>> i = wrapped.getFields(); i.hasNext();) {
final Entry<String, JsonNode> e = i.next();
final JzonElement el = JacksonWrapper.wrap(e.getValue());
jset.add(new Entry<String, JzonElement>() {
@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();
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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<? extends Entry<String, JzonElement>> entrySet() {
HashSet<Entry<String, JzonElement>> jset = new HashSet<Entry<String, JzonElement>>();
for (Iterator<Entry<String, JsonNode>> i = wrapped.fields() ; i.hasNext();) {
final Entry<String, JsonNode> e = i.next();
final JzonElement el = Jackson2Wrapper.wrap(e.getValue());
jset.add(new Entry<String, JzonElement>() {
@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();
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -12,6 +12,7 @@ public class DetailColumn {
private String columnName;
private Object originalValue;
private Object newValue;
private Object diffValue;
public DetailColumn() {

View File

@ -6,10 +6,12 @@ import java.util.Map;
public class DefinitionReference {
public static Map<String, String> definitionColumns = new LinkedHashMap<>();
public static Map<String, String> caseColumns = new LinkedHashMap<>();
public static Map<String, String> 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", "按列存储");
}
}

View File

@ -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 = {};

View File

@ -21,10 +21,10 @@
<div v-if="scope.row.details && scope.row.details.columns">
<div v-for="detail in scope.row.details.columns" :key="detail.id">
<div v-if="linkDatas.indexOf(detail.columnName)!== -1">
<el-link style="color: #409EFF" @click="openDetail(scope.row,detail)">{{$t('operating_log.info')}}</el-link>
<el-link style="color: #409EFF" @click="openDetail(scope.row,detail)">{{ $t('operating_log.info') }}</el-link>
</div>
<el-tooltip :content="detail.originalValue" v-else>
<div class="current-value">{{ detail.originalValue ?detail.originalValue :"空值"}}</div>
<div class="current-value">{{ detail.originalValue ? detail.originalValue : "空值" }}</div>
</el-tooltip>
</div>
</div>
@ -35,10 +35,10 @@
<div v-if="scope.row.details && scope.row.details.columns">
<div v-for="detail in scope.row.details.columns" :key="detail.id">
<div v-if="linkDatas.indexOf(detail.columnName)!== -1">
<el-link style="color: #409EFF" @click="openDetail(scope.row,detail)">{{$t('operating_log.info')}}</el-link>
<el-link style="color: #409EFF" @click="openDetail(scope.row,detail)">{{ $t('operating_log.info') }}</el-link>
</div>
<el-tooltip :content="detail.newValue" v-else>
<div class="current-value">{{ detail.newValue ? detail.newValue : "空值"}}</div>
<div class="current-value">{{ detail.newValue ? detail.newValue : "空值" }}</div>
</el-tooltip>
</div>
</div>
@ -46,62 +46,73 @@
</el-table-column>
</el-table>
<ms-history-detail ref="historyDetail"></ms-history-detail>
<ms-history-detail ref="historyDetail"/>
<ms-tags-history-detail ref="tagsHistoryDetail"/>
<ms-api-history-detail ref="apiHistoryDetail"/>
</el-dialog>
</template>
<script>
import MsHistoryDetail from "./HistoryDetail";
import MsHistoryDetail from "./HistoryDetail";
import MsTagsHistoryDetail from "./tags/TagsHistoryDetail";
import MsApiHistoryDetail from "./api/ApiHistoryDetail";
export default {
name: "MsChangeHistory",
components: {MsHistoryDetail},
props: {
title: String,
export default {
name: "MsChangeHistory",
components: {MsHistoryDetail, MsTagsHistoryDetail, MsApiHistoryDetail},
props: {
title: String,
},
data() {
return {
infoVisible: false,
loading: false,
details: [],
linkDatas: ["prerequisite", "steps", "remark", "request", "response", "scenarioDefinition", "tags", "loadConfiguration", "advancedConfiguration"],
}
},
methods: {
handleClose() {
this.infoVisible = false;
},
data() {
return {
infoVisible: false,
loading: false,
details: [],
linkDatas: ["prerequisite", "steps", "remark", "request", "response", "scenarioDefinition","tags", "loadConfiguration", "advancedConfiguration"],
getDetails(id) {
this.result = this.$get("/operating/log/get/source/" + id, response => {
let data = response.data;
this.loading = false;
if (data) {
this.details = data;
}
})
},
open(id) {
this.infoVisible = true;
this.loading = true;
this.getDetails(id);
},
openDetail(row, value) {
value.createUser = row.details.createUser;
value.operTime = row.operTime;
if (value.columnName === "tags") {
this.$refs.tagsHistoryDetail.open(value);
} else if (value.columnName === "request" &&
(row.operModule === "接口定义" || row.operModule === "接口定義" || row.operModule === "Api definition")) {
this.$refs.apiHistoryDetail.open(value);
} else {
this.$refs.historyDetail.open(value);
}
},
methods: {
handleClose() {
this.infoVisible = false;
},
getDetails(id) {
this.result = this.$get("/operating/log/get/source/" + id, response => {
let data = response.data;
this.loading =false;
if (data) {
this.details = data;
}
})
},
open(id) {
this.infoVisible = true;
this.loading = true;
this.getDetails(id);
},
openDetail(row, value) {
value.createUser = row.details.createUser;
value.operTime = row.operTime;
this.$refs.historyDetail.open(value);
},
}
}
}
</script>
<style scoped>
.current-value {
display: inline-block;
overflow-x: hidden;
padding-bottom: 0;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
width: 120px;
}
.current-value {
display: inline-block;
overflow-x: hidden;
padding-bottom: 0;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
width: 120px;
}
</style>

View File

@ -0,0 +1,126 @@
<template>
<div>
<el-tabs v-model="activeName">
<el-tab-pane label="Config Center" name="config" v-if="request.config && request.config.length > 0">
<el-table :data="request.config">
<el-table-column prop="columnTitle" :label="$t('operating_log.change_field')">
</el-table-column>
<el-table-column prop="originalValue" :label="$t('operating_log.before_change')">
</el-table-column>
<el-table-column prop="newValue" :label="$t('operating_log.after_change')">
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="Registry Center" name="registry" v-if="request.registry && request.registry.length > 0">
<el-table :data="request.registry">
<el-table-column prop="columnTitle" :label="$t('operating_log.change_field')"/>
<el-table-column prop="originalValue" :label="$t('operating_log.before_change')">
<template v-slot:default="scope">
<el-tooltip :content="scope.row.originalValue">
<div class="current-value ms-tag-del">{{ scope.row.originalValue }}</div>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="newValue" :label="$t('operating_log.after_change')">
<template v-slot:default="scope">
<el-tooltip :content="scope.row.newValue">
<div class="current-value ms-tag-add">{{ scope.row.newValue }}</div>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="Consumer & service" name="service" v-if="request.service && request.service.length > 0">
<el-table :data="request.service">
<el-table-column prop="columnTitle" :label="$t('operating_log.change_field')">
</el-table-column>
<el-table-column prop="originalValue" :label="$t('operating_log.before_change')">
</el-table-column>
<el-table-column prop="newValue" :label="$t('operating_log.after_change')">
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="Args" name="args" v-if="request.args && request.args.length >0 ">
<ms-api-key-value-detail :show-required="true" :items="request.args" :showDesc="true" :format="request.headerId"/>
</el-tab-pane>
<el-tab-pane label="Attachment Args" name="attachment" v-if="request.attachment && request.attachment.length >0 ">
<ms-api-key-value-detail :show-required="true" :items="request.attachment" :showDesc="true" :format="request.headerId"/>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import MsApiKeyValueDetail from "./common/ApiKeyValueDetail";
export default {
name: "ApiDubboParameters",
components: {MsApiKeyValueDetail},
props: {
request: {},
isReadOnly: {
type: Boolean,
default: true
},
},
data() {
return {
spanNum: 21,
activeName: "config",
}
},
created() {
if (this.request.config && this.request.config.length > 0) {
this.activeName = "config";
} else if (this.request.registry && this.request.registry.length > 0) {
this.activeName = "registry";
} else if (this.request.service && this.request.service.length > 0) {
this.activeName = "service";
} else if (this.request.args && this.request.args.length > 0) {
this.activeName = "args";
} else if (this.request.attachment && this.request.attachment.length > 0) {
this.activeName = "attachment";
}
},
watch: {
'request.headerId'() {
if (this.request.config && this.request.config.length > 0) {
this.activeName = "config";
} else if (this.request.registry && this.request.registry.length > 0) {
this.activeName = "registry";
} else if (this.request.service && this.request.service.length > 0) {
this.activeName = "service";
} else if (this.request.args && this.request.args.length > 0) {
this.activeName = "args";
} else if (this.request.attachment && this.request.attachment.length > 0) {
this.activeName = "attachment";
}
}
},
methods: {}
}
</script>
<style scoped>
.current-value {
display: inline-block;
overflow-x: hidden;
padding-bottom: 0;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
width: 120px;
}
.ms-tag-del {
text-decoration:line-through;
text-decoration-color: red;
-moz-text-decoration-line: line-through;
background: #F3E6E7;
}
.ms-tag-add {
background: #E2ECDC;
}
</style>

View File

@ -0,0 +1,218 @@
<template>
<el-dialog :close-on-click-modal="false" :title="$t('operating_log.info')" :visible.sync="infoVisible" width="900px" :destroy-on-close="true"
@close="handleClose" append-to-body>
<div style="height: 700px;overflow: auto">
<div v-if="detail.createUser">
<p class="tip">{{ this.$t('report.user_name') }} {{ detail.createUser }}</p>
</div>
<div>
<p class="tip">{{ this.$t('operating_log.time') }} {{ detail.operTime | timestampFormatDate }}</p>
</div>
<div style="overflow: auto">
<p class="tip">{{ this.$t('report.test_log_details') }} </p>
<ms-api-http-request-params :request="detail" v-if="detail.type === 'HTTPSamplerProxy'"/>
<ms-api-tcp-parameters :request="detail" v-if="detail.type === 'TCPSampler'"/>
<ms-api-jdbc-parameters :request="detail" v-if="detail.type === 'JDBCSampler'"/>
<ms-api-dubbo-parameters :request="detail" v-if="detail.type === 'DubboSampler'"/>
</div>
</div>
</el-dialog>
</template>
<script>
import MsApiHttpRequestParams from "./ApiHttpRequestParams";
import MsApiTcpParameters from "./ApiTcpParameters";
import MsApiJdbcParameters from "./ApiJdbcParameters";
import MsApiDubboParameters from "./ApiDubboParameters";
import {getUUID} from "@/common/js/utils";
import Convert from "@/business/components/common/json-schema/convert/convert";
export default {
name: "MsApiHistoryDetail",
components: {MsApiHttpRequestParams, MsApiTcpParameters, MsApiJdbcParameters, MsApiDubboParameters},
props: {
title: String,
},
data() {
return {
infoVisible: false,
detail: {headerId: getUUID(), body: {}, type: ""},
}
},
methods: {
handleClose() {
this.infoVisible = false;
},
open(value) {
this.infoVisible = true;
this.detail = value;
let diffValue = value.diffValue;
if (diffValue) {
if (value != null && value.diffValue != 'null' && value.diffValue != '' && value.diffValue != undefined) {
if (Object.prototype.toString.call(value.diffValue).match(/\[object (\w+)\]/)[1].toLowerCase() !== 'object'
&& Object.prototype.toString.call(value.diffValue).match(/\[object (\w+)\]/)[1].toLowerCase() !== 'array') {
diffValue = JSON.parse(value.diffValue);
}
}
if (diffValue.type === 'HTTPSamplerProxy') {
this.formatHttp(diffValue);
} else if (diffValue.type === 'TCPSampler') {
this.formatTcp(diffValue);
} else if (diffValue.type === 'JDBCSampler') {
this.formatJdbc(diffValue);
} else if (diffValue.type === 'DubboSampler') {
this.formatDubbo(diffValue);
}
this.detail.type = diffValue.type;
}
},
formatJson(properties) {
if (properties) {
for (let key in properties) {
let value = JSON.stringify(properties[key].mock);
if (value && value.indexOf("**mock") !== -1) {
properties["++" + key] = JSON.parse(JSON.stringify(properties[key]));
properties["--" + key] = JSON.parse(JSON.stringify(properties[key]));
properties["--" + key].mock = {mock: JSON.parse(value)["**mock"]};
this.$delete(properties, key);
}
if (properties[key] && properties[key]["++description"]) {
properties["++" + key] = JSON.parse(JSON.stringify(properties[key]));
properties["++" + key].description = properties[key]["++description"];
properties["--" + key] = JSON.parse(JSON.stringify(properties[key]));
this.$delete(properties, key);
}
if (properties[key] && properties[key]["**description"]) {
properties["++" + key] = JSON.parse(JSON.stringify(properties[key]));
properties["--" + key] = JSON.parse(JSON.stringify(properties[key]));
properties["--" + key].description = properties[key]["**description"];
this.$delete(properties, key);
}
if (properties[key] && properties[key].properties) {
this.formatJson(properties[key].properties);
}
}
}
},
formatHttp(diffValue) {
this.detail.body = {};
this.detail.headerId = getUUID();
if (diffValue.body) {
let jsonSchema = (JSON.parse(diffValue.body)).jsonSchema;
this.formatJson(jsonSchema.properties);
this.detail.body.jsonSchema = jsonSchema;
this.detail.headerId = getUUID();
}
if (diffValue.body_form) {
let form = (JSON.parse(diffValue.body_form)).root;
this.detail.body.form = form;
this.detail.headerId = getUUID();
}
if (diffValue.body_raw_1 || diffValue.body_raw_2) {
this.detail.body.raw_1 = diffValue.body_raw_1;
this.detail.body.raw_2 = diffValue.body_raw_2;
this.detail.headerId = getUUID();
}
if (diffValue.header) {
let header = (JSON.parse(diffValue.header)).root;
this.detail.header = header;
this.detail.headerId = getUUID();
}
if (diffValue.query) {
let query = (JSON.parse(diffValue.query)).root;
this.detail.query = query;
this.detail.headerId = getUUID();
}
if (diffValue.rest) {
let rest = (JSON.parse(diffValue.rest)).root;
this.detail.rest = rest;
this.detail.headerId = getUUID();
}
},
formatTcp(diffValue) {
if (!this.detail.body) {
this.detail.body = {};
}
if (diffValue.query) {
let parameters = (JSON.parse(diffValue.query)).root;
this.detail.parameters = parameters;
this.detail.headerId = getUUID();
}
if (diffValue.body_json) {
const MsConvert = new Convert();
let data = MsConvert.format(JSON.parse(diffValue.body_json));
this.detail.body.jsonSchema = data;
this.detail.headerId = getUUID();
}
if (diffValue.body_xml) {
let parameters = (JSON.parse(diffValue.body_xml)).root;
this.detail.body.xml = parameters;
this.detail.body.xml_1 = diffValue.body_xml_1;
this.detail.body.xml_2 = diffValue.body_xml_2;
this.detail.headerId = getUUID();
}
if (diffValue.body_raw_1 || diffValue.body_raw_2) {
this.detail.body.raw_1 = diffValue.body_raw_1 ? diffValue.body_raw_1 : "";
this.detail.body.raw_2 = diffValue.body_raw_2 ? diffValue.body_raw_2 : "";
this.detail.headerId = getUUID();
}
if (diffValue.body_xml) {
this.detail.body.xml = diffValue.body_xml;
this.detail.headerId = getUUID();
}
if (diffValue.script_1 || diffValue.script_2) {
this.detail.script_1 = diffValue.script_1;
this.detail.script_2 = diffValue.script_2;
this.detail.headerId = getUUID();
}
},
formatJdbc(diffValue) {
if (diffValue.base) {
let parameters = JSON.parse(diffValue.base);
this.detail.base = parameters;
this.detail.headerId = getUUID();
}
if (diffValue.variables) {
let parameters = (JSON.parse(diffValue.variables)).root;
this.detail.variables = parameters;
this.detail.headerId = getUUID();
}
if (diffValue.query_1 || diffValue.query_2) {
this.detail.query_1 = diffValue.query_1;
this.detail.query_2 = diffValue.query_2;
this.detail.headerId = getUUID();
}
},
formatDubbo(diffValue) {
if (diffValue.config) {
this.detail.config = JSON.parse(diffValue.config);
this.detail.headerId = getUUID();
}
if (diffValue.registry) {
this.detail.registry = JSON.parse(diffValue.registry);
this.detail.headerId = getUUID();
}
if (diffValue.service) {
this.detail.service = JSON.parse(diffValue.service);
this.detail.headerId = getUUID();
}
if (diffValue.args) {
let parameters = (JSON.parse(diffValue.args)).root;
this.detail.args = parameters;
this.detail.headerId = getUUID();
}
if (diffValue.attachment) {
let parameters = (JSON.parse(diffValue.attachment)).root;
this.detail.attachment = parameters;
this.detail.headerId = getUUID();
}
},
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,151 @@
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
<!-- HTTP 请求参数 -->
<div style="border:1px #DCDFE6 solid;" v-if="!loading">
<el-tabs v-model="activeName" class="request-tabs">
<!-- 请求头-->
<el-tab-pane :label="$t('api_test.request.headers')" name="headers" v-if="request.header">
<ms-api-key-value-detail :items="request.header" :showDesc="true" :format="request.headerId"/>
</el-tab-pane>
<!--query 参数-->
<el-tab-pane :label="$t('api_test.definition.request.query_param')" name="parameters" v-if="request.query">
<ms-api-key-value-detail :show-required="true" :items="request.query" :showDesc="true" :format="request.headerId"/>
</el-tab-pane>
<!--REST 参数-->
<el-tab-pane :label="$t('api_test.definition.request.rest_param')" name="rest" v-if="request.rest">
<ms-api-key-value-detail :show-required="true" :items="request.rest" :showDesc="true" :format="request.headerId"/>
</el-tab-pane>
<!--请求体-->
<el-tab-pane :label="$t('api_test.request.body')" name="body" v-if="request.body && (request.body.jsonSchema ||request.body.raw_1 || request.body.raw_2 )">
<el-radio-group v-model="activeBody" size="mini">
<el-radio-button label="json"/>
<el-radio-button label="raw"/>
<el-radio-button label="form"/>
</el-radio-group>
<ms-json-code-edit :body="request.body" ref="jsonCodeEdit" v-if="activeBody === 'json'"/>
<pre v-html="getDiff(request.body.raw_2,request.body.raw_1)" v-if="activeBody === 'raw'"></pre>
<ms-api-key-value-detail :show-required="true" :items="request.body.form" :showDesc="true" :format="request.headerId" v-if="activeBody === 'form'"/>
</el-tab-pane>
<!-- 认证配置 -->
<!-- <el-tab-pane :label="$t('api_test.definition.request.auth_config')" name="authConfig">-->
<!-- </el-tab-pane>-->
<!-- <el-tab-pane :label="$t('api_test.definition.request.other_config')" name="advancedConfig">-->
<!-- </el-tab-pane>-->
</el-tabs>
</div>
</template>
<script>
import MsJsonCodeEdit from "./json-view/ComparedEditor";
import MsApiKeyValueDetail from "./common/ApiKeyValueDetail";
const jsondiffpatch = require('jsondiffpatch');
const formattersHtml = jsondiffpatch.formatters.html;
export default {
name: "MsApiHttpRequestParams",
components: {MsJsonCodeEdit, MsApiKeyValueDetail},
props: {
method: String,
request: {},
type: String,
},
data() {
return {
activeName: "",
activeBody: "",
loading: false,
}
},
created() {
if (this.request.body && (this.request.body.jsonSchema || this.request.body.form || this.request.body.raw_1 || this.request.body.raw_2)) {
this.activeName = "body";
if (this.request.body.jsonSchema) {
this.activeBody = "json";
}
if (this.request.body.form) {
this.activeBody = "form";
}
if (this.request.body.raw_1 || this.request.body.raw_2) {
this.activeBody = "raw";
}
} else if (this.request.query) {
this.activeName = "parameters";
} else if (this.request.header) {
this.activeName = "headers";
} else if (this.request.rest) {
this.activeName = "rest";
}
this.reloadCodeEdit();
},
watch: {
'request.headerId'() {
if (this.request.body && (this.request.body.jsonSchema || this.request.body.form || this.request.body.raw_1 || this.request.body.raw_2)) {
this.activeName = "body";
if (this.request.body.json) {
this.activeBody = "json";
}
if (this.request.body.form) {
this.activeBody = "form";
}
if (this.request.body.raw_1 || this.request.body.raw_2) {
this.activeBody = "raw";
}
} else if (this.request.query) {
this.activeName = "parameters";
} else if (this.request.header) {
this.activeName = "headers";
} else if (this.request.rest) {
this.activeName = "rest";
}
this.reloadCodeEdit();
}
},
methods: {
getDiff(v1, v2) {
let delta = jsondiffpatch.diff(v1, v2);
return formattersHtml.format(delta, v1);
},
reloadCodeEdit() {
this.loading = true;
this.$nextTick(() => {
this.loading = false;
});
},
}
}
</script>
<style scoped>
.ms-query {
background: #783887;
color: white;
height: 18px;
border-radius: 42%;
}
.ms-header {
background: #783887;
color: white;
height: 18px;
border-radius: 42%;
}
.request-tabs {
margin: 20px;
min-height: 200px;
}
.ms-el-link {
float: right;
margin-right: 45px;
}
@import "~jsondiffpatch/dist/formatters-styles/html.css";
@import "~jsondiffpatch/dist/formatters-styles/annotated.css";
</style>

View File

@ -0,0 +1,100 @@
<template>
<div v-loading="isReloadData">
<el-tabs v-model="activeName">
<el-tab-pane :label="$t('api_test.definition.request.req_param')" name="parameters" v-if="request.base && request.base.length > 0">
<el-table :data="request.base">
<el-table-column prop="columnTitle" :label="$t('operating_log.change_field')">
</el-table-column>
<el-table-column prop="originalValue" :label="$t('operating_log.before_change')">
</el-table-column>
<el-table-column prop="newValue" :label="$t('operating_log.after_change')">
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.scenario.variables')" name="variables" v-if="request.variables && request.variables.length >0">
<ms-api-key-value-detail :items="request.variables" :showDesc="true" :format="request.headerId"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.sql.sql_script')" name="sql" v-if="request.query_1 || request.query_2">
<pre v-html="getDiff(request.query_2,request.query_1)"/>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import MsApiKeyValueDetail from "./common/ApiKeyValueDetail";
const jsondiffpatch = require('jsondiffpatch');
const formattersHtml = jsondiffpatch.formatters.html;
export default {
name: "MsApiJdbcParameters",
components: {MsApiKeyValueDetail},
props: {
request: {},
basisData: {},
moduleOptions: Array,
showScript: {
type: Boolean,
default: true,
},
isReadOnly: {
type: Boolean,
default: false
},
},
data() {
return {
spanNum: 21,
environments: [],
currentEnvironment: {},
databaseConfigsOptions: [],
isReloadData: false,
activeName: "variables",
rules: {},
}
},
watch: {
'request.headerId'() {
if (this.request.base) {
this.activeName = "parameters";
} else if (this.request.variables) {
this.activeName = "variables";
} else if (this.request.query_1 || this.request.query_2) {
this.activeName = "sql";
}
}
},
created() {
if (this.request.base) {
this.activeName = "parameters";
} else if (this.request.variables) {
this.activeName = "variables";
} else if (this.request.query_1 || this.request.query_2) {
this.activeName = "sql";
}
},
computed: {},
methods: {
getDiff(v1, v2) {
let delta = jsondiffpatch.diff(v1, v2);
return formattersHtml.format(delta, v1);
},
}
}
</script>
<style scoped>
.one-row .el-form-item {
display: inline-block;
}
.one-row .el-form-item:nth-child(2) {
margin-left: 60px;
}
@import "~jsondiffpatch/dist/formatters-styles/html.css";
@import "~jsondiffpatch/dist/formatters-styles/annotated.css";
</style>

View File

@ -0,0 +1,187 @@
<template>
<div>
<div style="border:1px #DCDFE6 solid; height: 100%;border-radius: 4px ;width: 98% ;">
<el-form class="tcp" :model="request" ref="request" :disabled="isReadOnly" style="margin: 20px">
<el-tabs v-model="activeName" class="request-tabs">
<el-tab-pane name="parameters" :label="$t('api_test.definition.request.req_param')" v-if="request.parameters">
<ms-api-key-value-detail :items="request.parameters" :show-required="true" :showDesc="true" :format="request.headerId"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.definition.document.request_body')" name="request" v-if="request.body && (request.body.jsonSchema ||request.body.xml || request.body.raw_1 || request.body.raw_2)">
<el-radio-group v-model="reportType" size="mini" style="margin: 10px 0px;">
<el-radio :disabled="isReadOnly" label="json">
json
</el-radio>
<el-radio :disabled="isReadOnly" label="xml">
xml
</el-radio>
<el-radio :disabled="isReadOnly" label="raw">
raw
</el-radio>
</el-radio-group>
<div v-if="reportType === 'xml'">
<pre v-html="getDiff(request.body.xml_2,request.body.xml_1)"></pre>
</div>
<div v-if="reportType === 'json'">
<div class="send-request">
<ms-json-code-edit :body="request.body" ref="jsonCodeEdit"/>
</div>
</div>
<div v-if="reportType === 'raw'">
<div class="send-request">
<pre v-html="getDiff(request.body.raw_2,request.body.raw_1)"></pre>
</div>
</div>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.definition.request.pre_script')" name="script" v-if="request.script_1 || request.script_2">
<pre v-html="getDiff(request.script_2,request.script_1)"></pre>
</el-tab-pane>
<!-- <el-tab-pane :label="$t('api_test.definition.request.other_config')" name="other" class="other-config">-->
<!-- </el-tab-pane>-->
</el-tabs>
</el-form>
</div>
</div>
</template>
<script>
import MsJsonCodeEdit from "./json-view/ComparedEditor";
import MsApiKeyValueDetail from "./common/ApiKeyValueDetail";
const jsondiffpatch = require('jsondiffpatch');
const formattersHtml = jsondiffpatch.formatters.html;
export default {
name: "MsApiTcpParameters",
components: {MsJsonCodeEdit, MsApiKeyValueDetail},
props: {
request: {},
basisData: {},
moduleOptions: Array,
isReadOnly: {
type: Boolean,
default: false
},
showScript: {
type: Boolean,
default: true,
},
referenced: {
type: Boolean,
default: false,
},
},
data() {
return {
spanNum: 21,
activeName: "request",
reportType: "xml",
isReloadData: false,
refreshedXmlTable: true,
currentProjectId: "",
}
},
created() {
if (this.request.body && (this.request.body.jsonSchema || this.request.body.xml || this.request.body.raw_1 || this.request.body.raw_2)) {
this.activeName = "request";
if (this.request.body.jsonSchema) {
this.reportType = "json";
}
if (this.request.body.xml) {
this.reportType = "xml";
}
if (this.request.body.raw_1 || this.request.body.raw_2) {
this.reportType = "raw";
}
} else if (this.request.parameters) {
this.activeName = "parameters";
} else if (this.request.script_1 || this.request.script_2) {
this.activeName = "script";
}
},
watch: {
'request.headerId'() {
if (this.request.body) {
this.activeName = "request";
if (this.request.body.jsonSchema) {
this.reportType = "json";
}
if (this.request.body.xml) {
this.reportType = "xml";
}
if (this.request.body.raw_1 || this.request.body.raw_2) {
this.reportType = "raw";
}
} else if (this.request.parameters) {
this.activeName = "parameters";
} else if (this.request.script_1 || this.request.script_2) {
this.activeName = "script";
}
}
},
methods: {
getDiff(v1, v2) {
let delta = jsondiffpatch.diff(v1, v2);
return formattersHtml.format(delta, v1);
},
}
}
</script>
<style scoped>
.tcp >>> .el-input-number {
width: 100%;
}
.send-request {
padding: 0px 0;
height: 300px;
border: 1px #DCDFE6 solid;
border-radius: 4px;
width: 100%;
}
.ms-left-cell {
margin-top: 40px;
}
.ms-left-buttion {
margin: 6px 0px 8px 30px;
}
/deep/ .el-form-item {
margin-bottom: 15px;
}
.ms-left-cell {
margin-top: 40px;
}
.ms-left-buttion {
margin: 6px 0px 8px 30px;
}
/deep/ .el-form-item {
margin-bottom: 15px;
}
/deep/ .instructions-icon {
font-size: 14px !important;
}
.request-tabs {
margin: 20px;
min-height: 200px;
}
.other-config {
padding: 15px;
}
@import "~jsondiffpatch/dist/formatters-styles/html.css";
@import "~jsondiffpatch/dist/formatters-styles/annotated.css";
</style>

View File

@ -0,0 +1,179 @@
<template>
<div v-loading="loading">
<div class="kv-row item" v-for="(item, index) in data" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle" :style="item.style">
<div class="box" v-if="item.box"/>
<el-col class="kv-checkbox" v-if="isShowEnable">
<el-checkbox v-if="!isDisable(index)" v-model="item.enable" :disabled="isReadOnly"/>
</el-col>
<span style="margin-left: 10px" v-else></span>
<el-col class="item">
<input class="el-input el-input__inner" v-if="!suggestions" :disabled="isReadOnly" v-model="item.name" size="small" maxlength="200" show-word-limit :style="item.style"/>
<el-autocomplete :disabled="isReadOnly" :maxlength="400" v-if="suggestions" v-model="item.name" size="small" show-word-limit :style="item.style"/>
</el-col>
<el-col v-if="showRequired">
<input class="el-input el-input__inner" :disabled="isReadOnly" v-model="item.required" size="small" :style="item.style"/>
</el-col>
<el-col class="item">
<input class="el-input el-input__inner" :disabled="isReadOnly" v-model="item.value" size="small" show-word-limit :style="item.style"/>
</el-col>
<el-col class="item" v-if="showDesc">
<input class="el-input el-input__inner" v-model="item.description" size="small" maxlength="200"
:style="item.style"
:placeholder="$t('commons.description')" show-word-limit/>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
export default {
name: "MsApiKeyValueDetail",
components: {},
props: {
keyPlaceholder: String,
valuePlaceholder: String,
isShowEnable: {
type: Boolean,
},
description: String,
items: Array,
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array,
needMock: {
type: Boolean,
default: false
},
showDesc: Boolean,
showRequired: {
type: Boolean,
default: false,
},
format: String,
},
data() {
return {
keyValues: [],
loading: false,
currentItem: {},
isSelectAll: true,
data: [],
}
},
watch: {
isSelectAll: function (to, from) {
if (from == false && to == true) {
this.selectAll();
} else if (from == true && to == false) {
this.invertSelect();
}
},
format: function (to, from) {
this.formatItem();
this.reload();
}
},
methods: {
reload() {
this.loading = true
this.$nextTick(() => {
this.loading = false
})
},
isDisable: function (index) {
return this.items.length - 1 === index;
},
selectAll() {
this.items.forEach(item => {
item.enable = true;
});
},
invertSelect() {
this.items.forEach(item => {
item.enable = false;
});
},
formatItem() {
this.data = [];
if (this.items && this.items.length > 0) {
for (let i in this.items) {
let item = this.items[i];
item.required = item.required ? this.$t('commons.selector.required') : this.$t('commons.selector.not_required');
let itemMap = new Map(Object.entries(item));
let newObj = new Map()
let itemStr = JSON.stringify(item);
if (itemStr.indexOf("--name") !== -1) {
itemStr = itemStr.replaceAll("--", "");
let obj = JSON.parse(itemStr);
obj.style = "background:#F3E6E7;text-decoration:line-through;text-decoration-color:red";
obj.box = true;
this.data.push(obj);
} else if (itemStr.indexOf("++name") !== -1) {
itemStr = itemStr.replaceAll("++", "");
let obj = JSON.parse(itemStr);
obj.style = "background:#E2ECDC";
this.data.push(obj);
} else if (itemStr.indexOf("**") !== -1) {
itemMap.forEach(function (value, key) {
if (key && key.indexOf("**") !== -1) {
item.style = item.style ? item.style : "";
newObj[key.substr(2)] = value;
} else {
item.style = item.style ? item.style : "";
newObj[key] = value;
}
});
item.style = "background:#E2ECDC";
this.data.push(item);
newObj["box"] = true;
newObj["style"] = "background:#F3E6E7;text-decoration:line-through;text-decoration-color:red";
newObj["required"] = newObj.required ? this.$t('commons.selector.required') : this.$t('commons.selector.not_required');
this.data.push(newObj);
} else {
this.data.push(item);
}
}
}
}
},
created() {
this.formatItem();
}
}
</script>
<style scoped>
.el-input {
margin: 0px;
height: 32px;
}
.kv-row {
margin-top: 10px;
}
.kv-checkbox {
width: 20px;
margin-right: 10px;
}
.box {
position: absolute;
width: 100%;
height: 1px;
top: 16px;
z-index: 999;
border-color: red;
background: red;
}
i:hover {
color: #783887;
}
</style>

View File

@ -0,0 +1,89 @@
<template>
<div id="app" v-loading="loading">
<div style="width: 98%">
<compared-editor class="schema" :value="schema" lang="zh_CN" custom/>
</div>
</div>
</template>
<script>
const Convert = require('./convert/convert.js');
const MsConvert = new Convert();
export default {
name: 'App',
components: {},
props: {
body: {},
showPreview: {
type: Boolean,
default: true
},
},
created() {
if (!this.body.jsonSchema && this.body.raw && this.checkIsJson(this.body.raw)) {
let obj = {"root": MsConvert.format(JSON.parse(this.body.raw))}
this.schema = obj;
} else if (this.body.jsonSchema) {
this.schema = {"root": this.body.jsonSchema};
}
this.body.jsonSchema = this.schema.root;
},
watch: {
schema: {
handler(newValue, oldValue) {
this.body.jsonSchema = this.schema.root;
},
deep: true
},
body: {
handler(newValue, oldValue) {
if (!this.body.jsonSchema && this.body.raw && this.checkIsJson(this.body.raw)) {
let obj = {"root": MsConvert.format(JSON.parse(this.body.raw))}
this.schema = obj;
} else if (this.body.jsonSchema) {
this.schema = {"root": this.body.jsonSchema};
}
this.body.jsonSchema = this.schema.root;
},
deep: true
}
},
data() {
return {
schema:
{
"root": {
"type": "object",
"properties": {},
}
},
loading: false,
preview: null,
activeName: "apiTemplate",
}
},
methods: {
openOneClickOperation() {
this.$refs.importJson.openOneClickOperation();
},
checkIsJson(json) {
try {
JSON.parse(json);
return true;
} catch (e) {
return false;
}
},
jsonData(data) {
this.schema.root = {};
this.$nextTick(() => {
this.schema.root = data;
this.body.jsonSchema = this.schema.root;
})
}
}
}
</script>
<style>
</style>

View File

@ -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;
};

View File

@ -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;

View File

@ -0,0 +1,7 @@
import ComparedEditor from './main.vue'
ComparedEditor.install = function (Vue) {
Vue.component(ComparedEditor.name, ComparedEditor)
}
export default ComparedEditor

View File

@ -0,0 +1,430 @@
<template>
<div class="json-schema-editor">
<el-row id="rowId" class="row" :gutter="20">
<div class="box" v-if="pickOpt"/>
<el-col :span="8" class="ms-col-name">
<div :style="{marginLeft:`${10*deep}px`}" class="ms-col-name-c"/>
<span v-if="pickValue.type==='object'" :class="hidden? 'el-icon-caret-left ms-transform':
'el-icon-caret-bottom'" @click="hidden = !hidden"/>
<span v-else style="width:10px;display:inline-block"/>
<input class="el-input el-input__inner ms-input-css" :style="{'background':getBg()}" :disabled="disabled" :value="pickKey" @blur="onInputName" size="small"/>
<el-tooltip v-if="root" :content="$t('schema.checked_all')" placement="top">
<input type="checkbox" :disabled="disabled" class="ms-col-name-required" :style="{'background':getBg()}" @change="onRootCheck"/>
</el-tooltip>
<el-tooltip v-else :content="$t('schema.required')" placement="top">
<input type="checkbox" :disabled="disabled" :checked="checked" :style="{'background-color':getBg()}" class="ms-col-name-required" @change="onCheck"/>
</el-tooltip>
</el-col>
<el-col :span="4">
<input v-model="pickValue.type" :disabled="disabled" class="el-input el-input__inner ms-input-css" size="small" :style="{'background-color':getBg()}"/>
</el-col>
<el-col :span="6">
<input v-if="pickValue && pickValue.mock" v-model="pickValue.mock.mock" :disabled="disabled" class="el-input el-input__inner ms-input-css" size="small" :style="{'background-color':getBg()}"/>
<input v-else v-model="defaultValue" :disabled="disabled" class="el-input el-input__inner ms-input-css" size="small" :style="{'background-color':getBg()}"/>
</el-col>
<el-col :span="6">
<input v-model="pickValue.description" :disabled="disabled" class="el-input el-input__inner ms-input-css" size="small" :style="{'background-color':getBg()}"/>
</el-col>
</el-row>
<template v-if="!hidden && pickValue.properties && !isArray && reloadItemOver">
<compared-editor v-for="(item,key,index) in pickValue.properties"
:value="{[key]:item}" :parent="pickValue" :key="index" :deep="deep+1" :root="false" class="children" :lang="lang" :custom="custom" @changeAllItemsType="changeAllItemsType" @reloadItems="reloadItems"/>
</template>
<template v-if="isArray && reloadItemOver">
<compared-editor v-for="(item,key,index) in pickValue.items" :value="{[key]:item}" :parent="pickValue" :key="index" :deep="deep+1" :root="false" class="children" :lang="lang" :custom="custom" @changeAllItemsType="changeAllItemsType"/>
</template>
</div>
</template>
<script>
import {isNull} from './util'
import {TYPE_NAME, TYPE} from './type/type'
import {getUUID} from "@/common/js/utils";
export default {
name: 'ComparedEditor',
components: {},
props: {
value: {
type: Object,
required: true
},
disabled: { //namename,name
type: Boolean,
default: true
},
disabledType: { //
type: Boolean,
default: false
},
isItem: { //
type: Boolean,
default: false
},
deep: { // deep=0
type: Number,
default: 0
},
root: { //root
type: Boolean,
default: true
},
parent: { //
type: Object,
default: null
},
custom: { //enable custom properties
type: Boolean,
default: false
},
lang: { // i18n language
type: String,
default: 'zh_CN'
}
},
computed: {
pickValue() {
return Object.values(this.value)[0]
},
pickOpt() {
let value = Object.keys(this.value)[0];
if (value && value.indexOf("--") !== -1) {
return true;
}
return false;
},
pickKey() {
let value = Object.keys(this.value)[0];
if (value && value.indexOf("--") !== -1) {
return value.substr(2);
} else if (value && value.indexOf("++") !== -1) {
return value.substr(2);
}
return Object.keys(this.value)[0]
},
isObject() {
return this.pickValue.type === 'object'
},
isArray() {
return this.pickValue.type === 'array'
},
checked() {
return this.parent && this.parent.required && this.parent.required.indexOf(this.pickKey) >= 0
},
advanced() {
return TYPE[this.pickValue.type]
},
advancedAttr() {
return TYPE[this.pickValue.type].attr
},
advancedNotEmptyValue() {
const jsonNode = Object.assign({}, this.advancedValue);
for (let key in jsonNode) {
isNull(jsonNode[key]) && delete jsonNode[key]
}
return jsonNode
},
completeNodeValue() {
const t = {}
for (const item of this.customProps) {
t[item.key] = item.value
}
return Object.assign({}, this.pickValue, this.advancedNotEmptyValue, t)
}
},
data() {
return {
TYPE_NAME,
hidden: false,
countAdd: 1,
modalVisible: false,
reloadItemOver: true,
advancedValue: {},
addProp: {},//
customProps: [],
customing: false,
defaultValue: "",
}
},
methods: {
getBg() {
let value = Object.keys(this.value)[0];
if (value && value.indexOf("--") !== -1) {
return "#F3E6E7";
} else if (value && value.indexOf("++") !== -1) {
return "#E2ECDC";
} else {
return "";
}
},
onInputName(e) {
const val = e.target.value
const p = {};
for (let key in this.parent.properties) {
if (key != this.pickKey) {
p[key] = this.parent.properties[key]
} else {
p[val] = this.parent.properties[key]
delete this.parent.properties[key]
}
}
this.$set(this.parent, 'properties', p)
},
onChangeType() {
if (this.parent && this.parent.type === 'array') {
this.$emit('changeAllItemsType', this.pickValue.type);
} else {
this.$delete(this.pickValue, 'properties')
this.$delete(this.pickValue, 'items')
this.$delete(this.pickValue, 'required')
this.$delete(this.pickValue, 'mock')
if (this.isArray) {
this.$set(this.pickValue, 'items', [{type: 'string', mock: {mock: ""}}]);
}
}
},
changeAllItemsType(changeType) {
if (this.isArray && this.pickValue.items && this.pickValue.items.length > 0) {
this.pickValue.items.forEach(item => {
item.type = changeType;
this.$delete(item, 'properties')
this.$delete(item, 'items')
this.$delete(item, 'required')
this.$delete(item, 'mock')
if (changeType === 'array') {
this.$set(item, 'items', [{type: 'string', mock: {mock: ""}}]);
}
});
}
},
onCheck(e) {
this._checked(e.target.checked, this.parent)
},
onRootCheck(e) {
const checked = e.target.checked
this._deepCheck(checked, this.pickValue)
},
_deepCheck(checked, node) {
if (node.type === 'object' && node.properties) {
checked ? this.$set(node, 'required', Object.keys(node.properties)) : this.$delete(node, 'required')
Object.keys(node.properties).forEach(key => this._deepCheck(checked, node.properties[key]))
} else if (node.type === 'array' && node.items.type === 'object') {
checked ? this.$set(node.items, 'required', Object.keys(node.items.properties)) : this.$delete(node.items, 'required')
Object.keys(node.items.properties).forEach(key => this._deepCheck(checked, node.items.properties[key]))
}
},
_checked(checked, parent) {
let required = parent.required
if (checked) {
required || this.$set(this.parent, 'required', [])
required = this.parent.required
required.indexOf(this.pickKey) === -1 && required.push(this.pickKey)
} else {
const pos = required.indexOf(this.pickKey)
pos >= 0 && required.splice(pos, 1)
}
required.length === 0 && this.$delete(parent, 'required')
},
addChild() {
const node = this.pickValue;
if (this.isArray) {
let childObj = {type: 'string', mock: {mock: ""}}
if (node.items && node.items.length > 0) {
childObj.type = node.items[0].type;
node.items.push(childObj);
} else {
this.$set(this.pickValue, 'items', [childObj]);
}
} else {
const name = this._joinName()
const type = 'string'
node.properties || this.$set(node, 'properties', {})
const props = node.properties
this.$set(props, name, {type: type, mock: {mock: ""}})
}
},
addCustomNode() {
this.$set(this.addProp, 'key', this._joinName())
this.$set(this.addProp, 'value', '')
this.customing = true
},
confirmAddCustomNode() {
this.customProps.push(this.addProp)
this.addProp = {}
this.customing = false
},
removeNode() {
if (this.parent.type && this.parent.type === 'object') {
const {properties, required} = this.parent
this.$delete(properties, this.pickKey)
if (required) {
const pos = required.indexOf(this.pickKey)
pos >= 0 && required.splice(pos, 1)
required.length === 0 && this.$delete(this.parent, 'required')
}
} else if (this.parent.type && this.parent.type === 'array') {
const {items, required} = this.parent
this.$delete(items, this.pickKey)
if (required) {
const pos = required.indexOf(this.pickKey)
pos >= 0 && required.splice(pos, 1)
required.length === 0 && this.$delete(this.parent, 'required')
}
}
this.parentReloadItems();
},
_joinName() {
return `feild_${this.deep}_${this.countAdd++}_${getUUID().substring(0, 5)}`
},
onSetting() {
this.modalVisible = true;
this.advancedValue = {};
this.advancedValue = this.advanced.value
for (const k in this.advancedValue) {
this.advancedValue[k] = this.pickValue[k]
}
},
handleClose() {
this.modalVisible = false;
},
handleOk() {
this.modalVisible = false
for (const key in this.advancedValue) {
if (isNull(this.advancedValue[key])) {
this.$delete(this.pickValue, key)
} else {
this.$set(this.pickValue, key, this.advancedValue[key])
}
}
for (const item of this.customProps) {
this.$set(this.pickValue, item.key, item.value)
}
},
parentReloadItems() {
this.$emit("reloadItems");
},
reloadItems() {
this.reloadItemOver = false;
this.$nextTick(() => {
this.reloadItemOver = true;
})
}
}
}
</script>
<style scoped>
.row-add {
background: #E2ECDC;
}
.row-del {
text-decoration: none;
text-decoration-color: red;
background: #F3E6E7;
}
.row-update {
background: #E2ECDC;
}
.json-schema-editor .row {
display: flex;
margin: 2px;
}
.json-schema-editor .row .ms-col-name {
display: flex;
align-items: center;
}
.json-schema-editor .row .ms-col-name .ms-col-name-c {
display: flex;
align-items: center;
}
.json-schema-editor .row .ms-col-name .ms-col-name-required {
flex: 0 0 30px;
text-align: center;
}
.json-schema-editor .row .ms-col-type {
width: 100%;
}
.json-schema-editor .row .ms-col-setting {
display: inline-block;
}
.json-schema-editor .row .setting-icon {
color: rgba(0, 0, 0, 0.45);
border: none;
}
.json-schema-editor .row .plus-icon {
border: none;
}
.ms-input-css {
height: 32px;
}
.json-schema-editor .row .close-icon {
color: #888;
border: none;
}
.json-schema-editor-advanced-modal {
color: rgba(0, 0, 0, 0.65);
min-width: 600px;
}
.json-schema-editor-advanced-modal pre {
font-family: monospace;
height: 100%;
overflow-y: auto;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 4px;
padding: 12px;
width: 50%;
}
.json-schema-editor-advanced-modal h3 {
display: block;
border-left: 3px solid #1890ff;
padding: 0 8px;
}
.json-schema-editor-advanced-modal .ms-advanced-search-form {
display: flex;
}
.json-schema-editor-advanced-modal .ms-advanced-search-form .ms-form-item .ms-form-item-control-wrapper {
flex: 1;
}
.col-item-setting {
padding-top: 8px;
cursor: pointer;
}
.ms-transform {
transform: rotate(-180deg);
transition: 0ms;
}
.box {
position: absolute;
width: 100%;
height: 1px;
top: 16px;
z-index: 999;
border-color: red;
background: red;
}
</style>

View File

@ -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

View File

@ -0,0 +1,11 @@
const value = {
description: null
}
const attr = {
description: {
name: '描述',
type: 'string'
}
}
const wrapper = {value, attr}
export default wrapper

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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;
}

View File

@ -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
}

View File

@ -0,0 +1,194 @@
<template>
<div
class="el-input-tag input-tag-wrapper"
:class="[size ? 'el-input-tag--' + size : '']"
style="height: auto">
<el-tag
:class="getClass(tag)"
v-for="(tag, idx) in innerTags"
v-bind="$attrs"
type="info"
:key="tag"
:size="size"
:closable="!readOnly"
:disable-transitions="false"
@close="remove(idx)">
{{ getTag(tag) }}
</el-tag>
<input
:disabled="readOnly"
class="tag-input el-input"
v-model="newTag"
:placeholder="$t('commons.tag_tip')"
@keydown.delete.stop="removeLastTag"
@keydown="addNew"
@blur="addNew"/>
</div>
</template>
<script>
export default {
name: 'MsInputTag',
props: {
data: {},
addTagOnKeys: {
type: Array,
default: () => [13, 188, 9]
},
readOnly: {
type: Boolean,
default: false
},
size: {type: String, default: "small"},
prop: {
type: String,
default: "diffValue"
}
},
created() {
if (!this.data[this.prop]) {
this.data[this.prop] = [];
}
},
data() {
return {
newTag: '',
innerTags: this.data[this.prop] ? [...this.data[this.prop]] : []
}
},
watch: {
innerTags() {
this.data[this.prop] = this.innerTags;
}
},
methods: {
addNew(e) {
if (e && (!this.addTagOnKeys.includes(e.keyCode)) && (e.type !== 'blur')) {
return
}
if (e) {
e.stopPropagation()
e.preventDefault()
}
let addSuccess = false
if (this.newTag.includes(',')) {
this.newTag.split(',').forEach(item => {
if (this.addTag(item.trim())) {
addSuccess = true
}
})
} else {
if (this.addTag(this.newTag.trim())) {
addSuccess = true
}
}
if (addSuccess) {
this.tagChange()
this.newTag = ''
}
},
addTag(tag) {
tag = tag.trim()
if (tag && !this.innerTags.includes(tag)) {
this.innerTags.push(tag)
return true
}
return false
},
remove(index) {
this.innerTags.splice(index, 1)
this.tagChange()
},
removeLastTag() {
if (this.newTag) {
return
}
this.innerTags.pop()
this.tagChange()
},
tagChange() {
this.$emit('input', this.innerTags)
},
getTag(tag) {
if (tag && (tag.indexOf("++") !== -1 || tag.indexOf("--") !== -1)) {
tag = tag.substring(2);
}
return tag && tag.length > 10 ? tag.substring(0, 10) + "..." : tag;
},
getClass(tag) {
if (tag && tag.indexOf("++") !== -1) {
return "ms-tag-add";
}
if (tag && tag.indexOf("--") !== -1) {
return "ms-tag-del";
}
return "";
}
}
}
</script>
<style scoped>
.input-tag-wrapper {
position: relative;
font-size: 14px;
background-color: #fff;
background-image: none;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
color: #606266;
display: inline-block;
outline: none;
padding: 0 10px 0 5px;
transition: border-color .2s cubic-bezier(.645, .045, .355, 1);
width: 100%;
}
.el-tag {
margin-right: 4px;
}
.tag-input {
background: transparent;
border: 0;
color: #303133;
font-size: 12px;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", Arial, sans-serif;
outline: none;
padding-left: 0;
width: 100px;
}
.el-input-tag {
height: 40px;
line-height: 40px;
}
.el-input-tag--mini {
height: 28px;
line-height: 28px;
font-size: 12px;
}
.el-input-tag--small {
line-height: 30px;
}
.el-input-tag--medium {
height: 36px;
line-height: 36px;
}
.ms-tag-del {
text-decoration:line-through;
text-decoration-color: red;
-moz-text-decoration-line: line-through;
background: #F3E6E7;
}
.ms-tag-add {
background: #E2ECDC;
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<el-dialog :close-on-click-modal="false" :title="$t('operating_log.info')" :visible.sync="infoVisible" width="900px" :destroy-on-close="true"
@close="handleClose" append-to-body>
<div style="height: 700px;overflow: auto">
<div v-if="detail.createUser">
<p class="tip">{{ this.$t('report.user_name') }} {{ detail.createUser }}</p>
</div>
<div>
<p class="tip">{{ this.$t('operating_log.time') }} {{ detail.operTime | timestampFormatDate }}</p>
</div>
<div style="overflow: auto">
<p class="tip">{{ this.$t('report.test_log_details') }} </p>
<div v-if="!loading">
{{ $t('commons.tag') }}
<ms-input-tag :read-only="true" :data="detail" ref="tag" style="width: 90%"/>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
import MsInputTag from "./MsInputTag";
export default {
name: "MsTagsHistoryDetail",
components: {MsInputTag},
props: {
title: String,
},
data() {
return {
infoVisible: false,
detail: {},
loading: false,
}
},
methods: {
handleClose() {
this.infoVisible = false;
this.detail = {};
},
open(value) {
this.infoVisible = true;
this.detail = value;
if (value != null && value.diffValue != 'null' && value.diffValue != '' && value.diffValue != undefined) {
if (Object.prototype.toString.call(value.diffValue).match(/\[object (\w+)\]/)[1].toLowerCase() !== 'object'
&& Object.prototype.toString.call(value.diffValue).match(/\[object (\w+)\]/)[1].toLowerCase() !== 'array') {
let diffValue = JSON.parse(value.diffValue);
if (diffValue) {
this.detail.diffValue = diffValue.root;
this.reload();
}
}
}
this.reload();
},
getType(type) {
return this.LOG_TYPE_MAP.get(type);
},
reload() {
this.loading = true
this.$nextTick(() => {
this.loading = false
})
},
}
}
</script>
<style scoped>
</style>

View File

@ -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);