PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
query_response_parser.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
12#include <pugixml.hpp>
13
14#include <cstdlib>
15#include <string>
16#include <string_view>
17
19
20namespace {
21
22// Response-size cap shared with the retrieve parser. The http_client write
23// callback already bounds the body to 8 MiB; this is a defensive second
24// gate in case a future non-libcurl transport is wired in without the
25// same write limit.
26constexpr std::size_t kMaxRootXmlBytes = 8 * 1024 * 1024;
27
28constexpr std::string_view kSuccessStatus =
29 "urn:oasis:names:tc:ebxml-regrep:ResponseStatusType:Success";
30constexpr std::string_view kPartialSuccessStatus =
31 "urn:oasis:names:tc:ebxml-regrep:ResponseStatusType:PartialSuccess";
32
33// Node accessor for the nameAttribute-scoped XDS classification codes.
34// ebRIM encodes class, format, type, and content-type codes as
35// Classification children of the ExtrinsicObject, each tagged by a
36// classificationScheme UUID. The registry-side UUIDs are fixed by the
37// IHE ITI TF-3 ยง4.1.5 metadata attribute table - any drift means the
38// classification applies to a different attribute than the caller
39// expects, which would silently populate the wrong field.
40constexpr std::string_view kSchemeClassCode =
41 "urn:uuid:41a5887f-8865-4c09-adf7-e362475b143a";
42constexpr std::string_view kSchemeFormatCode =
43 "urn:uuid:a09d5840-386c-46f2-b5ad-9c3699a4309d";
44constexpr std::string_view kSchemeTypeCode =
45 "urn:uuid:f0306f51-975f-434e-a61b-a3f66f70e3dc";
46
47// External-identifier scheme UUIDs for patient id and XDSDocumentEntry
48// unique id on ExtrinsicObject, from IHE ITI TF-3 ยง4.1.5 metadata table.
49constexpr std::string_view kExtIdPatientId =
50 "urn:uuid:58a6f841-87b3-4a3e-92fd-a8ffeff98427";
51constexpr std::string_view kExtIdUniqueId =
52 "urn:uuid:2e82c1f6-a085-4c72-9da3-8640a32e42ab";
53
54std::string get_slot_value(pugi::xml_node extrinsic, const char* slot_name) {
55 for (auto slot : extrinsic.select_nodes("*[local-name()='Slot']")) {
56 auto node = slot.node();
57 if (std::string_view(node.attribute("name").as_string()) ==
58 slot_name) {
59 auto value = node.select_node(
60 "*[local-name()='ValueList']/"
61 "*[local-name()='Value']").node();
62 return value.text().as_string();
63 }
64 }
65 return {};
66}
67
68std::string get_classification_code(pugi::xml_node extrinsic,
69 std::string_view scheme) {
70 for (auto c : extrinsic.select_nodes("*[local-name()='Classification']")) {
71 auto node = c.node();
72 if (std::string_view(
73 node.attribute("classificationScheme").as_string()) ==
74 scheme) {
75 return node.attribute("nodeRepresentation").as_string();
76 }
77 }
78 return {};
79}
80
81std::string get_external_identifier(pugi::xml_node extrinsic,
82 std::string_view scheme) {
83 for (auto ei : extrinsic.select_nodes(
84 "*[local-name()='ExternalIdentifier']")) {
85 auto node = ei.node();
86 if (std::string_view(
87 node.attribute("identificationScheme").as_string()) ==
88 scheme) {
89 return node.attribute("value").as_string();
90 }
91 }
92 return {};
93}
94
95std::string get_localized_name(pugi::xml_node extrinsic) {
96 // XDS Name is an i18n container: <Name><LocalizedString value="..."/></Name>.
97 // Pick the first LocalizedString regardless of xml:lang - callers that
98 // need a specific locale can re-walk the tree, but the common case is
99 // "whatever the registry emitted".
100 auto ls = extrinsic.select_node(
101 "*[local-name()='Name']/"
102 "*[local-name()='LocalizedString']").node();
103 return ls.attribute("value").as_string();
104}
105
106std::uint64_t parse_size_slot(pugi::xml_node extrinsic) {
107 const std::string s = get_slot_value(extrinsic, "size");
108 if (s.empty()) return 0;
109 // Guard against malformed slot values: strtoull returns 0 on failure
110 // which collides with the "omitted" sentinel but is the safer pick.
111 // Callers that require strict size reporting should re-parse the
112 // raw envelope themselves.
113 char* end = nullptr;
114 const auto v = std::strtoull(s.c_str(), &end, 10);
115 if (end == s.c_str()) return 0;
116 return static_cast<std::uint64_t>(v);
117}
118
119registry_document_entry extract_entry(pugi::xml_node extrinsic) {
120 registry_document_entry e;
121 e.entry_uuid = extrinsic.attribute("id").as_string();
122 e.mime_type = extrinsic.attribute("mimeType").as_string();
123 e.status = extrinsic.attribute("status").as_string();
124 e.home_community_id = extrinsic.attribute("home").as_string();
125
126 e.unique_id = get_external_identifier(extrinsic, kExtIdUniqueId);
127 e.patient_id = get_external_identifier(extrinsic, kExtIdPatientId);
128
129 e.creation_time = get_slot_value(extrinsic, "creationTime");
130 e.repository_unique_id =
131 get_slot_value(extrinsic, "repositoryUniqueId");
132 e.hash = get_slot_value(extrinsic, "hash");
133 e.author_person = get_slot_value(extrinsic, "authorPerson");
134 e.size_bytes = parse_size_slot(extrinsic);
135
136 e.title = get_localized_name(extrinsic);
137 e.class_code = get_classification_code(extrinsic, kSchemeClassCode);
138 e.format_code = get_classification_code(extrinsic, kSchemeFormatCode);
139 e.type_code = get_classification_code(extrinsic, kSchemeTypeCode);
140
141 return e;
142}
143
144} // namespace
145
147 const std::string& root_xml) {
148 if (root_xml.size() > kMaxRootXmlBytes) {
149 return kcenon::common::make_error<registry_query_result>(
151 "AdhocQueryResponse root part exceeds 8 MiB cap (" +
152 std::to_string(root_xml.size()) + " bytes)",
153 std::string(error_source));
154 }
155
156 pugi::xml_document doc;
157 auto parse = doc.load_buffer(root_xml.data(), root_xml.size(),
158 pugi::parse_default, pugi::encoding_utf8);
159 if (!parse) {
160 return kcenon::common::make_error<registry_query_result>(
162 std::string("failed to parse response envelope: ") +
163 parse.description(),
164 std::string(error_source));
165 }
166
167 // Use local-name() selectors so we do not depend on the specific
168 // namespace prefix the registry emits. ebRS stacks (Apache CXF,
169 // .NET WIF, Gazelle, Open Health Tools) pick different prefixes for
170 // the query and rim namespaces.
171 auto adhoc_response =
172 doc.select_node(
173 "//*[local-name()='AdhocQueryResponse']").node();
174 if (!adhoc_response) {
175 return kcenon::common::make_error<registry_query_result>(
177 "response contains no AdhocQueryResponse element",
178 std::string(error_source));
179 }
180
181 const std::string status =
182 adhoc_response.attribute("status").as_string();
183 if (status.empty()) {
184 return kcenon::common::make_error<registry_query_result>(
186 "AdhocQueryResponse is missing status attribute",
187 std::string(error_source));
188 }
189 // PartialSuccess means "some entries returned with warnings" in
190 // IHE ITI-18 - the registry has produced a valid RegistryObjectList
191 // alongside a RegistryErrorList. An earlier draft returned
192 // registry_partial_success as an error, which dropped the entries
193 // and contradicted the public-header contract that callers branch on
194 // entries.empty() and inspect registry_response_status. Treat
195 // PartialSuccess as a non-fatal status: fall through to the
196 // ExtrinsicObject walk and surface the raw status string in the
197 // returned result so callers can distinguish Success from
198 // PartialSuccess if their policy differs (review M2).
199 if (status != kSuccessStatus && status != kPartialSuccessStatus) {
200 return kcenon::common::make_error<registry_query_result>(
202 "registry reported non-success status: " + status,
203 std::string(error_source));
204 }
205
207 out.registry_response_status = status;
208
209 // Narrow child xpath (review m6): match ExtrinsicObject children of
210 // RegistryObjectList only, not arbitrary descendants. This avoids
211 // picking up nested ExtrinsicObject elements that could appear inside
212 // a RegistryError's detail payload on non-conformant responses.
213 // The RegistryObjectList is optional when no entries match; a Success
214 // response with zero results is valid and maps to out.entries.empty().
215 for (auto node : adhoc_response.select_nodes(
216 "*[local-name()='RegistryObjectList']/"
217 "*[local-name()='ExtrinsicObject']")) {
218 out.entries.push_back(extract_entry(node.node()));
219 }
220
221 return out;
222}
223
224} // namespace kcenon::pacs::ihe::xds::detail
kcenon::common::Result< registry_query_result > parse_query_response(const std::string &root_xml)
Parse an AdhocQueryResponse envelope into a registry_query_result.
constexpr std::string_view error_source
Short identifier used as the "source" field of make_error.
Definition errors.h:95
Parser for IHE XDS.b ITI-18 AdhocQueryResponse.
std::string out
ITI-18 RegistryStoredQueryResponse payload.