changeset 3681:9dac85e807c2 storage-commitment

integration mainline->storage-commitment
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 20 Feb 2020 20:36:47 +0100
parents 340bdcc298e9 (diff) 453c0ece560a (current diff)
children 898903022836
files Core/DicomFormat/DicomMap.cpp NEWS OrthancServer/ServerContext.cpp OrthancServer/ServerContext.h
diffstat 53 files changed, 2827 insertions(+), 269 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Thu Feb 20 20:09:24 2020 +0100
+++ b/CMakeLists.txt	Thu Feb 20 20:36:47 2020 +0100
@@ -103,6 +103,7 @@
   OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp
   OrthancServer/ServerJobs/ResourceModificationJob.cpp
   OrthancServer/ServerJobs/SplitStudyJob.cpp
+  OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp
   OrthancServer/ServerToolbox.cpp
   OrthancServer/SliceOrdering.cpp
   )
@@ -483,9 +484,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/DicomNetworking/DicomServer.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/DicomNetworking/DicomServer.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/DicomNetworking/DicomServer.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/DicomNetworking/DicomUserConnection.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -252,7 +252,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 +275,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)
   {
@@ -994,7 +1038,7 @@
     }
   }
 
-  void DicomUserConnection::Open()
+  void DicomUserConnection::OpenInternal(Mode mode)
   {
     if (IsOpen())
     {
@@ -1034,7 +1078,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_),
@@ -1339,4 +1383,354 @@
             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::auto_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;
+        
+        if (!DIMSE_receiveCommand(pimpl_->assoc_, DIMSE_NONBLOCKING, 1, &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);
+    }
+
+    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;
+        
+        if (!DIMSE_receiveCommand(pimpl_->assoc_, DIMSE_NONBLOCKING, 1, &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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/DicomNetworking/DicomUserConnection.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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();
 
@@ -212,5 +225,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	Thu Feb 20 20:36:47 2020 +0100
@@ -0,0 +1,65 @@
+/**
+ * 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::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	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -92,9 +92,12 @@
 #include "MoveScp.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>
 
@@ -271,33 +274,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,
@@ -361,144 +337,204 @@
                 << " 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);
+        }
 
-      // 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);
-      }
-
-      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);
+        }
+
+        // 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);
+        }
+
+        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 */
@@ -700,6 +736,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;
@@ -781,6 +827,14 @@
               }
               break;
 
+            case DicomRequestType_NAction:
+              cond = NActionScp(&msg, presID);
+              break;              
+
+            case DicomRequestType_NEventReport:
+              cond = NEventReportScp(&msg, presID);
+              break;              
+
             default:
               // Should never happen
               break;
@@ -834,5 +888,352 @@
       }
       return cond;
     }
