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 diff(TypeSchema oldSchema, TypeSchema newSchema) { List changes = new ArrayList<>(); Map oldAll = fieldsByPath(oldSchema); Map newAll = fieldsByPath(newSchema); Map oldLeaves = leaves(oldSchema); Map newLeaves = leaves(newSchema); Set 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 removed = new LinkedHashSet<>(oldLeaves.keySet()); removed.removeAll(newLeaves.keySet()); removed.removeAll(typeChangedPaths); Set added = new LinkedHashSet<>(newLeaves.keySet()); added.removeAll(oldLeaves.keySet()); added.removeAll(typeChangedPaths); // 路径迁移: // - 下沉(加包装):旧路径是新路径后缀,如 dbName → vo.dbName // - 上提(拆包装):新路径是旧路径后缀,如 vo.dbName → dbName List moves = new ArrayList<>(); Set matchedRemoved = new LinkedHashSet<>(); Set 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 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 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 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 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 fieldsByPath(TypeSchema schema) { Map result = new LinkedHashMap<>(); if (schema == null) { return result; } for (FieldSchema f : schema.getFields().values()) { result.put(f.getPath(), f); } return result; } private Map leaves(TypeSchema schema) { Map 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; } }