Files
schemaCheck/src/main/java/com/codechecker/cache/schema/SkeletonJsonRenderer.java
dongzi 110beb79c0
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped
feat: 项目整体命名修改cache-schema-checker
2026-07-14 11:03:34 +08:00

376 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String> protectedPaths, int maxLen) {
if (schema == null || schema.isEmpty()) {
return "{}";
}
Set<String> 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<Seg> 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<Seg> parsePath(String path) {
List<Seg> 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<String> 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<String, Node> 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<String> 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<String> protectedPaths, int maxLen) {
List<String> 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<String, Node> children = new LinkedHashMap<>();
Node(JsonType type) {
this.type = type;
}
}
}