changeset 3799:320a2d224902

merge
author Alain Mazy <alain@mazy.be>
date Wed, 01 Apr 2020 10:15:33 +0200
parents c38b82bb6fd3 (current diff) d73ce7c537c3 (diff)
children 38b0f51781aa
files Core/DicomNetworking/DicomUserConnection.cpp
diffstat 91 files changed, 4826 insertions(+), 527 deletions(-) [+]
line wrap: on
line diff
--- a/AUTHORS	Wed Apr 01 10:14:49 2020 +0200
+++ b/AUTHORS	Wed Apr 01 10:15:33 2020 +0200
@@ -15,7 +15,7 @@
   Belgium
 
 * Osimis S.A.
-  Rue du Bois Saint-Jean 15/1
-  4102 Seraing
+  Quai Banning 6
+  4000 Liege
   Belgium
   http://www.osimis.io/
--- a/CMakeLists.txt	Wed Apr 01 10:14:49 2020 +0200
+++ b/CMakeLists.txt	Wed Apr 01 10:15:33 2020 +0200
@@ -25,6 +25,9 @@
 set(ENABLE_WEB_SERVER ON)
 set(ENABLE_ZLIB ON)
 
+# To test transcoding
+#set(ENABLE_DCMTK_TRANSCODING ON)
+
 set(HAS_EMBEDDED_RESOURCES ON)
 
 
@@ -103,8 +106,10 @@
   OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp
   OrthancServer/ServerJobs/ResourceModificationJob.cpp
   OrthancServer/ServerJobs/SplitStudyJob.cpp
+  OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp
   OrthancServer/ServerToolbox.cpp
   OrthancServer/SliceOrdering.cpp
+  OrthancServer/StorageCommitmentReports.cpp
   )
 
 
@@ -483,9 +488,13 @@
       )
   endif()
 
-  externalproject_add(ConnectivityChecksProject
+  externalproject_add(ConnectivityChecks
     SOURCE_DIR "${ORTHANC_ROOT}/Plugins/Samples/ConnectivityChecks"
 
+    # We explicitly provide a build directory, in order to avoid paths
+    # that are too long on our Visual Studio 2008 CIS
+    BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/ConnectivityChecks-build"
+
     CMAKE_ARGS
     -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
     -DCMAKE_INSTALL_PREFIX=${CMAKE_CURRENT_BINARY_DIR}
--- a/Core/Compatibility.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/Compatibility.h	Wed Apr 01 10:15:33 2020 +0200
@@ -47,17 +47,16 @@
 // the compiler ("/Zc:__cplusplus")
 // To make this header more robust, we use the _MSVC_LANG equivalent macro.
 
-#  if _MSC_VER > 1900
-#    if (defined _MSVC_LANG) && (_MSVC_LANG >= 201103L)
-#      define ORTHANC_Cxx03_DETECTED 0
-#    else
-#      define ORTHANC_Cxx03_DETECTED 1
-#    endif
-#  elif _MSC_VER > 1800
+// please note that not all C++11 features are supported when _MSC_VER == 1600
+// (or higher). This header file can be made for fine-grained, if required, 
+// based on specific _MSC_VER values
+
+#  if _MSC_VER >= 1600
 #    define ORTHANC_Cxx03_DETECTED 0
 #  else
 #    define ORTHANC_Cxx03_DETECTED 1
 #  endif
+
 #else
 // of _MSC_VER is not defined, we assume __cplusplus is correctly defined
 // if __cplusplus is not defined (very old compilers??), then the following
@@ -89,12 +88,12 @@
   class unique_ptr : public boost::movelib::unique_ptr<T>
   {
   public:
-    unique_ptr() :
+    explicit unique_ptr() :
       boost::movelib::unique_ptr<T>()
     {
     }      
 
-    unique_ptr(T* p) :
+    explicit unique_ptr(T* p) :
       boost::movelib::unique_ptr<T>(p)
     {
     }      
--- a/Core/DicomFormat/DicomMap.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomFormat/DicomMap.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -745,9 +745,8 @@
   }
 
 
-  bool DicomMap::ParseDicomMetaInformation(DicomMap& result,
-                                           const char* dicom,
-                                           size_t size)
+  bool DicomMap::IsDicomFile(const char* dicom,
+                             size_t size)
   {
     /**
      * http://dicom.nema.org/medical/dicom/current/output/chtml/part10/chapter_7.html
@@ -756,11 +755,19 @@
      * account to determine whether the file is or is not a DICOM file.
      **/
 
-    if (size < 132 ||
-        dicom[128] != 'D' ||
-        dicom[129] != 'I' ||
-        dicom[130] != 'C' ||
-        dicom[131] != 'M')
+    return (size >= 132 &&
+            dicom[128] == 'D' &&
+            dicom[129] == 'I' &&
+            dicom[130] == 'C' &&
+            dicom[131] == 'M');
+  }
+    
+
+  bool DicomMap::ParseDicomMetaInformation(DicomMap& result,
+                                           const char* dicom,
+                                           size_t size)
+  {
+    if (!IsDicomFile(dicom, size))
     {
       return false;
     }
--- a/Core/DicomFormat/DicomMap.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomFormat/DicomMap.h	Wed Apr 01 10:15:33 2020 +0200
@@ -180,6 +180,9 @@
 
     void GetTags(std::set<DicomTag>& tags) const;
 
+    static bool IsDicomFile(const char* dicom,
+                            size_t size);
+    
     static bool ParseDicomMetaInformation(DicomMap& result,
                                           const char* dicom,
                                           size_t size);
--- a/Core/DicomNetworking/DicomServer.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomNetworking/DicomServer.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -94,6 +94,7 @@
     moveRequestHandlerFactory_ = NULL;
     storeRequestHandlerFactory_ = NULL;
     worklistRequestHandlerFactory_ = NULL;
+    storageCommitmentFactory_ = NULL;
     applicationEntityFilter_ = NULL;
     checkCalledAet_ = true;
     associationTimeout_ = 30;
@@ -289,6 +290,29 @@
     }
   }
 
+  void DicomServer::SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& factory)
+  {
+    Stop();
+    storageCommitmentFactory_ = &factory;
+  }
+
+  bool DicomServer::HasStorageCommitmentRequestHandlerFactory() const
+  {
+    return (storageCommitmentFactory_ != NULL);
+  }
+
+  IStorageCommitmentRequestHandlerFactory& DicomServer::GetStorageCommitmentRequestHandlerFactory() const
+  {
+    if (HasStorageCommitmentRequestHandlerFactory())
+    {
+      return *storageCommitmentFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoStorageCommitmentHandler);
+    }
+  }
+
   void DicomServer::SetApplicationEntityFilter(IApplicationEntityFilter& factory)
   {
     Stop();
@@ -378,5 +402,4 @@
       return modalities_->IsSameAETitle(aet, GetApplicationEntityTitle());
     }
   }
-
 }
--- a/Core/DicomNetworking/DicomServer.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomNetworking/DicomServer.h	Wed Apr 01 10:15:33 2020 +0200
@@ -41,6 +41,7 @@
 #include "IMoveRequestHandlerFactory.h"
 #include "IStoreRequestHandlerFactory.h"
 #include "IWorklistRequestHandlerFactory.h"
+#include "IStorageCommitmentRequestHandlerFactory.h"
 #include "IApplicationEntityFilter.h"
 #include "RemoteModalityParameters.h"
 
@@ -82,6 +83,7 @@
     IMoveRequestHandlerFactory* moveRequestHandlerFactory_;
     IStoreRequestHandlerFactory* storeRequestHandlerFactory_;
     IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_;
+    IStorageCommitmentRequestHandlerFactory* storageCommitmentFactory_;
     IApplicationEntityFilter* applicationEntityFilter_;
 
     static void ServerThread(DicomServer* server);
@@ -122,6 +124,10 @@
     bool HasWorklistRequestHandlerFactory() const;
     IWorklistRequestHandlerFactory& GetWorklistRequestHandlerFactory() const;
 
+    void SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& handler);
+    bool HasStorageCommitmentRequestHandlerFactory() const;
+    IStorageCommitmentRequestHandlerFactory& GetStorageCommitmentRequestHandlerFactory() const;
+
     void SetApplicationEntityFilter(IApplicationEntityFilter& handler);
     bool HasApplicationEntityFilter() const;
     IApplicationEntityFilter& GetApplicationEntityFilter() const;
--- a/Core/DicomNetworking/DicomUserConnection.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomNetworking/DicomUserConnection.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -86,6 +86,7 @@
 #  error The macro DCMTK_VERSION_NUMBER must be defined
 #endif
 
+#include "../Compatibility.h"
 #include "../DicomFormat/DicomArray.h"
 #include "../Logging.h"
 #include "../OrthancException.h"
@@ -158,7 +159,9 @@
 
     void CheckIsOpen() const;
 
-    void Store(DcmInputStream& is, 
+    void Store(std::string& sopClassUidOut  /* out */,
+               std::string& sopInstanceUidOut  /* out */,
+               DcmInputStream& is, 
                DicomUserConnection& connection,
                const std::string& moveOriginatorAET,
                uint16_t moveOriginatorID);
@@ -252,7 +255,8 @@
   }
   
     
-  void DicomUserConnection::SetupPresentationContexts(const std::string& preferredTransferSyntax)
+  void DicomUserConnection::SetupPresentationContexts(Mode mode,
+                                                      const std::string& preferredTransferSyntax)
   {
     // Flatten an array with the preferred transfer syntax
     const char* asPreferred[1] = { preferredTransferSyntax.c_str() };
@@ -274,30 +278,73 @@
     }
 
     CheckStorageSOPClassesInvariant();
-    unsigned int presentationContextId = 1;
+
+    switch (mode)
+    {
+      case Mode_Generic:
+      {
+        unsigned int presentationContextId = 1;
+
+        for (std::list<std::string>::const_iterator it = reservedStorageSOPClasses_.begin();
+             it != reservedStorageSOPClasses_.end(); ++it)
+        {
+          RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
+                                  *it, asPreferred, asFallback, remoteAet_);
+        }
 
-    for (std::list<std::string>::const_iterator it = reservedStorageSOPClasses_.begin();
-         it != reservedStorageSOPClasses_.end(); ++it)
-    {
-      RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
-                              *it, asPreferred, asFallback, remoteAet_);
-    }
+        for (std::set<std::string>::const_iterator it = storageSOPClasses_.begin();
+             it != storageSOPClasses_.end(); ++it)
+        {
+          RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
+                                  *it, asPreferred, asFallback, remoteAet_);
+        }
+
+        for (std::set<std::string>::const_iterator it = defaultStorageSOPClasses_.begin();
+             it != defaultStorageSOPClasses_.end(); ++it)
+        {
+          RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
+                                  *it, asPreferred, asFallback, remoteAet_);
+        }
+
+        break;
+      }
 
-    for (std::set<std::string>::const_iterator it = storageSOPClasses_.begin();
-         it != storageSOPClasses_.end(); ++it)
-    {
-      RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
-                              *it, asPreferred, asFallback, remoteAet_);
-    }
+      case Mode_RequestStorageCommitment:
+      case Mode_ReportStorageCommitment:
+      {
+        const char* as = UID_StorageCommitmentPushModelSOPClass;
+
+        std::vector<const char*> ts;
+        ts.push_back(UID_LittleEndianExplicitTransferSyntax);
+        ts.push_back(UID_LittleEndianImplicitTransferSyntax);
 
-    for (std::set<std::string>::const_iterator it = defaultStorageSOPClasses_.begin();
-         it != defaultStorageSOPClasses_.end(); ++it)
-    {
-      RegisterStorageSOPClass(pimpl_->params_, presentationContextId, 
-                              *it, asPreferred, asFallback, remoteAet_);
+        T_ASC_SC_ROLE role;
+        switch (mode)
+        {
+          case Mode_RequestStorageCommitment:
+            role = ASC_SC_ROLE_DEFAULT;
+            break;
+            
+          case Mode_ReportStorageCommitment:
+            role = ASC_SC_ROLE_SCP;
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+        
+        Check(ASC_addPresentationContext(pimpl_->params_, 1 /*presentationContextId*/,
+                                         as, &ts[0], ts.size(), role),
+              remoteAet_, "initializing");
+              
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
     }
   }
-
+  
 
   static bool IsGenericTransferSyntax(const std::string& syntax)
   {
@@ -307,7 +354,9 @@
   }
 
 
-  void DicomUserConnection::PImpl::Store(DcmInputStream& is, 
+  void DicomUserConnection::PImpl::Store(std::string& sopClassUidOut,
+                                         std::string& sopInstanceUidOut,
+                                         DcmInputStream& is, 
                                          DicomUserConnection& connection,
                                          const std::string& moveOriginatorAET,
                                          uint16_t moveOriginatorID)
@@ -390,6 +439,9 @@
                              connection.remoteAet_);
     }
 
+    sopClassUidOut.assign(sopClass);
+    sopInstanceUidOut.assign(sopInstance);
+
     // Figure out which of the accepted presentation contexts should be used
     int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass);
     if (presID == 0)
@@ -1060,7 +1112,7 @@
     }
   }
 
-  void DicomUserConnection::Open()
+  void DicomUserConnection::OpenInternal(Mode mode)
   {
     if (IsOpen())
     {
@@ -1100,7 +1152,7 @@
     Check(ASC_setTransportLayerType(pimpl_->params_, /*opt_secureConnection*/ false),
           remoteAet_, "connecting");
 
-    SetupPresentationContexts(preferredTransferSyntax_);
+    SetupPresentationContexts(mode, preferredTransferSyntax_);
 
     // Do the association
     Check(ASC_requestAssociation(pimpl_->net_, pimpl_->params_, &pimpl_->assoc_),
@@ -1144,7 +1196,9 @@
     return pimpl_->IsOpen();
   }
 
-  void DicomUserConnection::Store(const char* buffer, 
+  void DicomUserConnection::Store(std::string& sopClassUid /* out */,
+                                  std::string& sopInstanceUid /* out */,
+                                  const char* buffer, 
                                   size_t size,
                                   const std::string& moveOriginatorAET,
                                   uint16_t moveOriginatorID)
@@ -1155,26 +1209,31 @@
       is.setBuffer(buffer, size);
     is.setEos();
       
-    pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID);
+    pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID);
   }
 
-  void DicomUserConnection::Store(const std::string& buffer,
+  void DicomUserConnection::Store(std::string& sopClassUid /* out */,
+                                  std::string& sopInstanceUid /* out */,
+                                  const std::string& buffer,
                                   const std::string& moveOriginatorAET,
                                   uint16_t moveOriginatorID)
   {
     if (buffer.size() > 0)
-      Store(&buffer[0], buffer.size(), moveOriginatorAET, moveOriginatorID);
+      Store(sopClassUid, sopInstanceUid, &buffer[0], buffer.size(),
+            moveOriginatorAET, moveOriginatorID);
     else
-      Store(NULL, 0, moveOriginatorAET, moveOriginatorID);
+      Store(sopClassUid, sopInstanceUid, NULL, 0, moveOriginatorAET, moveOriginatorID);
   }
 
-  void DicomUserConnection::StoreFile(const std::string& path,
+  void DicomUserConnection::StoreFile(std::string& sopClassUid /* out */,
+                                      std::string& sopInstanceUid /* out */,
+                                      const std::string& path,
                                       const std::string& moveOriginatorAET,
                                       uint16_t moveOriginatorID)
   {
     // Prepare an input stream for the file
     DcmInputFileStream is(path.c_str());
-    pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID);
+    pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID);
   }
 
   bool DicomUserConnection::Echo()
@@ -1405,4 +1464,369 @@
             remotePort_ == remote.GetPortNumber() &&
             manufacturer_ == remote.GetManufacturer());
   }
+
+
+  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 DicomUserConnection::ReportStorageCommitment(
+    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);
+    }
+    
+    if (IsOpen())
+    {
+      Close();
+    }
+
+    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));
+        }
+      }
+    }
+    
+    try
+    {
+      OpenInternal(Mode_ReportStorageCommitment);
+
+      /**
+       * 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 \"" << remoteAet_
+                << "\" about storage commitment transaction: " << transactionUid
+                << " (" << successSopClassUids.size() << " successes, " 
+                << failedSopClassUids.size() << " failures)";
+      const DIC_US messageId = pimpl_->assoc_->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(
+          pimpl_->assoc_, UID_StorageCommitmentPushModelSOPClass);
+        if (presID == 0)
+        {
+          throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                                 "Unable to send N-EVENT-REPORT request to AET: " + remoteAet_);
+        }
+
+        if (!DIMSE_sendMessageUsingMemoryData(
+              pimpl_->assoc_, 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;
+
+        const int timeout = pimpl_->dimseTimeout_;
+        if (!DIMSE_receiveCommand(pimpl_->assoc_,
+                                  (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout,
+                                  &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: " + remoteAet_);
+        }
+
+        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: " + remoteAet_);
+        }
+
+        if (content.DimseStatus != 0 /* success */)
+        {
+          throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                                 "The request cannot be handled by remote AET: " + remoteAet_);
+        }
+      }
+
+      Close();
+    }
+    catch (OrthancException&)
+    {
+      Close();
+      throw;
+    }
+  }
+
+
+  
+  void DicomUserConnection::RequestStorageCommitment(
+    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);
+    }
+
+    if (IsOpen())
+    {
+      Close();
+    }
+
+    try
+    {
+      OpenInternal(Mode_RequestStorageCommitment);
+
+      /**
+       * 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 \"" << remoteAet_
+                << "\" about storage commitment for " << sopClassUids.size()
+                << " instances, with transaction UID: " << transactionUid;
+      const DIC_US messageId = pimpl_->assoc_->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(
+          pimpl_->assoc_, UID_StorageCommitmentPushModelSOPClass);
+        if (presID == 0)
+        {
+          throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                                 "Unable to send N-ACTION request to AET: " + remoteAet_);
+        }
+
+        if (!DIMSE_sendMessageUsingMemoryData(
+              pimpl_->assoc_, 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;
+        
+        const int timeout = pimpl_->dimseTimeout_;
+        if (!DIMSE_receiveCommand(pimpl_->assoc_,
+                                  (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout,
+                                  &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: " + remoteAet_);
+        }
+
+        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: " + remoteAet_);
+        }
+
+        if (content.DimseStatus != 0 /* success */)
+        {
+          throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                                 "The request cannot be handled by remote AET: " + remoteAet_);
+        }
+      }
+
+      Close();
+    }
+    catch (OrthancException&)
+    {
+      Close();
+      throw;
+    }
+  }
 }
--- a/Core/DicomNetworking/DicomUserConnection.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomNetworking/DicomUserConnection.h	Wed Apr 01 10:15:33 2020 +0200
@@ -54,6 +54,13 @@
     struct PImpl;
     boost::shared_ptr<PImpl> pimpl_;
 
+    enum Mode
+    {
+      Mode_Generic,
+      Mode_ReportStorageCommitment,
+      Mode_RequestStorageCommitment
+    };
+    
     // Connection parameters
     std::string preferredTransferSyntax_;
     std::string localAet_;
@@ -67,7 +74,8 @@
 
     void CheckIsOpen() const;
 
-    void SetupPresentationContexts(const std::string& preferredTransferSyntax);
+    void SetupPresentationContexts(Mode mode,
+                                   const std::string& preferredTransferSyntax);
 
     void MoveInternal(const std::string& targetAet,
                       ResourceType level,
@@ -79,6 +87,8 @@
 
     void DefaultSetup();
 
+    void OpenInternal(Mode mode);
+
   public:
     DicomUserConnection();
 
@@ -137,7 +147,10 @@
 
     void AddStorageSOPClass(const char* sop);
 
-    void Open();
+    void Open()
+    {
+      OpenInternal(Mode_Generic);
+    }
 
     void Close();
 
@@ -145,33 +158,45 @@
 
     bool Echo();
 
-    void Store(const char* buffer, 
+    void Store(std::string& sopClassUid /* out */,
+               std::string& sopInstanceUid /* out */,
+               const char* buffer, 
                size_t size,
                const std::string& moveOriginatorAET,
                uint16_t moveOriginatorID);
 
-    void Store(const char* buffer, 
+    void Store(std::string& sopClassUid /* out */,
+               std::string& sopInstanceUid /* out */,
+               const char* buffer, 
                size_t size)
     {
-      Store(buffer, size, "", 0);  // Not a C-Move
+      Store(sopClassUid, sopInstanceUid, buffer, size, "", 0);  // Not a C-Move
     }
 
-    void Store(const std::string& buffer,
+    void Store(std::string& sopClassUid /* out */,
+               std::string& sopInstanceUid /* out */,
+               const std::string& buffer,
                const std::string& moveOriginatorAET,
                uint16_t moveOriginatorID);
 
-    void Store(const std::string& buffer)
+    void Store(std::string& sopClassUid /* out */,
+               std::string& sopInstanceUid /* out */,
+               const std::string& buffer)
     {
-      Store(buffer, "", 0);  // Not a C-Move
+      Store(sopClassUid, sopInstanceUid, buffer, "", 0);  // Not a C-Move
     }
 
-    void StoreFile(const std::string& path,
+    void StoreFile(std::string& sopClassUid /* out */,
+                   std::string& sopInstanceUid /* out */,
+                   const std::string& path,
                    const std::string& moveOriginatorAET,
                    uint16_t moveOriginatorID);
 
-    void StoreFile(const std::string& path)
+    void StoreFile(std::string& sopClassUid /* out */,
+                   std::string& sopInstanceUid /* out */,
+                   const std::string& path)
     {
-      StoreFile(path, "", 0);  // Not a C-Move
+      StoreFile(sopClassUid, sopInstanceUid, path, "", 0);  // Not a C-Move
     }
 
     void Find(DicomFindAnswers& result,
@@ -212,5 +237,17 @@
 
     bool IsSameAssociation(const std::string& localAet,
                            const RemoteModalityParameters& remote) const;
+
+    void ReportStorageCommitment(
+      const std::string& transactionUid,
+      const std::vector<std::string>& sopClassUids,
+      const std::vector<std::string>& sopInstanceUids,
+      const std::vector<StorageCommitmentFailureReason>& failureReasons);
+
+    // transactionUid: To be generated by Toolbox::GenerateDicomPrivateUniqueIdentifier()
+    void RequestStorageCommitment(
+      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/IStorageCommitmentRequestHandler.h	Wed Apr 01 10:15:33 2020 +0200
@@ -0,0 +1,66 @@
+/**
+ * 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 <boost/noncopyable.hpp>
+#include <string>
+#include <vector>
+
+namespace Orthanc
+{
+  class IStorageCommitmentRequestHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IStorageCommitmentRequestHandler()
+    {
+    }
+
+    virtual void HandleRequest(const std::string& transactionUid,
+                               const std::vector<std::string>& sopClassUids,
+                               const std::vector<std::string>& sopInstanceUids,
+                               const std::string& remoteIp,
+                               const std::string& remoteAet,
+                               const std::string& calledAet) = 0;
+
+    virtual void HandleReport(const std::string& transactionUid,
+                              const std::vector<std::string>& successSopClassUids,
+                              const std::vector<std::string>& successSopInstanceUids,
+                              const std::vector<std::string>& failedSopClassUids,
+                              const std::vector<std::string>& failedSopInstanceUids,
+                              const std::vector<StorageCommitmentFailureReason>& failureReasons,
+                              const std::string& remoteIp,
+                              const std::string& remoteAet,
+                              const std::string& calledAet) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h	Wed Apr 01 10:15:33 2020 +0200
@@ -0,0 +1,49 @@
+/**
+ * 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 "IStorageCommitmentRequestHandler.h"
+
+namespace Orthanc
+{
+  class IStorageCommitmentRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IStorageCommitmentRequestHandlerFactory()
+    {
+    }
+
+    virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() = 0;
+  };
+}
--- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -93,9 +93,12 @@
 #include "../../Compatibility.h"
 #include "../../Toolbox.h"
 #include "../../Logging.h"
+#include "../../OrthancException.h"
 
+#include <dcmtk/dcmdata/dcdeftag.h>     /* for storage commitment */
+#include <dcmtk/dcmdata/dcsequen.h>     /* for class DcmSequenceOfItems */
+#include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
 #include <dcmtk/dcmnet/dcasccfg.h>      /* for class DcmAssociationConfiguration */
-#include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
 
 #include <boost/lexical_cast.hpp>
 
@@ -272,33 +275,6 @@
       OFString sprofile;
       OFString temp_str;
 
-      std::vector<const char*> knownAbstractSyntaxes;
-
-      // For C-STORE
-      if (server.HasStoreRequestHandlerFactory())
-      {
-        knownAbstractSyntaxes.push_back(UID_VerificationSOPClass);
-      }
-
-      // For C-FIND
-      if (server.HasFindRequestHandlerFactory())
-      {
-        knownAbstractSyntaxes.push_back(UID_FINDPatientRootQueryRetrieveInformationModel);
-        knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel);
-      }
-
-      if (server.HasWorklistRequestHandlerFactory())
-      {
-        knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel);
-      }
-
-      // For C-MOVE
-      if (server.HasMoveRequestHandlerFactory())
-      {
-        knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel);
-        knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel);
-      }
-
       cond = ASC_receiveAssociation(net, &assoc, 
                                     /*opt_maxPDU*/ ASC_DEFAULTMAXPDU, 
                                     NULL, NULL,
@@ -362,146 +338,206 @@
                 << " on IP " << remoteIp;
 
 
-      std::vector<const char*> transferSyntaxes;
-
-      // This is the list of the transfer syntaxes that were supported up to Orthanc 0.7.1
-      transferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
-      transferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
-      transferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
-
-      // New transfer syntaxes supported since Orthanc 0.7.2
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated))
       {
-        transferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); 
-      }
+        /* accept the abstract syntaxes for C-ECHO, C-FIND, C-MOVE,
+           and storage commitment, if presented */
 
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg))
-      {
-        transferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax);
-      }
+        std::vector<const char*> genericTransferSyntaxes;
+        genericTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
+        genericTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
+        genericTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
 
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000))
-      {
-        transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
-        transferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
-        transferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
-        transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax);
-        transferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax);
-      }
+        std::vector<const char*> knownAbstractSyntaxes;
 
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless))
-      {
-        transferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax);
-        transferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax);
-      }
-
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip))
-      {
-        transferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax);
-        transferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax);
-      }
+        // For C-ECHO (always enabled since Orthanc 1.6.0; in earlier
+        // versions, only enabled if C-STORE was also enabled)
+        knownAbstractSyntaxes.push_back(UID_VerificationSOPClass);
 
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2))
-      {
-        transferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax);
-        transferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax);
-      }
+        // For C-FIND
+        if (server.HasFindRequestHandlerFactory())
+        {
+          knownAbstractSyntaxes.push_back(UID_FINDPatientRootQueryRetrieveInformationModel);
+          knownAbstractSyntaxes.push_back(UID_FINDStudyRootQueryRetrieveInformationModel);
+        }
 
-#if DCMTK_VERSION_NUMBER >= 361
-      // New in Orthanc 1.6.0
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg4))
-      {
-        transferSyntaxes.push_back(UID_MPEG4BDcompatibleHighProfileLevel4_1TransferSyntax);
-        transferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_1TransferSyntax);
-        transferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For2DVideoTransferSyntax);
-        transferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For3DVideoTransferSyntax);
-        transferSyntaxes.push_back(UID_MPEG4StereoHighProfileLevel4_2TransferSyntax);
-      }
-#endif
-
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle))
-      {
-        transferSyntaxes.push_back(UID_RLELosslessTransferSyntax);
-      }
+        if (server.HasWorklistRequestHandlerFactory())
+        {
+          knownAbstractSyntaxes.push_back(UID_FINDModalityWorklistInformationModel);
+        }
 
