changeset 3875:ea1d32861cfc transcoding

moving timeout from DicomAssocation to DicomAssociationParameters
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 04 May 2020 14:49:31 +0200
parents 2effa961f67f
children 92ecaf877baf
files Core/DicomNetworking/DicomAssociation.cpp Core/DicomNetworking/DicomAssociationParameters.cpp Core/DicomNetworking/DicomAssociationParameters.h Core/DicomNetworking/DicomControlUserConnection.cpp Core/DicomNetworking/DicomControlUserConnection.h Core/DicomNetworking/DicomStoreUserConnection.cpp Core/DicomNetworking/DicomStoreUserConnection.h Core/DicomNetworking/TimeoutDicomConnectionManager.cpp OrthancServer/OrthancRestApi/OrthancRestModalities.cpp OrthancServer/ServerJobs/DicomModalityStoreJob.cpp OrthancServer/ServerJobs/DicomModalityStoreJob.h OrthancServer/ServerJobs/DicomMoveScuJob.cpp UnitTestsSources/MultiThreadingTests.cpp
diffstat 13 files changed, 191 insertions(+), 177 deletions(-) [+]
line wrap: on
line diff
--- a/Core/DicomNetworking/DicomAssociation.cpp	Thu Apr 30 15:00:20 2020 +0200
+++ b/Core/DicomNetworking/DicomAssociation.cpp	Mon May 04 14:49:31 2020 +0200
@@ -259,10 +259,10 @@
 
     LOG(INFO) << "Opening a DICOM SCU connection from AET \""
               << parameters.GetLocalApplicationEntityTitle() 
-              << "\" to AET \"" << parameters.GetRemoteApplicationEntityTitle()
-              << "\" on host " << parameters.GetRemoteHost()
-              << ":" << parameters.GetRemotePort() 
-              << " (manufacturer: " << EnumerationToString(parameters.GetRemoteManufacturer()) << ")";
+              << "\" to AET \"" << parameters.GetRemoteModality().GetApplicationEntityTitle()
+              << "\" on host " << parameters.GetRemoteModality().GetHost()
+              << ":" << parameters.GetRemoteModality().GetPortNumber() 
+              << " (manufacturer: " << EnumerationToString(parameters.GetRemoteModality().GetManufacturer()) << ")";
 
     CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_));
     CheckConnecting(parameters, ASC_createAssociationParameters(&params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU));
@@ -270,7 +270,7 @@
     // Set this application's title and the called application's title in the params
     CheckConnecting(parameters, ASC_setAPTitles(
                       params_, parameters.GetLocalApplicationEntityTitle().c_str(),
-                      parameters.GetRemoteApplicationEntityTitle().c_str(), NULL));
+                      parameters.GetRemoteModality().GetApplicationEntityTitle().c_str(), NULL));
 
     // Set the network addresses of the local and remote entities
     char localHost[HOST_NAME_MAX];
@@ -284,7 +284,8 @@
       snprintf
 #endif
       (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d",
-       parameters.GetRemoteHost().c_str(), parameters.GetRemotePort());
+       parameters.GetRemoteModality().GetHost().c_str(),
+       parameters.GetRemoteModality().GetPortNumber());
 
     CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort));
 
@@ -339,7 +340,7 @@
           else
           {
             LOG(WARNING) << "Unknown transfer syntax received from AET \""
-                         << parameters.GetRemoteApplicationEntityTitle()
+                         << parameters.GetRemoteModality().GetApplicationEntityTitle()
                          << "\": " << pc->acceptedTransferSyntax;
           }
         }
@@ -352,7 +353,7 @@
     {
       throw OrthancException(ErrorCode_NoPresentationContext,
                              "Unable to negotiate a presentation context with AET \"" +
-                             parameters.GetRemoteApplicationEntityTitle() + "\"");
+                             parameters.GetRemoteModality().GetApplicationEntityTitle() + "\"");
     }
   }
 
