Mercurial > hg > orthanc
view OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp @ 5899:f622e5964cfa get-scu
Get-SCU: proposed TS + Get-SCP: accept more transcoding
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Tue, 03 Dec 2024 15:19:55 +0100 |
parents | ed74c56db02f |
children |
line wrap: on
line source
/** * Orthanc - A Lightweight, RESTful DICOM Store * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium * Copyright (C) 2017-2023 Osimis S.A., Belgium * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser 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" #ifdef _WIN32 # include <winsock.h> #endif #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::CheckConnecting(const DicomAssociationParameters& parameters, const OFCondition& cond) { try { if (cond.bad() && cond == DUL_ASSOCIATIONREJECTED) { T_ASC_RejectParameters rej; ASC_getRejectParameters(params_, &rej); OFString str; CLOG(TRACE, DICOM) << "Association Rejected:" << std::endl << ASC_printRejectParameters(str, &rej); } CheckCondition(cond, parameters, "connecting"); } catch (OrthancException&) { CloseInternal(); throw; } } void DicomAssociation::CloseInternal() { CLOG(INFO, DICOM) << "Closing DICOM association"; #if ORTHANC_ENABLE_SSL == 1 tls_.reset(NULL); // Transport layer must be destroyed before the association itself #endif 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()) { CLOG(WARNING, DICOM) << "The same transfer syntax (" << GetTransferSyntaxUid(syntax) << ") was accepted twice for the same abstract syntax UID (" << abstractSyntax << ")"; } else { found->second[syntax] = presentationContextId; } } } DicomAssociation::DicomAssociation() { isOpen_ = false; net_ = NULL; params_ = NULL; assoc_ = NULL; // Must be after "isOpen_ = false" ClearPresentationContexts(); } DicomAssociation::~DicomAssociation() { try { Close(); } catch (OrthancException& e) { // Don't throw exception in destructors CLOG(ERROR, DICOM) << "Error while destroying a DICOM association: " << e.What(); } } void DicomAssociation::ClearPresentationContexts() { Close(); proposed_.clear(); proposed_.reserve(MAX_PROPOSED_PRESENTATIONS); } static T_ASC_SC_ROLE GetDcmtkRole(DicomAssociationRole role) { switch (role) { case DicomAssociationRole_Default: return ASC_SC_ROLE_DEFAULT; case DicomAssociationRole_Scu: return ASC_SC_ROLE_SCU; case DicomAssociationRole_Scp: return ASC_SC_ROLE_SCP; default: throw OrthancException(ErrorCode_ParameterOutOfRange); } } 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); } assert(net_ == NULL && params_ == NULL && assoc_ == NULL); #if ORTHANC_ENABLE_SSL == 1 assert(tls_.get() == NULL); #endif if (proposed_.empty()) { throw OrthancException(ErrorCode_BadSequenceOfCalls, "No presentation context was proposed"); } std::string localAet = parameters.GetLocalApplicationEntityTitle(); if (parameters.GetRemoteModality().HasLocalAet()) { localAet = parameters.GetRemoteModality().GetLocalAet(); } CLOG(INFO, DICOM) << "Opening a DICOM SCU connection " << (parameters.GetRemoteModality().IsDicomTlsEnabled() ? "using DICOM TLS" : "without DICOM TLS") << " from AET \"" << localAet << "\" to AET \"" << parameters.GetRemoteModality().GetApplicationEntityTitle() << "\" on host " << parameters.GetRemoteModality().GetHost() << ":" << parameters.GetRemoteModality().GetPortNumber() << " (manufacturer: " << EnumerationToString(parameters.GetRemoteModality().GetManufacturer()) << ", " << (parameters.HasTimeout() ? "timeout: " + boost::lexical_cast<std::string>(parameters.GetTimeout()) + "s" : "no timeout") << ")"; CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_)); #if DCMTK_VERSION_NUMBER >= 368 CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, parameters.GetMaximumPduLength(), acseTimeout)); #else // from 3.6.8, this version is obsolete CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, parameters.GetMaximumPduLength())); #endif #if ORTHANC_ENABLE_SSL == 1 if (parameters.GetRemoteModality().IsDicomTlsEnabled()) { try { assert(net_ != NULL && params_ != NULL); tls_.reset(Internals::InitializeDicomTls(net_, NET_REQUESTOR, parameters.GetOwnPrivateKeyPath(), parameters.GetOwnCertificatePath(), parameters.GetTrustedCertificatesPath(), parameters.IsRemoteCertificateRequired(), parameters.GetMinimumTlsVersion(), parameters.GetAcceptedCiphers())); } catch (OrthancException&) { CloseInternal(); throw; } } #endif // Set this application's title and the called application's title in the params CheckConnecting(parameters, ASC_setAPTitles( params_, localAet.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 #if ORTHANC_ENABLE_SSL == 1 CheckConnecting(parameters, ASC_setTransportLayerType(params_, (tls_.get() != NULL) /*opt_secureConnection*/)); #else CheckConnecting(parameters, ASC_setTransportLayerType(params_, false /*opt_secureConnection*/)); #endif // 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::list<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_; std::vector<const char*> transferSyntaxes; transferSyntaxes.reserve(source.size()); for (std::list<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(), GetDcmtkRole(proposed_[i].role_))); presentationContextId += 2; } { OFString str; CLOG(TRACE, DICOM) << "Request Parameters:" << std::endl << ASC_dumpParameters(str, params_, ASC_ASSOC_RQ); } // Do the association CheckConnecting(parameters, ASC_requestAssociation(net_, params_, &assoc_)); isOpen_ = true; { OFString str; CLOG(TRACE, DICOM) << "Connection Parameters: " << ASC_dumpConnectionParameters(str, assoc_); CLOG(TRACE, DICOM) << "Association Parameters Negotiated:" << std::endl << ASC_dumpParameters(str, params_, ASC_ASSOC_AC); } // 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 && strlen(pc->abstractSyntax) > 0) { CLOG(TRACE, DICOM) << "DicomAssociation::Open, adding SOPClassUID " << pc->abstractSyntax << " - TS " << pc->acceptedTransferSyntax << " - PC ID " << boost::lexical_cast<std::string>(static_cast<int>(pc->presentationContextID)); DicomTransferSyntax transferSyntax; if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax)) { AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID); } else { CLOG(WARNING, DICOM) << "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, DicomAssociationRole role) { std::list<DicomTransferSyntax> ts; ts.push_back(DicomTransferSyntax_LittleEndianExplicit); // the most standard one first ! ts.push_back(DicomTransferSyntax_LittleEndianImplicit); ts.push_back(DicomTransferSyntax_BigEndianExplicit); // Retired but was historicaly proposed by Orthanc ProposePresentationContext(abstractSyntax, ts, role); } void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax) { ProposeGenericPresentationContext(abstractSyntax, DicomAssociationRole_Default); } void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax, DicomTransferSyntax transferSyntax) { ProposePresentationContext(abstractSyntax, transferSyntax, DicomAssociationRole_Default); } void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax, DicomTransferSyntax transferSyntax, DicomAssociationRole role) { std::list<DicomTransferSyntax> ts; ts.push_back(transferSyntax); ProposePresentationContext(abstractSyntax, ts, role); } 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::list<DicomTransferSyntax>& transferSyntaxes) { ProposePresentationContext(abstractSyntax, transferSyntaxes, DicomAssociationRole_Default); } void DicomAssociation::ProposePresentationContext( const std::string& abstractSyntax, const std::list<DicomTransferSyntax>& transferSyntaxes, DicomAssociationRole role) { 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; context.role_ = role; 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"); } } bool DicomAssociation::GetAssociationParameters(std::string& remoteAet, std::string& remoteIp, std::string& calledAet) const { T_ASC_Association& dcmtkAssoc = GetDcmtkAssociation(); DIC_AE remoteAet_C; DIC_AE calledAet_C; DIC_AE remoteIp_C; DIC_AE calledIP_C; if ( #if DCMTK_VERSION_NUMBER >= 364 ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).good() && ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).good() #else ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, calledAet_C, NULL).good() && ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, calledIP_C).good() #endif ) { remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C)); remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C)); calledAet = (/*OFSTRING_GUARD*/(calledAet_C)); return true; } return false; } 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::list<DicomTransferSyntax> transferSyntaxes; transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit); transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit); association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass, transferSyntaxes, DicomAssociationRole_Scp); } 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 **/ CLOG(INFO, DICOM) << "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()); } { std::stringstream s; // DcmObject::PrintHelper cannot be used with VS2008 dataset.print(s); OFString str; CLOG(TRACE, DICOM) << "Sending Storage Commitment Report:" << std::endl << DIMSE_dumpMessage(str, message, DIMSE_OUTGOING) << std::endl << s.str(); } 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()); } { OFString str; CLOG(TRACE, DICOM) << "Received Storage Commitment Report Response:" << std::endl << DIMSE_dumpMessage(str, message, DIMSE_INCOMING, NULL, presID); } 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::list<DicomTransferSyntax> transferSyntaxes; transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit); transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit); // association.SetRole(DicomAssociationRole_Default); association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass, transferSyntaxes, DicomAssociationRole_Default); } 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 **/ CLOG(INFO, DICOM) << "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()); } { std::stringstream s; // DcmObject::PrintHelper cannot be used with VS2008 dataset.print(s); OFString str; CLOG(TRACE, DICOM) << "Sending Storage Commitment Request:" << std::endl << DIMSE_dumpMessage(str, message, DIMSE_OUTGOING) << std::endl << s.str(); } 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()); } { OFString str; CLOG(TRACE, DICOM) << "Received Storage Commitment Request Response:" << std::endl << DIMSE_dumpMessage(str, message, DIMSE_INCOMING, NULL, presID); } if (content.DimseStatus != 0 /* success */) { throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - " "The request cannot be handled by remote AET: " + parameters.GetRemoteModality().GetApplicationEntityTitle()); } } association.Close(); } }