changeset 3734:4fc24b69446a storage-commitment

triggering storage commitment scu from REST API
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 10 Mar 2020 13:22:02 +0100
parents e7ff4f9b34bd
children 77183afbf55e
files Core/DicomNetworking/DicomUserConnection.cpp Core/DicomNetworking/DicomUserConnection.h NEWS OrthancServer/OrthancMoveRequestHandler.cpp OrthancServer/OrthancRestApi/OrthancRestModalities.cpp OrthancServer/ServerJobs/DicomModalityStoreJob.cpp OrthancServer/ServerJobs/DicomModalityStoreJob.h OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp OrthancServer/main.cpp
diffstat 9 files changed, 216 insertions(+), 50 deletions(-) [+]
line wrap: on
line diff
--- a/Core/DicomNetworking/DicomUserConnection.cpp	Mon Mar 09 17:19:45 2020 +0100
+++ b/Core/DicomNetworking/DicomUserConnection.cpp	Tue Mar 10 13:22:02 2020 +0100
@@ -158,7 +158,9 @@
 
     void CheckIsOpen() const;
 
-    void Store(DcmInputStream& is, 
+    void Store(std::string& sopClassUidOut  /* out */,
+               std::string& sopInstanceUidOut  /* out */,
+               DcmInputStream& is, 
                DicomUserConnection& connection,
                const std::string& moveOriginatorAET,
                uint16_t moveOriginatorID);
@@ -351,7 +353,9 @@
   }
 
 
-  void DicomUserConnection::PImpl::Store(DcmInputStream& is, 
+  void DicomUserConnection::PImpl::Store(std::string& sopClassUidOut,
+                                         std::string& sopInstanceUidOut,
+                                         DcmInputStream& is, 
                                          DicomUserConnection& connection,
                                          const std::string& moveOriginatorAET,
                                          uint16_t moveOriginatorID)
@@ -434,6 +438,9 @@
                              connection.remoteAet_);
     }
 
+    sopClassUidOut.assign(sopClass);
+    sopInstanceUidOut.assign(sopInstance);
+
     // Figure out which of the accepted presentation contexts should be used
     int presID = ASC_findAcceptedPresentationContextID(assoc_, sopClass);
     if (presID == 0)
@@ -1175,7 +1182,9 @@
     return pimpl_->IsOpen();
   }
 
-  void DicomUserConnection::Store(const char* buffer, 
+  void DicomUserConnection::Store(std::string& sopClassUid /* out */,
+                                  std::string& sopInstanceUid /* out */,
+                                  const char* buffer, 
                                   size_t size,
                                   const std::string& moveOriginatorAET,
                                   uint16_t moveOriginatorID)
@@ -1186,26 +1195,31 @@
       is.setBuffer(buffer, size);
     is.setEos();
       
-    pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID);
+    pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID);
   }
 