@@ -517,7 +518,7 @@
 
       throw OrthancException(ErrorCode_NetworkProtocol,
                              "DicomAssociation - " + command + " to AET \"" +
-                             parameters.GetRemoteApplicationEntityTitle() +
+                             parameters.GetRemoteModality().GetApplicationEntityTitle() +
                              "\": " + info);
     }
   }
@@ -604,7 +605,7 @@
      **/
 
     LOG(INFO) << "Reporting modality \""
-              << parameters.GetRemoteApplicationEntityTitle()
+              << parameters.GetRemoteModality().GetApplicationEntityTitle()
               << "\" about storage commitment transaction: " << transactionUid
               << " (" << successSopClassUids.size() << " successes, " 
               << failedSopClassUids.size() << " failures)";
@@ -654,7 +655,7 @@
       {
         throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
                                "Unable to send N-EVENT-REPORT request to AET: " +
-                               parameters.GetRemoteApplicationEntityTitle());
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
       }
 
       if (!DIMSE_sendMessageUsingMemoryData(
@@ -682,7 +683,7 @@
       {
         throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
                                "Unable to read N-EVENT-REPORT response from AET: " +
-                               parameters.GetRemoteApplicationEntityTitle());
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
       }
 
       const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP;
@@ -696,14 +697,14 @@
       {
         throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
                                "Badly formatted N-EVENT-REPORT response from AET: " +
-                               parameters.GetRemoteApplicationEntityTitle());
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
       }
 
       if (content.DimseStatus != 0 /* success */)
       {
         throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
                                "The request cannot be handled by remote AET: " +
-                               parameters.GetRemoteApplicationEntityTitle());
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
       }
     }
 
@@ -767,7 +768,7 @@
      **/
 
     LOG(INFO) << "Request to modality \""
-              << parameters.GetRemoteApplicationEntityTitle()
+              << parameters.GetRemoteModality().GetApplicationEntityTitle()
               << "\" about storage commitment for " << sopClassUids.size()
               << " instances, with transaction UID: " << transactionUid;
     const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++;
@@ -801,7 +802,7 @@
       {
         throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
                                "Unable to send N-ACTION request to AET: " +
-                               parameters.GetRemoteApplicationEntityTitle());
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
       }
 
       if (!DIMSE_sendMessageUsingMemoryData(
@@ -829,7 +830,7 @@
       {
         throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
                                "Unable to read N-ACTION response from AET: " +
-                               parameters.GetRemoteApplicationEntityTitle());
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
       }
 
       const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP;
@@ -843,14 +844,14 @@
       {
         throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
                                "Badly formatted N-ACTION response from AET: " +
-                               parameters.GetRemoteApplicationEntityTitle());
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
       }
 
       if (content.DimseStatus != 0 /* success */)
       {
         throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
                                "The request cannot be handled by remote AET: " +
-                               parameters.GetRemoteApplicationEntityTitle());
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
       }
     }
 
--- a/Core/DicomNetworking/DicomAssociationParameters.cpp	Thu Apr 30 15:00:20 2020 +0200
+++ b/Core/DicomNetworking/DicomAssociationParameters.cpp	Mon May 04 14:49:31 2020 +0200
@@ -37,6 +37,7 @@
 #include "../Compatibility.h"
 #include "../Logging.h"
 #include "../OrthancException.h"
+#include "../SerializationToolbox.h"
 #include "NetworkingCompatibility.h"
 
 #include <boost/thread/mutex.hpp>
@@ -48,68 +49,111 @@
 
 namespace Orthanc
 {
-  void DicomAssociationParameters::ReadDefaultTimeout()
+  void DicomAssociationParameters::CheckHost(const std::string& host)
+  {
+    if (host.size() > HOST_NAME_MAX - 10)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Invalid host name (too long): " + host);
+    }
+  }
+
+  
+  uint32_t DicomAssociationParameters::GetDefaultTimeout()
   {
     boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
-    timeout_ = defaultTimeout_;
+    return defaultTimeout_;
   }
 
 
   DicomAssociationParameters::DicomAssociationParameters() :