-      /* accept the Verification SOP Class if presented */
-      cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
-        assoc->params,
-        &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(),
-        &transferSyntaxes[0], transferSyntaxes.size());
-      if (cond.bad())
-      {
-        LOG(INFO) << cond.text();
-        AssociationCleanup(assoc);
-        return NULL;
-      }
-
-      /* the array of Storage SOP Class UIDs that is defined within "dcmdata/libsrc/dcuid.cc" */
-      size_t count = 0;
-      while (dcmAllStorageSOPClassUIDs[count] != NULL)
-      {
-        count++;
-      }
+        // For C-MOVE
+        if (server.HasMoveRequestHandlerFactory())
+        {
+          knownAbstractSyntaxes.push_back(UID_MOVEStudyRootQueryRetrieveInformationModel);
+          knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel);
+        }
 
-#if DCMTK_VERSION_NUMBER >= 362
-      // The global variable "numberOfDcmAllStorageSOPClassUIDs" is
-      // only published if DCMTK >= 3.6.2:
-      // https://bitbucket.org/sjodogne/orthanc/issues/137
-      assert(static_cast<int>(count) == numberOfDcmAllStorageSOPClassUIDs);
-#endif
-      
-      cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
-        assoc->params,
-        dcmAllStorageSOPClassUIDs, count,
-        &transferSyntaxes[0], transferSyntaxes.size());
-      if (cond.bad())
-      {
-        LOG(INFO) << cond.text();
-        AssociationCleanup(assoc);
-        return NULL;
-      }
-
-      if (!server.HasApplicationEntityFilter() ||
-          server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet))
-      {
-        /*
-         * Promiscous mode is enabled: Accept everything not known not
-         * to be a storage SOP class.
-         **/
-        cond = acceptUnknownContextsWithPreferredTransferSyntaxes(
-          assoc->params, &transferSyntaxes[0], transferSyntaxes.size(), ASC_SC_ROLE_DEFAULT);
+        cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
+          assoc->params,
+          &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(),
+          &genericTransferSyntaxes[0], genericTransferSyntaxes.size());
         if (cond.bad())
         {
           LOG(INFO) << cond.text();
           AssociationCleanup(assoc);
           return NULL;
         }
+
+      
+        /* storage commitment support, new in Orthanc 1.6.0 */
+        if (server.HasStorageCommitmentRequestHandlerFactory())
+        {
+          /**
+           * "ASC_SC_ROLE_SCUSCP": The "SCU" role is needed to accept
+           * remote storage commitment requests, and the "SCP" role is
+           * needed to receive storage commitments answers.
+           **/        
+          const char* as[1] = { UID_StorageCommitmentPushModelSOPClass }; 
+          cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
+            assoc->params, as, 1,
+            &genericTransferSyntaxes[0], genericTransferSyntaxes.size(), ASC_SC_ROLE_SCUSCP);
+          if (cond.bad())
+          {
+            LOG(INFO) << cond.text();
+            AssociationCleanup(assoc);
+            return NULL;
+          }
+        }
+      }
+      
+
+      {
+        /* accept the abstract syntaxes for C-STORE, if presented */
+
+        std::vector<const char*> storageTransferSyntaxes;
+
+        // This is the list of the transfer syntaxes that were supported up to Orthanc 0.7.1
+        storageTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
+        storageTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
+        storageTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
+
+        // New transfer syntaxes supported since Orthanc 0.7.2
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated))
+        {
+          storageTransferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); 
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg))
+        {
+          storageTransferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax);
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000))
+        {
+          storageTransferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax);
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless))
+        {
+          storageTransferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax);
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip))
+        {
+          storageTransferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax);
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2))
+        {
+          storageTransferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax);
+        }
+
+#if DCMTK_VERSION_NUMBER >= 361
+        // New in Orthanc 1.6.0
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg4))
+        {
+          storageTransferSyntaxes.push_back(UID_MPEG4BDcompatibleHighProfileLevel4_1TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_1TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For2DVideoTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For3DVideoTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG4StereoHighProfileLevel4_2TransferSyntax);
+        }
+#endif
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle))
+        {
+          storageTransferSyntaxes.push_back(UID_RLELosslessTransferSyntax);
+        }
+
+        /* the array of Storage SOP Class UIDs that is defined within "dcmdata/libsrc/dcuid.cc" */
+        size_t count = 0;
+        while (dcmAllStorageSOPClassUIDs[count] != NULL)
+        {
+          count++;
+        }
+        
+#if DCMTK_VERSION_NUMBER >= 362
+        // The global variable "numberOfDcmAllStorageSOPClassUIDs" is
+        // only published if DCMTK >= 3.6.2:
+        // https://bitbucket.org/sjodogne/orthanc/issues/137
+        assert(static_cast<int>(count) == numberOfDcmAllStorageSOPClassUIDs);
+#endif
+      
+        cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
+          assoc->params,
+          dcmAllStorageSOPClassUIDs, count,
+          &storageTransferSyntaxes[0], storageTransferSyntaxes.size());
+        if (cond.bad())
+        {
+          LOG(INFO) << cond.text();
+          AssociationCleanup(assoc);
+          return NULL;
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsUnknownSopClassAccepted(remoteIp, remoteAet, calledAet))
+        {
+          /*
+           * Promiscous mode is enabled: Accept everything not known not
+           * to be a storage SOP class.
+           **/
+          cond = acceptUnknownContextsWithPreferredTransferSyntaxes(
+            assoc->params, &storageTransferSyntaxes[0], storageTransferSyntaxes.size(), ASC_SC_ROLE_DEFAULT);
+          if (cond.bad())
+          {
+            LOG(INFO) << cond.text();
+            AssociationCleanup(assoc);
+            return NULL;
+          }
+        }
       }
 
       /* set our app title */
@@ -703,6 +739,16 @@
             supported = true;
             break;
 
+          case DIMSE_N_ACTION_RQ:
+            request = DicomRequestType_NAction;
+            supported = true;
+            break;
+
+          case DIMSE_N_EVENT_REPORT_RQ:
+            request = DicomRequestType_NEventReport;
+            supported = true;
+            break;
+
           default:
             // we cannot handle this kind of message
             cond = DIMSE_BADCOMMANDTYPE;
@@ -784,6 +830,14 @@
               }
               break;
 
+            case DicomRequestType_NAction:
+              cond = NActionScp(&msg, presID);
+              break;              
+
+            case DicomRequestType_NEventReport:
+              cond = NEventReportScp(&msg, presID);
+              break;              
+
             default:
               // Should never happen
               break;
@@ -837,5 +891,379 @@
       }
       return cond;
     }
+
+
+    static DcmDataset* ReadDataset(T_ASC_Association* assoc,
+                                   const char* errorMessage,
+                                   int timeout)
+    {
+      DcmDataset *tmp = NULL;
+      T_ASC_PresentationContextID presIdData;
+    
+      OFCondition cond = DIMSE_receiveDataSetInMemory(
+        assoc, (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout,
+        &presIdData, &tmp, NULL, NULL);
+      if (!cond.good() ||
+          tmp == NULL)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, errorMessage);
+      }
+
+      return tmp;
+    }
+
+
+    static std::string ReadString(DcmDataset& dataset,
+                                  const DcmTagKey& tag)
+    {
+      const char* s = NULL;
+      if (!dataset.findAndGetString(tag, s).good() ||
+          s == NULL)
+      {
+        char buf[64];
+        sprintf(buf, "Missing mandatory tag in dataset: (%04X,%04X)",
+                tag.getGroup(), tag.getElement());
+        throw OrthancException(ErrorCode_NetworkProtocol, buf);
+      }
+
+      return std::string(s);
+    }
+
+
+    static void ReadSopSequence(
+      std::vector<std::string>& sopClassUids,
+      std::vector<std::string>& sopInstanceUids,
+      std::vector<StorageCommitmentFailureReason>* failureReasons, // Can be NULL
+      DcmDataset& dataset,
+      const DcmTagKey& tag,
+      bool mandatory)
+    {
+      sopClassUids.clear();
+      sopInstanceUids.clear();
+
+      if (failureReasons)
+      {
+        failureReasons->clear();
+      }
+
+      DcmSequenceOfItems* sequence = NULL;
+      if (!dataset.findAndGetSequence(tag, sequence).good() ||
+          sequence == NULL)
+      {
+        if (mandatory)
+        {        
+          char buf[64];
+          sprintf(buf, "Missing mandatory sequence in dataset: (%04X,%04X)",
+                  tag.getGroup(), tag.getElement());
+          throw OrthancException(ErrorCode_NetworkProtocol, buf);
+        }
+        else
+        {
+          return;
+        }
+      }
+
+      sopClassUids.reserve(sequence->card());
+      sopInstanceUids.reserve(sequence->card());
+
+      if (failureReasons)
+      {
+        failureReasons->reserve(sequence->card());
+      }
+
+      for (unsigned long i = 0; i < sequence->card(); i++)
+      {
+        const char* a = NULL;
+        const char* b = NULL;
+        if (!sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPClassUID, a).good() ||
+            !sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPInstanceUID, b).good() ||
+            a == NULL ||
+            b == NULL)
+        {
+          throw OrthancException(ErrorCode_NetworkProtocol,
+                                 "Missing Referenced SOP Class/Instance UID "
+                                 "in storage commitment dataset");
+        }
+
+        sopClassUids.push_back(a);
+        sopInstanceUids.push_back(b);
+
+        if (failureReasons != NULL)
+        {
+          Uint16 reason;
+          if (!sequence->getItem(i)->findAndGetUint16(DCM_FailureReason, reason).good())
+          {
+            throw OrthancException(ErrorCode_NetworkProtocol,
+                                   "Missing Failure Reason (0008,1197) "
+                                   "in storage commitment dataset");
+          }
+
+          failureReasons->push_back(static_cast<StorageCommitmentFailureReason>(reason));
+        }
+      }
+    }
+
+    
+    OFCondition CommandDispatcher::NActionScp(T_DIMSE_Message* msg,
+                                              T_ASC_PresentationContextID presID)
+    {
+      /**
+       * Starting with Orthanc 1.6.0, only storage commitment is
+       * supported with DICOM N-ACTION. This corresponds to the case
+       * where "Action Type ID" equals "1".
+       * 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
+       **/
+      
+      if (msg->CommandField != DIMSE_N_ACTION_RQ /* value == 304 == 0x0130 */ ||
+          !server_.HasStorageCommitmentRequestHandlerFactory())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+
+      /**
+       * Check that the storage commitment request is correctly formatted.
+       **/
+      
+      const T_DIMSE_N_ActionRQ& request = msg->msg.NActionRQ;
+
+      if (request.ActionTypeID != 1)
+      {
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Only storage commitment is implemented for DICOM N-ACTION SCP");
+      }
+
+      if (std::string(request.RequestedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
+          std::string(request.RequestedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Unexpected incoming SOP class or instance UID for storage commitment");
+      }
+
+      if (request.DataSetType != DIMSE_DATASET_PRESENT)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Incoming storage commitment request without a dataset");
+      }
+
+
+      /**
+       * Extract the DICOM dataset that is associated with the DIMSE
+       * message. The content of this dataset is documented in "Table
+       * J.3-1. Storage Commitment Request - Action Information":
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html#table_J.3-1
+       **/
+      
+      std::unique_ptr<DcmDataset> dataset(
+        ReadDataset(assoc_, "Cannot read the dataset in N-ACTION SCP", associationTimeout_));
+
+      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
+
+      std::vector<std::string> sopClassUid, sopInstanceUid;
+      ReadSopSequence(sopClassUid, sopInstanceUid, NULL,
+                      *dataset, DCM_ReferencedSOPSequence, true /* mandatory */);
+
+      LOG(INFO) << "Incoming storage commitment request, with transaction UID: " << transactionUid;
+
+      for (size_t i = 0; i < sopClassUid.size(); i++)
+      {
+        LOG(INFO) << "  (" << (i + 1) << "/" << sopClassUid.size()
+                  << ") queried SOP Class/Instance UID: "
+                  << sopClassUid[i] << " / " << sopInstanceUid[i];
+      }
+
+
+      /**
+       * Call the Orthanc handler. The list of available DIMSE status
+       * codes can be found at:
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10
+       **/
+
+      DIC_US dimseStatus;
+  
+      try
+      {
+        std::unique_ptr<IStorageCommitmentRequestHandler> handler
+          (server_.GetStorageCommitmentRequestHandlerFactory().
+           ConstructStorageCommitmentRequestHandler());
+
+        handler->HandleRequest(transactionUid, sopClassUid, sopInstanceUid,
+                               remoteIp_, remoteAet_, calledAet_);
+        
+        dimseStatus = 0;  // Success
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Error while processing an incoming storage commitment request: " << e.What();
+
+        // Code 0x0110 - "General failure in processing the operation was encountered"
+        dimseStatus = STATUS_N_ProcessingFailure;
+      }
+
+
+      /**
+       * Send the DIMSE status back to the SCU.
+       **/
+
+      {
+        T_DIMSE_Message response;
+        memset(&response, 0, sizeof(response));
+        response.CommandField = DIMSE_N_ACTION_RSP;
+
+        T_DIMSE_N_ActionRSP& content = response.msg.NActionRSP;
+        content.MessageIDBeingRespondedTo = request.MessageID;
+        strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
+        content.DimseStatus = dimseStatus;
+        strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
+        content.ActionTypeID = 0; // Not present, as "O_NACTION_ACTIONTYPEID" not set in "opts"
+        content.DataSetType = DIMSE_DATASET_NULL;  // Dataset is absent in storage commitment response
+        content.opts = O_NACTION_AFFECTEDSOPCLASSUID | O_NACTION_AFFECTEDSOPINSTANCEUID;
+
+        return DIMSE_sendMessageUsingMemoryData(
+          assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */,
+          NULL /* callback */, NULL /* callback context */, NULL /* commandSet */);
+      }
+    }
+
+
+    OFCondition CommandDispatcher::NEventReportScp(T_DIMSE_Message* msg,
+                                                   T_ASC_PresentationContextID presID)
+    {
+      /**
+       * Starting with Orthanc 1.6.0, handling N-EVENT-REPORT for
+       * storage commitment.
+       * 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
+       **/
+
+      if (msg->CommandField != DIMSE_N_EVENT_REPORT_RQ /* value == 256 == 0x0100 */ ||
+          !server_.HasStorageCommitmentRequestHandlerFactory())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+
+      /**
+       * Check that the storage commitment report is correctly formatted.
+       **/
+      
+      const T_DIMSE_N_EventReportRQ& report = msg->msg.NEventReportRQ;
+
+      if (report.EventTypeID != 1 /* successful */ &&
+          report.EventTypeID != 2 /* failures exist */)
+      {
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Unknown event for DICOM N-EVENT-REPORT SCP");
+      }
+
+      if (std::string(report.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
+          std::string(report.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Unexpected incoming SOP class or instance UID for storage commitment");
+      }
+
+      if (report.DataSetType != DIMSE_DATASET_PRESENT)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Incoming storage commitment report without a dataset");
+      }
+
+
+      /**
+       * Extract the DICOM dataset that is associated with the DIMSE
+       * message. The content of this dataset is documented in "Table
+       * J.3-2. Storage Commitment Result - Event Information":
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html#table_J.3-2
+       **/
+      
+      std::unique_ptr<DcmDataset> dataset(
+        ReadDataset(assoc_, "Cannot read the dataset in N-EVENT-REPORT SCP", associationTimeout_));
+
+      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
+
+      std::vector<std::string> successSopClassUid, successSopInstanceUid;
+      ReadSopSequence(successSopClassUid, successSopInstanceUid, NULL,
+                      *dataset, DCM_ReferencedSOPSequence,
+                      (report.EventTypeID == 1) /* mandatory in the case of success */);
+
+      std::vector<std::string> failedSopClassUid, failedSopInstanceUid;
+      std::vector<StorageCommitmentFailureReason> failureReasons;
+
+      if (report.EventTypeID == 2 /* failures exist */)
+      {
+        ReadSopSequence(failedSopClassUid, failedSopInstanceUid, &failureReasons,
+                        *dataset, DCM_FailedSOPSequence, true);
+      }
+
+      LOG(INFO) << "Incoming storage commitment report, with transaction UID: " << transactionUid;
+
+      for (size_t i = 0; i < successSopClassUid.size(); i++)
+      {
+        LOG(INFO) << "  (success " << (i + 1) << "/" << successSopClassUid.size()
+                  << ") SOP Class/Instance UID: "
+                  << successSopClassUid[i] << " / " << successSopInstanceUid[i];
+      }
+
+      for (size_t i = 0; i < failedSopClassUid.size(); i++)
+      {
+        LOG(INFO) << "  (failure " << (i + 1) << "/" << failedSopClassUid.size()
+                  << ") SOP Class/Instance UID: "
+                  << failedSopClassUid[i] << " / " << failedSopInstanceUid[i];
+      }
+
+      /**
+       * Call the Orthanc handler. The list of available DIMSE status
+       * codes can be found at:
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10
+       **/
+
+      DIC_US dimseStatus;
+
+      try
+      {
+        std::unique_ptr<IStorageCommitmentRequestHandler> handler
+          (server_.GetStorageCommitmentRequestHandlerFactory().
+           ConstructStorageCommitmentRequestHandler());
+
+        handler->HandleReport(transactionUid, successSopClassUid, successSopInstanceUid,
+                              failedSopClassUid, failedSopInstanceUid, failureReasons,
+                              remoteIp_, remoteAet_, calledAet_);
+        
+        dimseStatus = 0;  // Success
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Error while processing an incoming storage commitment report: " << e.What();
+
+        // Code 0x0110 - "General failure in processing the operation was encountered"
+        dimseStatus = STATUS_N_ProcessingFailure;
+      }
+
+      
+      /**
+       * Send the DIMSE status back to the SCU.
+       **/
+
+      {
+        T_DIMSE_Message response;
+        memset(&response, 0, sizeof(response));
+        response.CommandField = DIMSE_N_EVENT_REPORT_RSP;
+
+        T_DIMSE_N_EventReportRSP& content = response.msg.NEventReportRSP;
+        content.MessageIDBeingRespondedTo = report.MessageID;
+        strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
+        content.DimseStatus = dimseStatus;
+        strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
+        content.EventTypeID = 0; // Not present, as "O_NEVENTREPORT_EVENTTYPEID" not set in "opts"
+        content.DataSetType = DIMSE_DATASET_NULL;  // Dataset is absent in storage commitment response
+        content.opts = O_NEVENTREPORT_AFFECTEDSOPCLASSUID | O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID;
+
+        return DIMSE_sendMessageUsingMemoryData(
+          assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */,
+          NULL /* callback */, NULL /* callback context */, NULL /* commandSet */);
+      }
+    }
   }
 }
--- a/Core/DicomNetworking/Internals/CommandDispatcher.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomNetworking/Internals/CommandDispatcher.h	Wed Apr 01 10:15:33 2020 +0200
@@ -56,6 +56,12 @@
       std::string calledAet_;
       IApplicationEntityFilter* filter_;
 
+      OFCondition NActionScp(T_DIMSE_Message* msg, 
+                             T_ASC_PresentationContextID presID);
+
+      OFCondition NEventReportScp(T_DIMSE_Message* msg, 
+                                  T_ASC_PresentationContextID presID);
+      
     public:
       CommandDispatcher(const DicomServer& server,
                         T_ASC_Association* assoc,
@@ -69,11 +75,11 @@
       virtual bool Step();
     };
 
-    OFCondition EchoScp(T_ASC_Association * assoc, 
-                        T_DIMSE_Message * msg, 
-                        T_ASC_PresentationContextID presID);
-
     CommandDispatcher* AcceptAssociation(const DicomServer& server, 
                                          T_ASC_Network *net);
+
+    OFCondition EchoScp(T_ASC_Association* assoc, 
+                        T_DIMSE_Message* msg, 
+                        T_ASC_PresentationContextID presID);
   }
 }
--- a/Core/DicomNetworking/RemoteModalityParameters.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomNetworking/RemoteModalityParameters.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -48,6 +48,9 @@
 static const char* KEY_ALLOW_GET = "AllowGet";
 static const char* KEY_ALLOW_MOVE = "AllowMove";
 static const char* KEY_ALLOW_STORE = "AllowStore";
+static const char* KEY_ALLOW_N_ACTION = "AllowNAction";
+static const char* KEY_ALLOW_N_EVENT_REPORT = "AllowEventReport";
+static const char* KEY_ALLOW_STORAGE_COMMITMENT = "AllowStorageCommitment";
 static const char* KEY_HOST = "Host";
 static const char* KEY_MANUFACTURER = "Manufacturer";
 static const char* KEY_PORT = "Port";
@@ -66,6 +69,8 @@
     allowFind_ = true;
     allowMove_ = true;
     allowGet_ = true;
+    allowNAction_ = true;  // For storage commitment
+    allowNEventReport_ = true;  // For storage commitment
   }
 
 
@@ -211,6 +216,23 @@
     {
       allowMove_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_MOVE);
     }
+
+    if (serialized.isMember(KEY_ALLOW_N_ACTION))
+    {
+      allowNAction_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_N_ACTION);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_N_EVENT_REPORT))
+    {
+      allowNEventReport_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_N_EVENT_REPORT);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_STORAGE_COMMITMENT))
+    {
+      bool allow = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_STORAGE_COMMITMENT);
+      allowNAction_ = allow;
+      allowNEventReport_ = allow;
+    }
   }
 
 
@@ -233,6 +255,12 @@
       case DicomRequestType_Store:
         return allowStore_;
 
+      case DicomRequestType_NAction:
+        return allowNAction_;
+
+      case DicomRequestType_NEventReport:
+        return allowNEventReport_;
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -264,6 +292,14 @@
         allowStore_ = allowed;
         break;
 
+      case DicomRequestType_NAction:
+        allowNAction_ = allowed;
+        break;
+
+      case DicomRequestType_NEventReport:
+        allowNEventReport_ = allowed;
+        break;
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -276,7 +312,9 @@
             !allowStore_ ||
             !allowFind_ ||
             !allowGet_ ||
-            !allowMove_);
+            !allowMove_ ||
+            !allowNAction_ ||
+            !allowNEventReport_);
   }
 
   
@@ -296,6 +334,8 @@
       target[KEY_ALLOW_FIND] = allowFind_;
       target[KEY_ALLOW_GET] = allowGet_;
       target[KEY_ALLOW_MOVE] = allowMove_;
+      target[KEY_ALLOW_N_ACTION] = allowNAction_;
+      target[KEY_ALLOW_N_EVENT_REPORT] = allowNEventReport_;
     }
     else
     {
--- a/Core/DicomNetworking/RemoteModalityParameters.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomNetworking/RemoteModalityParameters.h	Wed Apr 01 10:15:33 2020 +0200
@@ -53,7 +53,9 @@
     bool                  allowFind_;
     bool                  allowMove_;
     bool                  allowGet_;
-
+    bool                  allowNAction_;
+    bool                  allowNEventReport_;
+    
     void Clear();
 
     void UnserializeArray(const Json::Value& serialized);
--- a/Core/DicomParsing/FromDcmtkBridge.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomParsing/FromDcmtkBridge.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -68,10 +68,11 @@
 #include <dcmtk/dcmdata/dcdicent.h>
 #include <dcmtk/dcmdata/dcdict.h>
 #include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcistrmb.h>
 #include <dcmtk/dcmdata/dcostrmb.h>
 #include <dcmtk/dcmdata/dcpixel.h>
 #include <dcmtk/dcmdata/dcuid.h>
-#include <dcmtk/dcmdata/dcistrmb.h>
+#include <dcmtk/dcmdata/dcxfer.h>
 
 #include <dcmtk/dcmdata/dcvrae.h>
 #include <dcmtk/dcmdata/dcvras.h>
@@ -1878,7 +1879,15 @@
     std::unique_ptr<DcmFileFormat> result(new DcmFileFormat);
 
     result->transferInit();
-    if (!result->read(is).good())
+
+    /**
+     * New in Orthanc 1.6.0: The "size" is given as an argument to the
+     * "read()" method. This can avoid huge memory consumption if
+     * parsing an invalid DICOM file, which can notably been observed
+     * by executing the integration test "test_upload_compressed" on
+     * valgrind running Orthanc.
+     **/
+    if (!result->read(is, EXS_Unknown, EGL_noChange, size).good())
     {
       throw OrthancException(ErrorCode_BadFileFormat,
                              "Cannot parse an invalid DICOM file (size: " +
@@ -2081,7 +2090,7 @@
     // Unregister JPEG codecs
     DJDecoderRegistration::cleanup();
 # if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-    DJDecoderRegistration::cleanup();
+    DJEncoderRegistration::cleanup();
 # endif
 #endif
   }
--- a/Core/DicomParsing/FromDcmtkBridge.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomParsing/FromDcmtkBridge.h	Wed Apr 01 10:15:33 2020 +0200
@@ -271,7 +271,10 @@
                       ITagVisitor& visitor,
                       Encoding defaultEncoding);
 
-    static bool GetDcmtkTransferSyntax(E_TransferSyntax& target,
-                                       DicomTransferSyntax syntax);
+    static bool LookupDcmtkTransferSyntax(E_TransferSyntax& target,
+                                          DicomTransferSyntax source);
+
+    static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                            E_TransferSyntax source);
   };
 }
