changeset 3613:c1e2b91c2ab4 storage-commitment

all the abstractions for storage commitment are available
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 21 Jan 2020 17:01:46 +0100
parents 22eef03feed7
children 4543ffad256d
files Core/DicomNetworking/DicomUserConnection.cpp Core/DicomNetworking/IStorageCommitmentRequestHandler.h Core/DicomNetworking/Internals/CommandDispatcher.cpp Core/DicomNetworking/Internals/CommandDispatcher.h Core/DicomNetworking/RemoteModalityParameters.cpp Core/DicomNetworking/RemoteModalityParameters.h Core/Enumerations.cpp Core/Enumerations.h OrthancServer/OrthancRestApi/OrthancRestModalities.cpp OrthancServer/main.cpp Resources/Configuration.json UnitTestsSources/MultiThreadingTests.cpp
diffstat 12 files changed, 381 insertions(+), 101 deletions(-) [+]
line wrap: on
line diff
--- a/Core/DicomNetworking/DicomUserConnection.cpp	Tue Jan 21 14:20:50 2020 +0100
+++ b/Core/DicomNetworking/DicomUserConnection.cpp	Tue Jan 21 17:01:46 2020 +0100
@@ -1388,13 +1388,17 @@
   static void FillSopSequence(DcmDataset& dataset,
                               const DcmTagKey& tag,
                               const std::vector<std::string>& sopClassUids,
-                              const std::vector<std::string>& sopInstanceUids)
+                              const std::vector<std::string>& sopInstanceUids,
+                              bool hasFailureReason,
+                              Uint16 failureReason)
   {
     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() ||
+          (hasFailureReason &&
+           !item->putAndInsertUint16(DCM_FailureReason, failureReason).good()) ||
           !dataset.insertSequenceItem(tag, item.release()).good())
       {
         throw OrthancException(ErrorCode_InternalError);
@@ -1463,7 +1467,8 @@
           throw OrthancException(ErrorCode_InternalError);
         }
 
-        FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids, successSopInstanceUids);
+        FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids,
+                        successSopInstanceUids, false, 0);
 
         // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
         if (failureSopClassUids.empty())
@@ -1473,7 +1478,11 @@
         else
         {
           content.EventTypeID = 2;  // "Storage Commitment Request Complete - Failures Exist"
-          FillSopSequence(dataset, DCM_FailedSOPSequence, failureSopClassUids, failureSopInstanceUids);
+
+          // 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, failureSopClassUids,
+                          failureSopInstanceUids, true, 0x0112 /* No such object instance == 274 */);
         }
 
         int presID = ASC_findAcceptedPresentationContextID(
@@ -1601,7 +1610,7 @@
           throw OrthancException(ErrorCode_InternalError);
         }
 
-        FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids);
+        FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, false, 0);
 
         int presID = ASC_findAcceptedPresentationContextID(
           pimpl_->assoc_, UID_StorageCommitmentPushModelSOPClass);
--- a/Core/DicomNetworking/IStorageCommitmentRequestHandler.h	Tue Jan 21 14:20:50 2020 +0100
+++ b/Core/DicomNetworking/IStorageCommitmentRequestHandler.h	Tue Jan 21 17:01:46 2020 +0100
@@ -33,7 +33,9 @@
 
 #pragma once
 
-#include "DicomFindAnswers.h"
+#include <boost/noncopyable.hpp>
+#include <string>
+#include <vector>
 
 namespace Orthanc
 {
@@ -44,11 +46,20 @@
     {
     }
 
-    virtual void Handle(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) = 0;
+    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;
   };
 }
--- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp	Tue Jan 21 14:20:50 2020 +0100
+++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp	Tue Jan 21 17:01:46 2020 +0100
@@ -730,6 +730,11 @@
             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;
@@ -815,6 +820,10 @@
               cond = NActionScp(&msg, presID);
               break;              
 
+            case DicomRequestType_NEventReport:
+              cond = NEventReportScp(&msg, presID);
+              break;              
+
             default:
               // Should never happen
               break;
@@ -869,6 +878,80 @@
       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)
+    {
+      DcmSequenceOfItems* sequence = NULL;
+      if (!dataset.findAndGetSequence(tag, sequence).good() ||
+          sequence == NULL)
+      {
+        char buf[64];
+        sprintf(buf, "Missing mandatory sequence in dataset: (%04X,%04X)",
+                tag.getGroup(), tag.getElement());
+        throw OrthancException(ErrorCode_NetworkProtocol, buf);
+      }
+
+      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)
@@ -921,81 +1004,22 @@
        * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html#table_J.3-1
        **/
       