-    localAet_("STORESCU"),
-    remoteAet_("ANY-SCP"),
-    remoteHost_("127.0.0.1"),
-    remotePort_(104),
-    manufacturer_(ModalityManufacturer_Generic)
+    localAet_("ORTHANC"),
+    timeout_(GetDefaultTimeout())
   {
-    ReadDefaultTimeout();
+    remote_.SetApplicationEntityTitle("ANY-SCP");
   }
 
     
   DicomAssociationParameters::DicomAssociationParameters(const std::string& localAet,
                                                          const RemoteModalityParameters& remote) :
     localAet_(localAet),
-    remoteAet_(remote.GetApplicationEntityTitle()),
-    remoteHost_(remote.GetHost()),
-    remotePort_(remote.GetPortNumber()),
-    manufacturer_(remote.GetManufacturer()),
-    timeout_(defaultTimeout_)
+    timeout_(GetDefaultTimeout())
   {
-    ReadDefaultTimeout();
+    SetRemoteModality(remote);
   }
 
     
-  void DicomAssociationParameters::SetRemoteHost(const std::string& host)
+  void DicomAssociationParameters::SetRemoteModality(const RemoteModalityParameters& remote)
   {
-    if (host.size() > HOST_NAME_MAX - 10)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Invalid host name (too long): " + host);
-    }
-
-    remoteHost_ = host;
+    CheckHost(remote.GetHost());
+    remote_ = remote;
   }
 
 
-  void DicomAssociationParameters::SetRemoteModality(const RemoteModalityParameters& parameters)
+  void DicomAssociationParameters::SetRemoteHost(const std::string& host)
   {
-    SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle());
-    SetRemoteHost(parameters.GetHost());
-    SetRemotePort(parameters.GetPortNumber());
-    SetRemoteManufacturer(parameters.GetManufacturer());
+    CheckHost(host);
+    remote_.SetHost(host);
+  }
+
+
+  void DicomAssociationParameters::SetTimeout(uint32_t seconds)
+  {
+    assert(seconds != -1);
+    timeout_ = seconds;
   }
 
 
   bool DicomAssociationParameters::IsEqual(const DicomAssociationParameters& other) const
   {
     return (localAet_ == other.localAet_ &&
-            remoteAet_ == other.remoteAet_ &&
-            remoteHost_ == other.remoteHost_ &&
-            remotePort_ == other.remotePort_ &&
-            manufacturer_ == other.manufacturer_);
+            remote_.GetApplicationEntityTitle() == other.remote_.GetApplicationEntityTitle() &&
+            remote_.GetHost() == other.remote_.GetHost() &&
+            remote_.GetPortNumber() == other.remote_.GetPortNumber() &&
+            remote_.GetManufacturer() == other.remote_.GetManufacturer() &&
+            timeout_ == other.timeout_);
   }
 