--- a/Core/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h	Wed Apr 01 10:15:33 2020 +0200
@@ -34,10 +34,10 @@
 
 namespace Orthanc
 {
-  bool GetDcmtkTransferSyntax(E_TransferSyntax& target,
-                              DicomTransferSyntax syntax)
+  bool FromDcmtkBridge::LookupDcmtkTransferSyntax(E_TransferSyntax& target,
+                                                  DicomTransferSyntax source)
   {
-    switch (syntax)
+    switch (source)
     {
       case DicomTransferSyntax_LittleEndianImplicit:
         target = EXS_LittleEndianImplicit;
@@ -56,75 +56,147 @@
         return true;
 
       case DicomTransferSyntax_JPEGProcess1:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess1TransferSyntax;
+#  else
         target = EXS_JPEGProcess1;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess2_4:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess2_4TransferSyntax;
+#  else
         target = EXS_JPEGProcess2_4;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess3_5:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess3_5TransferSyntax;
+#  else
         target = EXS_JPEGProcess3_5;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess6_8:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess6_8TransferSyntax;
+#  else
         target = EXS_JPEGProcess6_8;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess7_9:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess7_9TransferSyntax;
+#  else
         target = EXS_JPEGProcess7_9;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess10_12:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess10_12TransferSyntax;
+#  else
         target = EXS_JPEGProcess10_12;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess11_13:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess11_13TransferSyntax;
+#  else
         target = EXS_JPEGProcess11_13;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess14:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess14TransferSyntax;
+#  else
         target = EXS_JPEGProcess14;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess15:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess15TransferSyntax;
+#  else
         target = EXS_JPEGProcess15;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess16_18:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess16_18TransferSyntax;
+#  else
         target = EXS_JPEGProcess16_18;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess17_19:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess17_19TransferSyntax;
+#  else
         target = EXS_JPEGProcess17_19;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess20_22:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess20_22TransferSyntax;
+#  else
         target = EXS_JPEGProcess20_22;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess21_23:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess21_23TransferSyntax;
+#  else
         target = EXS_JPEGProcess21_23;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess24_26:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess24_26TransferSyntax;
+#  else
         target = EXS_JPEGProcess24_26;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess25_27:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess25_27TransferSyntax;
+#  else
         target = EXS_JPEGProcess25_27;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess28:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess28TransferSyntax;
+#  else
         target = EXS_JPEGProcess28;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess29:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess29TransferSyntax;
+#  else
         target = EXS_JPEGProcess29;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGProcess14SV1:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess14SV1TransferSyntax;
+#  else
         target = EXS_JPEGProcess14SV1;
+#  endif
         return true;
 
       case DicomTransferSyntax_JPEGLSLossless:
@@ -167,25 +239,35 @@
         target = EXS_MPEG2MainProfileAtHighLevel;
         return true;
 
+#if DCMTK_VERSION_NUMBER >= 361
       case DicomTransferSyntax_MPEG4HighProfileLevel4_1:
         target = EXS_MPEG4HighProfileLevel4_1;
         return true;
+#endif
 
+#if DCMTK_VERSION_NUMBER >= 361
       case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1:
         target = EXS_MPEG4BDcompatibleHighProfileLevel4_1;
         return true;
+#endif
 
+#if DCMTK_VERSION_NUMBER >= 361
       case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo:
         target = EXS_MPEG4HighProfileLevel4_2_For2DVideo;
         return true;
+#endif
 
+#if DCMTK_VERSION_NUMBER >= 361
       case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo:
         target = EXS_MPEG4HighProfileLevel4_2_For3DVideo;
         return true;
+#endif
 
+#if DCMTK_VERSION_NUMBER >= 361
       case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2:
         target = EXS_MPEG4StereoHighProfileLevel4_2;
         return true;
+#endif
 
 #if DCMTK_VERSION_NUMBER >= 362
       case DicomTransferSyntax_HEVCMainProfileLevel5_1:
@@ -207,4 +289,261 @@
         return false;
     }
   }
+  
+
+  bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                                    E_TransferSyntax source)
+  {
+    switch (source)
+    {
+      case EXS_LittleEndianImplicit:
+        target = DicomTransferSyntax_LittleEndianImplicit;
+        return true;
+
+      case EXS_LittleEndianExplicit:
+        target = DicomTransferSyntax_LittleEndianExplicit;
+        return true;
+
+      case EXS_DeflatedLittleEndianExplicit:
+        target = DicomTransferSyntax_DeflatedLittleEndianExplicit;
+        return true;
+
+      case EXS_BigEndianExplicit:
+        target = DicomTransferSyntax_BigEndianExplicit;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess1TransferSyntax:
+#  else
+      case EXS_JPEGProcess1:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess1;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess2_4TransferSyntax:
+#  else
+      case EXS_JPEGProcess2_4:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess2_4;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess3_5TransferSyntax:
+#  else
+      case EXS_JPEGProcess3_5:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess3_5;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess6_8TransferSyntax:
+#  else
+      case EXS_JPEGProcess6_8:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess6_8;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess7_9TransferSyntax:
+#  else
+      case EXS_JPEGProcess7_9:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess7_9;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess10_12TransferSyntax:
+#  else
+      case EXS_JPEGProcess10_12:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess10_12;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess11_13TransferSyntax:
+#  else
+      case EXS_JPEGProcess11_13:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess11_13;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess14TransferSyntax:
+#  else
+      case EXS_JPEGProcess14:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess14;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess15TransferSyntax:
+#  else
+      case EXS_JPEGProcess15:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess15;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess16_18TransferSyntax:
+#  else
+      case EXS_JPEGProcess16_18:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess16_18;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess17_19TransferSyntax:
+#  else
+      case EXS_JPEGProcess17_19:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess17_19;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess20_22TransferSyntax:
+#  else
+      case EXS_JPEGProcess20_22:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess20_22;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess21_23TransferSyntax:
+#  else
+      case EXS_JPEGProcess21_23:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess21_23;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess24_26TransferSyntax:
+#  else
+      case EXS_JPEGProcess24_26:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess24_26;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess25_27TransferSyntax:
+#  else
+      case EXS_JPEGProcess25_27:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess25_27;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess28TransferSyntax:
+#  else
+      case EXS_JPEGProcess28:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess28;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess29TransferSyntax:
+#  else
+      case EXS_JPEGProcess29:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess29;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess14SV1TransferSyntax:
+#  else
+      case EXS_JPEGProcess14SV1:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess14SV1;
+        return true;
+
+      case EXS_JPEGLSLossless:
+        target = DicomTransferSyntax_JPEGLSLossless;
+        return true;
+
+      case EXS_JPEGLSLossy:
+        target = DicomTransferSyntax_JPEGLSLossy;
+        return true;
+
+      case EXS_JPEG2000LosslessOnly:
+        target = DicomTransferSyntax_JPEG2000LosslessOnly;
+        return true;
+
+      case EXS_JPEG2000:
+        target = DicomTransferSyntax_JPEG2000;
+        return true;
+
+      case EXS_JPEG2000MulticomponentLosslessOnly:
+        target = DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly;
+        return true;
+
+      case EXS_JPEG2000Multicomponent:
+        target = DicomTransferSyntax_JPEG2000Multicomponent;
+        return true;
+
+      case EXS_JPIPReferenced:
+        target = DicomTransferSyntax_JPIPReferenced;
+        return true;
+
+      case EXS_JPIPReferencedDeflate:
+        target = DicomTransferSyntax_JPIPReferencedDeflate;
+        return true;
+
+      case EXS_MPEG2MainProfileAtMainLevel:
+        target = DicomTransferSyntax_MPEG2MainProfileAtMainLevel;
+        return true;
+
+      case EXS_MPEG2MainProfileAtHighLevel:
+        target = DicomTransferSyntax_MPEG2MainProfileAtHighLevel;
+        return true;
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4HighProfileLevel4_1:
+        target = DicomTransferSyntax_MPEG4HighProfileLevel4_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4BDcompatibleHighProfileLevel4_1:
+        target = DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4HighProfileLevel4_2_For2DVideo:
+        target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4HighProfileLevel4_2_For3DVideo:
+        target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4StereoHighProfileLevel4_2:
+        target = DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case EXS_HEVCMainProfileLevel5_1:
+        target = DicomTransferSyntax_HEVCMainProfileLevel5_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case EXS_HEVCMain10ProfileLevel5_1:
+        target = DicomTransferSyntax_HEVCMain10ProfileLevel5_1;
+        return true;
+#endif
+
+      case EXS_RLELossless:
+        target = DicomTransferSyntax_RLELossless;
+        return true;
+
+      default:
+        return false;
+    }
+  }
 }
--- a/Core/DicomParsing/Internals/DicomFrameIndex.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomParsing/Internals/DicomFrameIndex.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -68,7 +68,10 @@
       uint32_t length = item->getLength();
       if (length == 0)
       {
-        table.clear();
+        // Degenerate case: Empty offset table means only one frame
+        // that overlaps all the fragments
+        table.resize(1);
+        table[0] = 0;
         return;
       }
 
@@ -146,7 +149,6 @@
         throw OrthancException(ErrorCode_BadFileFormat);
       }
 
-
       // Loop over the fragments (ignoring the offset table). This is
       // an alternative, faster implementation to DCMTK's
       // "DcmCodec::determineStartFragment()".
@@ -318,46 +320,10 @@
   };
 
 
-
-  bool DicomFrameIndex::IsVideo(DcmFileFormat& dicom)
+  unsigned int DicomFrameIndex::GetFramesCount(DcmDataset& dicom)
   {
-    // Retrieve the transfer syntax from the DICOM header
-    const char* value = NULL;
-    if (!dicom.getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() ||
-        value == NULL)
-    {
-      return false;
-    }
-
-    const std::string transferSyntax(value);
-
-    // Video standards supported in DICOM 2016a
-    // http://dicom.nema.org/medical/dicom/2016a/output/html/part05.html
-    if (transferSyntax == "1.2.840.10008.1.2.4.100" ||  // MPEG2 MP@ML option of ISO/IEC MPEG2
-        transferSyntax == "1.2.840.10008.1.2.4.101" ||  // MPEG2 MP@HL option of ISO/IEC MPEG2
-        transferSyntax == "1.2.840.10008.1.2.4.102" ||  // MPEG-4 AVC/H.264 High Profile / Level 4.1 of ITU-T H.264
-        transferSyntax == "1.2.840.10008.1.2.4.103" ||  // MPEG-4 AVC/H.264 BD-compat High Profile / Level 4.1 of ITU-T H.264
-        transferSyntax == "1.2.840.10008.1.2.4.104" ||  // MPEG-4 AVC/H.264 High Profile / Level 4.2 of ITU-T H.264
-        transferSyntax == "1.2.840.10008.1.2.4.105" ||  // MPEG-4 AVC/H.264 High Profile / Level 4.2 of ITU-T H.264
-        transferSyntax == "1.2.840.10008.1.2.4.106")    // MPEG-4 AVC/H.264 Stereo High Profile / Level 4.2 of the ITU-T H.264
-    {
-      return true;
-    }
-
-    return false;
-  }
-
-
-  unsigned int DicomFrameIndex::GetFramesCount(DcmFileFormat& dicom)
-  {
-    // Assume 1 frame for video transfer syntaxes
-    if (IsVideo(dicom))
-    {
-      return 1;
-    }        
-
     const char* tmp = NULL;
-    if (!dicom.getDataset()->findAndGetString(DCM_NumberOfFrames, tmp).good() ||
+    if (!dicom.findAndGetString(DCM_NumberOfFrames, tmp).good() ||
         tmp == NULL)
     {
       return 1;
@@ -378,12 +344,12 @@
     }
     else
     {
-      return count;
+      return static_cast<unsigned int>(count);
     }
   }
 
 
-  DicomFrameIndex::DicomFrameIndex(DcmFileFormat& dicom)
+  DicomFrameIndex::DicomFrameIndex(DcmDataset& dicom)
   {
     countFrames_ = GetFramesCount(dicom);
     if (countFrames_ == 0)
@@ -392,10 +358,8 @@
       return;
     }
 
-    DcmDataset& dataset = *dicom.getDataset();
-
     // Test whether this image is composed of a sequence of fragments
-    DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dataset);
+    DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dicom);
     if (pixelSequence != NULL)
     {
       index_.reset(new FragmentIndex(pixelSequence, countFrames_));
@@ -404,18 +368,18 @@
 
     // Extract information about the image structure
     DicomMap tags;
-    FromDcmtkBridge::ExtractDicomSummary(tags, dataset);
+    FromDcmtkBridge::ExtractDicomSummary(tags, dicom);
 
     DicomImageInformation information(tags);
 
     // Access to the raw pixel data
-    if (DicomImageDecoder::IsPsmctRle1(dataset))
+    if (DicomImageDecoder::IsPsmctRle1(dicom))
     {
-      index_.reset(new PsmctRle1Index(dataset, countFrames_, information.GetFrameSize()));
+      index_.reset(new PsmctRle1Index(dicom, countFrames_, information.GetFrameSize()));
     }
     else
     {
-      index_.reset(new UncompressedIndex(dataset, countFrames_, information.GetFrameSize()));
+      index_.reset(new UncompressedIndex(dicom, countFrames_, information.GetFrameSize()));
     }
   }
 
--- a/Core/DicomParsing/Internals/DicomFrameIndex.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomParsing/Internals/DicomFrameIndex.h	Wed Apr 01 10:15:33 2020 +0200
@@ -67,7 +67,7 @@
     unsigned int             countFrames_;
 
   public:
-    DicomFrameIndex(DcmFileFormat& dicom);
+    DicomFrameIndex(DcmDataset& dicom);
 
     unsigned int GetFramesCount() const
     {
@@ -77,8 +77,6 @@
     void GetRawFrame(std::string& frame,
                      unsigned int index) const;
 
-    static bool IsVideo(DcmFileFormat& dicom);
-
-    static unsigned int GetFramesCount(DcmFileFormat& dicom);
+    static unsigned int GetFramesCount(DcmDataset& dicom);
   };
 }
--- a/Core/DicomParsing/Internals/DicomImageDecoder.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomParsing/Internals/DicomImageDecoder.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -34,6 +34,8 @@
 #include "../../PrecompiledHeaders.h"
 #include "DicomImageDecoder.h"
 
+#include "../ParsedDicomFile.h"
+
 
 /*=========================================================================
 
@@ -84,7 +86,6 @@
 #include "../../DicomFormat/DicomIntegerPixelAccessor.h"
 #include "../ToDcmtkBridge.h"
 #include "../FromDcmtkBridge.h"
-#include "../ParsedDicomFile.h"
 
 #if ORTHANC_ENABLE_PNG == 1
 #  include "../../Images/PngWriter.h"
@@ -98,7 +99,6 @@
 #include <boost/lexical_cast.hpp>
 
 #include <dcmtk/dcmdata/dcdeftag.h>
-#include <dcmtk/dcmdata/dcfilefo.h>
 #include <dcmtk/dcmdata/dcrleccd.h>
 #include <dcmtk/dcmdata/dcrlecp.h>
 #include <dcmtk/dcmdata/dcrlerp.h>
@@ -662,7 +662,20 @@
   ImageAccessor* DicomImageDecoder::Decode(ParsedDicomFile& dicom,
                                            unsigned int frame)
   {
-    DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
+    if (dicom.GetDcmtkObject().getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      return Decode(*dicom.GetDcmtkObject().getDataset(), frame);
+    }
+  }
+
+
+  ImageAccessor* DicomImageDecoder::Decode(DcmDataset& dataset,
+                                           unsigned int frame)
+  {
     E_TransferSyntax syntax = dataset.getOriginalXfer();
 
     /**
--- a/Core/DicomParsing/Internals/DicomImageDecoder.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomParsing/Internals/DicomImageDecoder.h	Wed Apr 01 10:15:33 2020 +0200
@@ -34,7 +34,7 @@
 #pragma once
 
 #include "../../Compatibility.h"
-#include "../ParsedDicomFile.h"
+#include "../../Images/ImageAccessor.h"
 
 #include <memory>
 
@@ -62,6 +62,8 @@
 
 namespace Orthanc
 {
+  class ParsedDicomFile;
+  
   class DicomImageDecoder : public boost::noncopyable
   {
   private:
@@ -102,6 +104,9 @@
     static ImageAccessor *Decode(ParsedDicomFile& dicom,
                                  unsigned int frame);
 
+    static ImageAccessor *Decode(DcmDataset& dataset,
+                                 unsigned int frame);
+
     static void ExtractPamImage(std::string& result,
                                 std::unique_ptr<ImageAccessor>& image,
                                 ImageExtractionMode mode,
--- a/Core/DicomParsing/ParsedDicomFile.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/DicomParsing/ParsedDicomFile.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -1557,7 +1557,9 @@
   {
     if (pimpl_->frameIndex_.get() == NULL)
     {
-      pimpl_->frameIndex_.reset(new DicomFrameIndex(*pimpl_->file_));
+      assert(pimpl_->file_ != NULL &&
+             pimpl_->file_->getDataset() != NULL);
+      pimpl_->frameIndex_.reset(new DicomFrameIndex(*pimpl_->file_->getDataset()));
     }
 
     pimpl_->frameIndex_->GetRawFrame(target, frameId);
@@ -1589,7 +1591,9 @@
 
   unsigned int ParsedDicomFile::GetFramesCount() const
   {
-    return DicomFrameIndex::GetFramesCount(*pimpl_->file_);
+    assert(pimpl_->file_ != NULL &&
+           pimpl_->file_->getDataset() != NULL);
+    return DicomFrameIndex::GetFramesCount(*pimpl_->file_->getDataset());
   }
 
 
--- a/Core/Enumerations.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/Enumerations.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -366,6 +366,9 @@
       case ErrorCode_AlreadyExistingTag:
         return "Cannot override the value of a tag that already exists";
 
+      case ErrorCode_NoStorageCommitmentHandler:
+        return "No request handler factory for DICOM N-ACTION SCP (storage commitment)";
+
       case ErrorCode_UnsupportedMediaType:
         return "Unsupported media type";
 
@@ -860,6 +863,14 @@
         return "Store";
         break;
 
+      case DicomRequestType_NAction:
+        return "N-ACTION";
+        break;
+
+      case DicomRequestType_NEventReport:
+        return "N-EVENT-REPORT";
+        break;
+
       default: 
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -1157,6 +1168,41 @@
   }
 
 
+  const char* EnumerationToString(StorageCommitmentFailureReason reason)
+  {
+    switch (reason)
+    {
+      case StorageCommitmentFailureReason_Success:
+        return "Success";
+
+      case StorageCommitmentFailureReason_ProcessingFailure:
+        return "A general failure in processing the operation was encountered";
+
+      case StorageCommitmentFailureReason_NoSuchObjectInstance:
+        return "One or more of the elements in the Referenced SOP "
+          "Instance Sequence was not available";
+        
+      case StorageCommitmentFailureReason_ResourceLimitation:
+        return "The SCP does not currently have enough resources to "
+          "store the requested SOP Instance(s)";
+
+      case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
+        return "Storage Commitment has been requested for a SOP Instance "
+          "with a SOP Class that is not supported by the SCP";
+
+      case StorageCommitmentFailureReason_ClassInstanceConflict:
+        return "The SOP Class of an element in the Referenced SOP Instance Sequence "
+          "did not correspond to the SOP class registered for this SOP Instance at the SCP";
+
+      case StorageCommitmentFailureReason_DuplicateTransactionUID:
+        return "The Transaction UID of the Storage Commitment Request is already in use";
+
+      default:
+        return "Unknown failure reason";
+    }
+  }
+
+
   Encoding StringToEncoding(const char* encoding)
   {
     std::string s(encoding);
--- a/Core/Enumerations.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/Enumerations.h	Wed Apr 01 10:15:33 2020 +0200
@@ -239,6 +239,7 @@
     ErrorCode_CannotOrderSlices = 2040    /*!< Unable to order the slices of the series */,
     ErrorCode_NoWorklistHandler = 2041    /*!< No request handler factory for DICOM C-Find Modality SCP */,
     ErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
+    ErrorCode_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
     ErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
     ErrorCode_START_PLUGINS = 1000000
   };
@@ -670,7 +671,9 @@
     DicomRequestType_Find,
     DicomRequestType_Get,
     DicomRequestType_Move,
-    DicomRequestType_Store
+    DicomRequestType_Store,
+    DicomRequestType_NAction,
+    DicomRequestType_NEventReport
   };
 
   enum TransferSyntax
@@ -712,6 +715,36 @@
     JobStopReason_Retry
   };
 
+  
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
+  enum StorageCommitmentFailureReason
+  {
+    StorageCommitmentFailureReason_Success = 0,
+
+    // A general failure in processing the operation was encountered
+    StorageCommitmentFailureReason_ProcessingFailure = 0x0110,
+
+    // One or more of the elements in the Referenced SOP Instance
+    // Sequence was not available
+    StorageCommitmentFailureReason_NoSuchObjectInstance = 0x0112,
+
+    // The SCP does not currently have enough resources to store the
+    // requested SOP Instance(s)
+    StorageCommitmentFailureReason_ResourceLimitation = 0x0213,
+
+    // Storage Commitment has been requested for a SOP Instance with a
+    // SOP Class that is not supported by the SCP
+    StorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 0x0122,
+
+    // The SOP Class of an element in the Referenced SOP Instance
+    // Sequence did not correspond to the SOP class registered for
+    // this SOP Instance at the SCP
+    StorageCommitmentFailureReason_ClassInstanceConflict = 0x0119,
+
+    // The Transaction UID of the Storage Commitment Request is already in use
+    StorageCommitmentFailureReason_DuplicateTransactionUID = 0x0131
+  };
+
 
   /**
    * WARNING: Do not change the explicit values in the enumerations
@@ -798,6 +831,8 @@
 
   const char* EnumerationToString(Endianness endianness);
 
+  const char* EnumerationToString(StorageCommitmentFailureReason reason);
+
   Encoding StringToEncoding(const char* encoding);
 
   ResourceType StringToResourceType(const char* type);
--- a/Core/HttpClient.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/HttpClient.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -234,7 +234,8 @@
         if (sourceRemainingSize > 0)
         {
           // transmit the end of current source buffer
-          memcpy(curlBuffer + curlBufferFilledSize, sourceBuffer_.data() + sourceBufferTransmittedSize_, sourceRemainingSize);
+          memcpy(curlBuffer + curlBufferFilledSize,
+                 sourceBuffer_.data() + sourceBufferTransmittedSize_, sourceRemainingSize);
 
           curlBufferFilledSize += sourceRemainingSize;
         }
@@ -248,11 +249,13 @@
         sourceRemainingSize = sourceBuffer_.size();
       }
 
-      if (sourceRemainingSize > 0 && (curlBufferSize - curlBufferFilledSize) > 0)
+      if (sourceRemainingSize > 0 &&
+          curlBufferSize > curlBufferFilledSize)
       {
         size_t s = std::min(sourceRemainingSize, curlBufferSize - curlBufferFilledSize);
 
-        memcpy(curlBuffer + curlBufferFilledSize, sourceBuffer_.data() + sourceBufferTransmittedSize_, s);
+        memcpy(curlBuffer + curlBufferFilledSize,
+               sourceBuffer_.data() + sourceBufferTransmittedSize_, s);
 
         sourceBufferTransmittedSize_ += s;
         curlBufferFilledSize += s;
--- a/Core/JobsEngine/IJob.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/JobsEngine/IJob.h	Wed Apr 01 10:15:33 2020 +0200
@@ -50,7 +50,7 @@
     // Method called once the job enters the jobs engine
     virtual void Start() = 0;
     
-    virtual JobStepResult Step() = 0;
+    virtual JobStepResult Step(const std::string& jobId) = 0;
 
     // Method called once the job is resubmitted after a failure
     virtual void Reset() = 0;
--- a/Core/JobsEngine/JobsEngine.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/JobsEngine/JobsEngine.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -71,7 +71,7 @@
 
     try
     {
-      result = running.GetJob().Step();
+      result = running.GetJob().Step(running.GetId());
     }
     catch (OrthancException& e)
     {
--- a/Core/JobsEngine/JobsEngine.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/JobsEngine/JobsEngine.h	Wed Apr 01 10:15:33 2020 +0200
@@ -41,7 +41,7 @@
 
 namespace Orthanc
 {
-  class JobsEngine
+  class JobsEngine : public boost::noncopyable
   {
   private:
     enum State
--- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -319,7 +319,7 @@
   }
 
 
-  JobStepResult SequenceOfOperationsJob::Step()
+  JobStepResult SequenceOfOperationsJob::Step(const std::string& jobId)
   {
     boost::mutex::scoped_lock lock(mutex_);
 
--- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h	Wed Apr 01 10:15:33 2020 +0200
@@ -126,30 +126,30 @@
                    size_t output);
     };
 
-    virtual void Start()
+    virtual void Start() ORTHANC_OVERRIDE
     {
     }
 
-    virtual JobStepResult Step();
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
 
-    virtual void Reset();
+    virtual void Reset() ORTHANC_OVERRIDE;
 
-    virtual void Stop(JobStopReason reason);
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress();
+    virtual float GetProgress() ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target)
+    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
     {
       target = "SequenceOfOperations";
     }
 
-    virtual void GetPublicContent(Json::Value& value);
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& value);
+    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
-                           const std::string& key)
+                           const std::string& key) ORTHANC_OVERRIDE
     {
       return false;
     }
--- a/Core/JobsEngine/SetOfCommandsJob.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/JobsEngine/SetOfCommandsJob.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -145,7 +145,7 @@
   }
       
 
-  JobStepResult SetOfCommandsJob::Step()
+  JobStepResult SetOfCommandsJob::Step(const std::string& jobId)
   {
     if (!started_)
     {
@@ -169,7 +169,7 @@
     try
     {
       // Not at the trailing step: Handle the current command
-      if (!commands_[position_]->Execute())
+      if (!commands_[position_]->Execute(jobId))
       {
         // Error
         if (!permissive_)
--- a/Core/JobsEngine/SetOfCommandsJob.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/JobsEngine/SetOfCommandsJob.h	Wed Apr 01 10:15:33 2020 +0200
@@ -49,7 +49,7 @@
       {
       }
 
-      virtual bool Execute() = 0;
+      virtual bool Execute(const std::string& jobId) = 0;
 
       virtual void Serialize(Json::Value& target) const = 0;
     };
@@ -110,14 +110,14 @@
 
     void SetPermissive(bool permissive);
 
-    virtual void Reset();
+    virtual void Reset() ORTHANC_OVERRIDE;
     
-    virtual void Start()
+    virtual void Start() ORTHANC_OVERRIDE
     {
       started_ = true;
     }
     
-    virtual float GetProgress();
+    virtual float GetProgress() ORTHANC_OVERRIDE;
 
     bool IsStarted() const
     {
@@ -126,15 +126,15 @@
 
     const ICommand& GetCommand(size_t index) const;
       
-    virtual JobStepResult Step();
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
     
-    virtual void GetPublicContent(Json::Value& value);
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
     
-    virtual bool Serialize(Json::Value& target);
+    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
-                           const std::string& key)
+                           const std::string& key) ORTHANC_OVERRIDE
     {
       return false;
     }
--- a/Core/JobsEngine/SetOfInstancesJob.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/JobsEngine/SetOfInstancesJob.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -60,7 +60,7 @@
       return instance_;
     }
       
-    virtual bool Execute()
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
     {
       if (!that_.HandleInstance(instance_))
       {
@@ -73,7 +73,7 @@
       }
     }
 
-    virtual void Serialize(Json::Value& target) const
+    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
     {
       target = instance_;
     }
@@ -91,12 +91,12 @@
     {
     }       
       
-    virtual bool Execute()
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
     {
       return that_.HandleTrailingStep();
     }
 
-    virtual void Serialize(Json::Value& target) const
+    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
     {
       target = Json::nullValue;
     }
--- a/Core/SerializationToolbox.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/SerializationToolbox.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -320,6 +320,28 @@
     }
 
 
+    void WriteListOfStrings(Json::Value& target,
+                            const std::list<std::string>& values,
+                            const std::string& field)
+    {
+      if (target.type() != Json::objectValue ||
+          target.isMember(field.c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      Json::Value& value = target[field];
+
+      value = Json::arrayValue;
+
+      for (std::list<std::string>::const_iterator it = values.begin();
+           it != values.end(); ++it)
+      {
+        value.append(*it);
+      }
+    }
+
+
     void WriteSetOfStrings(Json::Value& target,
                            const std::set<std::string>& values,
                            const std::string& field)
--- a/Core/SerializationToolbox.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/SerializationToolbox.h	Wed Apr 01 10:15:33 2020 +0200
@@ -83,6 +83,10 @@
                              const std::vector<std::string>& values,
                              const std::string& field);
 
+    void WriteListOfStrings(Json::Value& target,
+                            const std::list<std::string>& values,
+                            const std::string& field);
+
     void WriteSetOfStrings(Json::Value& target,
                            const std::set<std::string>& values,
                            const std::string& field);
--- a/Core/SharedLibrary.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/SharedLibrary.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -62,14 +62,19 @@
     }
 
 #elif defined(__linux__) || (defined(__APPLE__) && defined(__MACH__)) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__)
-
+   
     /**
      * "RTLD_LOCAL" is the default, and is only present to be
      * explicit. "RTLD_DEEPBIND" was added in Orthanc 1.6.0, in order
      * to avoid crashes while loading plugins from the LSB binaries of
      * the Orthanc core.
+     *
+     * BUT this had no effect, and this results in a crash if loading
+     * the Python 2.7 plugin => We disabled it again in Orthanc 1.6.1.
      **/
-#if defined(RTLD_DEEPBIND)  // This is a GNU extension
+    
+#if 0 // && defined(RTLD_DEEPBIND)  // This is a GNU extension
+    // Disabled in Orthanc 1.6.1
     handle_ = ::dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND);
 #else
     handle_ = ::dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL);
--- a/Core/Toolbox.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/Toolbox.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -2078,6 +2078,108 @@
       throw OrthancException(ErrorCode_BadFileFormat, "Invalid UTF-8 string");
     }
   }
