25constexpr const char* kSoapEnv =
"http://www.w3.org/2003/05/soap-envelope";
26constexpr const char* kWsa =
"http://www.w3.org/2005/08/addressing";
27constexpr const char* kWsse =
28 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
29constexpr const char* kWsu =
30 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
31constexpr const char* kLcm =
32 "urn:oasis:names:tc:ebxml-regrep:xsd:lcm:3.0";
33constexpr const char* kRim =
34 "urn:oasis:names:tc:ebxml-regrep:xsd:rim:3.0";
35constexpr const char* kXop =
"http://www.w3.org/2004/08/xop/include";
37constexpr const char* kSubmissionSetClassification =
38 "urn:uuid:a54d6aa5-d40d-43f9-88c5-b4633d873bdd";
39constexpr const char* kSubmissionSetIdScheme =
40 "urn:uuid:554ac39e-e3fe-47fe-b233-965d2a147832";
41constexpr const char* kDocumentEntryIdScheme =
42 "urn:uuid:2e82c1f6-a085-4c72-9da3-8640a32e42ab";
51std::string make_xml_id(std::string_view prefix) {
52 static thread_local std::mt19937_64 rng(
53 static_cast<std::uint64_t
>(
54 std::chrono::steady_clock::now().time_since_epoch().count()));
55 std::uniform_int_distribution<std::uint64_t> dist;
56 std::ostringstream oss;
57 oss << prefix <<
'-' << std::hex << dist(rng) << dist(rng);
64std::string utc_iso8601_now() {
65 const auto now = std::chrono::system_clock::now();
66 const auto tt = std::chrono::system_clock::to_time_t(now);
74 std::strftime(buf,
sizeof(buf),
"%Y-%m-%dT%H:%M:%SZ", &tm);
75 return std::string(buf);
78void add_classification(pugi::xml_node parent,
const char* classification_scheme,
79 const char* classified_object,
80 const char* node_representation,
81 const std::string& id_prefix) {
82 auto cls = parent.append_child(
"rim:Classification");
83 cls.append_attribute(
"classificationScheme") = classification_scheme;
84 cls.append_attribute(
"classifiedObject") = classified_object;
85 cls.append_attribute(
"nodeRepresentation") = node_representation;
86 cls.append_attribute(
"id") = make_xml_id(id_prefix).c_str();
87 cls.append_attribute(
"objectType") =
88 "urn:oasis:names:tc:ebxml-regrep:ObjectType:RegistryObject:Classification";
91void add_external_identifier(pugi::xml_node parent,
const char* scheme,
92 const char* registry_object,
const char* value,
94 auto eid = parent.append_child(
"rim:ExternalIdentifier");
95 eid.append_attribute(
"identificationScheme") = scheme;
96 eid.append_attribute(
"registryObject") = registry_object;
97 eid.append_attribute(
"value") = value;
98 eid.append_attribute(
"id") = make_xml_id(
"eid").c_str();
99 eid.append_attribute(
"objectType") =
100 "urn:oasis:names:tc:ebxml-regrep:ObjectType:RegistryObject:ExternalIdentifier";
101 auto name_node = eid.append_child(
"rim:Name");
102 auto loc = name_node.append_child(
"rim:LocalizedString");
103 loc.append_attribute(
"value") =
name;
106void add_registry_package(pugi::xml_node leaf_list,
const submission_set& s) {
107 auto rp = leaf_list.append_child(
"rim:RegistryPackage");
108 rp.append_attribute(
"id") = s.entry_uuid.c_str();
109 rp.append_attribute(
"objectType") =
110 "urn:oasis:names:tc:ebxml-regrep:ObjectType:RegistryObject:RegistryPackage";
112 if (!s.author_person.empty() || !s.author_institution.empty()) {
113 auto cls = rp.append_child(
"rim:Classification");
114 cls.append_attribute(
"classificationScheme") =
115 "urn:uuid:a7058bb9-b4e4-4307-ba5b-e3f0ab85e12d";
116 cls.append_attribute(
"classifiedObject") = s.entry_uuid.c_str();
117 cls.append_attribute(
"nodeRepresentation") =
"";
118 cls.append_attribute(
"id") = make_xml_id(
"cls").c_str();
119 cls.append_attribute(
"objectType") =
120 "urn:oasis:names:tc:ebxml-regrep:ObjectType:RegistryObject:Classification";
121 if (!s.author_person.empty()) {
122 auto slot = cls.append_child(
"rim:Slot");
123 slot.append_attribute(
"name") =
"authorPerson";
124 auto vl = slot.append_child(
"rim:ValueList");
125 vl.append_child(
"rim:Value")
127 .set(s.author_person.c_str());
129 if (!s.author_institution.empty()) {
130 auto slot = cls.append_child(
"rim:Slot");
131 slot.append_attribute(
"name") =
"authorInstitution";
132 auto vl = slot.append_child(
"rim:ValueList");
133 vl.append_child(
"rim:Value")
135 .set(s.author_institution.c_str());
139 if (!s.submission_time.empty()) {
140 auto slot = rp.append_child(
"rim:Slot");
141 slot.append_attribute(
"name") =
"submissionTime";
142 auto vl = slot.append_child(
"rim:ValueList");
143 vl.append_child(
"rim:Value").text().set(s.submission_time.c_str());
146 add_classification(rp, kSubmissionSetClassification,
147 s.entry_uuid.c_str(),
"",
"ssc");
149 add_external_identifier(rp, kSubmissionSetIdScheme, s.entry_uuid.c_str(),
150 s.submission_set_unique_id.c_str(),
151 "XDSSubmissionSet.uniqueId");
152 add_external_identifier(rp, kSubmissionSetIdScheme, s.entry_uuid.c_str(),
153 s.source_id.c_str(),
"XDSSubmissionSet.sourceId");
154 add_external_identifier(rp, kSubmissionSetIdScheme, s.entry_uuid.c_str(),
155 s.patient_id.c_str(),
"XDSSubmissionSet.patientId");
158void add_document_entry(pugi::xml_node leaf_list,
const xds_document& doc,
159 const std::string& cid) {
160 auto eo = leaf_list.append_child(
"rim:ExtrinsicObject");
161 eo.append_attribute(
"id") = doc.entry_uuid.c_str();
162 eo.append_attribute(
"mimeType") = doc.mime_type.c_str();
163 eo.append_attribute(
"objectType") =
164 "urn:uuid:7edca82f-054d-47f2-a032-9b2a5b5186c1";
166 auto include = eo.append_child(
"xop:Include");
167 include.append_attribute(
"href") = (
"cid:" + cid).c_str();
169 if (!doc.creation_time.empty()) {
170 auto slot = eo.append_child(
"rim:Slot");
171 slot.append_attribute(
"name") =
"creationTime";
172 auto vl = slot.append_child(
"rim:ValueList");
173 vl.append_child(
"rim:Value").text().set(doc.creation_time.c_str());
175 if (!doc.language_code.empty()) {
176 auto slot = eo.append_child(
"rim:Slot");
177 slot.append_attribute(
"name") =
"languageCode";
178 auto vl = slot.append_child(
"rim:ValueList");
179 vl.append_child(
"rim:Value").text().set(doc.language_code.c_str());
181 if (!doc.title.empty()) {
182 auto name_node = eo.append_child(
"rim:Name");
183 auto loc = name_node.append_child(
"rim:LocalizedString");
184 loc.append_attribute(
"value") = doc.title.c_str();
187 add_external_identifier(eo, kDocumentEntryIdScheme,
188 doc.entry_uuid.c_str(), doc.unique_id.c_str(),
189 "XDSDocumentEntry.uniqueId");
192struct xml_string_writer : pugi::xml_writer {
194 void write(
const void* data,
size_t size)
override {
195 out.append(
static_cast<const char*
>(data), size);
202 const submission_set& s,
const std::vector<std::string>& cid_refs) {
203 if (cid_refs.size() != s.
documents.size()) {
204 return kcenon::common::make_error<built_envelope>(
206 "content-id list size does not match document count",
210 pugi::xml_document doc;
211 auto decl = doc.append_child(pugi::node_declaration);
212 decl.append_attribute(
"version") =
"1.0";
213 decl.append_attribute(
"encoding") =
"UTF-8";
215 auto env = doc.append_child(
"soap:Envelope");
216 env.append_attribute(
"xmlns:soap") = kSoapEnv;
217 env.append_attribute(
"xmlns:wsa") = kWsa;
218 env.append_attribute(
"xmlns:wsse") = kWsse;
219 env.append_attribute(
"xmlns:wsu") = kWsu;
220 env.append_attribute(
"xmlns:lcm") = kLcm;
221 env.append_attribute(
"xmlns:rim") = kRim;
222 env.append_attribute(
"xmlns:xop") = kXop;
224 auto header = env.append_child(
"soap:Header");
226 auto action = header.append_child(
"wsa:Action");
227 action.append_attribute(
"soap:mustUnderstand") =
"true";
228 action.text().set(
"urn:ihe:iti:2007:RegisterDocumentSet-b");
230 auto message_id = header.append_child(
"wsa:MessageID");
231 message_id.text().set((
"urn:uuid:" + make_xml_id(
"msg")).c_str());
233 auto security = header.append_child(
"wsse:Security");
234 security.append_attribute(
"soap:mustUnderstand") =
"true";
237 out.timestamp_id = make_xml_id(
"ts");
238 out.binary_security_token_id = make_xml_id(
"bst");
239 out.body_id = make_xml_id(
"body");
241 auto timestamp = security.append_child(
"wsu:Timestamp");
242 timestamp.append_attribute(
"wsu:Id") =
out.timestamp_id.c_str();
243 auto created = timestamp.append_child(
"wsu:Created");
244 const std::string now = utc_iso8601_now();
245 created.text().set(now.c_str());
247 auto bst = security.append_child(
"wsse:BinarySecurityToken");
248 bst.append_attribute(
"wsu:Id") =
out.binary_security_token_id.c_str();
249 bst.append_attribute(
"EncodingType") =
250 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-"
251 "security-1.0#Base64Binary";
252 bst.append_attribute(
"ValueType") =
253 "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-"
254 "profile-1.0#X509v3";
256 auto body = env.append_child(
"soap:Body");
257 body.append_attribute(
"wsu:Id") =
out.body_id.c_str();
259 auto req = body.append_child(
"lcm:SubmitObjectsRequest");
261 auto leaf_list = req.append_child(
"rim:RegistryObjectList");
264 return kcenon::common::make_error<built_envelope>(
266 "submission_set.patient_id is empty",
270 return kcenon::common::make_error<built_envelope>(
272 "submission_set.source_id is empty",
276 return kcenon::common::make_error<built_envelope>(
278 "submission_set.submission_set_unique_id is empty",
282 return kcenon::common::make_error<built_envelope>(
284 "submission_set contains no documents",
288 for (std::size_t i = 0; i < s.
documents.size(); ++i) {
290 if (d.unique_id.empty()) {
291 return kcenon::common::make_error<built_envelope>(
294 "document[" + std::to_string(i) +
"].unique_id is empty",
297 if (d.mime_type.empty()) {
298 return kcenon::common::make_error<built_envelope>(
301 "document[" + std::to_string(i) +
"].mime_type is empty",
304 if (d.content.empty()) {
305 return kcenon::common::make_error<built_envelope>(
307 "document[" + std::to_string(i) +
"].content is empty",
310 add_document_entry(leaf_list, d, cid_refs[i]);
313 add_registry_package(leaf_list, s);
315 xml_string_writer writer;
316 doc.save(writer,
"", pugi::format_raw | pugi::format_no_declaration,
317 pugi::encoding_utf8);
318 if (writer.out.empty()) {
319 return kcenon::common::make_error<built_envelope>(
321 "pugixml produced an empty envelope",
325 out.xml = R
"(<?xml version="1.0" encoding="UTF-8"?>)";
326 out.xml += writer.out;
kcenon::common::Result< built_envelope > build_iti41_envelope(const submission_set &s, const std::vector< std::string > &cid_refs)
Build a SOAP envelope for ITI-41 from submission metadata and a list of content-id references for MTO...
constexpr std::string_view error_source
Short identifier used as the "source" field of make_error.
@ metadata_document_empty
@ metadata_document_missing_unique_id
@ metadata_document_missing_mime_type
@ metadata_missing_source_id
@ envelope_serialization_failed
@ metadata_missing_patient_id
@ metadata_missing_submission_set_uid
SOAP 1.2 envelope builder for IHE XDS.b ITI-41 requests.
Result of envelope construction.
ebRIM/ebRS-shaped submission set metadata.
std::string submission_set_unique_id
std::vector< xds_document > documents