+
+
+    static DcmDataset* ReadDataset(T_ASC_Association* assoc,
+                                   const char* errorMessage)
+    {
+      DcmDataset *tmp = NULL;
+      T_ASC_PresentationContextID presIdData;
+    
+      OFCondition cond = DIMSE_receiveDataSetInMemory(
+        assoc, /*opt_blockMode*/ DIMSE_BLOCKING,
+        /*opt_dimse_timeout*/ 0, &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,
+                                DcmDataset& dataset,
+                                const DcmTagKey& tag,
+                                bool mandatory)
+    {
+      sopClassUids.clear();
+      sopInstanceUids.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());
+
+      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 request");
+        }
+
+        sopClassUids.push_back(a);
+        sopInstanceUids.push_back(b);
+      }
+    }
+
+    
+    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::auto_ptr<DcmDataset> dataset(
+        ReadDataset(assoc_, "Cannot read the dataset in N-ACTION SCP"));
+
+      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
+
+      std::vector<std::string> sopClassUid, sopInstanceUid;
+      ReadSopSequence(sopClassUid, sopInstanceUid,
+                      *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::auto_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::auto_ptr<DcmDataset> dataset(
+        ReadDataset(assoc_, "Cannot read the dataset in N-EVENT-REPORT SCP"));
+
+      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
+
+      std::vector<std::string> successSopClassUid, successSopInstanceUid;
+      ReadSopSequence(successSopClassUid, successSopInstanceUid,
+                      *dataset, DCM_ReferencedSOPSequence,
+                      (report.EventTypeID == 1) /* mandatory in the case of success */);
+
+      std::vector<std::string> failedSopClassUid, failedSopInstanceUid;
+
+      if (report.EventTypeID == 2 /* failures exist */)
+      {
+        ReadSopSequence(failedSopClassUid, failedSopInstanceUid,
+                        *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::auto_ptr<IStorageCommitmentRequestHandler> handler
+          (server_.GetStorageCommitmentRequestHandlerFactory().
+           ConstructStorageCommitmentRequestHandler());
+
+        handler->HandleReport(transactionUid, successSopClassUid, successSopInstanceUid,
+                              failedSopClassUid, failedSopInstanceUid,
+                              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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/DicomNetworking/Internals/CommandDispatcher.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/DicomNetworking/RemoteModalityParameters.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/DicomNetworking/RemoteModalityParameters.h	Thu Feb 20 20:36:47 2020 +0100
@@ -53,7 +53,9 @@
     bool                  allowFind_;
     bool                  allowMove_;
     bool                  allowGet_;
-
+    bool                  allowNAction_;
+    bool                  allowNEventReport_;
+    
     void Clear();
 
     void UnserializeArray(const Json::Value& serialized);
--- a/Core/Enumerations.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/Enumerations.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -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);
     }
--- a/Core/Enumerations.h	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/Enumerations.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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
   };
@@ -622,7 +623,9 @@
     DicomRequestType_Find,
     DicomRequestType_Get,
     DicomRequestType_Move,
-    DicomRequestType_Store
+    DicomRequestType_Store,
+    DicomRequestType_NAction,
+    DicomRequestType_NEventReport
   };
 
   enum TransferSyntax
@@ -664,6 +667,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
--- a/Core/HttpClient.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/HttpClient.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/JobsEngine/IJob.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/JobsEngine/JobsEngine.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -71,7 +71,7 @@
 
     try
     {
-      result = running.GetJob().Step();
+      result = running.GetJob().Step(running.GetId());
     }
     catch (OrthancException& e)
     {
--- a/Core/JobsEngine/JobsEngine.h	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/JobsEngine/JobsEngine.h	Thu Feb 20 20:36:47 2020 +0100
@@ -39,7 +39,7 @@
 
 namespace Orthanc
 {
-  class JobsEngine
+  class JobsEngine : public boost::noncopyable
   {
   private:
     enum State
--- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/JobsEngine/SetOfCommandsJob.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/JobsEngine/SetOfCommandsJob.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/JobsEngine/SetOfInstancesJob.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/SerializationToolbox.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/SerializationToolbox.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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/Toolbox.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/Toolbox.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -2073,6 +2073,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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Core/Toolbox.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/NEWS	Thu Feb 20 20:36:47 2020 +0100
@@ -1,6 +1,11 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* Support of DICOM storage commitment
+
 REST API
 --------
 
@@ -15,6 +20,7 @@
 -------
 
 * New sample plugin: "ConnectivityChecks"
+* New primitives to handle storage commitment SCP by plugins
 
 Lua
 ---
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -1273,7 +1273,7 @@
     if (call.ParseJsonRequest(json))
     {
       const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-      RemoteModalityParameters remote =
+      const RemoteModalityParameters remote =
         MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
       std::auto_ptr<ParsedDicomFile> query
@@ -1298,6 +1298,40 @@
   }
 
 
+  static void TestStorageCommitment(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value json;
+    if (call.ParseJsonRequest(json))
+    {
+      const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
+      const RemoteModalityParameters remote =
+        MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+      {
+        DicomUserConnection scu(localAet, remote);
+
+        std::vector<std::string> sopClassUids, sopInstanceUids;
+        sopClassUids.push_back("a");
+        sopInstanceUids.push_back("b");
+        sopClassUids.push_back("1.2.840.10008.5.1.4.1.1.6.1");
+        sopInstanceUids.push_back("1.2.840.113543.6.6.4.7.64234348190163144631511103849051737563212");
+
+        std::string t = Toolbox::GenerateDicomPrivateUniqueIdentifier();
+        scu.RequestStorageCommitment(t, sopClassUids, sopInstanceUids);
+      }
+
+      Json::Value result;
+      call.GetOutput().AnswerJson(result);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
+    }
+  }
+
+
   void OrthancRestApi::RegisterModalities()
   {
     Register("/modalities", ListModalities);
@@ -1341,5 +1375,7 @@
     Register("/peers/{id}/system", PeerSystem);
 
     Register("/modalities/{id}/find-worklist", DicomFindWorklist);
+
+    Register("/modalities/{id}/storage-commitment", TestStorageCommitment);
   }
 }
--- a/OrthancServer/ServerContext.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/OrthancServer/ServerContext.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -1062,4 +1062,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	Thu Feb 20 20:09:24 2020 +0100
+++ b/OrthancServer/ServerContext.h	Thu Feb 20 20:36:47 2020 +0100
@@ -37,6 +37,7 @@
 #include "LuaScripting.h"
 #include "OrthancHttpHandler.h"
 #include "ServerIndex.h"
+#include "ServerJobs/IStorageCommitmentFactory.h"
 
 #include "../Core/Cache/MemoryCache.h"
 
@@ -60,7 +61,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
@@ -419,5 +422,13 @@
     {
       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;
   };
 }
--- a/OrthancServer/ServerJobs/ArchiveJob.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/OrthancServer/ServerJobs/ArchiveJob.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -935,7 +935,7 @@
   }
     
 