+
+  static const char* const LOCAL_AET = "LocalAet";
+  static const char* const REMOTE = "Remote";
+  static const char* const TIMEOUT = "Timeout";  // New in Orthanc in 1.7.0
+
+  
+  void DicomAssociationParameters::SerializeJob(Json::Value& target) const
+  {
+    if (target.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      target[LOCAL_AET] = localAet_;
+      remote_.Serialize(target[REMOTE], true /* force advanced format */);
+      target[TIMEOUT] = timeout_;
+    }
+  }
+
+
+  DicomAssociationParameters DicomAssociationParameters::UnserializeJob(const Json::Value& serialized)
+  {
+    if (serialized.type() == Json::objectValue)
+    {
+      DicomAssociationParameters result;
     
+      result.remote_ = RemoteModalityParameters(serialized[REMOTE]);
+      result.localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET);
+      result.timeout_ = SerializationToolbox::ReadInteger(serialized, TIMEOUT, GetDefaultTimeout());
+
+      return result;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+    
+
   void DicomAssociationParameters::SetDefaultTimeout(uint32_t seconds)
   {
     LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " 
--- a/Core/DicomNetworking/DicomAssociationParameters.h	Thu Apr 30 15:00:20 2020 +0200
+++ b/Core/DicomNetworking/DicomAssociationParameters.h	Mon May 04 14:49:31 2020 +0200
@@ -35,6 +35,8 @@
 
 #include "RemoteModalityParameters.h"
 
+#include <json/value.h>
+
 class OFCondition;  // From DCMTK
 
 namespace Orthanc
@@ -42,14 +44,13 @@
   class DicomAssociationParameters
   {
   private:
-    std::string           localAet_;
-    std::string           remoteAet_;
-    std::string           remoteHost_;
-    uint16_t              remotePort_;
-    ModalityManufacturer  manufacturer_;
-    uint32_t              timeout_;
+    std::string               localAet_;
+    RemoteModalityParameters  remote_;
+    uint32_t                  timeout_;
 
-    void ReadDefaultTimeout();
+    static void CheckHost(const std::string& host);
+
+    static uint32_t GetDefaultTimeout();
 
   public:
     DicomAssociationParameters();
@@ -62,56 +63,39 @@
       return localAet_;
     }
 
-    const std::string& GetRemoteApplicationEntityTitle() const
-    {
-      return remoteAet_;
-    }
-
-    const std::string& GetRemoteHost() const
-    {
-      return remoteHost_;
-    }
-
-    uint16_t GetRemotePort() const
-    {
-      return remotePort_;
-    }
-
-    ModalityManufacturer GetRemoteManufacturer() const
-    {
-      return manufacturer_;
-    }
-
     void SetLocalApplicationEntityTitle(const std::string& aet)
     {
       localAet_ = aet;
     }
 
+    const RemoteModalityParameters& GetRemoteModality() const
+    {
+      return remote_;
+    }
+
+    void SetRemoteModality(const RemoteModalityParameters& parameters);
+    
     void SetRemoteApplicationEntityTitle(const std::string& aet)
     {
-      remoteAet_ = aet;
+      remote_.SetApplicationEntityTitle(aet);
     }
 
     void SetRemoteHost(const std::string& host);
 
     void SetRemotePort(uint16_t port)
     {
-      remotePort_ = port;
+      remote_.SetPortNumber(port);
     }
 
     void SetRemoteManufacturer(ModalityManufacturer manufacturer)
     {
-      manufacturer_ = manufacturer;
+      remote_.SetManufacturer(manufacturer);
     }
 
-    void SetRemoteModality(const RemoteModalityParameters& parameters);
-
     bool IsEqual(const DicomAssociationParameters& other) const;
 
-    void SetTimeout(uint32_t seconds)
-    {
-      timeout_ = seconds;
-    }
+    // Setting it to "0" disables the timeout (infinite wait)
+    void SetTimeout(uint32_t seconds);
 
     uint32_t GetTimeout() const
     {
@@ -122,6 +106,10 @@
     {
       return timeout_ != 0;
     }
+
+    void SerializeJob(Json::Value& target) const;
+    
+    static DicomAssociationParameters UnserializeJob(const Json::Value& serialized);
     
     static void SetDefaultTimeout(uint32_t seconds);
   };
--- a/Core/DicomNetworking/DicomControlUserConnection.cpp	Thu Apr 30 15:00:20 2020 +0200
+++ b/Core/DicomNetworking/DicomControlUserConnection.cpp	Mon May 04 14:49:31 2020 +0200
@@ -257,7 +257,7 @@
     if (presID == 0)
     {
       throw OrthancException(ErrorCode_DicomFindUnavailable,
-                             "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle());
+                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
     }
 
     T_DIMSE_C_FindRQ request;
@@ -309,14 +309,14 @@
         throw OrthancException(ErrorCode_NetworkProtocol,
                                HttpStatus_422_UnprocessableEntity,
                                "C-FIND SCU to AET \"" +
-                               parameters_.GetRemoteApplicationEntityTitle() +
+                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                                "\" has failed with DIMSE status 0x" + buf +
                                " (unable to process - invalid query ?)");
       }
       else
       {
         throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" +
-                               parameters_.GetRemoteApplicationEntityTitle() +
+                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                                "\" has failed with DIMSE status 0x" + buf);
       }
     }
