feat: 项目整体命名修改cache-schema-checker
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped

This commit is contained in:
2026-07-14 11:03:34 +08:00
parent 114b053733
commit 110beb79c0
42 changed files with 197 additions and 199 deletions

View File

@@ -0,0 +1,375 @@
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;
}
}
}