+
+
+  std::string Toolbox::LargeHexadecimalToDecimal(const std::string& hex)
+  {
+    /**
+     * NB: Focus of the code below is *not* efficiency, but
+     * readability!
+     **/
+    
+    for (size_t i = 0; i < hex.size(); i++)
+    {
+      const char c = hex[i];
+      if (!((c >= 'A' && c <= 'F') ||
+            (c >= 'a' && c <= 'f') ||
+            (c >= '0' && c <= '9')))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                        "Not an hexadecimal number");
+      }
+    }
+    
+    std::vector<uint8_t> decimal;
+    decimal.push_back(0);
+
+    for (size_t i = 0; i < hex.size(); i++)
+    {
+      uint8_t hexDigit = static_cast<uint8_t>(Hex2Dec(hex[i]));
+      assert(hexDigit <= 15);
+
+      for (size_t j = 0; j < decimal.size(); j++)
+      {
+        uint8_t val = static_cast<uint8_t>(decimal[j]) * 16 + hexDigit;  // Maximum: 9 * 16 + 15
+        assert(val <= 159 /* == 9 * 16 + 15 */);
+      
+        decimal[j] = val % 10;
+        hexDigit = val / 10;
+        assert(hexDigit <= 15 /* == 159 / 10 */);
+      }
+
+      while (hexDigit > 0)
+      {
+        decimal.push_back(hexDigit % 10);
+        hexDigit /= 10;
+      }
+    }
+
+    size_t start = 0;
+    while (start < decimal.size() &&
+           decimal[start] == '0')
+    {
+      start++;
+    }
+
+    std::string s;
+    s.reserve(decimal.size() - start);
+
+    for (size_t i = decimal.size(); i > start; i--)
+    {
+      s.push_back(decimal[i - 1] + '0');
+    }
+
+    return s;
+  }
+
+
+  std::string Toolbox::GenerateDicomPrivateUniqueIdentifier()
+  {
+    /**
+     * REFERENCE: "Creating a Privately Defined Unique Identifier
+     * (Informative)" / "UUID Derived UID"
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html
+     * https://stackoverflow.com/a/46316162/881731
+     **/
+
+    std::string uuid = GenerateUuid();
+    assert(IsUuid(uuid) && uuid.size() == 36);
+
+    /**
+     * After removing the four dashes ("-") out of the 36-character
+     * UUID, we get a large hexadecimal number with 32 characters,
+     * each of those characters lying in the range [0,16[. The large
+     * number is thus in the [0,16^32[ = [0,256^16[ range. This number
+     * has a maximum of 39 decimal digits, as can be seen in Python:
+     * 
+     * # python -c 'import math; print(math.log(16**32))/math.log(10))'
+     * 38.531839445
+     *
+     * We now to convert the large hexadecimal number to a decimal
+     * number with up to 39 digits, remove the leading zeros, then
+     * prefix it with "2.25."
+     **/
+
+    // Remove the dashes
+    std::string hex = (uuid.substr(0, 8) +
+                       uuid.substr(9, 4) +
+                       uuid.substr(14, 4) +
+                       uuid.substr(19, 4) +
+                       uuid.substr(24, 12));
+    assert(hex.size() == 32);
+
+    return "2.25." + LargeHexadecimalToDecimal(hex);
+  }
 }
 
 
--- a/Core/Toolbox.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Core/Toolbox.h	Wed Apr 01 10:15:33 2020 +0200
@@ -257,6 +257,11 @@
                                 size_t& utf8Length,
                                 const std::string& utf8,
                                 size_t position);
+
+    std::string LargeHexadecimalToDecimal(const std::string& hex);
+
+    // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html
+    std::string GenerateDicomPrivateUniqueIdentifier();
   }
 }
 
--- a/NEWS	Wed Apr 01 10:14:49 2020 +0200
+++ b/NEWS	Wed Apr 01 10:15:33 2020 +0200
@@ -1,24 +1,47 @@
 Pending changes in the mainline
 ===============================
 
+
+Maintenance
+-----------
+
+* Source code repository moved from BitBucket to self-hosted server
+* Fix lookup form in Orthanc Explorer (wildcards not allowed in StudyDate)
+* Fix signature of "OrthancPluginRegisterStorageCommitmentScpCallback()" in plugins SDK
+
+
+Version 1.6.0 (2020-03-18)
+==========================
+
+General
+-------
+
+* Support of DICOM storage commitment
+
 REST API
 --------
 
 * API version has been upgraded to 5
-* added "/peers/{id}/system" route to test the connectivity with a remote peer (and eventually
-  retrieve its version number)
-* "/changes": Allow the "limit" argument to be greater than 100
-* "/instances/{id}/preview": Now takes the windowing into account
-* "/tools/log-level": Possibility to access and change the log level without restarting Orthanc
-* added "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered" routes
-  to render frames, taking windowing and resizing into account
-* "/instances": Support "Content-Encoding: gzip" to upload gzip-compressed DICOM files
-* ".../modify" and "/tools/create-dicom": New option "PrivateCreator" for private tags
+* Added:
+  - "/peers/{id}/system": Test the connectivity with a remote peer
+    (and also retrieve its version number)
+  - "/tools/log-level": Access and/or change the log level without restarting Orthanc
+  - "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered":
+    Render frames, taking windowing and resizing into account
+  - "/modalities/{...}/storage-commitment": Trigger storage commitment SCU
+  - "/storage-commitment/{...}": Access storage commitment reports
+  - "/storage-commitment/{...}/remove": Remove instances from storage commitment reports
+* Improved:
+  - "/changes": Allow the "limit" argument to be greater than 100
+  - "/instances": Support "Content-Encoding: gzip" to upload gzip-compressed DICOM files
+  - ".../modify" and "/tools/create-dicom": New option "PrivateCreator" for private tags
+  - "/modalities/{...}/store": New Boolean argument "StorageCommitment"
 
 Plugins
 -------
 
 * New sample plugin: "ConnectivityChecks"
+* New primitives to handle storage commitment SCP by plugins
 
 Lua
 ---
@@ -32,6 +55,7 @@
 Maintenance
 -----------
 
+* New configuration options: "DefaultPrivateCreator" and "StorageCommitmentReportsSize"
 * Support of MPEG4 transfer syntaxes in C-Store SCP
 * C-FIND SCU at Instance level now sets the 0008,0052 tag to IMAGE per default (was INSTANCE).
   Therefore, the "ClearCanvas" and "Dcm4Chee" modality manufacturer have now been deprecated.
--- a/OrthancExplorer/explorer.js	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancExplorer/explorer.js	Wed Apr 01 10:15:33 2020 +0200
@@ -431,7 +431,7 @@
   // NB: "GenerateDicomDate()" is defined in "query-retrieve.js"
   var target = $('#lookup-study-date');
   $('option', target).remove();
-  target.append($('<option>').attr('value', '*').text('Any date'));
+  target.append($('<option>').attr('value', '').text('Any date'));
   target.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today'));
   target.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday'));
   target.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days'));
--- a/OrthancExplorer/query-retrieve.js	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancExplorer/query-retrieve.js	Wed Apr 01 10:15:33 2020 +0200
@@ -38,7 +38,7 @@
 
   targetDate = $('#qr-date');
   $('option', targetDate).remove();
-  targetDate.append($('<option>').attr('value', '*').text('Any date'));
+  targetDate.append($('<option>').attr('value', '').text('Any date'));
   targetDate.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today'));
   targetDate.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday'));
   targetDate.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days'));
@@ -90,7 +90,7 @@
       'PatientName' : '',
       'PatientSex' : '',
       'StudyDate' : $('#qr-date').val(),
-      'StudyDescription' : '*'
+      'StudyDescription' : ''
     }
   };
 
@@ -204,10 +204,10 @@
     query = {
       'Level' : 'Series',
       'Query' : {
-        'Modality' : '*',
-        'ProtocolName' : '*',
-        'SeriesDescription' : '*',
-        'SeriesInstanceUID' : '*',
+        'Modality' : '',
+        'ProtocolName' : '',
+        'SeriesDescription' : '',
+        'SeriesInstanceUID' : '',
         'StudyInstanceUID' : pageData.uuid
       }
     };
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -116,7 +116,8 @@
           connection_.reset(new DicomUserConnection(localAet_, remote_));
         }
 
-        connection_->Store(dicom, originatorAet_, originatorId_);
+        std::string sopClassUid, sopInstanceUid;  // Unused
+        connection_->Store(sopClassUid, sopInstanceUid, dicom, originatorAet_, originatorId_);
 
         return Status_Success;
       }
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -46,6 +46,7 @@
 #include "../ServerJobs/DicomMoveScuJob.h"
 #include "../ServerJobs/OrthancPeerStoreJob.h"
 #include "../ServerToolbox.h"
+#include "../StorageCommitmentReports.h"
 
 
 namespace Orthanc
@@ -963,6 +964,12 @@
       job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID);
     }
 
+    // New in Orthanc 1.6.0
+    if (Toolbox::GetJsonBooleanField(request, "StorageCommitment", false))
+    {
+      job->EnableStorageCommitment(true);
+    }
+
     OrthancRestApi::GetApi(call).SubmitCommandsJob
       (call, job.release(), true /* synchronous by default */, request);
   }
@@ -1273,7 +1280,7 @@
     if (call.ParseJsonRequest(json))
     {
       const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-      RemoteModalityParameters remote =
+      const RemoteModalityParameters remote =
         MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
       std::unique_ptr<ParsedDicomFile> query
@@ -1299,6 +1306,251 @@
   }
 
 
+  // Storage commitment SCU ---------------------------------------------------
+
+  static void StorageCommitmentScu(RestApiPostCall& call)
+  {
+    static const char* const ORTHANC_RESOURCES = "Resources";
+    static const char* const DICOM_INSTANCES = "DicomInstances";
+    static const char* const SOP_CLASS_UID = "SOPClassUID";
+    static const char* const SOP_INSTANCE_UID = "SOPInstanceUID";
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value json;
+    if (!call.ParseJsonRequest(json) ||
+        json.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Must provide a JSON object with a list of resources");
+    }
+    else if (!json.isMember(ORTHANC_RESOURCES) &&
+             !json.isMember(DICOM_INSTANCES))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Empty storage commitment request, one of these fields is mandatory: \"" +
+                             std::string(ORTHANC_RESOURCES) + "\" or \"" + std::string(DICOM_INSTANCES) + "\"");
+    }
+    else
+    {
+      std::list<std::string> sopClassUids, sopInstanceUids;
+
+      if (json.isMember(ORTHANC_RESOURCES))
+      {
+        const Json::Value& resources = json[ORTHANC_RESOURCES];
+          
+        if (resources.type() != Json::arrayValue)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "The \"" + std::string(ORTHANC_RESOURCES) +
+                                 "\" field must provide an array of Orthanc resources");
+        }
+        else
+        {
+          for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
+          {
+            if (resources[i].type() != Json::stringValue)
+            {
+              throw OrthancException(ErrorCode_BadFileFormat,
+                                     "The \"" + std::string(ORTHANC_RESOURCES) +
+                                     "\" field must provide an array of strings, found: " + resources[i].toStyledString());
+            }
+
+            std::list<std::string> instances;
+            context.GetIndex().GetChildInstances(instances, resources[i].asString());
+            
+            for (std::list<std::string>::const_iterator
+                   it = instances.begin(); it != instances.end(); ++it)
+            {
+              std::string sopClassUid, sopInstanceUid;
+              DicomMap tags;
+              if (context.LookupOrReconstructMetadata(sopClassUid, *it, MetadataType_Instance_SopClassUid) &&
+                  context.GetIndex().GetAllMainDicomTags(tags, *it) &&
+                  tags.LookupStringValue(sopInstanceUid, DICOM_TAG_SOP_INSTANCE_UID, false))
+              {
+                sopClassUids.push_back(sopClassUid);
+                sopInstanceUids.push_back(sopInstanceUid);
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_InternalError,
+                                       "Cannot retrieve SOP Class/Instance UID of Orthanc instance: " + *it);
+              }
+            }
+          }
+        }
+      }
+
+      if (json.isMember(DICOM_INSTANCES))
+      {
+        const Json::Value& instances = json[DICOM_INSTANCES];
+          
+        if (instances.type() != Json::arrayValue)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "The \"" + std::string(DICOM_INSTANCES) +
+                                 "\" field must provide an array of DICOM instances");
+        }
+        else
+        {
+          for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
+          {
+            if (instances[i].type() == Json::arrayValue)
+            {
+              if (instances[i].size() != 2 ||
+                  instances[i][0].type() != Json::stringValue ||
+                  instances[i][1].type() != Json::stringValue)
+              {
+                throw OrthancException(ErrorCode_BadFileFormat,
+                                       "An instance entry must provide an array with 2 strings: "
+                                       "SOP Class UID and SOP Instance UID");
+              }
+              else
+              {
+                sopClassUids.push_back(instances[i][0].asString());
+                sopInstanceUids.push_back(instances[i][1].asString());
+              }
+            }
+            else if (instances[i].type() == Json::objectValue)
+            {
+              if (!instances[i].isMember(SOP_CLASS_UID) ||
+                  !instances[i].isMember(SOP_INSTANCE_UID) ||
+                  instances[i][SOP_CLASS_UID].type() != Json::stringValue ||
+                  instances[i][SOP_INSTANCE_UID].type() != Json::stringValue)
+              {
+                throw OrthancException(ErrorCode_BadFileFormat,
+                                       "An instance entry must provide an object with 2 string fiels: "
+                                       "\"" + std::string(SOP_CLASS_UID) + "\" and \"" +
+                                       std::string(SOP_INSTANCE_UID));
+              }
+              else
+              {
+                sopClassUids.push_back(instances[i][SOP_CLASS_UID].asString());
+                sopInstanceUids.push_back(instances[i][SOP_INSTANCE_UID].asString());
+              }
+            }
+            else
+            {
+              throw OrthancException(ErrorCode_BadFileFormat,
+                                     "JSON array or object is expected to specify one "
+                                     "instance to be queried, found: " + instances[i].toStyledString());
+            }
+          }
+        }
+      }
+
+      if (sopClassUids.size() != sopInstanceUids.size())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      const std::string transactionUid = Toolbox::GenerateDicomPrivateUniqueIdentifier();
+
+      if (sopClassUids.empty())
+      {
+        LOG(WARNING) << "Issuing an outgoing storage commitment request that is empty: " << transactionUid;
+      }
+
+      {
+        const RemoteModalityParameters remote =
+          MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+        const std::string& remoteAet = remote.GetApplicationEntityTitle();
+        const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
+        
+        // Create a "pending" storage commitment report BEFORE the
+        // actual SCU call in order to avoid race conditions
+        context.GetStorageCommitmentReports().Store(
+          transactionUid, new StorageCommitmentReports::Report(remoteAet));
+
+        DicomUserConnection scu(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);
+      }
+
+      Json::Value result = Json::objectValue;
+      result["ID"] = transactionUid;
+      result["Path"] = "/storage-commitment/" + transactionUid;
+      call.GetOutput().AnswerJson(result);
+    }
+  }
+
+
+  static void GetStorageCommitmentReport(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    const std::string& transactionUid = call.GetUriComponent("id", "");
+
+    {
+      StorageCommitmentReports::Accessor accessor(
+        context.GetStorageCommitmentReports(), transactionUid);
+
+      if (accessor.IsValid())
+      {
+        Json::Value json;
+        accessor.GetReport().Format(json);
+        call.GetOutput().AnswerJson(json);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InexistentItem,
+                               "No storage commitment transaction with UID: " + transactionUid);
+      }
+    }
+  }
+  
+
+  static void RemoveAfterStorageCommitment(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    const std::string& transactionUid = call.GetUriComponent("id", "");
+
+    {
+      StorageCommitmentReports::Accessor accessor(
+        context.GetStorageCommitmentReports(), transactionUid);
+
+      if (!accessor.IsValid())
+      {
+        throw OrthancException(ErrorCode_InexistentItem,
+                               "No storage commitment transaction with UID: " + transactionUid);
+      }
+      else if (accessor.GetReport().GetStatus() != StorageCommitmentReports::Report::Status_Success)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "Cannot remove DICOM instances after failure "
+                               "in storage commitment transaction: " + transactionUid);
+      }
+      else
+      {
+        std::vector<std::string> sopInstanceUids;
+        accessor.GetReport().GetSuccessSopInstanceUids(sopInstanceUids);
+
+        for (size_t i = 0; i < sopInstanceUids.size(); i++)
+        {
+          std::vector<std::string> orthancId;
+          context.GetIndex().LookupIdentifierExact(
+            orthancId, ResourceType_Instance, DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUids[i]);
+
+          for (size_t j = 0; j < orthancId.size(); j++)
+          {
+            LOG(INFO) << "Storage commitment - Removing SOP instance UID / Orthanc ID: "
+                      << sopInstanceUids[i] << " / " << orthancId[j];
+
+            Json::Value tmp;
+            context.GetIndex().DeleteResource(tmp, orthancId[j], ResourceType_Instance);
+          }
+        }
+          
+        call.GetOutput().AnswerBuffer("{}", MimeType_Json);
+      }
+    }
+  }
+  
+
   void OrthancRestApi::RegisterModalities()
   {
     Register("/modalities", ListModalities);
@@ -1342,5 +1594,10 @@
     Register("/peers/{id}/system", PeerSystem);
 
     Register("/modalities/{id}/find-worklist", DicomFindWorklist);
+
+    // Storage commitment
+    Register("/modalities/{id}/storage-commitment", StorageCommitmentScu);
+    Register("/storage-commitment/{id}", GetStorageCommitmentReport);
+    Register("/storage-commitment/{id}/remove", RemoveAfterStorageCommitment);
   }
 }
--- a/OrthancServer/Search/DicomTagConstraint.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/Search/DicomTagConstraint.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -105,7 +105,8 @@
         (value.find('*') != std::string::npos ||
          value.find('?') != std::string::npos))
     {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Wildcards are not allowed on tag " + tag_.Format());
     }
 
     if (constraintType_ == ConstraintType_Equal ||
--- a/OrthancServer/ServerContext.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/ServerContext.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -49,6 +49,7 @@
 #include "Search/DatabaseLookup.h"
 #include "ServerJobs/OrthancJobUnserializer.h"
 #include "ServerToolbox.h"
+#include "StorageCommitmentReports.h"
 
 #include <EmbeddedResources.h>
 #include <dcmtk/dcmdata/dcfilefo.h>
@@ -259,6 +260,9 @@
       findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always"));
       limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0);
       limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0);
+
+      // New configuration option in Orthanc 1.6.0
+      storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100)));
     }
 
     jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
@@ -1062,4 +1066,24 @@
     }
 #endif
   }
+
+
+  IStorageCommitmentFactory::ILookupHandler*
+  ServerContext::CreateStorageCommitment(const std::string& jobId,
+                                         const std::string& transactionUid,
+                                         const std::vector<std::string>& sopClassUids,
+                                         const std::vector<std::string>& sopInstanceUids,
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet)
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPlugins())
+    {
+      return GetPlugins().CreateStorageCommitment(
+        jobId, transactionUid, sopClassUids, sopInstanceUids, remoteAet, calledAet);
+    }
+#endif
+
+    return NULL;
+  }
 }
--- a/OrthancServer/ServerContext.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/ServerContext.h	Wed Apr 01 10:15:33 2020 +0200
@@ -37,6 +37,7 @@
 #include "LuaScripting.h"
 #include "OrthancHttpHandler.h"
 #include "ServerIndex.h"
+#include "ServerJobs/IStorageCommitmentFactory.h"
 
 #include "../Core/Cache/MemoryCache.h"
 
@@ -53,6 +54,7 @@
   class SetOfInstancesJob;
   class SharedArchive;
   class SharedMessageQueue;
+  class StorageCommitmentReports;
   
   
   /**
@@ -60,7 +62,9 @@
    * filesystem (including compression), as well as the index of the
    * DICOM store. It implements the required locking mechanisms.
    **/
-  class ServerContext : private JobsRegistry::IObserver
+  class ServerContext :
+    public IStorageCommitmentFactory,
+    private JobsRegistry::IObserver
   {
   public:
     class ILookupVisitor : public boost::noncopyable
@@ -164,11 +168,11 @@
 
     void SaveJobsEngine();
 
-    virtual void SignalJobSubmitted(const std::string& jobId);
+    virtual void SignalJobSubmitted(const std::string& jobId) ORTHANC_OVERRIDE;
 
-    virtual void SignalJobSuccess(const std::string& jobId);
+    virtual void SignalJobSuccess(const std::string& jobId) ORTHANC_OVERRIDE;
 
-    virtual void SignalJobFailure(const std::string& jobId);
+    virtual void SignalJobFailure(const std::string& jobId) ORTHANC_OVERRIDE;
 
     ServerIndex index_;
     IStorageArea& area_;
@@ -218,6 +222,8 @@
     bool isHttpServerSecure_;
     bool isExecuteLuaEnabled_;
 
+    std::unique_ptr<StorageCommitmentReports>  storageCommitmentReports_;
+
   public:
     class DicomCacheLocker : public boost::noncopyable
     {
@@ -419,5 +425,18 @@
     {
       return isExecuteLuaEnabled_;
     }
+
+    virtual IStorageCommitmentFactory::ILookupHandler*
+    CreateStorageCommitment(const std::string& jobId,
+                            const std::string& transactionUid,
+                            const std::vector<std::string>& sopClassUids,
+                            const std::vector<std::string>& sopInstanceUids,
+                            const std::string& remoteAet,
+                            const std::string& calledAet) ORTHANC_OVERRIDE;
+
+    StorageCommitmentReports& GetStorageCommitmentReports()
+    {
+      return *storageCommitmentReports_;
+    }
   };
 }
--- a/OrthancServer/ServerJobs/ArchiveJob.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/ServerJobs/ArchiveJob.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -935,7 +935,7 @@
   }
     
 
-  JobStepResult ArchiveJob::Step()
+  JobStepResult ArchiveJob::Step(const std::string& jobId)
   {
     assert(writer_.get() != NULL);
 
--- a/OrthancServer/ServerJobs/ArchiveJob.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/ServerJobs/ArchiveJob.h	Wed Apr 01 10:15:33 2020 +0200
@@ -89,29 +89,29 @@
 
     void AddResource(const std::string& publicId);
 
-    virtual void Reset();
+    virtual void Reset() ORTHANC_OVERRIDE;
 
-    virtual void Start();
+    virtual void Start() ORTHANC_OVERRIDE;
 
-    virtual JobStepResult Step();
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
 
-    virtual void Stop(JobStopReason reason)
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
     {
     }
 
-    virtual float GetProgress();
+    virtual float GetProgress() ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target);
+    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE;
     
-    virtual void GetPublicContent(Json::Value& value);
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& value)
+    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
     {
       return false;  // Cannot serialize this kind of job
     }
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
-                           const std::string& key);
+                           const std::string& key) ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -38,6 +38,7 @@
 #include "../../Core/Logging.h"
 #include "../../Core/SerializationToolbox.h"
 #include "../ServerContext.h"
+#include "../StorageCommitmentReports.h"
 
 
 namespace Orthanc
@@ -72,14 +73,47 @@
       LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
       return false;
     }
+    
+    std::string sopClassUid, sopInstanceUid;
 
     if (HasMoveOriginator())
     {
-      connection_->Store(dicom, moveOriginatorAet_, moveOriginatorId_);
+      connection_->Store(sopClassUid, sopInstanceUid, dicom, moveOriginatorAet_, moveOriginatorId_);
     }
     else
     {
-      connection_->Store(dicom);
+      connection_->Store(sopClassUid, sopInstanceUid, dicom);
+    }
+
+    if (storageCommitment_)
+    {
+      sopClassUids_.push_back(sopClassUid);
+      sopInstanceUids_.push_back(sopInstanceUid);
+
+      if (sopClassUids_.size() != sopInstanceUids_.size() ||
+          sopClassUids_.size() > GetInstancesCount())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      
+      if (sopClassUids_.size() == GetInstancesCount())
+      {
+        const std::string& remoteAet = remote_.GetApplicationEntityTitle();
+        
+        LOG(INFO) << "Sending storage commitment request to modality: " << remoteAet;
+
+        // Create a "pending" storage commitment report BEFORE the
+        // actual SCU call in order to avoid race conditions
+        context_.GetStorageCommitmentReports().Store(
+          transactionUid_, new StorageCommitmentReports::Report(remoteAet));
+        
+        assert(IsStarted());
+        OpenConnection();
+
+        std::vector<std::string> a(sopClassUids_.begin(), sopClassUids_.end());
+        std::vector<std::string> b(sopInstanceUids_.begin(), sopInstanceUids_.end());
+        connection_->RequestStorageCommitment(transactionUid_, a, b);
+      }
     }
 
     //boost::this_thread::sleep(boost::posix_time::milliseconds(500));
@@ -97,8 +131,10 @@
   DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) :
     context_(context),
     localAet_("ORTHANC"),
-    moveOriginatorId_(0)  // By default, not a C-MOVE
+    moveOriginatorId_(0),      // By default, not a C-MOVE
+    storageCommitment_(false)  // By default, no storage commitment
   {
+    ResetStorageCommitment();
   }
 
 
@@ -179,6 +215,38 @@
   }
 
 
+  void DicomModalityStoreJob::ResetStorageCommitment()
+  {
+    if (storageCommitment_)
+    {
+      transactionUid_ = Toolbox::GenerateDicomPrivateUniqueIdentifier();
+      sopClassUids_.clear();
+      sopInstanceUids_.clear();
+    }
+  }
+  
+
+  void DicomModalityStoreJob::Reset()
+  {
+    SetOfInstancesJob::Reset();
+
+    /**
+     * "After the N-EVENT-REPORT has been sent, the Transaction UID is
+     * no longer active and shall not be reused for other
+     * transactions." => Need to reset the transaction UID here
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
+     **/
+    ResetStorageCommitment();
+  }
+  
+
+  void DicomModalityStoreJob::EnableStorageCommitment(bool enabled)
+  {
+    storageCommitment_ = enabled;
+    ResetStorageCommitment();
+  }
+  
+
   void DicomModalityStoreJob::GetPublicContent(Json::Value& value)
   {
     SetOfInstancesJob::GetPublicContent(value);
@@ -191,6 +259,11 @@
       value["MoveOriginatorAET"] = GetMoveOriginatorAet();
       value["MoveOriginatorID"] = GetMoveOriginatorId();
     }
+
+    if (storageCommitment_)
+    {
+      value["StorageCommitmentTransactionUID"] = transactionUid_;
+    }
   }
 
 
@@ -198,6 +271,7 @@
   static const char* REMOTE = "Remote";
   static const char* MOVE_ORIGINATOR_AET = "MoveOriginatorAet";
   static const char* MOVE_ORIGINATOR_ID = "MoveOriginatorId";
+  static const char* STORAGE_COMMITMENT = "StorageCommitment";
   
 
   DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context,
@@ -210,6 +284,7 @@
     moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET);
     moveOriginatorId_ = static_cast<uint16_t>
       (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID));
+    EnableStorageCommitment(SerializationToolbox::ReadBoolean(serialized, STORAGE_COMMITMENT));
   }
 
 
@@ -225,6 +300,7 @@
       remote_.Serialize(target[REMOTE], true /* force advanced format */);
       target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_;
       target[MOVE_ORIGINATOR_ID] = moveOriginatorId_;
+      target[STORAGE_COMMITMENT] = storageCommitment_;
       return true;
     }
   }  
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h	Wed Apr 01 10:15:33 2020 +0200
@@ -50,13 +50,21 @@
     std::string                           moveOriginatorAet_;
     uint16_t                              moveOriginatorId_;
     std::unique_ptr<DicomUserConnection>  connection_;
