All checks were successful
序列化结构检查 / serialization-schema-check (push) Has been skipped
281 lines
9.7 KiB
Java
281 lines
9.7 KiB
Java
package com.codechecker.cache.diff;
|
||
|
||
import com.codechecker.cache.schema.FieldSchema;
|
||
import com.codechecker.cache.schema.JsonType;
|
||
import com.codechecker.cache.schema.TypeSchema;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.LinkedHashMap;
|
||
import java.util.LinkedHashSet;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.Set;
|
||
|
||
/**
|
||
* 对比两个版本的 {@link TypeSchema},输出结构变更列表。基于叶子字段路径集合进行差异分析。
|
||
*/
|
||
public class SchemaDiffer {
|
||
|
||
/**
|
||
* @return 结构变更列表(不含 keyPattern/location 上下文,由调用方补充)
|
||
*/
|
||
public List<SchemaChange> diff(TypeSchema oldSchema, TypeSchema newSchema) {
|
||
List<SchemaChange> changes = new ArrayList<>();
|
||
|
||
Map<String, FieldSchema> oldAll = fieldsByPath(oldSchema);
|
||
Map<String, FieldSchema> newAll = fieldsByPath(newSchema);
|
||
|
||
Map<String, FieldSchema> oldLeaves = leaves(oldSchema);
|
||
Map<String, FieldSchema> newLeaves = leaves(newSchema);
|
||
|
||
Set<String> typeChangedPaths = new LinkedHashSet<>();
|
||
|
||
// 类型变更:同路径下 JsonType 或 Java 类型任一变化(覆盖 BigDecimal→String 等同桶变更)
|
||
for (String path : oldAll.keySet()) {
|
||
FieldSchema oldField = oldAll.get(path);
|
||
FieldSchema newField = newAll.get(path);
|
||
if (newField == null) {
|
||
continue;
|
||
}
|
||
if (!isTypeChange(oldField, newField)) {
|
||
continue;
|
||
}
|
||
typeChangedPaths.add(path);
|
||
String oldLabel = typeLabel(oldField);
|
||
String newLabel = typeLabel(newField);
|
||
SchemaChange c = new SchemaChange(ChangeType.TYPE_CHANGED);
|
||
c.setFieldPath(path);
|
||
c.setOldValue(oldLabel);
|
||
c.setNewValue(newLabel);
|
||
c.setMessage("字段 " + path + " 类型由 " + oldLabel + " 变为 " + newLabel);
|
||
changes.add(c);
|
||
}
|
||
|
||
Set<String> removed = new LinkedHashSet<>(oldLeaves.keySet());
|
||
removed.removeAll(newLeaves.keySet());
|
||
removed.removeAll(typeChangedPaths);
|
||
Set<String> added = new LinkedHashSet<>(newLeaves.keySet());
|
||
added.removeAll(oldLeaves.keySet());
|
||
added.removeAll(typeChangedPaths);
|
||
|
||
// 路径迁移:
|
||
// - 下沉(加包装):旧路径是新路径后缀,如 dbName → vo.dbName
|
||
// - 上提(拆包装):新路径是旧路径后缀,如 vo.dbName → dbName
|
||
List<String[]> moves = new ArrayList<>();
|
||
Set<String> matchedRemoved = new LinkedHashSet<>();
|
||
Set<String> matchedAdded = new LinkedHashSet<>();
|
||
for (String r : removed) {
|
||
if (matchedRemoved.contains(r)) {
|
||
continue;
|
||
}
|
||
for (String a : added) {
|
||
if (matchedAdded.contains(a)) {
|
||
continue;
|
||
}
|
||
if (isSuffix(a, r) || isSuffix(r, a)) {
|
||
moves.add(new String[]{r, a});
|
||
matchedRemoved.add(r);
|
||
matchedAdded.add(a);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 新增包装层:多个迁移共享同一新前缀
|
||
Map<String, Integer> newPrefixCount = new LinkedHashMap<>();
|
||
for (String[] move : moves) {
|
||
String a = move[1];
|
||
if (a.contains(".")) {
|
||
String prefix = a.substring(0, a.indexOf('.'));
|
||
newPrefixCount.merge(prefix, 1, Integer::sum);
|
||
}
|
||
}
|
||
for (Map.Entry<String, Integer> e : newPrefixCount.entrySet()) {
|
||
if (e.getValue() >= 2) {
|
||
SchemaChange c = new SchemaChange(ChangeType.WRAPPER_ADDED);
|
||
c.setFieldPath(e.getKey());
|
||
c.setNewValue(e.getKey());
|
||
c.setMessage("新增包装层 " + e.getKey() + ",原顶层字段被下移至该层(影响 " + e.getValue() + " 个字段)");
|
||
changes.add(c);
|
||
}
|
||
}
|
||
|
||
// 删除包装层:多个迁移共享同一旧前缀
|
||
Map<String, Integer> oldPrefixCount = new LinkedHashMap<>();
|
||
for (String[] move : moves) {
|
||
String r = move[0];
|
||
if (r.contains(".")) {
|
||
String prefix = r.substring(0, r.indexOf('.'));
|
||
oldPrefixCount.merge(prefix, 1, Integer::sum);
|
||
}
|
||
}
|
||
for (Map.Entry<String, Integer> e : oldPrefixCount.entrySet()) {
|
||
if (e.getValue() >= 2) {
|
||
SchemaChange c = new SchemaChange(ChangeType.WRAPPER_REMOVED);
|
||
c.setFieldPath(e.getKey());
|
||
c.setOldValue(e.getKey());
|
||
c.setMessage("删除包装层 " + e.getKey() + ",其下字段被提升为外层(影响 " + e.getValue() + " 个字段)");
|
||
changes.add(c);
|
||
}
|
||
}
|
||
|
||
for (String[] move : moves) {
|
||
SchemaChange c = new SchemaChange(ChangeType.FIELD_PATH_MOVED);
|
||
c.setFieldPath(move[1]);
|
||
c.setOldValue(move[0]);
|
||
c.setNewValue(move[1]);
|
||
c.setMessage("字段路径迁移:" + move[0] + " → " + move[1]);
|
||
changes.add(c);
|
||
}
|
||
|
||
// 剩余删除
|
||
for (String r : removed) {
|
||
if (matchedRemoved.contains(r)) {
|
||
continue;
|
||
}
|
||
SchemaChange c = new SchemaChange(ChangeType.FIELD_REMOVED);
|
||
c.setFieldPath(r);
|
||
c.setOldValue(typeLabel(oldLeaves.get(r)));
|
||
c.setMessage("删除字段 " + r);
|
||
changes.add(c);
|
||
}
|
||
|
||
// 剩余新增
|
||
for (String a : added) {
|
||
if (matchedAdded.contains(a)) {
|
||
continue;
|
||
}
|
||
SchemaChange c = new SchemaChange(ChangeType.FIELD_ADDED);
|
||
c.setFieldPath(a);
|
||
c.setNewValue(typeLabel(newLeaves.get(a)));
|
||
c.setMessage("新增字段 " + a);
|
||
changes.add(c);
|
||
}
|
||
|
||
return changes;
|
||
}
|
||
|
||
private boolean isTypeChange(FieldSchema oldField, FieldSchema newField) {
|
||
if (oldField.getJsonType() != newField.getJsonType()) {
|
||
return true;
|
||
}
|
||
String oldJava = normalizeJavaType(oldField.getJavaType());
|
||
String newJava = normalizeJavaType(newField.getJavaType());
|
||
if (oldJava.isEmpty() || newJava.isEmpty()) {
|
||
return false;
|
||
}
|
||
return !oldJava.equals(newJava);
|
||
}
|
||
|
||
/**
|
||
* 展示用类型标签:优先 Java 简单名,并附带 JsonType(便于识别同桶变更)。
|
||
*/
|
||
private String typeLabel(FieldSchema field) {
|
||
if (field == null) {
|
||
return "?";
|
||
}
|
||
String java = simpleJavaType(field.getJavaType());
|
||
JsonType json = field.getJsonType();
|
||
if (java.isEmpty() || "?".equals(java)) {
|
||
return json == null ? "?" : json.name();
|
||
}
|
||
if (json == null) {
|
||
return java;
|
||
}
|
||
return java + "/" + json.name();
|
||
}
|
||
|
||
/**
|
||
* 归一化后用于相等比较:去掉泛型与包名,基本类型与包装类型视为同一序列化类型。
|
||
*/
|
||
static String normalizeJavaType(String javaType) {
|
||
String simple = simpleJavaType(javaType);
|
||
if (simple.isEmpty() || "?".equals(simple)) {
|
||
return "";
|
||
}
|
||
switch (simple) {
|
||
case "int":
|
||
return "Integer";
|
||
case "long":
|
||
return "Long";
|
||
case "short":
|
||
return "Short";
|
||
case "byte":
|
||
return "Byte";
|
||
case "double":
|
||
return "Double";
|
||
case "float":
|
||
return "Float";
|
||
case "boolean":
|
||
return "Boolean";
|
||
case "char":
|
||
return "Character";
|
||
default:
|
||
return simple;
|
||
}
|
||
}
|
||
|
||
private static String simpleJavaType(String javaType) {
|
||
if (javaType == null) {
|
||
return "";
|
||
}
|
||
String s = javaType.trim();
|
||
if (s.isEmpty()) {
|
||
return "";
|
||
}
|
||
int lt = s.indexOf('<');
|
||
if (lt > 0) {
|
||
s = s.substring(0, lt).trim();
|
||
}
|
||
int dot = s.lastIndexOf('.');
|
||
if (dot >= 0 && dot < s.length() - 1) {
|
||
s = s.substring(dot + 1);
|
||
}
|
||
return s;
|
||
}
|
||
|
||
private Map<String, FieldSchema> fieldsByPath(TypeSchema schema) {
|
||
Map<String, FieldSchema> result = new LinkedHashMap<>();
|
||
if (schema == null) {
|
||
return result;
|
||
}
|
||
for (FieldSchema f : schema.getFields().values()) {
|
||
result.put(f.getPath(), f);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private Map<String, FieldSchema> leaves(TypeSchema schema) {
|
||
Map<String, FieldSchema> result = new LinkedHashMap<>();
|
||
if (schema == null) {
|
||
return result;
|
||
}
|
||
for (FieldSchema f : schema.getFields().values()) {
|
||
if (f.getJsonType() != JsonType.OBJECT && f.getJsonType() != JsonType.ARRAY) {
|
||
result.put(f.getPath(), f);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 判断 full 的按段后缀是否等于 suffix(如 vo.dbName 的后缀是 dbName)。
|
||
*/
|
||
private boolean isSuffix(String full, String suffix) {
|
||
if (full.equals(suffix)) {
|
||
return false;
|
||
}
|
||
String[] fullSeg = full.split("\\.");
|
||
String[] sufSeg = suffix.split("\\.");
|
||
if (sufSeg.length >= fullSeg.length) {
|
||
return false;
|
||
}
|
||
for (int i = 1; i <= sufSeg.length; i++) {
|
||
if (!fullSeg[fullSeg.length - i].equals(sufSeg[sufSeg.length - i])) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
}
|