# HG changeset patch # User Sebastien Jodogne # Date 1586449782 -7200 # Node ID f2488b645f5fcb150a404d71e6f8866ecd4ba1c3 # Parent f89eac983c9b8618600dc3e0963d3597a6e143df adding storage commitment to DicomAssociation diff -r f89eac983c9b -r f2488b645f5f UnitTestsSources/FromDcmtkTests.cpp --- a/UnitTestsSources/FromDcmtkTests.cpp Thu Apr 09 17:45:25 2020 +0200 +++ b/UnitTestsSources/FromDcmtkTests.cpp Thu Apr 09 18:29:42 2020 +0200 @@ -2452,7 +2452,6 @@ std::string remoteHost_; uint16_t remotePort_; ModalityManufacturer manufacturer_; - DicomAssociationRole role_; uint32_t timeout_; void ReadDefaultTimeout() @@ -2467,8 +2466,7 @@ remoteAet_("ANY-SCP"), remoteHost_("127.0.0.1"), remotePort_(104), - manufacturer_(ModalityManufacturer_Generic), - role_(DicomAssociationRole_Default) + manufacturer_(ModalityManufacturer_Generic) { ReadDefaultTimeout(); } @@ -2480,7 +2478,6 @@ remoteHost_(remote.GetHost()), remotePort_(remote.GetPortNumber()), manufacturer_(remote.GetManufacturer()), - role_(DicomAssociationRole_Default), timeout_(defaultTimeout_) { ReadDefaultTimeout(); @@ -2511,11 +2508,6 @@ return manufacturer_; } - DicomAssociationRole GetRole() const - { - return role_; - } - void SetLocalApplicationEntityTitle(const std::string& aet) { localAet_ = aet; @@ -2547,11 +2539,6 @@ manufacturer_ = manufacturer; } - void SetRole(DicomAssociationRole role) - { - role_ = role; - } - void SetRemoteModality(const RemoteModalityParameters& parameters) { SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle()); @@ -2566,8 +2553,7 @@ remoteAet_ == other.remoteAet_ && remoteHost_ == other.remoteHost_ && remotePort_ == other.remotePort_ && - manufacturer_ == other.manufacturer_ && - role_ == other.role_); + manufacturer_ == other.manufacturer_); } void SetTimeout(uint32_t seconds) @@ -2646,6 +2632,44 @@ }; + static void FillSopSequence(DcmDataset& dataset, + const DcmTagKey& tag, + const std::vector& sopClassUids, + const std::vector& sopInstanceUids, + const std::vector& failureReasons, + bool hasFailureReasons) + { + assert(sopClassUids.size() == sopInstanceUids.size() && + (hasFailureReasons ? + failureReasons.size() == sopClassUids.size() : + failureReasons.empty())); + + if (sopInstanceUids.empty()) + { + // Add an empty sequence + if (!dataset.insertEmptyElement(tag).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + else + { + for (size_t i = 0; i < sopClassUids.size(); i++) + { + std::unique_ptr item(new DcmItem); + if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, sopClassUids[i].c_str()).good() || + !item->putAndInsertString(DCM_ReferencedSOPInstanceUID, sopInstanceUids[i].c_str()).good() || + (hasFailureReasons && + !item->putAndInsertUint16(DCM_FailureReason, failureReasons[i]).good()) || + !dataset.insertSequenceItem(tag, item.release()).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } + } + } + + class DicomAssociation : public boost::noncopyable { private: @@ -2662,6 +2686,7 @@ typedef std::map > AcceptedPresentationContexts; + DicomAssociationRole role_; bool isOpen_; std::vector proposed_; AcceptedPresentationContexts accepted_; @@ -2671,6 +2696,7 @@ void Initialize() { + role_ = DicomAssociationRole_Default; isOpen_ = false; net_ = NULL; params_ = NULL; @@ -2773,6 +2799,15 @@ return isOpen_; } + void SetRole(DicomAssociationRole role) + { + if (role_ != role) + { + Close(); + role_ = role; + } + } + void ClearPresentationContexts() { Close(); @@ -2805,7 +2840,7 @@ } T_ASC_SC_ROLE dcmtkRole; - switch (parameters.GetRole()) + switch (role_) { case DicomAssociationRole_Default: dcmtkRole = ASC_SC_ROLE_DEFAULT; @@ -3032,6 +3067,338 @@ "The connection is not open"); } } + + + static void ReportStorageCommitment(const DicomAssociationParameters& parameters, + const std::string& transactionUid, + const std::vector& sopClassUids, + const std::vector& sopInstanceUids, + const std::vector& failureReasons) + { + if (sopClassUids.size() != sopInstanceUids.size() || + sopClassUids.size() != failureReasons.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + + std::vector successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids; + std::vector failedReasons; + + successSopClassUids.reserve(sopClassUids.size()); + successSopInstanceUids.reserve(sopClassUids.size()); + failedSopClassUids.reserve(sopClassUids.size()); + failedSopInstanceUids.reserve(sopClassUids.size()); + failedReasons.reserve(sopClassUids.size()); + + for (size_t i = 0; i < sopClassUids.size(); i++) + { + switch (failureReasons[i]) + { + case StorageCommitmentFailureReason_Success: + successSopClassUids.push_back(sopClassUids[i]); + successSopInstanceUids.push_back(sopInstanceUids[i]); + break; + + case StorageCommitmentFailureReason_ProcessingFailure: + case StorageCommitmentFailureReason_NoSuchObjectInstance: + case StorageCommitmentFailureReason_ResourceLimitation: + case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported: + case StorageCommitmentFailureReason_ClassInstanceConflict: + case StorageCommitmentFailureReason_DuplicateTransactionUID: + failedSopClassUids.push_back(sopClassUids[i]); + failedSopInstanceUids.push_back(sopInstanceUids[i]); + failedReasons.push_back(failureReasons[i]); + break; + + default: + { + char buf[16]; + sprintf(buf, "%04xH", failureReasons[i]); + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Unsupported failure reason for storage commitment: " + std::string(buf)); + } + } + } + + DicomAssociation association; + + { + std::set transferSyntaxes; + transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit); + transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit); + + association.SetRole(DicomAssociationRole_Scp); + association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass, + transferSyntaxes); + } + + association.Open(parameters); + + /** + * N-EVENT-REPORT + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1 + * + * Status code: + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8 + **/ + + /** + * Send the "EVENT_REPORT_RQ" request + **/ + + LOG(INFO) << "Reporting modality \"" + << parameters.GetRemoteApplicationEntityTitle() + << "\" about storage commitment transaction: " << transactionUid + << " (" << successSopClassUids.size() << " successes, " + << failedSopClassUids.size() << " failures)"; + const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++; + + { + T_DIMSE_Message message; + memset(&message, 0, sizeof(message)); + message.CommandField = DIMSE_N_EVENT_REPORT_RQ; + + T_DIMSE_N_EventReportRQ& content = message.msg.NEventReportRQ; + content.MessageID = messageId; + strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); + strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); + content.DataSetType = DIMSE_DATASET_PRESENT; + + DcmDataset dataset; + if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + + { + std::vector empty; + FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids, + successSopInstanceUids, empty, false); + } + + // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html + if (failedSopClassUids.empty()) + { + content.EventTypeID = 1; // "Storage Commitment Request Successful" + } + else + { + content.EventTypeID = 2; // "Storage Commitment Request Complete - Failures Exist" + + // Failure reason + // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part03/sect_C.14.html#sect_C.14.1.1 + FillSopSequence(dataset, DCM_FailedSOPSequence, failedSopClassUids, + failedSopInstanceUids, failedReasons, true); + } + + int presID = ASC_findAcceptedPresentationContextID( + &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass); + if (presID == 0) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Unable to send N-EVENT-REPORT request to AET: " + + parameters.GetRemoteApplicationEntityTitle()); + } + + if (!DIMSE_sendMessageUsingMemoryData( + &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */, + &dataset, NULL /* callback */, NULL /* callback context */, + NULL /* commandSet */).good()) + { + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + + /** + * Read the "EVENT_REPORT_RSP" response + **/ + + { + T_ASC_PresentationContextID presID = 0; + T_DIMSE_Message message; + + if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(), + (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), + parameters.GetTimeout(), &presID, &message, + NULL /* no statusDetail */).good() || + message.CommandField != DIMSE_N_EVENT_REPORT_RSP) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Unable to read N-EVENT-REPORT response from AET: " + + parameters.GetRemoteApplicationEntityTitle()); + } + + const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP; + if (content.MessageIDBeingRespondedTo != messageId || + !(content.opts & O_NEVENTREPORT_AFFECTEDSOPCLASSUID) || + !(content.opts & O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID) || + //(content.opts & O_NEVENTREPORT_EVENTTYPEID) || // Pedantic test - The "content.EventTypeID" is not used by Orthanc + std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || + std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance || + content.DataSetType != DIMSE_DATASET_NULL) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Badly formatted N-EVENT-REPORT response from AET: " + + parameters.GetRemoteApplicationEntityTitle()); + } + + if (content.DimseStatus != 0 /* success */) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "The request cannot be handled by remote AET: " + + parameters.GetRemoteApplicationEntityTitle()); + } + } + + association.Close(); + } + + static void RequestStorageCommitment(const DicomAssociationParameters& parameters, + const std::string& transactionUid, + const std::vector& sopClassUids, + const std::vector& sopInstanceUids) + { + if (sopClassUids.size() != sopInstanceUids.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + for (size_t i = 0; i < sopClassUids.size(); i++) + { + if (sopClassUids[i].empty() || + sopInstanceUids[i].empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "The SOP class/instance UIDs cannot be empty, found: \"" + + sopClassUids[i] + "\" / \"" + sopInstanceUids[i] + "\""); + } + } + + if (transactionUid.size() < 5 || + transactionUid.substr(0, 5) != "2.25.") + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + DicomAssociation association; + + { + std::set transferSyntaxes; + transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit); + transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit); + + association.SetRole(DicomAssociationRole_Default); + association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass, + transferSyntaxes); + } + + association.Open(parameters); + + /** + * N-ACTION + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4 + * + * Status code: + * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8 + **/ + + /** + * Send the "N_ACTION_RQ" request + **/ + + LOG(INFO) << "Request to modality \"" + << parameters.GetRemoteApplicationEntityTitle() + << "\" about storage commitment for " << sopClassUids.size() + << " instances, with transaction UID: " << transactionUid; + const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++; + + { + T_DIMSE_Message message; + memset(&message, 0, sizeof(message)); + message.CommandField = DIMSE_N_ACTION_RQ; + + T_DIMSE_N_ActionRQ& content = message.msg.NActionRQ; + content.MessageID = messageId; + strncpy(content.RequestedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN); + strncpy(content.RequestedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN); + content.ActionTypeID = 1; // "Request Storage Commitment" + content.DataSetType = DIMSE_DATASET_PRESENT; + + DcmDataset dataset; + if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + + { + std::vector empty; + FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, empty, false); + } + + int presID = ASC_findAcceptedPresentationContextID( + &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass); + if (presID == 0) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Unable to send N-ACTION request to AET: " + + parameters.GetRemoteApplicationEntityTitle()); + } + + if (!DIMSE_sendMessageUsingMemoryData( + &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */, + &dataset, NULL /* callback */, NULL /* callback context */, + NULL /* commandSet */).good()) + { + throw OrthancException(ErrorCode_NetworkProtocol); + } + } + + /** + * Read the "N_ACTION_RSP" response + **/ + + { + T_ASC_PresentationContextID presID = 0; + T_DIMSE_Message message; + + if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(), + (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), + parameters.GetTimeout(), &presID, &message, + NULL /* no statusDetail */).good() || + message.CommandField != DIMSE_N_ACTION_RSP) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Unable to read N-ACTION response from AET: " + + parameters.GetRemoteApplicationEntityTitle()); + } + + const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP; + if (content.MessageIDBeingRespondedTo != messageId || + !(content.opts & O_NACTION_AFFECTEDSOPCLASSUID) || + !(content.opts & O_NACTION_AFFECTEDSOPINSTANCEUID) || + //(content.opts & O_NACTION_ACTIONTYPEID) || // Pedantic test - The "content.ActionTypeID" is not used by Orthanc + std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass || + std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance || + content.DataSetType != DIMSE_DATASET_NULL) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "Badly formatted N-ACTION response from AET: " + + parameters.GetRemoteApplicationEntityTitle()); + } + + if (content.DimseStatus != 0 /* success */) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "The request cannot be handled by remote AET: " + + parameters.GetRemoteApplicationEntityTitle()); + } + } + + association.Close(); + } }; @@ -3668,7 +4035,6 @@ FindInternal(result, dataset, sopClass, true, NULL); } }; - }