Mercurial > hg > orthanc
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(¶ms_); | |
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(¶ms_, /*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 = ¶ms_->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 } |