diff OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp @ 4044:d25f4c0fa160 framework

splitting code into OrthancFramework and OrthancServer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 10 Jun 2020 20:30:34 +0200
parents Core/DicomNetworking/Internals/CommandDispatcher.cpp@b3f09bc9734b
children bf7b9edf6b81
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1346 @@
+/**
+ * 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/>.
+ **/
+
+
+
+
+/*=========================================================================
+
+  This file is based on portions of the following project:
+
+  Program: DCMTK 3.6.0
+  Module:  http://dicom.offis.de/dcmtk.php.en
+
+Copyright (C) 1994-2011, OFFIS e.V.
+All rights reserved.
+
+This software and supporting documentation were developed by
+
+  OFFIS e.V.
+  R&D Division Health
+  Escherweg 2
+  26121 Oldenburg, Germany
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+- Neither the name of OFFIS nor the names of its contributors may be
+  used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=========================================================================*/
+
+
+#include "../../PrecompiledHeaders.h"
+#include "CommandDispatcher.h"
+
+#if !defined(DCMTK_VERSION_NUMBER)
+#  error The macro DCMTK_VERSION_NUMBER must be defined
+#endif
+
+#include "FindScp.h"
+#include "StoreScp.h"
+#include "MoveScp.h"
+#include "GetScp.h"
+#include "../../Compatibility.h"
+#include "../../Toolbox.h"
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+
+#include <dcmtk/dcmdata/dcdeftag.h>     /* for storage commitment */
+#include <dcmtk/dcmdata/dcsequen.h>     /* for class DcmSequenceOfItems */
+#include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
+#include <dcmtk/dcmnet/dcasccfg.h>      /* for class DcmAssociationConfiguration */
+
+#include <boost/lexical_cast.hpp>
+
+  static OFBool    opt_rejectWithoutImplementationUID = OFFalse;
+
+
+
+static DUL_PRESENTATIONCONTEXT *
+findPresentationContextID(LST_HEAD * head,
+                          T_ASC_PresentationContextID presentationContextID)
+{
+  DUL_PRESENTATIONCONTEXT *pc;
+  LST_HEAD **l;
+  OFBool found = OFFalse;
+
+  if (head == NULL)
+    return NULL;
+
+  l = &head;
+  if (*l == NULL)
+    return NULL;
+
+  pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Head(l));
+  (void)LST_Position(l, OFstatic_cast(LST_NODE *, pc));
+
+  while (pc && !found) {
+    if (pc->presentationContextID == presentationContextID) {
+      found = OFTrue;
+    } else {
+      pc = OFstatic_cast(DUL_PRESENTATIONCONTEXT *, LST_Next(l));
+    }
+  }
+  return pc;
+}
+
+
+/** accept all presenstation contexts for unknown SOP classes,
+ *  i.e. UIDs appearing in the list of abstract syntaxes
+ *  where no corresponding name is defined in the UID dictionary.
+ *  @param params pointer to association parameters structure
+ *  @param transferSyntax transfer syntax to accept
+ *  @param acceptedRole SCU/SCP role to accept
+ */
+static OFCondition acceptUnknownContextsWithTransferSyntax(
+  T_ASC_Parameters * params,
+  const char* transferSyntax,
+  T_ASC_SC_ROLE acceptedRole)
+{
+  OFCondition cond = EC_Normal;
+  int n, i, k;
+  DUL_PRESENTATIONCONTEXT *dpc;
+  T_ASC_PresentationContext pc;
+  OFBool accepted = OFFalse;
+  OFBool abstractOK = OFFalse;
+
+  n = ASC_countPresentationContexts(params);
+  for (i = 0; i < n; i++)
+  {
+    cond = ASC_getPresentationContext(params, i, &pc);
+    if (cond.bad()) return cond;
+    abstractOK = OFFalse;
+    accepted = OFFalse;
+
+    if (dcmFindNameOfUID(pc.abstractSyntax) == NULL)
+    {
+      abstractOK = OFTrue;
+
+      /* check the transfer syntax */
+      for (k = 0; (k < OFstatic_cast(int, pc.transferSyntaxCount)) && !accepted; k++)
+      {
+        if (strcmp(pc.proposedTransferSyntaxes[k], transferSyntax) == 0)
+        {
+          accepted = OFTrue;
+        }
+      }
+    }
+    
+    if (accepted)
+    {
+      cond = ASC_acceptPresentationContext(
+        params, pc.presentationContextID,
+        transferSyntax, acceptedRole);
+      if (cond.bad()) return cond;
+    } else {
+      T_ASC_P_ResultReason reason;
+
+      /* do not refuse if already accepted */
+      dpc = findPresentationContextID(params->DULparams.acceptedPresentationContext,
+                                      pc.presentationContextID);
+      if ((dpc == NULL) || ((dpc != NULL) && (dpc->result != ASC_P_ACCEPTANCE)))
+      {
+
+        if (abstractOK) {
+          reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED;
+        } else {
+          reason = ASC_P_ABSTRACTSYNTAXNOTSUPPORTED;
+        }
+        /*
+         * If previously this presentation context was refused
+         * because of bad transfer syntax let it stay that way.
+         */
+        if ((dpc != NULL) && (dpc->result == ASC_P_TRANSFERSYNTAXESNOTSUPPORTED))
+          reason = ASC_P_TRANSFERSYNTAXESNOTSUPPORTED;
+
+        cond = ASC_refusePresentationContext(params, pc.presentationContextID, reason);
+        if (cond.bad()) return cond;
+      }
+    }
+  }
+  return EC_Normal;
+}
+
+
+/** accept all presenstation contexts for unknown SOP classes,
+ *  i.e. UIDs appearing in the list of abstract syntaxes
+ *  where no corresponding name is defined in the UID dictionary.
+ *  This method is passed a list of "preferred" transfer syntaxes.
+ *  @param params pointer to association parameters structure
+ *  @param transferSyntax transfer syntax to accept
+ *  @param acceptedRole SCU/SCP role to accept
+ */
+static OFCondition acceptUnknownContextsWithPreferredTransferSyntaxes(
+  T_ASC_Parameters * params,
+  const char* transferSyntaxes[], int transferSyntaxCount,
+  T_ASC_SC_ROLE acceptedRole)
+{
+  OFCondition cond = EC_Normal;
+  /*
+  ** Accept in the order "least wanted" to "most wanted" transfer
+  ** syntax.  Accepting a transfer syntax will override previously
+  ** accepted transfer syntaxes.
+  */
+  for (int i = transferSyntaxCount - 1; i >= 0; i--)
+  {
+    cond = acceptUnknownContextsWithTransferSyntax(params, transferSyntaxes[i], acceptedRole);
+    if (cond.bad()) return cond;
+  }
+  return cond;
+}
+
+
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    OFCondition AssociationCleanup(T_ASC_Association *assoc)
+    {
+      OFCondition cond = ASC_dropSCPAssociation(assoc);
+      if (cond.bad())
+      {
+        LOG(ERROR) << cond.text();
+        return cond;
+      }
+
+      cond = ASC_destroyAssociation(&assoc);
+      if (cond.bad())
+      {
+        LOG(ERROR) << cond.text();
+        return cond;
+      }
+
+      return cond;
+    }
+
+
+
+    CommandDispatcher* AcceptAssociation(const DicomServer& server, T_ASC_Network *net)
+    {
+      DcmAssociationConfiguration asccfg;
+      char buf[BUFSIZ];
+      T_ASC_Association *assoc;
+      OFCondition cond;
+      OFString sprofile;
+      OFString temp_str;
+
+      cond = ASC_receiveAssociation(net, &assoc, 
+                                    /*opt_maxPDU*/ ASC_DEFAULTMAXPDU, 
+                                    NULL, NULL,
+                                    /*opt_secureConnection*/ OFFalse,
+                                    DUL_NOBLOCK, 1);
+
+      if (cond == DUL_NOASSOCIATIONREQUEST)
+      {
+        // Timeout
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      // if some kind of error occured, take care of it
+      if (cond.bad())
+      {
+        LOG(ERROR) << "Receiving Association failed: " << cond.text();
+        // no matter what kind of error occurred, we need to do a cleanup
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      // Retrieve the AET and the IP address of the remote modality
+      std::string remoteAet;
+      std::string remoteIp;
+      std::string calledAet;
+  
+      {
+        DIC_AE remoteAet_C;
+        DIC_AE calledAet_C;
+        DIC_AE remoteIp_C;
+        DIC_AE calledIP_C;
+
+        if (
+#if DCMTK_VERSION_NUMBER >= 364
+          ASC_getAPTitles(assoc->params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).bad() ||
+          ASC_getPresentationAddresses(assoc->params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).bad()
+#else
+          ASC_getAPTitles(assoc->params, remoteAet_C, calledAet_C, NULL).bad() ||
+          ASC_getPresentationAddresses(assoc->params, remoteIp_C, calledIP_C).bad()
+#endif
+          )
+        {
+          T_ASC_RejectParameters rej =
+            {
+              ASC_RESULT_REJECTEDPERMANENT,
+              ASC_SOURCE_SERVICEUSER,
+              ASC_REASON_SU_NOREASON
+            };
+          ASC_rejectAssociation(assoc, &rej);
+          AssociationCleanup(assoc);
+          return NULL;
+        }
+
+        remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C));
+        remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C));
+        calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
+      }
+
+      LOG(INFO) << "Association Received from AET " << remoteAet 
+                << " on IP " << remoteIp;
+
+
+      {
+        /* accept the abstract syntaxes for C-ECHO, C-FIND, C-MOVE,
+           and storage commitment, if presented */
+
+        std::vector<const char*> genericTransferSyntaxes;
+        genericTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
+        genericTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
+        genericTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
+
+        std::vector<const char*> knownAbstractSyntaxes;
+
+        // 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);
+
+        // 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);
+        }
+
+        // For C-GET
+        if (server.HasGetRequestHandlerFactory())
+        {
+          knownAbstractSyntaxes.push_back(UID_GETStudyRootQueryRetrieveInformationModel);
+          knownAbstractSyntaxes.push_back(UID_GETPatientRootQueryRetrieveInformationModel);
+        }
+
+        cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
+          assoc->params,
+          &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(),
+          &genericTransferSyntaxes[0], genericTransferSyntaxes.size());
+        if (cond.bad())
+        {
+          LOG(INFO) << cond.text();
+          AssociationCleanup(assoc);
+          return NULL;
+        }
+
+      
+        /* storage commitment support, new in Orthanc 1.6.0 */
+        if (server.HasStorageCommitmentRequestHandlerFactory())
+        {
+          /**
+           * "ASC_SC_ROLE_SCUSCP": The "SCU" role is needed to accept
+           * remote storage commitment requests, and the "SCP" role is
+           * needed to receive storage commitments answers.
+           **/        
+          const char* as[1] = { UID_StorageCommitmentPushModelSOPClass }; 
+          cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
+            assoc->params, as, 1,
+            &genericTransferSyntaxes[0], genericTransferSyntaxes.size(), ASC_SC_ROLE_SCUSCP);
+          if (cond.bad())
+          {
+            LOG(INFO) << cond.text();
+            AssociationCleanup(assoc);
+            return NULL;
+          }
+        }
+      }
+      
+
+      {
+        /* accept the abstract syntaxes for C-STORE, if presented */
+
+        std::vector<const char*> storageTransferSyntaxes;
+
+        // This is the list of the transfer syntaxes that were supported up to Orthanc 0.7.1
+        storageTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
+        storageTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
+        storageTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
+
+        // New transfer syntaxes supported since Orthanc 0.7.2
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated))
+        {
+          storageTransferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); 
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg))
+        {
+          storageTransferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax);
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000))
+        {
+          storageTransferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax);
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless))
+        {
+          storageTransferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax);
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip))
+        {
+          storageTransferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax);
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2))
+        {
+          storageTransferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax);
+        }
+
+#if DCMTK_VERSION_NUMBER >= 361
+        // New in Orthanc 1.6.0
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg4))
+        {
+          storageTransferSyntaxes.push_back(UID_MPEG4BDcompatibleHighProfileLevel4_1TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_1TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For2DVideoTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For3DVideoTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG4StereoHighProfileLevel4_2TransferSyntax);
+        }
+#endif
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle))
+        {
+          storageTransferSyntaxes.push_back(UID_RLELosslessTransferSyntax);
+        }
+
+        /* the array of Storage SOP Class UIDs that is defined within "dcmdata/libsrc/dcuid.cc" */
+        size_t count = 0;
+        while (dcmAllStorageSOPClassUIDs[count] != NULL)
+        {
+          count++;
+        }
+        
+#if DCMTK_VERSION_NUMBER >= 362
+        // The global variable "numberOfDcmAllStorageSOPClassUIDs" is
+        // only published if DCMTK >= 3.6.2:
+        // https://bitbucket.org/sjodogne/orthanc/issues/137
+        assert(static_cast<int>(count) == numberOfDcmAllStorageSOPClassUIDs);
+#endif
+      
+        if (!server.HasGetRequestHandlerFactory())    // dcmqrsrv.cc line 828
+        {
+          // This branch exactly corresponds to Orthanc <= 1.6.1 (in
+          // which C-GET SCP was not supported)
+          cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
+            assoc->params, dcmAllStorageSOPClassUIDs, count,
+            &storageTransferSyntaxes[0], storageTransferSyntaxes.size());
+          if (cond.bad())
+          {
+            LOG(INFO) << cond.text();
+            AssociationCleanup(assoc);
+            return NULL;
+          }
+        }
+        else                                         // see dcmqrsrv.cc lines 839 - 876
+        {
+          /* accept storage syntaxes with proposed role */
+          int npc = ASC_countPresentationContexts(assoc->params);
+          for (int i = 0; i < npc; i++)
+          {
+            T_ASC_PresentationContext pc;
+            ASC_getPresentationContext(assoc->params, i, &pc);
+            if (dcmIsaStorageSOPClassUID(pc.abstractSyntax))
+            {
+              /**
+               * We are prepared to accept whatever role the caller
+               * proposes.  Normally we can be the SCP of the Storage
+               * Service Class.  When processing the C-GET operation
+               * we can be the SCU of the Storage Service Class.
+               **/
+              const T_ASC_SC_ROLE role = pc.proposedRole;
+            
+              /**
+               * Accept in the order "least wanted" to "most wanted"
+               * transfer syntax.  Accepting a transfer syntax will
+               * override previously accepted transfer syntaxes.
+               **/
+              for (int k = static_cast<int>(storageTransferSyntaxes.size()) - 1; k >= 0; k--)
+              {
+                for (int j = 0; j < static_cast<int>(pc.transferSyntaxCount); j++)
+                {
+                  /**
+                   * If the transfer syntax was proposed then we can accept it
+                   * appears in our supported list of transfer syntaxes
+                   **/
+                  if (strcmp(pc.proposedTransferSyntaxes[j], storageTransferSyntaxes[k]) == 0)
+                  {
+                    cond = ASC_acceptPresentationContext(
+                      assoc->params, pc.presentationContextID, storageTransferSyntaxes[k], role);
+                    if (cond.bad())
+                    {
+                      LOG(INFO) << cond.text();
+                      AssociationCleanup(assoc);
+                      return NULL;
+                    }
+                  }
+                }
+              }
+            }
+          } /* for */
+        }
+
+        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 */
+      ASC_setAPTitles(assoc->params, NULL, NULL, server.GetApplicationEntityTitle().c_str());
+
+      /* acknowledge or reject this association */
+#if DCMTK_VERSION_NUMBER >= 364
+      cond = ASC_getApplicationContextName(assoc->params, buf, sizeof(buf));
+#else
+      cond = ASC_getApplicationContextName(assoc->params, buf);
+#endif
+
+      if ((cond.bad()) || strcmp(buf, UID_StandardApplicationContext) != 0)
+      {
+        /* reject: the application context name is not supported */
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_APPCONTEXTNAMENOTSUPPORTED
+          };
+
+        LOG(INFO) << "Association Rejected: Bad Application Context Name: " << buf;
+        cond = ASC_rejectAssociation(assoc, &rej);
+        if (cond.bad())
+        {
+          LOG(INFO) << cond.text();
+        }
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      /* check the AETs */
+      if (!server.IsMyAETitle(calledAet))
+      {
+        LOG(WARNING) << "Rejected association, because of a bad called AET in the request (" << calledAet << ")";
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_CALLEDAETITLENOTRECOGNIZED
+          };
+        ASC_rejectAssociation(assoc, &rej);
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      if (server.HasApplicationEntityFilter() &&
+          !server.GetApplicationEntityFilter().IsAllowedConnection(remoteIp, remoteAet, calledAet))
+      {
+        LOG(WARNING) << "Rejected association for remote AET " << remoteAet << " on IP " << remoteIp;
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_CALLINGAETITLENOTRECOGNIZED
+          };
+        ASC_rejectAssociation(assoc, &rej);
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      if (opt_rejectWithoutImplementationUID && 
+          strlen(assoc->params->theirImplementationClassUID) == 0)
+      {
+        /* reject: the no implementation Class UID provided */
+        T_ASC_RejectParameters rej =
+          {
+            ASC_RESULT_REJECTEDPERMANENT,
+            ASC_SOURCE_SERVICEUSER,
+            ASC_REASON_SU_NOREASON
+          };
+
+        LOG(INFO) << "Association Rejected: No Implementation Class UID provided";
+        cond = ASC_rejectAssociation(assoc, &rej);
+        if (cond.bad())
+        {
+          LOG(INFO) << cond.text();
+        }
+        AssociationCleanup(assoc);
+        return NULL;
+      }
+
+      {
+        cond = ASC_acknowledgeAssociation(assoc);
+        if (cond.bad())
+        {
+          LOG(ERROR) << cond.text();
+          AssociationCleanup(assoc);
+          return NULL;
+        }
+        LOG(INFO) << "Association Acknowledged (Max Send PDV: " << assoc->sendPDVLength << ")";
+        if (ASC_countAcceptedPresentationContexts(assoc->params) == 0)
+          LOG(INFO) << "    (but no valid presentation contexts)";
+      }
+
+      IApplicationEntityFilter* filter = server.HasApplicationEntityFilter() ? &server.GetApplicationEntityFilter() : NULL;
+      return new CommandDispatcher(server, assoc, remoteIp, remoteAet, calledAet, filter);
+    }
+
+
+    CommandDispatcher::CommandDispatcher(const DicomServer& server,
+                                         T_ASC_Association* assoc,
+                                         const std::string& remoteIp,
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet,
+                                         IApplicationEntityFilter* filter) :
+      server_(server),
+      assoc_(assoc),
+      remoteIp_(remoteIp),
+      remoteAet_(remoteAet),
+      calledAet_(calledAet),
+      filter_(filter)
+    {
+      associationTimeout_ = server.GetAssociationTimeout();
+      elapsedTimeSinceLastCommand_ = 0;
+    }
+
+
+    CommandDispatcher::~CommandDispatcher()
+    {
+      try
+      {
+        AssociationCleanup(assoc_);
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Some association was not cleanly aborted";
+      }
+    }
+
+
+    bool CommandDispatcher::Step()
+    /*
+     * This function receives DIMSE commmands over the network connection
+     * and handles these commands correspondingly. Note that in case of
+     * storscp only C-ECHO-RQ and C-STORE-RQ commands can be processed.
+     */
+    {
+      bool finished = false;
+
+      // receive a DIMSE command over the network, with a timeout of 1 second
+      DcmDataset *statusDetail = NULL;
+      T_ASC_PresentationContextID presID = 0;
+      T_DIMSE_Message msg;
+
+      OFCondition cond = DIMSE_receiveCommand(assoc_, DIMSE_NONBLOCKING, 1, &presID, &msg, &statusDetail);
+      elapsedTimeSinceLastCommand_++;
+    
+      // if the command which was received has extra status
+      // detail information, dump this information
+      if (statusDetail != NULL)
+      {
+        //LOG4CPP_WARN(Internals::GetLogger(), "Status Detail:" << OFendl << DcmObject::PrintHelper(*statusDetail));
+        delete statusDetail;
+      }
+
+      if (cond == DIMSE_OUTOFRESOURCES)
+      {
+        finished = true;
+      }
+      else if (cond == DIMSE_NODATAAVAILABLE)
+      {
+        // Timeout due to DIMSE_NONBLOCKING
+        if (associationTimeout_ != 0 && 
+            elapsedTimeSinceLastCommand_ >= associationTimeout_)
+        {
+          // This timeout is actually a association timeout
+          finished = true;
+        }
+      }
+      else if (cond == EC_Normal)
+      {
+        // Reset the association timeout counter
+        elapsedTimeSinceLastCommand_ = 0;
+
+        // Convert the type of request to Orthanc's internal type
+        bool supported = false;
+        DicomRequestType request;
+        switch (msg.CommandField)
+        {
+          case DIMSE_C_ECHO_RQ:
+            request = DicomRequestType_Echo;
+            supported = true;
+            break;
+
+          case DIMSE_C_STORE_RQ:
+            request = DicomRequestType_Store;
+            supported = true;
+            break;
+
+          case DIMSE_C_MOVE_RQ:
+            request = DicomRequestType_Move;
+            supported = true;
+            break;
+            
+          case DIMSE_C_GET_RQ:
+            request = DicomRequestType_Get;
+            supported = true;
+            break;
+
+          case DIMSE_C_FIND_RQ:
+            request = DicomRequestType_Find;
+            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;
+            LOG(ERROR) << "cannot handle command: 0x" << std::hex << msg.CommandField;
+            break;
+        }
+
+
+        // Check whether this request is allowed by the security filter
+        if (supported && 
+            filter_ != NULL &&
+            !filter_->IsAllowedRequest(remoteIp_, remoteAet_, calledAet_, request))
+        {
+          LOG(WARNING) << "Rejected " << EnumerationToString(request)
+                       << " request from remote DICOM modality with AET \""
+                       << remoteAet_ << "\" and hostname \"" << remoteIp_ << "\"";
+          cond = DIMSE_ILLEGALASSOCIATION;
+          supported = false;
+          finished = true;
+        }
+
+        // in case we received a supported message, process this command
+        if (supported)
+        {
+          // If anything goes wrong, there will be a "BADCOMMANDTYPE" answer
+          cond = DIMSE_BADCOMMANDTYPE;
+
+          switch (request)
+          {
+            case DicomRequestType_Echo:
+              cond = EchoScp(assoc_, &msg, presID);
+              break;
+
+            case DicomRequestType_Store:
+              if (server_.HasStoreRequestHandlerFactory()) // Should always be true
+              {
+                std::unique_ptr<IStoreRequestHandler> handler
+                  (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler());
+
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_, associationTimeout_);
+                }
+              }
+              break;
+
+            case DicomRequestType_Move:
+              if (server_.HasMoveRequestHandlerFactory()) // Should always be true
+              {
+                std::unique_ptr<IMoveRequestHandler> handler
+                  (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler());
+
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_, associationTimeout_);
+                }
+              }
+              break;
+              
+            case DicomRequestType_Get:
+              if (server_.HasGetRequestHandlerFactory()) // Should always be true
+              {
+                std::unique_ptr<IGetRequestHandler> handler
+                  (server_.GetGetRequestHandlerFactory().ConstructGetRequestHandler());
+                
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::getScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_, associationTimeout_);
+                }
+              }
+              break;
+
+            case DicomRequestType_Find:
+              if (server_.HasFindRequestHandlerFactory() || // Should always be true
+                  server_.HasWorklistRequestHandlerFactory())
+              {
+                std::unique_ptr<IFindRequestHandler> findHandler;
+                if (server_.HasFindRequestHandlerFactory())
+                {
+                  findHandler.reset(server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler());
+                }
+
+                std::unique_ptr<IWorklistRequestHandler> worklistHandler;
+                if (server_.HasWorklistRequestHandlerFactory())
+                {
+                  worklistHandler.reset(server_.GetWorklistRequestHandlerFactory().ConstructWorklistRequestHandler());
+                }
+
+                cond = Internals::findScp(assoc_, &msg, presID, server_.GetRemoteModalities(),
+                                          findHandler.get(), worklistHandler.get(),
+                                          remoteIp_, remoteAet_, calledAet_, associationTimeout_);
+              }
+              break;
+
+            case DicomRequestType_NAction:
+              cond = NActionScp(&msg, presID);
+              break;              
+
+            case DicomRequestType_NEventReport:
+              cond = NEventReportScp(&msg, presID);
+              break;              
+
+            default:
+              // Should never happen
+              break;
+          }
+        }
+      }
+      else
+      {
+        // Bad status, which indicates the closing of the connection by
+        // the peer or a network error
+        finished = true;
+
+        LOG(INFO) << cond.text();
+      }
+    
+      if (finished)
+      {
+        if (cond == DUL_PEERREQUESTEDRELEASE)
+        {
+          LOG(INFO) << "Association Release";
+          ASC_acknowledgeRelease(assoc_);
+        }
+        else if (cond == DUL_PEERABORTEDASSOCIATION)
+        {
+          LOG(INFO) << "Association Aborted";
+        }
+        else
+        {
+          OFString temp_str;
+          LOG(INFO) << "DIMSE failure (aborting association): " << cond.text();
+          /* some kind of error so abort the association */
+          ASC_abortAssociation(assoc_);
+        }
+      }
+
+      return !finished;
+    }
+
+
+    OFCondition EchoScp(T_ASC_Association * assoc, T_DIMSE_Message * msg, T_ASC_PresentationContextID presID)
+    {
+      OFString temp_str;
+      LOG(INFO) << "Received Echo Request";
+      //LOG(DEBUG) << DIMSE_dumpMessage(temp_str, msg->msg.CEchoRQ, DIMSE_INCOMING, NULL, presID));
+
+      /* the echo succeeded !! */
+      OFCondition cond = DIMSE_sendEchoResponse(assoc, presID, &msg->msg.CEchoRQ, STATUS_Success, NULL);
+      if (cond.bad())
+      {
+        LOG(ERROR) << "Echo SCP Failed: " << cond.text();
+      }
+      return cond;
+    }
+
+
+    static DcmDataset* ReadDataset(T_ASC_Association* assoc,
+                                   const char* errorMessage,
+                                   int timeout)
+    {
+      DcmDataset *tmp = NULL;
+      T_ASC_PresentationContextID presIdData;
+    
+      OFCondition cond = DIMSE_receiveDataSetInMemory(
+        assoc, (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout,
+        &presIdData, &tmp, NULL, NULL);
+      if (!cond.good() ||
+          tmp == NULL)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, errorMessage);
+      }
+
+      return tmp;
+    }
+
+
+    static std::string ReadString(DcmDataset& dataset,
+                                  const DcmTagKey& tag)
+    {
+      const char* s = NULL;
+      if (!dataset.findAndGetString(tag, s).good() ||
+          s == NULL)
+      {
+        char buf[64];
+        sprintf(buf, "Missing mandatory tag in dataset: (%04X,%04X)",
+                tag.getGroup(), tag.getElement());
+        throw OrthancException(ErrorCode_NetworkProtocol, buf);
+      }
+
+      return std::string(s);
+    }
+
+
+    static void ReadSopSequence(
+      std::vector<std::string>& sopClassUids,
+      std::vector<std::string>& sopInstanceUids,
+      std::vector<StorageCommitmentFailureReason>* failureReasons, // Can be NULL
+      DcmDataset& dataset,
+      const DcmTagKey& tag,
+      bool mandatory)
+    {
+      sopClassUids.clear();
+      sopInstanceUids.clear();
+
+      if (failureReasons)
+      {
+        failureReasons->clear();
+      }
+
+      DcmSequenceOfItems* sequence = NULL;
+      if (!dataset.findAndGetSequence(tag, sequence).good() ||
+          sequence == NULL)
+      {
+        if (mandatory)
+        {        
+          char buf[64];
+          sprintf(buf, "Missing mandatory sequence in dataset: (%04X,%04X)",
+                  tag.getGroup(), tag.getElement());
+          throw OrthancException(ErrorCode_NetworkProtocol, buf);
+        }
+        else
+        {
+          return;
+        }
+      }
+
+      sopClassUids.reserve(sequence->card());
+      sopInstanceUids.reserve(sequence->card());
+
+      if (failureReasons)
+      {
+        failureReasons->reserve(sequence->card());
+      }
+
+      for (unsigned long i = 0; i < sequence->card(); i++)
+      {
+        const char* a = NULL;
+        const char* b = NULL;
+        if (!sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPClassUID, a).good() ||
+            !sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPInstanceUID, b).good() ||
+            a == NULL ||
+            b == NULL)
+        {
+          throw OrthancException(ErrorCode_NetworkProtocol,
+                                 "Missing Referenced SOP Class/Instance UID "
+                                 "in storage commitment dataset");
+        }
+
+        sopClassUids.push_back(a);
+        sopInstanceUids.push_back(b);
+
+        if (failureReasons != NULL)
+        {
+          Uint16 reason;
+          if (!sequence->getItem(i)->findAndGetUint16(DCM_FailureReason, reason).good())
+          {
+            throw OrthancException(ErrorCode_NetworkProtocol,
+                                   "Missing Failure Reason (0008,1197) "
+                                   "in storage commitment dataset");
+          }
+
+          failureReasons->push_back(static_cast<StorageCommitmentFailureReason>(reason));
+        }
+      }
+    }
+
+    
+    OFCondition CommandDispatcher::NActionScp(T_DIMSE_Message* msg,
+                                              T_ASC_PresentationContextID presID)
+    {
+      /**
+       * Starting with Orthanc 1.6.0, only storage commitment is
+       * supported with DICOM N-ACTION. This corresponds to the case
+       * where "Action Type ID" equals "1".
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4
+       **/
+      
+      if (msg->CommandField != DIMSE_N_ACTION_RQ /* value == 304 == 0x0130 */ ||
+          !server_.HasStorageCommitmentRequestHandlerFactory())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+
+      /**
+       * Check that the storage commitment request is correctly formatted.
+       **/
+      
+      const T_DIMSE_N_ActionRQ& request = msg->msg.NActionRQ;
+
+      if (request.ActionTypeID != 1)
+      {
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Only storage commitment is implemented for DICOM N-ACTION SCP");
+      }
+
+      if (std::string(request.RequestedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
+          std::string(request.RequestedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Unexpected incoming SOP class or instance UID for storage commitment");
+      }
+
+      if (request.DataSetType != DIMSE_DATASET_PRESENT)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Incoming storage commitment request without a dataset");
+      }
+
+
+      /**
+       * Extract the DICOM dataset that is associated with the DIMSE
+       * message. The content of this dataset is documented in "Table
+       * J.3-1. Storage Commitment Request - Action Information":
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html#table_J.3-1
+       **/
+      
+      std::unique_ptr<DcmDataset> dataset(
+        ReadDataset(assoc_, "Cannot read the dataset in N-ACTION SCP", associationTimeout_));
+
+      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
+
+      std::vector<std::string> sopClassUid, sopInstanceUid;
+      ReadSopSequence(sopClassUid, sopInstanceUid, NULL,
+                      *dataset, DCM_ReferencedSOPSequence, true /* mandatory */);
+
+      LOG(INFO) << "Incoming storage commitment request, with transaction UID: " << transactionUid;
+
+      for (size_t i = 0; i < sopClassUid.size(); i++)
+      {
+        LOG(INFO) << "  (" << (i + 1) << "/" << sopClassUid.size()
+                  << ") queried SOP Class/Instance UID: "
+                  << sopClassUid[i] << " / " << sopInstanceUid[i];
+      }
+
+
+      /**
+       * Call the Orthanc handler. The list of available DIMSE status
+       * codes can be found at:
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10
+       **/
+
+      DIC_US dimseStatus;
+  
+      try
+      {
+        std::unique_ptr<IStorageCommitmentRequestHandler> handler
+          (server_.GetStorageCommitmentRequestHandlerFactory().
+           ConstructStorageCommitmentRequestHandler());
+
+        handler->HandleRequest(transactionUid, sopClassUid, sopInstanceUid,
+                               remoteIp_, remoteAet_, calledAet_);
+        
+        dimseStatus = 0;  // Success
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Error while processing an incoming storage commitment request: " << e.What();
+
+        // Code 0x0110 - "General failure in processing the operation was encountered"
+        dimseStatus = STATUS_N_ProcessingFailure;
+      }
+
+
+      /**
+       * Send the DIMSE status back to the SCU.
+       **/
+
+      {
+        T_DIMSE_Message response;
+        memset(&response, 0, sizeof(response));
+        response.CommandField = DIMSE_N_ACTION_RSP;
+
+        T_DIMSE_N_ActionRSP& content = response.msg.NActionRSP;
+        content.MessageIDBeingRespondedTo = request.MessageID;
+        strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
+        content.DimseStatus = dimseStatus;
+        strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
+        content.ActionTypeID = 0; // Not present, as "O_NACTION_ACTIONTYPEID" not set in "opts"
+        content.DataSetType = DIMSE_DATASET_NULL;  // Dataset is absent in storage commitment response
+        content.opts = O_NACTION_AFFECTEDSOPCLASSUID | O_NACTION_AFFECTEDSOPINSTANCEUID;
+
+        return DIMSE_sendMessageUsingMemoryData(
+          assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */,
+          NULL /* callback */, NULL /* callback context */, NULL /* commandSet */);
+      }
+    }
+
+
+    OFCondition CommandDispatcher::NEventReportScp(T_DIMSE_Message* msg,
+                                                   T_ASC_PresentationContextID presID)
+    {
+      /**
+       * Starting with Orthanc 1.6.0, handling N-EVENT-REPORT for
+       * storage commitment.
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1
+       **/
+
+      if (msg->CommandField != DIMSE_N_EVENT_REPORT_RQ /* value == 256 == 0x0100 */ ||
+          !server_.HasStorageCommitmentRequestHandlerFactory())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+
+      /**
+       * Check that the storage commitment report is correctly formatted.
+       **/
+      
+      const T_DIMSE_N_EventReportRQ& report = msg->msg.NEventReportRQ;
+
+      if (report.EventTypeID != 1 /* successful */ &&
+          report.EventTypeID != 2 /* failures exist */)
+      {
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Unknown event for DICOM N-EVENT-REPORT SCP");
+      }
+
+      if (std::string(report.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
+          std::string(report.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Unexpected incoming SOP class or instance UID for storage commitment");
+      }
+
+      if (report.DataSetType != DIMSE_DATASET_PRESENT)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Incoming storage commitment report without a dataset");
+      }
+
+
+      /**
+       * Extract the DICOM dataset that is associated with the DIMSE
+       * message. The content of this dataset is documented in "Table
+       * J.3-2. Storage Commitment Result - Event Information":
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html#table_J.3-2
+       **/
+      
+      std::unique_ptr<DcmDataset> dataset(
+        ReadDataset(assoc_, "Cannot read the dataset in N-EVENT-REPORT SCP", associationTimeout_));
+
+      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
+
+      std::vector<std::string> successSopClassUid, successSopInstanceUid;
+      ReadSopSequence(successSopClassUid, successSopInstanceUid, NULL,
+                      *dataset, DCM_ReferencedSOPSequence,
+                      (report.EventTypeID == 1) /* mandatory in the case of success */);
+
+      std::vector<std::string> failedSopClassUid, failedSopInstanceUid;
+      std::vector<StorageCommitmentFailureReason> failureReasons;
+
+      if (report.EventTypeID == 2 /* failures exist */)
+      {
+        ReadSopSequence(failedSopClassUid, failedSopInstanceUid, &failureReasons,
+                        *dataset, DCM_FailedSOPSequence, true);
+      }
+
+      LOG(INFO) << "Incoming storage commitment report, with transaction UID: " << transactionUid;
+
+      for (size_t i = 0; i < successSopClassUid.size(); i++)
+      {
+        LOG(INFO) << "  (success " << (i + 1) << "/" << successSopClassUid.size()
+                  << ") SOP Class/Instance UID: "
+                  << successSopClassUid[i] << " / " << successSopInstanceUid[i];
+      }
+
+      for (size_t i = 0; i < failedSopClassUid.size(); i++)
+      {
+        LOG(INFO) << "  (failure " << (i + 1) << "/" << failedSopClassUid.size()
+                  << ") SOP Class/Instance UID: "
+                  << failedSopClassUid[i] << " / " << failedSopInstanceUid[i];
+      }
+
+      /**
+       * Call the Orthanc handler. The list of available DIMSE status
+       * codes can be found at:
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10
+       **/
+
+      DIC_US dimseStatus;
+
+      try
+      {
+        std::unique_ptr<IStorageCommitmentRequestHandler> handler
+          (server_.GetStorageCommitmentRequestHandlerFactory().
+           ConstructStorageCommitmentRequestHandler());
+
+        handler->HandleReport(transactionUid, successSopClassUid, successSopInstanceUid,
+                              failedSopClassUid, failedSopInstanceUid, failureReasons,
+                              remoteIp_, remoteAet_, calledAet_);
+        
+        dimseStatus = 0;  // Success
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Error while processing an incoming storage commitment report: " << e.What();
+
+        // Code 0x0110 - "General failure in processing the operation was encountered"
+        dimseStatus = STATUS_N_ProcessingFailure;
+      }
+
+      
+      /**
+       * Send the DIMSE status back to the SCU.
+       **/
+
+      {
+        T_DIMSE_Message response;
+        memset(&response, 0, sizeof(response));
+        response.CommandField = DIMSE_N_EVENT_REPORT_RSP;
+
+        T_DIMSE_N_EventReportRSP& content = response.msg.NEventReportRSP;
+        content.MessageIDBeingRespondedTo = report.MessageID;
+        strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
+        content.DimseStatus = dimseStatus;
+        strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
+        content.EventTypeID = 0; // Not present, as "O_NEVENTREPORT_EVENTTYPEID" not set in "opts"
+        content.DataSetType = DIMSE_DATASET_NULL;  // Dataset is absent in storage commitment response
+        content.opts = O_NEVENTREPORT_AFFECTEDSOPCLASSUID | O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID;
+
+        return DIMSE_sendMessageUsingMemoryData(
+          assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */,
+          NULL /* callback */, NULL /* callback context */, NULL /* commandSet */);
+      }
+    }
+  }
+}