changeset 3858:3ab2d48c8f69 c-get

integration mainline->c-get
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 21 Apr 2020 16:37:25 +0200
parents 4f78da5613a1 (current diff) dd0fcbf6a791 (diff)
children d30bce4bdae9
files Core/DicomNetworking/IDicomConnectionManager.h Core/Enumerations.h OrthancServer/main.cpp Plugins/Include/orthanc/OrthancCPlugin.h Resources/CMake/OrthancFrameworkConfiguration.cmake Resources/Configuration.json
diffstat 75 files changed, 3633 insertions(+), 536 deletions(-) [+]
line wrap: on
line diff
--- a/Core/DicomFormat/DicomMap.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/DicomFormat/DicomMap.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -745,7 +745,7 @@
   }
 
 
-  bool DicomMap::IsDicomFile(const char* dicom,
+  bool DicomMap::IsDicomFile(const void* dicom,
                              size_t size)
   {
     /**
@@ -755,16 +755,18 @@
      * account to determine whether the file is or is not a DICOM file.
      **/
 
+    const uint8_t* p = reinterpret_cast<const uint8_t*>(dicom);
+
     return (size >= 132 &&
-            dicom[128] == 'D' &&
-            dicom[129] == 'I' &&
-            dicom[130] == 'C' &&
-            dicom[131] == 'M');
+            p[128] == 'D' &&
+            p[129] == 'I' &&
+            p[130] == 'C' &&
+            p[131] == 'M');
   }
     
 
   bool DicomMap::ParseDicomMetaInformation(DicomMap& result,
-                                           const char* dicom,
+                                           const void* dicom,
                                            size_t size)
   {
     if (!IsDicomFile(dicom, size))
@@ -788,7 +790,7 @@
     DicomTag tag(0x0000, 0x0000);  // Dummy initialization
     ValueRepresentation vr;
     std::string value;
-    if (!ReadNextTag(tag, vr, value, dicom, size, position) ||
+    if (!ReadNextTag(tag, vr, value, reinterpret_cast<const char*>(dicom), size, position) ||
         tag.GetGroup() != 0x0002 ||
         tag.GetElement() != 0x0000 ||
         vr != ValueRepresentation_UnsignedLong ||
@@ -805,7 +807,7 @@
 
     while (position < stopPosition)
     {
-      if (ReadNextTag(tag, vr, value, dicom, size, position))
+      if (ReadNextTag(tag, vr, value, reinterpret_cast<const char*>(dicom), size, position))
       {
         result.SetValue(tag, value, IsBinaryValueRepresentation(vr));
       }
--- a/Core/DicomFormat/DicomMap.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/DicomFormat/DicomMap.h	Tue Apr 21 16:37:25 2020 +0200
@@ -180,11 +180,11 @@
 
     void GetTags(std::set<DicomTag>& tags) const;
 
-    static bool IsDicomFile(const char* dicom,
+    static bool IsDicomFile(const void* dicom,
                             size_t size);
     
     static bool ParseDicomMetaInformation(DicomMap& result,
-                                          const char* dicom,
+                                          const void* dicom,
                                           size_t size);
 
     void LogMissingTagsForStore() const;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomAssociation.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -0,0 +1,859 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "DicomAssociation.h"
+
+#if !defined(DCMTK_VERSION_NUMBER)
+#  error The macro DCMTK_VERSION_NUMBER must be defined
+#endif
+
+#include "../Compatibility.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "NetworkingCompatibility.h"
+
+#include <dcmtk/dcmnet/diutil.h>  // For dcmConnectionTimeout()
+#include <dcmtk/dcmdata/dcdeftag.h>
+
+namespace Orthanc
+{
+  static void FillSopSequence(DcmDataset& dataset,
+                              const DcmTagKey& tag,
+                              const std::vector<std::string>& sopClassUids,
+                              const std::vector<std::string>& sopInstanceUids,
+                              const std::vector<StorageCommitmentFailureReason>& failureReasons,
+                              bool hasFailureReasons)
+  {
+    assert(sopClassUids.size() == sopInstanceUids.size() &&
+           (hasFailureReasons ?
+            failureReasons.size() == sopClassUids.size() :
+            failureReasons.empty()));
+
+    if (sopInstanceUids.empty())
+    {
+      // Add an empty sequence
+      if (!dataset.insertEmptyElement(tag).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    else
+    {
+      for (size_t i = 0; i < sopClassUids.size(); i++)
+      {
+        std::unique_ptr<DcmItem> item(new DcmItem);
+        if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, sopClassUids[i].c_str()).good() ||
+            !item->putAndInsertString(DCM_ReferencedSOPInstanceUID, sopInstanceUids[i].c_str()).good() ||
+            (hasFailureReasons &&
+             !item->putAndInsertUint16(DCM_FailureReason, failureReasons[i]).good()) ||
+            !dataset.insertSequenceItem(tag, item.release()).good())
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    }
+  }                              
+
+
+  void DicomAssociation::Initialize()
+  {
+    role_ = DicomAssociationRole_Default;
+    isOpen_ = false;
+    net_ = NULL; 
+    params_ = NULL;
+    assoc_ = NULL;      
+
+    // Must be after "isOpen_ = false"
+    ClearPresentationContexts();
+  }
+
+    
+  void DicomAssociation::CheckConnecting(const DicomAssociationParameters& parameters,
+                                         const OFCondition& cond)
+  {
+    try
+    {
+      CheckCondition(cond, parameters, "connecting");
+    }
+    catch (OrthancException&)
+    {
+      CloseInternal();
+      throw;
+    }
+  }
+
+    
+  void DicomAssociation::CloseInternal()
+  {
+    if (assoc_ != NULL)
+    {
+      ASC_releaseAssociation(assoc_);
+      ASC_destroyAssociation(&assoc_);
+      assoc_ = NULL;
+      params_ = NULL;
+    }
+    else
+    {
+      if (params_ != NULL)
+      {
+        ASC_destroyAssociationParameters(&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& e)
+    {
+      // Don't throw exception in destructors
+      LOG(ERROR) << "Error while destroying a DICOM association: " << e.What();
+    }
+  }
+
+
+  void DicomAssociation::SetRole(DicomAssociationRole role)
+  {
+    if (role_ != role)
+    {
+      Close();
+      role_ = role;
+    }
+  }
+
+  
+  void DicomAssociation::ClearPresentationContexts()
+  {
+    Close();
+    proposed_.clear();
+    proposed_.reserve(MAX_PROPOSED_PRESENTATIONS);
+  }
+
+  
+  void DicomAssociation::Open(const DicomAssociationParameters& parameters)
+  {
+    if (isOpen_)
+    {
+      return;  // Already open
+    }
+
+    // Timeout used during association negociation and ASC_releaseAssociation()
+    uint32_t acseTimeout = parameters.GetTimeout();
+    if (acseTimeout == 0)
+    {
+      /**
+       * Timeout is disabled. Global timeout (seconds) for
+       * connecting to remote hosts.  Default value is -1 which
+       * selects infinite timeout, i.e. blocking connect().
+       **/
+      dcmConnectionTimeout.set(-1);
+      acseTimeout = 10;
+    }
+    else
+    {
+      dcmConnectionTimeout.set(acseTimeout);
+    }
+      
+    T_ASC_SC_ROLE dcmtkRole;
+    switch (role_)
+    {
+      case DicomAssociationRole_Default:
+        dcmtkRole = ASC_SC_ROLE_DEFAULT;
+        break;
+
+      case DicomAssociationRole_Scu:
+        dcmtkRole = ASC_SC_ROLE_SCU;
+        break;
+
+      case DicomAssociationRole_Scp:
+        dcmtkRole = ASC_SC_ROLE_SCP;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(net_ == NULL &&
+           params_ == NULL &&
+           assoc_ == NULL);
+
+    if (proposed_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "No presentation context was proposed");
+    }
+
+    LOG(INFO) << "Opening a DICOM SCU connection from AET \""
+              << parameters.GetLocalApplicationEntityTitle() 
+              << "\" to AET \"" << parameters.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,
+                             "DicomAssociation - " + 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	Tue Apr 21 16:37:25 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	Tue Apr 21 16:37:25 2020 +0200
@@ -0,0 +1,123 @@
+/**
+ * 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"
+
+#include "../Compatibility.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "NetworkingCompatibility.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;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomAssociationParameters.h	Tue Apr 21 16:37:25 2020 +0200
@@ -0,0 +1,128 @@
+/**
+ * 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);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomControlUserConnection.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -0,0 +1,681 @@
+/**
+ * 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 "../Compatibility.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()
+  {
+    assert(association_.get() != NULL);
+    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));
+    assert(association_.get() != 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)
+  {
+    assert(association_.get() != NULL);
+    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();
+  }
+    
+
+  DicomControlUserConnection::DicomControlUserConnection(const std::string& localAet,
+                                                         const RemoteModalityParameters& remote) :
+    parameters_(localAet, remote),
+    association_(new DicomAssociation)
+  {
+    SetupPresentationContexts();
+  }
+    
+
+  void DicomControlUserConnection::Close()
+  {
+    assert(association_.get() != NULL);
+    association_->Close();
+  }
+
+
+  bool DicomControlUserConnection::Echo()
+  {
+    assert(association_.get() != NULL);
+    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	Tue Apr 21 16:37:25 2020 +0200
@@ -0,0 +1,112 @@
+/**
+ * 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 std::string& localAet,
+                               const RemoteModalityParameters& remote);
+    
+    DicomControlUserConnection(const DicomAssociationParameters& params);
+    
+    const DicomAssociationParameters& GetParameters() const
+    {
+      return parameters_;
+    }
+
+    void Close();
+
+    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	Tue Apr 21 16:37:25 2020 +0200
@@ -0,0 +1,359 @@
+/**
+ * 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 "../DicomParsing/FromDcmtkBridge.h"
+#include "../DicomParsing/ParsedDicomFile.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "DicomAssociation.h"
+
+#include <dcmtk/dcmdata/dcdeftag.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);
+  }
+
+
+  void DicomStoreUserConnection::Store(std::string& sopClassUid,
+                                       std::string& sopInstanceUid,
+                                       DcmDataset& dataset,
+                                       const std::string& moveOriginatorAET,
+                                       uint16_t moveOriginatorID)
+  {
+    OFString a, b;
+    if (!dataset.findAndGetOFString(DCM_SOPClassUID, a).good() ||
+        !dataset.findAndGetOFString(DCM_SOPInstanceUID, b).good())
+    {
+      throw OrthancException(ErrorCode_NoSopClassOrInstance,
+                             "Unable to determine the SOP class/instance for C-STORE with AET " +
+                             parameters_.GetRemoteApplicationEntityTitle());
+    }
+
+    sopClassUid.assign(a.c_str());
+    sopInstanceUid.assign(b.c_str());
+
+    DicomTransferSyntax transferSyntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(
+          transferSyntax, dataset.getOriginalXfer()))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Unknown transfer syntax from DCMTK");
+    }
+
+    // Figure out which accepted presentation context should be used
+    uint8_t presID;
+    if (!NegotiatePresentationContext(presID, sopClassUid.c_str(), transferSyntax))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "No valid presentation context was negotiated upfront");
+    }
+    
+    // Prepare the transmission of data
+    T_DIMSE_C_StoreRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    strncpy(request.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN);
+    request.Priority = DIMSE_PRIORITY_MEDIUM;
+    request.DataSetType = DIMSE_DATASET_PRESENT;
+    strncpy(request.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN);
+
+    if (!moveOriginatorAET.empty())
+    {
+      strncpy(request.MoveOriginatorApplicationEntityTitle, 
+              moveOriginatorAET.c_str(), DIC_AE_LEN);
+      request.opts = O_STORE_MOVEORIGINATORAETITLE;
+
+      request.MoveOriginatorID = moveOriginatorID;  // The type DIC_US is an alias for uint16_t
+      request.opts |= O_STORE_MOVEORIGINATORID;
+    }
+
+    // Finally conduct transmission of data
+    T_DIMSE_C_StoreRSP response;
+    DcmDataset* statusDetail = NULL;
+    DicomAssociation::CheckCondition(
+      DIMSE_storeUser(&association_->GetDcmtkAssociation(), presID, &request,
+                      NULL, &dataset, /*progressCallback*/ NULL, NULL,
+                      /*opt_blockMode*/ (GetParameters().HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                      /*opt_dimse_timeout*/ GetParameters().GetTimeout(),
+                      &response, &statusDetail, NULL),
+      GetParameters(), "C-STORE");
+
+    if (statusDetail != NULL) 
+    {
+      delete statusDetail;
+    }
+    
+    /**
+     * New in Orthanc 1.6.0: Deal with failures during C-STORE.
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_B.2.3.html#table_B.2-1
+     **/
+    
+    if (response.DimseStatus != 0x0000 &&  // Success
+        response.DimseStatus != 0xB000 &&  // Warning - Coercion of Data Elements
+        response.DimseStatus != 0xB007 &&  // Warning - Data Set does not match SOP Class
+        response.DimseStatus != 0xB006)    // Warning - Elements Discarded
+    {
+      char buf[16];
+      sprintf(buf, "%04X", response.DimseStatus);
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "C-STORE SCU to AET \"" +
+                             GetParameters().GetRemoteApplicationEntityTitle() +
+                             "\" has failed with DIMSE status 0x" + buf);
+    }
+  }
+
+
+  void DicomStoreUserConnection::Store(std::string& sopClassUid,
+                                       std::string& sopInstanceUid,
+                                       ParsedDicomFile& parsed,
+                                       const std::string& moveOriginatorAET,
+                                       uint16_t moveOriginatorID)
+  {
+    Store(sopClassUid, sopInstanceUid, *parsed.GetDcmtkObject().getDataset(),
+          moveOriginatorAET, moveOriginatorID);
+  }
+
+
+  void DicomStoreUserConnection::Store(std::string& sopClassUid,
+                                       std::string& sopInstanceUid,
+                                       const void* buffer,
+                                       size_t size,
+                                       const std::string& moveOriginatorAET,
+                                       uint16_t moveOriginatorID)
+  {
+    std::unique_ptr<DcmFileFormat> dicom(
+      FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size));
+
+    Store(sopClassUid, sopInstanceUid, *dicom->getDataset(),
+          moveOriginatorAET, moveOriginatorID);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/DicomStoreUserConnection.h	Tue Apr 21 16:37:25 2020 +0200
@@ -0,0 +1,155 @@
+/**
+ * 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
+
+
+class DcmDataset;
+
+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 ParsedDicomFile;
+
+  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);
+        
+    // Should only be used if transcoding
+    bool LookupPresentationContext(uint8_t& presentationContextId,
+                                   const std::string& sopClassUid,
+                                   DicomTransferSyntax transferSyntax);
+
+  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);
+
+    // TODO => to private
+    bool NegotiatePresentationContext(uint8_t& presentationContextId,
+                                      const std::string& sopClassUid,
+                                      DicomTransferSyntax transferSyntax);
+
+    void Store(std::string& sopClassUid,
+               std::string& sopInstanceUid,
+               DcmDataset& dataset,
+               const std::string& moveOriginatorAET,
+               uint16_t moveOriginatorID);
+
+    void Store(std::string& sopClassUid,
+               std::string& sopInstanceUid,
+               ParsedDicomFile& parsed,
+               const std::string& moveOriginatorAET,
+               uint16_t moveOriginatorID);
+
+    void Store(std::string& sopClassUid,
+               std::string& sopInstanceUid,
+               const void* buffer,
+               size_t size,
+               const std::string& moveOriginatorAET,
+               uint16_t moveOriginatorID);
+  };
+}
--- a/Core/DicomNetworking/DicomUserConnection.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/DicomNetworking/DicomUserConnection.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -92,6 +92,7 @@
 #include "../OrthancException.h"
 #include "../DicomParsing/FromDcmtkBridge.h"
 #include "../DicomParsing/ToDcmtkBridge.h"
+#include "NetworkingCompatibility.h"
 
 #include <dcmtk/dcmdata/dcdeftag.h>
 #include <dcmtk/dcmdata/dcfilefo.h>
@@ -103,30 +104,6 @@
 #include <set>
 
 
-#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
-
-
 static const char* DEFAULT_PREFERRED_TRANSFER_SYNTAX = UID_LittleEndianImplicitTransferSyntax;
 
 /**
--- a/Core/DicomNetworking/IDicomConnectionManager.h	Fri Mar 27 10:06:58 2020 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-/**
- * 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 !defined(ORTHANC_ENABLE_DCMTK_NETWORKING)
-#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be defined
-#endif
-
-#if ORTHANC_ENABLE_DCMTK_NETWORKING == 0
-
-namespace Orthanc
-{
-  // DICOM networking is disabled, this is just a void class
-  class IDicomConnectionManager : public boost::noncopyable
-  {
-  public:
-    virtual ~IDicomConnectionManager()
-    {
-    }
-  };
-}
-
-#else
-
-#include "DicomUserConnection.h"
-
-namespace Orthanc
-{
-  class IDicomConnectionManager : public boost::noncopyable
-  {
-  public:
-    virtual ~IDicomConnectionManager()
-    {
-    }
-
-    class IResource : public boost::noncopyable
-    {
-    public:
-      virtual ~IResource()
-      {
-      }
-
-      virtual DicomUserConnection& GetConnection() = 0;
-    };
-
-    virtual IResource* AcquireConnection(const std::string& localAet,
-                                         const RemoteModalityParameters& remote) = 0;
-  };
-}
-
-#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/NetworkingCompatibility.h	Tue Apr 21 16:37:25 2020 +0200
@@ -0,0 +1,58 @@
+/**
+ * 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
+
+
+#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
--- a/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -44,64 +44,59 @@
     return boost::posix_time::microsec_clock::universal_time();
   }
 
-  class TimeoutDicomConnectionManager::Resource : public IDicomConnectionManager::IResource
+
+  TimeoutDicomConnectionManager::Lock::Lock(TimeoutDicomConnectionManager& that,
+                                            const std::string& localAet,
+                                            const RemoteModalityParameters& remote) : 
+    that_(that),
+    lock_(that_.mutex_)
   {
-  private:
-    TimeoutDicomConnectionManager&  that_;
+    // Calling "Touch()" will be done by the "~Lock()" destructor
+    that_.OpenInternal(localAet, remote);
+  }
 
-  public:
-    Resource(TimeoutDicomConnectionManager& that) : 
-      that_(that)
+  
+  TimeoutDicomConnectionManager::Lock::~Lock()
+  {
+    that_.TouchInternal();
+  }
+
+  
+  DicomUserConnection& TimeoutDicomConnectionManager::Lock::GetConnection()
+  {
+    if (that_.connection_.get() == NULL)
     {
-      if (that_.connection_.get() == NULL)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
+      // The allocation should have been done by "that_.Open()" in the constructor
+      throw OrthancException(ErrorCode_InternalError);
     }
-
-    ~Resource()
+    else
     {
-      that_.Touch();
-    }
-
-    DicomUserConnection& GetConnection()
-    {
-      assert(that_.connection_.get() != NULL);
       return *that_.connection_;
     }
-  };
+  }
 
 
-  void TimeoutDicomConnectionManager::Touch()
+  // Mutex must be locked
+  void TimeoutDicomConnectionManager::TouchInternal()
   {
     lastUse_ = GetNow();
   }
 
 
-  void TimeoutDicomConnectionManager::CheckTimeoutInternal()
+  // Mutex must be locked
+  void TimeoutDicomConnectionManager::OpenInternal(const std::string& localAet,
+                                                   const RemoteModalityParameters& remote)
   {
-    if (connection_.get() != NULL &&
-        (GetNow() - lastUse_) >= timeout_)
+    if (connection_.get() == NULL ||
+        !connection_->IsSameAssociation(localAet, remote))
     {
-      Close();
+      connection_.reset(new DicomUserConnection(localAet, remote));
     }
   }
 
 
-  void TimeoutDicomConnectionManager::SetTimeout(unsigned int timeout)
-  {
-    timeout_ = boost::posix_time::milliseconds(timeout);
-    CheckTimeoutInternal();
-  }
-
-
-  unsigned int TimeoutDicomConnectionManager::GetTimeout()
-  {
-    return static_cast<unsigned int>(timeout_.total_milliseconds());
-  }
-
-
-  void TimeoutDicomConnectionManager::Close()
+  // Mutex must be locked
+  void TimeoutDicomConnectionManager::CloseInternal()
   {
     if (connection_.get() != NULL)
     {
@@ -113,22 +108,29 @@
   }
 
 
-  void TimeoutDicomConnectionManager::CheckTimeout()
+  void TimeoutDicomConnectionManager::SetInactivityTimeout(unsigned int milliseconds)
   {
-    CheckTimeoutInternal();
+    boost::mutex::scoped_lock lock(mutex_);
+    timeout_ = boost::posix_time::milliseconds(milliseconds);
+    CloseInternal();
   }
 
 
-  IDicomConnectionManager::IResource* 
-  TimeoutDicomConnectionManager::AcquireConnection(const std::string& localAet,
-                                                   const RemoteModalityParameters& remote)
+  unsigned int TimeoutDicomConnectionManager::GetInactivityTimeout()
   {
-    if (connection_.get() == NULL ||
-        !connection_->IsSameAssociation(localAet, remote))
+    boost::mutex::scoped_lock lock(mutex_);
+    return static_cast<unsigned int>(timeout_.total_milliseconds());
+  }
+
+
+  void TimeoutDicomConnectionManager::CloseIfInactive()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (connection_.get() != NULL &&
+        (GetNow() - lastUse_) >= timeout_)
     {
-      connection_.reset(new DicomUserConnection(localAet, remote));
+      CloseInternal();
     }
-
-    return new Resource(*this);
   }
 }
--- a/Core/DicomNetworking/TimeoutDicomConnectionManager.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.h	Tue Apr 21 16:37:25 2020 +0200
@@ -33,72 +33,72 @@
 
 #pragma once
 
-#include "IDicomConnectionManager.h"
+#if !defined(ORTHANC_ENABLE_DCMTK_NETWORKING)
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be defined
+#endif
 
-#if ORTHANC_ENABLE_DCMTK_NETWORKING == 0
+#if ORTHANC_ENABLE_DCMTK_NETWORKING != 1
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be 1 to use this file
+#endif
+
+
+#include "../Compatibility.h"
+#include "DicomUserConnection.h"
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/thread/mutex.hpp>
 
 namespace Orthanc
 {
-  class TimeoutDicomConnectionManager : public IDicomConnectionManager
-  {
-  public:
-    void SetTimeout(unsigned int timeout)
-    {
-    }
-
-    unsigned int GetTimeout()
-    {
-      return 0;
-    }
-
-    void Close()
-    {
-    }
-
-    void CheckTimeout()
-    {
-    }
-  };
-}
-
-#else
-
-#include "../Compatibility.h"
-
-#include <boost/date_time/posix_time/posix_time.hpp>
-
-namespace Orthanc
-{
-  class TimeoutDicomConnectionManager : public IDicomConnectionManager
+  /**
+   * This class corresponds to a singleton to a DICOM SCU connection.
+   **/
+  class TimeoutDicomConnectionManager : public boost::noncopyable
   {
   private:
-    class Resource;
-
+    boost::mutex                          mutex_;
     std::unique_ptr<DicomUserConnection>  connection_;
     boost::posix_time::ptime              lastUse_;
     boost::posix_time::time_duration      timeout_;
 
-    void Touch();
+    // Mutex must be locked
+    void TouchInternal();
 
-    void CheckTimeoutInternal();
+    // Mutex must be locked
+    void OpenInternal(const std::string& localAet,
+                      const RemoteModalityParameters& remote);
+
+    // Mutex must be locked
+    void CloseInternal();
 
   public:
+    class Lock : public boost::noncopyable
+    {
+    private:
+      TimeoutDicomConnectionManager&  that_;
+      boost::mutex::scoped_lock       lock_;
+
+    public:
+      Lock(TimeoutDicomConnectionManager& that,
+           const std::string& localAet,
+           const RemoteModalityParameters& remote);
+      
+      ~Lock();
+
+      DicomUserConnection& GetConnection();
+    };
+
     TimeoutDicomConnectionManager() :
       timeout_(boost::posix_time::milliseconds(1000))
     {
     }
 
-    void SetTimeout(unsigned int timeout);
+    void SetInactivityTimeout(unsigned int milliseconds);
 
-    unsigned int GetTimeout();
+    unsigned int GetInactivityTimeout();  // In milliseconds
 
     void Close();
 
-    void CheckTimeout();
-
-    virtual IResource* AcquireConnection(const std::string& localAet,
-                                         const RemoteModalityParameters& remote);
+    void CloseIfInactive();
   };
 }
-
-#endif
--- a/Core/DicomParsing/ParsedDicomFile.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/DicomParsing/ParsedDicomFile.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -952,7 +952,7 @@
        * equals the empty string, then proceed. In Orthanc <= 1.5.6,
        * an exception "Bad file format" was generated.
        * https://groups.google.com/d/msg/orthanc-users/aphG_h1AHVg/rfOTtTPTAgAJ
-       * https://bitbucket.org/sjodogne/orthanc/commits/4c45e018bd3de3cfa21d6efc6734673aaaee4435
+       * https://hg.orthanc-server.com/orthanc/rev/4c45e018bd3de3cfa21d6efc6734673aaaee4435
        **/
       patientId.clear();
     }        
--- a/Core/Enumerations.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/Enumerations.h	Tue Apr 21 16:37:25 2020 +0200
@@ -748,6 +748,14 @@
   };
 
 
+  enum DicomAssociationRole
+  {
+    DicomAssociationRole_Default,
+    DicomAssociationRole_Scu,
+    DicomAssociationRole_Scp
+  };
+  
+
   /**
    * WARNING: Do not change the explicit values in the enumerations
    * below this point. This would result in incompatible databases
--- a/Core/HttpServer/HttpServer.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/HttpServer/HttpServer.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -170,7 +170,7 @@
 
 
 
-  class ChunkStore
+  class ChunkStore : public boost::noncopyable
   {
   private:
     typedef std::list<ChunkedFile*>  Content;
@@ -308,7 +308,7 @@
                                                   struct mg_connection *connection,
                                                   const std::string& contentLength)
   {
-    int length;      
+    int length;
     try
     {
       length = boost::lexical_cast<int>(contentLength);
@@ -904,7 +904,6 @@
       }
     }
 
-
     if (!found && 
         server.HasHandler())
     {
--- a/Core/Images/PamReader.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/Images/PamReader.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -42,6 +42,7 @@
 #  include "../SystemToolbox.h"
 #endif
 
+#include <stdlib.h>  // For malloc/free
 #include <boost/algorithm/string/find.hpp>
 #include <boost/lexical_cast.hpp>
 
@@ -200,7 +201,30 @@
     }
 
     size_t offset = content_.size() - pitch * height;
-    AssignWritable(format, width, height, pitch, &content_[offset]);
+
+    {
+      intptr_t bufferAddr = reinterpret_cast<intptr_t>(&content_[offset]);
+      if((bufferAddr % 8) == 0)
+        LOG(TRACE) << "PamReader::ParseContent() image address = " << bufferAddr;
+      else
+        LOG(TRACE) << "PamReader::ParseContent() image address = " << bufferAddr << " (not a multiple of 8!)";
+    }
+    
+    // if we want to enforce alignment, we need to use a freshly allocated
+    // buffer, since we have no alignment guarantees on the original one
+    if (enforceAligned_)
+    {
+      if (alignedImageBuffer_ != NULL)
+        free(alignedImageBuffer_);
+      alignedImageBuffer_ = malloc(pitch * height);
+      memcpy(alignedImageBuffer_, &content_[offset], pitch* height);
+      content_ = "";
+      AssignWritable(format, width, height, pitch, alignedImageBuffer_);
+    }
+    else
+    {
+      AssignWritable(format, width, height, pitch, &content_[offset]);
+    }
 
     // Byte swapping if needed
     if (bytesPerChannel != 1 &&
@@ -231,7 +255,7 @@
             at SAFE_HEAP_LOAD_i32_2_2 (wasm-function[251132]:39)
             at __ZN7Orthanc9PamReader12ParseContentEv (wasm-function[11457]:8088)
 
-          Web Assenmbly IS LITTLE ENDIAN!
+          Web Assembly IS LITTLE ENDIAN!
 
           Perhaps in htobe16 ?
           */
@@ -274,4 +298,12 @@
     content_.assign(reinterpret_cast<const char*>(buffer), size);
     ParseContent();
   }
+
+  PamReader::~PamReader()
+  {
+    if (alignedImageBuffer_ != NULL)
+    {
+      free(alignedImageBuffer_);
+    }
+  }
 }
--- a/Core/Images/PamReader.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/Images/PamReader.h	Tue Apr 21 16:37:25 2020 +0200
@@ -46,9 +46,41 @@
   private:
     void ParseContent();
     
+    /**
+    Whether we want to use the default malloc alignment in the image buffer,
+    at the expense of an extra copy
+    */
+    bool enforceAligned_;
+
+    /**
+    This is actually a copy of wrappedContent_, but properly aligned.
+
+    It is only used if the enforceAligned parameter is set to true in the
+    constructor.
+    */
+    void* alignedImageBuffer_;
+    
+    /**
+    Points somewhere in the content_ buffer.      
+    */
+    ImageAccessor wrappedContent_;
+
+    /**
+    Raw content (file bytes or answer from the server, for instance). 
+    */
     std::string content_;
 
   public:
+    /**
+    See doc for field enforceAligned_
+    */
+    PamReader(bool enforceAligned = false) :
+      enforceAligned_(enforceAligned),
+      alignedImageBuffer_(NULL)
+    {
+    }
+
+    virtual ~PamReader();
 
 #if ORTHANC_SANDBOXED == 0
     void ReadFromFile(const std::string& filename);
--- a/Core/JobsEngine/Operations/IJobOperation.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/JobsEngine/Operations/IJobOperation.h	Tue Apr 21 16:37:25 2020 +0200
@@ -34,7 +34,6 @@
 #pragma once
 
 #include "JobOperationValues.h"
-#include "../../DicomNetworking/IDicomConnectionManager.h"
 
 namespace Orthanc
 {
@@ -46,8 +45,7 @@
     }
 
     virtual void Apply(JobOperationValues& outputs,
-                       const JobOperationValue& input,
-                       IDicomConnectionManager& dicomConnection) = 0;
+                       const JobOperationValue& input) = 0;
 
     virtual void Serialize(Json::Value& result) const = 0;
   };