@@ -331,7 +331,7 @@
     association_->Open(parameters_);
 
     std::unique_ptr<ParsedDicomFile> query(
-      ConvertQueryFields(fields, parameters_.GetRemoteManufacturer()));
+      ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer()));
     DcmDataset* dataset = query->GetDcmtkObject().getDataset();
 
     const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
@@ -362,7 +362,7 @@
     if (presID == 0)
     {
       throw OrthancException(ErrorCode_DicomMoveUnavailable,
-                             "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle());
+                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
     }
 
     T_DIMSE_C_MoveRQ request;
@@ -412,14 +412,14 @@
         throw OrthancException(ErrorCode_NetworkProtocol,
                                HttpStatus_422_UnprocessableEntity,
                                "C-MOVE SCU to AET \"" +
-                               parameters_.GetRemoteApplicationEntityTitle() +
+                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                                "\" has failed with DIMSE status 0x" + buf +
                                " (unable to process - resource not found ?)");
       }
       else
       {
         throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" +
-                               parameters_.GetRemoteApplicationEntityTitle() +
+                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
                                "\" has failed with DIMSE status 0x" + buf);
       }
     }
@@ -470,7 +470,7 @@
     {
       DicomMap fields;
       NormalizeFindQuery(fields, level, originalFields);
-      query.reset(ConvertQueryFields(fields, parameters_.GetRemoteManufacturer()));
+      query.reset(ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer()));
     }
     else
     {
@@ -516,7 +516,7 @@
 
 
     const char* universal;
-    if (parameters_.GetRemoteManufacturer() == ModalityManufacturer_GE)
+    if (parameters_.GetRemoteModality().GetManufacturer() == ModalityManufacturer_GE)
     {
       universal = "*";
     }
@@ -660,10 +660,6 @@
     MoveInternal(targetAet, ResourceType_Instance, query);
   }
 
-  void DicomControlUserConnection::SetTimeout(uint32_t seconds)
-  {
-    parameters_.SetTimeout(seconds);
-  }
 
   void DicomControlUserConnection::FindWorklist(DicomFindAnswers& result,
                                                 ParsedDicomFile& query)
--- a/Core/DicomNetworking/DicomControlUserConnection.h	Thu Apr 30 15:00:20 2020 +0200
+++ b/Core/DicomNetworking/DicomControlUserConnection.h	Mon May 04 14:49:31 2020 +0200
@@ -105,7 +105,5 @@
 
     void FindWorklist(DicomFindAnswers& result,
                       ParsedDicomFile& query);
-
-    void SetTimeout(uint32_t seconds); // 0 = no timeout
   };
 }
--- a/Core/DicomNetworking/DicomStoreUserConnection.cpp	Thu Apr 30 15:00:20 2020 +0200
+++ b/Core/DicomNetworking/DicomStoreUserConnection.cpp	Mon May 04 14:49:31 2020 +0200
@@ -179,7 +179,7 @@
     {
       throw OrthancException(ErrorCode_NoSopClassOrInstance,
                              "Unable to determine the SOP class/instance for C-STORE with AET " +
-                             parameters_.GetRemoteApplicationEntityTitle());
+                             parameters_.GetRemoteModality().GetApplicationEntityTitle());
     }
 
     sopClassUid.assign(a.c_str());
