comparison Core/DicomNetworking/DicomAssociation.cpp @ 3825:4570c57668a8

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