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 merged = loadDefault(); if (businessConfigPath != null && Files.exists(businessConfigPath)) { Map business = loadYaml(businessConfigPath); merged = deepMerge(merged, business); } return bind(merged); } @SuppressWarnings("unchecked") static Map 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) obj; } catch (IOException e) { throw new IllegalStateException("读取默认配置失败", e); } } @SuppressWarnings("unchecked") static Map loadYaml(Path path) { try (InputStream in = Files.newInputStream(path)) { Object obj = new Yaml().load(in); return obj == null ? new LinkedHashMap<>() : (Map) obj; } catch (IOException e) { throw new IllegalStateException("读取业务配置失败: " + path, e); } } @SuppressWarnings("unchecked") static Map deepMerge(Map base, Map override) { Map result = new LinkedHashMap<>(base); for (Map.Entry 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) baseValue, (Map) overrideValue)); } else { // 标量、列表:业务配置直接覆盖 result.put(key, overrideValue); } } return result; } @SuppressWarnings("unchecked") private static CheckerConfig bind(Map 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 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 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"))); Map detection = asMap(map.get("detection")); CheckerConfig.Detection d = config.getDetection(); d.setPatterns(strList(detection.get("patterns"))); d.setMinConfidence(dbl(detection, "min_confidence", 0.6)); d.setMaxFieldDepth((int) lng(detection, "max_field_depth", 8)); Map severityOverrides = asMap(map.get("severity_overrides")); Map so = new LinkedHashMap<>(); for (Map.Entry e : severityOverrides.entrySet()) { so.put(e.getKey(), String.valueOf(e.getValue())); } config.setSeverityOverrides(so); List mappings = new ArrayList<>(); for (Object item : asList(map.get("manual_mappings"))) { Map 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 suppressions = new ArrayList<>(); for (Object item : asList(map.get("suppressions"))) { Map 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 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 asMap(Object obj) { if (obj instanceof Map) { return (Map) obj; } return new LinkedHashMap<>(); } private static List asList(Object obj) { if (obj instanceof List) { return (List) obj; } return new ArrayList<>(); } private static List strList(Object obj) { List 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 map, String key, String def) { Object v = map.get(key); return v == null ? def : String.valueOf(v); } private static boolean bool(Map 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 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 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)); } }