-  void DicomUserConnection::Store(const std::string& buffer,
+  void DicomUserConnection::Store(std::string& sopClassUid /* out */,
+                                  std::string& sopInstanceUid /* out */,
+                                  const std::string& buffer,
                                   const std::string& moveOriginatorAET,
                                   uint16_t moveOriginatorID)
   {
     if (buffer.size() > 0)
-      Store(&buffer[0], buffer.size(), moveOriginatorAET, moveOriginatorID);
+      Store(sopClassUid, sopInstanceUid, &buffer[0], buffer.size(),
+            moveOriginatorAET, moveOriginatorID);
     else
-      Store(NULL, 0, moveOriginatorAET, moveOriginatorID);
+      Store(sopClassUid, sopInstanceUid, NULL, 0, moveOriginatorAET, moveOriginatorID);
   }
 
-  void DicomUserConnection::StoreFile(const std::string& path,
+  void DicomUserConnection::StoreFile(std::string& sopClassUid /* out */,
+                                      std::string& sopInstanceUid /* out */,
+                                      const std::string& path,
                                       const std::string& moveOriginatorAET,
                                       uint16_t moveOriginatorID)
   {
     // Prepare an input stream for the file
     DcmInputFileStream is(path.c_str());
-    pimpl_->Store(is, *this, moveOriginatorAET, moveOriginatorID);
+    pimpl_->Store(sopClassUid, sopInstanceUid, is, *this, moveOriginatorAET, moveOriginatorID);
   }
 
   bool DicomUserConnection::Echo()
--- a/Core/DicomNetworking/DicomUserConnection.h	Mon Mar 09 17:19:45 2020 +0100
+++ b/Core/DicomNetworking/DicomUserConnection.h	Tue Mar 10 13:22:02 2020 +0100
@@ -158,33 +158,45 @@
 
     bool Echo();
 
-    void Store(const char* buffer, 
+    void Store(std::string& sopClassUid /* out */,
+               std::string& sopInstanceUid /* out */,
+               const char* buffer, 
                size_t size,
                const std::string& moveOriginatorAET,
                uint16_t moveOriginatorID);
 
-    void Store(const char* buffer, 
+    void Store(std::string& sopClassUid /* out */,
+               std::string& sopInstanceUid /* out */,
+               const char* buffer, 
                size_t size)
     {
-      Store(buffer, size, "", 0);  // Not a C-Move
+      Store(sopClassUid, sopInstanceUid, buffer, size, "", 0);  // Not a C-Move
     }
 
-    void Store(const std::string& buffer,
+    void Store(std::string& sopClassUid /* out */,
+               std::string& sopInstanceUid /* out */,
+               const std::string& buffer,
                const std::string& moveOriginatorAET,
                uint16_t moveOriginatorID);
 
-    void Store(const std::string& buffer)
+    void Store(std::string& sopClassUid /* out */,
+               std::string& sopInstanceUid /* out */,
+               const std::string& buffer)
     {
-      Store(buffer, "", 0);  // Not a C-Move
+      Store(sopClassUid, sopInstanceUid, buffer, "", 0);  // Not a C-Move
     }
 
-    void StoreFile(const std::string& path,
+    void StoreFile(std::string& sopClassUid /* out */,
+                   std::string& sopInstanceUid /* out */,
+                   const std::string& path,
                    const std::string& moveOriginatorAET,
                    uint16_t moveOriginatorID);
 
-    void StoreFile(const std::string& path)
+    void StoreFile(std::string& sopClassUid /* out */,
+                   std::string& sopInstanceUid /* out */,
+                   const std::string& path)
     {
-      StoreFile(path, "", 0);  // Not a C-Move
+      StoreFile(sopClassUid, sopInstanceUid, path, "", 0);  // Not a C-Move
     }
 
     void Find(DicomFindAnswers& result,
--- a/NEWS	Mon Mar 09 17:19:45 2020 +0100
+++ b/NEWS	Tue Mar 10 13:22:02 2020 +0100
@@ -10,15 +10,17 @@
 --------
 
 * API version has been upgraded to 5
-* added "/peers/{id}/system" route to test the connectivity with a remote peer (and eventually
+* Added "/peers/{id}/system" route to test the connectivity with a remote peer (and eventually
   retrieve its version number)
 * "/changes": Allow the "limit" argument to be greater than 100
 * "/instances/{id}/preview": Now takes the windowing into account
 * "/tools/log-level": Possibility to access and change the log level without restarting Orthanc
-* added "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered" routes
+* Added "/instances/{id}/frames/{frame}/rendered" and "/instances/{id}/rendered" routes
   to render frames, taking windowing and resizing into account
 * "/instances": Support "Content-Encoding: gzip" to upload gzip-compressed DICOM files
 * ".../modify" and "/tools/create-dicom": New option "PrivateCreator" for private tags
+* Added "/modalities/{...}/storage-commitment" route
+* "/modalities/{...}/store" now accepts the Boolean argument "StorageCommitment"
 
 Plugins
 -------
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Mon Mar 09 17:19:45 2020 +0100
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Tue Mar 10 13:22:02 2020 +0100
@@ -116,7 +116,8 @@
           connection_.reset(new DicomUserConnection(localAet_, remote_));
         }
 
-        connection_->Store(dicom, originatorAet_, originatorId_);
+        std::string sopClassUid, sopInstanceUid;  // Unused
+        connection_->Store(sopClassUid, sopInstanceUid, dicom, originatorAet_, originatorId_);
 
         return Status_Success;
       }
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Mon Mar 09 17:19:45 2020 +0100
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Tue Mar 10 13:22:02 2020 +0100
@@ -963,6 +963,12 @@
       job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID);
     }
 
+    // New in Orthanc 1.6.0
+    if (Toolbox::GetJsonBooleanField(request, "StorageCommitment", false))
+    {
+      job->EnableStorageCommitment(true);
+    }
+
     OrthancRestApi::GetApi(call).SubmitCommandsJob
       (call, job.release(), true /* synchronous by default */, request);
   }
@@ -1299,36 +1305,85 @@
   }
 
 
-  static void TestStorageCommitment(RestApiPostCall& call)
+  static void StorageCommitment(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     Json::Value json;
-    if (call.ParseJsonRequest(json))
+    if (call.ParseJsonRequest(json) ||
+        json.type() != Json::arrayValue)
     {
-      const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-      const RemoteModalityParameters remote =
-        MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+      std::vector<std::string> sopClassUids, sopInstanceUids;
+      sopClassUids.resize(json.size());
+      sopInstanceUids.resize(json.size());
+
+      for (Json::Value::ArrayIndex i = 0; i < json.size(); i++)
+      {
+        if (json[i].type() == Json::arrayValue)
+        {
+          if (json[i].size() != 2 ||
+              json[i][0].type() != Json::stringValue ||
+              json[i][1].type() != Json::stringValue)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat,
+                                   "An instance entry must provide an array with 2 strings: "
+                                   "SOP Class UID and SOP Instance UID");
+          }
+          else
+          {
+            sopClassUids[i] = json[i][0].asString();
+            sopInstanceUids[i] = json[i][1].asString();
+          }
+        }
+        else if (json[i].type() == Json::objectValue)
+        {
+          static const char* const SOP_CLASS_UID = "SOPClassUID";
+          static const char* const SOP_INSTANCE_UID = "SOPInstanceUID";
+
+          if (!json[i].isMember(SOP_CLASS_UID) ||
+              !json[i].isMember(SOP_INSTANCE_UID) ||
+              json[i][SOP_CLASS_UID].type() != Json::stringValue ||
+              json[i][SOP_INSTANCE_UID].type() != Json::stringValue)
+          {
+            throw OrthancException(ErrorCode_BadFileFormat,
+                                   "An instance entry must provide an object with 2 string fiels: "
+                                   "\"" + std::string(SOP_CLASS_UID) + "\" and \"" +
+                                   std::string(SOP_INSTANCE_UID));
+          }
+          else
+          {
+            sopClassUids[i] = json[i][SOP_CLASS_UID].asString();
+            sopInstanceUids[i] = json[i][SOP_INSTANCE_UID].asString();
+          }
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "JSON array or object is expected to specify one "
+                                 "instance to be queried, found: " + json[i].toStyledString());
+        }
+      }
+
+      const std::string transaction = Toolbox::GenerateDicomPrivateUniqueIdentifier();
 
       {
-        DicomUserConnection scu(localAet, remote);
+        const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
+        const RemoteModalityParameters remote =
+          MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
-        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);
+        DicomUserConnection scu(localAet, remote);
+        scu.RequestStorageCommitment(transaction, sopClassUids, sopInstanceUids);
       }
 
