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(&params_);
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(&params_, /*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 = &params_->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 }