PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
query_scp.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
11
18
19#include <kcenon/common/patterns/event_bus.h>
20
21namespace kcenon::pacs::services {
22
23namespace {
24// Convert services::query_level to events::query_level
25kcenon::pacs::events::query_level to_event_level(query_level level) {
26 switch (level) {
32 }
33}
34} // namespace
35
36// =============================================================================
37// Construction
38// =============================================================================
39
40query_scp::query_scp(std::shared_ptr<di::ILogger> logger)
41 : scp_service(std::move(logger)) {}
42
43// =============================================================================
44// Configuration
45// =============================================================================
46
48 handler_ = std::move(handler);
49}
50
51void query_scp::set_max_results(size_t max) noexcept {
52 max_results_ = max;
53}
54
55size_t query_scp::max_results() const noexcept {
56 return max_results_;
57}
58
60 cancel_check_ = std::move(check);
61}
62
64 std::shared_ptr<kcenon::pacs::security::atna_service_auditor> auditor) {
65 auditor_ = std::move(auditor);
66}
67
68// =============================================================================
69// scp_service Interface Implementation
70// =============================================================================
71
72std::vector<std::string> query_scp::supported_sop_classes() const {
73 return {
76 };
77}
78
81 uint8_t context_id,
82 const network::dimse::dimse_message& request) {
83
84 using namespace network::dimse;
85 auto start_time = std::chrono::steady_clock::now();
86 std::string calling_ae{assoc.calling_ae()};
87
88 // Verify the message is a C-FIND request
89 if (request.command() != command_field::c_find_rq) {
92 "Expected C-FIND-RQ but received " +
93 std::string(to_string(request.command())));
94 }
95
96 // Verify we have a handler
97 if (!handler_) {
98 kcenon::common::get_event_bus().publish(
100 calling_ae,
102 "No query handler configured"
103 }
104 );
107 "No query handler configured");
108 }
109
110 // Get the SOP Class UID from the request
111 auto sop_class_uid = request.affected_sop_class_uid();
112
113 // Verify we have a dataset
114 if (!request.has_dataset()) {
115 return send_final_response(
116 assoc, context_id, request.message_id(),
117 sop_class_uid, status_error_cannot_understand);
118 }
119
120 // Extract query level
121 const auto& query_keys = request.dataset().value().get();
122 auto level = extract_query_level(query_keys);
123
124 if (!level.has_value()) {
125 // Invalid or missing query level
126 return send_final_response(
127 assoc, context_id, request.message_id(),
128 sop_class_uid, status_error_cannot_understand);
129 }
130
131 // Call the handler to get matching results
132 auto results = handler_(level.value(), query_keys, calling_ae);
133
134 // Apply max results limit if configured
135 if (max_results_ > 0 && results.size() > max_results_) {
136 results.resize(max_results_);
137 }
138
139 // Send pending responses for each result
140 for (const auto& result : results) {
141 // Check for cancel request
142 if (cancel_check_ && cancel_check_()) {
143 // Increment statistics
145
146 // Send cancel response
147 return send_final_response(
148 assoc, context_id, request.message_id(),
149 sop_class_uid, status_cancel);
150 }
151
152 // Send pending response with matching dataset
153 auto send_result = send_pending_response(
154 assoc, context_id, request.message_id(),
155 sop_class_uid, result);
156
157 if (send_result.is_err()) {
158 return send_result;
159 }
160 }
161
162 // Increment statistics
164
165 // Calculate execution time
166 auto end_time = std::chrono::steady_clock::now();
167 auto execution_time_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
168 end_time - start_time).count();
169
170 // Publish query executed event
171 kcenon::common::get_event_bus().publish(
173 to_event_level(level.value()),
174 calling_ae,
175 results.size(),
176 static_cast<uint64_t>(execution_time_ms)
177 }
178 );
179
180 // Emit ATNA audit event
181 if (auditor_) {
183 calling_ae,
184 std::string(assoc.called_ae()),
185 std::string(to_string(level.value())),
186 true);
187 }
188
189 // Send final success response (no dataset)
190 return send_final_response(
191 assoc, context_id, request.message_id(),
192 sop_class_uid, status_success);
193}
194
195std::string_view query_scp::service_name() const noexcept {
196 return "Query SCP";
197}
198
199// =============================================================================
200// Statistics
201// =============================================================================
202
203size_t query_scp::queries_processed() const noexcept {
204 return queries_processed_.load();
205}
206
209}
210
211// =============================================================================
212// Private Implementation
213// =============================================================================
214
215std::optional<query_level> query_scp::extract_query_level(
216 const core::dicom_dataset& dataset) const {
217
218 // Get Query/Retrieve Level tag (0008,0052)
219 auto level_str = dataset.get_string(core::tags::query_retrieve_level);
220
221 if (level_str.empty()) {
222 return std::nullopt;
223 }
224
225 return parse_query_level(level_str);
226}
227
230 uint8_t context_id,
231 uint16_t message_id,
232 std::string_view sop_class_uid,
233 const core::dicom_dataset& result) {
234
235 using namespace network::dimse;
236
237 // Create C-FIND response with pending status
238 auto response = make_c_find_rsp(
239 message_id,
240 sop_class_uid,
241 status_pending
242 );
243
244 // Attach the matching dataset
245 response.set_dataset(result);
246
247 // Send the response
248 return assoc.send_dimse(context_id, response);
249}
250
253 uint8_t context_id,
254 uint16_t message_id,
255 std::string_view sop_class_uid,
257
258 using namespace network::dimse;
259
260 // Create C-FIND response with final status (no dataset)
261 auto response = make_c_find_rsp(
262 message_id,
263 sop_class_uid,
264 status
265 );
266
267 // Send the response
268 return assoc.send_dimse(context_id, response);
269}
270
271} // namespace kcenon::pacs::services
High-level facade for ATNA audit logging in DICOM services.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
Result< std::monostate > send_dimse(uint8_t context_id, const dimse::dimse_message &msg)
Send a DIMSE message.
std::string_view calling_ae() const noexcept
Get calling AE title.
std::string_view called_ae() const noexcept
Get called AE title.
auto message_id() const noexcept -> uint16_t
Get the message ID.
auto affected_sop_class_uid() const -> std::string
Get the Affected SOP Class UID.
auto has_dataset() const noexcept -> bool
Check if the message has an associated data set.
auto dataset() -> kcenon::pacs::Result< std::reference_wrapper< core::dicom_dataset > >
Get mutable reference to the data set.
auto command() const noexcept -> command_field
Get the command field.
void audit_query(const std::string &calling_ae, const std::string &called_ae, const std::string &query_level, bool success)
Audit a C-FIND (Query) event.
std::vector< std::string > supported_sop_classes() const override
Get supported SOP Class UIDs.
Definition query_scp.cpp:72
void set_audit_handler(std::shared_ptr< kcenon::pacs::security::atna_service_auditor > auditor)
Set the ATNA audit handler for C-FIND operations.
Definition query_scp.cpp:63
std::optional< query_level > extract_query_level(const core::dicom_dataset &dataset) const
Extract query level from request dataset.
network::Result< std::monostate > send_pending_response(network::association &assoc, uint8_t context_id, uint16_t message_id, std::string_view sop_class_uid, const core::dicom_dataset &result)
Send a pending C-FIND response with matching dataset.
size_t max_results() const noexcept
Get maximum number of results.
Definition query_scp.cpp:55
void set_cancel_check(cancel_check check)
Set the cancel check function.
Definition query_scp.cpp:59
void reset_statistics() noexcept
Reset statistics counters.
std::shared_ptr< kcenon::pacs::security::atna_service_auditor > auditor_
Definition query_scp.h:356
std::atomic< size_t > queries_processed_
Definition query_scp.h:358
void set_handler(query_handler handler)
Set the query handler function.
Definition query_scp.cpp:47
std::string_view service_name() const noexcept override
Get the service name.
network::Result< std::monostate > handle_message(network::association &assoc, uint8_t context_id, const network::dimse::dimse_message &request) override
Handle an incoming DIMSE message (C-FIND-RQ)
Definition query_scp.cpp:79
network::Result< std::monostate > send_final_response(network::association &assoc, uint8_t context_id, uint16_t message_id, std::string_view sop_class_uid, network::dimse::status_code status)
Send the final C-FIND response (success or cancel)
void set_max_results(size_t max) noexcept
Set maximum number of results to return.
Definition query_scp.cpp:51
size_t queries_processed() const noexcept
Get total number of queries processed.
query_scp(std::shared_ptr< di::ILogger > logger=nullptr)
Construct Query SCP with optional logger.
Definition query_scp.cpp:40
DIMSE command field enumeration.
Compile-time constants for commonly used DICOM tags.
DICOM event definitions for event-based communication.
constexpr dicom_tag query_retrieve_level
Query/Retrieve Level.
constexpr int find_handler_not_set
Definition result.h:154
constexpr int find_unexpected_command
Definition result.h:157
query_level
Query level enumeration.
Definition events.h:166
uint16_t status_code
DIMSE status code type alias.
constexpr std::string_view study_root_find_sop_class_uid
Study Root Query/Retrieve Information Model - FIND.
Definition query_scp.h:42
constexpr std::string_view patient_root_find_sop_class_uid
Patient Root Query/Retrieve Information Model - FIND.
Definition query_scp.h:38
std::function< std::vector< core::dicom_dataset >( query_level level, const core::dicom_dataset &query_keys, const std::string &calling_ae)> query_handler
Query handler function type.
Definition query_scp.h:113
std::optional< query_level > parse_query_level(std::string_view level_str) noexcept
Parse query level from DICOM string.
Definition query_scp.h:90
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
Definition mpps_scp.h:60
std::function< bool()> cancel_check
Cancel check function type.
Definition query_scp.h:126
@ study
Study level - query study information.
@ image
Image (Instance) level - query instance information.
@ patient
Patient level - query patient demographics.
@ series
Series level - query series information.
VoidResult pacs_void_error(int code, const std::string &message, const std::string &details="")
Create a PACS void error result.
Definition result.h:249
DICOM Query SCP service (C-FIND handler)
Result<T> type aliases and helpers for PACS system.
DIMSE status codes.
Event published when a C-FIND query is executed.
Definition events.h:189
Event published when a C-FIND query fails.
Definition events.h:210