-      Json::Value result;
+      Json::Value result = Json::objectValue;
+      result["ID"] = transaction;
+      result["Path"] = "/storage-commitment/" + transaction;
       call.GetOutput().AnswerJson(result);
     }
     else
     {
-      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Must provide a JSON array with a list of instances");
     }
   }
 
@@ -1377,6 +1432,6 @@
 
     Register("/modalities/{id}/find-worklist", DicomFindWorklist);
 
-    Register("/modalities/{id}/storage-commitment", TestStorageCommitment);
+    Register("/modalities/{id}/storage-commitment", StorageCommitment);
   }
 }
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Mon Mar 09 17:19:45 2020 +0100
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Tue Mar 10 13:22:02 2020 +0100
@@ -72,14 +72,39 @@
       LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
       return false;
     }
+    
+    std::string sopClassUid, sopInstanceUid;
 
     if (HasMoveOriginator())
     {
-      connection_->Store(dicom, moveOriginatorAet_, moveOriginatorId_);
+      connection_->Store(sopClassUid, sopInstanceUid, dicom, moveOriginatorAet_, moveOriginatorId_);
     }
     else
     {
-      connection_->Store(dicom);
+      connection_->Store(sopClassUid, sopInstanceUid, dicom);
+    }
+
+    if (storageCommitment_)
+    {
+      sopClassUids_.push_back(sopClassUid);
+      sopInstanceUids_.push_back(sopInstanceUid);
+
+      if (sopClassUids_.size() != sopInstanceUids_.size() ||
+          sopClassUids_.size() > GetInstancesCount())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      
+      if (sopClassUids_.size() == GetInstancesCount())
+      {      
+        LOG(INFO) << "Sending storage commitment request to modality: "
+                  << remote_.GetApplicationEntityTitle();
+        
+        assert(IsStarted());
+        OpenConnection();
+        
+        connection_->RequestStorageCommitment(transactionUid_, sopClassUids_, sopInstanceUids_);
+      }
     }
 
     //boost::this_thread::sleep(boost::posix_time::milliseconds(500));
@@ -97,7 +122,8 @@
   DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) :
     context_(context),
     localAet_("ORTHANC"),
-    moveOriginatorId_(0)  // By default, not a C-MOVE
+    moveOriginatorId_(0),      // By default, not a C-MOVE
+    storageCommitment_(false)  // By default, no storage commitment
   {
   }
 
@@ -179,6 +205,44 @@
   }
 
 