--- a/Core/JobsEngine/Operations/LogJobOperation.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/JobsEngine/Operations/LogJobOperation.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -40,8 +40,7 @@
 namespace Orthanc
 {
   void LogJobOperation::Apply(JobOperationValues& outputs,
-                              const JobOperationValue& input,
-                              IDicomConnectionManager& connectionManager)
+                              const JobOperationValue& input)
   {
     switch (input.GetType())
     {
--- a/Core/JobsEngine/Operations/LogJobOperation.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/JobsEngine/Operations/LogJobOperation.h	Tue Apr 21 16:37:25 2020 +0200
@@ -41,8 +41,7 @@
   {
   public:
     virtual void Apply(JobOperationValues& outputs,
-                       const JobOperationValue& input,
-                       IDicomConnectionManager& connectionManager);
+                       const JobOperationValue& input);
 
     virtual void Serialize(Json::Value& result) const
     {
--- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -43,7 +43,6 @@
 {
   static const char* CURRENT = "Current";
   static const char* DESCRIPTION = "Description";
-  static const char* DICOM_TIMEOUT = "DicomTimeout";
   static const char* NEXT_OPERATIONS = "Next";
   static const char* OPERATION = "Operation";
   static const char* OPERATIONS = "Operations";
@@ -127,7 +126,7 @@
       return currentInput_ >= originalInputs_->GetSize() + workInputs_->GetSize();
     }
 
-    void Step(IDicomConnectionManager& connectionManager)
+    void Step()
     {
       if (IsDone())
       {
@@ -146,7 +145,7 @@
       }
 
       JobOperationValues outputs;
-      operation_->Apply(outputs, *input, connectionManager);
+      operation_->Apply(outputs, *input);
 
       if (!nextOperations_.empty())
       {
@@ -254,12 +253,6 @@
   }
 
   
-  void SequenceOfOperationsJob::Lock::SetDicomAssociationTimeout(unsigned int timeout)
-  {
-    that_.connectionManager_.SetTimeout(timeout);
-  }
-
-
   size_t SequenceOfOperationsJob::Lock::AddOperation(IJobOperation* operation)
   {
     if (IsDone())
@@ -341,7 +334,6 @@
           (*it)->SignalDone(*this);
         }
 
-        connectionManager_.Close();
         return JobStepResult::Success();
       }
       else
@@ -360,11 +352,9 @@
 
     if (current_ < operations_.size())
     {
-      operations_[current_]->Step(connectionManager_);
+      operations_[current_]->Step();
     }
 
-    connectionManager_.CheckTimeout();
-
     return JobStepResult::Continue();
   }
 
@@ -383,13 +373,6 @@
   }
 
 
-  void SequenceOfOperationsJob::Stop(JobStopReason reason)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    connectionManager_.Close();
-  }
-
-
   float SequenceOfOperationsJob::GetProgress()
   {
     boost::mutex::scoped_lock lock(mutex_);
@@ -420,7 +403,6 @@
     
     value[DESCRIPTION] = description_;
     value[TRAILING_TIMEOUT] = static_cast<unsigned int>(trailingTimeout_.total_milliseconds());
-    value[DICOM_TIMEOUT] = connectionManager_.GetTimeout();
     value[CURRENT] = static_cast<unsigned int>(current_);
     
     Json::Value tmp = Json::arrayValue;
@@ -454,8 +436,6 @@
     description_ = SerializationToolbox::ReadString(serialized, DESCRIPTION);
     trailingTimeout_ = boost::posix_time::milliseconds
       (SerializationToolbox::ReadUnsignedInteger(serialized, TRAILING_TIMEOUT));
-    connectionManager_.SetTimeout
-      (SerializationToolbox::ReadUnsignedInteger(serialized, DICOM_TIMEOUT));
     current_ = SerializationToolbox::ReadUnsignedInteger(serialized, CURRENT);
 
     const Json::Value& ops = serialized[OPERATIONS];
--- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h	Tue Apr 21 16:37:25 2020 +0200
@@ -36,8 +36,6 @@
 #include "../IJob.h"
 #include "IJobOperation.h"
 
-#include "../../DicomNetworking/TimeoutDicomConnectionManager.h"
-
 #include <boost/thread/mutex.hpp>
 #include <boost/thread/condition_variable.hpp>
 
@@ -69,7 +67,6 @@
     boost::condition_variable         operationAdded_;
     boost::posix_time::time_duration  trailingTimeout_;
     std::list<IObserver*>             observers_;
-    TimeoutDicomConnectionManager     connectionManager_;
 
     void NotifyDone() const;
 
@@ -109,8 +106,6 @@
       }
 
       void SetTrailingOperationTimeout(unsigned int timeout);