+    bool                                  storageCommitment_;
+
+    // For storage commitment
+    std::string             transactionUid_;
+    std::list<std::string>  sopInstanceUids_;
+    std::list<std::string>  sopClassUids_;
 
     void OpenConnection();
 
+    void ResetStorageCommitment();
+
   protected:
-    virtual bool HandleInstance(const std::string& instance);
+    virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE;
     
-    virtual bool HandleTrailingStep();
+    virtual bool HandleTrailingStep() ORTHANC_OVERRIDE;
 
   public:
     DicomModalityStoreJob(ServerContext& context);
@@ -90,15 +98,19 @@
     void SetMoveOriginator(const std::string& aet,
                            int id);
 
-    virtual void Stop(JobStopReason reason);
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target)
+    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
     {
       target = "DicomModalityStore";
     }
 
-    virtual void GetPublicContent(Json::Value& value);
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+
+    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target);
+    virtual void Reset() ORTHANC_OVERRIDE;
+
+    void EnableStorageCommitment(bool enabled);
   };
 }
--- a/OrthancServer/ServerJobs/DicomMoveScuJob.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/ServerJobs/DicomMoveScuJob.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -57,13 +57,13 @@
     {
     }
 
-    virtual bool Execute()
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
     {
       that_.Retrieve(*findAnswer_);
       return true;
     }
 
-    virtual void Serialize(Json::Value& target) const
+    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
     {
       findAnswer_->Serialize(target);
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/IStorageCommitmentFactory.h	Wed Apr 01 10:15:33 2020 +0200
@@ -0,0 +1,66 @@
+/**
+ * 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 <string>
+#include <vector>
+
+namespace Orthanc
+{
+  class IStorageCommitmentFactory : public boost::noncopyable
+  {
+  public:
+    class ILookupHandler : public boost::noncopyable
+    {
+    public:
+      virtual ~ILookupHandler()
+      {
+      }
+
+      virtual StorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
+                                                    const std::string& sopInstanceUid) = 0;
+    };
+
+    virtual ~IStorageCommitmentFactory()
+    {
+    }
+
+    virtual ILookupHandler* CreateStorageCommitment(const std::string& jobId,
+                                                    const std::string& transactionUid,
+                                                    const std::vector<std::string>& sopClassUids,
+                                                    const std::vector<std::string>& sopInstanceUids,
+                                                    const std::string& remoteAet,
+                                                    const std::string& calledAet) = 0;
+  };
+}
--- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -70,7 +70,9 @@
     {
       std::string dicom;
       instance.ReadDicom(dicom);
-      resource->GetConnection().Store(dicom);
+
+      std::string sopClassUid, sopInstanceUid;  // Unused
+      resource->GetConnection().Store(sopClassUid, sopInstanceUid, dicom);
     }
     catch (OrthancException& e)
     {
--- a/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -49,10 +49,11 @@
 
 #include "DicomModalityStoreJob.h"
 #include "DicomMoveScuJob.h"
+#include "MergeStudyJob.h"
 #include "OrthancPeerStoreJob.h"
 #include "ResourceModificationJob.h"
-#include "MergeStudyJob.h"
 #include "SplitStudyJob.h"
+#include "StorageCommitmentScpJob.h"
 
 
 namespace Orthanc
@@ -96,6 +97,10 @@
     {
       return new DicomMoveScuJob(context_, source);
     }
+    else if (type == "StorageCommitmentScp")
+    {
+      return new StorageCommitmentScpJob(context_, source);
+    }
     else
     {
       return GenericJobUnserializer::UnserializeJob(source);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -0,0 +1,454 @@
+/**
+ * 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 "../PrecompiledHeadersServer.h"
+#include "StorageCommitmentScpJob.h"
+
+#include "../../Core/DicomNetworking/DicomUserConnection.h"
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/SerializationToolbox.h"
+#include "../OrthancConfiguration.h"
+#include "../ServerContext.h"
+
+
+static const char* ANSWER = "Answer";
+static const char* CALLED_AET = "CalledAet";
+static const char* INDEX = "Index";
+static const char* LOOKUP = "Lookup";
+static const char* REMOTE_MODALITY = "RemoteModality";
+static const char* SETUP = "Setup";
+static const char* SOP_CLASS_UIDS = "SopClassUids";
+static const char* SOP_INSTANCE_UIDS = "SopInstanceUids";
+static const char* TRANSACTION_UID = "TransactionUid";
+static const char* TYPE = "Type";
+
+
+
+namespace Orthanc
+{
+  class StorageCommitmentScpJob::StorageCommitmentCommand : public SetOfCommandsJob::ICommand
+  {
+  public:
+    virtual CommandType GetType() const = 0;
+  };
+
+  
+  class StorageCommitmentScpJob::SetupCommand : public StorageCommitmentCommand
+  {
+  private:
+    StorageCommitmentScpJob&  that_;
+
+  public:
+    SetupCommand(StorageCommitmentScpJob& that) :
+      that_(that)
+    {
+    }
+
+    virtual CommandType GetType() const ORTHANC_OVERRIDE
+    {
+      return CommandType_Setup;
+    }
+    
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      that_.Setup(jobId);
+      return true;
+    }
+
+    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
+    {
+      target = Json::objectValue;
+      target[TYPE] = SETUP;
+    }
+  };
+
+
+  class StorageCommitmentScpJob::LookupCommand : public StorageCommitmentCommand
+  {
+  private:
+    StorageCommitmentScpJob&        that_;
+    size_t                          index_;
+    bool                            hasFailureReason_;
+    StorageCommitmentFailureReason  failureReason_;
+
+  public:
+    LookupCommand(StorageCommitmentScpJob&  that,
+                  size_t index) :
+      that_(that),
+      index_(index),
+      hasFailureReason_(false)
+    {
+    }
+
+    virtual CommandType GetType() const ORTHANC_OVERRIDE
+    {
+      return CommandType_Lookup;
+    }
+    
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      failureReason_ = that_.Lookup(index_);
+      hasFailureReason_ = true;
+      return true;
+    }
+
+    size_t GetIndex() const
+    {
+      return index_;
+    }
+
+    StorageCommitmentFailureReason GetFailureReason() const
+    {
+      if (hasFailureReason_)
+      {
+        return failureReason_;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
+    {
+      target = Json::objectValue;
+      target[TYPE] = LOOKUP;
+      target[INDEX] = static_cast<unsigned int>(index_);
+    }
+  };
+
+  
+  class StorageCommitmentScpJob::AnswerCommand : public StorageCommitmentCommand
+  {
+  private:
+    StorageCommitmentScpJob&  that_;
+
+  public:
+    AnswerCommand(StorageCommitmentScpJob& that) :
+      that_(that)
+    {
+      if (that_.ready_)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        that_.ready_ = true;
+      }
+    }
+
+    virtual CommandType GetType() const ORTHANC_OVERRIDE
+    {
+      return CommandType_Answer;
+    }
+    
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      that_.Answer();
+      return true;
+    }
+
+    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
+    {
+      target = Json::objectValue;
+      target[TYPE] = ANSWER;
+    }
+  };
+    
+
+  class StorageCommitmentScpJob::Unserializer : public SetOfCommandsJob::ICommandUnserializer
+  {
+  private:
+    StorageCommitmentScpJob&  that_;
+
+  public:
+    Unserializer(StorageCommitmentScpJob& that) :
+      that_(that)
+    {
+      that_.ready_ = false;
+    }
+
+    virtual ICommand* Unserialize(const Json::Value& source) const
+    {
+      const std::string type = SerializationToolbox::ReadString(source, TYPE);
+
+      if (type == SETUP)
+      {
+        return new SetupCommand(that_);
+      }
+      else if (type == LOOKUP)
+      {
+        return new LookupCommand(that_, SerializationToolbox::ReadUnsignedInteger(source, INDEX));
+      }
+      else if (type == ANSWER)
+      {
+        return new AnswerCommand(that_);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+  };
+
+
+  void StorageCommitmentScpJob::CheckInvariants()
+  {
+    const size_t n = GetCommandsCount();
+
+    if (n <= 1)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    for (size_t i = 0; i < n; i++)
+    {
+      const CommandType type = dynamic_cast<const StorageCommitmentCommand&>(GetCommand(i)).GetType();
+      
+      if ((i == 0 && type != CommandType_Setup) ||
+          (i >= 1 && i < n - 1 && type != CommandType_Lookup) ||
+          (i == n - 1 && type != CommandType_Answer))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (type == CommandType_Lookup)
+      {
+        const LookupCommand& lookup = dynamic_cast<const LookupCommand&>(GetCommand(i));
+        if (lookup.GetIndex() != i - 1)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    }
+  }
+    
+
+  void StorageCommitmentScpJob::Setup(const std::string& jobId)
+  {
+    CheckInvariants();
+
+    const std::string& remoteAet = remoteModality_.GetApplicationEntityTitle();
+    lookupHandler_.reset(context_.CreateStorageCommitment(jobId, transactionUid_, sopClassUids_,
+                                                          sopInstanceUids_, remoteAet, calledAet_));
+  }
+
+
+  StorageCommitmentFailureReason StorageCommitmentScpJob::Lookup(size_t index)
+  {
+#ifndef NDEBUG
+    CheckInvariants();
+#endif
+
+    if (index >= sopClassUids_.size())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else if (lookupHandler_.get() != NULL)
+    {
+      return lookupHandler_->Lookup(sopClassUids_[index], sopInstanceUids_[index]);
+    }
+    else
+    {
+      // This is the default implementation of Orthanc (if no storage
+      // commitment plugin is installed)
+      bool success = false;
+      StorageCommitmentFailureReason reason =
+        StorageCommitmentFailureReason_NoSuchObjectInstance /* 0x0112 == 274 */;
+      
+      try
+      {
+        std::vector<std::string> orthancId;
+        context_.GetIndex().LookupIdentifierExact(orthancId, ResourceType_Instance, DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUids_[index]);
+
+        if (orthancId.size() == 1)
+        {
+          std::string a, b;
+
+          // Make sure that the DICOM file can be re-read by DCMTK
+          // from the file storage, and that the actual SOP
+          // class/instance UIDs do match
+          ServerContext::DicomCacheLocker locker(context_, orthancId[0]);
+          if (locker.GetDicom().GetTagValue(a, DICOM_TAG_SOP_CLASS_UID) &&
+              locker.GetDicom().GetTagValue(b, DICOM_TAG_SOP_INSTANCE_UID) &&
+              b == sopInstanceUids_[index])
+          {
+            if (a == sopClassUids_[index])
+            {
+              success = true;
+              reason = StorageCommitmentFailureReason_Success;
+            }
+            else
+            {
+              // Mismatch in the SOP class UID
+              reason = StorageCommitmentFailureReason_ClassInstanceConflict /* 0x0119 */;
+            }
+          }
+        }
+      }
+      catch (OrthancException&)
+      {
+      }
+
+      LOG(INFO) << "  Storage commitment SCP job: " << (success ? "Success" : "Failure")
+                << " while looking for " << sopClassUids_[index] << " / " << sopInstanceUids_[index];
+
+      return reason;
+    }
+  }
+  
+  
+  void StorageCommitmentScpJob::Answer()
+  {   
+    CheckInvariants();
+    LOG(INFO) << "  Storage commitment SCP job: Sending answer";
+
+    std::vector<StorageCommitmentFailureReason> failureReasons;
+    failureReasons.reserve(sopClassUids_.size());
+
+    for (size_t i = 1; i < GetCommandsCount() - 1; i++)
+    {
+      const LookupCommand& lookup = dynamic_cast<const LookupCommand&>(GetCommand(i));
+      failureReasons.push_back(lookup.GetFailureReason());
+    }
+
+    if (failureReasons.size() != sopClassUids_.size())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+      
+    DicomUserConnection scu(calledAet_, remoteModality_);
+    scu.ReportStorageCommitment(transactionUid_, sopClassUids_, sopInstanceUids_, failureReasons);
+  }
+    
+
+  StorageCommitmentScpJob::StorageCommitmentScpJob(ServerContext& context,
+                                                   const std::string& transactionUid,
+                                                   const std::string& remoteAet,
+                                                   const std::string& calledAet) :
+    context_(context),
+    ready_(false),
+    transactionUid_(transactionUid),
+    calledAet_(calledAet)
+  {
+    {
+      OrthancConfiguration::ReaderLock lock;
+      if (!lock.GetConfiguration().LookupDicomModalityUsingAETitle(remoteModality_, remoteAet))
+      {
+        throw OrthancException(ErrorCode_InexistentItem,
+                               "Unknown remote modality for storage commitment SCP: " + remoteAet);
+      }
+    }
+
+    AddCommand(new SetupCommand(*this));
+  }
+    
+
+  void StorageCommitmentScpJob::Reserve(size_t size)
+  {
+    if (ready_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      sopClassUids_.reserve(size);
+      sopInstanceUids_.reserve(size);
+    }
+  }
+    
+
+  void StorageCommitmentScpJob::AddInstance(const std::string& sopClassUid,
+                                            const std::string& sopInstanceUid)
+  {
+    if (ready_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      assert(sopClassUids_.size() == sopInstanceUids_.size());
+      AddCommand(new LookupCommand(*this, sopClassUids_.size()));
+      sopClassUids_.push_back(sopClassUid);
+      sopInstanceUids_.push_back(sopInstanceUid);
+    }
+  }
+    
+
+  void StorageCommitmentScpJob::MarkAsReady()
+  {
+    AddCommand(new AnswerCommand(*this));
+  }
+
+
+  void StorageCommitmentScpJob::GetPublicContent(Json::Value& value)
+  {
+    SetOfCommandsJob::GetPublicContent(value);
+      
+    value["CalledAet"] = calledAet_;
+    value["RemoteAet"] = remoteModality_.GetApplicationEntityTitle();
+    value["TransactionUid"] = transactionUid_;
+  }
+
+
+  StorageCommitmentScpJob::StorageCommitmentScpJob(ServerContext& context,
+                                                   const Json::Value& serialized) :
+    SetOfCommandsJob(new Unserializer(*this), serialized),
+    context_(context)
+  {
+    transactionUid_ = SerializationToolbox::ReadString(serialized, TRANSACTION_UID);
+    remoteModality_ = RemoteModalityParameters(serialized[REMOTE_MODALITY]);
+    calledAet_ = SerializationToolbox::ReadString(serialized, CALLED_AET);
+    SerializationToolbox::ReadArrayOfStrings(sopClassUids_, serialized, SOP_CLASS_UIDS);
+    SerializationToolbox::ReadArrayOfStrings(sopInstanceUids_, serialized, SOP_INSTANCE_UIDS);
+  }
+  
+
+  bool StorageCommitmentScpJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfCommandsJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      target[TRANSACTION_UID] = transactionUid_;
+      remoteModality_.Serialize(target[REMOTE_MODALITY], true /* force advanced format */);
+      target[CALLED_AET] = calledAet_;
+      SerializationToolbox::WriteArrayOfStrings(target, sopClassUids_, SOP_CLASS_UIDS);
+      SerializationToolbox::WriteArrayOfStrings(target, sopInstanceUids_, SOP_INSTANCE_UIDS);
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/StorageCommitmentScpJob.h	Wed Apr 01 10:15:33 2020 +0200
@@ -0,0 +1,111 @@
+/**
+ * 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 "../../Core/Compatibility.h"
+#include "../../Core/DicomNetworking/RemoteModalityParameters.h"
+#include "../../Core/JobsEngine/SetOfCommandsJob.h"
+#include "IStorageCommitmentFactory.h"
+
+#include <memory>
+#include <vector>
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class StorageCommitmentScpJob : public SetOfCommandsJob
+  {
+  private:
+    enum CommandType
+    {
+      CommandType_Setup,
+      CommandType_Lookup,
+      CommandType_Answer
+    };
+    
+    class StorageCommitmentCommand;
+    class SetupCommand;
+    class LookupCommand;
+    class AnswerCommand;
+    class Unserializer;
+
+    ServerContext&            context_;
+    bool                      ready_;
+    std::string               transactionUid_;
+    RemoteModalityParameters  remoteModality_;
+    std::string               calledAet_;
+    std::vector<std::string>  sopClassUids_;
+    std::vector<std::string>  sopInstanceUids_;
+
+    std::unique_ptr<IStorageCommitmentFactory::ILookupHandler>  lookupHandler_;
+
+    void CheckInvariants();
+    
+    void Setup(const std::string& jobId);
+    
+    StorageCommitmentFailureReason Lookup(size_t index);
+    
+    void Answer();
+    
+  public:
+    StorageCommitmentScpJob(ServerContext& context,
+                            const std::string& transactionUid,
+                            const std::string& remoteAet,
+                            const std::string& calledAet);
+
+    StorageCommitmentScpJob(ServerContext& context,
+                            const Json::Value& serialized);
+
+    void Reserve(size_t size);
+    
+    void AddInstance(const std::string& sopClassUid,
+                     const std::string& sopInstanceUid);
+
+    void MarkAsReady();
+
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    {
+      target = "StorageCommitmentScp";
+    }
+
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+
+    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/StorageCommitmentReports.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -0,0 +1,272 @@
+/**
+ * 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 "PrecompiledHeadersServer.h"
+#include "StorageCommitmentReports.h"
+
+#include "../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  void StorageCommitmentReports::Report::MarkAsComplete()
+  {
+    if (isComplete_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      isComplete_ = true;
+    }
+  }
+
+  void StorageCommitmentReports::Report::AddSuccess(const std::string& sopClassUid,
+                                                    const std::string& sopInstanceUid)
+  {
+    if (isComplete_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      Success success;
+      success.sopClassUid_ = sopClassUid;
+      success.sopInstanceUid_ = sopInstanceUid;
+      success_.push_back(success);
+    }
+  }
+
+  void StorageCommitmentReports::Report::AddFailure(const std::string& sopClassUid,
+                                                    const std::string& sopInstanceUid,
+                                                    StorageCommitmentFailureReason reason)
+  {
+    if (isComplete_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      Failure failure;
+      failure.sopClassUid_ = sopClassUid;
+      failure.sopInstanceUid_ = sopInstanceUid;
+      failure.reason_ = reason;
+      failures_.push_back(failure);
+    }
+  }
+
+  
+  StorageCommitmentReports::Report::Status StorageCommitmentReports::Report::GetStatus() const
+  {
+    if (!isComplete_)
+    {
+      return Status_Pending;
+    }
+    else if (failures_.empty())
+    {
+      return Status_Success;
+    }
+    else
+    {
+      return Status_Failure;
+    }
+  }
+
+
+  void StorageCommitmentReports::Report::Format(Json::Value& json) const
+  {
+    static const char* const FIELD_STATUS = "Status";
+    static const char* const FIELD_SOP_CLASS_UID = "SOPClassUID";
+    static const char* const FIELD_SOP_INSTANCE_UID = "SOPInstanceUID";
+    static const char* const FIELD_FAILURE_REASON = "FailureReason";
+    static const char* const FIELD_DESCRIPTION = "Description";
+    static const char* const FIELD_REMOTE_AET = "RemoteAET";
+    static const char* const FIELD_SUCCESS = "Success";
+    static const char* const FIELD_FAILURES = "Failures";
+
+    
+    json = Json::objectValue;
+    json[FIELD_REMOTE_AET] = remoteAet_;
+
+    bool pending;
+    
+    switch (GetStatus())
+    {
+      case Status_Pending:
+        json[FIELD_STATUS] = "Pending";
+        pending = true;
+        break;
+
+      case Status_Success:
+        json[FIELD_STATUS] = "Success";
+        pending = false;
+        break;
+
+      case Status_Failure:
+        json[FIELD_STATUS] = "Failure";
+        pending = false;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (!pending)
+    {
+      {
+        Json::Value success = Json::arrayValue;
+        for (std::list<Success>::const_iterator
+               it = success_.begin(); it != success_.end(); ++it)
+        {
+          Json::Value item = Json::objectValue;
+          item[FIELD_SOP_CLASS_UID] = it->sopClassUid_;
+          item[FIELD_SOP_INSTANCE_UID] = it->sopInstanceUid_;
+          success.append(item);
+        }
+
+        json[FIELD_SUCCESS] = success;
+      }
+
+      {
+        Json::Value failures = Json::arrayValue;
+        for (std::list<Failure>::const_iterator
+               it = failures_.begin(); it != failures_.end(); ++it)
+        {
+          Json::Value item = Json::objectValue;
+          item[FIELD_SOP_CLASS_UID] = it->sopClassUid_;
+          item[FIELD_SOP_INSTANCE_UID] = it->sopInstanceUid_;
+          item[FIELD_FAILURE_REASON] = it->reason_;
+          item[FIELD_DESCRIPTION] = EnumerationToString(it->reason_);
+          failures.append(item);
+        }
+
+        json[FIELD_FAILURES] = failures;
+      }
+    }
+  }
+
+
+  void StorageCommitmentReports::Report::GetSuccessSopInstanceUids(
+    std::vector<std::string>& target) const
+  {
+    target.clear();
+    target.reserve(success_.size());
+
+    for (std::list<Success>::const_iterator
+           it = success_.begin(); it != success_.end(); ++it)
+    {
+      target.push_back(it->sopInstanceUid_);
+    }
+  }
+
+
+  StorageCommitmentReports::~StorageCommitmentReports()
+  {
+    while (!content_.IsEmpty())
+    {
+      Report* report = NULL;
+      content_.RemoveOldest(report);
+
+      assert(report != NULL);
+      delete report;
+    }
+  }
+
+  
+  void StorageCommitmentReports::Store(const std::string& transactionUid,
+                                       Report* report)
+  {
+    std::unique_ptr<Report> protection(report);
+    
+    boost::mutex::scoped_lock lock(mutex_);
+
+    {
+      Report* previous = NULL;
+      if (content_.Contains(transactionUid, previous))
+      {
+        assert(previous != NULL);
+        delete previous;
+
+        content_.Invalidate(transactionUid);
+      }
+    }
+
+    assert(maxSize_ == 0 ||
+           content_.GetSize() <= maxSize_);
+
+    if (maxSize_ != 0 &&
+        content_.GetSize() == maxSize_)
+    {
+      assert(!content_.IsEmpty());
+      
+      Report* oldest = NULL;
+      content_.RemoveOldest(oldest);
+
+      assert(oldest != NULL);
+      delete oldest;
+    }
+
+    assert(maxSize_ == 0 ||
+           content_.GetSize() < maxSize_);
+
+    content_.Add(transactionUid, protection.release());
+  }
+
+
+  StorageCommitmentReports::Accessor::Accessor(StorageCommitmentReports& that,
+                                               const std::string& transactionUid) :
+    lock_(that.mutex_),
+    transactionUid_(transactionUid)
+  {
+    if (that.content_.Contains(transactionUid, report_))
+    {
+      that.content_.MakeMostRecent(transactionUid);
+    }
+    else
+    {
+      report_ = NULL;
+    }
+  }
+
+  const StorageCommitmentReports::Report&
+  StorageCommitmentReports::Accessor::GetReport() const
+  {
+    if (report_ == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *report_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/StorageCommitmentReports.h	Wed Apr 01 10:15:33 2020 +0200
@@ -0,0 +1,147 @@
+/**
+ * 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 "../Core/Cache/LeastRecentlyUsedIndex.h"
+
+namespace Orthanc
+{
+  class StorageCommitmentReports
+  {
+  public:
+    class Report : public boost::noncopyable
+    {
+    public:
+      enum Status
+      {
+        Status_Success,
+        Status_Failure,
+        Status_Pending
+      };
+      
+    private:
+      struct Success
+      {
+        std::string  sopClassUid_;
+        std::string  sopInstanceUid_;
+      };
+      
+      struct Failure
+      {
+        std::string  sopClassUid_;
+        std::string  sopInstanceUid_;
+        StorageCommitmentFailureReason  reason_;
+      };
+      
+      bool                isComplete_;
+      std::list<Success>  success_;
+      std::list<Failure>  failures_;
+      std::string         remoteAet_;
+
+    public:
+      Report(const std::string& remoteAet) :
+        isComplete_(false),
+        remoteAet_(remoteAet)
+      {
+      }
+
+      const std::string& GetRemoteAet() const
+      {
+        return remoteAet_;
+      }
+
+      void MarkAsComplete();
+
+      void AddSuccess(const std::string& sopClassUid,
+                      const std::string& sopInstanceUid);
+
+      void AddFailure(const std::string& sopClassUid,
+                      const std::string& sopInstanceUid,
+                      StorageCommitmentFailureReason reason);
+
+      Status GetStatus() const;
+
+      void Format(Json::Value& json) const;
+
+      void GetSuccessSopInstanceUids(std::vector<std::string>& target) const;
+    };
+
+  private:
+    typedef LeastRecentlyUsedIndex<std::string, Report*>  Content;
+    
+    boost::mutex   mutex_;
+    Content        content_;
+    size_t         maxSize_;
+
+  public:
+    StorageCommitmentReports(size_t maxSize) :
+      maxSize_(maxSize)
+    {
+    }
+
+    ~StorageCommitmentReports();
+
+    size_t GetMaxSize() const
+    {
+      return maxSize_;
+    }
+
+    void Store(const std::string& transactionUid,
+               Report* report); // Takes ownership
+
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      boost::mutex::scoped_lock  lock_;
+      std::string                transactionUid_;
+      Report                    *report_;
+
+    public:
+      Accessor(StorageCommitmentReports& that,
+               const std::string& transactionUid);
+
+      const std::string& GetTransactionUid() const
+      {
+        return transactionUid_;
+      }
+
+      bool IsValid() const
+      {
+        return report_ != NULL;
+      }
+
+      const Report& GetReport() const;
+    };
+  };
+}
--- a/OrthancServer/main.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/OrthancServer/main.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -36,6 +36,7 @@
 
 #include <boost/algorithm/string/predicate.hpp>
 
+#include "../Core/Compatibility.h"
 #include "../Core/DicomFormat/DicomArray.h"
 #include "../Core/DicomNetworking/DicomServer.h"
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
@@ -50,7 +51,9 @@
 #include "OrthancInitialization.h"
 #include "OrthancMoveRequestHandler.h"
 #include "ServerContext.h"
+#include "ServerJobs/StorageCommitmentScpJob.h"
 #include "ServerToolbox.h"
+#include "StorageCommitmentReports.h"
 
 using namespace Orthanc;
 
@@ -58,11 +61,11 @@
 class OrthancStoreRequestHandler : public IStoreRequestHandler
 {
 private:
-  ServerContext& server_;
+  ServerContext& context_;
 
 public:
   OrthancStoreRequestHandler(ServerContext& context) :
-    server_(context)
+    context_(context)
   {
   }
 
@@ -84,8 +87,82 @@
       toStore.SetJson(dicomJson);
 
       std::string id;
-      server_.Store(id, toStore);
+      context_.Store(id, toStore);
+    }
+  }
+};
+
+
+
+class OrthancStorageCommitmentRequestHandler : public IStorageCommitmentRequestHandler
+{
+private:
+  ServerContext& context_;
+  
+public:
+  OrthancStorageCommitmentRequestHandler(ServerContext& context) :
+    context_(context)
+  {
+  }
+
+  virtual void HandleRequest(const std::string& transactionUid,
+                             const std::vector<std::string>& referencedSopClassUids,
+                             const std::vector<std::string>& referencedSopInstanceUids,
+                             const std::string& remoteIp,
+                             const std::string& remoteAet,
+                             const std::string& calledAet)
+  {
+    if (referencedSopClassUids.size() != referencedSopInstanceUids.size())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    std::unique_ptr<StorageCommitmentScpJob> job(
+      new StorageCommitmentScpJob(context_, transactionUid, remoteAet, calledAet));
+
+    for (size_t i = 0; i < referencedSopClassUids.size(); i++)
+    {
+      job->AddInstance(referencedSopClassUids[i], referencedSopInstanceUids[i]);
     }
+
+    job->MarkAsReady();
+
+    context_.GetJobsEngine().GetRegistry().Submit(job.release(), 0 /* default priority */);
+  }
+
+  virtual void HandleReport(const std::string& transactionUid,
+                            const std::vector<std::string>& successSopClassUids,
+                            const std::vector<std::string>& successSopInstanceUids,
+                            const std::vector<std::string>& failedSopClassUids,
+                            const std::vector<std::string>& failedSopInstanceUids,
+                            const std::vector<StorageCommitmentFailureReason>& failureReasons,
+                            const std::string& remoteIp,
+                            const std::string& remoteAet,
+                            const std::string& calledAet)
+  {
+    if (successSopClassUids.size() != successSopInstanceUids.size() ||
+        failedSopClassUids.size() != failedSopInstanceUids.size() ||
+        failedSopClassUids.size() != failureReasons.size())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    std::unique_ptr<StorageCommitmentReports::Report> report(
+      new StorageCommitmentReports::Report(remoteAet));
+
+    for (size_t i = 0; i < successSopClassUids.size(); i++)
+    {
+      report->AddSuccess(successSopClassUids[i], successSopInstanceUids[i]);
+    }
+
+    for (size_t i = 0; i < failedSopClassUids.size(); i++)
+    {
+      report->AddFailure(failedSopClassUids[i], failedSopInstanceUids[i], failureReasons[i]);
+    }
+
+    report->MarkAsComplete();
+
+    context_.GetStorageCommitmentReports().Store(transactionUid, report.release());
   }
 };
 
