changeset 3604:e327b44780bb storage-commitment

abstraction: storage commitment handler
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 16 Jan 2020 18:14:43 +0100
parents 7e303ba837d9
children 05872838ebf3
files Core/DicomNetworking/DicomServer.cpp Core/DicomNetworking/DicomServer.h Core/DicomNetworking/IStorageCommitmentRequestHandler.h Core/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h Core/DicomNetworking/Internals/CommandDispatcher.cpp Core/DicomNetworking/Internals/CommandDispatcher.h OrthancServer/main.cpp
diffstat 7 files changed, 374 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/Core/DicomNetworking/DicomServer.cpp	Thu Jan 16 18:10:57 2020 +0100
+++ b/Core/DicomNetworking/DicomServer.cpp	Thu Jan 16 18:14:43 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 Jan 16 18:10:57 2020 +0100
+++ b/Core/DicomNetworking/DicomServer.h	Thu Jan 16 18:14:43 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;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IStorageCommitmentRequestHandler.h	Thu Jan 16 18:14:43 2020 +0100
@@ -0,0 +1,54 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "DicomFindAnswers.h"
+
+namespace Orthanc
+{
+  class IStorageCommitmentRequestHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IStorageCommitmentRequestHandler()
+    {
+    }
+
+    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;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h	Thu Jan 16 18:14:43 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-2019 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 Jan 16 18:10:57 2020 +0100
+++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp	Thu Jan 16 18:14:43 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>
 
@@ -298,6 +301,12 @@
         knownAbstractSyntaxes.push_back(UID_MOVEPatientRootQueryRetrieveInformationModel);
       }
 
+      // For Storage Commitment
+      if (server.HasStorageCommitmentRequestHandlerFactory())
+      {
+        knownAbstractSyntaxes.push_back(UID_StorageCommitmentPushModelSOPClass);
+      }
+
       cond = ASC_receiveAssociation(net, &assoc, 
                                     /*opt_maxPDU*/ ASC_DEFAULTMAXPDU, 
                                     NULL, NULL,
@@ -689,6 +698,11 @@
             supported = true;
             break;
 
+          case DIMSE_N_ACTION_RQ:
+            request = DicomRequestType_NAction;
+            supported = true;
+            break;
+
           default:
             // we cannot handle this kind of message
             cond = DIMSE_BADCOMMANDTYPE;
@@ -770,6 +784,10 @@
               }
               break;
 
+            case DicomRequestType_NAction:
+              cond = NActionScp(&msg, presID);
+              break;              
+
             default:
               // Should never happen
               break;
@@ -823,5 +841,187 @@
       }
       return cond;
     }
+
+    
+    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;
+
+      {
+        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;
+        }
+
+        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);
+      }
+
+      {
+        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;
+        }
+
+        referencedSopClassUid.reserve(sequence->card());
+        referencedSopInstanceUid.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)
+          {
+            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++)
+      {
+        LOG(INFO) << "  (" << (i + 1) << "/" << referencedSopClassUid.size()
+                  << ") queried SOP Class/Instance UID: "
+                  << referencedSopClassUid[i] << " / " << referencedSopInstanceUid[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->Handle(transactionUid, referencedSopClassUid, referencedSopInstanceUid,
+                        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 */);
+      }
+    }
   }
 }
--- a/Core/DicomNetworking/Internals/CommandDispatcher.h	Thu Jan 16 18:10:57 2020 +0100
+++ b/Core/DicomNetworking/Internals/CommandDispatcher.h	Thu Jan 16 18:14:43 2020 +0100
@@ -56,6 +56,9 @@
       std::string calledAet_;
       IApplicationEntityFilter* filter_;
 
+      OFCondition NActionScp(T_DIMSE_Message* msg, 
+                             T_ASC_PresentationContextID presID);
+      
     public:
       CommandDispatcher(const DicomServer& server,
                         T_ASC_Association* assoc,
@@ -69,11 +72,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/OrthancServer/main.cpp	Thu Jan 16 18:10:57 2020 +0100
+++ b/OrthancServer/main.cpp	Thu Jan 16 18:14:43 2020 +0100
@@ -91,6 +91,30 @@
 
 
 
+class OrthancStorageCommitmentRequestHandler : public IStorageCommitmentRequestHandler
+{
+private:
+  ServerContext& server_;
+
+public:
+  OrthancStorageCommitmentRequestHandler(ServerContext& context) :
+    server_(context)
+  {
+  }
+
+  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)
+  {
+    // TODO - Enqueue a Storage commitment job
+  }
+};
+
+
+
 class ModalitiesFromConfiguration : public DicomServer::IRemoteModalities
 {
 public:
@@ -113,7 +137,8 @@
 class MyDicomServerFactory : 
   public IStoreRequestHandlerFactory,
   public IFindRequestHandlerFactory, 
-  public IMoveRequestHandlerFactory
+  public IMoveRequestHandlerFactory, 
+  public IStorageCommitmentRequestHandlerFactory
 {
 private:
   ServerContext& context_;
@@ -166,6 +191,11 @@
     return new OrthancMoveRequestHandler(context_);
   }
 
+  virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler()
+  {
+    return new OrthancStorageCommitmentRequestHandler(context_);
+  }
+
   void Done()
   {
   }
@@ -672,6 +702,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");
   }
 
@@ -966,6 +997,7 @@
     dicomServer.SetStoreRequestHandlerFactory(serverFactory);
     dicomServer.SetMoveRequestHandlerFactory(serverFactory);
     dicomServer.SetFindRequestHandlerFactory(serverFactory);
+    dicomServer.SetStorageCommitmentRequestHandlerFactory(serverFactory);
 
     {
       OrthancConfiguration::ReaderLock lock;