-
-      void SetDicomAssociationTimeout(unsigned int timeout);
       
       size_t AddOperation(IJobOperation* operation);
 
@@ -134,7 +129,9 @@
 
     virtual void Reset() ORTHANC_OVERRIDE;
 
-    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
+    {
+    }
 
     virtual float GetProgress() ORTHANC_OVERRIDE;
 
--- a/NEWS	Fri Mar 27 10:06:58 2020 -0400
+++ b/NEWS	Tue Apr 21 16:37:25 2020 +0200
@@ -2,6 +2,9 @@
 ===============================
 
 
+Version 1.6.1 (2020-04-21)
+==========================
+
 REST API
 --------
 
@@ -10,6 +13,19 @@
   - "/modalities/{id}/store-straight": Synchronously send the DICOM instance in POST
     body to another modality (alternative to command-line tools such as "storescu")
 
+Plugins
+-------
+
+* New functions in the SDK:
+  - OrthancPluginRegisterIncomingDicomInstanceFilter()
+  - OrthancPluginGetInstanceTransferSyntaxUid()
+  - OrthancPluginHasInstancePixelData()
+
+Lua
+---
+
+* New "info" field in "ReceivedInstanceFilter()" callback, containing
+  "HasPixelData" and "TransferSyntaxUID" information
 
 Maintenance
 -----------
@@ -19,6 +35,8 @@
 * Fix lookup form in Orthanc Explorer (wildcards not allowed in StudyDate)
 * Fix signature of "OrthancPluginRegisterStorageCommitmentScpCallback()" in plugins SDK
 * Error reporting on failure while initializing SSL
+* Fix unit test ParsedDicomFile.ToJsonFlags2 on big-endian architectures
+* Avoid one memcpy of the DICOM buffer on "POST /instances"
 * Upgraded dependencies for static builds (notably on Windows):
   - civetweb 1.12
   - openssl 1.1.1f
--- a/OrthancServer/DicomInstanceToStore.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/DicomInstanceToStore.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -150,18 +150,48 @@
   {
   public:
     DicomInstanceOrigin                  origin_;
-    SmartContainer<std::string>          buffer_;
+    bool                                 hasBuffer_;
+    std::unique_ptr<std::string>         ownBuffer_;
+    const void*                          bufferData_;
+    size_t                               bufferSize_;
     SmartContainer<ParsedDicomFile>      parsed_;
     SmartContainer<DicomMap>             summary_;
     SmartContainer<Json::Value>          json_;
     MetadataMap                          metadata_;
 
+    PImpl() :
+      hasBuffer_(false),
+      bufferData_(NULL),
+      bufferSize_(0)
+    {
+    }
+
   private:
     std::unique_ptr<DicomInstanceHasher>  hasher_;
 
+    void ParseDicomFile()
+    {
+      if (!parsed_.HasContent())
+      {
+        if (!hasBuffer_)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      
+        if (ownBuffer_.get() != NULL)
+        {
+          parsed_.TakeOwnership(new ParsedDicomFile(*ownBuffer_));
+        }
+        else
+        {
+          parsed_.TakeOwnership(new ParsedDicomFile(bufferData_, bufferSize_));
+        }
+      }
+    }
+
     void ComputeMissingInformation()
     {
-      if (buffer_.HasContent() &&
+      if (hasBuffer_ &&
           summary_.HasContent() &&
           json_.HasContent())
       {
@@ -169,7 +199,7 @@
         return; 
       }
     
-      if (!buffer_.HasContent())
+      if (!hasBuffer_)
       {
         if (!parsed_.HasContent())
         {
@@ -186,13 +216,15 @@
         }
 
         // Serialize the parsed DICOM file
-        buffer_.Allocate();
-        if (!FromDcmtkBridge::SaveToMemoryBuffer(buffer_.GetContent(), 
+        ownBuffer_.reset(new std::string);
+        if (!FromDcmtkBridge::SaveToMemoryBuffer(*ownBuffer_,
                                                  *parsed_.GetContent().GetDcmtkObject().getDataset()))
         {
           throw OrthancException(ErrorCode_InternalError,
                                  "Unable to serialize a DICOM file to a memory buffer");
         }
+
+        hasBuffer_ = true;
       }
 
       if (summary_.HasContent() &&
@@ -205,10 +237,8 @@
       // memory buffer, but that its summary or its JSON version is
       // missing
 
-      if (!parsed_.HasContent())
-      {
-        parsed_.TakeOwnership(new ParsedDicomFile(buffer_.GetConstContent()));
-      }
+      ParseDicomFile();
+      assert(parsed_.HasContent());
 
       // At this point, we have parsed the DICOM file
     
@@ -232,22 +262,38 @@
 
 
   public:
-    const char* GetBufferData()
+    void SetBuffer(const void* data,
+                   size_t size)
+    {
+      ownBuffer_.reset(NULL);
+      bufferData_ = data;
+      bufferSize_ = size;
+      hasBuffer_ = true;
+    }
+    
+    const void* GetBufferData()
     {
       ComputeMissingInformation();
-    
-      if (!buffer_.HasContent())
+
+      if (!hasBuffer_)
       {
         throw OrthancException(ErrorCode_InternalError);
       }
 
-      if (buffer_.GetConstContent().size() == 0)
+      if (ownBuffer_.get() != NULL)
       {
-        return NULL;
+        if (ownBuffer_->empty())
+        {
+          return NULL;
+        }
+        else
+        {
+          return ownBuffer_->c_str();
+        }
       }
       else
       {
-        return buffer_.GetConstContent().c_str();
+        return bufferData_;
       }
     }
 
@@ -256,12 +302,19 @@
     {
       ComputeMissingInformation();
     
-      if (!buffer_.HasContent())
+      if (!hasBuffer_)
       {
         throw OrthancException(ErrorCode_InternalError);
       }
 
-      return buffer_.GetConstContent().size();
+      if (ownBuffer_.get() != NULL)
+      {
+        return ownBuffer_->size();
+      }
+      else
+      {
+        return bufferSize_;
+      }
     }
 
 
@@ -326,6 +379,22 @@
 
       return false;
     }
+
+
+    bool HasPixelData()
+    {
+      ComputeMissingInformation();
+      ParseDicomFile();
+      
+      if (parsed_.HasContent())
+      {
+        return parsed_.GetContent().HasTag(DICOM_TAG_PIXEL_DATA);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
   };
 
 
@@ -347,9 +416,10 @@
   }
 
     
-  void DicomInstanceToStore::SetBuffer(const std::string& dicom)
+  void DicomInstanceToStore::SetBuffer(const void* dicom,
+                                       size_t size)
   {
-    pimpl_->buffer_.SetConstReference(dicom);
+    pimpl_->SetBuffer(dicom, size);
   }
 
 
@@ -391,15 +461,15 @@
   }
 
 
-  const char* DicomInstanceToStore::GetBufferData()
+  const void* DicomInstanceToStore::GetBufferData() const
   {
-    return pimpl_->GetBufferData();
+    return const_cast<PImpl&>(*pimpl_).GetBufferData();
   }
 
 
-  size_t DicomInstanceToStore::GetBufferSize()
+  size_t DicomInstanceToStore::GetBufferSize() const
   {
-    return pimpl_->GetBufferSize();
+    return const_cast<PImpl&>(*pimpl_).GetBufferSize();
   }
 
 
@@ -409,15 +479,15 @@
   }
 
     
-  const Json::Value& DicomInstanceToStore::GetJson()
+  const Json::Value& DicomInstanceToStore::GetJson() const
   {
-    return pimpl_->GetJson();
+    return const_cast<PImpl&>(*pimpl_).GetJson();
   }
 
 
-  bool DicomInstanceToStore::LookupTransferSyntax(std::string& result)
+  bool DicomInstanceToStore::LookupTransferSyntax(std::string& result) const
   {
-    return pimpl_->LookupTransferSyntax(result);
+    return const_cast<PImpl&>(*pimpl_).LookupTransferSyntax(result);
   }
 
 
@@ -425,4 +495,9 @@
   {
     return pimpl_->GetHasher();
   }
+
+  bool DicomInstanceToStore::HasPixelData() const
+  {
+    return const_cast<PImpl&>(*pimpl_).HasPixelData();
+  }
 }
--- a/OrthancServer/DicomInstanceToStore.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/DicomInstanceToStore.h	Tue Apr 21 16:37:25 2020 +0200
@@ -44,7 +44,7 @@
 {
   class ParsedDicomFile;
 
-  class DicomInstanceToStore
+  class DicomInstanceToStore : public boost::noncopyable
   {
   public:
     typedef std::map<std::pair<ResourceType, MetadataType>, std::string>  MetadataMap;
@@ -59,8 +59,11 @@
     void SetOrigin(const DicomInstanceOrigin& origin);
     
     const DicomInstanceOrigin& GetOrigin() const;
-    
-    void SetBuffer(const std::string& dicom);
+
+    // WARNING: The buffer is not copied, it must not be removed as
+    // long as the "DicomInstanceToStore" object is alive
+    void SetBuffer(const void* dicom,
+                   size_t size);
 
     void SetParsedDicomFile(ParsedDicomFile& parsed);
 
@@ -76,16 +79,18 @@
                      MetadataType metadata,
                      const std::string& value);
 
-    const char* GetBufferData();
+    const void* GetBufferData() const;
 
-    size_t GetBufferSize();
+    size_t GetBufferSize() const;
 
     const DicomMap& GetSummary();
     
-    const Json::Value& GetJson();
+    const Json::Value& GetJson() const;
 
-    bool LookupTransferSyntax(std::string& result);
+    bool LookupTransferSyntax(std::string& result) const;
 
     DicomInstanceHasher& GetHasher();
+
+    bool HasPixelData() const;
   };
 }
--- a/OrthancServer/LuaScripting.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/LuaScripting.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -772,6 +772,8 @@
           LOG(ERROR) << "Error while processing Lua events: " << e.What();
         }
       }
