changeset 3827:638906dcfe32 transcoding

integration mainline->transcoding
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 10 Apr 2020 16:18:17 +0200
parents 6762506ef4fb (current diff) e82bd07c384e (diff)
children 4fde7933e504
files Core/DicomNetworking/DicomStoreUserConnection.h Core/Enumerations.h UnitTestsSources/FromDcmtkTests.cpp
diffstat 12 files changed, 2475 insertions(+), 1929 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomAssociation.cpp	Fri Apr 10 16:18:17 2020 +0200
@@ -0,0 +1,856 @@
+/**
+ * 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 "../Logging.h"
+#include "../OrthancException.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(&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())
+      {
+        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&)
+    {
+      // Don't throw exception in destructors
+    }
+  }
+
+
+  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.GetRemoteApplicationEntityTitle()
+              << "\" on host " << parameters.GetRemoteHost()
+              << ":" << parameters.GetRemotePort() 
+              << " (manufacturer: " << EnumerationToString(parameters.GetRemoteManufacturer()) << ")";
+
+    CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_));
+    CheckConnecting(parameters, ASC_createAssociationParameters(&params_, /*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.GetRemoteApplicationEntityTitle().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.GetRemoteHost().c_str(), parameters.GetRemotePort());
+
+    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 = &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)
+        {
+          DicomTransferSyntax transferSyntax;
+          if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
+          {
+            AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID);
+          }
+          else
+          {
+            LOG(WARNING) << "Unknown transfer syntax received from AET \""
+                         << parameters.GetRemoteApplicationEntityTitle()
+                         << "\": " << pc->acceptedTransferSyntax;
+          }
+        }
+            
+        pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l);
+      }
+    }
+
+    if (accepted_.empty())
+    {
+      throw OrthancException(ErrorCode_NoPresentationContext,
+                             "Unable to negotiate a presentation context with AET \"" +
+                             parameters.GetRemoteApplicationEntityTitle() + "\"");
+    }
+  }
+
+  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,
+                             "DicomUserConnection - " + command + " to AET \"" +
+                             parameters.GetRemoteApplicationEntityTitle() +
+                             "\": " + 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.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<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.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();
+  }
+
+    
+  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.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<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.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();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomAssociation.h	Fri Apr 10 16:18:17 2020 +0200
@@ -0,0 +1,143 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1
+#endif
+
+#include "DicomAssociationParameters.h"
+
+#include <dcmtk/dcmnet/dimse.h>
+
+#include <stdint.h>   // For uint8_t
+#include <boost/noncopyable.hpp>
+#include <set>
+
+namespace Orthanc
+{
+  class DicomAssociation : public boost::noncopyable
+  {
+  private:
+    // This is the maximum number of presentation context IDs (the
+    // number of odd integers between 1 and 255)
+    // http://dicom.nema.org/medical/dicom/2019e/output/chtml/part08/sect_9.3.2.2.html
+    static const size_t MAX_PROPOSED_PRESENTATIONS = 128;
+    
+    struct ProposedPresentationContext
+    {
+      std::string                    abstractSyntax_;
+      std::set<DicomTransferSyntax>  transferSyntaxes_;
+    };
+
+    typedef std::map<std::string, std::map<DicomTransferSyntax, uint8_t> >
+    AcceptedPresentationContexts;
+
+    DicomAssociationRole                      role_;
+    bool                                      isOpen_;
+    std::vector<ProposedPresentationContext>  proposed_;
+    AcceptedPresentationContexts              accepted_;
+    T_ASC_Network*                            net_;
+    T_ASC_Parameters*                         params_;
+    T_ASC_Association*                        assoc_;
+
+    void Initialize();
+
+    void CheckConnecting(const DicomAssociationParameters& parameters,
+                         const OFCondition& cond);
+    
+    void CloseInternal();
+
+    void AddAccepted(const std::string& abstractSyntax,
+                     DicomTransferSyntax syntax,
+                     uint8_t presentationContextId);
+
+  public:
+    DicomAssociation()
+    {
+      Initialize();
+    }
+
+    ~DicomAssociation();
+
+    bool IsOpen() const
+    {
+      return isOpen_;
+    }
+
+    void SetRole(DicomAssociationRole role);
+
+    void ClearPresentationContexts();
+
+    void Open(const DicomAssociationParameters& parameters);
+    
+    void Close();
+
+    bool LookupAcceptedPresentationContext(
+      std::map<DicomTransferSyntax, uint8_t>& target,
+      const std::string& abstractSyntax) const;
+
+    void ProposeGenericPresentationContext(const std::string& abstractSyntax);
+
+    void ProposePresentationContext(const std::string& abstractSyntax,
+                                    DicomTransferSyntax transferSyntax);
+
+    size_t GetRemainingPropositions() const;
+
+    void ProposePresentationContext(
+      const std::string& abstractSyntax,
+      const std::set<DicomTransferSyntax>& transferSyntaxes);
+    
+    T_ASC_Association& GetDcmtkAssociation() const;
+
+    T_ASC_Network& GetDcmtkNetwork() const;
+
+    static void CheckCondition(const OFCondition& cond,
+                               const DicomAssociationParameters& parameters,
+                               const std::string& command);
+
+    static void 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);
+    
+    static void RequestStorageCommitment(
+      const DicomAssociationParameters& parameters,
+      const std::string& transactionUid,
+      const std::vector<std::string>& sopClassUids,
+      const std::vector<std::string>& sopInstanceUids);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomAssociationParameters.cpp	Fri Apr 10 16:18:17 2020 +0200
@@ -0,0 +1,152 @@
+/**
+ * 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 "DicomAssociationParameters.h"
+
+
+#ifdef _WIN32
+/**
+ * "The maximum length, in bytes, of the string returned in the buffer 
+ * pointed to by the name parameter is dependent on the namespace provider,
+ * but this string must be 256 bytes or less.
+ * http://msdn.microsoft.com/en-us/library/windows/desktop/ms738527(v=vs.85).aspx
+ **/
+#  define HOST_NAME_MAX 256
+#  include <winsock.h>
+#endif 
+
+
+#if !defined(HOST_NAME_MAX) && defined(_POSIX_HOST_NAME_MAX)
+/**
+ * TO IMPROVE: "_POSIX_HOST_NAME_MAX is only the minimum value that
+ * HOST_NAME_MAX can ever have [...] Therefore you cannot allocate an
+ * array of size _POSIX_HOST_NAME_MAX, invoke gethostname() and expect
+ * that the result will fit."
+ * http://lists.gnu.org/archive/html/bug-gnulib/2009-08/msg00128.html
+ **/
+#define HOST_NAME_MAX _POSIX_HOST_NAME_MAX
+#endif
+
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+#include <boost/thread/mutex.hpp>
+
+// By default, the timeout for client DICOM connections is set to 10 seconds
+static boost::mutex  defaultTimeoutMutex_;
+static uint32_t defaultTimeout_ = 10;
+
+
+namespace Orthanc
+{
+  void DicomAssociationParameters::ReadDefaultTimeout()
+  {
+    boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
+    timeout_ = defaultTimeout_;
+  }
+
+
+  DicomAssociationParameters::DicomAssociationParameters() :
+    localAet_("STORESCU"),
+    remoteAet_("ANY-SCP"),
+    remoteHost_("127.0.0.1"),
+    remotePort_(104),
+    manufacturer_(ModalityManufacturer_Generic)
+  {
+    ReadDefaultTimeout();
+  }
+
+    
+  DicomAssociationParameters::DicomAssociationParameters(const std::string& localAet,
+                                                         const RemoteModalityParameters& remote) :
+    localAet_(localAet),
+    remoteAet_(remote.GetApplicationEntityTitle()),
+    remoteHost_(remote.GetHost()),
+    remotePort_(remote.GetPortNumber()),
+    manufacturer_(remote.GetManufacturer()),
+    timeout_(defaultTimeout_)
+  {
+    ReadDefaultTimeout();
+  }
+
+    
+  void DicomAssociationParameters::SetRemoteHost(const std::string& host)
+  {
+    if (host.size() > HOST_NAME_MAX - 10)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Invalid host name (too long): " + host);
+    }
+
+    remoteHost_ = host;
+  }
+
+
+  void DicomAssociationParameters::SetRemoteModality(const RemoteModalityParameters& parameters)
+  {
+    SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle());
+    SetRemoteHost(parameters.GetHost());
+    SetRemotePort(parameters.GetPortNumber());
+    SetRemoteManufacturer(parameters.GetManufacturer());
+  }
+
+
+  bool DicomAssociationParameters::IsEqual(const DicomAssociationParameters& other) const
+  {
+    return (localAet_ == other.localAet_ &&
+            remoteAet_ == other.remoteAet_ &&
+            remoteHost_ == other.remoteHost_ &&
+            remotePort_ == other.remotePort_ &&
+            manufacturer_ == other.manufacturer_);
+  }
+
+    
+  void DicomAssociationParameters::SetDefaultTimeout(uint32_t seconds)
+  {
+    LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " 
+              << seconds << " seconds (0 = no timeout)";
+
+    {
+      boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
+      defaultTimeout_ = seconds;
+    }
+  }
+
+
+  size_t DicomAssociationParameters::GetMaxHostNameSize()
+  {
+    return HOST_NAME_MAX;
+  }  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomAssociationParameters.h	Fri Apr 10 16:18:17 2020 +0200
@@ -0,0 +1,130 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "RemoteModalityParameters.h"
+
+class OFCondition;  // From DCMTK
+
+namespace Orthanc
+{
+  class DicomAssociationParameters
+  {
+  private:
+    std::string           localAet_;
+    std::string           remoteAet_;
+    std::string           remoteHost_;
+    uint16_t              remotePort_;
+    ModalityManufacturer  manufacturer_;
+    uint32_t              timeout_;
+
+    void ReadDefaultTimeout();
+
+  public:
+    DicomAssociationParameters();
+    
+    DicomAssociationParameters(const std::string& localAet,
+                               const RemoteModalityParameters& remote);
+    
+    const std::string& GetLocalApplicationEntityTitle() const
+    {
+      return localAet_;
+    }
+
+    const std::string& GetRemoteApplicationEntityTitle() const
+    {
+      return remoteAet_;
+    }
+
+    const std::string& GetRemoteHost() const
+    {
+      return remoteHost_;
+    }
+
+    uint16_t GetRemotePort() const
+    {
+      return remotePort_;
+    }
+
+    ModalityManufacturer GetRemoteManufacturer() const
+    {
+      return manufacturer_;
+    }
+
+    void SetLocalApplicationEntityTitle(const std::string& aet)
+    {
+      localAet_ = aet;
+    }
+
+    void SetRemoteApplicationEntityTitle(const std::string& aet)
+    {
+      remoteAet_ = aet;
+    }
+
+    void SetRemoteHost(const std::string& host);
+
+    void SetRemotePort(uint16_t port)
+    {
+      remotePort_ = port;
+    }
+
+    void SetRemoteManufacturer(ModalityManufacturer manufacturer)
+    {
+      manufacturer_ = manufacturer;
+    }
+
+    void SetRemoteModality(const RemoteModalityParameters& parameters);
+
+    bool IsEqual(const DicomAssociationParameters& other) const;
+
+    void SetTimeout(uint32_t seconds)
+    {
+      timeout_ = seconds;
+    }
+
+    uint32_t GetTimeout() const
+    {
+      return timeout_;
+    }
+
+    bool HasTimeout() const
+    {
+      return timeout_ != 0;
+    }
+    
+    static void SetDefaultTimeout(uint32_t seconds);
+
+    static size_t GetMaxHostNameSize();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomControlUserConnection.cpp	Fri Apr 10 16:18:17 2020 +0200
@@ -0,0 +1,660 @@
+/**
+ * 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 "DicomControlUserConnection.h"
+
+#include "../DicomParsing/FromDcmtkBridge.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "DicomAssociation.h"
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmnet/diutil.h>
+
+namespace Orthanc
+{
+  static void TestAndCopyTag(DicomMap& result,
+                             const DicomMap& source,
+                             const DicomTag& tag)
+  {
+    if (!source.HasTag(tag))
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+    else
+    {
+      result.SetValue(tag, source.GetValue(tag));
+    }
+  }
+
+
+  namespace
+  {
+    struct FindPayload
+    {
+      DicomFindAnswers* answers;
+      const char*       level;
+      bool              isWorklist;
+    };
+  }
+
+
+  static void FindCallback(
+    /* in */
+    void *callbackData,
+    T_DIMSE_C_FindRQ *request,      /* original find request */
+    int responseCount,
+    T_DIMSE_C_FindRSP *response,    /* pending response received */
+    DcmDataset *responseIdentifiers /* pending response identifiers */
+    )
+  {
+    FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData);
+
+    if (responseIdentifiers != NULL)
+    {
+      if (payload.isWorklist)
+      {
+        ParsedDicomFile answer(*responseIdentifiers);
+        payload.answers->Add(answer);
+      }
+      else
+      {
+        DicomMap m;
+        FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers);
+        
+        if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
+        {
+          m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false);
+        }
+
+        payload.answers->Add(m);
+      }
+    }
+  }
+
+
+  static void NormalizeFindQuery(DicomMap& fixedQuery,
+                                 ResourceType level,
+                                 const DicomMap& fields)
+  {
+    std::set<DicomTag> allowedTags;
+
+    // WARNING: Do not add "break" or reorder items in this switch-case!
+    switch (level)
+    {
+      case ResourceType_Instance:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance);
+
+      case ResourceType_Series:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Series);
+
+      case ResourceType_Study:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Study);
+
+      case ResourceType_Patient:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
+        break;
+
+      case ResourceType_Study:
+        allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
+        allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY);
+        break;
+
+      case ResourceType_Series:
+        allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
+        break;
+
+      default:
+        break;
+    }
+
+    allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+
+    DicomArray query(fields);
+    for (size_t i = 0; i < query.GetSize(); i++)
+    {
+      const DicomTag& tag = query.GetElement(i).GetTag();
+      if (allowedTags.find(tag) == allowedTags.end())
+      {
+        LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag;
+      }
+      else
+      {
+        fixedQuery.SetValue(tag, query.GetElement(i).GetValue());
+      }
+    }
+  }
+
+
+
+  static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields,
+                                             ModalityManufacturer manufacturer)
+  {
+    // Fix outgoing C-Find requests issue for Syngo.Via and its
+    // solution was reported by Emsy Chan by private mail on
+    // 2015-06-17. According to Robert van Ommen (2015-11-30), the
+    // same fix is required for Agfa Impax. This was generalized for
+    // generic manufacturer since it seems to affect PhilipsADW,
+    // GEWAServer as well:
+    // https://bitbucket.org/sjodogne/orthanc/issues/31/
+
+    switch (manufacturer)
+    {
+      case ModalityManufacturer_GenericNoWildcardInDates:
+      case ModalityManufacturer_GenericNoUniversalWildcard:
+      {
+        std::unique_ptr<DicomMap> fix(fields.Clone());
+
+        std::set<DicomTag> tags;
+        fix->GetTags(tags);
+
+        for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+        {
+          // Replace a "*" wildcard query by an empty query ("") for
+          // "date" or "all" value representations depending on the
+          // type of manufacturer.
+          if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard ||
+              (manufacturer == ModalityManufacturer_GenericNoWildcardInDates &&
+               FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date))
+          {
+            const DicomValue* value = fix->TestAndGetValue(*it);
+
+            if (value != NULL && 
+                !value->IsNull() &&
+                value->GetContent() == "*")
+            {
+              fix->SetValue(*it, "", false);
+            }
+          }
+        }
+
+        return new ParsedDicomFile(*fix, GetDefaultDicomEncoding(), false /* be strict */);
+      }
+
+      default:
+        return new ParsedDicomFile(fields, GetDefaultDicomEncoding(), false /* be strict */);
+    }
+  }
+
+
+
+  void DicomControlUserConnection::SetupPresentationContexts()
+  {
+    association_->ProposeGenericPresentationContext(UID_VerificationSOPClass);
+    association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
+    association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
+    association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
+    association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
+  }
+    
+
+  void DicomControlUserConnection::FindInternal(DicomFindAnswers& answers,
+                                                DcmDataset* dataset,
+                                                const char* sopClass,
+                                                bool isWorklist,
+                                                const char* level)
+  {
+    assert(isWorklist ^ (level != NULL));
+
+    association_->Open(parameters_);
+
+    FindPayload payload;
+    payload.answers = &answers;
+    payload.level = level;
+    payload.isWorklist = isWorklist;
+
+    // Figure out which of the accepted presentation contexts should be used
+    int presID = ASC_findAcceptedPresentationContextID(
+      &association_->GetDcmtkAssociation(), sopClass);
+    if (presID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomFindUnavailable,
+                             "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle());
+    }
+
+    T_DIMSE_C_FindRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
+    request.Priority = DIMSE_PRIORITY_MEDIUM;
+    request.DataSetType = DIMSE_DATASET_PRESENT;
+
+    T_DIMSE_C_FindRSP response;
+    DcmDataset* statusDetail = NULL;
+
+#if DCMTK_VERSION_NUMBER >= 364
+    int responseCount;
+#endif
+
+    OFCondition cond = DIMSE_findUser(
+      &association_->GetDcmtkAssociation(), presID, &request, dataset,
+#if DCMTK_VERSION_NUMBER >= 364
+      responseCount,
+#endif
+      FindCallback, &payload,
+      /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+      /*opt_dimse_timeout*/ parameters_.GetTimeout(),
+      &response, &statusDetail);
+    
+    if (statusDetail)
+    {
+      delete statusDetail;
+    }
+
+    DicomAssociation::CheckCondition(cond, parameters_, "C-FIND");
+
+    
+    /**
+     * New in Orthanc 1.6.0: Deal with failures during C-FIND.
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1
+     **/
+    
+    if (response.DimseStatus != 0x0000 &&  // Success
+        response.DimseStatus != 0xFF00 &&  // Pending - Matches are continuing 
+        response.DimseStatus != 0xFF01)    // Pending - Matches are continuing 
+    {
+      char buf[16];
+      sprintf(buf, "%04X", response.DimseStatus);
+
+      if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               HttpStatus_422_UnprocessableEntity,
+                               "C-FIND SCU to AET \"" +
+                               parameters_.GetRemoteApplicationEntityTitle() +
+                               "\" has failed with DIMSE status 0x" + buf +
+                               " (unable to process - invalid query ?)");
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" +
+                               parameters_.GetRemoteApplicationEntityTitle() +
+                               "\" has failed with DIMSE status 0x" + buf);
+      }
+    }
+  }
+
+    
+  void DicomControlUserConnection::MoveInternal(const std::string& targetAet,
+                                                ResourceType level,
+                                                const DicomMap& fields)
+  {
+    association_->Open(parameters_);
+
+    std::unique_ptr<ParsedDicomFile> query(
+      ConvertQueryFields(fields, parameters_.GetRemoteManufacturer()));
+    DcmDataset* dataset = query->GetDcmtkObject().getDataset();
+
+    const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
+    switch (level)
+    {
+      case ResourceType_Patient:
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
+        break;
+
+      case ResourceType_Study:
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
+        break;
+
+      case ResourceType_Series:
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
+        break;
+
+      case ResourceType_Instance:
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    // Figure out which of the accepted presentation contexts should be used
+    int presID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass);
+    if (presID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomMoveUnavailable,
+                             "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle());
+    }
+
+    T_DIMSE_C_MoveRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
+    request.Priority = DIMSE_PRIORITY_MEDIUM;
+    request.DataSetType = DIMSE_DATASET_PRESENT;
+    strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN);
+
+    T_DIMSE_C_MoveRSP response;
+    DcmDataset* statusDetail = NULL;
+    DcmDataset* responseIdentifiers = NULL;
+    OFCondition cond = DIMSE_moveUser(
+      &association_->GetDcmtkAssociation(), presID, &request, dataset, NULL, NULL,
+      /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+      /*opt_dimse_timeout*/ parameters_.GetTimeout(),
+      &association_->GetDcmtkNetwork(), NULL, NULL,
+      &response, &statusDetail, &responseIdentifiers);
+
+    if (statusDetail)
+    {
+      delete statusDetail;
+    }
+
+    if (responseIdentifiers)
+    {
+      delete responseIdentifiers;
+    }
+
+    DicomAssociation::CheckCondition(cond, parameters_, "C-MOVE");
+
+    
+    /**
+     * New in Orthanc 1.6.0: Deal with failures during C-MOVE.
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2
+     **/
+    
+    if (response.DimseStatus != 0x0000 &&  // Success
+        response.DimseStatus != 0xFF00)    // Pending - Sub-operations are continuing
+    {
+      char buf[16];
+      sprintf(buf, "%04X", response.DimseStatus);
+
+      if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               HttpStatus_422_UnprocessableEntity,
+                               "C-MOVE SCU to AET \"" +
+                               parameters_.GetRemoteApplicationEntityTitle() +
+                               "\" has failed with DIMSE status 0x" + buf +
+                               " (unable to process - resource not found ?)");
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" +
+                               parameters_.GetRemoteApplicationEntityTitle() +
+                               "\" has failed with DIMSE status 0x" + buf);
+      }
+    }
+  }
+    
+
+  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params) :
+    parameters_(params),
+    association_(new DicomAssociation)
+  {
+    SetupPresentationContexts();
+  }
+    
+
+  bool DicomControlUserConnection::Echo()
+  {
+    association_->Open(parameters_);
+
+    DIC_US status;
+    DicomAssociation::CheckCondition(
+      DIMSE_echoUser(&association_->GetDcmtkAssociation(),
+                     association_->GetDcmtkAssociation().nextMsgID++, 
+                     /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                     /*opt_dimse_timeout*/ parameters_.GetTimeout(),
+                     &status, NULL),
+      parameters_, "C-ECHO");
+      
+    return status == STATUS_Success;
+  }
+
+
+  void DicomControlUserConnection::Find(DicomFindAnswers& result,
+                                        ResourceType level,
+                                        const DicomMap& originalFields,
+                                        bool normalize)
+  {
+    std::unique_ptr<ParsedDicomFile> query;
+
+    if (normalize)
+    {
+      DicomMap fields;
+      NormalizeFindQuery(fields, level, originalFields);
+      query.reset(ConvertQueryFields(fields, parameters_.GetRemoteManufacturer()));
+    }
+    else
+    {
+      query.reset(new ParsedDicomFile(originalFields,
+                                      GetDefaultDicomEncoding(),
+                                      false /* be strict */));
+    }
+    
+    DcmDataset* dataset = query->GetDcmtkObject().getDataset();
+
+    const char* clevel = NULL;
+    const char* sopClass = NULL;
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        clevel = "PATIENT";
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
+        sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
+        break;
+
+      case ResourceType_Study:
+        clevel = "STUDY";
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
+        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
+
+      case ResourceType_Series:
+        clevel = "SERIES";
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
+        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
+
+      case ResourceType_Instance:
+        clevel = "IMAGE";
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
+        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+
+    const char* universal;
+    if (parameters_.GetRemoteManufacturer() == ModalityManufacturer_GE)
+    {
+      universal = "*";
+    }
+    else
+    {
+      universal = "";
+    }      
+    
+
+    // Add the expected tags for this query level.
+    // WARNING: Do not reorder or add "break" in this switch-case!
+    switch (level)
+    {
+      case ResourceType_Instance:
+        if (!dataset->tagExists(DCM_SOPInstanceUID))
+        {
+          DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal);
+        }
+
+      case ResourceType_Series:
+        if (!dataset->tagExists(DCM_SeriesInstanceUID))
+        {
+          DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal);
+        }
+
+      case ResourceType_Study:
+        if (!dataset->tagExists(DCM_AccessionNumber))
+        {
+          DU_putStringDOElement(dataset, DCM_AccessionNumber, universal);
+        }
+
+        if (!dataset->tagExists(DCM_StudyInstanceUID))
+        {
+          DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal);
+        }
+
+      case ResourceType_Patient:
+        if (!dataset->tagExists(DCM_PatientID))
+        {
+          DU_putStringDOElement(dataset, DCM_PatientID, universal);
+        }
+        
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(clevel != NULL && sopClass != NULL);
+    FindInternal(result, dataset, sopClass, false, clevel);
+  }
+    
+
+  void DicomControlUserConnection::Move(const std::string& targetAet,
+                                        ResourceType level,
+                                        const DicomMap& findResult)
+  {
+    DicomMap move;
+    switch (level)
+    {
+      case ResourceType_Patient:
+        TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID);
+        break;
+
+      case ResourceType_Study:
+        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
+        break;
+
+      case ResourceType_Series:
+        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
+        TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
+        break;
+
+      case ResourceType_Instance:
+        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
+        TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
+        TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    MoveInternal(targetAet, level, move);
+  }
+
+
+  void DicomControlUserConnection::Move(const std::string& targetAet,
+                                        const DicomMap& findResult)
+  {
+    if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
+    ResourceType level = StringToResourceType(tmp.c_str());
+
+    Move(targetAet, level, findResult);
+  }
+
+
+  void DicomControlUserConnection::MovePatient(const std::string& targetAet,
+                                               const std::string& patientId)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false);
+    MoveInternal(targetAet, ResourceType_Patient, query);
+  }
+    
+
+  void DicomControlUserConnection::MoveStudy(const std::string& targetAet,
+                                             const std::string& studyUid)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
+    MoveInternal(targetAet, ResourceType_Study, query);
+  }
+
+    
+  void DicomControlUserConnection::MoveSeries(const std::string& targetAet,
+                                              const std::string& studyUid,
+                                              const std::string& seriesUid)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
+    query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
+    MoveInternal(targetAet, ResourceType_Series, query);
+  }
+
+
+  void DicomControlUserConnection::MoveInstance(const std::string& targetAet,
+                                                const std::string& studyUid,
+                                                const std::string& seriesUid,
+                                                const std::string& instanceUid)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
+    query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
+    query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false);
+    MoveInternal(targetAet, ResourceType_Instance, query);
+  }
+
+
+  void DicomControlUserConnection::FindWorklist(DicomFindAnswers& result,
+                                                ParsedDicomFile& query)
+  {
+    DcmDataset* dataset = query.GetDcmtkObject().getDataset();
+    const char* sopClass = UID_FINDModalityWorklistInformationModel;
+
+    FindInternal(result, dataset, sopClass, true, NULL);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomControlUserConnection.h	Fri Apr 10 16:18:17 2020 +0200
@@ -0,0 +1,107 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be set to 1
+#endif
+
+#include "DicomAssociationParameters.h"
+#include "DicomFindAnswers.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class DicomAssociation;  // Forward declaration for PImpl design pattern
+  
+  class DicomControlUserConnection : public boost::noncopyable
+  {
+  private:
+    DicomAssociationParameters           parameters_;
+    boost::shared_ptr<DicomAssociation>  association_;
+
+    void SetupPresentationContexts();
+
+    void FindInternal(DicomFindAnswers& answers,
+                      DcmDataset* dataset,
+                      const char* sopClass,
+                      bool isWorklist,
+                      const char* level);
+    
+    void MoveInternal(const std::string& targetAet,
+                      ResourceType level,
+                      const DicomMap& fields);
+    
+  public:
+    DicomControlUserConnection(const DicomAssociationParameters& params);
+    
+    const DicomAssociationParameters& GetParameters() const
+    {
+      return parameters_;
+    }
+
+    bool Echo();
+
+    void Find(DicomFindAnswers& result,
+              ResourceType level,
+              const DicomMap& originalFields,
+              bool normalize);
+
+    void Move(const std::string& targetAet,
+              ResourceType level,
+              const DicomMap& findResult);
+    
+    void Move(const std::string& targetAet,
+              const DicomMap& findResult);
+    
+    void MovePatient(const std::string& targetAet,
+                     const std::string& patientId);
+
+    void MoveStudy(const std::string& targetAet,
+                   const std::string& studyUid);
+
+    void MoveSeries(const std::string& targetAet,
+                    const std::string& studyUid,
+                    const std::string& seriesUid);
+
+    void MoveInstance(const std::string& targetAet,
+                      const std::string& studyUid,
+                      const std::string& seriesUid,
+                      const std::string& instanceUid);
+
+    void FindWorklist(DicomFindAnswers& result,
+                      ParsedDicomFile& query);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomStoreUserConnection.cpp	Fri Apr 10 16:18:17 2020 +0200
@@ -0,0 +1,240 @@
+/**
+ * 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 "DicomStoreUserConnection.h"
+
+#include "DicomAssociation.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  bool DicomStoreUserConnection::ProposeStorageClass(const std::string& sopClassUid,
+                                                     const std::set<DicomTransferSyntax>& syntaxes)
+  {
+    size_t requiredCount = syntaxes.size();
+    if (proposeUncompressedSyntaxes_)
+    {
+      requiredCount += 1;
+    }
+      
+    if (association_->GetRemainingPropositions() <= requiredCount)
+    {
+      return false;  // Not enough room
+    }
+      
+    for (std::set<DicomTransferSyntax>::const_iterator
+           it = syntaxes.begin(); it != syntaxes.end(); ++it)
+    {
+      association_->ProposePresentationContext(sopClassUid, *it);
+    }
+
+    if (proposeUncompressedSyntaxes_)
+    {
+      std::set<DicomTransferSyntax> uncompressed;
+        
+      if (syntaxes.find(DicomTransferSyntax_LittleEndianImplicit) == syntaxes.end())
+      {
+        uncompressed.insert(DicomTransferSyntax_LittleEndianImplicit);
+      }
+        
+      if (syntaxes.find(DicomTransferSyntax_LittleEndianExplicit) == syntaxes.end())
+      {
+        uncompressed.insert(DicomTransferSyntax_LittleEndianExplicit);
+      }
+        
+      if (proposeRetiredBigEndian_ &&
+          syntaxes.find(DicomTransferSyntax_BigEndianExplicit) == syntaxes.end())
+      {
+        uncompressed.insert(DicomTransferSyntax_BigEndianExplicit);
+      }
+
+      if (!uncompressed.empty())
+      {
+        association_->ProposePresentationContext(sopClassUid, uncompressed);
+      }
+    }      
+
+    return true;
+  }
+
+
+  bool DicomStoreUserConnection::LookupPresentationContext(
+    uint8_t& presentationContextId,
+    const std::string& sopClassUid,
+    DicomTransferSyntax transferSyntax)
+  {
+    typedef std::map<DicomTransferSyntax, uint8_t>  PresentationContexts;
+
+    PresentationContexts pc;
+    if (association_->IsOpen() &&
+        association_->LookupAcceptedPresentationContext(pc, sopClassUid))
+    {
+      PresentationContexts::const_iterator found = pc.find(transferSyntax);
+      if (found != pc.end())
+      {
+        presentationContextId = found->second;
+        return true;
+      }
+    }
+
+    return false;
+  }
+    
+        
+  DicomStoreUserConnection::DicomStoreUserConnection(
+    const DicomAssociationParameters& params) :
+    parameters_(params),
+    association_(new DicomAssociation),
+    proposeCommonClasses_(true),
+    proposeUncompressedSyntaxes_(true),
+    proposeRetiredBigEndian_(false)
+  {
+  }
+    
+
+  void DicomStoreUserConnection::PrepareStorageClass(const std::string& sopClassUid,
+                                                     DicomTransferSyntax syntax)
+  {
+    StorageClasses::iterator found = storageClasses_.find(sopClassUid);
+
+    if (found == storageClasses_.end())
+    {
+      std::set<DicomTransferSyntax> ts;
+      ts.insert(syntax);
+      storageClasses_[sopClassUid] = ts;
+    }
+    else
+    {
+      found->second.insert(syntax);
+    }
+  }
+
+
+  bool DicomStoreUserConnection::NegotiatePresentationContext(
+    uint8_t& presentationContextId,
+    const std::string& sopClassUid,
+    DicomTransferSyntax transferSyntax)
+  {
+    /**
+     * Step 1: Check whether this presentation context is already
+     * available in the previously negociated assocation.
+     **/
+
+    if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax))
+    {
+      return true;
+    }
+
+    // The association must be re-negotiated
+    LOG(INFO) << "Re-negociating DICOM association with "
+              << parameters_.GetRemoteApplicationEntityTitle();
+    association_->ClearPresentationContexts();
+    PrepareStorageClass(sopClassUid, transferSyntax);
+
+      
+    /**
+     * Step 2: Propose at least the mandatory SOP class.
+     **/
+
+    {
+      StorageClasses::const_iterator mandatory = storageClasses_.find(sopClassUid);
+
+      if (mandatory == storageClasses_.end() ||
+          mandatory->second.find(transferSyntax) == mandatory->second.end())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (!ProposeStorageClass(sopClassUid, mandatory->second))
+      {
+        // Should never happen in real life: There are no more than
+        // 128 transfer syntaxes in DICOM!
+        throw OrthancException(ErrorCode_InternalError,
+                               "Too many transfer syntaxes for SOP class UID: " + sopClassUid);
+      }
+    }
+
+      
+    /**
+     * Step 3: Propose all the previously spotted SOP classes, as
+     * registered through the "PrepareStorageClass()" method.
+     **/
+      
+    for (StorageClasses::const_iterator it = storageClasses_.begin();
+         it != storageClasses_.end(); ++it)
+    {
+      if (it->first != sopClassUid)
+      {
+        ProposeStorageClass(it->first, it->second);
+      }
+    }
+      
+
+    /**
+     * Step 4: As long as there is room left in the proposed
+     * presentation contexts, propose the uncompressed transfer syntaxes
+     * for the most common SOP classes, as can be found in the
+     * "dcmShortSCUStorageSOPClassUIDs" array from DCMTK. The
+     * preferred transfer syntax is "LittleEndianImplicit".
+     **/
+
+    if (proposeCommonClasses_)
+    {
+      std::set<DicomTransferSyntax> ts;
+      ts.insert(DicomTransferSyntax_LittleEndianImplicit);
+        
+      for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++)
+      {
+        std::string c(dcmShortSCUStorageSOPClassUIDs[i]);
+          
+        if (c != sopClassUid &&
+            storageClasses_.find(c) == storageClasses_.end())
+        {
+          ProposeStorageClass(c, ts);
+        }
+      }
+    }
+
+
+    /**
+     * Step 5: Open the association, and check whether the pair (SOP
+     * class UID, transfer syntax) was accepted by the remote host.
+     **/
+
+    association_->Open(parameters_);
+    return LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomStoreUserConnection.h	Fri Apr 10 16:18:17 2020 +0200
@@ -0,0 +1,131 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "DicomAssociationParameters.h"
+
+#include <boost/shared_ptr.hpp>
+#include <boost/noncopyable.hpp>
+#include <set>
+#include <stdint.h>  // For uint8_t
+
+
+namespace Orthanc
+{
+  /**
+
+     Orthanc < 1.7.0:
+
+     Input        | Output
+     -------------+---------------------------------------------
+     Compressed   | Same transfer syntax
+     Uncompressed | Same transfer syntax, or other uncompressed
+
+     Orthanc >= 1.7.0:
+
+     Input        | Output
+     -------------+---------------------------------------------
+     Compressed   | Same transfer syntax, or uncompressed
+     Uncompressed | Same transfer syntax, or other uncompressed
+
+  **/
+
+  class DicomAssociation;  // Forward declaration for PImpl design pattern
+
+  class DicomStoreUserConnection : public boost::noncopyable
+  {
+  private:
+    typedef std::map<std::string, std::set<DicomTransferSyntax> > StorageClasses;
+    
+    DicomAssociationParameters           parameters_;
+    boost::shared_ptr<DicomAssociation>  association_;
+    StorageClasses                       storageClasses_;
+    bool                                 proposeCommonClasses_;
+    bool                                 proposeUncompressedSyntaxes_;
+    bool                                 proposeRetiredBigEndian_;
+
+    // Return "false" if there is not enough room remaining in the association
+    bool ProposeStorageClass(const std::string& sopClassUid,
+                             const std::set<DicomTransferSyntax>& syntaxes);
+    
+  public:
+    DicomStoreUserConnection(const DicomAssociationParameters& params);
+    
+    const DicomAssociationParameters& GetParameters() const
+    {
+      return parameters_;
+    }
+
+    void SetCommonClassesProposed(bool proposed)
+    {
+      proposeCommonClasses_ = proposed;
+    }
+
+    bool IsCommonClassesProposed() const
+    {
+      return proposeCommonClasses_;
+    }
+
+    void SetUncompressedSyntaxesProposed(bool proposed)
+    {
+      proposeUncompressedSyntaxes_ = proposed;
+    }
+
+    bool IsUncompressedSyntaxesProposed() const
+    {
+      return proposeUncompressedSyntaxes_;
+    }
+
+    void SetRetiredBigEndianProposed(bool propose)
+    {
+      proposeRetiredBigEndian_ = propose;
+    }
+
+    bool IsRetiredBigEndianProposed() const
+    {
+      return proposeRetiredBigEndian_;
+    }      
+
+    void PrepareStorageClass(const std::string& sopClassUid,
+                             DicomTransferSyntax syntax);
+
+    bool LookupPresentationContext(uint8_t& presentationContextId,
+                                   const std::string& sopClassUid,
+                                   DicomTransferSyntax transferSyntax);
+        
+    bool NegotiatePresentationContext(uint8_t& presentationContextId,
+                                      const std::string& sopClassUid,
+                                      DicomTransferSyntax transferSyntax);
+  };
+}
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Fri Apr 10 15:24:02 2020 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Fri Apr 10 16:18:17 2020 +0200
@@ -694,8 +694,8 @@
           dicom.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT);
         }
 
