comparison Core/DicomNetworking/DicomUserConnection.cpp @ 3786:3801435e34a1 SylvainRouquette/fix-issue169-95b752c

integration Orthanc-1.6.0->SylvainRouquette
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 19 Mar 2020 11:48:30 +0100
parents 763533d6dd67 c6658187e4b1
children
comparison
equal deleted inserted replaced
3785:763533d6dd67 3786:3801435e34a1
1 /** 1 /**
2 * Orthanc - A Lightweight, RESTful DICOM Store 2 * Orthanc - A Lightweight, RESTful DICOM Store
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics 3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium 4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2019 Osimis S.A., Belgium 5 * Copyright (C) 2017-2020 Osimis S.A., Belgium
6 * 6 *
7 * This program is free software: you can redistribute it and/or 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 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 9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version. 10 * License, or (at your option) any later version.
84 84
85 #if !defined(DCMTK_VERSION_NUMBER) 85 #if !defined(DCMTK_VERSION_NUMBER)
86 # error The macro DCMTK_VERSION_NUMBER must be defined 86 # error The macro DCMTK_VERSION_NUMBER must be defined
87 #endif 87 #endif
88 88
89 #include "../Compatibility.h"
89 #include "../DicomFormat/DicomArray.h" 90 #include "../DicomFormat/DicomArray.h"
90 #include "../Logging.h" 91 #include "../Logging.h"
91 #include "../OrthancException.h" 92 #include "../OrthancException.h"
92 #include "../DicomParsing/FromDcmtkBridge.h" 93 #include "../DicomParsing/FromDcmtkBridge.h"
93 #include "../DicomParsing/ToDcmtkBridge.h" 94 #include "../DicomParsing/ToDcmtkBridge.h"
156 return assoc_ != NULL; 157 return assoc_ != NULL;
157 } 158 }
158 159
159 void CheckIsOpen() const; 160 void CheckIsOpen() const;
160 161
161 void Store(DcmInputStream& is, 162 void Store(std::string& sopClassUidOut /* out */,
163 std::string& sopInstanceUidOut /* out */,
164 DcmInputStream& is,
162 DicomUserConnection& connection, 165 DicomUserConnection& connection,
163 const std::string& moveOriginatorAET, 166 const std::string& moveOriginatorAET,
164 uint16_t moveOriginatorID); 167 uint16_t moveOriginatorID);
165 }; 168 };
166 169
250 presentationContextId += 2; 253 presentationContextId += 2;
251 } 254 }
252 } 255 }
253 256
254 257
255 void DicomUserConnection::SetupPresentationContexts(const std::string& preferredTransferSyntax) 258 void DicomUserConnection::SetupPresentationContexts(Mode mode,
259 const std::string& preferredTransferSyntax)
256 { 260 {
257 // Flatten an array with the preferred transfer syntax 261 // Flatten an array with the preferred transfer syntax
258 const char* asPreferred[1] = { preferredTransferSyntax.c_str() }; 262 const char* asPreferred[1] = { preferredTransferSyntax.c_str() };
259 263
260 // Setup the fallback transfer syntaxes 264 // Setup the fallback transfer syntaxes
272 { 276 {
273 asFallback.push_back(it->c_str()); 277 asFallback.push_back(it->c_str());
274 } 278 }
275 279
276 CheckStorageSOPClassesInvariant(); 280 CheckStorageSOPClassesInvariant();
277 unsigned int presentationContextId = 1; 281
278 282 switch (mode)
279 for (std::list<std::string>::const_iterator it = reservedStorageSOPClasses_.begin(); 283 {
280 it != reservedStorageSOPClasses_.end(); ++it) 284 case Mode_Generic:
281 { 285 {
282 RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 286 unsigned int presentationContextId = 1;
283 *it, asPreferred, asFallback, remoteAet_); 287
284 } 288 for (std::list<std::string>::const_iterator it = reservedStorageSOPClasses_.begin();
285 289 it != reservedStorageSOPClasses_.end(); ++it)
286 for (std::set<std::string>::const_iterator it = storageSOPClasses_.begin(); 290 {
287 it != storageSOPClasses_.end(); ++it) 291 RegisterStorageSOPClass(pimpl_->params_, presentationContextId,
288 { 292 *it, asPreferred, asFallback, remoteAet_);
289 RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 293 }
290 *it, asPreferred, asFallback, remoteAet_); 294
291 } 295 for (std::set<std::string>::const_iterator it = storageSOPClasses_.begin();
292 296 it != storageSOPClasses_.end(); ++it)
293 for (std::set<std::string>::const_iterator it = defaultStorageSOPClasses_.begin(); 297 {
294 it != defaultStorageSOPClasses_.end(); ++it) 298 RegisterStorageSOPClass(pimpl_->params_, presentationContextId,
295 { 299 *it, asPreferred, asFallback, remoteAet_);
296 RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 300 }
297 *it, asPreferred, asFallback, remoteAet_); 301
298 } 302 for (std::set<std::string>::const_iterator it = defaultStorageSOPClasses_.begin();
299 } 303 it != defaultStorageSOPClasses_.end(); ++it)
300 304 {
305 RegisterStorageSOPClass(pimpl_->params_, presentationContextId,
306 *it, asPreferred, asFallback, remoteAet_);
307 }
308
309 break;
310 }
311
312 case Mode_RequestStorageCommitment:
313 case Mode_ReportStorageCommitment:
314 {
315 const char* as = UID_StorageCommitmentPushModelSOPClass;
316
317 std::vector<const char*> ts;
318 ts.push_back(UID_LittleEndianExplicitTransferSyntax);
319 ts.push_back(UID_LittleEndianImplicitTransferSyntax);
320
321 T_ASC_SC_ROLE role;
322 switch (mode)
323 {
324 case Mode_RequestStorageCommitment:
325 role = ASC_SC_ROLE_DEFAULT;
326 break;
327
328 case Mode_ReportStorageCommitment:
329 role = ASC_SC_ROLE_SCP;
330 break;
331
332 default:
333 throw OrthancException(ErrorCode_InternalError);
334 }
335
336 Check(ASC_addPresentationContext(pimpl_->params_, 1 /*presentationContextId*/,
337 as, &ts[0], ts.size(), role),
338 remoteAet_, "initializing");
339
340 break;
341 }
342
343 default:
344 throw OrthancException(ErrorCode_InternalError);
345 }
346 }
347
301 348
302 static bool IsGenericTransferSyntax(const std::string& syntax) 349 static bool IsGenericTransferSyntax(const std::string& syntax)
303 { 350 {
304 return (syntax == UID_LittleEndianExplicitTransferSyntax || 351 return (syntax == UID_LittleEndianExplicitTransferSyntax ||
305 syntax == UID_BigEndianExplicitTransferSyntax || 352 syntax == UID_BigEndianExplicitTransferSyntax ||
306 syntax == UID_LittleEndianImplicitTransferSyntax); 353 syntax == UID_LittleEndianImplicitTransferSyntax);
307 } 354 }
308 355
309 356
310 void DicomUserConnection::PImpl::Store(DcmInputStream& is, 357 void DicomUserConnection::PImpl::Store(std::string& sopClassUidOut,
358 std::string& sopInstanceUidOut,
359 DcmInputStream& is,
311 DicomUserConnection& connection, 360 DicomUserConnection& connection,
312 const std::string& moveOriginatorAET, 361 const std::string& moveOriginatorAET,
313 uint16_t moveOriginatorID) 362 uint16_t moveOriginatorID)
314 { 363 {
315 DcmFileFormat dcmff; 364 DcmFileFormat dcmff;
387 { 436 {
388 throw OrthancException(ErrorCode_NoSopClassOrInstance, 437 throw OrthancException(ErrorCode_NoSopClassOrInstance,
389 "Unable to determine the SOP class/instance for C-STORE with AET " + 438 "Unable to determine the SOP class/instance for C-STORE with AET " +
390 connection.remoteAet_); 439 connection.remoteAet_);
391 } 440 }
441
442 sopClassUidOut.assign(sopClass);
443 sopInstanceUidOut.assign(sopInstance);
392 444
393 // Figure out which of the accepted presentation contexts should be used 445 // Figure out which of the accepted presentation contexts should be used
394 int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass); 446 int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass);
395 if (presID == 0) 447 if (presID == 0)
396 { 448 {
420 request.MoveOriginatorID = moveOriginatorID; // The type DIC_US is an alias for uint16_t 472 request.MoveOriginatorID = moveOriginatorID; // The type DIC_US is an alias for uint16_t
421 request.opts |= O_STORE_MOVEORIGINATORID; 473 request.opts |= O_STORE_MOVEORIGINATORID;
422 } 474 }
423 475
424 // Finally conduct transmission of data 476 // Finally conduct transmission of data
425 T_DIMSE_C_StoreRSP rsp; 477 T_DIMSE_C_StoreRSP response;
426 DcmDataset* statusDetail = NULL; 478 DcmDataset* statusDetail = NULL;
427 Check(DIMSE_storeUser(assoc_, presID, &request, 479 Check(DIMSE_storeUser(assoc_, presID, &request,
428 NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL, 480 NULL, dcmff.getDataset(), /*progressCallback*/ NULL, NULL,
429 /*opt_blockMode*/ DIMSE_BLOCKING, /*opt_dimse_timeout*/ dimseTimeout_, 481 /*opt_blockMode*/ (dimseTimeout_ ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
430 &rsp, &statusDetail, NULL), 482 /*opt_dimse_timeout*/ dimseTimeout_,
483 &response, &statusDetail, NULL),
431 connection.remoteAet_, "C-STORE"); 484 connection.remoteAet_, "C-STORE");
432 485
433 if (statusDetail != NULL) 486 if (statusDetail != NULL)
434 { 487 {
435 delete statusDetail; 488 delete statusDetail;
489 }
490
491
492 /**
493 * New in Orthanc 1.6.0: Deal with failures during C-STORE.
494 * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_B.2.3.html#table_B.2-1
495 **/
496
497 if (response.DimseStatus != 0x0000 && // Success
498 response.DimseStatus != 0xB000 && // Warning - Coercion of Data Elements
499 response.DimseStatus != 0xB007 && // Warning - Data Set does not match SOP Class
500 response.DimseStatus != 0xB006) // Warning - Elements Discarded
501 {
502 char buf[16];
503 sprintf(buf, "%04X", response.DimseStatus);
504 throw OrthancException(ErrorCode_NetworkProtocol,
505 "C-STORE SCU to AET \"" + connection.remoteAet_ +
506 "\" has failed with DIMSE status 0x" + buf);
436 } 507 }
437 } 508 }
438 509
439 510
440 namespace 511 namespace
563 switch (manufacturer) 634 switch (manufacturer)
564 { 635 {
565 case ModalityManufacturer_GenericNoWildcardInDates: 636 case ModalityManufacturer_GenericNoWildcardInDates:
566 case ModalityManufacturer_GenericNoUniversalWildcard: 637 case ModalityManufacturer_GenericNoUniversalWildcard:
567 { 638 {
568 std::auto_ptr<DicomMap> fix(fields.Clone()); 639 std::unique_ptr<DicomMap> fix(fields.Clone());
569 640
570 std::set<DicomTag> tags; 641 std::set<DicomTag> tags;
571 fix->GetTags(tags); 642 fix->GetTags(tags);
572 643
573 for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) 644 for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
640 OFCondition cond = DIMSE_findUser(association, presID, &request, dataset, 711 OFCondition cond = DIMSE_findUser(association, presID, &request, dataset,
641 #if DCMTK_VERSION_NUMBER >= 364 712 #if DCMTK_VERSION_NUMBER >= 364
642 responseCount, 713 responseCount,
643 #endif 714 #endif
644 FindCallback, &payload, 715 FindCallback, &payload,
645 /*opt_blockMode*/ DIMSE_BLOCKING, 716 /*opt_blockMode*/ (dimseTimeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
646 /*opt_dimse_timeout*/ dimseTimeout, 717 /*opt_dimse_timeout*/ dimseTimeout,
647 &response, &statusDetail); 718 &response, &statusDetail);
648 719
649 if (statusDetail) 720 if (statusDetail)
650 { 721 {
651 delete statusDetail; 722 delete statusDetail;
652 } 723 }
653 724
654 Check(cond, remoteAet, "C-FIND"); 725 Check(cond, remoteAet, "C-FIND");
726
727
728 /**
729 * New in Orthanc 1.6.0: Deal with failures during C-FIND.
730 * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1
731 **/
732
733 if (response.DimseStatus != 0x0000 && // Success
734 response.DimseStatus != 0xFF00 && // Pending - Matches are continuing
735 response.DimseStatus != 0xFF01) // Pending - Matches are continuing
736 {
737 char buf[16];
738 sprintf(buf, "%04X", response.DimseStatus);
739 throw OrthancException(ErrorCode_NetworkProtocol,
740 "C-FIND SCU to AET \"" + remoteAet +
741 "\" has failed with DIMSE status 0x" + buf);
742 }
743
655 } 744 }
656 745
657 746
658 void DicomUserConnection::Find(DicomFindAnswers& result, 747 void DicomUserConnection::Find(DicomFindAnswers& result,
659 ResourceType level, 748 ResourceType level,
660 const DicomMap& originalFields, 749 const DicomMap& originalFields,
661 bool normalize) 750 bool normalize)
662 { 751 {
663 CheckIsOpen(); 752 CheckIsOpen();
664 753
665 std::auto_ptr<ParsedDicomFile> query; 754 std::unique_ptr<ParsedDicomFile> query;
666 755
667 if (normalize) 756 if (normalize)
668 { 757 {
669 DicomMap fields; 758 DicomMap fields;
670 NormalizeFindQuery(fields, level, originalFields); 759 NormalizeFindQuery(fields, level, originalFields);
701 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES"); 790 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
702 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; 791 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
703 break; 792 break;
704 793
705 case ResourceType_Instance: 794 case ResourceType_Instance:
706 clevel = "INSTANCE"; 795 clevel = "IMAGE";
707 if (manufacturer_ == ModalityManufacturer_ClearCanvas || 796 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
708 manufacturer_ == ModalityManufacturer_Dcm4Chee ||
709 manufacturer_ == ModalityManufacturer_GE)
710 {
711 // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>.
712 // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J
713 // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx
714 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
715 clevel = "IMAGE";
716 }
717 else
718 {
719 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "INSTANCE");
720 }
721
722 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; 797 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
723 break; 798 break;
724 799
725 default: 800 default:
726 throw OrthancException(ErrorCode_ParameterOutOfRange); 801 throw OrthancException(ErrorCode_ParameterOutOfRange);
787 ResourceType level, 862 ResourceType level,
788 const DicomMap& fields) 863 const DicomMap& fields)
789 { 864 {
790 CheckIsOpen(); 865 CheckIsOpen();
791 866
792 std::auto_ptr<ParsedDicomFile> query(ConvertQueryFields(fields, manufacturer_)); 867 std::unique_ptr<ParsedDicomFile> query(ConvertQueryFields(fields, manufacturer_));
793 DcmDataset* dataset = query->GetDcmtkObject().getDataset(); 868 DcmDataset* dataset = query->GetDcmtkObject().getDataset();
794 869
795 const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; 870 const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
796 switch (level) 871 switch (level)
797 { 872 {
806 case ResourceType_Series: 881 case ResourceType_Series:
807 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES"); 882 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
808 break; 883 break;
809 884
810 case ResourceType_Instance: 885 case ResourceType_Instance:
811 if (manufacturer_ == ModalityManufacturer_ClearCanvas || 886 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
812 manufacturer_ == ModalityManufacturer_Dcm4Chee ||
813 manufacturer_ == ModalityManufacturer_GE)
814 {
815 // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>.
816 // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J
817 // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx
818 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
819 }
820 else
821 {
822 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "INSTANCE");
823 }
824 break; 887 break;
825 888
826 default: 889 default:
827 throw OrthancException(ErrorCode_ParameterOutOfRange); 890 throw OrthancException(ErrorCode_ParameterOutOfRange);
828 } 891 }
846 T_DIMSE_C_MoveRSP response; 909 T_DIMSE_C_MoveRSP response;
847 DcmDataset* statusDetail = NULL; 910 DcmDataset* statusDetail = NULL;
848 DcmDataset* responseIdentifiers = NULL; 911 DcmDataset* responseIdentifiers = NULL;
849 OFCondition cond = DIMSE_moveUser(pimpl_->assoc_, presID, &request, dataset, 912 OFCondition cond = DIMSE_moveUser(pimpl_->assoc_, presID, &request, dataset,
850 NULL, NULL, 913 NULL, NULL,
851 /*opt_blockMode*/ DIMSE_BLOCKING, 914 /*opt_blockMode*/ (pimpl_->dimseTimeout_ ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
852 /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, 915 /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
853 pimpl_->net_, NULL, NULL, 916 pimpl_->net_, NULL, NULL,
854 &response, &statusDetail, &responseIdentifiers); 917 &response, &statusDetail, &responseIdentifiers);
855 918
856 if (statusDetail) 919 if (statusDetail)
862 { 925 {
863 delete responseIdentifiers; 926 delete responseIdentifiers;
864 } 927 }
865 928
866 Check(cond, remoteAet_, "C-MOVE"); 929 Check(cond, remoteAet_, "C-MOVE");
930
931
932 /**
933 * New in Orthanc 1.6.0: Deal with failures during C-MOVE.
934 * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2
935 **/
936
937 if (response.DimseStatus != 0x0000 && // Success
938 response.DimseStatus != 0xFF00) // Pending - Sub-operations are continuing
939 {
940 char buf[16];
941 sprintf(buf, "%04X", response.DimseStatus);
942 throw OrthancException(ErrorCode_NetworkProtocol,
943 "C-MOVE SCU to AET \"" + remoteAet_ +
944 "\" has failed with DIMSE status 0x" + buf);
945 }
867 } 946 }
868 947
869 948
870 void DicomUserConnection::ResetStorageSOPClasses() 949 void DicomUserConnection::ResetStorageSOPClasses()
871 { 950 {
1021 Close(); 1100 Close();
1022 remotePort_ = port; 1101 remotePort_ = port;
1023 } 1102 }
1024 } 1103 }
1025 1104
1026 void DicomUserConnection::Open() 1105 void DicomUserConnection::OpenInternal(Mode mode)
1027 { 1106 {
1028 if (IsOpen()) 1107 if (IsOpen())
1029 { 1108 {
1030 // Don't reopen the connection 1109 // Don't reopen the connection
1031 return; 1110 return;
1061 1140
1062 // Set various options 1141 // Set various options
1063 Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false), 1142 Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false),
1064 remoteAet_, "connecting"); 1143 remoteAet_, "connecting");
1065 1144
1066 SetupPresentationContexts(preferredTransferSyntax_); 1145 SetupPresentationContexts(mode, preferredTransferSyntax_);
1067 1146
1068 // Do the association 1147 // Do the association
1069 Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_), 1148 Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_),
1070 remoteAet_, "connecting"); 1149 remoteAet_, "connecting");
1071 1150
1105 bool DicomUserConnection::IsOpen() const 1184 bool DicomUserConnection::IsOpen() const
1106 { 1185 {
1107 return pimpl_->IsOpen(); 1186 return pimpl_->IsOpen();
1108 } 1187 }
1109 1188
1110 void DicomUserConnection::Store(const char* buffer, 1189 void DicomUserConnection::Store(std::string& sopClassUid /* out */,
1190 std::string& sopInstanceUid /* out */,
1191 const char* buffer,
1111 size_t size, 1192 size_t size,
1112 const std::string& moveOriginatorAET, 1193 const std::string& moveOriginatorAET,
1113 uint16_t moveOriginatorID) 1194 uint16_t moveOriginatorID)
1114 { 1195 {
1115 // Prepare an input stream for the memory buffer 1196 // Prepare an input stream for the memory buffer
1116 DcmInputBufferStream is; 1197 DcmInputBufferStream is;
1117 if (size > 0) 1198 if (size > 0)
1118 is.setBuffer(buffer, size); 1199 is.setBuffer(buffer, size);
1119 is.setEos(); 1200 is.setEos();
1120 1201
1121 pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID); 1202 pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID);
1122 } 1203 }
1123 1204
1124 void DicomUserConnection::Store(const std::string& buffer, 1205 void DicomUserConnection::Store(std::string& sopClassUid /* out */,
1206 std::string& sopInstanceUid /* out */,
1207 const std::string& buffer,
1125 const std::string& moveOriginatorAET, 1208 const std::string& moveOriginatorAET,
1126 uint16_t moveOriginatorID) 1209 uint16_t moveOriginatorID)
1127 { 1210 {
1128 if (buffer.size() > 0) 1211 if (buffer.size() > 0)
1129 Store(&buffer[0], buffer.size(), moveOriginatorAET, moveOriginatorID); 1212 Store(sopClassUid, sopInstanceUid, &buffer[0], buffer.size(),
1213 moveOriginatorAET, moveOriginatorID);
1130 else 1214 else
1131 Store(NULL, 0, moveOriginatorAET, moveOriginatorID); 1215 Store(sopClassUid, sopInstanceUid, NULL, 0, moveOriginatorAET, moveOriginatorID);
1132 } 1216 }
1133 1217
1134 void DicomUserConnection::StoreFile(const std::string& path, 1218 void DicomUserConnection::StoreFile(std::string& sopClassUid /* out */,
1219 std::string& sopInstanceUid /* out */,
1220 const std::string& path,
1135 const std::string& moveOriginatorAET, 1221 const std::string& moveOriginatorAET,
1136 uint16_t moveOriginatorID) 1222 uint16_t moveOriginatorID)
1137 { 1223 {
1138 // Prepare an input stream for the file 1224 // Prepare an input stream for the file
1139 DcmInputFileStream is(path.c_str()); 1225 DcmInputFileStream is(path.c_str());
1140 pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID); 1226 pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID);
1141 } 1227 }
1142 1228
1143 bool DicomUserConnection::Echo() 1229 bool DicomUserConnection::Echo()
1144 { 1230 {
1145 CheckIsOpen(); 1231 CheckIsOpen();
1146 DIC_US status; 1232 DIC_US status;
1147 Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++, 1233 Check(DIMSE_echoUser(pimpl_->assoc_, pimpl_->assoc_->nextMsgID++,
1148 /*opt_blockMode*/ DIMSE_BLOCKING, 1234 /*opt_blockMode*/ (pimpl_->dimseTimeout_ ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
1149 /*opt_dimse_timeout*/ pimpl_->dimseTimeout_, 1235 /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
1150 &status, NULL), remoteAet_, "C-ECHO"); 1236 &status, NULL), remoteAet_, "C-ECHO");
1151 return status == STATUS_Success; 1237 return status == STATUS_Success;
1152 } 1238 }
1153 1239
1263 } 1349 }
1264 else 1350 else
1265 { 1351 {
1266 dcmConnectionTimeout.set(seconds); 1352 dcmConnectionTimeout.set(seconds);
1267 pimpl_->dimseTimeout_ = seconds; 1353 pimpl_->dimseTimeout_ = seconds;
1268 pimpl_->acseTimeout_ = 10; // Timeout used during association negociation 1354 pimpl_->acseTimeout_ = seconds; // Timeout used during association negociation and ASC_releaseAssociation()
1269 } 1355 }
1270 } 1356 }
1271 1357
1272 1358
1273 void DicomUserConnection::DisableTimeout() 1359 void DicomUserConnection::DisableTimeout()
1276 * Global timeout (seconds) for connecting to remote hosts. 1362 * Global timeout (seconds) for connecting to remote hosts.
1277 * Default value is -1 which selects infinite timeout, i.e. blocking connect(). 1363 * Default value is -1 which selects infinite timeout, i.e. blocking connect().
1278 */ 1364 */
1279 dcmConnectionTimeout.set(-1); 1365 dcmConnectionTimeout.set(-1);
1280 pimpl_->dimseTimeout_ = 0; 1366 pimpl_->dimseTimeout_ = 0;
1281 pimpl_->acseTimeout_ = 10; // Timeout used during association negociation 1367 pimpl_->acseTimeout_ = 10; // Timeout used during association negociation and ASC_releaseAssociation()
1282 } 1368 }
1283 1369
1284 1370
1285 void DicomUserConnection::CheckStorageSOPClassesInvariant() const 1371 void DicomUserConnection::CheckStorageSOPClassesInvariant() const
1286 { 1372 {
1366 remoteAet_ == remote.GetApplicationEntityTitle() && 1452 remoteAet_ == remote.GetApplicationEntityTitle() &&
1367 remoteHost_ == remote.GetHost() && 1453 remoteHost_ == remote.GetHost() &&
1368 remotePort_ == remote.GetPortNumber() && 1454 remotePort_ == remote.GetPortNumber() &&
1369 manufacturer_ == remote.GetManufacturer()); 1455 manufacturer_ == remote.GetManufacturer());
1370 } 1456 }
1457
1458
1459 static void FillSopSequence(DcmDataset& dataset,
1460 const DcmTagKey& tag,
1461 const std::vector<std::string>& sopClassUids,
1462 const std::vector<std::string>& sopInstanceUids,
1463 const std::vector<StorageCommitmentFailureReason>& failureReasons,
1464 bool hasFailureReasons)
1465 {
1466 assert(sopClassUids.size() == sopInstanceUids.size() &&
1467 (hasFailureReasons ?
1468 failureReasons.size() == sopClassUids.size() :
1469 failureReasons.empty()));
1470
1471 if (sopInstanceUids.empty())
1472 {
1473 // Add an empty sequence
1474 if (!dataset.insertEmptyElement(tag).good())
1475 {
1476 throw OrthancException(ErrorCode_InternalError);
1477 }
1478 }
1479 else
1480 {
1481 for (size_t i = 0; i < sopClassUids.size(); i++)
1482 {
1483 std::unique_ptr<DcmItem> item(new DcmItem);
1484 if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, sopClassUids[i].c_str()).good() ||
1485 !item->putAndInsertString(DCM_ReferencedSOPInstanceUID, sopInstanceUids[i].c_str()).good() ||
1486 (hasFailureReasons &&
1487 !item->putAndInsertUint16(DCM_FailureReason, failureReasons[i]).good()) ||
1488 !dataset.insertSequenceItem(tag, item.release()).good())
1489 {
1490 throw OrthancException(ErrorCode_InternalError);
1491 }
1492 }
1493 }
1494 }
1495
1496
1497
1498
1499 void DicomUserConnection::ReportStorageCommitment(
1500 const std::string& transactionUid,
1501 const std::vector<std::string>& sopClassUids,
1502 const std::vector<std::string>& sopInstanceUids,
1503 const std::vector<StorageCommitmentFailureReason>& failureReasons)
1504 {
1505 if (sopClassUids.size() != sopInstanceUids.size() ||
1506 sopClassUids.size() != failureReasons.size())
1507 {
1508 throw OrthancException(ErrorCode_ParameterOutOfRange);
1509 }
1510
1511 if (IsOpen())
1512 {
1513 Close();
1514 }
1515
1516 std::vector<std::string> successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids;
1517 std::vector<StorageCommitmentFailureReason> failedReasons;
1518
1519 successSopClassUids.reserve(sopClassUids.size());
1520 successSopInstanceUids.reserve(sopClassUids.size());
1521 failedSopClassUids.reserve(sopClassUids.size());
1522 failedSopInstanceUids.reserve(sopClassUids.size());
1523 failedReasons.reserve(sopClassUids.size());
1524
1525 for (size_t i = 0; i < sopClassUids.size(); i++)
1526 {
1527 switch (failureReasons[i])
1528 {
1529 case StorageCommitmentFailureReason_Success:
1530 successSopClassUids.push_back(sopClassUids[i]);
1531 successSopInstanceUids.push_back(sopInstanceUids[i]);
1532 break;
1533
1534 case StorageCommitmentFailureReason_ProcessingFailure:
1535 case StorageCommitmentFailureReason_NoSuchObjectInstance:
1536 case StorageCommitmentFailureReason_ResourceLimitation:
1537 case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
1538 case StorageCommitmentFailureReason_ClassInstanceConflict:
1539 case StorageCommitmentFailureReason_DuplicateTransactionUID:
1540 failedSopClassUids.push_back(sopClassUids[i]);
1541 failedSopInstanceUids.push_back(sopInstanceUids[i]);
1542 failedReasons.push_back(failureReasons[i]);
1543 break;
1544
1545 default:
1546 {
1547 char buf[16];
1548 sprintf(buf, "%04xH", failureReasons[i]);
1549 throw OrthancException(ErrorCode_ParameterOutOfRange,
1550 "Unsupported failure reason for storage commitment: " + std::string(buf));
1551 }
1552 }
1553 }
1554
1555 try
1556 {
1557 OpenInternal(Mode_ReportStorageCommitment);
1558
1559 /**
1560 * N-EVENT-REPORT
1561 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
1562 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1
1563 *
1564 * Status code:
1565 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8
1566 **/
1567
1568 /**
1569 * Send the "EVENT_REPORT_RQ" request
1570 **/
1571
1572 LOG(INFO) << "Reporting modality \"" << remoteAet_
1573 << "\" about storage commitment transaction: " << transactionUid
1574 << " (" << successSopClassUids.size() << " successes, "
1575 << failedSopClassUids.size() << " failures)";
1576 const DIC_US messageId = pimpl_->assoc_->nextMsgID++;
1577
1578 {
1579 T_DIMSE_Message message;
1580 memset(&message, 0, sizeof(message));
1581 message.CommandField = DIMSE_N_EVENT_REPORT_RQ;
1582
1583 T_DIMSE_N_EventReportRQ& content = message.msg.NEventReportRQ;
1584 content.MessageID = messageId;
1585 strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
1586 strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
1587 content.DataSetType = DIMSE_DATASET_PRESENT;
1588
1589 DcmDataset dataset;
1590 if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good())
1591 {
1592 throw OrthancException(ErrorCode_InternalError);
1593 }
1594
1595 {
1596 std::vector<StorageCommitmentFailureReason> empty;
1597 FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids,
1598 successSopInstanceUids, empty, false);
1599 }
1600
1601 // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
1602 if (failedSopClassUids.empty())
1603 {
1604 content.EventTypeID = 1; // "Storage Commitment Request Successful"
1605 }
1606 else
1607 {
1608 content.EventTypeID = 2; // "Storage Commitment Request Complete - Failures Exist"
1609
1610 // Failure reason
1611 // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
1612 FillSopSequence(dataset, DCM_FailedSOPSequence, failedSopClassUids,
1613 failedSopInstanceUids, failedReasons, true);
1614 }
1615
1616 int presID = ASC_findAcceptedPresentationContextID(
1617 pimpl_->assoc_, UID_StorageCommitmentPushModelSOPClass);
1618 if (presID == 0)
1619 {
1620 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
1621 "Unable to send N-EVENT-REPORT request to AET: " + remoteAet_);
1622 }
1623
1624 if (!DIMSE_sendMessageUsingMemoryData(
1625 pimpl_->assoc_, presID, &message, NULL /* status detail */,
1626 &dataset, NULL /* callback */, NULL /* callback context */,
1627 NULL /* commandSet */).good())
1628 {
1629 throw OrthancException(ErrorCode_NetworkProtocol);
1630 }
1631 }
1632
1633 /**
1634 * Read the "EVENT_REPORT_RSP" response
1635 **/
1636
1637 {
1638 T_ASC_PresentationContextID presID = 0;
1639 T_DIMSE_Message message;
1640
1641 const int timeout = pimpl_->dimseTimeout_;
1642 if (!DIMSE_receiveCommand(pimpl_->assoc_,
1643 (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout,
1644 &presID, &message, NULL /* no statusDetail */).good() ||
1645 message.CommandField != DIMSE_N_EVENT_REPORT_RSP)
1646 {
1647 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
1648 "Unable to read N-EVENT-REPORT response from AET: " + remoteAet_);
1649 }
1650
1651 const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP;
1652 if (content.MessageIDBeingRespondedTo != messageId ||
1653 !(content.opts & O_NEVENTREPORT_AFFECTEDSOPCLASSUID) ||
1654 !(content.opts & O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID) ||
1655 //(content.opts & O_NEVENTREPORT_EVENTTYPEID) || // Pedantic test - The "content.EventTypeID" is not used by Orthanc
1656 std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
1657 std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance ||
1658 content.DataSetType != DIMSE_DATASET_NULL)
1659 {
1660 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
1661 "Badly formatted N-EVENT-REPORT response from AET: " + remoteAet_);
1662 }
1663
1664 if (content.DimseStatus != 0 /* success */)
1665 {
1666 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
1667 "The request cannot be handled by remote AET: " + remoteAet_);
1668 }
1669 }
1670
1671 Close();
1672 }
1673 catch (OrthancException&)
1674 {
1675 Close();
1676 throw;
1677 }
1678 }
1679
1680
1681
1682 void DicomUserConnection::RequestStorageCommitment(
1683 const std::string& transactionUid,
1684 const std::vector<std::string>& sopClassUids,
1685 const std::vector<std::string>& sopInstanceUids)
1686 {
1687 if (sopClassUids.size() != sopInstanceUids.size())
1688 {
1689 throw OrthancException(ErrorCode_ParameterOutOfRange);
1690 }
1691
1692 for (size_t i = 0; i < sopClassUids.size(); i++)
1693 {
1694 if (sopClassUids[i].empty() ||
1695 sopInstanceUids[i].empty())
1696 {
1697 throw OrthancException(ErrorCode_ParameterOutOfRange,
1698 "The SOP class/instance UIDs cannot be empty, found: \"" +
1699 sopClassUids[i] + "\" / \"" + sopInstanceUids[i] + "\"");
1700 }
1701 }
1702
1703 if (transactionUid.size() < 5 ||
1704 transactionUid.substr(0, 5) != "2.25.")
1705 {
1706 throw OrthancException(ErrorCode_ParameterOutOfRange);
1707 }
1708
1709 if (IsOpen())
1710 {
1711 Close();
1712 }
1713
1714 try
1715 {
1716 OpenInternal(Mode_RequestStorageCommitment);
1717
1718 /**
1719 * N-ACTION
1720 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html
1721 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4
1722 *
1723 * Status code:
1724 * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8
1725 **/
1726
1727 /**
1728 * Send the "N_ACTION_RQ" request
1729 **/
1730
1731 LOG(INFO) << "Request to modality \"" << remoteAet_
1732 << "\" about storage commitment for " << sopClassUids.size()
1733 << " instances, with transaction UID: " << transactionUid;
1734 const DIC_US messageId = pimpl_->assoc_->nextMsgID++;
1735
1736 {
1737 T_DIMSE_Message message;
1738 memset(&message, 0, sizeof(message));
1739 message.CommandField = DIMSE_N_ACTION_RQ;
1740
1741 T_DIMSE_N_ActionRQ& content = message.msg.NActionRQ;
1742 content.MessageID = messageId;
1743 strncpy(content.RequestedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
1744 strncpy(content.RequestedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
1745 content.ActionTypeID = 1; // "Request Storage Commitment"
1746 content.DataSetType = DIMSE_DATASET_PRESENT;
1747
1748 DcmDataset dataset;
1749 if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good())
1750 {
1751 throw OrthancException(ErrorCode_InternalError);
1752 }
1753
1754 {
1755 std::vector<StorageCommitmentFailureReason> empty;
1756 FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, empty, false);
1757 }
1758
1759 int presID = ASC_findAcceptedPresentationContextID(
1760 pimpl_->assoc_, UID_StorageCommitmentPushModelSOPClass);
1761 if (presID == 0)
1762 {
1763 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
1764 "Unable to send N-ACTION request to AET: " + remoteAet_);
1765 }
1766
1767 if (!DIMSE_sendMessageUsingMemoryData(
1768 pimpl_->assoc_, presID, &message, NULL /* status detail */,
1769 &dataset, NULL /* callback */, NULL /* callback context */,
1770 NULL /* commandSet */).good())
1771 {
1772 throw OrthancException(ErrorCode_NetworkProtocol);
1773 }
1774 }
1775
1776 /**
1777 * Read the "N_ACTION_RSP" response
1778 **/
1779
1780 {
1781 T_ASC_PresentationContextID presID = 0;
1782 T_DIMSE_Message message;
1783
1784 const int timeout = pimpl_->dimseTimeout_;
1785 if (!DIMSE_receiveCommand(pimpl_->assoc_,
1786 (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout,
1787 &presID, &message, NULL /* no statusDetail */).good() ||
1788 message.CommandField != DIMSE_N_ACTION_RSP)
1789 {
1790 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
1791 "Unable to read N-ACTION response from AET: " + remoteAet_);
1792 }
1793
1794 const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP;
1795 if (content.MessageIDBeingRespondedTo != messageId ||
1796 !(content.opts & O_NACTION_AFFECTEDSOPCLASSUID) ||
1797 !(content.opts & O_NACTION_AFFECTEDSOPINSTANCEUID) ||
1798 //(content.opts & O_NACTION_ACTIONTYPEID) || // Pedantic test - The "content.ActionTypeID" is not used by Orthanc
1799 std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
1800 std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance ||
1801 content.DataSetType != DIMSE_DATASET_NULL)
1802 {
1803 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
1804 "Badly formatted N-ACTION response from AET: " + remoteAet_);
1805 }
1806
1807 if (content.DimseStatus != 0 /* success */)
1808 {
1809 throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
1810 "The request cannot be handled by remote AET: " + remoteAet_);
1811 }
1812 }
1813
1814 Close();
1815 }
1816 catch (OrthancException&)
1817 {
1818 Close();
1819 throw;
1820 }
1821 }
1371 } 1822 }