+
+      that->jobManager_.GetDicomConnectionManager().CloseIfInactive();
     }
   }
 
@@ -874,6 +876,17 @@
       instance.GetOrigin().Format(origin);
       call.PushJson(origin);
 
+      Json::Value info = Json::objectValue;
+      info["HasPixelData"] = instance.HasPixelData();
+
+      std::string s;
+      if (instance.LookupTransferSyntax(s))
+      {
+        info["TransferSyntaxUID"] = s;
+      }
+
+      call.PushJson(info);
+
       if (!call.ExecutePredicate())
       {
         return false;
--- a/OrthancServer/LuaScripting.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/LuaScripting.h	Tue Apr 21 16:37:25 2020 +0200
@@ -136,5 +136,10 @@
     void SignalJobSuccess(const std::string& jobId);
 
     void SignalJobFailure(const std::string& jobId);
+
+    TimeoutDicomConnectionManager& GetDicomConnectionManager()
+    {
+      return jobManager_.GetDicomConnectionManager();
+    }
   };
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -121,22 +121,23 @@
                              "Received an empty DICOM file");
     }
 
+    // The lifetime of "dicom" must be longer than "toStore", as the
+    // latter can possibly store a reference to the former (*)
     std::string dicom;
 
+    DicomInstanceToStore toStore;
+    toStore.SetOrigin(DicomInstanceOrigin::FromRest(call));
+
     if (boost::iequals(call.GetHttpHeader("content-encoding", ""), "gzip"))
     {
       GzipCompressor compressor;
       compressor.Uncompress(dicom, call.GetBodyData(), call.GetBodySize());
+      toStore.SetBuffer(dicom.c_str(), dicom.size());  // (*)
     }
     else
     {
-      // TODO Remove unneccessary memcpy
-      call.BodyToString(dicom);
-    }
-
-    DicomInstanceToStore toStore;
-    toStore.SetOrigin(DicomInstanceOrigin::FromRest(call));
-    toStore.SetBuffer(dicom);
+      toStore.SetBuffer(call.GetBodyData(), call.GetBodySize());
+    }    
 
     std::string publicId;
     StoreStatus status = context.Store(publicId, toStore);
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -35,6 +35,8 @@
 #include "OrthancRestApi.h"
 
 #include "../../Core/Cache/SharedArchive.h"
+#include "../../Core/DicomNetworking/DicomAssociation.h"
+#include "../../Core/DicomNetworking/DicomControlUserConnection.h"
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/Logging.h"
 #include "../../Core/SerializationToolbox.h"
@@ -80,8 +82,7 @@
 
     try
     {
-      DicomUserConnection connection(localAet, remote);
-      connection.Open();
+      DicomControlUserConnection connection(localAet, remote);
       
       if (connection.Echo())
       {
@@ -127,7 +128,7 @@
 
 
   static void FindPatient(DicomFindAnswers& result,
-                          DicomUserConnection& connection,
+                          DicomControlUserConnection& connection,
                           const DicomMap& fields)
   {
     // Only keep the filters from "fields" that are related to the patient
@@ -138,7 +139,7 @@
 
 
   static void FindStudy(DicomFindAnswers& result,
-                        DicomUserConnection& connection,
+                        DicomControlUserConnection& connection,
                         const DicomMap& fields)
   {
     // Only keep the filters from "fields" that are related to the study
@@ -153,7 +154,7 @@
   }
 
   static void FindSeries(DicomFindAnswers& result,
-                         DicomUserConnection& connection,
+                         DicomControlUserConnection& connection,
                          const DicomMap& fields)
   {
     // Only keep the filters from "fields" that are related to the series
@@ -168,7 +169,7 @@
   }
 
   static void FindInstance(DicomFindAnswers& result,
-                           DicomUserConnection& connection,
+                           DicomControlUserConnection& connection,
                            const DicomMap& fields)
   {
     // Only keep the filters from "fields" that are related to the instance
@@ -203,8 +204,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomUserConnection connection(localAet, remote);
-      connection.Open();
+      DicomControlUserConnection connection(localAet, remote);
       FindPatient(answers, connection, fields);
     }
 
@@ -238,8 +238,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomUserConnection connection(localAet, remote);
-      connection.Open();
+      DicomControlUserConnection connection(localAet, remote);
       FindStudy(answers, connection, fields);
     }
 
@@ -274,8 +273,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomUserConnection connection(localAet, remote);
-      connection.Open();
+      DicomControlUserConnection connection(localAet, remote);
       FindSeries(answers, connection, fields);
     }
 
@@ -311,8 +309,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomUserConnection connection(localAet, remote);
-      connection.Open();
+      DicomControlUserConnection connection(localAet, remote);
       FindInstance(answers, connection, fields);
     }
 
@@ -350,8 +347,7 @@
     RemoteModalityParameters remote =
       MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
-    DicomUserConnection connection(localAet, remote);
-    connection.Open();
+    DicomControlUserConnection connection(localAet, remote);
     
     DicomFindAnswers patients(false);
     FindPatient(patients, connection, m);
@@ -804,7 +800,7 @@
         DicomMap answer;
         parent.GetHandler().GetAnswer(answer, index);
 
-        // This switch-case mimics "DicomUserConnection::Move()"
+        // This switch-case mimics "DicomControlUserConnection::Move()"
         switch (parent.GetHandler().GetLevel())
         {
           case ResourceType_Patient:
@@ -1031,8 +1027,7 @@
     const RemoteModalityParameters source =
       MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
-    DicomUserConnection connection(localAet, source);
-    connection.Open();
+    DicomControlUserConnection connection(localAet, source);
     
     for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++)
     {
@@ -1315,8 +1310,7 @@
       DicomFindAnswers answers(true);
 
       {
-        DicomUserConnection connection(localAet, remote);
-        connection.Open();
+        DicomControlUserConnection connection(localAet, remote);
         connection.FindWorklist(answers, *query);
       }
 
@@ -1486,11 +1480,11 @@
         context.GetStorageCommitmentReports().Store(
           transactionUid, new StorageCommitmentReports::Report(remoteAet));
 
-        DicomUserConnection scu(localAet, remote);
-
+        DicomAssociationParameters parameters(localAet, remote);
+        
         std::vector<std::string> a(sopClassUids.begin(), sopClassUids.end());
         std::vector<std::string> b(sopInstanceUids.begin(), sopInstanceUids.end());
-        scu.RequestStorageCommitment(transactionUid, a, b);
+        DicomAssociation::RequestStorageCommitment(parameters, transactionUid, a, b);
       }
 
       Json::Value result = Json::objectValue;
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Tue Apr 21 16:37:25 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/OrthancServer/QueryRetrieveHandler.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/QueryRetrieveHandler.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -36,6 +36,7 @@
 
 #include "OrthancConfiguration.h"
 
+#include "../Core/DicomNetworking/DicomControlUserConnection.h"
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/Logging.h"
 #include "LuaScripting.h"
@@ -81,8 +82,7 @@
       FixQueryLua(fixed, context_, modality_.GetApplicationEntityTitle()); 
 
       {
-        DicomUserConnection connection(localAet_, modality_);
-        connection.Open();
+        DicomControlUserConnection connection(localAet_, modality_);
         connection.Find(answers_, level_, fixed, findNormalized_);
       }
 
--- a/OrthancServer/ServerContext.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerContext.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -669,7 +669,7 @@
   {
 #if ENABLE_DICOM_CACHE == 0
     static std::unique_ptr<IDynamicObject> p;
-    p.reset(provider_.Provide(instancePublicId));
+    p.reset(that_.provider_.Provide(instancePublicId));
     dicom_ = dynamic_cast<ParsedDicomFile*>(p.get());
 #else
     dicom_ = &dynamic_cast<ParsedDicomFile&>(that_.dicomCache_.Access(instancePublicId));
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -47,9 +47,7 @@
   {
     if (connection_.get() == NULL)
     {
-      connection_.reset(new DicomUserConnection);
-      connection_->SetLocalApplicationEntityTitle(localAet_);
-      connection_->SetRemoteModality(remote_);
+      connection_.reset(new DicomUserConnection(localAet_, remote_));
     }
   }
 
--- a/OrthancServer/ServerJobs/DicomMoveScuJob.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/DicomMoveScuJob.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -96,8 +96,7 @@
   {
     if (connection_.get() == NULL)
     {
-      connection_.reset(new DicomUserConnection(localAet_, remote_));
-      connection_->Open();
+      connection_.reset(new DicomControlUserConnection(localAet_, remote_));
     }
     
     connection_->Move(targetAet_, findAnswer);
--- a/OrthancServer/ServerJobs/DicomMoveScuJob.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/DicomMoveScuJob.h	Tue Apr 21 16:37:25 2020 +0200
@@ -34,8 +34,8 @@
 #pragma once
 
 #include "../../Core/Compatibility.h"
+#include "../../Core/DicomNetworking/DicomControlUserConnection.h"
 #include "../../Core/JobsEngine/SetOfCommandsJob.h"
-#include "../../Core/DicomNetworking/DicomUserConnection.h"
 
 #include "../QueryRetrieveHandler.h"
 
@@ -49,13 +49,14 @@
     class Command;
     class Unserializer;
     
-    ServerContext&                        context_;
-    std::string                           localAet_;
-    std::string                           targetAet_;
-    RemoteModalityParameters              remote_;
-    std::unique_ptr<DicomUserConnection>  connection_;
-    Json::Value                           query_;
+    ServerContext&            context_;
+    std::string               localAet_;
+    std::string               targetAet_;
+    RemoteModalityParameters  remote_;
+    Json::Value               query_;
 
+    std::unique_ptr<DicomControlUserConnection>  connection_;
+    
     void Retrieve(const DicomMap& findAnswer);
     
   public:
--- a/OrthancServer/ServerJobs/LuaJobManager.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/LuaJobManager.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -68,13 +68,16 @@
     priority_(0),
     trailingTimeout_(5000)
   {
+    unsigned int dicomTimeout;
+    
     {
       OrthancConfiguration::ReaderLock lock;
-      dicomTimeout_ = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomAssociationCloseDelay", 5);
+      dicomTimeout = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomAssociationCloseDelay", 5);
     }
 
+    connectionManager_.SetInactivityTimeout(dicomTimeout * 1000);  // Milliseconds expected
     LOG(INFO) << "Lua: DICOM associations will be closed after "
-              << dicomTimeout_ << " seconds of inactivity";
+              << dicomTimeout << " seconds of inactivity";
   }
 
 
@@ -149,7 +152,6 @@
       {
         jobLock_.reset(new SequenceOfOperationsJob::Lock(*that_.currentJob_));
         jobLock_->SetTrailingOperationTimeout(that_.trailingTimeout_);
-        jobLock_->SetDicomAssociationTimeout(that_.dicomTimeout_ * 1000);  // Milliseconds expected
       }
     }
 
@@ -202,7 +204,7 @@
                                                    const RemoteModalityParameters& modality)
   {
     assert(jobLock_.get() != NULL);
-    return jobLock_->AddOperation(new StoreScuOperation(localAet, modality));    
+    return jobLock_->AddOperation(new StoreScuOperation(that_.connectionManager_, localAet, modality));    
   }
 
 
--- a/OrthancServer/ServerJobs/LuaJobManager.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/LuaJobManager.h	Tue Apr 21 16:37:25 2020 +0200
@@ -33,6 +33,7 @@
 
 #pragma once
 
+#include "../../Core/DicomNetworking/TimeoutDicomConnectionManager.h"
 #include "../../Core/DicomParsing/DicomModification.h"
 #include "../../Core/JobsEngine/JobsEngine.h"
 #include "../../Core/JobsEngine/Operations/SequenceOfOperationsJob.h"
@@ -51,7 +52,7 @@
     size_t                    maxOperations_;
     int                       priority_;
     unsigned int              trailingTimeout_;
-    unsigned int              dicomTimeout_;
+    TimeoutDicomConnectionManager  connectionManager_;
 
     virtual void SignalDone(const SequenceOfOperationsJob& job);
 
@@ -66,6 +67,11 @@
 
     void AwakeTrailingSleep();
 