@@ -213,7 +213,7 @@
     if (association_->IsOpen())
     {
       LOG(INFO) << "Re-negociating DICOM association with "
-                << parameters_.GetRemoteApplicationEntityTitle();
+                << parameters_.GetRemoteModality().GetApplicationEntityTitle();
     }
     
     association_->ClearPresentationContexts();
@@ -312,7 +312,7 @@
                              "SOP class UID [" + sopClassUid + "] and transfer "
                              "syntax [" + GetTransferSyntaxUid(transferSyntax) + "] "
                              "while sending to modality [" +
-                             parameters_.GetRemoteApplicationEntityTitle() + "]");
+                             parameters_.GetRemoteModality().GetApplicationEntityTitle() + "]");
     }
     
     // Prepare the transmission of data
@@ -364,7 +364,7 @@
       sprintf(buf, "%04X", response.DimseStatus);
       throw OrthancException(ErrorCode_NetworkProtocol,
                              "C-STORE SCU to AET \"" +
-                             GetParameters().GetRemoteApplicationEntityTitle() +
+                             GetParameters().GetRemoteModality().GetApplicationEntityTitle() +
                              "\" has failed with DIMSE status 0x" + buf);
     }
   }
--- a/Core/DicomNetworking/DicomStoreUserConnection.h	Thu Apr 30 15:00:20 2020 +0200
+++ b/Core/DicomNetworking/DicomStoreUserConnection.h	Mon May 04 14:49:31 2020 +0200
@@ -90,11 +90,6 @@
       return parameters_;
     }
 
-    void SetTimeout(int timeout)
-    {
-      parameters_.SetTimeout(timeout);
-    }
-
     void SetCommonClassesProposed(bool proposed)
     {
       proposeCommonClasses_ = proposed;
--- a/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp	Thu Apr 30 15:00:20 2020 +0200
+++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp	Mon May 04 14:49:31 2020 +0200
@@ -103,7 +103,7 @@
     if (connection_.get() != NULL)
     {
       LOG(INFO) << "Closing inactive DICOM association with modality: "
-                << connection_->GetParameters().GetRemoteApplicationEntityTitle();
+                << connection_->GetParameters().GetRemoteModality().GetApplicationEntityTitle();
 
       connection_.reset(NULL);
     }
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Thu Apr 30 15:00:20 2020 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Mon May 04 14:49:31 2020 +0200
@@ -87,14 +87,15 @@
     try
     {
       DicomAssociationParameters params(localAet, remote);
-      DicomControlUserConnection connection(params);
 
       // New in Orthanc 1.7.0
-      if (timeout != -1)
+      if (timeout >= 0)
       {
-        connection.SetTimeout(timeout);
+        params.SetTimeout(static_cast<uint32_t>(timeout));
       }
 
+      DicomControlUserConnection connection(params);
+
       if (connection.Echo())
       {
         // Echo has succeeded
@@ -649,7 +650,11 @@
       job->SetTargetAet(targetAet);
       job->SetLocalAet(query.GetHandler().GetLocalAet());
       job->SetRemoteModality(query.GetHandler().GetRemoteModality());
-      job->SetTimeout(timeout);
+
+      if (timeout >= 0)
+      {
+        job->SetTimeout(static_cast<uint32_t>(timeout));
+      }
 
       LOG(WARNING) << "Driving C-Move SCU on remote modality "
                    << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle()
@@ -997,7 +1002,10 @@
     }
 
     // New in Orthanc 1.7.0
-    job->SetTimeout(timeout);
+    if (timeout >= 0)
+    {
+      job->SetTimeout(static_cast<uint32_t>(timeout));
+    }
 
     OrthancRestApi::GetApi(call).SubmitCommandsJob
       (call, job.release(), true /* synchronous by default */, request);
@@ -1061,13 +1069,15 @@
       MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     DicomAssociationParameters params(localAet, source);
+
+    // New in Orthanc 1.7.0
+    if (timeout >= 0)
+    {
+      params.SetTimeout(static_cast<uint32_t>(timeout));
+    }
+
     DicomControlUserConnection connection(params);
 
-    if (timeout > -1)
-    {
-      connection.SetTimeout(timeout);
-    }
-    
     for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++)
     {
       DicomMap resource;
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Thu Apr 30 15:00:20 2020 +0200
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Mon May 04 14:49:31 2020 +0200
@@ -48,13 +48,7 @@
   {
     if (connection_.get() == NULL)
     {
-      DicomAssociationParameters params(localAet_, remote_);
-      connection_.reset(new DicomStoreUserConnection(params));
-
-      if (timeout_ > -1)
-      {
-        connection_->SetTimeout(timeout_);
-      }
+      connection_.reset(new DicomStoreUserConnection(parameters_));
     }
   }
 
@@ -65,7 +59,7 @@
     OpenConnection();
 
     LOG(INFO) << "Sending instance " << instance << " to modality \"" 
-              << remote_.GetApplicationEntityTitle() << "\"";
+              << parameters_.GetRemoteModality().GetApplicationEntityTitle() << "\"";
 
     std::string dicom;
 
@@ -109,7 +103,7 @@
         assert(IsStarted());
         connection_.reset(NULL);
         
-        const std::string& remoteAet = remote_.GetApplicationEntityTitle();
+        const std::string& remoteAet = parameters_.GetRemoteModality().GetApplicationEntityTitle();
         
         LOG(INFO) << "Sending storage commitment request to modality: " << remoteAet;
 
@@ -121,8 +115,7 @@
         std::vector<std::string> a(sopClassUids_.begin(), sopClassUids_.end());
         std::vector<std::string> b(sopInstanceUids_.begin(), sopInstanceUids_.end());
 
-        DicomAssociationParameters parameters(localAet_, remote_);
-        DicomAssociation::RequestStorageCommitment(parameters, transactionUid_, a, b);
+        DicomAssociation::RequestStorageCommitment(parameters_, transactionUid_, a, b);
       }
     }
 