-        windowWidth = static_cast<float>(1 << info.GetBitsStored());
-        windowCenter = windowWidth / 2.0f;
+        windowWidth = static_cast<float>(1 << info.GetBitsStored()) * rescaleSlope;
+        windowCenter = windowWidth / 2.0f + rescaleIntercept;
 
         if (dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) &&
             dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH))
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake	Fri Apr 10 15:24:02 2020 +0200
+++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake	Fri Apr 10 16:18:17 2020 +0200
@@ -481,6 +481,10 @@
   if (ENABLE_DCMTK_NETWORKING)
     add_definitions(-DORTHANC_ENABLE_DCMTK_NETWORKING=1)
     list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL
+      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomAssociation.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomAssociationParameters.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomControlUserConnection.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomStoreUserConnection.cpp
       ${ORTHANC_ROOT}/Core/DicomNetworking/DicomFindAnswers.cpp
       ${ORTHANC_ROOT}/Core/DicomNetworking/DicomServer.cpp
       ${ORTHANC_ROOT}/Core/DicomNetworking/DicomUserConnection.cpp
--- a/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Fri Apr 10 15:24:02 2020 +0200
+++ b/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Fri Apr 10 16:18:17 2020 +0200
@@ -72,7 +72,10 @@
             # Authentication (for some weird reason, this method does
             # not always work)
             # http://en.wikipedia.org/wiki/Basic_access_authentication
