package com.battery.device.api.credentials; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.Properties; import java.util.function.Function; import com.battery.device.api.DeviceAdapterException; /** * 来源凭据的取值顺序(ADR-0065):进程工作目录下的 {@code config/vendor-credentials.properties} * → 同名环境变量 → 同名 JVM 启动参数。 * *

凭据不进数据库、不进仓库,也不打包;仓库只提交 {@code .example} 模板。 * 文件优先是为了让部署只做"复制模板填一行"这一件事;保留环境变量与启动参数两条, * 是因为交付形态可能只提供其中某一种。三者都没有时装配失败,错误信息同时点名文件路径与键名, * 使"忘了放凭据"是可见的失败而不是沉默的缺失。 * *

直连厂商库的口令({@code NEWARE_MYSQL})与推式来源的接收端点令牌({@code PUSH_INGEST}) * 都是来源凭据,因此共用同一个文件与同一套取值顺序。 */ public final class LocalCredentials { /** 相对进程工作目录定位,使开发(仓库根)与交付(应用目录)用同一个位置。 */ public static final Path DEFAULT_FILE = Path.of("config", "vendor-credentials.properties"); private LocalCredentials() { } /** 按默认位置与进程环境取值;取不到时抛出 {@link DeviceAdapterException}。 */ public static String resolve(String key) { return resolve(key, DEFAULT_FILE.toAbsolutePath(), System.getenv(), System::getProperty); } /** * 取值顺序:文件 → 环境变量 → 启动参数。文件里的空白值不算配置过, * 否则一个空模板会把失败推到连接阶段。 */ public static String resolve(String key, Path credentialsFile, Map environment, Function startupParameters) { if (key == null || key.isBlank()) throw new DeviceAdapterException("未配置来源凭据的取值名"); String fromFile = fromFile(key, credentialsFile); if (fromFile != null) return fromFile; String fromEnvironment = environment == null ? null : environment.get(key); if (fromEnvironment != null && !fromEnvironment.isBlank()) return fromEnvironment.trim(); String fromParameter = startupParameters == null ? null : startupParameters.apply(key); if (fromParameter != null && !fromParameter.isBlank()) return fromParameter.trim(); throw new DeviceAdapterException("缺少来源凭据:" + credentialsFile + " 中没有键 " + key + ",环境变量与启动参数 -D" + key + " 也都没有"); } /** 凭据文件缺失不是错误:交付形态可能只用环境变量,所以这里只负责"读到了就返回"。 */ private static String fromFile(String key, Path credentialsFile) { if (credentialsFile == null || !Files.isRegularFile(credentialsFile)) return null; Properties properties = new Properties(); try (InputStream input = Files.newInputStream(credentialsFile)) { properties.load(input); } catch (IOException exception) { throw new DeviceAdapterException("来源凭据文件无法读取:" + credentialsFile, exception); } String value = properties.getProperty(key); return value == null || value.isBlank() ? null : value.trim(); } }