feat: 项目整体命名修改cache-schema-checker
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped
This commit is contained in:
375
src/main/java/com/codechecker/cache/schema/SkeletonJsonRenderer.java
vendored
Normal file
375
src/main/java/com/codechecker/cache/schema/SkeletonJsonRenderer.java
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user