-            headers['authorization'] = 'Basic ' + base64.b64encode(username + ':' + password)       
+            creds_str = username + ':' + password
+            creds_str_bytes = creds_str.encode("ascii")
+            creds_str_bytes_b64 = b'Basic ' + base64.b64encode(creds_str_bytes)
+            headers['authorization'] = creds_str_bytes_b64.decode("ascii")
 
         resp, content = h.request(URL, 'POST', 
                                   body = content,
@@ -85,6 +88,8 @@
             sys.stdout.write(" => failure (Is it a DICOM file? Is there a password?)\n")
 
     except:
+        type, value, traceback = sys.exc_info()
+        sys.stderr.write(str(value))
         sys.stdout.write(" => unable to connect (Is Orthanc running? Is there a password?)\n")
 
 
--- a/UnitTestsSources/FromDcmtkTests.cpp	Fri Apr 10 15:24:02 2020 +0200
+++ b/UnitTestsSources/FromDcmtkTests.cpp	Fri Apr 10 16:18:17 2020 +0200
@@ -2406,1930 +2406,9 @@
 
 
 
-#ifdef _WIN32
-/**
- * "The maximum length, in bytes, of the string returned in the buffer 
- * pointed to by the name parameter is dependent on the namespace provider,
- * but this string must be 256 bytes or less.
- * http://msdn.microsoft.com/en-us/library/windows/desktop/ms738527(v=vs.85).aspx
- **/
-#  define HOST_NAME_MAX 256
-#  include <winsock.h>
-#endif 
-
-
-#if !defined(HOST_NAME_MAX) && defined(_POSIX_HOST_NAME_MAX)
-/**
- * TO IMPROVE: "_POSIX_HOST_NAME_MAX is only the minimum value that
- * HOST_NAME_MAX can ever have [...] Therefore you cannot allocate an
- * array of size _POSIX_HOST_NAME_MAX, invoke gethostname() and expect
- * that the result will fit."
- * http://lists.gnu.org/archive/html/bug-gnulib/2009-08/msg00128.html
- **/
-#define HOST_NAME_MAX _POSIX_HOST_NAME_MAX
-#endif
-
-
-#include "../Core/DicomNetworking/RemoteModalityParameters.h"
-
-
-#include <dcmtk/dcmnet/diutil.h>  // For dcmConnectionTimeout()
-
-
-
-namespace Orthanc
-{
-  // By default, the timeout for client DICOM connections is set to 10 seconds
-  static boost::mutex  defaultTimeoutMutex_;
-  static uint32_t defaultTimeout_ = 10;
-
-
-  class DicomAssociationParameters
-  {
-  private:
-    std::string           localAet_;
-    std::string           remoteAet_;
-    std::string           remoteHost_;
-    uint16_t              remotePort_;
-    ModalityManufacturer  manufacturer_;
-    uint32_t              timeout_;
-
-    void ReadDefaultTimeout()
-    {
-      boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
-      timeout_ = defaultTimeout_;
-    }
-
-  public:
-    DicomAssociationParameters() :
-      localAet_("STORESCU"),
-      remoteAet_("ANY-SCP"),
-      remoteHost_("127.0.0.1"),
-      remotePort_(104),
-      manufacturer_(ModalityManufacturer_Generic)
-    {
-      ReadDefaultTimeout();
-    }
-    
-    DicomAssociationParameters(const std::string& localAet,
-                               const RemoteModalityParameters& remote) :
-      localAet_(localAet),
-      remoteAet_(remote.GetApplicationEntityTitle()),
-      remoteHost_(remote.GetHost()),
-      remotePort_(remote.GetPortNumber()),
-      manufacturer_(remote.GetManufacturer()),
-      timeout_(defaultTimeout_)
-    {
-      ReadDefaultTimeout();
-    }
-    
-    const std::string& GetLocalApplicationEntityTitle() const
-    {
-      return localAet_;
-    }
-
-    const std::string& GetRemoteApplicationEntityTitle() const
-    {
-      return remoteAet_;
-    }
-
-    const std::string& GetRemoteHost() const
-    {
-      return remoteHost_;
-    }
-
-    uint16_t GetRemotePort() const
-    {
-      return remotePort_;
-    }
-
-    ModalityManufacturer GetRemoteManufacturer() const
-    {
-      return manufacturer_;
-    }
-
-    void SetLocalApplicationEntityTitle(const std::string& aet)
-    {
-      localAet_ = aet;
-    }
-
-    void SetRemoteApplicationEntityTitle(const std::string& aet)
-    {
-      remoteAet_ = aet;
-    }
-
-    void SetRemoteHost(const std::string& host)
-    {
-      if (host.size() > HOST_NAME_MAX - 10)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange,
-                               "Invalid host name (too long): " + host);
-      }
-
-      remoteHost_ = host;
-    }
-
-    void SetRemotePort(uint16_t port)
-    {
-      remotePort_ = port;
-    }
-
-    void SetRemoteManufacturer(ModalityManufacturer manufacturer)
-    {
-      manufacturer_ = manufacturer;
-    }
-
-    void SetRemoteModality(const RemoteModalityParameters& parameters)
-    {
-      SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle());
-      SetRemoteHost(parameters.GetHost());
-      SetRemotePort(parameters.GetPortNumber());
-      SetRemoteManufacturer(parameters.GetManufacturer());
-    }
-
-    bool IsEqual(const DicomAssociationParameters& other) const
-    {
-      return (localAet_ == other.localAet_ &&
-              remoteAet_ == other.remoteAet_ &&
-              remoteHost_ == other.remoteHost_ &&
-              remotePort_ == other.remotePort_ &&
-              manufacturer_ == other.manufacturer_);
-    }
-
-    void SetTimeout(uint32_t seconds)
-    {
-      timeout_ = seconds;
-    }
-
-    uint32_t GetTimeout() const
-    {
-      return timeout_;
-    }
-
-    bool HasTimeout() const
-    {
-      return timeout_ != 0;
-    }
-    
-    static void SetDefaultTimeout(uint32_t seconds)
-    {
-      LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " 
-                << seconds << " seconds (0 = no timeout)";
-
-      {
-        boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
-        defaultTimeout_ = seconds;
-      }
-    }
-  };
-  
-
-  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);
-        }
-      }
-    }
-  }                              
-
-
-  class DicomAssociation : public boost::noncopyable
-  {
-  private:
-    // This is the maximum number of presentation context IDs (the
-    // number of odd integers between 1 and 255)
-    // http://dicom.nema.org/medical/dicom/2019e/output/chtml/part08/sect_9.3.2.2.html
-    static const size_t MAX_PROPOSED_PRESENTATIONS = 128;
-    
-    struct ProposedPresentationContext
-    {
-      std::string                    abstractSyntax_;
-      std::set<DicomTransferSyntax>  transferSyntaxes_;
-    };
-
-    typedef std::map<std::string, std::map<DicomTransferSyntax, uint8_t> >  AcceptedPresentationContexts;
-
-    DicomAssociationRole                      role_;
-    bool                                      isOpen_;
-    std::vector<ProposedPresentationContext>  proposed_;
-    AcceptedPresentationContexts              accepted_;
-    T_ASC_Network*                            net_;
-    T_ASC_Parameters*                         params_;
-    T_ASC_Association*                        assoc_;
-
-    void Initialize()
-    {
-      role_ = DicomAssociationRole_Default;
-      isOpen_ = false;
-      net_ = NULL; 
-      params_ = NULL;
-      assoc_ = NULL;      
-
-      // Must be after "isOpen_ = false"
-      ClearPresentationContexts();
-    }
-
-    void CheckConnecting(const DicomAssociationParameters& parameters,
-                         const OFCondition& cond)
-    {
-      try
-      {
-        CheckCondition(cond, parameters, "connecting");
-      }
-      catch (OrthancException&)
-      {
-        CloseInternal();
-        throw;
-      }
-    }
-    
-    void CloseInternal()
-    {
-      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 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;
-        }
-      }
-    }
-
-  public:
-    DicomAssociation()
-    {
-      Initialize();
-    }
-
-    ~DicomAssociation()
-    {
-      try
-      {
-        Close();
-      }
-      catch (OrthancException&)
-      {
-        // Don't throw exception in destructors
-      }
-    }
-
-    bool IsOpen() const
-    {
-      return isOpen_;
-    }
-
-    void SetRole(DicomAssociationRole role)
-    {
-      if (role_ != role)
-      {
-        Close();
-        role_ = role;
-      }
-    }
-
-    void ClearPresentationContexts()
-    {
-      Close();
-      proposed_.clear();
-      proposed_.reserve(MAX_PROPOSED_PRESENTATIONS);
-    }
-
-    void 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.GetRemoteApplicationEntityTitle()
-                << "\" on host " << parameters.GetRemoteHost()
-                << ":" << parameters.GetRemotePort() 
-                << " (manufacturer: " << EnumerationToString(parameters.GetRemoteManufacturer()) << ")";
-
-      CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_));
-      CheckConnecting(parameters, ASC_createAssociationParameters(&params_, /*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.GetRemoteApplicationEntityTitle().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.GetRemoteHost().c_str(), parameters.GetRemotePort());
-
-      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 = &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)
-          {
-            DicomTransferSyntax transferSyntax;
-            if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
-            {
-              AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID);
-            }
-            else
-            {
-              LOG(WARNING) << "Unknown transfer syntax received from AET \""
-                           << parameters.GetRemoteApplicationEntityTitle()
-                           << "\": " << pc->acceptedTransferSyntax;
-            }
-          }
-            
-          pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l);
-        }
-      }
-
-      if (accepted_.empty())
-      {
-        throw OrthancException(ErrorCode_NoPresentationContext,
-                               "Unable to negotiate a presentation context with AET \"" +
-                               parameters.GetRemoteApplicationEntityTitle() + "\"");
-      }
-    }
-
-    void Close()
-    {
-      if (isOpen_)
-      {
-        CloseInternal();
-      }
-    }
-
-    bool 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 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 ProposePresentationContext(const std::string& abstractSyntax,
-                                    DicomTransferSyntax transferSyntax)
-    {
-      std::set<DicomTransferSyntax> ts;
-      ts.insert(transferSyntax);
-      ProposePresentationContext(abstractSyntax, ts);
-    }
-
-    size_t GetRemainingPropositions() const
-    {
-      assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS);
-      return MAX_PROPOSED_PRESENTATIONS - proposed_.size();
-    }
-
-    void 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& GetDcmtkAssociation() const
-    {
-      if (isOpen_)
-      {
-        assert(assoc_ != NULL);
-        return *assoc_;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                               "The connection is not open");
-      }
-    }
-
-    T_ASC_Network& GetDcmtkNetwork() const
-    {
-      if (isOpen_)
-      {
-        assert(net_ != NULL);
-        return *net_;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                               "The connection is not open");
-      }
-    }
-
-    static void 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,
-                               "DicomUserConnection - " + command + " to AET \"" +
-                               parameters.GetRemoteApplicationEntityTitle() +
-                               "\": " + info);
-      }
-    }
-    
-
-    static void 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.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<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.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<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.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<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.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();
-    }
-  };
-
-
-
-  static void TestAndCopyTag(DicomMap& result,
-                             const DicomMap& source,
-                             const DicomTag& tag)
-  {
-    if (!source.HasTag(tag))
-    {
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-    else
-    {
-      result.SetValue(tag, source.GetValue(tag));
-    }
-  }
-
-
-  namespace
-  {
-    struct FindPayload
-    {
-      DicomFindAnswers* answers;
-      const char*       level;
-      bool              isWorklist;
-    };
-  }
-
-
-  static void FindCallback(
-    /* in */
-    void *callbackData,
-    T_DIMSE_C_FindRQ *request,      /* original find request */
-    int responseCount,
-    T_DIMSE_C_FindRSP *response,    /* pending response received */
-    DcmDataset *responseIdentifiers /* pending response identifiers */
-    )
-  {
-    FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData);
-
-    if (responseIdentifiers != NULL)
-    {
-      if (payload.isWorklist)
-      {
-        ParsedDicomFile answer(*responseIdentifiers);
-        payload.answers->Add(answer);
-      }
-      else
-      {
-        DicomMap m;
-        FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers);
-        
-        if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
-        {
-          m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false);
-        }
-
-        payload.answers->Add(m);
-      }
-    }
-  }
-
-
-  static void NormalizeFindQuery(DicomMap& fixedQuery,
-                                 ResourceType level,
-                                 const DicomMap& fields)
-  {
-    std::set<DicomTag> allowedTags;
-
-    // WARNING: Do not add "break" or reorder items in this switch-case!
-    switch (level)
-    {
-      case ResourceType_Instance:
-        DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance);
-
-      case ResourceType_Series:
-        DicomTag::AddTagsForModule(allowedTags, DicomModule_Series);
-
-      case ResourceType_Study:
-        DicomTag::AddTagsForModule(allowedTags, DicomModule_Study);
-
-      case ResourceType_Patient:
-        DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    switch (level)
-    {
-      case ResourceType_Patient:
-        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
-        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
-        allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
-        break;
-
-      case ResourceType_Study:
-        allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
-        allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
-        allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
-        allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY);
-        break;
-
-      case ResourceType_Series:
-        allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
-        break;
-
-      default:
-        break;
-    }
-
-    allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET);
-
-    DicomArray query(fields);
-    for (size_t i = 0; i < query.GetSize(); i++)
-    {
-      const DicomTag& tag = query.GetElement(i).GetTag();
-      if (allowedTags.find(tag) == allowedTags.end())
-      {
-        LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag;
-      }
-      else
-      {
-        fixedQuery.SetValue(tag, query.GetElement(i).GetValue());
-      }
-    }
-  }
-
-
-
-  static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields,
-                                             ModalityManufacturer manufacturer)
-  {
-    // Fix outgoing C-Find requests issue for Syngo.Via and its
-    // solution was reported by Emsy Chan by private mail on
-    // 2015-06-17. According to Robert van Ommen (2015-11-30), the
-    // same fix is required for Agfa Impax. This was generalized for
-    // generic manufacturer since it seems to affect PhilipsADW,
-    // GEWAServer as well:
-    // https://bitbucket.org/sjodogne/orthanc/issues/31/
-
-    switch (manufacturer)
-    {
-      case ModalityManufacturer_GenericNoWildcardInDates:
-      case ModalityManufacturer_GenericNoUniversalWildcard:
-      {
-        std::unique_ptr<DicomMap> fix(fields.Clone());
-
-        std::set<DicomTag> tags;
-        fix->GetTags(tags);
-
-        for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
-        {
-          // Replace a "*" wildcard query by an empty query ("") for
-          // "date" or "all" value representations depending on the
-          // type of manufacturer.
-          if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard ||
-              (manufacturer == ModalityManufacturer_GenericNoWildcardInDates &&
-               FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date))
-          {
-            const DicomValue* value = fix->TestAndGetValue(*it);
-
-            if (value != NULL && 
-                !value->IsNull() &&
-                value->GetContent() == "*")
-            {
-              fix->SetValue(*it, "", false);
-            }
-          }
-        }
-
-        return new ParsedDicomFile(*fix, GetDefaultDicomEncoding(), false /* be strict */);
-      }
-
-      default:
-        return new ParsedDicomFile(fields, GetDefaultDicomEncoding(), false /* be strict */);
-    }
-  }
-
-
-
-  class DicomControlUserConnection : public boost::noncopyable
-  {
-  private:
-    DicomAssociationParameters  parameters_;
-    DicomAssociation            association_;
-
-    void SetupPresentationContexts()
-    {
-      association_.ProposeGenericPresentationContext(UID_VerificationSOPClass);
-      association_.ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
-      association_.ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
-      association_.ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
-      association_.ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
-    }
-
-    void FindInternal(DicomFindAnswers& answers,
-                      DcmDataset* dataset,
-                      const char* sopClass,
-                      bool isWorklist,
-                      const char* level)
-    {
-      assert(isWorklist ^ (level != NULL));
-
-      association_.Open(parameters_);
-
-      FindPayload payload;
-      payload.answers = &answers;
-      payload.level = level;
-      payload.isWorklist = isWorklist;
-
-      // Figure out which of the accepted presentation contexts should be used
-      int presID = ASC_findAcceptedPresentationContextID(
-        &association_.GetDcmtkAssociation(), sopClass);
-      if (presID == 0)
-      {
-        throw OrthancException(ErrorCode_DicomFindUnavailable,
-                               "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle());
-      }
-
-      T_DIMSE_C_FindRQ request;
-      memset(&request, 0, sizeof(request));
-      request.MessageID = association_.GetDcmtkAssociation().nextMsgID++;
-      strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
-      request.Priority = DIMSE_PRIORITY_MEDIUM;
-      request.DataSetType = DIMSE_DATASET_PRESENT;
-
-      T_DIMSE_C_FindRSP response;
-      DcmDataset* statusDetail = NULL;
-
-#if DCMTK_VERSION_NUMBER >= 364
-      int responseCount;
-#endif
-
-      OFCondition cond = DIMSE_findUser(
-        &association_.GetDcmtkAssociation(), presID, &request, dataset,
-#if DCMTK_VERSION_NUMBER >= 364
-        responseCount,
-#endif
-        FindCallback, &payload,
-        /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-        /*opt_dimse_timeout*/ parameters_.GetTimeout(),
-        &response, &statusDetail);
-    
-      if (statusDetail)
-      {
-        delete statusDetail;
-      }
-
-      DicomAssociation::CheckCondition(cond, parameters_, "C-FIND");
-
-    
-      /**
-       * New in Orthanc 1.6.0: Deal with failures during C-FIND.
-       * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1
-       **/
-    
-      if (response.DimseStatus != 0x0000 &&  // Success
-          response.DimseStatus != 0xFF00 &&  // Pending - Matches are continuing 
-          response.DimseStatus != 0xFF01)    // Pending - Matches are continuing 
-      {
-        char buf[16];
-        sprintf(buf, "%04X", response.DimseStatus);
-
-        if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess)
-        {
-          throw OrthancException(ErrorCode_NetworkProtocol,
-                                 HttpStatus_422_UnprocessableEntity,
-                                 "C-FIND SCU to AET \"" +
-                                 parameters_.GetRemoteApplicationEntityTitle() +
-                                 "\" has failed with DIMSE status 0x" + buf +
-                                 " (unable to process - invalid query ?)");
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" +
-                                 parameters_.GetRemoteApplicationEntityTitle() +
-                                 "\" has failed with DIMSE status 0x" + buf);
-        }
-      }
-    }
-
-    void MoveInternal(const std::string& targetAet,
-                      ResourceType level,
-                      const DicomMap& fields)
-    {
-      association_.Open(parameters_);
-
-      std::unique_ptr<ParsedDicomFile> query(
-        ConvertQueryFields(fields, parameters_.GetRemoteManufacturer()));
-      DcmDataset* dataset = query->GetDcmtkObject().getDataset();
-
-      const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
-      switch (level)
-      {
-        case ResourceType_Patient:
-          DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
-          break;
-
-        case ResourceType_Study:
-          DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
-          break;
-
-        case ResourceType_Series:
-          DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
-          break;
-
-        case ResourceType_Instance:
-          DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-
-      // Figure out which of the accepted presentation contexts should be used
-      int presID = ASC_findAcceptedPresentationContextID(&association_.GetDcmtkAssociation(), sopClass);
-      if (presID == 0)
-      {
-        throw OrthancException(ErrorCode_DicomMoveUnavailable,
-                               "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle());
-      }
-
-      T_DIMSE_C_MoveRQ request;
-      memset(&request, 0, sizeof(request));
-      request.MessageID = association_.GetDcmtkAssociation().nextMsgID++;
-      strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN);
-      request.Priority = DIMSE_PRIORITY_MEDIUM;
-      request.DataSetType = DIMSE_DATASET_PRESENT;
-      strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN);
-
-      T_DIMSE_C_MoveRSP response;
-      DcmDataset* statusDetail = NULL;
-      DcmDataset* responseIdentifiers = NULL;
-      OFCondition cond = DIMSE_moveUser(
-        &association_.GetDcmtkAssociation(), presID, &request, dataset, NULL, NULL,
-        /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-        /*opt_dimse_timeout*/ parameters_.GetTimeout(),
-        &association_.GetDcmtkNetwork(), NULL, NULL,
-        &response, &statusDetail, &responseIdentifiers);
-
-      if (statusDetail)
-      {
-        delete statusDetail;
-      }
-
-      if (responseIdentifiers)
-      {
-        delete responseIdentifiers;
-      }
-
-      DicomAssociation::CheckCondition(cond, parameters_, "C-MOVE");
-
-    
-      /**
-       * New in Orthanc 1.6.0: Deal with failures during C-MOVE.
-       * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2
-       **/
-    
-      if (response.DimseStatus != 0x0000 &&  // Success
-          response.DimseStatus != 0xFF00)    // Pending - Sub-operations are continuing
-      {
-        char buf[16];
-        sprintf(buf, "%04X", response.DimseStatus);
-
-        if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess)
-        {
-          throw OrthancException(ErrorCode_NetworkProtocol,
-                                 HttpStatus_422_UnprocessableEntity,
-                                 "C-MOVE SCU to AET \"" +
-                                 parameters_.GetRemoteApplicationEntityTitle() +
-                                 "\" has failed with DIMSE status 0x" + buf +
-                                 " (unable to process - resource not found ?)");
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" +
-                                 parameters_.GetRemoteApplicationEntityTitle() +
-                                 "\" has failed with DIMSE status 0x" + buf);
-        }
-      }
-    }
-    
-  public:
-    DicomControlUserConnection(const DicomAssociationParameters& params) :
-      parameters_(params)
-    {
-      SetupPresentationContexts();
-    }
-    
-    const DicomAssociationParameters& GetParameters() const
-    {
-      return parameters_;
-    }
-
-    bool Echo()
-    {
-      association_.Open(parameters_);
-
-      DIC_US status;
-      DicomAssociation::CheckCondition(
-        DIMSE_echoUser(&association_.GetDcmtkAssociation(),
-                       association_.GetDcmtkAssociation().nextMsgID++, 
-                       /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-                       /*opt_dimse_timeout*/ parameters_.GetTimeout(),
-                       &status, NULL),
-        parameters_, "C-ECHO");
-      
-      return status == STATUS_Success;
-    }
-
-
-    void Find(DicomFindAnswers& result,
-              ResourceType level,
-              const DicomMap& originalFields,
-              bool normalize)
-    {
-      std::unique_ptr<ParsedDicomFile> query;
-
-      if (normalize)
-      {
-        DicomMap fields;
-        NormalizeFindQuery(fields, level, originalFields);
-        query.reset(ConvertQueryFields(fields, parameters_.GetRemoteManufacturer()));
-      }
-      else
-      {
-        query.reset(new ParsedDicomFile(originalFields,
-                                        GetDefaultDicomEncoding(),
-                                        false /* be strict */));
-      }
-    
-      DcmDataset* dataset = query->GetDcmtkObject().getDataset();
-
-      const char* clevel = NULL;
-      const char* sopClass = NULL;
-
-      switch (level)
-      {
-        case ResourceType_Patient:
-          clevel = "PATIENT";
-          DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
-          sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
-          break;
-
-        case ResourceType_Study:
-          clevel = "STUDY";
-          DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
-          sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
-          break;
-
-        case ResourceType_Series:
-          clevel = "SERIES";
-          DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
-          sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
-          break;
-
-        case ResourceType_Instance:
-          clevel = "IMAGE";
-          DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
-          sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-
-
-      const char* universal;
-      if (parameters_.GetRemoteManufacturer() == ModalityManufacturer_GE)
-      {
-        universal = "*";
-      }
-      else
-      {
-        universal = "";
-      }      
-    
-
-      // Add the expected tags for this query level.
-      // WARNING: Do not reorder or add "break" in this switch-case!
-      switch (level)
-      {
-        case ResourceType_Instance:
-          if (!dataset->tagExists(DCM_SOPInstanceUID))
-          {
-            DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal);
-          }
-
-        case ResourceType_Series:
-          if (!dataset->tagExists(DCM_SeriesInstanceUID))
-          {
-            DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal);
-          }
-
-        case ResourceType_Study:
-          if (!dataset->tagExists(DCM_AccessionNumber))
-          {
-            DU_putStringDOElement(dataset, DCM_AccessionNumber, universal);
-          }
-
-          if (!dataset->tagExists(DCM_StudyInstanceUID))
-          {
-            DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal);
-          }
-
-        case ResourceType_Patient:
-          if (!dataset->tagExists(DCM_PatientID))
-          {
-            DU_putStringDOElement(dataset, DCM_PatientID, universal);
-          }
-        
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-
-      assert(clevel != NULL && sopClass != NULL);
-      FindInternal(result, dataset, sopClass, false, clevel);
-    }
-    
-
-    void Move(const std::string& targetAet,
-              ResourceType level,
-              const DicomMap& findResult)
-    {
-      DicomMap move;
-      switch (level)
-      {
-        case ResourceType_Patient:
-          TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID);
-          break;
-
-        case ResourceType_Study:
-          TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
-          break;
-
-        case ResourceType_Series:
-          TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
-          TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
-          break;
-
-        case ResourceType_Instance:
-          TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
-          TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
-          TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID);
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
-      MoveInternal(targetAet, level, move);
-    }
-
-
-    void Move(const std::string& targetAet,
-              const DicomMap& findResult)
-    {
-      if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
-      ResourceType level = StringToResourceType(tmp.c_str());
-
-      Move(targetAet, level, findResult);
-    }
-
-
-    void MovePatient(const std::string& targetAet,
-                     const std::string& patientId)
-    {
-      DicomMap query;
-      query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false);
-      MoveInternal(targetAet, ResourceType_Patient, query);
-    }
-
-    void MoveStudy(const std::string& targetAet,
-                   const std::string& studyUid)
-    {
-      DicomMap query;
-      query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
-      MoveInternal(targetAet, ResourceType_Study, query);
-    }
-
-    void MoveSeries(const std::string& targetAet,
-                    const std::string& studyUid,
-                    const std::string& seriesUid)
-    {
-      DicomMap query;
-      query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
-      query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
-      MoveInternal(targetAet, ResourceType_Series, query);
-    }
-
-    void MoveInstance(const std::string& targetAet,
-                      const std::string& studyUid,
-                      const std::string& seriesUid,
-                      const std::string& instanceUid)
-    {
-      DicomMap query;
-      query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false);
-      query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false);
-      query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false);
-      MoveInternal(targetAet, ResourceType_Instance, query);
-    }
-
-
-    void FindWorklist(DicomFindAnswers& result,
-                      ParsedDicomFile& query)
-    {
-      DcmDataset* dataset = query.GetDcmtkObject().getDataset();
-      const char* sopClass = UID_FINDModalityWorklistInformationModel;
-
-      FindInternal(result, dataset, sopClass, true, NULL);
-    }
-  };
-
-
-  class DicomStoreUserConnection : public boost::noncopyable
-  {
-  private:
-    typedef std::map<std::string, std::set<DicomTransferSyntax> > StorageClasses;
-    
-    DicomAssociationParameters  parameters_;
-    DicomAssociation            association_;
-    StorageClasses              storageClasses_;
-    bool                        proposeCommonClasses_;
-    bool                        proposeUncompressedSyntaxes_;
-    bool                        proposeRetiredBigEndian_;
-
-
-    /**
-
-       Orthanc < 1.7.0:
-
-       Input        | Output
-       -------------+---------------------------------------------
-       Compressed   | Same transfer syntax
-       Uncompressed | Same transfer syntax, or other uncompressed
-
-       Orthanc >= 1.7.0:
-
-       Input        | Output
-       -------------+---------------------------------------------
-       Compressed   | Same transfer syntax, or uncompressed
-       Uncompressed | Same transfer syntax, or other uncompressed
-
-     **/
-
-
-    // Return "false" if there is not enough room remaining in the association
-    bool ProposeStorageClass(const std::string& sopClassUid,
-                             const std::set<DicomTransferSyntax>& syntaxes)
-    {
-      size_t requiredCount = syntaxes.size();
-      if (proposeUncompressedSyntaxes_)
-      {
-        requiredCount += 1;
-      }
-      
-      if (association_.GetRemainingPropositions() <= requiredCount)
-      {
-        return false;  // Not enough room
-      }
-      
-      for (std::set<DicomTransferSyntax>::const_iterator
-             it = syntaxes.begin(); it != syntaxes.end(); ++it)
-      {
-        association_.ProposePresentationContext(sopClassUid, *it);
-      }
-
-      if (proposeUncompressedSyntaxes_)
-      {
-        std::set<DicomTransferSyntax> uncompressed;
-        
-        if (syntaxes.find(DicomTransferSyntax_LittleEndianImplicit) == syntaxes.end())
-        {
-          uncompressed.insert(DicomTransferSyntax_LittleEndianImplicit);
-        }
-        
-        if (syntaxes.find(DicomTransferSyntax_LittleEndianExplicit) == syntaxes.end())
-        {
-          uncompressed.insert(DicomTransferSyntax_LittleEndianExplicit);
-        }
-        
-        if (proposeRetiredBigEndian_ &&
-            syntaxes.find(DicomTransferSyntax_BigEndianExplicit) == syntaxes.end())
-        {
-          uncompressed.insert(DicomTransferSyntax_BigEndianExplicit);
-        }
-
-        if (!uncompressed.empty())
-        {
-          association_.ProposePresentationContext(sopClassUid, uncompressed);
-        }
-      }      
-
-      return true;
-    }
-
-
-    bool LookupPresentationContext(uint8_t& presentationContextId,
-                                   const std::string& sopClassUid,
-                                   DicomTransferSyntax transferSyntax)
-    {
-      typedef std::map<DicomTransferSyntax, uint8_t>  PresentationContexts;
-
-      PresentationContexts pc;
-      if (association_.IsOpen() &&
-          association_.LookupAcceptedPresentationContext(pc, sopClassUid))
-      {
-        PresentationContexts::const_iterator found = pc.find(transferSyntax);
-        if (found != pc.end())
-        {
-          presentationContextId = found->second;
-          return true;
-        }
-      }
-
-      return false;
-    }
-    
-    
-    bool NegotiatePresentationContext(uint8_t& presentationContextId,
-                                      const std::string& sopClassUid,
-                                      DicomTransferSyntax transferSyntax)
-    {
-      /**
-       * Step 1: Check whether this presentation context is already
-       * available in the previously negociated assocation.
-       **/
-
-      if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax))
-      {
-        return true;
-      }
-
-      // The association must be re-negotiated
-      association_.ClearPresentationContexts();
-      PrepareStorageClass(sopClassUid, transferSyntax);
-
-      
-      /**
-       * Step 2: Propose at least the mandatory SOP class.
-       **/
-
-      {
-        StorageClasses::const_iterator mandatory = storageClasses_.find(sopClassUid);
-
-        if (mandatory == storageClasses_.end() ||
-            mandatory->second.find(transferSyntax) == mandatory->second.end())
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        if (!ProposeStorageClass(sopClassUid, mandatory->second))
-        {
-          // Should never happen in real life: There are no more than
-          // 128 transfer syntaxes in DICOM!
-          throw OrthancException(ErrorCode_InternalError,
-                                 "Too many transfer syntaxes for SOP class UID: " + sopClassUid);
-        }
-      }
-
-      
-      /**
-       * Step 3: Propose all the previously spotted SOP classes, as
-       * registered through the "PrepareStorageClass()" method.
-       **/
-      
-      for (StorageClasses::const_iterator it = storageClasses_.begin();
-           it != storageClasses_.end(); ++it)
-      {
-        if (it->first != sopClassUid)
-        {
-          ProposeStorageClass(it->first, it->second);
-        }
-      }
-      
-
-      /**
-       * Step 4: As long as there is room left in the proposed
-       * presentation contexts, propose the uncompressed transfer syntaxes
-       * for the most common SOP classes, as can be found in the
-       * "dcmShortSCUStorageSOPClassUIDs" array from DCMTK. The
-       * preferred transfer syntax is "LittleEndianImplicit".
-       **/
-
-      if (proposeCommonClasses_)
-      {
-        std::set<DicomTransferSyntax> ts;
-        ts.insert(DicomTransferSyntax_LittleEndianImplicit);
-        
-        for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++)
-        {
-          std::string c(dcmShortSCUStorageSOPClassUIDs[i]);
-          
-          if (c != sopClassUid &&
-              storageClasses_.find(c) == storageClasses_.end())
-          {
-            ProposeStorageClass(c, ts);
-          }
-        }
-      }
-
-
-      /**
-       * Step 5: Open the association, and check whether the pair (SOP
-       * class UID, transfer syntax) was accepted by the remote host.
-       **/
-
-      association_.Open(parameters_);
-      return LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax);
-    }
-    
-  public:
-    DicomStoreUserConnection(const DicomAssociationParameters& params) :
-      parameters_(params),
-      proposeCommonClasses_(true),
-      proposeUncompressedSyntaxes_(true),
-      proposeRetiredBigEndian_(false)
-    {
-    }
-    
-    const DicomAssociationParameters& GetParameters() const
-    {
-      return parameters_;
-    }
-
-    void SetCommonClassesProposed(bool proposed)
-    {
-      proposeCommonClasses_ = proposed;
-    }
-
-    bool IsCommonClassesProposed() const
-    {
-      return proposeCommonClasses_;
-    }
-
-    void SetUncompressedSyntaxesProposed(bool proposed)
-    {
-      proposeUncompressedSyntaxes_ = proposed;
-    }
-
-    bool IsUncompressedSyntaxesProposed() const
-    {
-      return proposeUncompressedSyntaxes_;
-    }
-
-    void SetRetiredBigEndianProposed(bool propose)
-    {
-      proposeRetiredBigEndian_ = propose;
-    }
-
-    bool IsRetiredBigEndianProposed() const
-    {
-      return proposeRetiredBigEndian_;
-    }      
-
-    void PrepareStorageClass(const std::string& sopClassUid,
-                             DicomTransferSyntax syntax)
-    {
-      StorageClasses::iterator found = storageClasses_.find(sopClassUid);
-
-      if (found == storageClasses_.end())
-      {
-        std::set<DicomTransferSyntax> ts;
-        ts.insert(syntax);
-        storageClasses_[sopClassUid] = ts;
-      }
-      else
-      {
-        found->second.insert(syntax);
-      }
-    }
-
-
-    void Toto(const std::string& sopClassUid,
-              DicomTransferSyntax transferSyntax)
-    {
-      uint8_t id;
-      
-      if (NegotiatePresentationContext(id, sopClassUid, transferSyntax))
-      {
-        printf("**** OK, without transcoding !! %d\n", id);
-      }
-      else
-      {
-        // Transcoding - only in Orthanc >= 1.7.0
-
-        const DicomTransferSyntax uncompressed[] = {
-          DicomTransferSyntax_LittleEndianImplicit,  // Default transfer syntax
-          DicomTransferSyntax_LittleEndianExplicit,
-          DicomTransferSyntax_BigEndianExplicit
-        };
-
-        bool found = false;
-        for (size_t i = 0; i < 3; i++)
-        {
-          if (LookupPresentationContext(id, sopClassUid, uncompressed[i]))
-          {
-            printf("**** TRANSCODING to %s => %d\n",
-                   GetTransferSyntaxUid(uncompressed[i]), id);
-            found = true;
-            break;
-          }
-        }
-
-        if (!found)
-        {
-          printf("**** KO KO KO\n");
-        }
-      }
-    }
-  };
-}
-
+#include "../Core/DicomNetworking/DicomAssociation.h"
+#include "../Core/DicomNetworking/DicomControlUserConnection.h"
+#include "../Core/DicomNetworking/DicomStoreUserConnection.h"
 
 TEST(Toto, DISABLED_DicomAssociation)
 {
@@ -4386,6 +2465,45 @@
 #endif
 }
 
