Mercurial > hg > orthanc
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 } |