215 lines
8.3 KiB
Java
215 lines
8.3 KiB
Java
package com.codechecker.cache.config;
|
||
|
||
import org.yaml.snakeyaml.Yaml;
|
||
|
||
import java.io.IOException;
|
||
import java.io.InputStream;
|
||
import java.nio.charset.StandardCharsets;
|
||
import java.nio.file.Files;
|
||
import java.nio.file.Path;
|
||
import java.util.ArrayList;
|
||
import java.util.LinkedHashMap;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
|
||
/**
|
||
* 双层配置加载器:先读取 jar 内 {@code default-config.yaml},再与业务仓库配置深度合并(业务配置优先)。
|
||
*/
|
||
public final class ConfigLoader {
|
||
|
||
private static final String DEFAULT_CONFIG_RESOURCE = "default-config.yaml";
|
||
|
||
private ConfigLoader() {
|
||
}
|
||
|
||
public static CheckerConfig load(Path businessConfigPath) {
|
||
Map<String, Object> merged = loadDefault();
|
||
if (businessConfigPath != null && Files.exists(businessConfigPath)) {
|
||
Map<String, Object> business = loadYaml(businessConfigPath);
|
||
merged = deepMerge(merged, business);
|
||
}
|
||
return bind(merged);
|
||
}
|
||
|
||
@SuppressWarnings("unchecked")
|
||
static Map<String, Object> loadDefault() {
|
||
try (InputStream in = ConfigLoader.class.getClassLoader().getResourceAsStream(DEFAULT_CONFIG_RESOURCE)) {
|
||
if (in == null) {
|
||
throw new IllegalStateException("jar 内缺少 default-config.yaml");
|
||
}
|
||
Object obj = new Yaml().load(in);
|
||
return obj == null ? new LinkedHashMap<>() : (Map<String, Object>) obj;
|
||
} catch (IOException e) {
|
||
throw new IllegalStateException("读取默认配置失败", e);
|
||
}
|
||
}
|
||
|
||
@SuppressWarnings("unchecked")
|
||
static Map<String, Object> loadYaml(Path path) {
|
||
try (InputStream in = Files.newInputStream(path)) {
|
||
Object obj = new Yaml().load(in);
|
||
return obj == null ? new LinkedHashMap<>() : (Map<String, Object>) obj;
|
||
} catch (IOException e) {
|
||
throw new IllegalStateException("读取业务配置失败: " + path, e);
|
||
}
|
||
}
|
||
|
||
@SuppressWarnings("unchecked")
|
||
static Map<String, Object> deepMerge(Map<String, Object> base, Map<String, Object> override) {
|
||
Map<String, Object> result = new LinkedHashMap<>(base);
|
||
for (Map.Entry<String, Object> entry : override.entrySet()) {
|
||
String key = entry.getKey();
|
||
Object overrideValue = entry.getValue();
|
||
Object baseValue = result.get(key);
|
||
if (baseValue instanceof Map && overrideValue instanceof Map) {
|
||
result.put(key, deepMerge((Map<String, Object>) baseValue, (Map<String, Object>) overrideValue));
|
||
} else {
|
||
// 标量、列表:业务配置直接覆盖
|
||
result.put(key, overrideValue);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
@SuppressWarnings("unchecked")
|
||
private static CheckerConfig bind(Map<String, Object> map) {
|
||
CheckerConfig config = new CheckerConfig();
|
||
|
||
config.setEnabled(bool(map, "enabled", true));
|
||
config.setMode(str(map, "mode", "notify"));
|
||
config.setScanTestSources(bool(map, "scan_test_sources", false));
|
||
config.setSourceRoots(strList(map.get("source_roots")));
|
||
config.setIncludeModules(strList(map.get("include_modules")));
|
||
config.setExcludeModules(strList(map.get("exclude_modules")));
|
||
|
||
Map<String, Object> notify = asMap(map.get("notify"));
|
||
CheckerConfig.Notify n = config.getNotify();
|
||
n.setEnabled(bool(notify, "enabled", true));
|
||
n.setWebhookUrl(resolveWebhookUrl(notify));
|
||
n.setNotifyOnClean(bool(notify, "notify_on_clean", false));
|
||
n.setTitlePrefix(str(notify, "title_prefix", "[序列化结构变更]"));
|
||
|
||
Map<String, Object> ignore = asMap(map.get("ignore"));
|
||
CheckerConfig.Ignore ig = config.getIgnore();
|
||
ig.setKeyPatterns(strList(ignore.get("key_patterns")));
|
||
ig.setFilePatterns(strList(ignore.get("file_patterns")));
|
||
ig.setWriterMethods(strList(ignore.get("writer_methods")));
|
||
ig.setMqDestinations(strList(ignore.get("mq_destinations")));
|
||
|
||
Map<String, Object> detection = asMap(map.get("detection"));
|
||
CheckerConfig.Detection d = config.getDetection();
|
||
d.setPatterns(strList(detection.get("patterns")));
|
||
d.setMqPatterns(strList(detection.get("mq_patterns")));
|
||
d.setMinConfidence(dbl(detection, "min_confidence", 0.6));
|
||
d.setMaxFieldDepth((int) lng(detection, "max_field_depth", 8));
|
||
d.setReadHintsEnabled(bool(detection, "read_hints_enabled", true));
|
||
d.setMqReadHintsEnabled(bool(detection, "mq_read_hints_enabled", true));
|
||
|
||
Map<String, Object> severityOverrides = asMap(map.get("severity_overrides"));
|
||
Map<String, String> so = new LinkedHashMap<>();
|
||
for (Map.Entry<String, Object> e : severityOverrides.entrySet()) {
|
||
so.put(e.getKey(), String.valueOf(e.getValue()));
|
||
}
|
||
config.setSeverityOverrides(so);
|
||
|
||
List<CheckerConfig.ManualMapping> mappings = new ArrayList<>();
|
||
for (Object item : asList(map.get("manual_mappings"))) {
|
||
Map<String, Object> m = asMap(item);
|
||
CheckerConfig.ManualMapping mm = new CheckerConfig.ManualMapping();
|
||
mm.setId(str(m, "id", null));
|
||
mm.setWriterMethod(str(m, "writer_method", null));
|
||
mm.setKeyPattern(str(m, "key_pattern", null));
|
||
mm.setValueType(str(m, "value_type", null));
|
||
mm.setDescription(str(m, "description", null));
|
||
mappings.add(mm);
|
||
}
|
||
config.setManualMappings(mappings);
|
||
|
||
List<CheckerConfig.Suppression> suppressions = new ArrayList<>();
|
||
for (Object item : asList(map.get("suppressions"))) {
|
||
Map<String, Object> m = asMap(item);
|
||
CheckerConfig.Suppression sp = new CheckerConfig.Suppression();
|
||
sp.setId(str(m, "id", null));
|
||
sp.setWriterMethod(str(m, "writer_method", null));
|
||
sp.setKeyPattern(str(m, "key_pattern", null));
|
||
sp.setChangeTypes(strList(m.get("change_types")));
|
||
sp.setReason(str(m, "reason", null));
|
||
suppressions.add(sp);
|
||
}
|
||
config.setSuppressions(suppressions);
|
||
|
||
return config;
|
||
}
|
||
|
||
/**
|
||
* 优先读取 webhook_url;兼容旧字段 webhook_env(值为 http 开头时视为 URL)。
|
||
*/
|
||
private static String resolveWebhookUrl(Map<String, Object> notify) {
|
||
String url = str(notify, "webhook_url", "");
|
||
if (url != null && !url.trim().isEmpty()) {
|
||
return url.trim();
|
||
}
|
||
String legacy = str(notify, "webhook_env", "");
|
||
if (legacy != null && legacy.trim().startsWith("http")) {
|
||
return legacy.trim();
|
||
}
|
||
return "";
|
||
}
|
||
|
||
@SuppressWarnings("unchecked")
|
||
private static Map<String, Object> asMap(Object obj) {
|
||
if (obj instanceof Map) {
|
||
return (Map<String, Object>) obj;
|
||
}
|
||
return new LinkedHashMap<>();
|
||
}
|
||
|
||
private static List<Object> asList(Object obj) {
|
||
if (obj instanceof List) {
|
||
return (List<Object>) obj;
|
||
}
|
||
return new ArrayList<>();
|
||
}
|
||
|
||
private static List<String> strList(Object obj) {
|
||
List<String> result = new ArrayList<>();
|
||
if (obj instanceof List) {
|
||
for (Object o : (List<?>) obj) {
|
||
if (o != null) {
|
||
result.add(String.valueOf(o));
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
private static String str(Map<String, Object> map, String key, String def) {
|
||
Object v = map.get(key);
|
||
return v == null ? def : String.valueOf(v);
|
||
}
|
||
|
||
private static boolean bool(Map<String, Object> map, String key, boolean def) {
|
||
Object v = map.get(key);
|
||
if (v instanceof Boolean) {
|
||
return (Boolean) v;
|
||
}
|
||
return v == null ? def : Boolean.parseBoolean(String.valueOf(v));
|
||
}
|
||
|
||
private static double dbl(Map<String, Object> map, String key, double def) {
|
||
Object v = map.get(key);
|
||
if (v instanceof Number) {
|
||
return ((Number) v).doubleValue();
|
||
}
|
||
return v == null ? def : Double.parseDouble(String.valueOf(v));
|
||
}
|
||
|
||
private static long lng(Map<String, Object> map, String key, long def) {
|
||
Object v = map.get(key);
|
||
if (v instanceof Number) {
|
||
return ((Number) v).longValue();
|
||
}
|
||
return v == null ? def : Long.parseLong(String.valueOf(v));
|
||
}
|
||
}
|