+  void DicomModalityStoreJob::ResetStorageCommitment()
+  {
+    if (storageCommitment_)
+    {
+      transactionUid_ = Toolbox::GenerateDicomPrivateUniqueIdentifier();
+      sopClassUids_.reserve(GetInstancesCount());
+      sopInstanceUids_.reserve(GetInstancesCount());
+    }
+  }
+  
+
+  void DicomModalityStoreJob::Start()
+  {
+    SetOfInstancesJob::Start();
+    ResetStorageCommitment();
+  }
+
+
+  void DicomModalityStoreJob::Reset()
+  {
+    SetOfInstancesJob::Reset();
+
+    /**
+     * "After the N-EVENT-REPORT has been sent, the Transaction UID is
+     * no longer active and shall not be reused for other
+     * transactions." => Need to reset the transaction UID here
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
+     **/
+    ResetStorageCommitment();
+  }
+  
+
+  void DicomModalityStoreJob::EnableStorageCommitment(bool enabled)
+  {
+    storageCommitment_ = enabled;
+  }
+  
+
   void DicomModalityStoreJob::GetPublicContent(Json::Value& value)
   {
     SetOfInstancesJob::GetPublicContent(value);
@@ -191,6 +255,11 @@
       value["MoveOriginatorAET"] = GetMoveOriginatorAet();
       value["MoveOriginatorID"] = GetMoveOriginatorId();
     }
+
+    if (storageCommitment_)
+    {
+      value["StorageCommitmentTransactionUID"] = transactionUid_;
+    }
   }
 
 
@@ -198,6 +267,7 @@
   static const char* REMOTE = "Remote";
   static const char* MOVE_ORIGINATOR_AET = "MoveOriginatorAet";
   static const char* MOVE_ORIGINATOR_ID = "MoveOriginatorId";
+  static const char* STORAGE_COMMITMENT = "StorageCommitment";
   
 
   DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context,
@@ -210,6 +280,7 @@
     moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET);
     moveOriginatorId_ = static_cast<uint16_t>
       (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID));
+    EnableStorageCommitment(SerializationToolbox::ReadBoolean(serialized, STORAGE_COMMITMENT));
   }
 
 
@@ -225,6 +296,7 @@
       remote_.Serialize(target[REMOTE], true /* force advanced format */);
       target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_;
       target[MOVE_ORIGINATOR_ID] = moveOriginatorId_;
+      target[STORAGE_COMMITMENT] = storageCommitment_;
       return true;
     }
   }  
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.h	Mon Mar 09 17:19:45 2020 +0100
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h	Tue Mar 10 13:22:02 2020 +0100
@@ -50,9 +50,17 @@
     std::string                           moveOriginatorAet_;
     uint16_t                              moveOriginatorId_;
     std::unique_ptr<DicomUserConnection>  connection_;
+    bool                                  storageCommitment_;
+
+    // For storage commitment
+    std::string               transactionUid_;
+    std::vector<std::string>  sopInstanceUids_;
+    std::vector<std::string>  sopClassUids_;
 
     void OpenConnection();
 
+    void ResetStorageCommitment();
+
   protected:
     virtual bool HandleInstance(const std::string& instance);
     
@@ -90,7 +98,7 @@
     void SetMoveOriginator(const std::string& aet,
                            int id);
 
-    virtual void Stop(JobStopReason reason);
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
     virtual void GetJobType(std::string& target)
     {
@@ -100,5 +108,11 @@
     virtual void GetPublicContent(Json::Value& value);
 
     virtual bool Serialize(Json::Value& target);
+
+    virtual void Start() ORTHANC_OVERRIDE;
+
+    virtual void Reset() ORTHANC_OVERRIDE;
+
+    void EnableStorageCommitment(bool enabled);
   };
 }
--- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp	Mon Mar 09 17:19:45 2020 +0100
+++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp	Tue Mar 10 13:22:02 2020 +0100
@@ -70,7 +70,9 @@
     {
       std::string dicom;
       instance.ReadDicom(dicom);
-      resource->GetConnection().Store(dicom);
+
+      std::string sopClassUid, sopInstanceUid;  // Unused
+      resource->GetConnection().Store(sopClassUid, sopInstanceUid, dicom);
     }
     catch (OrthancException& e)
     {
--- a/OrthancServer/main.cpp	Mon Mar 09 17:19:45 2020 +0100
+++ b/OrthancServer/main.cpp	Tue Mar 10 13:22:02 2020 +0100
@@ -137,14 +137,8 @@
                             const std::string& remoteAet,
                             const std::string& calledAet)
   {
+    // TODO
     printf("HANDLE REPORT\n");
-
-    /**
-     * "After the N-EVENT-REPORT has been sent, the Transaction UID is
-     * no longer active and shall not be reused for other
-     * transactions."
-     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
-     **/
   }
 };