PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
config_loader.cpp
Go to the documentation of this file.
1
6#include "config_loader.h"
7
8#include <algorithm>
9#include <cctype>
10#include <fstream>
11#include <iostream>
12#include <map>
13#include <sstream>
14#include <stdexcept>
15
16namespace kcenon::pacs::samples {
17
18namespace {
19
30class simple_yaml_parser {
31public:
32 using yaml_map = std::map<std::string, std::string>;
33 using yaml_list = std::vector<std::string>;
34
35 explicit simple_yaml_parser(std::string_view content) {
36 parse(content);
37 }
38
39 [[nodiscard]] auto get_string(const std::string& path,
40 const std::string& default_value = "") const
41 -> std::string {
42 auto it = values_.find(path);
43 return it != values_.end() ? it->second : default_value;
44 }
45
46 [[nodiscard]] auto get_int(const std::string& path, int default_value = 0) const
47 -> int {
48 auto value = get_string(path);
49 if (value.empty()) return default_value;
50 try {
51 return std::stoi(value);
52 } catch (...) {
53 return default_value;
54 }
55 }
56
57 [[nodiscard]] auto get_bool(const std::string& path, bool default_value = false) const
58 -> bool {
59 auto value = get_string(path);
60 if (value.empty()) return default_value;
61 std::transform(value.begin(), value.end(), value.begin(), ::tolower);
62 return value == "true" || value == "yes" || value == "on" || value == "1";
63 }
64
65 [[nodiscard]] auto get_list(const std::string& path) const
66 -> std::vector<std::string> {
67 auto it = lists_.find(path);
68 return it != lists_.end() ? it->second : std::vector<std::string>{};
69 }
70
71private:
72 void parse(std::string_view content) {
73 std::istringstream stream{std::string(content)};
74 std::string line;
75 std::vector<std::pair<int, std::string>> path_stack;
76 std::string current_list_path;
77
78 while (std::getline(stream, line)) {
79 // Skip empty lines
80 if (line.empty() || line.find_first_not_of(" \t\r\n") == std::string::npos) {
81 continue;
82 }
83
84 // Skip comments
85 auto non_ws = line.find_first_not_of(" \t");
86 if (non_ws != std::string::npos && line[non_ws] == '#') {
87 continue;
88 }
89
90 // Calculate indentation level
91 int indent = 0;
92 for (char c : line) {
93 if (c == ' ') indent++;
94 else if (c == '\t') indent += 2;
95 else break;
96 }
97
98 // Trim the line
99 auto trimmed = trim(line);
100
101 // Check for list item
102 if (trimmed.starts_with("- ")) {
103 auto item = trim(trimmed.substr(2));
104 item = strip_quotes(item);
105 if (!current_list_path.empty()) {
106 lists_[current_list_path].push_back(item);
107 }
108 continue;
109 }
110
111 // Reset list context if not a list item
112 current_list_path.clear();
113
114 // Parse key: value
115 auto colon_pos = trimmed.find(':');
116 if (colon_pos == std::string::npos) continue;
117
118 std::string key = trim(trimmed.substr(0, colon_pos));
119 std::string value = (colon_pos + 1 < trimmed.size())
120 ? trim(trimmed.substr(colon_pos + 1))
121 : "";
122
123 // Pop path stack to appropriate level
124 while (!path_stack.empty() && path_stack.back().first >= indent) {
125 path_stack.pop_back();
126 }
127
128 // Build full path
129 std::string full_path;
130 for (const auto& [_, segment] : path_stack) {
131 full_path += segment + ".";
132 }
133 full_path += key;
134
135 if (value.empty()) {
136 // This is a section header
137 path_stack.emplace_back(indent, key);
138 current_list_path = full_path;
139 } else {
140 // This is a value
141 value = strip_quotes(value);
142 values_[full_path] = value;
143 }
144 }
145 }
146
147 [[nodiscard]] static auto trim(std::string_view str) -> std::string {
148 auto start = str.find_first_not_of(" \t\r\n");
149 if (start == std::string::npos) return "";
150 auto end = str.find_last_not_of(" \t\r\n");
151 return std::string(str.substr(start, end - start + 1));
152 }
153
154 [[nodiscard]] static auto strip_quotes(std::string_view str) -> std::string {
155 if (str.length() >= 2) {
156 if ((str.front() == '"' && str.back() == '"') ||
157 (str.front() == '\'' && str.back() == '\'')) {
158 return std::string(str.substr(1, str.length() - 2));
159 }
160 }
161 return std::string(str);
162 }
163
164 yaml_map values_;
165 std::map<std::string, yaml_list> lists_;
166};
167
168} // namespace
169
170auto config_loader::load(const std::filesystem::path& path)
172 std::ifstream file(path);
173 if (!file.is_open()) {
175 kcenon::common::error_info("Failed to open configuration file: " + path.string()));
176 }
177
178 std::stringstream buffer;
179 buffer << file.rdbuf();
180 return load_from_string(buffer.str());
181}
182
183auto config_loader::load_from_string(std::string_view yaml_content)
185 try {
186 simple_yaml_parser parser(yaml_content);
187 production_config config = create_default();
188
189 // Server configuration
190 config.server.ae_title = parser.get_string("server.ae_title", config.server.ae_title);
191 config.server.port = static_cast<std::uint16_t>(
192 parser.get_int("server.port", config.server.port));
193 config.server.max_associations = static_cast<std::size_t>(
194 parser.get_int("server.max_associations",
195 static_cast<int>(config.server.max_associations)));
196 config.server.idle_timeout = std::chrono::seconds(
197 parser.get_int("server.idle_timeout_seconds",
198 static_cast<int>(config.server.idle_timeout.count())));
199
200 // TLS configuration
201 config.server.tls.enabled = parser.get_bool("server.tls.enabled",
202 config.server.tls.enabled);
203 config.server.tls.certificate = parser.get_string("server.tls.certificate");
204 config.server.tls.private_key = parser.get_string("server.tls.private_key");
205 config.server.tls.ca_certificate = parser.get_string("server.tls.ca_certificate");
207 parser.get_bool("server.tls.require_client_cert", false);
208 config.server.tls.min_version = parser.get_string("server.tls.min_version", "1.2");
209
210 // Storage configuration
211 config.storage.root_path = parser.get_string("storage.root_path",
212 config.storage.root_path.string());
213 config.storage.naming_scheme = parser.get_string("storage.naming_scheme",
214 config.storage.naming_scheme);
215 config.storage.duplicate_policy = parser.get_string("storage.duplicate_policy",
217 config.storage.database.path = parser.get_string("storage.database.path",
218 config.storage.database.path.string());
219
220 // Security configuration
222 parser.get_bool("security.access_control.enabled", true);
224 parser.get_string("security.access_control.default_role", "viewer");
226 parser.get_list("security.allowed_ae_titles");
228 parser.get_bool("security.anonymization.auto_anonymize", false);
229
230 auto profile_str = parser.get_string("security.anonymization.profile", "basic");
231 config.security.anonymization.profile = parse_anonymization_profile(profile_str);
232
233 // REST API configuration
234 config.rest_api.enabled = parser.get_bool("rest_api.enabled", true);
235 config.rest_api.port = static_cast<std::uint16_t>(
236 parser.get_int("rest_api.port", config.rest_api.port));
237 config.rest_api.cors_enabled = parser.get_bool("rest_api.cors_enabled", true);
238
239 // Monitoring configuration
240 config.monitoring.health_check_interval = std::chrono::seconds(
241 parser.get_int("monitoring.health_check_interval_seconds", 30));
242 config.monitoring.metrics_enabled = parser.get_bool("monitoring.metrics_enabled", true);
243
244 // Logging configuration
245 config.logging.level = parser.get_string("logging.level", "info");
246 config.logging.audit_log_path = parser.get_string("logging.audit_log_path");
247
248 // Validate configuration
249 auto validation = validate(config);
250 if (!validation.is_ok()) {
252 kcenon::common::error_info(validation.error().message));
253 }
254
255 return kcenon::common::Result<production_config>::ok(std::move(config));
256
257 } catch (const std::exception& e) {
259 kcenon::common::error_info(std::string("Configuration parsing error: ") + e.what()));
260 }
261}
262
264 production_config config;
265
266 // Server defaults
267 config.server.ae_title = "PROD_PACS";
268 config.server.port = 11112;
269 config.server.max_associations = 100;
270 config.server.idle_timeout = std::chrono::seconds{300};
271 config.server.tls.enabled = false;
272
273 // Storage defaults
274 config.storage.root_path = "./pacs_data";
275 config.storage.naming_scheme = "uid_hierarchical";
276 config.storage.duplicate_policy = "replace";
277 config.storage.database.path = "./pacs_data/index.db";
278
279 // Security defaults
280 config.security.access_control.enabled = true;
281 config.security.access_control.default_role = "viewer";
284
285 // REST API defaults
286 config.rest_api.enabled = true;
287 config.rest_api.port = 8080;
288 config.rest_api.cors_enabled = true;
289
290 // Monitoring defaults
291 config.monitoring.health_check_interval = std::chrono::seconds{30};
292 config.monitoring.metrics_enabled = true;
293
294 // Logging defaults
295 config.logging.level = "info";
296
297 return config;
298}
299
301 -> kcenon::common::VoidResult {
302 // Validate AE Title (max 16 characters, no leading/trailing spaces)
303 if (config.server.ae_title.empty()) {
304 return kcenon::common::VoidResult::err(
305 kcenon::common::error_info("AE Title cannot be empty"));
306 }
307 if (config.server.ae_title.length() > 16) {
308 return kcenon::common::VoidResult::err(
309 kcenon::common::error_info("AE Title exceeds maximum length of 16 characters"));
310 }
311
312 // Validate port
313 if (config.server.port == 0) {
314 return kcenon::common::VoidResult::err(
315 kcenon::common::error_info("Server port cannot be 0"));
316 }
317
318 // Validate REST API port
319 if (config.rest_api.enabled && config.rest_api.port == 0) {
320 return kcenon::common::VoidResult::err(
321 kcenon::common::error_info("REST API port cannot be 0"));
322 }
323
324 // Validate TLS configuration
325 if (config.server.tls.enabled) {
326 if (config.server.tls.certificate.empty()) {
327 return kcenon::common::VoidResult::err(
328 kcenon::common::error_info("TLS enabled but no certificate specified"));
329 }
330 if (config.server.tls.private_key.empty()) {
331 return kcenon::common::VoidResult::err(
332 kcenon::common::error_info("TLS enabled but no private key specified"));
333 }
334 }
335
336 // Validate naming scheme
337 const std::vector<std::string> valid_schemes = {
338 "uid_flat", "uid_hierarchical", "date_based"
339 };
340 auto scheme_it = std::find(valid_schemes.begin(), valid_schemes.end(),
341 config.storage.naming_scheme);
342 if (scheme_it == valid_schemes.end()) {
343 return kcenon::common::VoidResult::err(
344 kcenon::common::error_info("Invalid naming scheme: " + config.storage.naming_scheme));
345 }
346
347 // Validate duplicate policy
348 const std::vector<std::string> valid_policies = {"reject", "replace", "rename"};
349 auto policy_it = std::find(valid_policies.begin(), valid_policies.end(),
350 config.storage.duplicate_policy);
351 if (policy_it == valid_policies.end()) {
352 return kcenon::common::VoidResult::err(
353 kcenon::common::error_info("Invalid duplicate policy: " + config.storage.duplicate_policy));
354 }
355
356 return kcenon::common::VoidResult::ok(std::monostate{});
357}
358
359auto config_loader::parse_value(std::string_view value) -> std::string {
360 return trim(value);
361}
362
363auto config_loader::parse_bool(std::string_view value) -> bool {
364 auto v = trim(value);
365 std::transform(v.begin(), v.end(), v.begin(), ::tolower);
366 return v == "true" || v == "yes" || v == "on" || v == "1";
367}
368
369auto config_loader::parse_int(std::string_view value) -> int {
370 auto v = trim(value);
371 try {
372 return std::stoi(v);
373 } catch (...) {
374 return 0;
375 }
376}
377
380 auto v = trim(value);
381 std::transform(v.begin(), v.end(), v.begin(), ::tolower);
382
383 if (v == "basic" || v == "basic_profile") {
385 }
386 if (v == "hipaa" || v == "hipaa_safe_harbor") {
388 }
389 if (v == "retain_longitudinal" || v == "retain_longitudinal_full_dates") {
391 }
392 if (v == "retain_device" || v == "retain_device_identity" ||
393 v == "retain_patient" || v == "retain_patient_characteristics") {
395 }
396 if (v == "gdpr") {
398 }
399
400 // Default to basic
402}
403
404auto config_loader::trim(std::string_view str) -> std::string {
405 auto start = str.find_first_not_of(" \t\r\n");
406 if (start == std::string::npos) return "";
407 auto end = str.find_last_not_of(" \t\r\n");
408 return std::string(str.substr(start, end - start + 1));
409}
410
411} // namespace kcenon::pacs::samples
static auto create_default() -> production_config
Create a default configuration.
static auto parse_int(std::string_view value) -> int
Parse an integer value.
static auto load(const std::filesystem::path &path) -> kcenon::common::Result< production_config >
Load configuration from a YAML file.
static auto parse_anonymization_profile(std::string_view value) -> security::anonymization_profile
Parse anonymization profile.
static auto parse_bool(std::string_view value) -> bool
Parse a boolean value.
static auto validate(const production_config &config) -> kcenon::common::VoidResult
Validate a configuration.
static auto parse_value(std::string_view value) -> std::string
Parse a YAML value (simple types)
static auto load_from_string(std::string_view yaml_content) -> kcenon::common::Result< production_config >
Load configuration from a YAML string.
static auto trim(std::string_view str) -> std::string
Trim whitespace from string.
yaml_map values_
std::map< std::string, yaml_list > lists_
YAML configuration loader for Production PACS.
std::string buffer
constexpr dicom_tag item
Item.
anonymization_profile
DICOM de-identification profiles based on PS3.15 Annex E.
@ hipaa_safe_harbor
HIPAA Safe Harbor - 18 identifier removal.
@ gdpr_compliant
GDPR Compliant - European data protection.
@ retain_longitudinal
Retain Longitudinal - Preserve temporal relationships.
@ retain_patient_characteristics
Retain Patient Characteristics.
@ basic
Basic Profile - Remove direct identifiers.
std::string default_role
Default role for unknown AE titles.
bool enabled
Enable access control enforcement.
security::anonymization_profile profile
Anonymization profile to use.
bool auto_anonymize
Automatically anonymize incoming images.
std::filesystem::path path
Path to SQLite database file.
std::filesystem::path audit_log_path
Path to audit log file.
std::string level
Log level: "debug", "info", "warn", "error".
std::chrono::seconds health_check_interval
Interval between health checks (seconds)
bool metrics_enabled
Enable Prometheus-style metrics endpoint.
Complete production PACS configuration.
monitoring_config monitoring
Monitoring settings.
logging_config logging
Logging settings.
storage_config storage
Storage settings.
rest_api_config rest_api
REST API settings.
security_config security
Security settings.
server_config server
DICOM server settings.
std::uint16_t port
HTTP port for REST API.
bool cors_enabled
Enable CORS for web clients.
bool enabled
Enable REST API server.
std::vector< std::string > allowed_ae_titles
Allowed AE titles (supports wildcards with *)
access_control_config access_control
Access control settings.
anonymization_config anonymization
Anonymization settings.
std::uint16_t port
TCP port for DICOM connections.
tls_config tls
TLS configuration.
std::size_t max_associations
Maximum concurrent associations.
std::chrono::seconds idle_timeout
Idle timeout for associations (seconds)
std::string ae_title
Application Entity title (max 16 characters)
std::string duplicate_policy
Duplicate handling: "reject", "replace", "rename".
std::string naming_scheme
Naming scheme: "uid_flat", "uid_hierarchical", "date_based".
database_config database
Database configuration.
std::filesystem::path root_path
Root path for DICOM file storage.
std::filesystem::path ca_certificate
Path to CA certificate file (PEM format)
std::string min_version
Minimum TLS version ("1.2" or "1.3")
std::filesystem::path certificate
Path to server certificate file (PEM format)
bool require_client_cert
Require client certificate (mutual TLS)
std::filesystem::path private_key
Path to private key file (PEM format)
bool enabled
Enable TLS for DICOM connections.