+static void TestTranscode(DicomStoreUserConnection& scu,
+                          const std::string& sopClassUid,
+                          DicomTransferSyntax transferSyntax)
+{
+  uint8_t id;
+      
+  if (scu.NegotiatePresentationContext(id, sopClassUid, transferSyntax))
+  {
+    printf("**** OK, without transcoding !! %d\n", id);
+  }
+  else
+  {
+    // Transcoding - only in Orthanc >= 1.7.0
+
+    const DicomTransferSyntax uncompressed[] = {
+      DicomTransferSyntax_LittleEndianImplicit,  // Default transfer syntax
+      DicomTransferSyntax_LittleEndianExplicit,
+      DicomTransferSyntax_BigEndianExplicit
+    };
+
+    bool found = false;
+    for (size_t i = 0; i < 3; i++)
+    {
+      if (scu.LookupPresentationContext(id, sopClassUid, uncompressed[i]))
+      {
+        printf("**** TRANSCODING to %s => %d\n",
+               GetTransferSyntaxUid(uncompressed[i]), id);
+        found = true;
+        break;
+      }
+    }
+
+    if (!found)
+    {
+      printf("**** KO KO KO\n");
+    }
+  }
+}
+
 
 TEST(Toto, DISABLED_Store)
 {
@@ -4401,8 +2519,8 @@
 
   //assoc.SetUncompressedSyntaxesProposed(false);
   //assoc.SetCommonClassesProposed(false);
-  assoc.Toto(UID_MRImageStorage, DicomTransferSyntax_JPEG2000);
-  //assoc.Toto(UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit);
+  TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000);
+  //TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit);
 }
 
 #endif