Mercurial > hg > orthanc
diff OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp @ 4044:d25f4c0fa160 framework
splitting code into OrthancFramework and OrthancServer
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 10 Jun 2020 20:30:34 +0200 |
parents | Core/DicomNetworking/DicomAssociation.cpp@ea1d32861cfc |
children | bf7b9edf6b81 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp Wed Jun 10 20:30:34 2020 +0200 @@ -0,0 +1,860 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2020 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeaders.h" +#include "DicomAssociation.h" + +#if !defined(DCMTK_VERSION_NUMBER) +# error The macro DCMTK_VERSION_NUMBER must be defined +#endif + +#include "../Compatibility.h" +#include "../Logging.h" +#include "../OrthancException.h" +#include "NetworkingCompatibility.h" + +#include <dcmtk/dcmnet/diutil.h> // For dcmConnectionTimeout() +#include <dcmtk/dcmdata/dcdeftag.h> + +namespace Orthanc +{ + static void FillSopSequence(DcmDataset& dataset, + const DcmTagKey& tag, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::vector<StorageCommitmentFailureReason>& 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<DcmItem> 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); + } + } + } + } + + + void DicomAssociation::Initialize() + { + role_ = DicomAssociationRole_Default; + isOpen_ = false; + net_ = NULL; + params_ = NULL; + assoc_ = NULL; + + // Must be after "isOpen_ = false" + ClearPresentationContexts(); + } + + + void DicomAssociation::CheckConnecting(const DicomAssociationParameters& parameters, + const OFCondition& cond) + { + try + { + CheckCondition(cond, parameters, "connecting"); + } + catch (OrthancException&) + { + CloseInternal(); + throw; + } + } + + + void DicomAssociation::CloseInternal() + { + if (assoc_ != NULL) + { + ASC_releaseAssociation(assoc_); + ASC_destroyAssociation(&assoc_); + assoc_ = NULL; + params_ = NULL; + } + else + { + if (params_ != NULL) + { + ASC_destroyAssociationParameters(¶ms_); + params_ = NULL; + } + } + + if (net_ != NULL) + { + ASC_dropNetwork(&net_); + net_ = NULL; + } + + accepted_.clear(); + isOpen_ = false; + } + + + void DicomAssociation::AddAccepted(const std::string& abstractSyntax, + DicomTransferSyntax syntax, + uint8_t presentationContextId) + { + AcceptedPresentationContexts::iterator found = accepted_.find(abstractSyntax); + + if (found == accepted_.end()) + { + std::map<DicomTransferSyntax, uint8_t> syntaxes; + syntaxes[syntax] = presentationContextId; + accepted_[abstractSyntax] = syntaxes; + } + else + { + if (found->second.find(syntax) != found->second.end()) + { + LOG(WARNING) << "The same transfer syntax (" + << GetTransferSyntaxUid(syntax) + << ") was accepted twice for the same abstract syntax UID (" + << abstractSyntax << ")"; + } + else + { + found->second[syntax] = presentationContextId; + } + } + } + + + DicomAssociation::~DicomAssociation() + { + try + { + Close(); + } + catch (OrthancException& e) + { + // Don't throw exception in destructors + LOG(ERROR) << "Error while destroying a DICOM association: " << e.What(); + } + } + + + void DicomAssociation::SetRole(DicomAssociationRole role) + { + if (role_ != role) + { + Close(); + role_ = role; + } + } + + + void DicomAssociation::ClearPresentationContexts() + { + Close(); + proposed_.clear(); + proposed_.reserve(MAX_PROPOSED_PRESENTATIONS); + } + + + void DicomAssociation::Open(const DicomAssociationParameters& parameters) + { + if (isOpen_) + { + return; // Already open + } + + // Timeout used during association negociation and ASC_releaseAssociation() + uint32_t acseTimeout = parameters.GetTimeout(); + if (acseTimeout == 0) + { + /** + * Timeout is disabled. Global timeout (seconds) for + * connecting to remote hosts. Default value is -1 which + * selects infinite timeout, i.e. blocking connect(). + **/ + dcmConnectionTimeout.set(-1); + acseTimeout = 10; + } + else + { + dcmConnectionTimeout.set(acseTimeout); + } + + T_ASC_SC_ROLE dcmtkRole; + switch (role_) + { + case DicomAssociationRole_Default: + dcmtkRole = ASC_SC_ROLE_DEFAULT; + break; + + case DicomAssociationRole_Scu: + dcmtkRole = ASC_SC_ROLE_SCU; + break; + + case DicomAssociationRole_Scp: + dcmtkRole = ASC_SC_ROLE_SCP; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + assert(net_ == NULL && + params_ == NULL && + assoc_ == NULL); + + if (proposed_.empty()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "No presentation context was proposed"); + } + + LOG(INFO) << "Opening a DICOM SCU connection from AET \"" + << parameters.GetLocalApplicationEntityTitle() + << "\" to AET \"" << parameters.GetRemoteModality().GetApplicationEntityTitle() + << "\" on host " << parameters.GetRemoteModality().GetHost() + << ":" << parameters.GetRemoteModality().GetPortNumber() + << " (manufacturer: " << EnumerationToString(parameters.GetRemoteModality().GetManufacturer()) << ")"; + + CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_)); + CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU)); + + // Set this application's title and the called application's title in the params + CheckConnecting(parameters, ASC_setAPTitles( + params_, parameters.GetLocalApplicationEntityTitle().c_str(), + parameters.GetRemoteModality().GetApplicationEntityTitle().c_str(), NULL)); + + // Set the network addresses of the local and remote entities + char localHost[HOST_NAME_MAX]; + gethostname(localHost, HOST_NAME_MAX - 1); + + char remoteHostAndPort[HOST_NAME_MAX]; + +#ifdef _MSC_VER + _snprintf +#else + snprintf +#endif + (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", + parameters.GetRemoteModality().GetHost().c_str(), + parameters.GetRemoteModality().GetPortNumber()); + + CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort)); + + // Set various options + CheckConnecting(parameters, ASC_setTransportLayerType(params_, /*opt_secureConnection*/ false)); + + // Setup the list of proposed presentation contexts + unsigned int presentationContextId = 1; + for (size_t i = 0; i < proposed_.size(); i++) + { + assert(presentationContextId <= 255); + const char* abstractSyntax = proposed_[i].abstractSyntax_.c_str(); + + const std::set<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_; + + std::vector<const char*> transferSyntaxes; + transferSyntaxes.reserve(source.size()); + + for (std::set<DicomTransferSyntax>::const_iterator + it = source.begin(); it != source.end(); ++it) + { + transferSyntaxes.push_back(GetTransferSyntaxUid(*it)); + } + + assert(!transferSyntaxes.empty()); + CheckConnecting(parameters, ASC_addPresentationContext( + params_, presentationContextId, abstractSyntax, + &transferSyntaxes[0], transferSyntaxes.size(), dcmtkRole)); + + presentationContextId += 2; + } + + // Do the association + CheckConnecting(parameters, ASC_requestAssociation(net_, params_, &assoc_)); + isOpen_ = true; + + // Inspect the accepted transfer syntaxes + LST_HEAD **l = ¶ms_->DULparams.acceptedPresentationContext; + if (*l != NULL) + { + DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l); + LST_Position(l, (LST_NODE*)pc); + while (pc) + { + if (pc->result == ASC_P_ACCEPTANCE) + { + DicomTransferSyntax transferSyntax; + if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax)) + { + AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID); + } + else + { + LOG(WARNING) << "Unknown transfer syntax received from AET \"" + << parameters.GetRemoteModality().GetApplicationEntityTitle() + << "\": " << pc->acceptedTransferSyntax; + } + } + + pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l); + } + } + + if (accepted_.empty()) + { + throw OrthancException(ErrorCode_NoPresentationContext, + "Unable to negotiate a presentation context with AET \"" + + parameters.GetRemoteModality().GetApplicationEntityTitle() + "\""); + } + } + + void DicomAssociation::Close() + { + if (isOpen_) + { + CloseInternal(); + } + } + + + bool DicomAssociation::LookupAcceptedPresentationContext(std::map<DicomTransferSyntax, uint8_t>& target, + const std::string& abstractSyntax) const + { + if (!IsOpen()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "Connection not opened"); + } + + AcceptedPresentationContexts::const_iterator found = accepted_.find(abstractSyntax); + + if (found == accepted_.end()) + { + return false; + } + else + { + target = found->second; + return true; + } + } + + + void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax) + { + std::set<DicomTransferSyntax> ts; + ts.insert(DicomTransferSyntax_LittleEndianImplicit); + ts.insert(DicomTransferSyntax_LittleEndianExplicit); + ts.insert(DicomTransferSyntax_BigEndianExplicit); // Retired + ProposePresentationContext(abstractSyntax, ts); + } + + + void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax, + DicomTransferSyntax transferSyntax) + { + std::set<DicomTransferSyntax> ts; + ts.insert(transferSyntax); + ProposePresentationContext(abstractSyntax, ts); + } + + + size_t DicomAssociation::GetRemainingPropositions() const + { + assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS); + return MAX_PROPOSED_PRESENTATIONS - proposed_.size(); + } + + + void DicomAssociation::ProposePresentationContext( + const std::string& abstractSyntax, + const std::set<DicomTransferSyntax>& transferSyntaxes) + { + if (transferSyntaxes.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "No transfer syntax provided"); + } + + if (proposed_.size() >= MAX_PROPOSED_PRESENTATIONS) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Too many proposed presentation contexts"); + } + + if (IsOpen()) + { + Close(); + } + + ProposedPresentationContext context; + context.abstractSyntax_ = abstractSyntax; + context.transferSyntaxes_ = transferSyntaxes; + + proposed_.push_back(context); + } + + + T_ASC_Association& DicomAssociation::GetDcmtkAssociation() const + { + if (isOpen_) + { + assert(assoc_ != NULL); + return *assoc_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "The connection is not open"); + } + } + + + T_ASC_Network& DicomAssociation::GetDcmtkNetwork() const + { + if (isOpen_) + { + assert(net_ != NULL); + return *net_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "The connection is not open"); + } + } + + + void DicomAssociation::CheckCondition(const OFCondition& cond, + const DicomAssociationParameters& parameters, + const std::string& command) + { + if (cond.bad()) + { + // Reformat the error message from DCMTK by turning multiline + // errors into a single line + + std::string s(cond.text()); + std::string info; + info.reserve(s.size()); + + bool isMultiline = false; + for (size_t i = 0; i < s.size(); i++) + { + if (s[i] == '\r') + { + // Ignore + } + else if (s[i] == '\n') + { + if (isMultiline) + { + info += "; "; + } + else + { + info += " ("; + isMultiline = true; + } + } + else + { + info.push_back(s[i]); + } + } + + if (isMultiline) + { + info += ")"; + } + + throw OrthancException(ErrorCode_NetworkProtocol, + "DicomAssociation - " + command + " to AET \"" + + parameters.GetRemoteModality().GetApplicationEntityTitle() + + "\": " + info); + } + } + + + void DicomAssociation::ReportStorageCommitment( + const DicomAssociationParameters& parameters, + const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& sopInstanceUids, + const std::vector<StorageCommitmentFailureReason>& failureReasons) + { + if (sopClassUids.size() != sopInstanceUids.size() || + sopClassUids.size() != failureReasons.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + + std::vector<std::string> successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids; + std::vector<StorageCommitmentFailureReason> 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<DicomTransferSyntax> 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.GetRemoteModality().GetApplicationEntityTitle() + << "\" 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<StorageCommitmentFailureReason> 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.GetRemoteModality().GetApplicationEntityTitle()); + } + + 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.GetRemoteModality().GetApplicationEntityTitle()); + } + + 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.GetRemoteModality().GetApplicationEntityTitle()); + } + + if (content.DimseStatus != 0 /* success */) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "The request cannot be handled by remote AET: " + + parameters.GetRemoteModality().GetApplicationEntityTitle()); + } + } + + association.Close(); + } + + + void DicomAssociation::RequestStorageCommitment( + const DicomAssociationParameters& parameters, + const std::string& transactionUid, + const std::vector<std::string>& sopClassUids, + const std::vector<std::string>& 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<DicomTransferSyntax> 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.GetRemoteModality().GetApplicationEntityTitle() + << "\" 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<StorageCommitmentFailureReason> 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.GetRemoteModality().GetApplicationEntityTitle()); + } + + 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.GetRemoteModality().GetApplicationEntityTitle()); + } + + 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.GetRemoteModality().GetApplicationEntityTitle()); + } + + if (content.DimseStatus != 0 /* success */) + { + throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " + "The request cannot be handled by remote AET: " + + parameters.GetRemoteModality().GetApplicationEntityTitle()); + } + } + + association.Close(); + } +}