-  JobStepResult ArchiveJob::Step()
+  JobStepResult ArchiveJob::Step(const std::string& jobId)
   {
     assert(writer_.get() != NULL);
 
--- a/OrthancServer/ServerJobs/ArchiveJob.h	Thu Feb 20 20:09:24 2020 +0100
+++ b/OrthancServer/ServerJobs/ArchiveJob.h	Thu Feb 20 20:36:47 2020 +0100
@@ -92,7 +92,7 @@
 
     virtual void Start();
 
-    virtual JobStepResult Step();
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
 
     virtual void Stop(JobStopReason reason)
     {
--- a/OrthancServer/ServerJobs/DicomMoveScuJob.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/OrthancServer/ServerJobs/DicomMoveScuJob.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -57,7 +57,7 @@
     {
     }
 
-    virtual bool Execute()
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
     {
       that_.Retrieve(*findAnswer_);
       return true;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/IStorageCommitmentFactory.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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/OrthancJobUnserializer.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:36:47 2020 +0100
@@ -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
+    {
+      return CommandType_Setup;
+    }
+    
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      that_.Setup(jobId);
+      return true;
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      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
+    {
+      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
+    {
+      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
+    {
+      return CommandType_Answer;
+    }
+    
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      that_.Answer();
+      return true;
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      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	Thu Feb 20 20:36:47 2020 +0100
@@ -0,0 +1,110 @@
+/**
+ * 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/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::auto_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;
+  };
+}
--- a/OrthancServer/main.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/OrthancServer/main.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -50,6 +50,7 @@
 #include "OrthancInitialization.h"
 #include "OrthancMoveRequestHandler.h"
 #include "ServerContext.h"
+#include "ServerJobs/StorageCommitmentScpJob.h"
 #include "ServerToolbox.h"
 
 using namespace Orthanc;
@@ -58,11 +59,11 @@
 class OrthancStoreRequestHandler : public IStoreRequestHandler
 {
 private:
-  ServerContext& server_;
+  ServerContext& context_;
 
 public:
   OrthancStoreRequestHandler(ServerContext& context) :
-    server_(context)
+    context_(context)
   {
   }
 
@@ -84,8 +85,66 @@
       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::auto_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::string& remoteIp,
+                            const std::string& remoteAet,
+                            const std::string& calledAet)
+  {
+    printf("HANDLE REPORT\n");
+
+    /**
+     * "After the N-EVENT-REPORT has been sent, the Transaction UID is
+     * no longer active and shall not be reused for other
+     * transactions."
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
+     **/
   }
 };
 
@@ -113,7 +172,8 @@
 class MyDicomServerFactory : 
   public IStoreRequestHandlerFactory,
   public IFindRequestHandlerFactory, 
-  public IMoveRequestHandlerFactory
+  public IMoveRequestHandlerFactory, 
+  public IStorageCommitmentRequestHandlerFactory
 {
 private:
   ServerContext& context_;
@@ -166,6 +226,11 @@
     return new OrthancMoveRequestHandler(context_);
   }
 
+  virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler()
+  {
+    return new OrthancStorageCommitmentRequestHandler(context_);
+  }
+
   void Done()
   {
   }
@@ -676,6 +741,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 +1036,7 @@
     dicomServer.SetStoreRequestHandlerFactory(serverFactory);
     dicomServer.SetMoveRequestHandlerFactory(serverFactory);
     dicomServer.SetFindRequestHandlerFactory(serverFactory);
+    dicomServer.SetStorageCommitmentRequestHandlerFactory(serverFactory);
 
     {
       OrthancConfiguration::ReaderLock lock;
--- a/Plugins/Engine/OrthancPlugins.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/Plugins/Engine/OrthancPlugins.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -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::auto_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_;
@@ -1260,6 +1367,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) ||
@@ -1303,6 +1411,13 @@
     {
       delete *it;
     }
+
+    for (PImpl::StorageCommitmentScpCallbacks::iterator
+           it = pimpl_->storageCommitmentScpCallbacks_.begin(); 
+         it != pimpl_->storageCommitmentScpCallbacks_.end(); ++it)
+    {
+      delete *it;
+    } 
   }
 
 
@@ -1863,6 +1978,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 = 
@@ -3882,6 +4009,10 @@
         RegisterRefreshMetricsCallback(parameters);
         return true;
 
+      case _OrthancPluginService_RegisterStorageCommitmentScpCallback:
+        RegisterStorageCommitmentScpCallback(parameters);
+        return true;
+
       case _OrthancPluginService_RegisterStorageArea:
       {
         LOG(INFO) << "Plugin has registered a custom storage area";
@@ -4539,4 +4670,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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Plugins/Engine/OrthancPlugins.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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);
@@ -341,6 +345,15 @@
                                             HttpMethod method,
                                             const UriComponents& uri,
                                             const Arguments& headers);
+
+    // 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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Plugins/Engine/PluginsEnumerations.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Plugins/Engine/PluginsEnumerations.h	Thu Feb 20 20:36:47 2020 +0100
@@ -79,6 +79,8 @@
     OrthancPluginJobStepStatus Convert(JobStepCode step);
 
     JobStepCode Convert(OrthancPluginJobStepStatus step);
+
+    StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason);
   }
 }
 