-      std::auto_ptr<DcmDataset> dataset;
-
-      {
-        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())
-        {
-          return cond;
-        }
+      std::auto_ptr<DcmDataset> dataset(
+        ReadDataset(assoc_, "Cannot read the dataset in N-ACTION SCP"));
 
-        if (tmp == NULL)
-        {
-          LOG(ERROR) << "Cannot read the dataset in N-ACTION SCP";
-          return EC_InvalidStream;
-        }
-
-        dataset.reset(tmp);
-      }
-
-      std::string transactionUid;
-      std::vector<std::string> referencedSopClassUid, referencedSopInstanceUid;
-
-      {
-        const char* s = NULL;
-        if (!dataset->findAndGetString(DCM_TransactionUID, s).good() ||
-            s == NULL)
-        {
-          LOG(ERROR) << "Missing Transaction UID in storage commitment request";
-          return EC_InvalidStream;
-        }
-
-        transactionUid.assign(s);
-      }
+      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
 
-      {
-        DcmSequenceOfItems* sequence = NULL;
-        if (!dataset->findAndGetSequence(DCM_ReferencedSOPSequence, sequence).good() ||
-            sequence == NULL)
-        {
-          LOG(ERROR) << "Missing Referenced SOP Sequence in storage commitment request";
-          return EC_InvalidStream;
-        }
+      std::vector<std::string> sopClassUid, sopInstanceUid;
+      ReadSopSequence(sopClassUid, sopInstanceUid,
+                      *dataset, DCM_ReferencedSOPSequence);
 
-        referencedSopClassUid.reserve(sequence->card());
-        referencedSopInstanceUid.reserve(sequence->card());
+      LOG(INFO) << "Incoming storage commitment request, with transaction UID: " << transactionUid;
 
-        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)
-          {
-            LOG(ERROR) << "Missing Referenced SOP Class/Instance UID in storage commitment request";
-            return EC_InvalidStream;
-          }
-
-          referencedSopClassUid.push_back(a);
-          referencedSopInstanceUid.push_back(b);
-        }
-      }
-
-      LOG(INFO) << "Incoming storage commitment transaction, with UID: " << transactionUid;
-
-      for (size_t i = 0; i < referencedSopClassUid.size(); i++)
+      for (size_t i = 0; i < sopClassUid.size(); i++)
       {
-        LOG(INFO) << "  (" << (i + 1) << "/" << referencedSopClassUid.size()
+        LOG(INFO) << "  (" << (i + 1) << "/" << sopClassUid.size()
                   << ") queried SOP Class/Instance UID: "
-                  << referencedSopClassUid[i] << " / " << referencedSopInstanceUid[i];
+                  << sopClassUid[i] << " / " << sopInstanceUid[i];
       }
 
 
@@ -1013,8 +1037,8 @@
           (server_.GetStorageCommitmentRequestHandlerFactory().
            ConstructStorageCommitmentRequestHandler());
 
-        handler->Handle(transactionUid, referencedSopClassUid, referencedSopInstanceUid,
-                        remoteIp_, remoteAet_, calledAet_);
+        handler->HandleRequest(transactionUid, sopClassUid, sopInstanceUid,
+                               remoteIp_, remoteAet_, calledAet_);
         
         dimseStatus = 0;  // Success
       }
@@ -1050,5 +1074,139 @@
           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);
+
+      std::vector<std::string> failedSopClassUid, failedSopInstanceUid;
+      ReadSopSequence(failedSopClassUid, failedSopInstanceUid,
+                      *dataset, DCM_FailedSOPSequence);
+
+      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	Tue Jan 21 14:20:50 2020 +0100
+++ b/Core/DicomNetworking/Internals/CommandDispatcher.h	Tue Jan 21 17:01:46 2020 +0100
@@ -58,6 +58,9 @@
 
       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,
--- a/Core/DicomNetworking/RemoteModalityParameters.cpp	Tue Jan 21 14:20:50 2020 +0100
+++ b/Core/DicomNetworking/RemoteModalityParameters.cpp	Tue Jan 21 17:01:46 2020 +0100
@@ -49,6 +49,8 @@
 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";
