package com.codechecker.cache.schema; 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} 还原为带占位符的序列化骨架 JSON。 * 超长时整段压缩/截断,但受保护(改动)字段路径对应片段不被截断。 */ public class SkeletonJsonRenderer { /** 单侧骨架默认最大长度(企微 markdown 总长约 4096)。 */ public static final int DEFAULT_MAX_LEN = 1500; public String render(TypeSchema schema) { return render(schema, null, Integer.MAX_VALUE); } /** * @param protectedPaths 改动相关字段路径(如 vo.dbName、expiresAtMs),压缩时优先保留 * @param maxLen 输出最大字符数 */ public String render(TypeSchema schema, Set protectedPaths, int maxLen) { if (schema == null || schema.isEmpty()) { return "{}"; } Set protectedSet = protectedPaths == null ? new LinkedHashSet<>() : new LinkedHashSet<>(protectedPaths); Node root = buildTree(schema); String full = write(root, "", protectedSet, false); if (full.length() <= maxLen) { return full; } String compact = write(root, "", protectedSet, true); if (compact.length() <= maxLen) { return compact; } return truncatePreserve(compact, protectedSet, maxLen); } private Node buildTree(TypeSchema schema) { Node root = new Node(JsonType.OBJECT); for (FieldSchema field : schema.getFields().values()) { putPath(root, field.getPath(), field.getJsonType()); } // 根数组:字段以 [] / [].xxx 记录 if (root.children.size() == 1 && root.children.containsKey("[]")) { Node arr = new Node(JsonType.ARRAY); arr.children.put("[]", root.children.get("[]")); return arr; } return root; } private void putPath(Node root, String path, JsonType type) { List segs = parsePath(path); if (segs.isEmpty()) { return; } Node cur = root; for (int i = 0; i < segs.size(); i++) { Seg seg = segs.get(i); boolean last = i == segs.size() - 1; if (seg.array) { Node arr = cur.children.computeIfAbsent(seg.name, k -> new Node(JsonType.ARRAY)); arr.type = JsonType.ARRAY; Node elem = arr.children.computeIfAbsent("[]", k -> new Node(JsonType.OBJECT)); if (last) { if (type == JsonType.OBJECT || type == JsonType.ARRAY || type == JsonType.MAP) { elem.type = type; } else { elem.type = type; elem.leaf = true; } } cur = elem; } else { Node child = cur.children.computeIfAbsent(seg.name, k -> new Node(last ? type : JsonType.OBJECT)); if (last) { child.type = type; child.leaf = type != JsonType.OBJECT && type != JsonType.ARRAY && type != JsonType.MAP; } else if (child.type != JsonType.ARRAY) { child.type = JsonType.OBJECT; } cur = child; } } } private List parsePath(String path) { List segs = new ArrayList<>(); if (path == null || path.isEmpty()) { return segs; } for (String raw : path.split("\\.")) { if (raw.isEmpty()) { continue; } if ("[]".equals(raw)) { segs.add(new Seg("[]", false)); } else if (raw.endsWith("[]")) { segs.add(new Seg(raw.substring(0, raw.length() - 2), true)); } else { segs.add(new Seg(raw, false)); } } return segs; } private String write(Node node, String pathPrefix, Set protectedPaths, boolean compact) { if (node == null) { return "null"; } if (node.type == JsonType.ARRAY) { Node elem = node.children.get("[]"); if (elem == null) { return "[]"; } String elemPath = pathPrefix.isEmpty() ? "[]" : pathPrefix + "[]"; if (compact && !isProtectedUnder(pathPrefix, protectedPaths) && !isProtectedUnder(elemPath, protectedPaths)) { return "[...]"; } return "[" + write(elem, elemPath, protectedPaths, compact) + "]"; } if (node.leaf || isScalar(node.type)) { return placeholder(node.type); } if (node.type == JsonType.MAP) { return "{}"; } StringBuilder sb = new StringBuilder(); sb.append('{'); boolean first = true; for (Map.Entry e : node.children.entrySet()) { String name = e.getKey(); if (name == null || name.isEmpty()) { continue; } Node child = e.getValue(); String childPath = pathPrefix.isEmpty() ? name : pathPrefix + "." + name; // 根数组占位名 [] 不作为 JSON key 输出(由上层 ARRAY 处理) if ("[]".equals(name) && pathPrefix.isEmpty()) { continue; } if (!first) { sb.append(','); } first = false; sb.append('"').append(escape(name)).append("\":"); if (compact && !isProtectedUnder(childPath, protectedPaths) && child.type != JsonType.ARRAY) { sb.append(collapsedValue(child)); } else if (child.type == JsonType.ARRAY) { sb.append(write(child, childPath, protectedPaths, compact)); } else if (child.leaf || isScalar(child.type)) { sb.append(placeholder(child.type)); } else { sb.append(write(child, childPath, protectedPaths, compact)); } } sb.append('}'); return sb.toString(); } private String collapsedValue(Node child) { if (child.type == JsonType.ARRAY) { return "[...]"; } if (child.type == JsonType.OBJECT || child.type == JsonType.MAP) { return "\"...\""; } return placeholder(child.type); } private boolean isProtectedUnder(String pathPrefix, Set protectedPaths) { if (protectedPaths == null || protectedPaths.isEmpty() || pathPrefix == null) { return false; } for (String p : protectedPaths) { if (p == null || p.isEmpty()) { continue; } if (p.equals(pathPrefix)) { return true; } if (pathPrefix.isEmpty()) { continue; } if (p.startsWith(pathPrefix + ".") || p.startsWith(pathPrefix + "[")) { return true; } } return false; } private String truncatePreserve(String json, Set protectedPaths, int maxLen) { List fragments = new ArrayList<>(); for (String path : protectedPaths) { String key = lastSegment(path); if (key.isEmpty() || "[]".equals(key)) { continue; } String needle = "\"" + key + "\""; int idx = json.indexOf(needle); if (idx < 0) { continue; } int end = findValueEnd(json, idx + needle.length()); String frag = json.substring(idx, Math.min(json.length(), end)); if (!fragments.contains(frag)) { fragments.add(frag); } } StringBuilder kept = new StringBuilder(); for (String f : fragments) { if (kept.length() > 0 && kept.length() + f.length() + 1 > maxLen) { // 不截断改动字段:装不下则整段保留已收集的改动片段 break; } if (kept.length() > 0) { kept.append(','); } kept.append(f); // 单条改动字段允许超过 maxLen(不可截断) if (fragments.size() == 1 && kept.length() > maxLen) { return kept.toString(); } } String focus = kept.toString(); if (focus.length() >= maxLen) { return focus; } int markerLen = 20; int budget = maxLen - focus.length() - (focus.isEmpty() ? 0 : markerLen); if (budget < 8) { return focus.isEmpty() ? json.substring(0, Math.min(maxLen, json.length())) : "{...(truncated)," + focus + "}"; } String head = json.substring(0, Math.min(budget, json.length())); if (focus.isEmpty()) { return head + (json.length() > head.length() ? "..." : ""); } return head + "...[改动字段]..." + focus; } private int findValueEnd(String json, int afterKey) { int i = afterKey; while (i < json.length() && (json.charAt(i) == ':' || Character.isWhitespace(json.charAt(i)))) { i++; } if (i >= json.length()) { return json.length(); } char c = json.charAt(i); if (c == '"') { i++; while (i < json.length()) { char ch = json.charAt(i++); if (ch == '\\' && i < json.length()) { i++; } else if (ch == '"') { break; } } return i; } if (c == '{' || c == '[') { int depth = 0; for (; i < json.length(); i++) { char ch = json.charAt(i); if (ch == '{' || ch == '[') { depth++; } else if (ch == '}' || ch == ']') { depth--; if (depth == 0) { return i + 1; } } else if (ch == '"') { i++; while (i < json.length()) { char x = json.charAt(i++); if (x == '\\' && i < json.length()) { i++; } else if (x == '"') { break; } } i--; } } return json.length(); } while (i < json.length() && json.charAt(i) != ',' && json.charAt(i) != '}' && json.charAt(i) != ']') { i++; } return i; } private String lastSegment(String path) { if (path == null || path.isEmpty()) { return ""; } String p = path; if (p.endsWith("[]")) { p = p.substring(0, p.length() - 2); } int dot = p.lastIndexOf('.'); String seg = dot >= 0 ? p.substring(dot + 1) : p; if (seg.endsWith("[]")) { seg = seg.substring(0, seg.length() - 2); } return seg.replace("[]", ""); } private boolean isScalar(JsonType type) { return type == JsonType.STRING || type == JsonType.NUMBER || type == JsonType.BOOLEAN || type == JsonType.UNKNOWN; } private String placeholder(JsonType type) { if (type == null) { return "null"; } switch (type) { case NUMBER: return "0"; case BOOLEAN: return "false"; case STRING: return "\"\""; case MAP: case OBJECT: return "{}"; case ARRAY: return "[]"; case UNKNOWN: default: return "null"; } } private String escape(String s) { return s.replace("\\", "\\\\").replace("\"", "\\\""); } private static final class Seg { final String name; final boolean array; Seg(String name, boolean array) { this.name = name; this.array = array; } } private static final class Node { JsonType type; boolean leaf; final Map children = new LinkedHashMap<>(); Node(JsonType type) { this.type = type; } } }