376 lines
12 KiB
Java
376 lines
12 KiB
Java
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;
|
||
}
|
||
}
|
||
}
|