--- a/Plugins/Engine/PluginsJob.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/Plugins/Engine/PluginsJob.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Plugins/Engine/PluginsJob.h	Thu Feb 20 20:36:47 2020 +0100
@@ -55,7 +55,7 @@
     {
     }
     
-    virtual JobStepResult Step();
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
 
     virtual void Reset();
 
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Feb 20 20:09:24 2020 +0100
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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.
@@ -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,
+
+    /* 0110H: A general failure in processing the operation was
+     * encountered */
+    OrthancPluginStorageCommitmentFailureReason_ProcessingFailure = 1,
+
+    /* 0112H: One or more of the elements in the Referenced SOP
+       Instance Sequence was not available */
+    OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance = 2,
+
+    /* 0213H: The SCP does not currently have enough resources to
+       store the requested SOP Instance(s) */
+    OrthancPluginStorageCommitmentFailureReason_ResourceLimitation = 3,
+
+    /* 0122H: Storage Commitment has been requested for a SOP Instance
+       with a SOP Class that is not supported by the SCP */
+    OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 4,
+
+    /* 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_ClassInstanceConflict = 5,
+
+    /* 0131H: The Transaction UID of the Storage Commitment Request is
+       already in use */
+    OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID = 6
+  } OrthancPluginStorageCommitmentFailureReason;
+
   
 
   /**
@@ -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;
@@ -7259,6 +7299,45 @@
   }
 
 
+
+  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);
+
+  typedef void (*OrthancPluginStorageCommitmentDestructor) (void* handler);
+    
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentLookup) (
+    OrthancPluginStorageCommitmentFailureReason* target,
+    void* handler,
+    const char* sopClassUid,
+    const char* sopInstanceUid);
+    
+    
+  typedef struct
+  {
+    OrthancPluginStorageCommitmentFactory     factory;
+    OrthancPluginStorageCommitmentDestructor  destructor;
+    OrthancPluginStorageCommitmentLookup      lookup;
+  } _OrthancPluginRegisterStorageCommitmentScpCallback;
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageCommitmentScpCallback(
+    OrthancPluginContext*                     context,
+    OrthancPluginStorageCommitmentFactory     factory,
+    OrthancPluginStorageCommitmentDestructor  destructor,
+    OrthancPluginStorageCommitmentLookup      lookup)
+  {
+    _OrthancPluginRegisterStorageCommitmentScpCallback params;
+    params.factory = factory;
+    params.destructor = destructor;
+    params.lookup = lookup;
+    context->InvokeService(context, _OrthancPluginService_RegisterStorageCommitmentScpCallback, &params);
+  }
   
 #ifdef  __cplusplus
 }
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -3139,4 +3139,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	Thu Feb 20 20:09:24 2020 +0100
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Thu Feb 20 20:36:47 2020 +0100
@@ -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
@@ -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
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt	Thu Feb 20 20:36:47 2020 +0100
@@ -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	Thu Feb 20 20:36:47 2020 +0100
@@ -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/Configuration.json	Thu Feb 20 20:09:24 2020 +0100
+++ b/Resources/Configuration.json	Thu Feb 20 20:36:47 2020 +0100
@@ -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",
@@ -220,7 +220,8 @@
     //  "AllowEcho" : false,
     //  "AllowFind" : false,
     //  "AllowMove" : false,
-    //  "AllowStore" : true
+    //  "AllowStore" : true,
+    //  "AllowStorageCommitment" : false  // new in 1.6.0
     //}
   },
 
--- a/Resources/ErrorCodes.json	Thu Feb 20 20:09:24 2020 +0100
+++ b/Resources/ErrorCodes.json	Thu Feb 20 20:36:47 2020 +0100
@@ -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/UnitTestsSources/MultiThreadingTests.cpp	Thu Feb 20 20:09:24 2020 +0100
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -101,7 +101,7 @@
     {
     }
     
-    virtual JobStepResult Step() ORTHANC_OVERRIDE
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE
     {
       if (fails_)
       {
@@ -1045,12 +1045,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;
@@ -1101,7 +1101,7 @@
       lock.SetTrailingOperationTimeout(300);
     }
 
-    ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
 
     {
       GenericJobUnserializer unserializer;
@@ -1618,8 +1618,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());
@@ -1677,8 +1677,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));
@@ -1746,7 +1746,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());
     
@@ -1755,7 +1755,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
 
-    ASSERT_THROW(job.Step(), OrthancException);
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
   }
 
   {
@@ -1777,7 +1777,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());
     
@@ -1786,7 +1786,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());
     
@@ -1795,7 +1795,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
 
-    ASSERT_THROW(job.Step(), OrthancException);
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
   }
 
   {
@@ -1818,7 +1818,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());
     
@@ -1827,7 +1827,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
 
-    ASSERT_THROW(job.Step(), OrthancException);
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
   }
 
   {
@@ -1852,7 +1852,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());
     
@@ -1861,7 +1861,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());
     
@@ -1870,7 +1870,7 @@
       ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
     }
 
-    ASSERT_THROW(job.Step(), OrthancException);
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
   }
 }
 
@@ -1897,6 +1897,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;
@@ -1925,6 +1927,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";
@@ -1944,8 +1948,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)
@@ -1974,4 +1980,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	Thu Feb 20 20:09:24 2020 +0100
+++ b/UnitTestsSources/ToolboxTests.cpp	Thu Feb 20 20:36:47 2020 +0100
@@ -134,3 +134,26 @@
   printf("decoding took %zu ms\n", (std::chrono::duration_cast<std::chrono::milliseconds>(afterDecoding - afterEncoding)));
 }
 #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));
+}