Mercurial > hg > orthanc
comparison OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp @ 4044:d25f4c0fa160 framework
splitting code into OrthancFramework and OrthancServer
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 10 Jun 2020 20:30:34 +0200 |
parents | Core/DicomNetworking/DicomAssociation.cpp@ea1d32861cfc |
children | bf7b9edf6b81 |
comparison
equal
deleted
inserted
replaced
4043:6c6239aec462 | 4044:d25f4c0fa160 |
---|---|
1 /** | |
2 * Orthanc - A Lightweight, RESTful DICOM Store | |
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics | |
4 * Department, University Hospital of Liege, Belgium | |
5 * Copyright (C) 2017-2020 Osimis S.A., Belgium | |
6 * | |
7 * This program is free software: you can redistribute it and/or | |
8 * modify it under the terms of the GNU General Public License as | |
9 * published by the Free Software Foundation, either version 3 of the | |
10 * License, or (at your option) any later version. | |
11 * | |
12 * In addition, as a special exception, the copyright holders of this | |
13 * program give permission to link the code of its release with the | |
14 * OpenSSL project's "OpenSSL" library (or with modified versions of it | |
15 * that use the same license as the "OpenSSL" library), and distribute | |
16 * the linked executables. You must obey the GNU General Public License | |
17 * in all respects for all of the code used other than "OpenSSL". If you | |
18 * modify file(s) with this exception, you may extend this exception to | |
19 * your version of the file(s), but you are not obligated to do so. If | |
20 * you do not wish to do so, delete this exception statement from your | |
21 * version. If you delete this exception statement from all source files | |
22 * in the program, then also delete it here. | |
23 * | |
24 * This program is distributed in the hope that it will be useful, but | |
25 * WITHOUT ANY WARRANTY; without even the implied warranty of | |
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
27 * General Public License for more details. | |
28 * | |
29 * You should have received a copy of the GNU General Public License | |
30 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
31 **/ | |
32 | |
33 | |
34 #include "../PrecompiledHeaders.h" | |
35 #include "DicomAssociation.h" | |
36 | |
37 #if !defined(DCMTK_VERSION_NUMBER) | |
38 # error The macro DCMTK_VERSION_NUMBER must be defined | |
39 #endif | |
40 | |
41 #include "../Compatibility.h" | |
42 #include "../Logging.h" | |
43 #include "../OrthancException.h" | |
44 #include "NetworkingCompatibility.h" | |
45 | |
46 #include <dcmtk/dcmnet/diutil.h> // For dcmConnectionTimeout() | |
47 #include <dcmtk/dcmdata/dcdeftag.h> | |
48 | |
49 namespace Orthanc | |
50 { | |
51 static void FillSopSequence(DcmDataset& dataset, | |
52 const DcmTagKey& tag, | |
53 const std::vector<std::string>& sopClassUids, | |
54 const std::vector<std::string>& sopInstanceUids, | |
55 const std::vector<StorageCommitmentFailureReason>& failureReasons, | |
56 bool hasFailureReasons) | |
57 { | |
58 assert(sopClassUids.size() == sopInstanceUids.size() && | |
59 (hasFailureReasons ? | |
60 failureReasons.size() == sopClassUids.size() : | |
61 failureReasons.empty())); | |
62 | |
63 if (sopInstanceUids.empty()) | |
64 { | |
65 // Add an empty sequence | |
66 if (!dataset.insertEmptyElement(tag).good()) | |
67 { | |
68 throw OrthancException(ErrorCode_InternalError); | |
69 } | |
70 } | |
71 else | |
72 { | |
73 for (size_t i = 0; i < sopClassUids.size(); i++) | |
74 { | |
75 std::unique_ptr<DcmItem> item(new DcmItem); | |
76 if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, sopClassUids[i].c_str()).good() || | |
77 !item->putAndInsertString(DCM_ReferencedSOPInstanceUID, sopInstanceUids[i].c_str()).good() || | |
78 (hasFailureReasons && | |
79 !item->putAndInsertUint16(DCM_FailureReason, failureReasons[i]).good()) || | |
80 !dataset.insertSequenceItem(tag, item.release()).good()) | |
81 { | |
82 throw OrthancException(ErrorCode_InternalError); | |
83 } | |
84 } | |
85 } | |
86 } | |
87 | |
88 | |
89 void DicomAssociation::Initialize() | |
90 { | |
91 role_ = DicomAssociationRole_Default; | |
92 isOpen_ = false; | |
93 net_ = NULL; | |
94 params_ = NULL; | |
95 assoc_ = NULL; | |
96 | |
97 // Must be after "isOpen_ = false" | |
98 ClearPresentationContexts(); | |
99 } | |
100 | |
101 | |
102 void DicomAssociation::CheckConnecting(const DicomAssociationParameters& parameters, | |
103 const OFCondition& cond) | |
104 { | |
105 try | |
106 { | |
107 CheckCondition(cond, parameters, "connecting"); | |
108 } | |
109 catch (OrthancException&) | |
110 { | |
111 CloseInternal(); | |
112 throw; | |
113 } | |
114 } | |
115 | |
116 | |
117 void DicomAssociation::CloseInternal() | |
118 { | |
119 if (assoc_ != NULL) | |
120 { | |
121 ASC_releaseAssociation(assoc_); | |
122 ASC_destroyAssociation(&assoc_); | |
123 assoc_ = NULL; | |
124 params_ = NULL; | |
125 } | |
126 else | |
127 { | |
128 if (params_ != NULL) | |
129 { | |
130 ASC_destroyAssociationParameters(¶ms_); | |
131 params_ = NULL; | |
132 } | |
133 } | |
134 | |
135 if (net_ != NULL) | |
136 { | |
137 ASC_dropNetwork(&net_); | |
138 net_ = NULL; | |
139 } | |
140 | |
141 accepted_.clear(); | |
142 isOpen_ = false; | |
143 } | |
144 | |
145 | |
146 void DicomAssociation::AddAccepted(const std::string& abstractSyntax, | |
147 DicomTransferSyntax syntax, | |
148 uint8_t presentationContextId) | |
149 { | |
150 AcceptedPresentationContexts::iterator found = accepted_.find(abstractSyntax); | |
151 | |
152 if (found == accepted_.end()) | |
153 { | |
154 std::map<DicomTransferSyntax, uint8_t> syntaxes; | |
155 syntaxes[syntax] = presentationContextId; | |
156 accepted_[abstractSyntax] = syntaxes; | |
157 } | |
158 else | |
159 { | |
160 if (found->second.find(syntax) != found->second.end()) | |
161 { | |
162 LOG(WARNING) << "The same transfer syntax (" | |
163 << GetTransferSyntaxUid(syntax) | |
164 << ") was accepted twice for the same abstract syntax UID (" | |
165 << abstractSyntax << ")"; | |
166 } | |
167 else | |
168 { | |
169 found->second[syntax] = presentationContextId; | |
170 } | |
171 } | |
172 } | |
173 | |
174 | |
175 DicomAssociation::~DicomAssociation() | |
176 { | |
177 try | |
178 { | |
179 Close(); | |
180 } | |
181 catch (OrthancException& e) | |
182 { | |
183 // Don't throw exception in destructors | |
184 LOG(ERROR) << "Error while destroying a DICOM association: " << e.What(); | |
185 } | |
186 } | |
187 | |
188 | |
189 void DicomAssociation::SetRole(DicomAssociationRole role) | |
190 { | |
191 if (role_ != role) | |
192 { | |
193 Close(); | |
194 role_ = role; | |
195 } | |
196 } | |
197 | |
198 | |
199 void DicomAssociation::ClearPresentationContexts() | |
200 { | |
201 Close(); | |
202 proposed_.clear(); | |
203 proposed_.reserve(MAX_PROPOSED_PRESENTATIONS); | |
204 } | |
205 | |
206 | |
207 void DicomAssociation::Open(const DicomAssociationParameters& parameters) | |
208 { | |
209 if (isOpen_) | |
210 { | |
211 return; // Already open | |
212 } | |
213 | |
214 // Timeout used during association negociation and ASC_releaseAssociation() | |
215 uint32_t acseTimeout = parameters.GetTimeout(); | |
216 if (acseTimeout == 0) | |
217 { | |
218 /** | |
219 * Timeout is disabled. Global timeout (seconds) for | |
220 * connecting to remote hosts. Default value is -1 which | |
221 * selects infinite timeout, i.e. blocking connect(). | |
222 **/ | |
223 dcmConnectionTimeout.set(-1); | |
224 acseTimeout = 10; | |
225 } | |
226 else | |
227 { | |
228 dcmConnectionTimeout.set(acseTimeout); | |
229 } | |
230 | |
231 T_ASC_SC_ROLE dcmtkRole; | |
232 switch (role_) | |
233 { | |
234 case DicomAssociationRole_Default: | |
235 dcmtkRole = ASC_SC_ROLE_DEFAULT; | |
236 break; | |
237 | |
238 case DicomAssociationRole_Scu: | |
239 dcmtkRole = ASC_SC_ROLE_SCU; | |
240 break; | |
241 | |
242 case DicomAssociationRole_Scp: | |
243 dcmtkRole = ASC_SC_ROLE_SCP; | |
244 break; | |
245 | |
246 default: | |
247 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
248 } | |
249 | |
250 assert(net_ == NULL && | |
251 params_ == NULL && | |
252 assoc_ == NULL); | |
253 | |
254 if (proposed_.empty()) | |
255 { | |
256 throw OrthancException(ErrorCode_BadSequenceOfCalls, | |
257 "No presentation context was proposed"); | |
258 } | |
259 | |
260 LOG(INFO) << "Opening a DICOM SCU connection from AET \"" | |
261 << parameters.GetLocalApplicationEntityTitle() | |
262 << "\" to AET \"" << parameters.GetRemoteModality().GetApplicationEntityTitle() | |
263 << "\" on host " << parameters.GetRemoteModality().GetHost() | |
264 << ":" << parameters.GetRemoteModality().GetPortNumber() | |
265 << " (manufacturer: " << EnumerationToString(parameters.GetRemoteModality().GetManufacturer()) << ")"; | |
266 | |
267 CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_)); | |
268 CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU)); | |
269 | |
270 // Set this application's title and the called application's title in the params | |
271 CheckConnecting(parameters, ASC_setAPTitles( | |
272 params_, parameters.GetLocalApplicationEntityTitle().c_str(), | |
273 parameters.GetRemoteModality().GetApplicationEntityTitle().c_str(), NULL)); | |
274 | |
275 // Set the network addresses of the local and remote entities | |
276 char localHost[HOST_NAME_MAX]; | |
277 gethostname(localHost, HOST_NAME_MAX - 1); | |
278 | |
279 char remoteHostAndPort[HOST_NAME_MAX]; | |
280 | |
281 #ifdef _MSC_VER | |
282 _snprintf | |
283 #else | |
284 snprintf | |
285 #endif | |
286 (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", | |
287 parameters.GetRemoteModality().GetHost().c_str(), | |
288 parameters.GetRemoteModality().GetPortNumber()); | |
289 | |
290 CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort)); | |
291 | |
292 // Set various options | |
293 CheckConnecting(parameters, ASC_setTransportLayerType(params_, /*opt_secureConnection*/ false)); | |
294 | |
295 // Setup the list of proposed presentation contexts | |
296 unsigned int presentationContextId = 1; | |
297 for (size_t i = 0; i < proposed_.size(); i++) | |
298 { | |
299 assert(presentationContextId <= 255); | |
300 const char* abstractSyntax = proposed_[i].abstractSyntax_.c_str(); | |
301 | |
302 const std::set<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_; | |
303 | |
304 std::vector<const char*> transferSyntaxes; | |
305 transferSyntaxes.reserve(source.size()); | |
306 | |
307 for (std::set<DicomTransferSyntax>::const_iterator | |
308 it = source.begin(); it != source.end(); ++it) | |
309 { | |
310 transferSyntaxes.push_back(GetTransferSyntaxUid(*it)); | |
311 } | |
312 | |
313 assert(!transferSyntaxes.empty()); | |
314 CheckConnecting(parameters, ASC_addPresentationContext( | |
315 params_, presentationContextId, abstractSyntax, | |
316 &transferSyntaxes[0], transferSyntaxes.size(), dcmtkRole)); | |
317 | |
318 presentationContextId += 2; | |
319 } | |
320 | |
321 // Do the association | |
322 CheckConnecting(parameters, ASC_requestAssociation(net_, params_, &assoc_)); | |
323 isOpen_ = true; | |
324 | |
325 // Inspect the accepted transfer syntaxes | |
326 LST_HEAD **l = ¶ms_->DULparams.acceptedPresentationContext; | |
327 if (*l != NULL) | |
328 { | |
329 DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l); | |
330 LST_Position(l, (LST_NODE*)pc); | |
331 while (pc) | |
332 { | |
333 if (pc->result == ASC_P_ACCEPTANCE) | |
334 { | |
335 DicomTransferSyntax transferSyntax; | |
336 if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax)) | |
337 { | |
338 AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID); | |
339 } | |
340 else | |
341 { | |
342 LOG(WARNING) << "Unknown transfer syntax received from AET \"" | |
343 << parameters.GetRemoteModality().GetApplicationEntityTitle() | |
344 << "\": " << pc->acceptedTransferSyntax; | |
345 } | |
346 } | |
347 | |
348 pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l); | |
349 } | |
350 } | |
351 | |
352 if (accepted_.empty()) | |
353 { | |
354 throw OrthancException(ErrorCode_NoPresentationContext, | |
355 "Unable to negotiate a presentation context with AET \"" + | |
356 parameters.GetRemoteModality().GetApplicationEntityTitle() + "\""); | |
357 } | |
358 } | |
359 | |
360 void DicomAssociation::Close() | |
361 { | |
362 if (isOpen_) | |
363 { | |
364 CloseInternal(); | |
365 } | |
366 } | |
367 | |
368 | |
369 bool DicomAssociation::LookupAcceptedPresentationContext(std::map<DicomTransferSyntax, uint8_t>& target, | |
370 const std::string& abstractSyntax) const | |
371 { | |
372 if (!IsOpen()) | |
373 { | |
374 throw OrthancException(ErrorCode_BadSequenceOfCalls, "Connection not opened"); | |
375 } | |
376 | |
377 AcceptedPresentationContexts::const_iterator found = accepted_.find(abstractSyntax); | |
378 | |
379 if (found == accepted_.end()) | |
380 { | |
381 return false; | |
382 } | |
383 else | |
384 { | |
385 target = found->second; | |
386 return true; | |
387 } | |
388 } | |
389 | |
390 | |
391 void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax) | |
392 { | |
393 std::set<DicomTransferSyntax> ts; | |
394 ts.insert(DicomTransferSyntax_LittleEndianImplicit); | |
395 ts.insert(DicomTransferSyntax_LittleEndianExplicit); | |
396 ts.insert(DicomTransferSyntax_BigEndianExplicit); // Retired | |
397 ProposePresentationContext(abstractSyntax, ts); | |
398 } | |
399 | |
400 | |
401 void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax, | |
402 DicomTransferSyntax transferSyntax) | |
403 { | |
404 std::set<DicomTransferSyntax> ts; | |
405 ts.insert(transferSyntax); | |
406 ProposePresentationContext(abstractSyntax, ts); | |
407 } | |
408 | |
409 | |
410 size_t DicomAssociation::GetRemainingPropositions() const | |
411 { | |
412 assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS); | |
413 return MAX_PROPOSED_PRESENTATIONS - proposed_.size(); | |
414 } | |
415 | |
416 | |
417 void DicomAssociation::ProposePresentationContext( | |
418 const std::string& abstractSyntax, | |
419 const std::set<DicomTransferSyntax>& transferSyntaxes) | |
420 { | |
421 if (transferSyntaxes.empty()) | |
422 { | |
423 throw OrthancException(ErrorCode_ParameterOutOfRange, | |
424 "No transfer syntax provided"); | |
425 } | |
426 | |
427 if (proposed_.size() >= MAX_PROPOSED_PRESENTATIONS) | |
428 { | |
429 throw OrthancException(ErrorCode_ParameterOutOfRange, | |
430 "Too many proposed presentation contexts"); | |
431 } | |
432 | |
433 if (IsOpen()) | |
434 { | |
435 Close(); | |
436 } | |
437 | |
438 ProposedPresentationContext context; | |
439 context.abstractSyntax_ = abstractSyntax; | |
440 context.transferSyntaxes_ = transferSyntaxes; | |
441 | |
442 proposed_.push_back(context); | |
443 } | |
444 | |
445 | |
446 T_ASC_Association& DicomAssociation::GetDcmtkAssociation() const | |
447 { | |
448 if (isOpen_) | |
449 { | |
450 assert(assoc_ != NULL); | |
451 return *assoc_; | |
452 } | |
453 else | |
454 { | |
455 throw OrthancException(ErrorCode_BadSequenceOfCalls, | |
456 "The connection is not open"); | |
457 } | |
458 } | |
459 | |
460 | |
461 T_ASC_Network& DicomAssociation::GetDcmtkNetwork() const | |
462 { | |
463 if (isOpen_) | |
464 { | |
465 assert(net_ != NULL); | |
466 return *net_; | |
467 } | |
468 else | |
469 { | |
470 throw OrthancException(ErrorCode_BadSequenceOfCalls, | |
471 "The connection is not open"); | |
472 } | |
473 } | |
474 | |
475 | |
476 void DicomAssociation::CheckCondition(const OFCondition& cond, | |
477 const DicomAssociationParameters& parameters, | |
478 const std::string& command) | |
479 { | |
480 if (cond.bad()) | |
481 { | |
482 // Reformat the error message from DCMTK by turning multiline | |
483 // errors into a single line | |
484 | |
485 std::string s(cond.text()); | |
486 std::string info; | |
487 info.reserve(s.size()); | |
488 | |
489 bool isMultiline = false; | |
490 for (size_t i = 0; i < s.size(); i++) | |
491 { | |
492 if (s[i] == '\r') | |
493 { | |
494 // Ignore | |
495 } | |
496 else if (s[i] == '\n') | |
497 { | |
498 if (isMultiline) | |
499 { | |
500 info += "; "; | |
501 } | |
502 else | |
503 { | |
504 info += " ("; | |
505 isMultiline = true; | |
506 } | |
507 } | |
508 else | |
509 { | |
510 info.push_back(s[i]); | |
511 } | |
512 } | |
513 | |
514 if (isMultiline) | |
515 { | |
516 info += ")"; | |
517 } | |
518 | |
519 throw OrthancException(ErrorCode_NetworkProtocol, | |
520 "DicomAssociation - " + command + " to AET \"" + | |
521 parameters.GetRemoteModality().GetApplicationEntityTitle() + | |
522 "\": " + info); | |
523 } | |
524 } | |
525 | |
526 | |
527 void DicomAssociation::ReportStorageCommitment( | |
528 const DicomAssociationParameters& parameters, | |
529 const std::string& transactionUid, | |
530 const std::vector<std::string>& sopClassUids, | |
531 const std::vector<std::string>& sopInstanceUids, | |
532 const std::vector<StorageCommitmentFailureReason>& failureReasons) | |
533 { | |
534 if (sopClassUids.size() != sopInstanceUids.size() || | |
535 sopClassUids.size() != failureReasons.size()) | |
536 { | |
537 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
538 } | |
539 | |
540 | |
541 std::vector<std::string> successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids; | |
542 std::vector<StorageCommitmentFailureReason> failedReasons; | |
543 | |
544 successSopClassUids.reserve(sopClassUids.size()); | |
545 successSopInstanceUids.reserve(sopClassUids.size()); | |
546 failedSopClassUids.reserve(sopClassUids.size()); | |
547 failedSopInstanceUids.reserve(sopClassUids.size()); | |
548 failedReasons.reserve(sopClassUids.size()); | |
549 | |
550 for (size_t i = 0; i < sopClassUids.size(); i++) | |
551 { | |
552 switch (failureReasons[i]) | |
553 { | |
554 case StorageCommitmentFailureReason_Success: | |
555 successSopClassUids.push_back(sopClassUids[i]); | |
556 successSopInstanceUids.push_back(sopInstanceUids[i]); | |
557 break; | |
558 | |
559 case StorageCommitmentFailureReason_ProcessingFailure: | |
560 case StorageCommitmentFailureReason_NoSuchObjectInstance: | |
561 case StorageCommitmentFailureReason_ResourceLimitation: | |
562 case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported: | |
563 case StorageCommitmentFailureReason_ClassInstanceConflict: | |
564 case StorageCommitmentFailureReason_DuplicateTransactionUID: | |
565 failedSopClassUids.push_back(sopClassUids[i]); | |
566 failedSopInstanceUids.push_back(sopInstanceUids[i]); | |
567 failedReasons.push_back(failureReasons[i]); | |
568 break; | |
569 | |
570 default: | |
571 { | |
572 char buf[16]; | |
573 sprintf(buf, "%04xH", failureReasons[i]); | |
574 throw OrthancException(ErrorCode_ParameterOutOfRange, | |
575 "Unsupported failure reason for storage commitment: " + std::string(buf)); | |
576 } | |
577 } | |
578 } | |
579 | |
580 DicomAssociation association; | |
581 | |
582 { | |
583 std::set<DicomTransferSyntax> transferSyntaxes; | |
584 transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit); | |
585 transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit); | |
586 | |
587 association.SetRole(DicomAssociationRole_Scp); | |
588 association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass, | |
589 transferSyntaxes); | |
590 } | |
591 | |
592 association.Open(parameters); | |
593 | |
594 /** | |
595 * N-EVENT-REPORT | |
596 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html | |
597 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1 | |
598 * | |
599 * Status code: | |
600 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8 | |
601 **/ | |
602 | |
603 /** | |
604 * Send the "EVENT_REPORT_RQ" request | |
605 **/ | |
606 | |
607 LOG(INFO) << "Reporting modality \"" | |
608 << parameters.GetRemoteModality().GetApplicationEntityTitle() | |
609 << "\" about storage commitment transaction: " << transactionUid | |
610 << " (" << successSopClassUids.size() << " successes, " | |
611 << failedSopClassUids.size() << " failures)"; | |
612 const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++; | |
613 | |
614 { | |
615 T_DIMSE_Message message; | |
616 memset(&message, 0, sizeof(message)); | |
617 message.CommandField = DIMSE_N_EVENT_REPORT_RQ; | |
618 | |
619 T_DIMSE_N_EventReportRQ& content = message.msg.NEventReportRQ; | |
620 content.MessageID = messageId; | |
621 strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); | |
622 strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); | |
623 content.DataSetType = DIMSE_DATASET_PRESENT; | |
624 | |
625 DcmDataset dataset; | |
626 if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good()) | |
627 { | |
628 throw OrthancException(ErrorCode_InternalError); | |
629 } | |
630 | |
631 { | |
632 std::vector<StorageCommitmentFailureReason> empty; | |
633 FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids, | |
634 successSopInstanceUids, empty, false); | |
635 } | |
636 | |
637 // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html | |
638 if (failedSopClassUids.empty()) | |
639 { | |
640 content.EventTypeID = 1; // "Storage Commitment Request Successful" | |
641 } | |
642 else | |
643 { | |
644 content.EventTypeID = 2; // "Storage Commitment Request Complete - Failures Exist" | |
645 | |
646 // Failure reason | |
647 // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part03/sect_C.14.html#sect_C.14.1.1 | |
648 FillSopSequence(dataset, DCM_FailedSOPSequence, failedSopClassUids, | |
649 failedSopInstanceUids, failedReasons, true); | |
650 } | |
651 | |
652 int presID = ASC_findAcceptedPresentationContextID( | |
653 &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass); | |
654 if (presID == 0) | |
655 { | |
656 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " | |
657 "Unable to send N-EVENT-REPORT request to AET: " + | |
658 parameters.GetRemoteModality().GetApplicationEntityTitle()); | |
659 } | |
660 | |
661 if (!DIMSE_sendMessageUsingMemoryData( | |
662 &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */, | |
663 &dataset, NULL /* callback */, NULL /* callback context */, | |
664 NULL /* commandSet */).good()) | |
665 { | |
666 throw OrthancException(ErrorCode_NetworkProtocol); | |
667 } | |
668 } | |
669 | |
670 /** | |
671 * Read the "EVENT_REPORT_RSP" response | |
672 **/ | |
673 | |
674 { | |
675 T_ASC_PresentationContextID presID = 0; | |
676 T_DIMSE_Message message; | |
677 | |
678 if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(), | |
679 (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), | |
680 parameters.GetTimeout(), &presID, &message, | |
681 NULL /* no statusDetail */).good() || | |
682 message.CommandField != DIMSE_N_EVENT_REPORT_RSP) | |
683 { | |
684 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " | |
685 "Unable to read N-EVENT-REPORT response from AET: " + | |
686 parameters.GetRemoteModality().GetApplicationEntityTitle()); | |
687 } | |
688 | |
689 const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP; | |
690 if (content.MessageIDBeingRespondedTo != messageId || | |
691 !(content.opts & O_NEVENTREPORT_AFFECTEDSOPCLASSUID) || | |
692 !(content.opts & O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID) || | |
693 //(content.opts & O_NEVENTREPORT_EVENTTYPEID) || // Pedantic test - The "content.EventTypeID" is not used by Orthanc | |
694 std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || | |
695 std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance || | |
696 content.DataSetType != DIMSE_DATASET_NULL) | |
697 { | |
698 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " | |
699 "Badly formatted N-EVENT-REPORT response from AET: " + | |
700 parameters.GetRemoteModality().GetApplicationEntityTitle()); | |
701 } | |
702 | |
703 if (content.DimseStatus != 0 /* success */) | |
704 { | |
705 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " | |
706 "The request cannot be handled by remote AET: " + | |
707 parameters.GetRemoteModality().GetApplicationEntityTitle()); | |
708 } | |
709 } | |
710 | |
711 association.Close(); | |
712 } | |
713 | |
714 | |
715 void DicomAssociation::RequestStorageCommitment( | |
716 const DicomAssociationParameters& parameters, | |
717 const std::string& transactionUid, | |
718 const std::vector<std::string>& sopClassUids, | |
719 const std::vector<std::string>& sopInstanceUids) | |
720 { | |
721 if (sopClassUids.size() != sopInstanceUids.size()) | |
722 { | |
723 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
724 } | |
725 | |
726 for (size_t i = 0; i < sopClassUids.size(); i++) | |
727 { | |
728 if (sopClassUids[i].empty() || | |
729 sopInstanceUids[i].empty()) | |
730 { | |
731 throw OrthancException(ErrorCode_ParameterOutOfRange, | |
732 "The SOP class/instance UIDs cannot be empty, found: \"" + | |
733 sopClassUids[i] + "\" / \"" + sopInstanceUids[i] + "\""); | |
734 } | |
735 } | |
736 | |
737 if (transactionUid.size() < 5 || | |
738 transactionUid.substr(0, 5) != "2.25.") | |
739 { | |
740 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
741 } | |
742 | |
743 DicomAssociation association; | |
744 | |
745 { | |
746 std::set<DicomTransferSyntax> transferSyntaxes; | |
747 transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit); | |
748 transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit); | |
749 | |
750 association.SetRole(DicomAssociationRole_Default); | |
751 association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass, | |
752 transferSyntaxes); | |
753 } | |
754 | |
755 association.Open(parameters); | |
756 | |
757 /** | |
758 * N-ACTION | |
759 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html | |
760 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4 | |
761 * | |
762 * Status code: | |
763 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8 | |
764 **/ | |
765 | |
766 /** | |
767 * Send the "N_ACTION_RQ" request | |
768 **/ | |
769 | |
770 LOG(INFO) << "Request to modality \"" | |
771 << parameters.GetRemoteModality().GetApplicationEntityTitle() | |
772 << "\" about storage commitment for " << sopClassUids.size() | |
773 << " instances, with transaction UID: " << transactionUid; | |
774 const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++; | |
775 | |
776 { | |
777 T_DIMSE_Message message; | |
778 memset(&message, 0, sizeof(message)); | |
779 message.CommandField = DIMSE_N_ACTION_RQ; | |
780 | |
781 T_DIMSE_N_ActionRQ& content = message.msg.NActionRQ; | |
782 content.MessageID = messageId; | |
783 strncpy(content.RequestedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); | |
784 strncpy(content.RequestedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); | |
785 content.ActionTypeID = 1; // "Request Storage Commitment" | |
786 content.DataSetType = DIMSE_DATASET_PRESENT; | |
787 | |
788 DcmDataset dataset; | |
789 if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good()) | |
790 { | |
791 throw OrthancException(ErrorCode_InternalError); | |
792 } | |
793 | |
794 { | |
795 std::vector<StorageCommitmentFailureReason> empty; | |
796 FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, empty, false); | |
797 } | |
798 | |
799 int presID = ASC_findAcceptedPresentationContextID( | |
800 &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass); | |
801 if (presID == 0) | |
802 { | |
803 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " | |
804 "Unable to send N-ACTION request to AET: " + | |
805 parameters.GetRemoteModality().GetApplicationEntityTitle()); | |
806 } | |
807 | |
808 if (!DIMSE_sendMessageUsingMemoryData( | |
809 &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */, | |
810 &dataset, NULL /* callback */, NULL /* callback context */, | |
811 NULL /* commandSet */).good()) | |
812 { | |
813 throw OrthancException(ErrorCode_NetworkProtocol); | |
814 } | |
815 } | |
816 | |
817 /** | |
818 * Read the "N_ACTION_RSP" response | |
819 **/ | |
820 | |
821 { | |
822 T_ASC_PresentationContextID presID = 0; | |
823 T_DIMSE_Message message; | |
824 | |
825 if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(), | |
826 (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), | |
827 parameters.GetTimeout(), &presID, &message, | |
828 NULL /* no statusDetail */).good() || | |
829 message.CommandField != DIMSE_N_ACTION_RSP) | |
830 { | |
831 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " | |
832 "Unable to read N-ACTION response from AET: " + | |
833 parameters.GetRemoteModality().GetApplicationEntityTitle()); | |
834 } | |
835 | |
836 const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP; | |
837 if (content.MessageIDBeingRespondedTo != messageId || | |
838 !(content.opts & O_NACTION_AFFECTEDSOPCLASSUID) || | |
839 !(content.opts & O_NACTION_AFFECTEDSOPINSTANCEUID) || | |
840 //(content.opts & O_NACTION_ACTIONTYPEID) || // Pedantic test - The "content.ActionTypeID" is not used by Orthanc | |
841 std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || | |
842 std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance || | |
843 content.DataSetType != DIMSE_DATASET_NULL) | |
844 { | |
845 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " | |
846 "Badly formatted N-ACTION response from AET: " + | |
847 parameters.GetRemoteModality().GetApplicationEntityTitle()); | |
848 } | |
849 | |
850 if (content.DimseStatus != 0 /* success */) | |
851 { | |
852 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " | |
853 "The request cannot be handled by remote AET: " + | |
854 parameters.GetRemoteModality().GetApplicationEntityTitle()); | |
855 } | |
856 } | |
857 | |
858 association.Close(); | |
859 } | |
860 } |