+    TimeoutDicomConnectionManager& GetDicomConnectionManager()
+    {
+      return connectionManager_;
+    }
+
     class Lock : public boost::noncopyable
     {
     private:
--- a/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -43,8 +43,7 @@
 namespace Orthanc
 {
   void DeleteResourceOperation::Apply(JobOperationValues& outputs,
-                                      const JobOperationValue& input,
-                                      IDicomConnectionManager& connectionManager)
+                                      const JobOperationValue& input)
   {
     switch (input.GetType())
     {
--- a/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h	Tue Apr 21 16:37:25 2020 +0200
@@ -51,8 +51,7 @@
     }
 
     virtual void Apply(JobOperationValues& outputs,
-                       const JobOperationValue& input,
-                       IDicomConnectionManager& connectionManager);
+                       const JobOperationValue& input);
 
     virtual void Serialize(Json::Value& result) const
     {
--- a/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -81,8 +81,7 @@
   }
 
   void ModifyInstanceOperation::Apply(JobOperationValues& outputs,
-                                      const JobOperationValue& input,
-                                      IDicomConnectionManager& connectionManager)
+                                      const JobOperationValue& input)
   {
     if (input.GetType() != JobOperationValue::Type_DicomInstance)
     {
--- a/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h	Tue Apr 21 16:37:25 2020 +0200
@@ -67,8 +67,7 @@
     }
 
     virtual void Apply(JobOperationValues& outputs,
-                       const JobOperationValue& input,
-                       IDicomConnectionManager& connectionManager);
+                       const JobOperationValue& input);
 
     virtual void Serialize(Json::Value& target) const;
   };
--- a/OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -44,8 +44,7 @@
 namespace Orthanc
 {
   void StorePeerOperation::Apply(JobOperationValues& outputs,
-                                 const JobOperationValue& input,
-                                 IDicomConnectionManager& connectionManager)
+                                 const JobOperationValue& input)
   {
     // Configure the HTTP client
     HttpClient client(peer_, "instances");
--- a/OrthancServer/ServerJobs/Operations/StorePeerOperation.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/Operations/StorePeerOperation.h	Tue Apr 21 16:37:25 2020 +0200
@@ -57,8 +57,7 @@
     }
 
     virtual void Apply(JobOperationValues& outputs,
-                       const JobOperationValue& input,
-                       IDicomConnectionManager& connectionManager);
+                       const JobOperationValue& input);
 
     virtual void Serialize(Json::Value& result) const;
   };
--- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -43,18 +43,10 @@
 namespace Orthanc
 {
   void StoreScuOperation::Apply(JobOperationValues& outputs,
-                                const JobOperationValue& input,
-                                IDicomConnectionManager& connectionManager)
+                                const JobOperationValue& input)
   {
-    std::unique_ptr<IDicomConnectionManager::IResource> resource
-      (connectionManager.AcquireConnection(localAet_, modality_));
-
-    if (resource.get() == NULL)
-    {
-      LOG(ERROR) << "Lua: Cannot connect to modality: " << modality_.GetApplicationEntityTitle();
-      return;
-    }
-
+    TimeoutDicomConnectionManager::Lock lock(connectionManager_, localAet_, modality_);
+    
     if (input.GetType() != JobOperationValue::Type_DicomInstance)
     {
       throw OrthancException(ErrorCode_BadParameterType);
@@ -72,7 +64,7 @@
       instance.ReadDicom(dicom);
 
       std::string sopClassUid, sopInstanceUid;  // Unused
-      resource->GetConnection().Store(sopClassUid, sopInstanceUid, dicom);
+      lock.GetConnection().Store(sopClassUid, sopInstanceUid, dicom);
     }
     catch (OrthancException& e)
     {
@@ -93,7 +85,9 @@
   }
 
 
-  StoreScuOperation::StoreScuOperation(const Json::Value& serialized)
+  StoreScuOperation::StoreScuOperation(TimeoutDicomConnectionManager& connectionManager,
+                                       const Json::Value& serialized) :
+    connectionManager_(connectionManager)
   {
     if (SerializationToolbox::ReadString(serialized, "Type") != "StoreScu" ||
         !serialized.isMember("LocalAET"))
--- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.h	Tue Apr 21 16:37:25 2020 +0200
@@ -34,25 +34,29 @@
 #pragma once
 
 #include "../../../Core/JobsEngine/Operations/IJobOperation.h"
-#include "../../../Core/DicomNetworking/RemoteModalityParameters.h"
+#include "../../../Core/DicomNetworking/TimeoutDicomConnectionManager.h"
 
 namespace Orthanc
 {
   class StoreScuOperation : public IJobOperation
   {
   private:
-    std::string               localAet_;
-    RemoteModalityParameters  modality_;
+    TimeoutDicomConnectionManager&  connectionManager_;
+    std::string                     localAet_;
+    RemoteModalityParameters        modality_;
     
   public:
-    StoreScuOperation(const std::string& localAet,
+    StoreScuOperation(TimeoutDicomConnectionManager& connectionManager,
+                      const std::string& localAet,
                       const RemoteModalityParameters& modality) :
+      connectionManager_(connectionManager),
       localAet_(localAet),
       modality_(modality)
     {
     }
 
-    StoreScuOperation(const Json::Value& serialized);
+    StoreScuOperation(TimeoutDicomConnectionManager& connectionManager,
+                      const Json::Value& serialized);
 
     const std::string& GetLocalAet() const
     {
@@ -65,8 +69,7 @@
     }
 
     virtual void Apply(JobOperationValues& outputs,
-                       const JobOperationValue& input,
-                       IDicomConnectionManager& manager);
+                       const JobOperationValue& input);
 
     virtual void Serialize(Json::Value& result) const;
   };
--- a/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -74,8 +74,7 @@
 
 
   void SystemCallOperation::Apply(JobOperationValues& outputs,
-                                  const JobOperationValue& input,
-                                  IDicomConnectionManager& connectionManager)
+                                  const JobOperationValue& input)
   {
     std::vector<std::string> arguments = preArguments_;
 
--- a/OrthancServer/ServerJobs/Operations/SystemCallOperation.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/Operations/SystemCallOperation.h	Tue Apr 21 16:37:25 2020 +0200
@@ -93,8 +93,7 @@
     const std::string& GetPostArgument(size_t i) const;
 
     virtual void Apply(JobOperationValues& outputs,
-                       const JobOperationValue& input,
-                       IDicomConnectionManager& connectionManager);
+                       const JobOperationValue& input);
 
     virtual void Serialize(Json::Value& result) const;
   };
--- a/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -126,7 +126,8 @@
     }
     else if (type == "StoreScu")
     {
-      return new StoreScuOperation(source);
+      return new StoreScuOperation(
+        context_.GetLuaScripting().GetDicomConnectionManager(), source);
     }
     else if (type == "SystemCall")
     {
--- a/OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -34,7 +34,7 @@
 #include "../PrecompiledHeadersServer.h"
 #include "StorageCommitmentScpJob.h"
 
-#include "../../Core/DicomNetworking/DicomUserConnection.h"
+#include "../../Core/DicomNetworking/DicomAssociation.h"
 #include "../../Core/Logging.h"
 #include "../../Core/OrthancException.h"
 #include "../../Core/SerializationToolbox.h"
@@ -347,9 +347,10 @@
     {
       throw OrthancException(ErrorCode_InternalError);
     }
-      
-    DicomUserConnection scu(calledAet_, remoteModality_);
-    scu.ReportStorageCommitment(transactionUid_, sopClassUids_, sopInstanceUids_, failureReasons);
+
+    DicomAssociationParameters parameters(calledAet_, remoteModality_);
+    DicomAssociation::ReportStorageCommitment(
+      parameters, transactionUid_, sopClassUids_, sopInstanceUids_, failureReasons);
   }
     
 
--- a/OrthancServer/main.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/OrthancServer/main.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -83,7 +83,7 @@
       DicomInstanceToStore toStore;
       toStore.SetOrigin(DicomInstanceOrigin::FromDicomProtocol
                         (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str()));
-      toStore.SetBuffer(dicomFile);
+      toStore.SetBuffer(dicomFile.c_str(), dicomFile.size());
       toStore.SetSummary(dicomSummary);
       toStore.SetJson(dicomJson);
 
--- a/Plugins/Engine/OrthancPlugins.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/Plugins/Engine/OrthancPlugins.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -825,6 +825,7 @@
     typedef std::list<OrthancPluginOnChangeCallback>  OnChangeCallbacks;
     typedef std::list<OrthancPluginIncomingHttpRequestFilter>  IncomingHttpRequestFilters;
     typedef std::list<OrthancPluginIncomingHttpRequestFilter2>  IncomingHttpRequestFilters2;
+    typedef std::list<OrthancPluginIncomingDicomInstanceFilter>  IncomingDicomInstanceFilters;
     typedef std::list<OrthancPluginDecodeImageCallback>  DecodeImageCallbacks;
     typedef std::list<OrthancPluginJobsUnserializer>  JobsUnserializers;
     typedef std::list<OrthancPluginRefreshMetricsCallback>  RefreshMetricsCallbacks;
@@ -844,6 +845,7 @@
     _OrthancPluginMoveCallback moveCallbacks_;
     IncomingHttpRequestFilters  incomingHttpRequestFilters_;
     IncomingHttpRequestFilters2 incomingHttpRequestFilters2_;
+    IncomingDicomInstanceFilters  incomingDicomInstanceFilters_;
     RefreshMetricsCallbacks refreshMetricsCallbacks_;
     StorageCommitmentScpCallbacks storageCommitmentScpCallbacks_;
     std::unique_ptr<StorageAreaFactory>  storageArea_;
@@ -1782,7 +1784,33 @@
   }
 
 
-
+  bool OrthancPlugins::FilterIncomingInstance(const DicomInstanceToStore& instance,
+                                              const Json::Value& simplified)
+  {
+    boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
+    
+    for (PImpl::IncomingDicomInstanceFilters::const_iterator
+           filter = pimpl_->incomingDicomInstanceFilters_.begin();
+         filter != pimpl_->incomingDicomInstanceFilters_.end(); ++filter)
+    {
+      int32_t allowed = (*filter) (
+        reinterpret_cast<const OrthancPluginDicomInstance*>(&instance));
+
+      if (allowed == 0)
+      {
+        return false;
+      }
+      else if (allowed != 1)
+      {
+        // The callback is only allowed to answer 0 or 1
+        throw OrthancException(ErrorCode_Plugin);
+      }
+    }
+
+    return true;
+  }
+
+  
   void OrthancPlugins::SignalChangeInternal(OrthancPluginChangeType changeType,
                                             OrthancPluginResourceType resourceType,
                                             const char* resource)
@@ -1967,6 +1995,16 @@
   }
 
 
+  void OrthancPlugins::RegisterIncomingDicomInstanceFilter(const void* parameters)
+  {
+    const _OrthancPluginIncomingDicomInstanceFilter& p = 
+      *reinterpret_cast<const _OrthancPluginIncomingDicomInstanceFilter*>(parameters);
+
+    LOG(INFO) << "Plugin has registered a callback to filter incoming DICOM instances";
+    pimpl_->incomingDicomInstanceFilters_.push_back(p.callback);
+  }
+
+
   void OrthancPlugins::RegisterRefreshMetricsCallback(const void* parameters)
   {
     const _OrthancPluginRegisterRefreshMetricsCallback& p = 
@@ -2419,8 +2457,8 @@
     const _OrthancPluginAccessDicomInstance& p = 
       *reinterpret_cast<const _OrthancPluginAccessDicomInstance*>(parameters);
 
-    DicomInstanceToStore& instance =
-      *reinterpret_cast<DicomInstanceToStore*>(p.instance);
+    const DicomInstanceToStore& instance =
+      *reinterpret_cast<const DicomInstanceToStore*>(p.instance);
 
     switch (service)
     {
@@ -2433,7 +2471,7 @@
         return;
 
       case _OrthancPluginService_GetInstanceData:
-        *p.resultString = instance.GetBufferData();
+        *p.resultString = reinterpret_cast<const char*>(instance.GetBufferData());
         return;
 
       case _OrthancPluginService_HasInstanceMetadata:
@@ -2469,6 +2507,22 @@
         *p.resultOrigin = Plugins::Convert(instance.GetOrigin().GetRequestOrigin());
         return;
 
+      case _OrthancPluginService_GetInstanceTransferSyntaxUid:   // New in Orthanc 1.6.1
+      {
+        std::string s;
+        if (!instance.LookupTransferSyntax(s))
+        {
+          s.clear();
+        }
+        
+        *p.resultStringToFree = CopyString(s);
+        return;
+      }
+
+      case _OrthancPluginService_HasInstancePixelData:   // New in Orthanc 1.6.1
+        *p.resultInt64 = instance.HasPixelData();
+        return;
+
       default:
         throw OrthancException(ErrorCode_InternalError);
     }
@@ -3420,6 +3474,8 @@
       case _OrthancPluginService_HasInstanceMetadata:
       case _OrthancPluginService_GetInstanceMetadata:
       case _OrthancPluginService_GetInstanceOrigin:
+      case _OrthancPluginService_GetInstanceTransferSyntaxUid:
+      case _OrthancPluginService_HasInstancePixelData:
         AccessDicomInstance(service, parameters);
         return true;
 
@@ -4034,6 +4090,10 @@
         RegisterIncomingHttpRequestFilter2(parameters);
         return true;
 
+      case _OrthancPluginService_RegisterIncomingDicomInstanceFilter:
+        RegisterIncomingDicomInstanceFilter(parameters);
+        return true;
+
       case _OrthancPluginService_RegisterRefreshMetricsCallback:
         RegisterRefreshMetricsCallback(parameters);
         return true;
@@ -4477,46 +4537,50 @@
       getValues[i] = getArguments[i].second.c_str();
     }
 