@@ -113,7 +190,8 @@
 class MyDicomServerFactory : 
   public IStoreRequestHandlerFactory,
   public IFindRequestHandlerFactory, 
-  public IMoveRequestHandlerFactory
+  public IMoveRequestHandlerFactory, 
+  public IStorageCommitmentRequestHandlerFactory
 {
 private:
   ServerContext& context_;
@@ -166,6 +244,11 @@
     return new OrthancMoveRequestHandler(context_);
   }
 
+  virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler()
+  {
+    return new OrthancStorageCommitmentRequestHandler(context_);
+  }
+
   void Done()
   {
   }
@@ -676,6 +759,7 @@
     PrintErrorCode(ErrorCode_CannotOrderSlices, "Unable to order the slices of the series");
     PrintErrorCode(ErrorCode_NoWorklistHandler, "No request handler factory for DICOM C-Find Modality SCP");
     PrintErrorCode(ErrorCode_AlreadyExistingTag, "Cannot override the value of a tag that already exists");
+    PrintErrorCode(ErrorCode_NoStorageCommitmentHandler, "No request handler factory for DICOM N-ACTION SCP (storage commitment)");
     PrintErrorCode(ErrorCode_UnsupportedMediaType, "Unsupported media type");
   }
 
@@ -970,6 +1054,7 @@
     dicomServer.SetStoreRequestHandlerFactory(serverFactory);
     dicomServer.SetMoveRequestHandlerFactory(serverFactory);
     dicomServer.SetFindRequestHandlerFactory(serverFactory);
+    dicomServer.SetStorageCommitmentRequestHandlerFactory(serverFactory);
 
     {
       OrthancConfiguration::ReaderLock lock;
--- a/Plugins/Engine/OrthancPlugins.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -682,6 +682,110 @@
     };
 
 
+
+    class StorageCommitmentScp : public IStorageCommitmentFactory
+    {
+    private:
+      class Handler : public IStorageCommitmentFactory::ILookupHandler
+      {
+      private:
+        _OrthancPluginRegisterStorageCommitmentScpCallback  parameters_;
+        void*    handler_;
+
+      public:
+        Handler(_OrthancPluginRegisterStorageCommitmentScpCallback  parameters,
+                void* handler) :
+          parameters_(parameters),
+          handler_(handler)
+        {
+          if (handler == NULL)
+          {
+            throw OrthancException(ErrorCode_NullPointer);
+          }
+        }
+
+        virtual ~Handler()
+        {
+          assert(handler_ != NULL);
+          parameters_.destructor(handler_);
+          handler_ = NULL;
+        }
+
+        virtual StorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
+                                                      const std::string& sopInstanceUid)
+        {
+          assert(handler_ != NULL);
+          OrthancPluginStorageCommitmentFailureReason reason =
+            OrthancPluginStorageCommitmentFailureReason_Success;
+          OrthancPluginErrorCode error = parameters_.lookup(
+            &reason, handler_, sopClassUid.c_str(), sopInstanceUid.c_str());
+          if (error == OrthancPluginErrorCode_Success)
+          {
+            return Plugins::Convert(reason);
+          }
+          else
+          {
+            throw OrthancException(static_cast<ErrorCode>(error));
+          }
+        }
+      };
+      
+      _OrthancPluginRegisterStorageCommitmentScpCallback  parameters_;
+
+    public:
+      StorageCommitmentScp(_OrthancPluginRegisterStorageCommitmentScpCallback parameters) :
+        parameters_(parameters)
+      {
+      }
+
+      virtual ILookupHandler* CreateStorageCommitment(
+        const std::string& jobId,
+        const std::string& transactionUid,
+        const std::vector<std::string>& sopClassUids,
+        const std::vector<std::string>& sopInstanceUids,
+        const std::string& remoteAet,
+        const std::string& calledAet) ORTHANC_OVERRIDE
+      {
+        const size_t n = sopClassUids.size();
+        
+        if (sopInstanceUids.size() != n)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        
+        std::vector<const char*> a, b;
+        a.resize(n);
+        b.resize(n);
+
+        for (size_t i = 0; i < n; i++)
+        {
+          a[i] = sopClassUids[i].c_str();
+          b[i] = sopInstanceUids[i].c_str();
+        }
+
+        void* handler = NULL;
+        OrthancPluginErrorCode error = parameters_.factory(
+          &handler, jobId.c_str(), transactionUid.c_str(),
+          a.empty() ? NULL : &a[0], b.empty() ? NULL : &b[0], static_cast<uint32_t>(n),
+          remoteAet.c_str(), calledAet.c_str());
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          throw OrthancException(static_cast<ErrorCode>(error));          
+        }
+        else if (handler == NULL)
+        {
+          // This plugin won't handle this storage commitment request
+          return NULL;
+        }
+        else
+        {
+          return new Handler(parameters_, handler);
+        }
+      }
+    };
+
+
     class ServerContextLock
     {
     private:
@@ -724,6 +828,7 @@
     typedef std::list<OrthancPluginDecodeImageCallback>  DecodeImageCallbacks;
     typedef std::list<OrthancPluginJobsUnserializer>  JobsUnserializers;
     typedef std::list<OrthancPluginRefreshMetricsCallback>  RefreshMetricsCallbacks;
+    typedef std::list<StorageCommitmentScp*>  StorageCommitmentScpCallbacks;
     typedef std::map<Property, std::string>  Properties;
 
     PluginsManager manager_;
@@ -740,6 +845,7 @@
     IncomingHttpRequestFilters  incomingHttpRequestFilters_;
     IncomingHttpRequestFilters2 incomingHttpRequestFilters2_;
     RefreshMetricsCallbacks refreshMetricsCallbacks_;
+    StorageCommitmentScpCallbacks storageCommitmentScpCallbacks_;
     std::unique_ptr<StorageAreaFactory>  storageArea_;
 
     boost::recursive_mutex restCallbackMutex_;
@@ -750,6 +856,7 @@
     boost::mutex decodeImageCallbackMutex_;
     boost::mutex jobsUnserializersMutex_;
     boost::mutex refreshMetricsMutex_;
+    boost::mutex storageCommitmentScpMutex_;
     boost::recursive_mutex invokeServiceMutex_;
 
     Properties properties_;
@@ -1261,6 +1368,7 @@
         sizeof(int32_t) != sizeof(OrthancPluginConstraintType) ||
         sizeof(int32_t) != sizeof(OrthancPluginMetricsType) ||
         sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) ||
+        sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeBinary) != static_cast<int>(DicomToJsonFlags_IncludeBinary) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludePrivateTags) != static_cast<int>(DicomToJsonFlags_IncludePrivateTags) ||
         static_cast<int>(OrthancPluginDicomToJsonFlags_IncludeUnknownTags) != static_cast<int>(DicomToJsonFlags_IncludeUnknownTags) ||
@@ -1304,6 +1412,13 @@
     {
       delete *it;
     }
+
+    for (PImpl::StorageCommitmentScpCallbacks::iterator
+           it = pimpl_->storageCommitmentScpCallbacks_.begin(); 
+         it != pimpl_->storageCommitmentScpCallbacks_.end(); ++it)
+    {
+      delete *it;
+    } 
   }
 
 
@@ -1864,6 +1979,18 @@
   }
 
 
+  void OrthancPlugins::RegisterStorageCommitmentScpCallback(const void* parameters)
+  {
+    const _OrthancPluginRegisterStorageCommitmentScpCallback& p = 
+      *reinterpret_cast<const _OrthancPluginRegisterStorageCommitmentScpCallback*>(parameters);
+
+    boost::mutex::scoped_lock lock(pimpl_->storageCommitmentScpMutex_);
+    LOG(INFO) << "Plugin has registered a storage commitment callback";
+
+    pimpl_->storageCommitmentScpCallbacks_.push_back(new PImpl::StorageCommitmentScp(p));
+  }
+
+
   void OrthancPlugins::AnswerBuffer(const void* parameters)
   {
     const _OrthancPluginAnswerBuffer& p = 
@@ -3911,6 +4038,10 @@
         RegisterRefreshMetricsCallback(parameters);
         return true;
 
+      case _OrthancPluginService_RegisterStorageCommitmentScpCallback:
+        RegisterStorageCommitmentScpCallback(parameters);
+        return true;
+
       case _OrthancPluginService_RegisterStorageArea:
       {
         LOG(INFO) << "Plugin has registered a custom storage area";
@@ -4568,4 +4699,32 @@
       }
     }
   }
+
+
+  IStorageCommitmentFactory::ILookupHandler* OrthancPlugins::CreateStorageCommitment(
+    const std::string& jobId,
+    const std::string& transactionUid,
+    const std::vector<std::string>& sopClassUids,
+    const std::vector<std::string>& sopInstanceUids,
+    const std::string& remoteAet,
+    const std::string& calledAet)
+  {
+    boost::mutex::scoped_lock lock(pimpl_->storageCommitmentScpMutex_);
+
+    for (PImpl::StorageCommitmentScpCallbacks::iterator
+           it = pimpl_->storageCommitmentScpCallbacks_.begin(); 
+         it != pimpl_->storageCommitmentScpCallbacks_.end(); ++it)
+    {
+      assert(*it != NULL);
+      IStorageCommitmentFactory::ILookupHandler* handler = (*it)->CreateStorageCommitment
+        (jobId, transactionUid, sopClassUids, sopInstanceUids, remoteAet, calledAet);
+
+      if (handler != NULL)
+      {
+        return handler;
+      }
+    } 
+    
+    return NULL;
+  }
 }
--- a/Plugins/Engine/OrthancPlugins.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Engine/OrthancPlugins.h	Wed Apr 01 10:15:33 2020 +0200
@@ -62,6 +62,7 @@
 #include "../../Core/JobsEngine/IJob.h"
 #include "../../OrthancServer/IDicomImageDecoder.h"
 #include "../../OrthancServer/IServerListener.h"
+#include "../../OrthancServer/ServerJobs/IStorageCommitmentFactory.h"
 #include "OrthancPluginDatabase.h"
 #include "PluginsManager.h"
 
@@ -80,7 +81,8 @@
     public IDicomImageDecoder,
     public IIncomingHttpRequestFilter,
     public IFindRequestHandlerFactory,
-    public IMoveRequestHandlerFactory
+    public IMoveRequestHandlerFactory,
+    public IStorageCommitmentFactory
   {
   private:
     class PImpl;
@@ -124,6 +126,8 @@
 
     void RegisterRefreshMetricsCallback(const void* parameters);
 
+    void RegisterStorageCommitmentScpCallback(const void* parameters);
+
     void AnswerBuffer(const void* parameters);
 
     void Redirect(const void* parameters);
@@ -235,20 +239,20 @@
                         const Arguments& headers,
                         const GetArguments& getArguments,
                         const void* bodyData,
-                        size_t bodySize);
+                        size_t bodySize) ORTHANC_OVERRIDE;
 
     virtual bool InvokeService(SharedLibrary& plugin,
                                _OrthancPluginService service,
-                               const void* parameters);
+                               const void* parameters) ORTHANC_OVERRIDE;
 
-    virtual void SignalChange(const ServerIndexChange& change);
-
+    virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE;
+    
     virtual void SignalStoredInstance(const std::string& instanceId,
                                       DicomInstanceToStore& instance,
-                                      const Json::Value& simplifiedTags);
+                                      const Json::Value& simplifiedTags) ORTHANC_OVERRIDE;
 
     virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
-                                        const Json::Value& simplified)
+                                        const Json::Value& simplified) ORTHANC_OVERRIDE
     {
       return true; // TODO Enable filtering of instances from plugins
     }
@@ -298,7 +302,7 @@
 
     bool HasWorklistHandler();
 
-    virtual IWorklistRequestHandler* ConstructWorklistRequestHandler();
+    virtual IWorklistRequestHandler* ConstructWorklistRequestHandler() ORTHANC_OVERRIDE;
 
     bool HasCustomImageDecoder();
 
@@ -311,22 +315,22 @@
 
     virtual ImageAccessor* Decode(const void* dicom,
                                   size_t size,
-                                  unsigned int frame);
+                                  unsigned int frame) ORTHANC_OVERRIDE;
 
     virtual bool IsAllowed(HttpMethod method,
                            const char* uri,
                            const char* ip,
                            const char* username,
                            const IHttpHandler::Arguments& httpHeaders,
-                           const IHttpHandler::GetArguments& getArguments);
+                           const IHttpHandler::GetArguments& getArguments) ORTHANC_OVERRIDE;
 
     bool HasFindHandler();
 
-    virtual IFindRequestHandler* ConstructFindRequestHandler();
+    virtual IFindRequestHandler* ConstructFindRequestHandler() ORTHANC_OVERRIDE;
 
     bool HasMoveHandler();
 
-    virtual IMoveRequestHandler* ConstructMoveRequestHandler();
+    virtual IMoveRequestHandler* ConstructMoveRequestHandler() ORTHANC_OVERRIDE;
 
     IJob* UnserializeJob(const std::string& type,
                          const Json::Value& value);
@@ -340,7 +344,16 @@
                                             const char* username,
                                             HttpMethod method,
                                             const UriComponents& uri,
-                                            const Arguments& headers);
+                                            const Arguments& headers) ORTHANC_OVERRIDE;
+
+    // New in Orthanc 1.6.0
+    IStorageCommitmentFactory::ILookupHandler* CreateStorageCommitment(
+      const std::string& jobId,
+      const std::string& transactionUid,
+      const std::vector<std::string>& sopClassUids,
+      const std::vector<std::string>& sopInstanceUids,
+      const std::string& remoteAet,
+      const std::string& calledAet) ORTHANC_OVERRIDE;
   };
 }
 
--- a/Plugins/Engine/PluginsEnumerations.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Engine/PluginsEnumerations.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -549,5 +549,36 @@
           throw OrthancException(ErrorCode_ParameterOutOfRange);
       }
     }
+
+
+    StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason)
+    {
+      switch (reason)
+      {
+        case OrthancPluginStorageCommitmentFailureReason_Success:
+          return StorageCommitmentFailureReason_Success;
+          
+        case OrthancPluginStorageCommitmentFailureReason_ProcessingFailure:
+          return StorageCommitmentFailureReason_ProcessingFailure;
+
+        case OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance:
+          return StorageCommitmentFailureReason_NoSuchObjectInstance;
+
+        case OrthancPluginStorageCommitmentFailureReason_ResourceLimitation:
+          return StorageCommitmentFailureReason_ResourceLimitation;
+
+        case OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
+          return StorageCommitmentFailureReason_ReferencedSOPClassNotSupported;
+
+        case OrthancPluginStorageCommitmentFailureReason_ClassInstanceConflict:
+          return StorageCommitmentFailureReason_ClassInstanceConflict;
+
+        case OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID:
+          return StorageCommitmentFailureReason_DuplicateTransactionUID;
+             
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
   }
 }
--- a/Plugins/Engine/PluginsEnumerations.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Engine/PluginsEnumerations.h	Wed Apr 01 10:15:33 2020 +0200
@@ -79,6 +79,8 @@
     OrthancPluginJobStepStatus Convert(JobStepCode step);
 
     JobStepCode Convert(OrthancPluginJobStepStatus step);
+
+    StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason);
   }
 }
 
--- a/Plugins/Engine/PluginsJob.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Engine/PluginsJob.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -78,7 +78,7 @@
     parameters_.finalize(parameters_.job);
   }
 
-  JobStepResult PluginsJob::Step()
+  JobStepResult PluginsJob::Step(const std::string& jobId)
   {
     OrthancPluginJobStepStatus status = parameters_.step(parameters_.job);
 
--- a/Plugins/Engine/PluginsJob.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Engine/PluginsJob.h	Wed Apr 01 10:15:33 2020 +0200
@@ -51,30 +51,30 @@
 
     virtual ~PluginsJob();
 
-    virtual void Start()
+    virtual void Start() ORTHANC_OVERRIDE
     {
     }
     
-    virtual JobStepResult Step();
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
 
-    virtual void Reset();
+    virtual void Reset() ORTHANC_OVERRIDE;
 
-    virtual void Stop(JobStopReason reason);
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress();
+    virtual float GetProgress() ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target)
+    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
     {
       target = type_;
     }
     
