view OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp @ 5911:bfae0fc2ea1b get-scu-test

Started to work on handling errors as warnings when trying to store instances whose SOPClassUID has not been accepted during the negotiation. Work to be finalized later
author Alain Mazy <am@orthanc.team>
date Mon, 09 Dec 2024 10:07:19 +0100
parents f622e5964cfa
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(&params_);
        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(&params_, parameters.GetMaximumPduLength(), acseTimeout));
#else
    // from 3.6.8, this version is obsolete
    CheckConnecting(parameters, ASC_createAssociationParameters(&params_, 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 = &params_->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();
  }
}