33inline constexpr core::dicom_tag institutional_department_name{0x0008, 0x1040};
43[[nodiscard]] std::string to_lower(std::string_view str) {
45 result.reserve(str.size());
47 result +=
static_cast<char>(std::tolower(
static_cast<unsigned char>(c)));
59 std::shared_ptr<storage::routing_repository> repo,
60 std::shared_ptr<job_manager> job_mgr,
61 std::shared_ptr<di::ILogger> logger)
63 , repo_(std::move(repo))
64 , job_manager_(std::move(job_mgr))
65 , logger_(logger ? std::move(logger) : di::null_logger()) {
70 logger_->info_fmt(
"routing_manager: Initialized with {} rules",
rules_.size());
76 std::shared_ptr<storage::routing_repository> repo,
77 std::shared_ptr<job_manager> job_mgr,
78 std::shared_ptr<di::ILogger> logger)
80 , repo_(std::move(repo))
81 , job_manager_(std::move(job_mgr))
82 , logger_(logger ? std::move(logger) : di::null_logger()) {
88 logger_->info_fmt(
"routing_manager: Initialized with {} rules (enabled={})",
115 auto result =
repo_->save(rule);
116 if (result.is_err()) {
119 "Failed to save rule: " + result.error().message);
130 return a.priority > b.priority;
138 return kcenon::pacs::ok();
143 auto result =
repo_->save(rule);
144 if (result.is_err()) {
147 "Failed to update rule: " + result.error().message);
155 return r.rule_id == rule.rule_id;
166 return a.priority > b.priority;
174 return kcenon::pacs::ok();
179 auto result =
repo_->remove(std::string(rule_id));
180 if (!result.is_ok()) {
190 return r.rule_id == rule_id;
196 logger_->info_fmt(
"routing_manager: Removed rule: {}", rule_id);
199 return kcenon::pacs::ok();
206 return r.rule_id == rule_id;
221 std::vector<routing_rule> result;
222 std::copy_if(
rules_.begin(),
rules_.end(), std::back_inserter(result),
232 auto result =
repo_->update_priority(rule_id, priority);
233 if (!result.is_ok()) {
241 return r.rule_id == rule_id;
244 it->priority = priority;
250 return a.priority > b.priority;
254 return kcenon::pacs::ok();
261 int priority =
static_cast<int>(rule_ids.size());
262 for (
const auto& rule_id : rule_ids) {
263 auto repo_result =
repo_->update_priority(rule_id, priority);
264 if (!repo_result.is_ok()) {
270 return r.rule_id == rule_id;
273 it->priority = priority;
281 return a.priority > b.priority;
284 return kcenon::pacs::ok();
292 std::vector<routing_action> result;
302 for (
const auto& rule :
rules_) {
303 if (!rule.is_effective_now()) {
308 bool all_match = !rule.conditions.empty();
309 for (
const auto& condition : rule.conditions) {
320 for (
const auto& action : rule.actions) {
321 result.push_back(action);
333std::vector<std::pair<std::string, std::vector<routing_action>>>
335 std::vector<std::pair<std::string, std::vector<routing_action>>> result;
345 for (
const auto& rule :
rules_) {
346 if (!rule.is_effective_now()) {
350 bool all_match = !rule.conditions.empty();
351 for (
const auto& condition : rule.conditions) {
360 result.emplace_back(rule.rule_id, rule.actions);
377 if (sop_instance_uid.empty()) {
379 logger_->warn(
"routing_manager: Cannot route dataset without SOP Instance UID");
386 for (
const auto& [rule_id, actions] : matches) {
388 auto stat_result =
repo_->increment_triggered(rule_id);
389 if (!stat_result.is_ok() &&
logger_) {
390 logger_->warn_fmt(
"routing_manager: Failed to update statistics for rule: {}",
412 logger_->warn_fmt(
"routing_manager: route(sop_instance_uid) not fully implemented - "
413 "use route(dataset) instead. UID: {}", sop_instance_uid);
424 logger_->info(
"routing_manager: Routing enabled");
431 logger_->info(
"routing_manager: Routing disabled");
454 const std::string& ) {
455 this->
route(dataset);
459 logger_->info(
"routing_manager: Attached to Storage SCP");
469 logger_->info(
"routing_manager: Detached from Storage SCP");
491 for (
const auto& rule :
rules_) {
492 if (!rule.is_effective_now()) {
496 bool all_match = !rule.conditions.empty();
497 for (
const auto& condition : rule.conditions) {
531#ifdef PACS_WITH_DATABASE_SYSTEM
532 auto rule_result =
repo_->find_by_id(std::string(rule_id));
533 if (rule_result.is_ok()) {
534 const auto& rule = rule_result.value();
535 stats.total_evaluated = 0;
536 stats.total_matched = rule.triggered_count;
537 stats.total_forwarded = rule.success_count;
538 stats.total_failed = rule.failure_count;
541 auto rule =
repo_->find_by_id(rule_id);
543 stats.total_evaluated = 0;
544 stats.total_matched = rule->triggered_count;
545 stats.total_forwarded = rule->success_count;
546 stats.total_failed = rule->failure_count;
561 for (
const auto& rule :
rules_) {
562 auto result =
repo_->reset_statistics(rule.rule_id);
563 if (!result.is_ok() &&
logger_) {
564 logger_->warn_fmt(
"routing_manager: Failed to reset statistics for rule: {}",
596 std::string_view value,
597 bool case_sensitive)
const {
599 std::string pat_str = case_sensitive ? std::string(pattern) : to_lower(pattern);
600 std::string val_str = case_sensitive ? std::string(value) : to_lower(value);
606 const char* p = pat_str.c_str();
607 const char* v = val_str.c_str();
609 const char* star_p =
nullptr;
610 const char* star_v =
nullptr;
616 }
else if (*p ==
'?' || *p == *v) {
647 return dataset.
get_string(institutional_department_name);
659 return dataset.
get_string(body_part_examined);
673 const std::vector<routing_action>& actions) {
674 for (
const auto& action : actions) {
675 if (action.destination_node_id.empty()) {
677 logger_->warn_fmt(
"routing_manager: Skipping action with empty destination for UID: {}",
684 std::vector<std::string> instance_uids{sop_instance_uid};
687 action.destination_node_id,
694 logger_->info_fmt(
"routing_manager: Created forward job {} for UID {} -> {}",
695 job_id, sop_instance_uid, action.destination_node_id);
700 if (action.delay.count() > 0 &&
logger_) {
701 logger_->debug_fmt(
"routing_manager: Delayed forwarding ({} min) not yet implemented",
702 action.delay.count());
708#ifdef PACS_WITH_DATABASE_SYSTEM
709 auto loaded_result =
repo_->find_enabled_rules();
710 if (loaded_result.is_err()) {
712 logger_->warn_fmt(
"routing_manager: Failed to load rules: {}",
713 loaded_result.error().message);
719 rules_ = std::move(loaded_result.value());
721 auto loaded =
repo_->find_enabled_rules();
724 rules_ = std::move(loaded);
730 return a.priority > b.priority;
void attach_to_storage_scp(services::storage_scp &scp)
Attach to a Storage SCP for automatic routing.
void set_routing_callback(routing_event_callback callback)
Set callback for routing events.
std::atomic< size_t > total_forwarded_
~routing_manager()
Destructor.
std::atomic< size_t > total_failed_
auto test_rules(const core::dicom_dataset &dataset) const -> routing_test_result
Test rules against a dataset without executing actions.
auto get_field_value(routing_field field, const core::dicom_dataset &dataset) const -> std::string
Get DICOM field value from dataset.
auto get_statistics() const -> routing_statistics
Get overall routing statistics.
auto get_rule(std::string_view rule_id) const -> std::optional< routing_rule >
Get a routing rule by ID.
auto remove_rule(std::string_view rule_id) -> kcenon::pacs::VoidResult
Remove a routing rule.
std::shared_mutex rules_mutex_
auto match_pattern(std::string_view pattern, std::string_view value, bool case_sensitive) const -> bool
Match a wildcard pattern against a value.
void enable()
Enable routing globally.
auto get_rule_statistics(std::string_view rule_id) const -> routing_statistics
Get statistics for a specific rule.
routing_manager(std::shared_ptr< storage::routing_repository > repo, std::shared_ptr< job_manager > job_manager, std::shared_ptr< di::ILogger > logger=nullptr)
Construct a routing manager with default configuration.
auto evaluate(const core::dicom_dataset &dataset) -> std::vector< routing_action >
Evaluate rules against a dataset.
auto set_rule_priority(std::string_view rule_id, int priority) -> kcenon::pacs::VoidResult
Set the priority of a rule.
routing_event_callback routing_callback_
auto config() const noexcept -> const routing_manager_config &
Get current configuration.
void route(const core::dicom_dataset &dataset)
Route a DICOM dataset based on matching rules.
std::shared_ptr< storage::routing_repository > repo_
auto reorder_rules(const std::vector< std::string > &rule_ids) -> kcenon::pacs::VoidResult
Reorder rules by specifying the desired order.
void load_rules()
Load rules from repository into cache.
std::shared_ptr< di::ILogger > logger_
std::atomic< bool > enabled_
services::storage_scp * attached_scp_
std::shared_ptr< job_manager > job_manager_
void reset_statistics()
Reset all statistics.
auto match_condition(const routing_condition &condition, const core::dicom_dataset &dataset) const -> bool
Check if a condition matches a dataset.
auto is_enabled() const noexcept -> bool
Check if routing is enabled.
std::atomic< size_t > total_matched_
void disable()
Disable routing globally.
auto list_rules() const -> std::vector< routing_rule >
List all routing rules.
routing_manager_config config_
std::atomic< size_t > total_evaluated_
auto add_rule(const routing_rule &rule) -> kcenon::pacs::VoidResult
Add a new routing rule.
void detach_from_storage_scp()
Detach from the currently attached Storage SCP.
auto evaluate_with_rule_ids(const core::dicom_dataset &dataset) -> std::vector< std::pair< std::string, std::vector< routing_action > > >
Evaluate rules and return with matched rule IDs.
auto update_rule(const routing_rule &rule) -> kcenon::pacs::VoidResult
Update an existing routing rule.
auto list_enabled_rules() const -> std::vector< routing_rule >
List only enabled routing rules.
std::vector< routing_rule > rules_
void execute_actions(const std::string &sop_instance_uid, const std::vector< routing_action > &actions)
Execute routing actions.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
void set_post_store_handler(post_store_handler handler)
DICOM Dataset - ordered collection of Data Elements.
DICOM Tag representation (Group, Element pairs)
Compile-time constants for commonly used DICOM tags.
Job manager for asynchronous DICOM operations.
routing_field
DICOM field to match in routing conditions.
@ series_description
(0008,103E) Series Description
@ department
(0008,1040) Institutional Department Name
@ body_part
(0018,0015) Body Part Examined
@ study_description
(0008,1030) Study Description
@ modality
(0008,0060) Modality - CT, MR, US, etc.
@ sop_class_uid
(0008,0016) SOP Class UID
@ institution
(0008,0080) Institution Name
@ station_ae
(0008,1010) Station Name or calling AE
@ patient_id_pattern
(0010,0020) Patient ID (pattern matching)
@ referring_physician
(0008,0090) Referring Physician's Name
std::function< void( const std::string &rule_id, const std::string &instance_uid, const std::vector< routing_action > &triggered_actions)> routing_event_callback
Callback type for routing events.
VoidResult pacs_void_error(int code, const std::string &message, const std::string &details="")
Create a PACS void error result.
Routing manager for automatic DICOM image forwarding.
Repository for routing rule persistence using base_repository pattern.
DICOM Storage SCP service (C-STORE handler)
A single condition for routing rule evaluation.
std::string pattern
Pattern to match (supports wildcards: *, ?)
routing_field match_field
The DICOM field to match.
bool negate
Invert the match result.
bool case_sensitive
Whether matching is case-sensitive.
Configuration for the routing manager.
size_t max_rules
Maximum number of rules.
bool enabled
Enable routing globally.
A complete routing rule with conditions and actions.
std::string name
Human-readable name.
std::string rule_id
Unique rule identifier.
Statistics for routing operations.
Result of testing rules against a dataset (dry run)
std::string matched_rule_id
ID of the matched rule.
bool matched
Whether any rule matched.
std::vector< routing_action > actions
Actions that would execute.