-    virtual void GetPublicContent(Json::Value& value);
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& value);
+    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
-                           const std::string& key)
+                           const std::string& key) ORTHANC_OVERRIDE
     {
       // TODO
       return false;
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Apr 01 10:15:33 2020 +0200
@@ -26,6 +26,7 @@
  *    - Possibly register a callback to unserialize jobs using OrthancPluginRegisterJobsUnserializer().
  *    - 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().
  * -# <tt>void OrthancPluginFinalize()</tt>:
  *    This function is invoked by Orthanc during its shutdown. The plugin
  *    must free all its memory.
@@ -58,7 +59,7 @@
  * @brief Functions to register and manage callbacks by the plugins.
  *
  * @defgroup DicomCallbacks DicomCallbacks
- * @brief Functions to register and manage DICOM callbacks (worklists, C-Find, C-MOVE).
+ * @brief Functions to register and manage DICOM callbacks (worklists, C-FIND, C-MOVE, storage commitment).
  *
  * @defgroup Orthanc Orthanc
  * @brief Functions to access the content of the Orthanc server.
@@ -122,16 +123,16 @@
 #endif
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
-#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     5
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  7
+#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     6
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  0
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
-#define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision) \
-  (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major ||               \
-   (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major &&             \
-    (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor ||             \
-     (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor &&           \
+#define ORTHANC_PLUGINS_VERSION_IS_ABOVE(major, minor, revision)        \
+  (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER > major ||                      \
+   (ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER == major &&                    \
+    (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER > minor ||                    \
+     (ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER == minor &&                  \
       ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER >= revision))))
 #endif
 
@@ -301,6 +302,7 @@
     OrthancPluginErrorCode_CannotOrderSlices = 2040    /*!< Unable to order the slices of the series */,
     OrthancPluginErrorCode_NoWorklistHandler = 2041    /*!< No request handler factory for DICOM C-Find Modality SCP */,
     OrthancPluginErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
+    OrthancPluginErrorCode_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
     OrthancPluginErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
 
     _OrthancPluginErrorCode_INTERNAL = 0x7fffffff
@@ -450,6 +452,7 @@
     _OrthancPluginService_RegisterIncomingHttpRequestFilter2 = 1010,
     _OrthancPluginService_RegisterRefreshMetricsCallback = 1011,
     _OrthancPluginService_RegisterChunkedRestCallback = 1012,  /* New in Orthanc 1.5.7 */
+    _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013,
 
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
@@ -909,14 +912,14 @@
    **/
   typedef enum
   {
-    OrthancPluginMetricsType_Default,   /*!< Default metrics */
+    OrthancPluginMetricsType_Default = 0,   /*!< Default metrics */
 
     /**
      * This metrics represents a time duration. Orthanc will keep the
      * maximum value of the metrics over a sliding window of ten
      * seconds, which is useful if the metrics is sampled frequently.
      **/
-    OrthancPluginMetricsType_Timer
+    OrthancPluginMetricsType_Timer = 1
   } OrthancPluginMetricsType;
   
 
@@ -926,11 +929,47 @@
    **/
   typedef enum
   {
-    OrthancPluginDicomWebBinaryMode_Ignore,        /*!< Don't include binary tags */
-    OrthancPluginDicomWebBinaryMode_InlineBinary,  /*!< Inline encoding using Base64 */
-    OrthancPluginDicomWebBinaryMode_BulkDataUri    /*!< Use a bulk data URI field */
+    OrthancPluginDicomWebBinaryMode_Ignore = 0,        /*!< Don't include binary tags */
+    OrthancPluginDicomWebBinaryMode_InlineBinary = 1,  /*!< Inline encoding using Base64 */
+    OrthancPluginDicomWebBinaryMode_BulkDataUri = 2    /*!< Use a bulk data URI field */
   } OrthancPluginDicomWebBinaryMode;
 
+
+  /**
+   * The available values for the Failure Reason (0008,1197) during
+   * storage commitment.
+   * http://dicom.nema.org/medical/dicom/2019e/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
+   **/
+  typedef enum
+  {
+    OrthancPluginStorageCommitmentFailureReason_Success = 0,
+    /*!< Success: The DICOM instance is properly stored in the SCP */
+
+    OrthancPluginStorageCommitmentFailureReason_ProcessingFailure = 1,
+    /*!< 0110H: A general failure in processing the operation was encountered */
+
+    OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance = 2,
+    /*!< 0112H: One or more of the elements in the Referenced SOP
+      Instance Sequence was not available */
+
+    OrthancPluginStorageCommitmentFailureReason_ResourceLimitation = 3,
+    /*!< 0213H: The SCP does not currently have enough resources to
+      store the requested SOP Instance(s) */
+
+    OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 4,
+    /*!< 0122H: Storage Commitment has been requested for a SOP
+      Instance with a SOP Class that is not supported by the SCP */
+
+    OrthancPluginStorageCommitmentFailureReason_ClassInstanceConflict = 5,
+    /*!< 0119H: The SOP Class of an element in the Referenced SOP
+      Instance Sequence did not correspond to the SOP class registered
+      for this SOP Instance at the SCP */
+
+    OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID = 6
+    /*!< 0131H: The Transaction UID of the Storage Commitment Request
+      is already in use */
+  } OrthancPluginStorageCommitmentFailureReason;
+
   
 
   /**
@@ -1023,7 +1062,7 @@
    * @brief Opaque structure to an object that can be used to check whether a DICOM instance matches a C-Find query.
    * @ingroup Toolbox
    **/
-  typedef struct _OrthancPluginFindAnswers_t OrthancPluginFindMatcher;
+  typedef struct _OrthancPluginFindMatcher_t OrthancPluginFindMatcher;
 
 
   
@@ -1658,7 +1697,8 @@
         sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) ||
         sizeof(int32_t) != sizeof(OrthancPluginConstraintType) ||
         sizeof(int32_t) != sizeof(OrthancPluginMetricsType) ||
-        sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode))
+        sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) ||
+        sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason))
     {
       /* Mismatch in the size of the enumerations */
       return 0;
@@ -2738,7 +2778,7 @@
    * @return The pointer to the DICOM data, NULL in case of error.
    * @ingroup Callbacks
    **/
-  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceData(
+  ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetInstanceData(
     OrthancPluginContext*        context,
     OrthancPluginDicomInstance*  instance)
   {
@@ -7259,6 +7299,117 @@
   }
 
 
+
+  /**
+   * @brief Callback executed by the storage commitment SCP.
+   *
+   * Signature of a factory function that creates an object to handle
+   * one incoming storage commitment request.
+   *
+   * @remark The factory receives the list of the SOP class/instance
+   * UIDs of interest to the remote storage commitment SCU. This gives
+   * the factory the possibility to start some prefetch process
+   * upfront in the background, before the handler object is actually
+   * queried about the status of these DICOM instances.
+   *
+   * @param handler Output variable where the factory puts the handler object it created.
+   * @param jobId ID of the Orthanc job that is responsible for handling 
+   * the storage commitment request. This job will successively look for the
+   * status of all the individual queried DICOM instances.
+   * @param transactionUid UID of the storage commitment transaction
+   * provided by the storage commitment SCU. It contains the value of the
+   * (0008,1195) DICOM tag.
+   * @param sopClassUids Array of the SOP class UIDs (0008,0016) that are queried by the SCU.
+   * @param sopInstanceUids Array of the SOP instance UIDs (0008,0018) that are queried by the SCU.
+   * @param countInstances Number of DICOM instances that are queried. This is the size
+   * of the `sopClassUids` and `sopInstanceUids` arrays.
+   * @param remoteAet The AET of the storage commitment SCU.
+   * @param calledAet The AET used by the SCU to contact the storage commitment SCP (i.e. Orthanc).
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentFactory) (
+    void**              handler /* out */,
+    const char*         jobId,
+    const char*         transactionUid,
+    const char* const*  sopClassUids,
+    const char* const*  sopInstanceUids,
+    uint32_t            countInstances,
+    const char*         remoteAet,
+    const char*         calledAet);
+
+  
+  /**
+   * @brief Callback to free one storage commitment SCP handler.
+   * 
+   * Signature of a callback function that releases the resources
+   * allocated by the factory of the storage commitment SCP. The
+   * handler is the return value of a previous call to the
+   * OrthancPluginStorageCommitmentFactory() callback.
+   *
+   * @param handler The handler object to be destructed.
+   * @ingroup DicomCallbacks
+   **/
+  typedef void (*OrthancPluginStorageCommitmentDestructor) (void* handler);
+
+
+  /**
+   * @brief Callback to get the status of one DICOM instance in the
+   * storage commitment SCP.
+   *
+   * Signature of a callback function that is successively invoked for
+   * each DICOM instance that is queried by the remote storage
+   * commitment SCU.  The function must be tought of as a method of
+   * the handler object that was created by a previous call to the
+   * OrthancPluginStorageCommitmentFactory() callback. After each call
+   * to this method, the progress of the associated Orthanc job is
+   * updated.
+   * 
+   * @param target Output variable where to put the status for the queried instance.
+   * @param handler The handler object associated with this storage commitment request.
+   * @param sopClassUid The SOP class UID (0008,0016) of interest.
+   * @param sopInstanceUid The SOP instance UID (0008,0018) of interest.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentLookup) (
+    OrthancPluginStorageCommitmentFailureReason* target,
+    void* handler,
+    const char* sopClassUid,
+    const char* sopInstanceUid);
+    
+    
+  typedef struct
+  {
+    OrthancPluginStorageCommitmentFactory     factory;
+    OrthancPluginStorageCommitmentDestructor  destructor;
+    OrthancPluginStorageCommitmentLookup      lookup;
+  } _OrthancPluginRegisterStorageCommitmentScpCallback;
+
+  /**
+   * @brief Register a callback to handle incoming requests to the storage commitment SCP.
+   *
+   * This function registers a callback to handle storage commitment SCP requests.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param factory Factory function that creates the handler object
+   * for incoming storage commitment requests.
+   * @param destructor Destructor function to destroy the handler object.
+   * @param lookup Callback method to get the status of one DICOM instance.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterStorageCommitmentScpCallback(
+    OrthancPluginContext*                     context,
+    OrthancPluginStorageCommitmentFactory     factory,
+    OrthancPluginStorageCommitmentDestructor  destructor,
+    OrthancPluginStorageCommitmentLookup      lookup)
+  {
+    _OrthancPluginRegisterStorageCommitmentScpCallback params;
+    params.factory = factory;
+    params.destructor = destructor;
+    params.lookup = lookup;
+    return context->InvokeService(context, _OrthancPluginService_RegisterStorageCommitmentScpCallback, &params);
+  }
   
 #ifdef  __cplusplus
 }
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -33,8 +33,9 @@
 
 #include "OrthancPluginCppWrapper.h"
 
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/move/unique_ptr.hpp>
 #include <boost/thread.hpp>
-#include <boost/algorithm/string/predicate.hpp>
 #include <json/reader.h>
 #include <json/writer.h>
 
@@ -2168,7 +2169,7 @@
     static const char* KEY_ASYNCHRONOUS = "Asynchronous";
     static const char* KEY_PRIORITY = "Priority";
 
-    std::auto_ptr<OrthancJob> protection(job);
+    boost::movelib::unique_ptr<OrthancJob> protection(job);
   
     if (body.type() != Json::objectValue)
     {
@@ -3059,7 +3060,7 @@
             }
             else
             {
-              std::auto_ptr<IChunkedRequestReader> reader(PostHandler(url, request));
+              boost::movelib::unique_ptr<IChunkedRequestReader> reader(PostHandler(url, request));
               if (reader.get() == NULL)
               {
                 ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
@@ -3092,7 +3093,7 @@
             }
             else
             {
-              std::auto_ptr<IChunkedRequestReader> reader(PutHandler(url, request));
+              boost::movelib::unique_ptr<IChunkedRequestReader> reader(PutHandler(url, request));
               if (reader.get() == NULL)
               {
                 ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
@@ -3139,4 +3140,41 @@
     }
 #endif
   }
+
+
+#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1
+  OrthancPluginErrorCode IStorageCommitmentScpHandler::Lookup(
+    OrthancPluginStorageCommitmentFailureReason* target,
+    void* rawHandler,
+    const char* sopClassUid,
+    const char* sopInstanceUid)
+  {
+    assert(target != NULL &&
+           rawHandler != NULL);
+      
+    try
+    {
+      IStorageCommitmentScpHandler& handler = *reinterpret_cast<IStorageCommitmentScpHandler*>(rawHandler);
+      *target = handler.Lookup(sopClassUid, sopInstanceUid);
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1
+  void IStorageCommitmentScpHandler::Destructor(void* rawHandler)
+  {
+    assert(rawHandler != NULL);
+    delete reinterpret_cast<IStorageCommitmentScpHandler*>(rawHandler);
+  }
+#endif
 }
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Wed Apr 01 10:15:33 2020 +0200
@@ -103,6 +103,12 @@
 #  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER  0
 #endif
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 0)
+#  define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP  1
+#else
+#  define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP  0
+#endif
+
 
 
 namespace OrthancPlugins
@@ -297,7 +303,7 @@
   public:
     OrthancConfiguration();
 
-    OrthancConfiguration(bool load);
+    explicit OrthancConfiguration(bool load);
 
     const Json::Value& GetJson() const
     {
@@ -363,7 +369,7 @@
   public:
     OrthancImage();
 
-    OrthancImage(OrthancPluginImage*    image);
+    explicit OrthancImage(OrthancPluginImage* image);
 
     OrthancImage(OrthancPluginPixelFormat  format,
                  uint32_t                  width,
@@ -429,15 +435,15 @@
                     uint32_t               size);
 
   public:
-    FindMatcher(const OrthancPluginWorklistQuery*  worklist);
+    explicit FindMatcher(const OrthancPluginWorklistQuery*  worklist);
 
-    FindMatcher(const void*            query,
-                uint32_t               size)
+    FindMatcher(const void*  query,
+                uint32_t     size)
     {
       SetupDicom(query, size);
     }
 
-    FindMatcher(const MemoryBuffer&    dicom)
+    explicit FindMatcher(const MemoryBuffer&  dicom)
     {
       SetupDicom(dicom.GetData(), dicom.GetSize());
     }
@@ -804,7 +810,7 @@
     boost::posix_time::ptime  start_;
 
   public:
-    MetricsTimer(const char* name);
+    explicit MetricsTimer(const char* name);
 
     ~MetricsTimer();
   };
@@ -1100,4 +1106,26 @@
 #endif
     }
   };
+
+  
+
+#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1
+  class IStorageCommitmentScpHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IStorageCommitmentScpHandler()
+    {
+    }
+    
+    virtual OrthancPluginStorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
+                                                               const std::string& sopInstanceUid) = 0;
+    
+    static OrthancPluginErrorCode Lookup(OrthancPluginStorageCommitmentFailureReason* target,
+                                         void* rawHandler,
+                                         const char* sopClassUid,
+                                         const char* sopInstanceUid);
+
+    static void Destructor(void* rawHandler);
+  };
+#endif
 }
--- a/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -21,6 +21,7 @@
 
 #include "GdcmDecoderCache.h"
 
+#include "../../../Core/Compatibility.h"
 #include "OrthancImageWrapper.h"
 
 namespace OrthancPlugins
@@ -83,13 +84,13 @@
     }
 
     // This is not the same image
-    std::auto_ptr<GdcmImageDecoder> decoder(new GdcmImageDecoder(dicom, size));
-    std::auto_ptr<OrthancImageWrapper> image(new OrthancImageWrapper(context, decoder->Decode(context, frameIndex)));
+    std::unique_ptr<GdcmImageDecoder> decoder(new GdcmImageDecoder(dicom, size));
+    std::unique_ptr<OrthancImageWrapper> image(new OrthancImageWrapper(context, decoder->Decode(context, frameIndex)));
 
     {
       // Cache the newly created decoder for further use
       boost::mutex::scoped_lock lock(mutex_);
-      decoder_ = decoder;
+      decoder_.reset(decoder.release());
       size_ = size;
       md5_ = md5;
     }
--- a/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Samples/GdcmDecoder/GdcmDecoderCache.h	Wed Apr 01 10:15:33 2020 +0200
@@ -21,6 +21,7 @@
 
 #pragma once
 
+#include "../../../Core/Compatibility.h"
 #include "GdcmImageDecoder.h"
 #include "OrthancImageWrapper.h"
 
@@ -33,7 +34,7 @@
   {
   private:
     boost::mutex   mutex_;
-    std::auto_ptr<OrthancPlugins::GdcmImageDecoder>  decoder_;
+    std::unique_ptr<OrthancPlugins::GdcmImageDecoder>  decoder_;
     size_t       size_;
     std::string  md5_;
 
--- a/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Samples/GdcmDecoder/GdcmImageDecoder.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -21,6 +21,7 @@
 
 #include "GdcmImageDecoder.h"
 
+#include "../../../Core/Compatibility.h"
 #include "OrthancImageWrapper.h"
 
 #include <gdcmImageReader.h>
@@ -40,9 +41,9 @@
     size_t                size_;
 
     gdcm::ImageReader reader_;
-    std::auto_ptr<gdcm::ImageApplyLookupTable> lut_;
-    std::auto_ptr<gdcm::ImageChangePhotometricInterpretation> photometric_;
-    std::auto_ptr<gdcm::ImageChangePlanarConfiguration> interleaved_;
+    std::unique_ptr<gdcm::ImageApplyLookupTable> lut_;
+    std::unique_ptr<gdcm::ImageChangePhotometricInterpretation> photometric_;
+    std::unique_ptr<gdcm::ImageChangePlanarConfiguration> interleaved_;
     std::string decoded_;
 
     PImpl(const void* dicom,
--- a/Plugins/Samples/GdcmDecoder/Plugin.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Samples/GdcmDecoder/Plugin.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -19,6 +19,7 @@
  **/
 
 
+#include "../../../Core/Compatibility.h"
 #include "GdcmDecoderCache.h"
 #include "OrthancImageWrapper.h"
 
@@ -35,7 +36,7 @@
 {
   try
   {
-    std::auto_ptr<OrthancPlugins::OrthancImageWrapper> image;
+    std::unique_ptr<OrthancPlugins::OrthancImageWrapper> image;
 
 #if 0
     // Do not use the cache
--- a/Plugins/Samples/ModalityWorklists/Plugin.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/Plugins/Samples/ModalityWorklists/Plugin.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -19,6 +19,7 @@
  **/
 
 
+#include "../../../Core/Compatibility.h"
 #include "../Common/OrthancPluginCppWrapper.h"
 
 #include <boost/filesystem.hpp>
@@ -142,7 +143,7 @@
   try
   {
     // Construct an object to match the worklists in the database against the C-Find query
-    std::auto_ptr<OrthancPlugins::FindMatcher> matcher(CreateMatcher(query, issuerAet));
+    std::unique_ptr<OrthancPlugins::FindMatcher> matcher(CreateMatcher(query, issuerAet));
 
     // Loop over the regular files in the database folder
     namespace fs = boost::filesystem;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/README.txt	Wed Apr 01 10:15:33 2020 +0200
@@ -0,0 +1,3 @@
+More contributed samples of plugins can be found and added in
+the "OrthancContributed" repository on GitHub:
+https://github.com/jodogne/OrthancContributed/tree/master/Plugins
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt	Wed Apr 01 10:15:33 2020 +0200
@@ -0,0 +1,37 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(StorageCommitmentScp)
+
+SET(PLUGIN_VERSION "0.0" CACHE STRING "Version of the plugin")
+SET(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
+SET(ALLOW_DOWNLOADS OFF CACHE BOOL "Allow CMake to download packages")
+
+SET(USE_SYSTEM_JSONCPP ON CACHE BOOL "Use the system version of JsonCpp")
+SET(USE_SYSTEM_BOOST ON CACHE BOOL "Use the system version of boost")
+
+set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
+include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
+
+add_library(StorageCommitmentScp SHARED 
+  Plugin.cpp
+  ../Common/OrthancPluginCppWrapper.cpp
+  ${JSONCPP_SOURCES}
+  ${BOOST_SOURCES}
+  )
+
+message("Setting the version of the plugin to ${PLUGIN_VERSION}")
+add_definitions(
+  -DPLUGIN_VERSION="${PLUGIN_VERSION}"
+  )
+
+set_target_properties(StorageCommitmentScp PROPERTIES 
+  VERSION ${PLUGIN_VERSION} 
+  SOVERSION ${PLUGIN_VERSION})
+
+install(
+  TARGETS StorageCommitmentScp
+  RUNTIME DESTINATION lib    # Destination for Windows
+  LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/StorageCommitmentScp/Plugin.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -0,0 +1,116 @@
+/**
+ * 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.
+ *
+ * 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 "../Common/OrthancPluginCppWrapper.h"
+
+#include <json/value.h>
+#include <json/reader.h>
+
+
+
+class StorageCommitmentSample : public OrthancPlugins::IStorageCommitmentScpHandler
+{
+private:
+  int count_;
+  
+public:
+  StorageCommitmentSample() : count_(0)
+  {
+  }
+  
+  virtual OrthancPluginStorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
+                                                             const std::string& sopInstanceUid)
+  {
+    printf("?? [%s] [%s]\n", sopClassUid.c_str(), sopInstanceUid.c_str());
+    if (count_++ % 2 == 0)
+      return OrthancPluginStorageCommitmentFailureReason_Success;
+    else
+      return OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance;
+  }
+};
+
+
+static OrthancPluginErrorCode StorageCommitmentScp(void**              handler /* out */,
+                                                   const char*         jobId,
+                                                   const char*         transactionUid,
+                                                   const char* const*  sopClassUids,
+                                                   const char* const*  sopInstanceUids,
+                                                   uint32_t            countInstances,
+                                                   const char*         remoteAet,
+                                                   const char*         calledAet)
+{
+  /*std::string s;
+    OrthancPlugins::RestApiPost(s, "/jobs/" + std::string(jobId) + "/pause", NULL, 0, false);*/
+  
+  printf("[%s] [%s] [%s] [%s]\n", jobId, transactionUid, remoteAet, calledAet);
+
+  for (uint32_t i = 0; i < countInstances; i++)
+  {
+    printf("++ [%s] [%s]\n", sopClassUids[i], sopInstanceUids[i]);
+  }
+
+  *handler = new StorageCommitmentSample;
+  return OrthancPluginErrorCode_Success;
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    OrthancPlugins::SetGlobalContext(c);
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 0)
+    {
+      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      return -1;
+    }
+
+    OrthancPluginSetDescription(c, "Sample storage commitment SCP plugin.");
+
+    OrthancPluginRegisterStorageCommitmentScpCallback(
+      c, StorageCommitmentScp,
+      OrthancPlugins::IStorageCommitmentScpHandler::Destructor,
+      OrthancPlugins::IStorageCommitmentScpHandler::Lookup);
+    
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "storage-commitment-scp";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return PLUGIN_VERSION;
+  }
+}
--- a/Resources/CMake/BoostConfiguration.cmake	Wed Apr 01 10:14:49 2020 +0200
+++ b/Resources/CMake/BoostConfiguration.cmake	Wed Apr 01 10:15:33 2020 +0200
@@ -12,7 +12,7 @@
   endif()
 
   list(APPEND ORTHANC_BOOST_COMPONENTS filesystem thread system date_time regex)
-  find_package(Boost COMPONENTS "${ORTHANC_BOOST_COMPONENTS}")
+  find_package(Boost COMPONENTS ${ORTHANC_BOOST_COMPONENTS})
 
   if (NOT Boost_FOUND)
     foreach (item ${ORTHANC_BOOST_COMPONENTS})
--- a/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake	Wed Apr 01 10:14:49 2020 +0200
+++ b/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake	Wed Apr 01 10:15:33 2020 +0200
@@ -185,11 +185,26 @@
   )
 
 
-if(${CMAKE_SYSTEM_NAME} STREQUAL "Windows" AND
-    CMAKE_COMPILER_IS_GNUCXX)
-  # This is MinGW
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  # For compatibility with Windows XP, avoid using fiber-local-storage
+  # in log4cplus, but use thread-local-storage instead. Otherwise,
+  # Windows XP complains about missing "FlsGetValue()" in KERNEL32.dll
   add_definitions(
     -DDCMTK_LOG4CPLUS_AVOID_WIN32_FLS
-    -DDCMTK_LOG4CPLUS_SINGLE_THREADED
     )
+
+  if (CMAKE_COMPILER_IS_GNUCXX OR             # MinGW
+      "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4")  # MSVC for 32bit (*)
+
+    # (*) With multithreaded logging enabled, Visual Studio 2008 fails
+    # with error: ".\dcmtk-3.6.5\oflog\libsrc\globinit.cc(422) : error
+    # C2664: 'dcmtk::log4cplus::thread::impl::tls_init' : cannot
+    # convert parameter 1 from 'void (__stdcall *)(void *)' to
+    # 'dcmtk::log4cplus::thread::impl::tls_init_cleanup_func_type'"
+    #   None of the functions with this name in scope match the target type
+
+    add_definitions(
+      -DDCMTK_LOG4CPLUS_SINGLE_THREADED
+      )
+  endif()
 endif()
--- a/Resources/Configuration.json	Wed Apr 01 10:14:49 2020 +0200
+++ b/Resources/Configuration.json	Wed Apr 01 10:15:33 2020 +0200
@@ -205,13 +205,13 @@
 
     /**
      * By default, the Orthanc SCP accepts all DICOM commands (C-ECHO,
-     * C-STORE, C-FIND, C-MOVE) issued by the registered remote SCU
-     * modalities. Starting with Orthanc 1.5.0, it is possible to
-     * specify which DICOM commands are allowed, separately for each
-     * remote modality, using the syntax below. The "AllowEcho" (resp.
-     * "AllowStore") option only has an effect respectively if global
-     * option "DicomAlwaysAllowEcho" (resp. "DicomAlwaysAllowStore")
-     * is set to false.
+     * C-STORE, C-FIND, C-MOVE, and storage commitment) issued by the
+     * registered remote SCU modalities. Starting with Orthanc 1.5.0,
+     * it is possible to specify which DICOM commands are allowed,
+     * separately for each remote modality, using the syntax
+     * below. The "AllowEcho" (resp.  "AllowStore") option only has an
+     * effect respectively if global option "DicomAlwaysAllowEcho"
+     * (resp. "DicomAlwaysAllowStore") is set to false.
      **/
     //"untrusted" : {
     //  "AET" : "ORTHANC",
@@ -221,7 +221,8 @@
     //  "AllowEcho" : false,
     //  "AllowFind" : false,
     //  "AllowMove" : false,
-    //  "AllowStore" : true
+    //  "AllowStore" : true,
+    //  "AllowStorageCommitment" : false  // new in 1.6.0
     //}
   },
 
@@ -527,5 +528,9 @@
   // Set the default private creator that is used by Orthanc when it
   // looks for a private tag in its dictionary (cf. "Dictionary"
   // option), or when it creates/modifies a DICOM file (new in Orthanc 1.6.0).
-  "DefaultPrivateCreator" : ""
+  "DefaultPrivateCreator" : "",
+
+  // Maximum number of storage commitment reports (i.e. received from
+  // remote modalities) to be kept in memory (new in Orthanc 1.6.0).
+  "StorageCommitmentReportsSize" : 100
 }
--- a/Resources/DicomTransferSyntaxes.json	Wed Apr 01 10:14:49 2020 +0200
+++ b/Resources/DicomTransferSyntaxes.json	Wed Apr 01 10:15:33 2020 +0200
@@ -40,6 +40,7 @@
     "Retired" : false,
     "Note" : "Default Transfer Syntax for Lossy JPEG 8-bit Image Compression",
     "DCMTK" : "EXS_JPEGProcess1",
+    "DCMTK360" : "EXS_JPEGProcess1TransferSyntax",
     "GDCM" : "gdcm::TransferSyntax::JPEGBaselineProcess1"
   },
   
@@ -50,6 +51,7 @@
     "Retired" : false,
     "Note" : "Default Transfer Syntax for Lossy JPEG (lossy, 8/12 bit), 12-bit Image Compression (Process 4 only)",
     "DCMTK" : "EXS_JPEGProcess2_4",
+    "DCMTK360" : "EXS_JPEGProcess2_4TransferSyntax",
     "GDCM" : "gdcm::TransferSyntax::JPEGExtendedProcess2_4"
   },
 
@@ -58,7 +60,8 @@
     "Name" : "JPEG Extended Sequential (lossy, 8/12 bit), arithmetic coding",
     "Value" : "JPEGProcess3_5",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess3_5"
+    "DCMTK" : "EXS_JPEGProcess3_5",
+    "DCMTK360" : "EXS_JPEGProcess3_5TransferSyntax"
   },
 
   {
@@ -66,7 +69,8 @@
     "Name" : "JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit)",
     "Value" : "JPEGProcess6_8",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess6_8"
+    "DCMTK" : "EXS_JPEGProcess6_8",
+    "DCMTK360" : "EXS_JPEGProcess6_8TransferSyntax"
   },
 
   {
@@ -74,7 +78,8 @@
     "Name" : "JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit), arithmetic coding",
     "Value" : "JPEGProcess7_9",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess7_9"
+    "DCMTK" : "EXS_JPEGProcess7_9",
+    "DCMTK360" : "EXS_JPEGProcess7_9TransferSyntax"
   },
 
   {
@@ -82,7 +87,8 @@
     "Name" : "JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit)",
     "Value" : "JPEGProcess10_12",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess10_12"
+    "DCMTK" : "EXS_JPEGProcess10_12",
+    "DCMTK360" : "EXS_JPEGProcess10_12TransferSyntax"
   },
 
   {
@@ -90,7 +96,8 @@
     "Name" : "JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit), arithmetic coding",
     "Value" : "JPEGProcess11_13",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess11_13"
+    "DCMTK" : "EXS_JPEGProcess11_13",
+    "DCMTK360" : "EXS_JPEGProcess11_13TransferSyntax"
   },
 
   {
@@ -99,6 +106,7 @@
     "Value" : "JPEGProcess14",
     "Retired" : false,
     "DCMTK" : "EXS_JPEGProcess14",
+    "DCMTK360" : "EXS_JPEGProcess14TransferSyntax",
     "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14"
   },
 
@@ -107,7 +115,8 @@
     "Name" : "JPEG Lossless with any selection value, arithmetic coding",
     "Value" : "JPEGProcess15",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess15"
+    "DCMTK" : "EXS_JPEGProcess15",
+    "DCMTK360" : "EXS_JPEGProcess15TransferSyntax"
   },
   
   {
@@ -115,7 +124,8 @@
     "Name" : "JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit)",
     "Value" : "JPEGProcess16_18",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess16_18"
+    "DCMTK" : "EXS_JPEGProcess16_18",
+    "DCMTK360" : "EXS_JPEGProcess16_18TransferSyntax"
   },
   
   {
@@ -123,7 +133,8 @@
     "Name" : "JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit), arithmetic coding",
     "Value" : "JPEGProcess17_19",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess17_19"
+    "DCMTK" : "EXS_JPEGProcess17_19",
+    "DCMTK360" : "EXS_JPEGProcess17_19TransferSyntax"
   },
   
   {
@@ -131,7 +142,8 @@
     "Name" : "JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit)",
     "Value" : "JPEGProcess20_22",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess20_22"
+    "DCMTK" : "EXS_JPEGProcess20_22",
+    "DCMTK360" : "EXS_JPEGProcess20_22TransferSyntax"
   },
   
   {
@@ -139,7 +151,8 @@
     "Name" : "JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit), arithmetic coding",
     "Value" : "JPEGProcess21_23",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess21_23"
+    "DCMTK" : "EXS_JPEGProcess21_23",
+    "DCMTK360" : "EXS_JPEGProcess21_23TransferSyntax"
   },
   
   {
@@ -147,7 +160,8 @@
     "Name" : "JPEG Full Progression, Hierarchical (lossy, 8/12 bit)",
     "Value" : "JPEGProcess24_26",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess24_26"
+    "DCMTK" : "EXS_JPEGProcess24_26",
+    "DCMTK360" : "EXS_JPEGProcess24_26TransferSyntax"
   },
   
   {
@@ -155,7 +169,8 @@
     "Name" : "JPEG Full Progression, Hierarchical (lossy, 8/12 bit), arithmetic coding",
     "Value" : "JPEGProcess25_27",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess25_27"
+    "DCMTK" : "EXS_JPEGProcess25_27",
+    "DCMTK360" : "EXS_JPEGProcess25_27TransferSyntax"
   },
   
   {
@@ -163,7 +178,8 @@
     "Name" : "JPEG Lossless, Hierarchical",
     "Value" : "JPEGProcess28",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess28"
+    "DCMTK" : "EXS_JPEGProcess28",
+    "DCMTK360" : "EXS_JPEGProcess28TransferSyntax"
   },
   
   {
@@ -171,7 +187,8 @@
     "Name" : "JPEG Lossless, Hierarchical, arithmetic coding",
     "Value" : "JPEGProcess29",
     "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess29"
+    "DCMTK" : "EXS_JPEGProcess29",
+    "DCMTK360" : "EXS_JPEGProcess29TransferSyntax"
   },
 
   {
@@ -181,6 +198,7 @@
     "Retired" : false,
     "Note" : "Default Transfer Syntax for Lossless JPEG Image Compression",
     "DCMTK" : "EXS_JPEGProcess14SV1",
+    "DCMTK360" : "EXS_JPEGProcess14SV1TransferSyntax",
     "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14_1"
   },
 
@@ -275,7 +293,8 @@
     "Name" : "MPEG4 High Profile / Level 4.1",
     "Value" : "MPEG4HighProfileLevel4_1",
     "Retired" : false,
-    "DCMTK" : "EXS_MPEG4HighProfileLevel4_1"
+    "DCMTK" : "EXS_MPEG4HighProfileLevel4_1",
+    "SinceDCMTK" : "361"
   },
 
   {
@@ -283,7 +302,8 @@
     "Name" : "MPEG4 BD-compatible High Profile / Level 4.1",
     "Value" : "MPEG4BDcompatibleHighProfileLevel4_1",
     "Retired" : false,
-    "DCMTK" : "EXS_MPEG4BDcompatibleHighProfileLevel4_1"
+    "DCMTK" : "EXS_MPEG4BDcompatibleHighProfileLevel4_1",
+    "SinceDCMTK" : "361"
   },
 
   {
@@ -291,7 +311,8 @@
     "Name" : "MPEG4 High Profile / Level 4.2 For 2D Video",
     "Value" : "MPEG4HighProfileLevel4_2_For2DVideo",
     "Retired" : false,
-    "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For2DVideo"
+    "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For2DVideo",
+    "SinceDCMTK" : "361"
   },
 
   {
@@ -299,7 +320,8 @@
     "Name" : "MPEG4 High Profile / Level 4.2 For 3D Video",
     "Value" : "MPEG4HighProfileLevel4_2_For3DVideo",
     "Retired" : false,
-    "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For3DVideo"
+    "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For3DVideo",
+    "SinceDCMTK" : "361"
   },
 
   {
@@ -307,7 +329,8 @@
     "Name" : "1.2.840.10008.1.2.4.106",
     "Value" : "MPEG4StereoHighProfileLevel4_2",
     "Retired" : false,
-    "DCMTK" : "EXS_MPEG4StereoHighProfileLevel4_2"
+    "DCMTK" : "EXS_MPEG4StereoHighProfileLevel4_2",
+    "SinceDCMTK" : "361"
   },
 
   {
--- a/Resources/DownloadOrthancFramework.cmake	Wed Apr 01 10:14:49 2020 +0200
+++ b/Resources/DownloadOrthancFramework.cmake	Wed Apr 01 10:15:33 2020 +0200
@@ -112,6 +112,8 @@
         set(ORTHANC_FRAMEWORK_MD5 "e1b76f01116d9b5d4ac8cc39980560e3")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.8")
         set(ORTHANC_FRAMEWORK_MD5 "82323e8c49a667f658a3639ea4dbc336")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.0")
+        set(ORTHANC_FRAMEWORK_MD5 "eab428d6e53f61e847fa360bb17ebe25")
 
       # Below this point are development snapshots that were used to
       # release some plugin, before an official release of the Orthanc
--- a/Resources/ErrorCodes.json	Wed Apr 01 10:14:49 2020 +0200
+++ b/Resources/ErrorCodes.json	Wed Apr 01 10:15:33 2020 +0200
@@ -547,6 +547,11 @@
     "Name": "AlreadyExistingTag",
     "Description": "Cannot override the value of a tag that already exists"
   },
+  {
+    "Code": 2043, 
+    "Name": "NoStorageCommitmentHandler", 
+    "Description": "No request handler factory for DICOM N-ACTION SCP (storage commitment)"
+  },
 
 
 