-    // Improved callback with support for GET arguments, since Orthanc 1.3.0
-    for (PImpl::IncomingHttpRequestFilters2::const_iterator
-           filter = pimpl_->incomingHttpRequestFilters2_.begin();
-         filter != pimpl_->incomingHttpRequestFilters2_.end(); ++filter)
     {
-      int32_t allowed = (*filter) (cMethod, uri, ip,
-                                   httpKeys.size(),
-                                   httpKeys.empty() ? NULL : &httpKeys[0],
-                                   httpValues.empty() ? NULL : &httpValues[0],
-                                   getKeys.size(),
-                                   getKeys.empty() ? NULL : &getKeys[0],
-                                   getValues.empty() ? NULL : &getValues[0]);
-
-      if (allowed == 0)
-      {
-        return false;
-      }
-      else if (allowed != 1)
+      boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
+    
+      // Improved callback with support for GET arguments, since Orthanc 1.3.0
+      for (PImpl::IncomingHttpRequestFilters2::const_iterator
+             filter = pimpl_->incomingHttpRequestFilters2_.begin();
+           filter != pimpl_->incomingHttpRequestFilters2_.end(); ++filter)
       {
-        // The callback is only allowed to answer 0 or 1
-        throw OrthancException(ErrorCode_Plugin);
+        int32_t allowed = (*filter) (cMethod, uri, ip,
+                                     httpKeys.size(),
+                                     httpKeys.empty() ? NULL : &httpKeys[0],
+                                     httpValues.empty() ? NULL : &httpValues[0],
+                                     getKeys.size(),
+                                     getKeys.empty() ? NULL : &getKeys[0],
+                                     getValues.empty() ? NULL : &getValues[0]);
+
+        if (allowed == 0)
+        {
+          return false;
+        }
+        else if (allowed != 1)
+        {
+          // The callback is only allowed to answer 0 or 1
+          throw OrthancException(ErrorCode_Plugin);
+        }
       }
-    }
-
-    for (PImpl::IncomingHttpRequestFilters::const_iterator
-           filter = pimpl_->incomingHttpRequestFilters_.begin();
-         filter != pimpl_->incomingHttpRequestFilters_.end(); ++filter)
-    {
-      int32_t allowed = (*filter) (cMethod, uri, ip, httpKeys.size(),
-                                   httpKeys.empty() ? NULL : &httpKeys[0],
-                                   httpValues.empty() ? NULL : &httpValues[0]);
-
-      if (allowed == 0)
+
+      for (PImpl::IncomingHttpRequestFilters::const_iterator
+             filter = pimpl_->incomingHttpRequestFilters_.begin();
+           filter != pimpl_->incomingHttpRequestFilters_.end(); ++filter)
       {
-        return false;
-      }
-      else if (allowed != 1)
-      {
-        // The callback is only allowed to answer 0 or 1
-        throw OrthancException(ErrorCode_Plugin);
+        int32_t allowed = (*filter) (cMethod, uri, ip, httpKeys.size(),
+                                     httpKeys.empty() ? NULL : &httpKeys[0],
+                                     httpValues.empty() ? NULL : &httpValues[0]);
+
+        if (allowed == 0)
+        {
+          return false;
+        }
+        else if (allowed != 1)
+        {
+          // The callback is only allowed to answer 0 or 1
+          throw OrthancException(ErrorCode_Plugin);
+        }
       }
     }
 
--- a/Plugins/Engine/OrthancPlugins.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/Plugins/Engine/OrthancPlugins.h	Tue Apr 21 16:37:25 2020 +0200
@@ -124,6 +124,8 @@
 
     void RegisterIncomingHttpRequestFilter2(const void* parameters);
 
+    void RegisterIncomingDicomInstanceFilter(const void* parameters);
+
     void RegisterRefreshMetricsCallback(const void* parameters);
 
     void RegisterStorageCommitmentScpCallback(const void* parameters);
@@ -252,10 +254,7 @@
                                       const Json::Value& simplifiedTags) ORTHANC_OVERRIDE;
 
     virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
-                                        const Json::Value& simplified) ORTHANC_OVERRIDE
-    {
-      return true; // TODO Enable filtering of instances from plugins
-    }
+                                        const Json::Value& simplified) ORTHANC_OVERRIDE;
 
     bool HasStorageArea() const;
 
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Mar 27 10:06:58 2020 -0400
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Apr 21 16:37:25 2020 +0200
@@ -27,6 +27,7 @@
  *    - Possibly register a callback to refresh its metrics using OrthancPluginRegisterRefreshMetricsCallback().
  *    - Possibly register a callback to answer chunked HTTP transfers using ::OrthancPluginRegisterChunkedRestCallback().
  *    - Possibly register a callback for Storage Commitment SCP using ::OrthancPluginRegisterStorageCommitmentScpCallback().
+ *    - Possibly register a callback to filter incoming DICOM instance using OrthancPluginRegisterIncomingDicomInstanceFilter().
  * -# <tt>void OrthancPluginFinalize()</tt>:
  *    This function is invoked by Orthanc during its shutdown. The plugin
  *    must free all its memory.
@@ -124,7 +125,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     6
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  0
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  1
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -179,7 +180,7 @@
 /**
  * For Microsoft Visual Studio, a compatibility "stdint.h" can be
  * downloaded at the following URL:
- * https://bitbucket.org/sjodogne/orthanc/raw/default/Resources/ThirdParty/VisualStudio/stdint.h
+ * https://hg.orthanc-server.com/orthanc/raw-file/tip/Resources/ThirdParty/VisualStudio/stdint.h
  **/
 #include <stdint.h>
 
@@ -455,6 +456,7 @@
     _OrthancPluginService_RegisterRefreshMetricsCallback = 1011,
     _OrthancPluginService_RegisterChunkedRestCallback = 1012,  /* New in Orthanc 1.5.7 */
     _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013,
+    _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014,
 
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
@@ -499,6 +501,8 @@
     _OrthancPluginService_HasInstanceMetadata = 4005,
     _OrthancPluginService_GetInstanceMetadata = 4006,
     _OrthancPluginService_GetInstanceOrigin = 4007,
+    _OrthancPluginService_GetInstanceTransferSyntaxUid = 4008,
+    _OrthancPluginService_HasInstancePixelData = 4009,
 
     /* Services for plugins implementing a database back-end */
     _OrthancPluginService_RegisterDatabaseBackend = 5000,
@@ -1105,11 +1109,11 @@
 
 
   /**
-   * @brief Signature of a callback function that is triggered when Orthanc receives a DICOM instance.
+   * @brief Signature of a callback function that is triggered when Orthanc stores a new DICOM instance.
    * @ingroup Callbacks
    **/
   typedef OrthancPluginErrorCode (*OrthancPluginOnStoredInstanceCallback) (
-    OrthancPluginDicomInstance* instance,
+    const OrthancPluginDicomInstance* instance,
     const char* instanceId);
 
 
@@ -2694,12 +2698,12 @@
 
   typedef struct
   {
-    char**                       resultStringToFree;
-    const char**                 resultString;
-    int64_t*                     resultInt64;
-    const char*                  key;
-    OrthancPluginDicomInstance*  instance;
-    OrthancPluginInstanceOrigin* resultOrigin;   /* New in Orthanc 0.9.5 SDK */
+    char**                             resultStringToFree;
+    const char**                       resultString;
+    int64_t*                           resultInt64;
+    const char*                        key;
+    const OrthancPluginDicomInstance*  instance;
+    OrthancPluginInstanceOrigin*       resultOrigin;   /* New in Orthanc 0.9.5 SDK */
   } _OrthancPluginAccessDicomInstance;
 
 
@@ -2715,8 +2719,8 @@
    * @ingroup Callbacks
    **/
   ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceRemoteAet(
-    OrthancPluginContext*        context,
-    OrthancPluginDicomInstance*  instance)
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
   {
     const char* result;
 
@@ -2748,8 +2752,8 @@
    * @ingroup Callbacks
    **/
   ORTHANC_PLUGIN_INLINE int64_t OrthancPluginGetInstanceSize(
-    OrthancPluginContext*       context,
-    OrthancPluginDicomInstance* instance)
+    OrthancPluginContext*             context,
+    const OrthancPluginDicomInstance* instance)
   {
     int64_t size;
 
@@ -2781,8 +2785,8 @@
    * @ingroup Callbacks
    **/
   ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetInstanceData(
-    OrthancPluginContext*        context,
-    OrthancPluginDicomInstance*  instance)
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
   {
     const char* result;
 
@@ -2817,8 +2821,8 @@
    * @ingroup Callbacks
    **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceJson(
-    OrthancPluginContext*        context,
-    OrthancPluginDicomInstance*  instance)
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
   {
     char* result;
 
@@ -2855,8 +2859,8 @@
    * @ingroup Callbacks
    **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceSimplifiedJson(
-    OrthancPluginContext*        context,
-    OrthancPluginDicomInstance*  instance)
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
   {
     char* result;
 
@@ -2894,9 +2898,9 @@
    * @ingroup Callbacks
    **/
   ORTHANC_PLUGIN_INLINE int  OrthancPluginHasInstanceMetadata(
-    OrthancPluginContext*        context,
-    OrthancPluginDicomInstance*  instance,
-    const char*                  metadata)
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance,
+    const char*                        metadata)
   {
     int64_t result;
 
@@ -2935,9 +2939,9 @@
    * @ingroup Callbacks
    **/
   ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceMetadata(
-    OrthancPluginContext*        context,
-    OrthancPluginDicomInstance*  instance,
-    const char*                  metadata)
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance,
+    const char*                        metadata)
   {
     const char* result;
 
@@ -5107,8 +5111,8 @@
    * @ingroup Callbacks
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginInstanceOrigin OrthancPluginGetInstanceOrigin(
-    OrthancPluginContext*       context,
-    OrthancPluginDicomInstance* instance)
+    OrthancPluginContext*             context,
+    const OrthancPluginDicomInstance* instance)
   {
     OrthancPluginInstanceOrigin origin;
 
@@ -7413,6 +7417,128 @@
     return context->InvokeService(context, _OrthancPluginService_RegisterStorageCommitmentScpCallback, &params);
   }
   
+
+
+  /**
+   * @brief Callback to filter incoming DICOM instances received by Orthanc.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives a new DICOM instance (e.g. through REST API or
+   * DICOM protocol), and that answers whether this DICOM instance
+   * should be accepted or discarded by Orthanc.
+   *
+   * Note that the metadata information is not available
+   * (i.e. GetInstanceMetadata() should not be used on "instance").
+   *
+   * @param instance The received DICOM instance.
+   * @return 0 to discard the instance, 1 to store the instance, -1 if error.
+   * @ingroup Callback
+   **/
+  typedef int32_t (*OrthancPluginIncomingDicomInstanceFilter) (
+    const OrthancPluginDicomInstance* instance);
+
+
+  typedef struct
+  {
+    OrthancPluginIncomingDicomInstanceFilter callback;
+  } _OrthancPluginIncomingDicomInstanceFilter;
+
+  /**
+   * @brief Register a callback to filter incoming DICOM instance.
+   *
+   * This function registers a custom callback to filter incoming
+   * DICOM instances received by Orthanc (either through the REST API
+   * or through the DICOM protocol).
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingDicomInstanceFilter(
+    OrthancPluginContext*                     context,
+    OrthancPluginIncomingDicomInstanceFilter  callback)
+  {
+    _OrthancPluginIncomingDicomInstanceFilter params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterIncomingDicomInstanceFilter, &params);
+  }
+
+
+  /**
+   * @brief Get the transfer syntax of a DICOM file.
+   *
+   * This function returns a pointer to a newly created string that
+   * contains the transfer syntax UID of the DICOM instance. The empty
+   * string might be returned if this information is unknown.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The NULL value in case of error, or a string containing the
+   * transfer syntax UID. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceTransferSyntaxUid(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
+  {
+    char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultStringToFree = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceTransferSyntaxUid, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Check whether the DICOM file has pixel data.
+   *
+   * This function returns a Boolean value indicating whether the
+   * DICOM instance contains the pixel data (7FE0,0010) tag.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return "1" if the DICOM instance contains pixel data, or "0" if
+   * the tag is missing, or "-1" in the case of an error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE int32_t OrthancPluginHasInstancePixelData(
+    OrthancPluginContext*             context,
+    const OrthancPluginDicomInstance* instance)
+  {
+    int64_t hasPixelData;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultInt64 = &hasPixelData;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_HasInstancePixelData, &params) != OrthancPluginErrorCode_Success ||
+        hasPixelData < 0 ||
+        hasPixelData > 1)
+    {
+      /* Error */
+      return -1;
+    }
+    else
+    {
+      return hasPixelData;
+    }
+  }
+
+
 #ifdef  __cplusplus
 }
 #endif
--- a/Plugins/Samples/Basic/Plugin.c	Fri Mar 27 10:06:58 2020 -0400
+++ b/Plugins/Samples/Basic/Plugin.c	Tue Apr 21 16:37:25 2020 +0200
@@ -29,9 +29,9 @@
 static OrthancPluginErrorCode customError;
 
 
-ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback1(OrthancPluginRestOutput* output,
-                                                     const char* url,
-                                                     const OrthancPluginHttpRequest* request)
+OrthancPluginErrorCode Callback1(OrthancPluginRestOutput* output,
+                                 const char* url,
+                                 const OrthancPluginHttpRequest* request)
 {
   char buffer[1024];
   uint32_t i;
@@ -83,9 +83,9 @@
 }
 
 
-ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback2(OrthancPluginRestOutput* output,
-                                                     const char* url,
-                                                     const OrthancPluginHttpRequest* request)
+OrthancPluginErrorCode Callback2(OrthancPluginRestOutput* output,
+                                 const char* url,
+                                 const OrthancPluginHttpRequest* request)
 {
   /* Answer with a sample 16bpp image. */
 
@@ -115,9 +115,9 @@
 }
 
 
-ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback3(OrthancPluginRestOutput* output,
-                                                     const char* url,
-                                                     const OrthancPluginHttpRequest* request)
+OrthancPluginErrorCode Callback3(OrthancPluginRestOutput* output,
+                                 const char* url,
+                                 const OrthancPluginHttpRequest* request)
 {
   if (request->method != OrthancPluginHttpMethod_Get)
   {
@@ -140,9 +140,9 @@
 }
 
 
-ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback4(OrthancPluginRestOutput* output,
-                                                     const char* url,
-                                                     const OrthancPluginHttpRequest* request)
+OrthancPluginErrorCode Callback4(OrthancPluginRestOutput* output,
+                                 const char* url,
+                                 const OrthancPluginHttpRequest* request)
 {
   /* Answer with a sample 8bpp image. */
 
@@ -172,9 +172,9 @@
 }
 
 
-ORTHANC_PLUGINS_API OrthancPluginErrorCode Callback5(OrthancPluginRestOutput* output,
-                                                     const char* url,
-                                                     const OrthancPluginHttpRequest* request)
+OrthancPluginErrorCode Callback5(OrthancPluginRestOutput* output,
+                                 const char* url,
+                                 const OrthancPluginHttpRequest* request)
 {
   /**
    * Demonstration the difference between the
@@ -222,9 +222,9 @@
 }
 
 
-ORTHANC_PLUGINS_API OrthancPluginErrorCode CallbackCreateDicom(OrthancPluginRestOutput* output,
-                                                               const char* url,
-                                                               const OrthancPluginHttpRequest* request)
+OrthancPluginErrorCode CallbackCreateDicom(OrthancPluginRestOutput* output,
+                                           const char* url,
+                                           const OrthancPluginHttpRequest* request)
 {
   const char* pathLocator = "\"Path\" : \"";
   char info[1024];
@@ -266,7 +266,7 @@
 }
 
 
-ORTHANC_PLUGINS_API void DicomWebBinaryCallback(
+void DicomWebBinaryCallback(
   OrthancPluginDicomWebNode*          node,
   OrthancPluginDicomWebSetBinaryNode  setter,
   uint32_t                            levelDepth,
@@ -281,8 +281,8 @@
 }
 
 
-ORTHANC_PLUGINS_API OrthancPluginErrorCode OnStoredCallback(OrthancPluginDicomInstance* instance,
-                                                            const char* instanceId)
+OrthancPluginErrorCode OnStoredCallback(const OrthancPluginDicomInstance* instance,
+                                        const char* instanceId)
 {
   char buffer[256];
   FILE* fp;
@@ -333,9 +333,9 @@
 }
 
 
-ORTHANC_PLUGINS_API OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
-                                                            OrthancPluginResourceType resourceType,
-                                                            const char* resourceId)
+OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
+                                        OrthancPluginResourceType resourceType,
+                                        const char* resourceId)
 {
   char info[1024];
 
@@ -391,12 +391,12 @@
 }
 
 
-ORTHANC_PLUGINS_API int32_t FilterIncomingHttpRequest(OrthancPluginHttpMethod  method,
-                                                      const char*              uri,
-                                                      const char*              ip,
-                                                      uint32_t                 headersCount,
-                                                      const char* const*       headersKeys,
-                                                      const char* const*       headersValues)
+int32_t FilterIncomingHttpRequest(OrthancPluginHttpMethod  method,
+                                  const char*              uri,
+                                  const char*              ip,
+                                  uint32_t                 headersCount,
+                                  const char* const*       headersKeys,
+                                  const char* const*       headersValues)
 {
   uint32_t i;
 
@@ -423,11 +423,31 @@
 }
 
 
-ORTHANC_PLUGINS_API void RefreshMetrics()
+static void RefreshMetrics()
 {
   static unsigned int count = 0;
   OrthancPluginSetMetricsValue(context, "sample_counter", 
-	  (float) (count++), OrthancPluginMetricsType_Default); 
+                               (float) (count++), OrthancPluginMetricsType_Default); 
+}
+
+
+static int32_t FilterIncomingDicomInstance(const OrthancPluginDicomInstance* instance)
+{
+  char buf[1024];
+  char* s;
+  int32_t hasPixelData;
+
+  s = OrthancPluginGetInstanceTransferSyntaxUid(context, instance);
+  sprintf(buf, "Incoming transfer syntax: %s", s);
+  OrthancPluginFreeString(context, s);
+  OrthancPluginLogWarning(context, buf);
+
+  hasPixelData = OrthancPluginHasInstancePixelData(context, instance);
+  sprintf(buf, "Incoming has pixel data: %d", hasPixelData);
+  OrthancPluginLogWarning(context, buf);
+
+  /* Reject all instances without pixel data */
+  return hasPixelData;
 }
 
 
@@ -495,7 +515,8 @@
   OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
   OrthancPluginRegisterIncomingHttpRequestFilter(context, FilterIncomingHttpRequest);
   OrthancPluginRegisterRefreshMetricsCallback(context, RefreshMetrics);
-
+  OrthancPluginRegisterIncomingDicomInstanceFilter(context, FilterIncomingDicomInstance);
+    
   
   /* Declare several properties of the plugin */
   OrthancPluginSetRootUri(context, "/plugin/hello");
--- a/Resources/CMake/Compiler.cmake	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/CMake/Compiler.cmake	Tue Apr 21 16:37:25 2020 +0200
@@ -1,6 +1,7 @@
 # This file sets all the compiler-related flags
 
-if (CMAKE_CROSSCOMPILING OR
+if ((CMAKE_CROSSCOMPILING AND NOT
+      "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") OR    
     "${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
   # Cross-compilation necessarily implies standalone and static build
   SET(STATIC_BUILD ON)
--- a/Resources/CMake/DcmtkConfiguration.cmake	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/CMake/DcmtkConfiguration.cmake	Tue Apr 21 16:37:25 2020 +0200
@@ -133,13 +133,34 @@
 
 
 else()
-  # The following line allows to manually add libraries at the
-  # command-line, which is necessary for Ubuntu/Debian packages
-  set(tmp "${DCMTK_LIBRARIES}")
-  include(FindDCMTK)
-  list(APPEND DCMTK_LIBRARIES "${tmp}")
+  if (CMAKE_CROSSCOMPILING AND
+      "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
+
+    CHECK_INCLUDE_FILE_CXX(dcmtk/dcmdata/dcfilefo.h HAVE_DCMTK_H)
+    if (NOT HAVE_DCMTK_H)
+      message(FATAL_ERROR "Please install the libdcmtk-dev package")
+    endif()
+
+    CHECK_LIBRARY_EXISTS(dcmdata "dcmDataDict" "" HAVE_DCMTK_LIB)
+    if (NOT HAVE_DCMTK_LIB)
+      message(FATAL_ERROR "Please install the libdcmtk package")
+    endif()  
 
-  include_directories(${DCMTK_INCLUDE_DIRS})
+    find_path(DCMTK_INCLUDE_DIRS dcmtk/config/osconfig.h
+      /usr/include
+      )
+
+    link_libraries(dcmdata dcmnet dcmjpeg oflog ofstd)
+
+  else()
+    # The following line allows to manually add libraries at the
+    # command-line, which is necessary for Ubuntu/Debian packages
+    set(tmp "${DCMTK_LIBRARIES}")
+    include(FindDCMTK)
+    list(APPEND DCMTK_LIBRARIES "${tmp}")
+
+    include_directories(${DCMTK_INCLUDE_DIRS})
+  endif()
 
   add_definitions(
     -DHAVE_CONFIG_H=1
@@ -210,6 +231,13 @@
       message(FATAL_ERROR "Cannot locate the DICOM dictionary on this system")
     endif()
 
+    if (CMAKE_CROSSCOMPILING AND
+        "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
+      # Remove the sysroot prefix
+      file(RELATIVE_PATH tmp ${CMAKE_FIND_ROOT_PATH} ${DCMTK_DICTIONARY_DIR_AUTO})
+      set(DCMTK_DICTIONARY_DIR_AUTO /${tmp} CACHE INTERNAL "")
+    endif()
+
     message("Autodetected path to the DICOM dictionaries: ${DCMTK_DICTIONARY_DIR_AUTO}")
     add_definitions(-DDCMTK_DICTIONARY_DIR="${DCMTK_DICTIONARY_DIR_AUTO}")
   else()
--- a/Resources/CMake/GoogleTestConfiguration.cmake	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/CMake/GoogleTestConfiguration.cmake	Tue Apr 21 16:37:25 2020 +0200
@@ -2,15 +2,15 @@
   find_path(GOOGLE_TEST_DEBIAN_SOURCES_DIR
     NAMES src/gtest-all.cc
     PATHS
-    /usr/src/gtest
-    /usr/src/googletest/googletest
+    ${CROSSTOOL_NG_IMAGE}/usr/src/gtest
+    ${CROSSTOOL_NG_IMAGE}/usr/src/googletest/googletest
     PATH_SUFFIXES src
     )
 
   find_path(GOOGLE_TEST_DEBIAN_INCLUDE_DIR
     NAMES gtest.h
     PATHS
-    /usr/include/gtest
+    ${CROSSTOOL_NG_IMAGE}/usr/include/gtest
     )
 
   message("Path to the Debian Google Test sources: ${GOOGLE_TEST_DEBIAN_SOURCES_DIR}")
--- a/Resources/CMake/LibCurlConfiguration.cmake	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/CMake/LibCurlConfiguration.cmake	Tue Apr 21 16:37:25 2020 +0200
@@ -262,7 +262,7 @@
 
     check_struct_has_member("struct sockaddr_un" sun_path "sys/un.h" USE_UNIX_SOCKETS)
 
-    set(CMAKE_REQUIRED_INCLUDES "${CURL_SOURCES_DIR}/include")
+    list(APPEND CMAKE_REQUIRED_INCLUDES "${CURL_SOURCES_DIR}/include")
     set(CMAKE_EXTRA_INCLUDE_FILES "curl/system.h")
     check_type_size("curl_off_t"  SIZEOF_CURL_OFF_T)
 
@@ -312,6 +312,22 @@
       ${CURL_SOURCES_DIR}/lib/curl_config.h
       )
   endif()
+
+elseif (CMAKE_CROSSCOMPILING AND
+    "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
+
+  CHECK_INCLUDE_FILE_CXX(curl/curl.h HAVE_CURL_H)
+  if (NOT HAVE_CURL_H)
+    message(FATAL_ERROR "Please install the libcurl-dev package")
+  endif()
+
+  CHECK_LIBRARY_EXISTS(curl "curl_easy_init" "" HAVE_CURL_LIB)
+  if (NOT HAVE_CURL_LIB)
+    message(FATAL_ERROR "Please install the libcurl package")
+  endif()  
+  
+  link_libraries(curl)
+
 else()
   include(FindCURL)
   include_directories(${CURL_INCLUDE_DIRS})
--- a/Resources/CMake/LuaConfiguration.cmake	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/CMake/LuaConfiguration.cmake	Tue Apr 21 16:37:25 2020 +0200
@@ -100,6 +100,32 @@
 
   source_group(ThirdParty\\Lua REGULAR_EXPRESSION ${LUA_SOURCES_DIR}/.*)
 
+elseif (CMAKE_CROSSCOMPILING AND
+    "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
+
+  set(LUA_VERSIONS 5.3 5.2 5.1)
+
+  unset(LUA_VERSION)
+  foreach(version IN ITEMS ${LUA_VERSIONS})
+    CHECK_INCLUDE_FILE(lua${version}/lua.h HAVE_LUA${version}_H)
+    if (HAVE_LUA${version}_H)
+      set(LUA_VERSION ${version})
+      break()
+    endif()
+  endforeach()
+
+  if (NOT LUA_VERSION)
+    message(FATAL_ERROR "Please install the liblua-dev package")
+  endif()
+  
+  CHECK_LIBRARY_EXISTS(lua${LUA_VERSION} "lua_call" "${LUA_LIB_DIR}" HAVE_LUA_LIB)
+  if (NOT HAVE_LUA_LIB)
+    message(FATAL_ERROR "Please install the liblua package")
+  endif()  
+
+  include_directories(${CROSSTOOL_NG_IMAGE}/usr/include/lua${LUA_VERSION})
+  link_libraries(lua${LUA_VERSION})
+
 else()
   include(FindLua)
 
--- a/Resources/CMake/OpenSslConfiguration.cmake	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/CMake/OpenSslConfiguration.cmake	Tue Apr 21 16:37:25 2020 +0200
@@ -9,6 +9,26 @@
 
   source_group(ThirdParty\\OpenSSL REGULAR_EXPRESSION ${OPENSSL_SOURCES_DIR}/.*)
 
+elseif (CMAKE_CROSSCOMPILING AND
+    "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
+
+  CHECK_INCLUDE_FILE_CXX(openssl/opensslv.h HAVE_OPENSSL_H)
+  if (NOT HAVE_OPENSSL_H)
+    message(FATAL_ERROR "Please install the libopenssl-dev package")
+  endif()
+
+  CHECK_LIBRARY_EXISTS(crypto "OPENSSL_init" "" HAVE_OPENSSL_CRYPTO_LIB)
+  if (NOT HAVE_OPENSSL_CRYPTO_LIB)
+    message(FATAL_ERROR "Please install the libopenssl package")
+  endif()  
+  
+  CHECK_LIBRARY_EXISTS(ssl "SSL_library_init" "" HAVE_OPENSSL_SSL_LIB)
+  if (NOT HAVE_OPENSSL_SSL_LIB)
+    message(FATAL_ERROR "Please install the libopenssl package")
+  endif()  
+  
+  link_libraries(crypto ssl)
+
 else()
   include(FindOpenSSL)
 
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake	Tue Apr 21 16:37:25 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/CMake/SQLiteConfiguration.cmake	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/CMake/SQLiteConfiguration.cmake	Tue Apr 21 16:37:25 2020 +0200
@@ -41,12 +41,14 @@
   source_group(ThirdParty\\SQLite REGULAR_EXPRESSION ${SQLITE_SOURCES_DIR}/.*)
 
 else()
-  CHECK_INCLUDE_FILE_CXX(sqlite3.h HAVE_SQLITE_H)
+  CHECK_INCLUDE_FILE(sqlite3.h HAVE_SQLITE_H)
   if (NOT HAVE_SQLITE_H)
     message(FATAL_ERROR "Please install the libsqlite3-dev package")
   endif()
 
-  find_path(SQLITE_INCLUDE_DIR sqlite3.h
+  find_path(SQLITE_INCLUDE_DIR
+    NAMES sqlite3.h
+    PATHS
     /usr/include
     /usr/local/include
     )
--- a/Resources/Configuration.json	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/Configuration.json	Tue Apr 21 16:37:25 2020 +0200
@@ -409,7 +409,7 @@
   // as new DICOM commands are issued. This option sets the number of
   // seconds of inactivity to wait before automatically closing a
   // DICOM association used by Lua. If set to 0, the connection is
-  // closed immediately.
+  // closed immediately. This option is only used in Lua scripts.
   "DicomAssociationCloseDelay" : 5,
 
   // Maximum number of query/retrieve DICOM requests that are
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CrossToolchain.cmake	Tue Apr 21 16:37:25 2020 +0200
@@ -0,0 +1,56 @@
+#
+#  $ CROSSTOOL_NG_ARCH=mips CROSSTOOL_NG_BOARD=malta CROSSTOOL_NG_IMAGE=/tmp/mips cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/CrossToolchain.cmake -DBUILD_CONNECTIVITY_CHECKS=OFF -DUSE_SYSTEM_CIVETWEB=OFF -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON -DUSE_SYSTEM_JSONCPP=OFF -DUSE_SYSTEM_UUID=OFF -DENABLE_DCMTK_JPEG_LOSSLESS=OFF -G Ninja && ninja
+#
+
+INCLUDE(CMakeForceCompiler)
+
+SET(CROSSTOOL_NG_ROOT $ENV{CROSSTOOL_NG_ROOT} CACHE STRING "")
+SET(CROSSTOOL_NG_ARCH $ENV{CROSSTOOL_NG_ARCH} CACHE STRING "")
+SET(CROSSTOOL_NG_BOARD $ENV{CROSSTOOL_NG_BOARD} CACHE STRING "")
+SET(CROSSTOOL_NG_SUFFIX $ENV{CROSSTOOL_NG_SUFFIX} CACHE STRING "")
+SET(CROSSTOOL_NG_IMAGE $ENV{CROSSTOOL_NG_IMAGE} CACHE STRING "")
+
+IF ("${CROSSTOOL_NG_ROOT}" STREQUAL "")
+  SET(CROSSTOOL_NG_ROOT "/home/$ENV{USER}/x-tools")
+ENDIF()
+
+IF ("${CROSSTOOL_NG_SUFFIX}" STREQUAL "")
+  SET(CROSSTOOL_NG_SUFFIX "linux-gnu")
+ENDIF()
+
+SET(CROSSTOOL_NG_NAME ${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_BOARD}-${CROSSTOOL_NG_SUFFIX})
+SET(CROSSTOOL_NG_BASE ${CROSSTOOL_NG_ROOT}/${CROSSTOOL_NG_NAME})
+
+# the name of the target operating system
+SET(CMAKE_SYSTEM_NAME Linux)
+SET(CMAKE_SYSTEM_VERSION CrossToolNg)
+SET(CMAKE_SYSTEM_PROCESSOR ${CROSSTOOL_NG_ARCH})
+
+# which compilers to use for C and C++
+SET(CMAKE_C_COMPILER ${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-gcc)
+
+if (${CMAKE_VERSION} VERSION_LESS "3.6.0") 
+  CMAKE_FORCE_CXX_COMPILER(${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-g++ GNU)
+else()
+  SET(CMAKE_CXX_COMPILER ${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-g++)
+endif()
+
+# here is the target environment located
+SET(CMAKE_FIND_ROOT_PATH ${CROSSTOOL_NG_IMAGE})
+#SET(CMAKE_FIND_ROOT_PATH ${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# search headers and libraries in the target environment, search 
+# programs in the host environment
+SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+SET(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+
+SET(CMAKE_CROSSCOMPILING ON)
+#SET(CROSS_COMPILER_PREFIX ${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX})
+
+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CROSSTOOL_NG_IMAGE}/usr/include -I${CROSSTOOL_NG_IMAGE}/usr/include/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
+SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CROSSTOOL_NG_IMAGE}/usr/include -I${CROSSTOOL_NG_IMAGE}/usr/include/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
+SET(CMAKE_EXE_LINKER_FLAGS "-Wl,--unresolved-symbols=ignore-in-shared-libs -L${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX} -L${CROSSTOOL_NG_IMAGE}/lib -L${CROSSTOOL_NG_IMAGE}/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
+SET(CMAKE_SHARED_LINKER_FLAGS "-Wl,--unresolved-symbols=ignore-in-shared-libs -L${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX} -L${CROSSTOOL_NG_IMAGE}/lib -L${CROSSTOOL_NG_IMAGE}/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
--- a/Resources/DicomConformanceStatement.txt	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/DicomConformanceStatement.txt	Tue Apr 21 16:37:25 2020 +0200
@@ -257,8 +257,10 @@
 
 The information above about the SCP support is readily extracted from
 the function "Orthanc::Internals::AcceptAssociation()" from file
-"OrthancServer/Internals/CommandDispatcher.cpp".
+"Core/DicomNetworking/Internals/CommandDispatcher.cpp".
 
-The information above about the SCU support is derived from the class
-"Orthanc::DicomUserConnection" from file
-"OrthancServer/DicomProtocol/DicomUserConnection.cpp".
+The information above about the SCU support is derived from the
+classes "Orthanc::DicomControlUserConnection" and
+"Orthanc::DicomStoreUserConnection" from file
+"Core/DicomNetworking/DicomControlUserConnection.cpp" and
+"Core/DicomNetworking/DicomStoreUserConnection.cpp".
--- a/Resources/DownloadOrthancFramework.cmake	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/DownloadOrthancFramework.cmake	Tue Apr 21 16:37:25 2020 +0200
@@ -114,6 +114,8 @@
         set(ORTHANC_FRAMEWORK_MD5 "82323e8c49a667f658a3639ea4dbc336")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.0")
         set(ORTHANC_FRAMEWORK_MD5 "eab428d6e53f61e847fa360bb17ebe25")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.1")
+        set(ORTHANC_FRAMEWORK_MD5 "3971f5de96ba71dc9d3f3690afeaa7c0")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
@@ -216,7 +218,7 @@
   else()
     message("Forking the Orthanc source repository using Mercurial")
     execute_process(
-      COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://bitbucket.org/sjodogne/orthanc"
+      COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://hg.orthanc-server.com/orthanc/"
       WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
       RESULT_VARIABLE Failure
       )    
--- a/Resources/LinuxStandardBaseToolchain.cmake	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/LinuxStandardBaseToolchain.cmake	Tue Apr 21 16:37:25 2020 +0200
@@ -10,10 +10,10 @@
 
 INCLUDE(CMakeForceCompiler)
 
-SET(LSB_PATH $ENV{LSB_PATH})
-SET(LSB_CC $ENV{LSB_CC})
-SET(LSB_CXX $ENV{LSB_CXX})
-SET(LSB_TARGET_VERSION "4.0")
+SET(LSB_PATH $ENV{LSB_PATH} CACHE STRING "")
+SET(LSB_CC $ENV{LSB_CC} CACHE STRING "")
+SET(LSB_CXX $ENV{LSB_CXX} CACHE STRING "")
+SET(LSB_TARGET_VERSION "4.0" CACHE STRING "")
 
 IF ("${LSB_PATH}" STREQUAL "")
   SET(LSB_PATH "/opt/lsb")
--- a/Resources/Samples/README.txt	Fri Mar 27 10:06:58 2020 -0400
+++ b/Resources/Samples/README.txt	Tue Apr 21 16:37:25 2020 +0200
@@ -2,6 +2,6 @@
 the "OrthancContributed" repository on GitHub:
 https://github.com/jodogne/OrthancContributed
 
-The integration tests of Orthanc provide a lot of samples about the
+The integration tests of Orthanc provide many samples about the
 features of the REST API of Orthanc:
-https://bitbucket.org/sjodogne/orthanc-tests/src/default/Tests/Tests.py
+https://hg.orthanc-server.com/orthanc-tests/file/tip/Tests/Tests.py
--- a/UnitTestsSources/FromDcmtkTests.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/UnitTestsSources/FromDcmtkTests.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -710,7 +710,14 @@
 TEST(ParsedDicomFile, ToJsonFlags2)
 {
   ParsedDicomFile f(true);
-  f.Insert(DICOM_TAG_PIXEL_DATA, "Pixels", false, "");
+
+  {
+    // "ParsedDicomFile" uses Little Endian => 'B' (least significant
+    // byte) will be stored first in the memory buffer and in the
+    // file, then 'A'. Hence the expected "BA" value below.
+    Uint16 v[] = { 'A' * 256 + 'B', 0 };
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertUint16Array(DCM_PixelData, v, 2).good());
+  }
 
   Json::Value v;
   f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
@@ -729,7 +736,7 @@
   ASSERT_EQ(6u, v.getMemberNames().size());
   ASSERT_TRUE(v.isMember("7fe0,0010"));  
   ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type());  
-  ASSERT_EQ("Pixels", v["7fe0,0010"].asString());  
+  ASSERT_EQ("BA", v["7fe0,0010"].asString().substr(0, 2));
 
   f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePixelData, 0);
   ASSERT_EQ(Json::objectValue, v.type());
@@ -739,7 +746,7 @@
   std::string mime, content;
   ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7fe0,0010"].asString()));
   ASSERT_EQ("application/octet-stream", mime);
-  ASSERT_EQ("Pixels", content);
+  ASSERT_EQ("BA", content.substr(0, 2));
 }
 
 
--- a/UnitTestsSources/ImageTests.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/UnitTestsSources/ImageTests.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -410,6 +410,28 @@
   }
 
   {
+    // true means "enforce alignment by using a temporary buffer"
+    Orthanc::PamReader r(true);
+    r.ReadFromMemory(s);
+
+    ASSERT_EQ(r.GetFormat(), Orthanc::PixelFormat_Grayscale16);
+    ASSERT_EQ(r.GetWidth(), width);
+    ASSERT_EQ(r.GetHeight(), height);
+
+    v = 0;
+    for (unsigned int y = 0; y < height; y++)
+    {
+      const uint16_t* p = reinterpret_cast<const uint16_t*>
+        ((const uint8_t*)r.GetConstBuffer() + y * r.GetPitch());
+      ASSERT_EQ(p, r.GetConstRow(y));
+      for (unsigned int x = 0; x < width; x++, p++, v++)
+      {
+        ASSERT_EQ(v, *p);
+      }
+    }
+  }
+
+  {
     Orthanc::TemporaryFile tmp;
     tmp.Write(s);
 
@@ -432,4 +454,30 @@
       }
     }
   }
+
+  {
+    Orthanc::TemporaryFile tmp;
+    tmp.Write(s);
+
+    // true means "enforce alignment by using a temporary buffer"
+    Orthanc::PamReader r2(true);
+    r2.ReadFromFile(tmp.GetPath());
+
+    ASSERT_EQ(r2.GetFormat(), Orthanc::PixelFormat_Grayscale16);
+    ASSERT_EQ(r2.GetWidth(), width);
+    ASSERT_EQ(r2.GetHeight(), height);
+
+    v = 0;
+    for (unsigned int y = 0; y < height; y++)
+    {
+      const uint16_t* p = reinterpret_cast<const uint16_t*>
+        ((const uint8_t*)r2.GetConstBuffer() + y * r2.GetPitch());
+      ASSERT_EQ(p, r2.GetConstRow(y));
+      for (unsigned int x = 0; x < width; x++, p++, v++)
+      {
+        ASSERT_EQ(*p, v);
+      }
+    }
+  }
+
 }
--- a/UnitTestsSources/MultiThreadingTests.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -1098,7 +1098,6 @@
       StringOperationValue s2("world");
       lock.AddInput(a, s1);
       lock.AddInput(a, s2);
-      lock.SetDicomAssociationTimeout(200);
       lock.SetTrailingOperationTimeout(300);
     }
 
@@ -1284,7 +1283,6 @@
     MemoryStorageArea              storage_;
     SQLiteDatabaseWrapper          db_;   // The SQLite DB is in memory
     std::unique_ptr<ServerContext>   context_;
-    TimeoutDicomConnectionManager  manager_;
 
   public:
     OrthancJobsSerialization()
@@ -1407,27 +1405,31 @@
   // StoreScuOperation
 
   {
-    RemoteModalityParameters modality;
-    modality.SetApplicationEntityTitle("REMOTE");
-    modality.SetHost("192.168.1.1");
-    modality.SetPortNumber(1000);
-    modality.SetManufacturer(ModalityManufacturer_StoreScp);
+    TimeoutDicomConnectionManager luaManager;
+    
+    {
+      RemoteModalityParameters modality;
+      modality.SetApplicationEntityTitle("REMOTE");
+      modality.SetHost("192.168.1.1");
+      modality.SetPortNumber(1000);
+      modality.SetManufacturer(ModalityManufacturer_StoreScp);
 
-    StoreScuOperation operation("TEST", modality);
+      StoreScuOperation operation(luaManager, "TEST", modality);
 
-    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
-    operation.Serialize(s);
-  }
+      ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+      operation.Serialize(s);
+    }
 
-  {
-    operation.reset(unserializer.UnserializeOperation(s));
+    {
+      operation.reset(unserializer.UnserializeOperation(s));
 
-    const StoreScuOperation& tmp = dynamic_cast<StoreScuOperation&>(*operation);
-    ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost());
-    ASSERT_EQ(1000, tmp.GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer());
-    ASSERT_EQ("TEST", tmp.GetLocalAet());
+      const StoreScuOperation& tmp = dynamic_cast<StoreScuOperation&>(*operation);
+      ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle());
+      ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost());
+      ASSERT_EQ(1000, tmp.GetRemoteModality().GetPortNumber());
+      ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer());
+      ASSERT_EQ("TEST", tmp.GetLocalAet());
+    }
   }
 
   // SystemCallOperation
--- a/UnitTestsSources/RestApiTests.cpp	Fri Mar 27 10:06:58 2020 -0400
+++ b/UnitTestsSources/RestApiTests.cpp	Tue Apr 21 16:37:25 2020 +0200
@@ -121,22 +121,26 @@
   HttpClient c;
   c.SetHttpsVerifyPeers(true);
   c.SetHttpsCACertificates("UnitTestsResults/bitbucket.cert");
-  c.SetUrl("https://bitbucket.org/sjodogne/orthanc/raw/Orthanc-0.9.3/Resources/Configuration.json");
+
+  // Test file modified on 2020-04-20, in order to use a git
+  // repository on BitBucket instead of a Mercurial repository
+  // (because Mercurial support disappears on 2020-05-31)
+  c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json");
 
   Json::Value v;
   c.Apply(v);
-  ASSERT_TRUE(v.isMember("LuaScripts"));
+  ASSERT_TRUE(v.isMember("ServeFolders"));
 }
 
 TEST(HttpClient, SslNoVerification)
 {
   HttpClient c;
   c.SetHttpsVerifyPeers(false);
-  c.SetUrl("https://bitbucket.org/sjodogne/orthanc/raw/Orthanc-0.9.3/Resources/Configuration.json");
+  c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json");
 
   Json::Value v;
   c.Apply(v);
-  ASSERT_TRUE(v.isMember("LuaScripts"));
+  ASSERT_TRUE(v.isMember("ServeFolders"));
 }
 
 #endif