@@ -140,8 +133,6 @@
 
   DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) :
     context_(context),
-    localAet_("ORTHANC"),
-    timeout_(-1),
     moveOriginatorId_(0),      // By default, not a C-MOVE
     storageCommitment_(false)  // By default, no storage commitment
   {
@@ -157,7 +148,7 @@
     }
     else
     {
-      localAet_ = aet;
+      parameters_.SetLocalApplicationEntityTitle(aet);
     }
   }
 
@@ -170,11 +161,24 @@
     }
     else
     {
-      remote_ = remote;
+      parameters_.SetRemoteModality(remote);
     }
   }
 
     
+  void DicomModalityStoreJob::SetTimeout(uint32_t seconds)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetTimeout(seconds);
+    }
+  }
+
+
   const std::string& DicomModalityStoreJob::GetMoveOriginatorAet() const
   {
     if (HasMoveOriginator())
@@ -262,8 +266,8 @@
   {
     SetOfInstancesJob::GetPublicContent(value);
     
-    value["LocalAet"] = localAet_;
-    value["RemoteAet"] = remote_.GetApplicationEntityTitle();
+    value["LocalAet"] = parameters_.GetLocalApplicationEntityTitle();
+    value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
 
     if (HasMoveOriginator())
     {
@@ -278,12 +282,9 @@
   }
 
 
-  static const char* LOCAL_AET = "LocalAet";
-  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";
-  static const char* TIMEOUT = "Timeout";
   
 
   DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context,
@@ -291,15 +292,12 @@
     SetOfInstancesJob(serialized),
     context_(context)
   {
-    localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET);
-    remote_ = RemoteModalityParameters(serialized[REMOTE]);
     moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET);
     moveOriginatorId_ = static_cast<uint16_t>
       (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID));
     EnableStorageCommitment(SerializationToolbox::ReadBoolean(serialized, STORAGE_COMMITMENT));
 