@@ -68,6 +70,7 @@
     allowMove_ = true;
     allowGet_ = true;
     allowNAction_ = true;  // For storage commitment
+    allowNEventReport_ = true;  // For storage commitment
   }
 
 
@@ -218,6 +221,18 @@
     {
       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;
+    }
   }
 
 
@@ -243,6 +258,9 @@
       case DicomRequestType_NAction:
         return allowNAction_;
 
+      case DicomRequestType_NEventReport:
+        return allowNEventReport_;
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -278,6 +296,10 @@
         allowNAction_ = allowed;
         break;
 
+      case DicomRequestType_NEventReport:
+        allowNEventReport_ = allowed;
+        break;
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -291,7 +313,8 @@
             !allowFind_ ||
             !allowGet_ ||
             !allowMove_ ||
-            !allowNAction_);
+            !allowNAction_ ||
+            !allowNEventReport_);
   }
 
   
@@ -312,6 +335,7 @@
       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	Tue Jan 21 14:20:50 2020 +0100
+++ b/Core/DicomNetworking/RemoteModalityParameters.h	Tue Jan 21 17:01:46 2020 +0100
@@ -54,7 +54,8 @@
     bool                  allowMove_;
     bool                  allowGet_;
     bool                  allowNAction_;
-
+    bool                  allowNEventReport_;
+    
     void Clear();
 
     void UnserializeArray(const Json::Value& serialized);
--- a/Core/Enumerations.cpp	Tue Jan 21 14:20:50 2020 +0100
+++ b/Core/Enumerations.cpp	Tue Jan 21 17:01:46 2020 +0100
@@ -864,7 +864,11 @@
         break;
 
       case DicomRequestType_NAction:
-        return "N-Action";
+        return "N-ACTION";
+        break;
+
+      case DicomRequestType_NEventReport:
+        return "N-EVENT-REPORT";
         break;
 
       default: 
--- a/Core/Enumerations.h	Tue Jan 21 14:20:50 2020 +0100
+++ b/Core/Enumerations.h	Tue Jan 21 17:01:46 2020 +0100
@@ -624,7 +624,8 @@
     DicomRequestType_Get,
     DicomRequestType_Move,
     DicomRequestType_Store,
-    DicomRequestType_NAction
+    DicomRequestType_NAction,
+    DicomRequestType_NEventReport
   };
 
   enum TransferSyntax
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Tue Jan 21 14:20:50 2020 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Tue Jan 21 17:01:46 2020 +0100
@@ -1315,6 +1315,8 @@
         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);
--- a/OrthancServer/main.cpp	Tue Jan 21 14:20:50 2020 +0100
+++ b/OrthancServer/main.cpp	Tue Jan 21 17:01:46 2020 +0100
@@ -129,16 +129,30 @@
   {
   }
 
-  virtual void Handle(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)
+  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)
   {
     // TODO - Enqueue a Storage commitment job
 
     boost::thread t(Toto, new std::string(transactionUid));
+
+    printf("HANDLE REQUEST\n");
+  }
+
+  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");
   }
 };
 
--- a/Resources/Configuration.json	Tue Jan 21 14:20:50 2020 +0100
+++ b/Resources/Configuration.json	Tue Jan 21 17:01:46 2020 +0100
@@ -204,13 +204,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,7 @@
     //  "AllowFind" : false,
     //  "AllowMove" : false,
     //  "AllowStore" : true,
-    //  "AllowNAction" : false  // Allow storage commitment (new in 1.6.0)
+    //  "AllowStorageCommitment" : false  // new in 1.6.0
     //}
   },
 
--- a/UnitTestsSources/MultiThreadingTests.cpp	Tue Jan 21 14:20:50 2020 +0100
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Tue Jan 21 17:01:46 2020 +0100
@@ -1898,6 +1898,7 @@
     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;
@@ -1927,6 +1928,7 @@
     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";
@@ -1947,8 +1949,9 @@
   operations.insert(DicomRequestType_Move);
   operations.insert(DicomRequestType_Store);
   operations.insert(DicomRequestType_NAction);
+  operations.insert(DicomRequestType_NEventReport);
 
-  ASSERT_EQ(6u, operations.size());
+  ASSERT_EQ(7u, operations.size());
 
   for (std::set<DicomRequestType>::const_iterator 
          it = operations.begin(); it != operations.end(); ++it)
@@ -1977,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));
+  }
 }