--- a/Resources/GenerateTransferSyntaxesDcmtk.mustache	Wed Apr 01 10:14:49 2020 +0200
+++ b/Resources/GenerateTransferSyntaxesDcmtk.mustache	Wed Apr 01 10:15:33 2020 +0200
@@ -34,10 +34,10 @@
 
 namespace Orthanc
 {
-  bool GetDcmtkTransferSyntax(E_TransferSyntax& target,
-                              DicomTransferSyntax syntax)
+  bool FromDcmtkBridge::LookupDcmtkTransferSyntax(E_TransferSyntax& target,
+                                                  DicomTransferSyntax source)
   {
-    switch (syntax)
+    switch (source)
     {
       {{#Syntaxes}}
       {{#DCMTK}}
@@ -45,7 +45,50 @@
 #if DCMTK_VERSION_NUMBER >= {{SinceDCMTK}}
       {{/SinceDCMTK}}
       case DicomTransferSyntax_{{Value}}:
+        {{#DCMTK360}}
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = {{DCMTK360}};
+#  else
         target = {{DCMTK}};
+#  endif
+        {{/DCMTK360}}
+        {{^DCMTK360}}
+        target = {{DCMTK}};
+        {{/DCMTK360}}
+        return true;
+      {{#SinceDCMTK}}
+#endif
+      {{/SinceDCMTK}}
+
+      {{/DCMTK}}
+      {{/Syntaxes}}
+      default:
+        return false;
+    }
+  }
+  
+
+  bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                                    E_TransferSyntax source)
+  {
+    switch (source)
+    {
+      {{#Syntaxes}}
+      {{#DCMTK}}
+      {{#SinceDCMTK}}
+#if DCMTK_VERSION_NUMBER >= {{SinceDCMTK}}
+      {{/SinceDCMTK}}
+      {{#DCMTK360}}
+#  if DCMTK_VERSION_NUMBER <= 360
+      case {{DCMTK360}}:
+#  else
+      case {{DCMTK}}:
+#  endif
+      {{/DCMTK360}}
+      {{^DCMTK360}}
+      case {{DCMTK}}:
+      {{/DCMTK360}}
+        target = DicomTransferSyntax_{{Value}};
         return true;
       {{#SinceDCMTK}}
 #endif
--- a/Resources/Patches/dcmtk-3.6.5.patch	Wed Apr 01 10:14:49 2020 +0200
+++ b/Resources/Patches/dcmtk-3.6.5.patch	Wed Apr 01 10:15:33 2020 +0200
@@ -1,6 +1,6 @@
 diff -urEb dcmtk-3.6.5.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.5/dcmdata/include/dcmtk/dcmdata/dcdict.h
---- dcmtk-3.6.5.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-03-05 19:43:37.678302817 +0100
-+++ dcmtk-3.6.5/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-03-05 19:43:41.198312828 +0100
+--- dcmtk-3.6.5.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-03-18 10:22:41.555166774 +0100
++++ dcmtk-3.6.5/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-03-18 10:22:53.395131056 +0100
 @@ -152,6 +152,12 @@
      /// returns an iterator to the end of the repeating tag dictionary
      DcmDictEntryListIterator repeatingEnd() { return repDict.end(); }
@@ -15,8 +15,8 @@
  
      /** private undefined assignment operator
 diff -urEb dcmtk-3.6.5.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.5/dcmdata/libsrc/dcdict.cc
---- dcmtk-3.6.5.orig/dcmdata/libsrc/dcdict.cc	2020-03-05 19:43:37.682302828 +0100
-+++ dcmtk-3.6.5/dcmdata/libsrc/dcdict.cc	2020-03-05 19:43:41.198312828 +0100
+--- dcmtk-3.6.5.orig/dcmdata/libsrc/dcdict.cc	2020-03-18 10:22:41.559166762 +0100
++++ dcmtk-3.6.5/dcmdata/libsrc/dcdict.cc	2020-03-18 10:22:53.395131056 +0100
 @@ -900,3 +900,6 @@
    wrlock().clear();
    wrunlock();
@@ -25,8 +25,8 @@
 +
 +#include "dcdict_orthanc.cc"
 diff -urEb dcmtk-3.6.5.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-3.6.5/dcmdata/libsrc/dcpxitem.cc
---- dcmtk-3.6.5.orig/dcmdata/libsrc/dcpxitem.cc	2020-03-05 19:43:37.682302828 +0100
-+++ dcmtk-3.6.5/dcmdata/libsrc/dcpxitem.cc	2020-03-05 19:43:41.198312828 +0100
+--- dcmtk-3.6.5.orig/dcmdata/libsrc/dcpxitem.cc	2020-03-18 10:22:41.559166762 +0100
++++ dcmtk-3.6.5/dcmdata/libsrc/dcpxitem.cc	2020-03-18 10:22:53.395131056 +0100
 @@ -36,6 +36,9 @@
  #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
  #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
@@ -38,8 +38,8 @@
  // ********************************
  
 diff -urEb dcmtk-3.6.5.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-3.6.5/oflog/include/dcmtk/oflog/thread/syncpub.h
---- dcmtk-3.6.5.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-03-05 19:43:37.686302839 +0100
-+++ dcmtk-3.6.5/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-03-05 19:43:41.198312828 +0100
+--- dcmtk-3.6.5.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-03-18 10:22:41.543166810 +0100
++++ dcmtk-3.6.5/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-03-18 10:22:53.395131056 +0100
 @@ -63,7 +63,7 @@
  
  DCMTK_LOG4CPLUS_INLINE_EXPORT
@@ -86,22 +86,22 @@
  
  
 diff -urEb dcmtk-3.6.5.orig/oflog/libsrc/oflog.cc dcmtk-3.6.5/oflog/libsrc/oflog.cc
---- dcmtk-3.6.5.orig/oflog/libsrc/oflog.cc	2020-03-05 19:43:37.690302851 +0100
-+++ dcmtk-3.6.5/oflog/libsrc/oflog.cc	2020-03-05 19:43:54.622350144 +0100
+--- dcmtk-3.6.5.orig/oflog/libsrc/oflog.cc	2020-03-18 10:22:41.547166798 +0100
++++ dcmtk-3.6.5/oflog/libsrc/oflog.cc	2020-03-18 11:55:50.116856932 +0100
 @@ -19,6 +19,10 @@
   *
   */
  
-+#ifdef __MINGW32__
-+#  include <winsock.h>
++#if defined(_WIN32)
++#  include <winsock2.h>
 +#endif
 +
  #include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
  #include "dcmtk/oflog/oflog.h"
  
 diff -urEb dcmtk-3.6.5.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.5/ofstd/include/dcmtk/ofstd/offile.h
---- dcmtk-3.6.5.orig/ofstd/include/dcmtk/ofstd/offile.h	2020-03-05 19:43:37.714302919 +0100
-+++ dcmtk-3.6.5/ofstd/include/dcmtk/ofstd/offile.h	2020-03-05 19:43:41.198312828 +0100
+--- dcmtk-3.6.5.orig/ofstd/include/dcmtk/ofstd/offile.h	2020-03-18 10:22:41.587166677 +0100
++++ dcmtk-3.6.5/ofstd/include/dcmtk/ofstd/offile.h	2020-03-18 10:22:53.395131056 +0100
 @@ -575,7 +575,7 @@
     */
    void setlinebuf()
--- a/Resources/Patches/dcmtk.txt	Wed Apr 01 10:14:49 2020 +0200
+++ b/Resources/Patches/dcmtk.txt	Wed Apr 01 10:15:33 2020 +0200
@@ -4,6 +4,7 @@
 diff -urEb dcmtk-3.6.0.orig/ dcmtk-3.6.0
 diff -urEb dcmtk-3.6.2.orig/ dcmtk-3.6.2
 diff -urEb dcmtk-3.6.4.orig/ dcmtk-3.6.4
+diff -urEb dcmtk-3.6.5.orig/ dcmtk-3.6.5
 
 For "dcmtk-3.6.2-private.dic"
 =============================
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Samples/README.txt	Wed Apr 01 10:15:33 2020 +0200
@@ -0,0 +1,7 @@
+More contributed samples and documentation can be found and added in
+the "OrthancContributed" repository on GitHub:
+https://github.com/jodogne/OrthancContributed
+
+The integration tests of Orthanc provide a lot of samples about the
+features of the REST API of Orthanc:
+https://bitbucket.org/sjodogne/orthanc-tests/src/default/Tests/Tests.py
--- a/UnitTestsSources/FromDcmtkTests.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/UnitTestsSources/FromDcmtkTests.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -1917,7 +1917,186 @@
 
 #if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
 
+#include "../Core/DicomParsing/Internals/DicomFrameIndex.h"
+
 #include <dcmtk/dcmdata/dcostrmb.h>
+#include <dcmtk/dcmdata/dcpixel.h>
+#include <dcmtk/dcmdata/dcpxitem.h>
+
+
+namespace Orthanc
+{
+  class IDicomTranscoder : public boost::noncopyable
+  {
+  public:
+    virtual ~IDicomTranscoder()
+    {
+    }
+
+    virtual DicomTransferSyntax GetTransferSyntax() = 0;
+
+    virtual std::string GetSopClassUid() = 0;
+
+    virtual std::string GetSopInstanceUid() = 0;
+
+    virtual unsigned int GetFramesCount() = 0;
+
+    virtual ImageAccessor* DecodeFrame(unsigned int frame) = 0;
+
+    virtual void GetCompressedFrame(std::string& target,
+                                    unsigned int frame) = 0;
+
+    virtual IDicomTranscoder* Transcode(std::set<DicomTransferSyntax> syntaxes,
+                                        bool allowNewSopInstanceUid) = 0;
+  };
+
+
+  class DcmtkTranscoder : public IDicomTranscoder
+  {
+  private:
+    std::unique_ptr<DcmFileFormat>    dicom_;
+    std::unique_ptr<DicomFrameIndex>  index_;
+    DicomTransferSyntax               transferSyntax_;
+    std::string                       sopClassUid_;
+    std::string                       sopInstanceUid_;
+
+    void Setup(DcmFileFormat* dicom)
+    {
+      dicom_.reset(dicom);
+      
+      if (dicom == NULL ||
+          dicom_->getDataset() == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+
+      DcmDataset& dataset = *dicom_->getDataset();
+      index_.reset(new DicomFrameIndex(dataset));
+
+      E_TransferSyntax xfer = dataset.getOriginalXfer();
+      if (xfer == EXS_Unknown)
+      {
+        dataset.updateOriginalXfer();
+        xfer = dataset.getOriginalXfer();
+        if (xfer == EXS_Unknown)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "Cannot determine the transfer syntax of the DICOM instance");
+        }
+      }
+
+      if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax_, xfer))
+      {
+        throw OrthancException(
+          ErrorCode_BadFileFormat,
+          "Unsupported transfer syntax: " + boost::lexical_cast<std::string>(xfer));
+      }
+
+      const char* a = NULL;
+      const char* b = NULL;
+
+      if (!dataset.findAndGetString(DCM_SOPClassUID, a).good() ||
+          !dataset.findAndGetString(DCM_SOPInstanceUID, b).good() ||
+          a == NULL ||
+          b == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Missing SOP class/instance UID in DICOM instance");
+      }
+
+      sopClassUid_.assign(a);
+      sopInstanceUid_.assign(b);
+    }
+    
+  public:
+    DcmtkTranscoder(DcmFileFormat* dicom)  // Takes ownership
+    {
+      Setup(dicom);
+    }
+
+    DcmtkTranscoder(const void* dicom,
+                    size_t size)
+    {
+      Setup(FromDcmtkBridge::LoadFromMemoryBuffer(dicom, size));
+    }
+
+    virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE
+    {
+      return transferSyntax_;
+    }
+
+    virtual std::string GetSopClassUid() ORTHANC_OVERRIDE
+    {
+      return sopClassUid_;
+    }
+    
+    virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE
+    {
+      return sopInstanceUid_;
+    }
+
+    virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE
+    {
+      return index_->GetFramesCount();
+    }
+
+    virtual ImageAccessor* DecodeFrame(unsigned int frame) ORTHANC_OVERRIDE
+    {
+      assert(dicom_->getDataset() != NULL);
+      return DicomImageDecoder::Decode(*dicom_->getDataset(), frame);
+    }
+
+    virtual void GetCompressedFrame(std::string& target,
+                                    unsigned int frame) ORTHANC_OVERRIDE
+    {
+#if 1
+      index_->GetRawFrame(target, frame);
+      printf("%d: %d\n", frame, target.size());
+#endif
+
+#if 1
+      assert(dicom_->getDataset() != NULL);
+      DcmDataset& dataset = *dicom_->getDataset();
+      
+      DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dataset);
+
+      if (pixelSequence != NULL &&
+          frame == 0 &&
+          pixelSequence->card() != GetFramesCount() + 1)
+      {
+        printf("COMPRESSED\n");
+        
+        // Check out "djcodecd.cc"
+        
+        printf("%d fragments\n", pixelSequence->card());
+        
+        // Skip the first fragment, that is the offset table
+        for (unsigned long i = 1; ;i++)
+        {
+          DcmPixelItem *fragment = NULL;
+          if (pixelSequence->getItem(fragment, i).good())
+          {
+            printf("fragment %d %d\n", i, fragment->getLength());
+          }
+          else
+          {
+            break;
+          }
+        }
+      }
+#endif
+    }
+
+    virtual IDicomTranscoder* Transcode(std::set<DicomTransferSyntax> syntaxes,
+                                        bool allowNewSopInstanceUid) ORTHANC_OVERRIDE
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  };
+}
+
+
+
 
 static bool Transcode(std::string& buffer,
                       DcmDataset& dataSet,
@@ -1991,44 +2170,97 @@
 #include "dcmtk/dcmjpeg/djrploss.h"  /* for DJ_RPLossy */
 #include "dcmtk/dcmjpeg/djrplol.h"   /* for DJ_RPLossless */
 
+#include <boost/filesystem.hpp>
+
+
+static void TestFile(const std::string& path)
+{
+  printf("** %s\n", path.c_str());
+
+  std::string s;
+  SystemToolbox::ReadFile(s, path);
+
+  Orthanc::DcmtkTranscoder transcoder(s.c_str(), s.size());
+
+  printf("[%s] [%s] [%s] %d\n", GetTransferSyntaxUid(transcoder.GetTransferSyntax()),
+         transcoder.GetSopClassUid().c_str(), transcoder.GetSopInstanceUid().c_str(),
+         transcoder.GetFramesCount());
+
+  for (size_t i = 0; i < transcoder.GetFramesCount(); i++)
+  {
+    std::string f;
+    transcoder.GetCompressedFrame(f, i);
+
+    if (i == 0)
+    {
+      static unsigned int i = 0;
+      char buf[1024];
+      sprintf(buf, "/tmp/frame-%06d.dcm", i++);
+      printf(">> %s\n", buf);
+      Orthanc::SystemToolbox::WriteFile(f, buf);
+    }
+  }
+
+  printf("\n");
+}
+
 TEST(Toto, Transcode)
 {
-  OFLog::configure(OFLogger::DEBUG_LOG_LEVEL);
-  std::string s;
-  //SystemToolbox::ReadFile(s, "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm");
-  //SystemToolbox::ReadFile(s, "/home/jodogne/DICOM/Alain.dcm");
-  SystemToolbox::ReadFile(s, "/home/jodogne/Subversion/orthanc-tests/Database/Brainix/Epi/IM-0001-0002.dcm");
+  if (0)
+  {
+    OFLog::configure(OFLogger::DEBUG_LOG_LEVEL);
 
-  std::auto_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(s.c_str(), s.size()));
+    std::string s;
+    //SystemToolbox::ReadFile(s, "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.4.50.dcm");
+    //SystemToolbox::ReadFile(s, "/home/jodogne/DICOM/Alain.dcm");
+    SystemToolbox::ReadFile(s, "/home/jodogne/Subversion/orthanc-tests/Database/Brainix/Epi/IM-0001-0002.dcm");
 
-  // less /home/jodogne/Downloads/dcmtk-3.6.4/dcmdata/include/dcmtk/dcmdata/dcxfer.h
-  printf(">> %d\n", dicom->getDataset()->getOriginalXfer());  // => 4 == EXS_JPEGProcess1
+    std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(s.c_str(), s.size()));
 
-  const DcmRepresentationParameter *p;
+    // less /home/jodogne/Downloads/dcmtk-3.6.4/dcmdata/include/dcmtk/dcmdata/dcxfer.h
+    printf(">> %d\n", dicom->getDataset()->getOriginalXfer());  // => 4 == EXS_JPEGProcess1
+
+    const DcmRepresentationParameter *p;
 
 #if 0
-  E_TransferSyntax target = EXS_LittleEndianExplicit;
-  p = NULL;
+    E_TransferSyntax target = EXS_LittleEndianExplicit;
+    p = NULL;
 #elif 1
-  E_TransferSyntax target = EXS_JPEGProcess14SV1;  
-  DJ_RPLossless rp_lossless(6, 0);
-  p = &rp_lossless;
+    E_TransferSyntax target = EXS_JPEGProcess14SV1;  
+    DJ_RPLossless rp_lossless(6, 0);
+    p = &rp_lossless;
 #else
-  E_TransferSyntax target = EXS_JPEGProcess1;
-  DJ_RPLossy rp_lossy(90);  // quality
-  p = &rp_lossy;
+    E_TransferSyntax target = EXS_JPEGProcess1;
+    DJ_RPLossy rp_lossy(90);  // quality
+    p = &rp_lossy;
 #endif 
   
-  //E_TransferSyntax target = EXS_LittleEndianImplicit;
-  
-  ASSERT_TRUE(dicom->getDataset()->chooseRepresentation(target, p).good());
-  ASSERT_TRUE(dicom->getDataset()->canWriteXfer(target));
+    ASSERT_TRUE(dicom->getDataset()->chooseRepresentation(target, p).good());
+    ASSERT_TRUE(dicom->getDataset()->canWriteXfer(target));
+
+    std::string t;
+    ASSERT_TRUE(Transcode(t, *dicom->getDataset(), target));
+
+    SystemToolbox::WriteFile(s, "source.dcm");
+    SystemToolbox::WriteFile(t, "target.dcm");
+  }
 
-  std::string t;
-  ASSERT_TRUE(Transcode(t, *dicom->getDataset(), target));
+  if (1)
+  {
+    const char* const PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes";
+    
+    for (boost::filesystem::directory_iterator it(PATH);
+         it != boost::filesystem::directory_iterator(); ++it)
+    {
+      if (boost::filesystem::is_regular_file(it->status()))
+      {
+        TestFile(it->path().string());
+      }
+    }
 
-  SystemToolbox::WriteFile(s, "source.dcm");
-  SystemToolbox::WriteFile(t, "target.dcm");
+    TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Multiframe.dcm");
+    TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Issue44/Monochrome1-Jpeg.dcm");
+  }
 }
 
 #endif
--- a/UnitTestsSources/MemoryCacheTests.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/UnitTestsSources/MemoryCacheTests.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -44,6 +44,7 @@
 #include "../Core/Cache/SharedArchive.h"
 #include "../Core/IDynamicObject.h"
 #include "../Core/Logging.h"
+#include "../OrthancServer/StorageCommitmentReports.h"
 
 
 TEST(LRU, Basic)
@@ -366,3 +367,94 @@
   ASSERT_FALSE(c.Fetch(v, "hello"));
   ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
 }
+
+
+TEST(StorageCommitmentReports, Basic)
+{
+  Orthanc::StorageCommitmentReports reports(2);
+  ASSERT_EQ(2u, reports.GetMaxSize());
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "nope");
+    ASSERT_EQ("nope", accessor.GetTransactionUid());
+    ASSERT_FALSE(accessor.IsValid());
+    ASSERT_THROW(accessor.GetReport(), Orthanc::OrthancException);
+  }
+
+  reports.Store("a", new Orthanc::StorageCommitmentReports::Report("aet_a"));
+  reports.Store("b", new Orthanc::StorageCommitmentReports::Report("aet_b"));
+  reports.Store("c", new Orthanc::StorageCommitmentReports::Report("aet_c"));
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a");
+    ASSERT_FALSE(accessor.IsValid());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b");
+    ASSERT_TRUE(accessor.IsValid());
+    ASSERT_EQ("aet_b", accessor.GetReport().GetRemoteAet());
+    ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Pending,
+              accessor.GetReport().GetStatus());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c");
+    ASSERT_EQ("aet_c", accessor.GetReport().GetRemoteAet());
+    ASSERT_TRUE(accessor.IsValid());
+  }
+
+  {
+    std::unique_ptr<Orthanc::StorageCommitmentReports::Report> report
+      (new Orthanc::StorageCommitmentReports::Report("aet"));
+    report->AddSuccess("class1", "instance1");
+    report->AddFailure("class2", "instance2",
+                       Orthanc::StorageCommitmentFailureReason_ReferencedSOPClassNotSupported);
+    report->MarkAsComplete();
+    reports.Store("a", report.release());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a");
+    ASSERT_TRUE(accessor.IsValid());
+    ASSERT_EQ("aet", accessor.GetReport().GetRemoteAet());
+    ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Failure,
+              accessor.GetReport().GetStatus());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b");
+    ASSERT_FALSE(accessor.IsValid());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c");
+    ASSERT_TRUE(accessor.IsValid());
+  }
+
+  {
+    std::unique_ptr<Orthanc::StorageCommitmentReports::Report> report
+      (new Orthanc::StorageCommitmentReports::Report("aet"));
+    report->AddSuccess("class1", "instance1");
+    report->MarkAsComplete();
+    reports.Store("a", report.release());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a");
+    ASSERT_TRUE(accessor.IsValid());
+    ASSERT_EQ("aet", accessor.GetReport().GetRemoteAet());
+    ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Success,
+              accessor.GetReport().GetStatus());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b");
+    ASSERT_FALSE(accessor.IsValid());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c");
+    ASSERT_TRUE(accessor.IsValid());
+  }
+}
--- a/UnitTestsSources/MultiThreadingTests.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -102,7 +102,7 @@
     {
     }
     
-    virtual JobStepResult Step() ORTHANC_OVERRIDE
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE
     {
       if (fails_)
       {
@@ -1046,12 +1046,12 @@
     job.AddInstance("nope");
     job.AddInstance("world");
     job.SetPermissive(true);
-    ASSERT_THROW(job.Step(), OrthancException);  // Not started yet
+    ASSERT_THROW(job.Step("jobId"), OrthancException);  // Not started yet
     ASSERT_FALSE(job.HasTrailingStep());
     ASSERT_FALSE(job.IsTrailingStepDone());
     job.Start();
-    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
-    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
 
     {
       DummyUnserializer unserializer;
@@ -1102,7 +1102,7 @@
       lock.SetTrailingOperationTimeout(300);
     }
 
-    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
 
     {
       GenericJobUnserializer unserializer;
@@ -1619,8 +1619,8 @@
 
       job.AddTrailingStep();
       job.Start();
-      ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
-      ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
+      ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+      ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
 
       study2 = job.GetTargetStudy();
       ASSERT_FALSE(study2.empty());
@@ -1678,8 +1678,8 @@
 
     job.AddTrailingStep();
     job.Start();
-    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
-    ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
 
     ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     ASSERT_TRUE(job.Serialize(s));
@@ -1747,7 +1747,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
     
-    ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
     ASSERT_EQ(1u, job.GetPosition());
     ASSERT_FALSE(job.IsTrailingStepDone());
     
@@ -1756,7 +1756,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
 
-    ASSERT_THROW(job.Step(), OrthancException);
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
   }
 
   {
@@ -1778,7 +1778,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
     
-    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
     ASSERT_EQ(1u, job.GetPosition());
     ASSERT_FALSE(job.IsTrailingStepDone());
     
@@ -1787,7 +1787,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
 
-    ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
     ASSERT_EQ(2u, job.GetPosition());
     ASSERT_FALSE(job.IsTrailingStepDone());
     
@@ -1796,7 +1796,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
 
-    ASSERT_THROW(job.Step(), OrthancException);
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
   }
 
   {
@@ -1819,7 +1819,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
     
-    ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
     ASSERT_EQ(1u, job.GetPosition());
     ASSERT_TRUE(job.IsTrailingStepDone());
     
@@ -1828,7 +1828,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
 
-    ASSERT_THROW(job.Step(), OrthancException);
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
   }
 
   {
@@ -1853,7 +1853,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
     
-    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
     ASSERT_EQ(1u, job.GetPosition());
     ASSERT_FALSE(job.IsTrailingStepDone());
     
@@ -1862,7 +1862,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
 
-    ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
     ASSERT_EQ(2u, job.GetPosition());
     ASSERT_TRUE(job.IsTrailingStepDone());
     
@@ -1871,7 +1871,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
 
-    ASSERT_THROW(job.Step(), OrthancException);
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
   }
 }
 
@@ -1898,6 +1898,8 @@
     ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Get));
     ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Store));
     ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
   }
 
   s = Json::nullValue;
@@ -1926,6 +1928,8 @@
     ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Get));
     ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Store));
     ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
   }
 
   s["Port"] = "46";
@@ -1945,8 +1949,10 @@
   operations.insert(DicomRequestType_Get);
   operations.insert(DicomRequestType_Move);
   operations.insert(DicomRequestType_Store);
+  operations.insert(DicomRequestType_NAction);
+  operations.insert(DicomRequestType_NEventReport);
 
-  ASSERT_EQ(5u, operations.size());
+  ASSERT_EQ(7u, operations.size());
 
   for (std::set<DicomRequestType>::const_iterator 
          it = operations.begin(); it != operations.end(); ++it)
@@ -1975,4 +1981,54 @@
       }
     }
   }
+
+  {
+    Json::Value s;
+    s["AllowStorageCommitment"] = false;
+    s["AET"] = "AET";
+    s["Host"] = "host";
+    s["Port"] = "104";
+    
+    RemoteModalityParameters modality(s);
+    ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
+    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("host", modality.GetHost());
+    ASSERT_EQ(104u, modality.GetPortNumber());
+    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+  }
+
+  {
+    Json::Value s;
+    s["AllowNAction"] = false;
+    s["AllowNEventReport"] = true;
+    s["AET"] = "AET";
+    s["Host"] = "host";
+    s["Port"] = "104";
+    
+    RemoteModalityParameters modality(s);
+    ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
+    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("host", modality.GetHost());
+    ASSERT_EQ(104u, modality.GetPortNumber());
+    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+  }
+
+  {
+    Json::Value s;
+    s["AllowNAction"] = true;
+    s["AllowNEventReport"] = true;
+    s["AET"] = "AET";
+    s["Host"] = "host";
+    s["Port"] = "104";
+    
+    RemoteModalityParameters modality(s);
+    ASSERT_FALSE(modality.IsAdvancedFormatNeeded());
+    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("host", modality.GetHost());
+    ASSERT_EQ(104u, modality.GetPortNumber());
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+  }
 }
--- a/UnitTestsSources/ToolboxTests.cpp	Wed Apr 01 10:14:49 2020 +0200
+++ b/UnitTestsSources/ToolboxTests.cpp	Wed Apr 01 10:15:33 2020 +0200
@@ -138,6 +138,29 @@
 #endif
 
 
+TEST(Toolbox, LargeHexadecimalToDecimal)
+{
+  // https://stackoverflow.com/a/16967286/881731
+  ASSERT_EQ(
+    "166089946137986168535368849184301740204613753693156360462575217560130904921953976324839782808018277000296027060873747803291797869684516494894741699267674246881622658654267131250470956587908385447044319923040838072975636163137212887824248575510341104029461758594855159174329892125993844566497176102668262139513",
+    Toolbox::LargeHexadecimalToDecimal("EC851A69B8ACD843164E10CFF70CF9E86DC2FEE3CF6F374B43C854E3342A2F1AC3E30C741CC41E679DF6D07CE6FA3A66083EC9B8C8BF3AF05D8BDBB0AA6Cb3ef8c5baa2a5e531ba9e28592f99e0fe4f95169a6c63f635d0197e325c5ec76219b907e4ebdcd401fb1986e4e3ca661ff73e7e2b8fd9988e753b7042b2bbca76679"));
+
+  ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal(""));
+  ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal("0"));
+  ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal("0000"));
+  ASSERT_EQ("255", Toolbox::LargeHexadecimalToDecimal("00000ff"));
+
+  ASSERT_THROW(Toolbox::LargeHexadecimalToDecimal("g"), Orthanc::OrthancException);
+}
+
+
+TEST(Toolbox, GenerateDicomPrivateUniqueIdentifier)
+{
+  std::string s = Toolbox::GenerateDicomPrivateUniqueIdentifier();
+  ASSERT_EQ("2.25.", s.substr(0, 5));
+}
+
+
 TEST(Toolbox, UniquePtr)
 {
   std::unique_ptr<int> i(new int(42));