-    // New in Orthanc in 1.7.0
-    timeout_ = SerializationToolbox::ReadInteger(serialized, TIMEOUT, -1);
+    parameters_ = DicomAssociationParameters::UnserializeJob(serialized);
   }
 
 
@@ -311,12 +309,10 @@
     }
     else
     {
-      target[LOCAL_AET] = localAet_;
-      remote_.Serialize(target[REMOTE], true /* force advanced format */);
+      parameters_.SerializeJob(target);
       target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_;
       target[MOVE_ORIGINATOR_ID] = moveOriginatorId_;
       target[STORAGE_COMMITMENT] = storageCommitment_;
-      target[TIMEOUT] = timeout_;
       return true;
     }
   }  
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.h	Thu Apr 30 15:00:20 2020 +0200
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h	Mon May 04 14:49:31 2020 +0200
@@ -47,9 +47,7 @@
   {
   private:
     ServerContext&                             context_;
-    std::string                                localAet_;
-    RemoteModalityParameters                   remote_;
-    int                                        timeout_;
+    DicomAssociationParameters                 parameters_;
     std::string                                moveOriginatorAet_;
     uint16_t                                   moveOriginatorId_;
     std::unique_ptr<DicomStoreUserConnection>  connection_;
@@ -75,29 +73,16 @@
     DicomModalityStoreJob(ServerContext& context,
                           const Json::Value& serialized);
 
-    const std::string& GetLocalAet() const
+    const DicomAssociationParameters& GetParameters() const
     {
-      return localAet_;
+      return parameters_;
     }
 
     void SetLocalAet(const std::string& aet);
 
-    const RemoteModalityParameters& GetRemoteModality() const
-    {
-      return remote_;
-    }
-
     void SetRemoteModality(const RemoteModalityParameters& remote);
 
-    void SetTimeout(int timeout)
-    {
-      timeout_ = timeout;
-    }
-
-    int GetTimeout() const
-    {
-      return timeout_;
-    }
+    void SetTimeout(uint32_t seconds);
 
     bool HasMoveOriginator() const
     {
--- a/OrthancServer/ServerJobs/DicomMoveScuJob.cpp	Thu Apr 30 15:00:20 2020 +0200
+++ b/OrthancServer/ServerJobs/DicomMoveScuJob.cpp	Mon May 04 14:49:31 2020 +0200
@@ -98,14 +98,15 @@
     if (connection_.get() == NULL)
     {
       DicomAssociationParameters params(localAet_, remote_);
+
+      if (timeout_ >= 0)
+      {
+        params.SetTimeout(static_cast<uint32_t>(timeout_));
+      }
+
       connection_.reset(new DicomControlUserConnection(params));
     }
     
-    if (timeout_ > -1)
-    {
-      connection_->SetTimeout(timeout_);
-    }
-
     connection_->Move(targetAet_, findAnswer);
   }
 
--- a/UnitTestsSources/MultiThreadingTests.cpp	Thu Apr 30 15:00:20 2020 +0200
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Mon May 04 14:49:31 2020 +0200
@@ -1514,11 +1514,11 @@
     job.reset(unserializer.UnserializeJob(s));
 
     DicomModalityStoreJob& tmp = dynamic_cast<DicomModalityStoreJob&>(*job);
-    ASSERT_EQ("LOCAL", tmp.GetLocalAet());
-    ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost());
-    ASSERT_EQ(1000, tmp.GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer());
+    ASSERT_EQ("LOCAL", tmp.GetParameters().GetLocalApplicationEntityTitle());
+    ASSERT_EQ("REMOTE", tmp.GetParameters().GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("192.168.1.1", tmp.GetParameters().GetRemoteModality().GetHost());
+    ASSERT_EQ(1000, tmp.GetParameters().GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetParameters().GetRemoteModality().GetManufacturer());
     ASSERT_TRUE(tmp.HasMoveOriginator());
     ASSERT_EQ("MOVESCU", tmp.GetMoveOriginatorAet());
     ASSERT_EQ(42, tmp.GetMoveOriginatorId());