changeset 5876:7f4fab033c87 get-scu

RejectedSopClasses + /queries/../get to retrieve a C-FIND answer
author Alain Mazy <am@orthanc.team>
date Thu, 21 Nov 2024 12:17:47 +0100
parents 94e6a9a66109
children ed74c56db02f
files NEWS OrthancFramework/Sources/DicomFormat/DicomMap.cpp OrthancFramework/Sources/DicomFormat/DicomMap.h OrthancFramework/Sources/Toolbox.h OrthancServer/CMakeLists.txt OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h OrthancServer/Resources/Configuration.json OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp OrthancServer/Sources/ServerJobs/DicomGetScuJob.h OrthancServer/UnitTestsSources/ServerConfigTests.cpp
diffstat 15 files changed, 503 insertions(+), 84 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed Nov 20 09:58:12 2024 +0100
+++ b/NEWS	Thu Nov 21 12:17:47 2024 +0100
@@ -6,11 +6,12 @@
 
 * DICOM:
   - Added support for C-GET SCU.
-  - Added a configuration "AcceptedSopClasses" to limit the SOP classes accepted by
-    Orthanc.
+  - Added a configuration "AcceptedSopClasses" and "RejectedSopClasses" to limit 
+    the SOP classes accepted by Orthanc when acting as C-STORE SCP.
+
 
 REST API
------------
+--------
 
 * API version upgraded to 26
 * Improved parsing of multiple numerical values in DICOM tags.
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Thu Nov 21 12:17:47 2024 +0100
@@ -1300,7 +1300,22 @@
       return value->CopyToString(result, allowBinary);
     }
   }
-    
+
+  bool DicomMap::LookupStringValues(std::set<std::string>& results,
+                                    const DicomTag& tag,
+                                    bool allowBinary) const
+  {
+    std::string tmp;
+    if (LookupStringValue(tmp, tag, allowBinary))
+    {
+      Toolbox::SplitString(results, tmp, '\\');
+      return true;
+    }
+
+    return false;
+  }
+
+
   bool DicomMap::ParseInteger32(int32_t& result,
                                 const DicomTag& tag) const
   {
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Thu Nov 21 12:17:47 2024 +0100
@@ -179,7 +179,11 @@
     bool LookupStringValue(std::string& result,
                            const DicomTag& tag,
                            bool allowBinary) const;
-    
+
+    bool LookupStringValues(std::set<std::string>& results,
+                           const DicomTag& tag,
+                           bool allowBinary) const;
+
     bool ParseInteger32(int32_t& result,
                         const DicomTag& tag) const;
 
--- a/OrthancFramework/Sources/Toolbox.h	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancFramework/Sources/Toolbox.h	Thu Nov 21 12:17:47 2024 +0100
@@ -262,7 +262,7 @@
       }
     }
 
-    // returns true if all element of 'needles' are found in 'haystack'
+    // returns the elements that are both in a and b
     template <typename T> static void GetIntersection(std::set<T>& target, const std::set<T>& a, const std::set<T>& b)
     {
       target.clear();
--- a/OrthancServer/CMakeLists.txt	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancServer/CMakeLists.txt	Thu Nov 21 12:17:47 2024 +0100
@@ -176,6 +176,7 @@
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/DatabaseLookupTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/LuaServerTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp
+  ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerConfigTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerIndexTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerJobsTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/SizeOfTests.cpp
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp	Thu Nov 21 12:17:47 2024 +0100
@@ -32,7 +32,8 @@
 #include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
 
 DicomFilter::DicomFilter() :
-  hasAcceptedTransferSyntaxes_(false)
+  hasAcceptedTransferSyntaxes_(false),
+  hasAcceptedStorageClasses_(false)
 {
   {
     OrthancPlugins::OrthancConfiguration config;
@@ -41,7 +42,6 @@
     alwaysAllowMove_ = config.GetBooleanValue("DicomAlwaysAllowMove", false);
     alwaysAllowStore_ = config.GetBooleanValue("DicomAlwaysAllowStore", true);
     unknownSopClassAccepted_ = config.GetBooleanValue("UnknownSopClassAccepted", false);
-    config.LookupSetOfStrings(acceptedStorageClasses_, "AcceptedSopClasses", false);
     isStrict_ = config.GetBooleanValue("StrictAetComparison", false);
     checkModalityHost_ = config.GetBooleanValue("DicomCheckModalityHost", false);
   }
@@ -212,40 +212,43 @@
 
 void DicomFilter::GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount)
 {
-  boost::shared_lock<boost::shared_mutex>  lock(mutex_);
-  
-  if (acceptedStorageClasses_.size() >= 0)
+  boost::unique_lock<boost::shared_mutex>  lock(mutex_);
+
+  if (!hasAcceptedStorageClasses_)
   {
-    size_t count = 0;
-    std::set<std::string>::const_iterator it = acceptedStorageClasses_.begin();
+    Json::Value jsonSopClasses;
 
-    while (it != acceptedStorageClasses_.end() && (maxCount == 0 || count < maxCount))
+    if (!OrthancPlugins::RestApiGet(jsonSopClasses, "/tools/accepted-sop-classes", false) ||
+        jsonSopClasses.type() != Json::arrayValue)
     {
-      sopClasses.insert(*it);
-      count++;
-    }
-  }
-  else
-  {
-    if (maxCount != 0)
-    {
-      size_t count = 0;
-      // we actually take a list of default 120 most common storage SOP classes defined in DCMTK
-      while (dcmLongSCUStorageSOPClassUIDs[count] != NULL && count < maxCount)
-      {
-        sopClasses.insert(dcmAllStorageSOPClassUIDs[count]);
-        count++;
-      }
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
     }
     else
     {
-      size_t count = 0;
-      // we actually take all known storage SOP classes defined in DCMTK
-      while (dcmAllStorageSOPClassUIDs[count] != NULL)
+      for (Json::Value::ArrayIndex i = 0; i < jsonSopClasses.size(); i++)
       {
-        sopClasses.insert(dcmAllStorageSOPClassUIDs[count]);
-        count++;
+        if (jsonSopClasses[i].type() != Json::stringValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+        else
+        {
+          acceptedStorageClasses_.insert(jsonSopClasses[i].asString());
+        }
       }
     }
+
+    hasAcceptedStorageClasses_ = true;
   }
+
+  std::set<std::string>::const_iterator it = acceptedStorageClasses_.begin();
+    size_t count = 0;
+
+  while (it != acceptedStorageClasses_.end() && (maxCount == 0 || count < maxCount))
+  {
+    sopClasses.insert(*it);
+    count++;
+    it++;
+  }
+
 }
\ No newline at end of file
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h	Thu Nov 21 12:17:47 2024 +0100
@@ -44,6 +44,7 @@
 
   bool hasAcceptedTransferSyntaxes_;
   std::set<Orthanc::DicomTransferSyntax>  acceptedTransferSyntaxes_;
+  bool hasAcceptedStorageClasses_;
   std::set<std::string>                   acceptedStorageClasses_;
 
 public:
--- a/OrthancServer/Resources/Configuration.json	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancServer/Resources/Configuration.json	Thu Nov 21 12:17:47 2024 +0100
@@ -207,12 +207,26 @@
   // to all DCMTK storage classes in case of C-STORE SCP
   // and to a reduced list of 120 most standard storage
   // classes in case of C-GET SCU.
+  // Each entry can contain wildcards ("?" or "*") to add
+  // subsets of SOP classes that are defined in DCMTK.
+  // If you want to adda a SOP class that is not defined in
+  // DCMTK, you must add it explicitely.
   // (new in Orthanc 1.12.6)
   // "AcceptedSopClasses" : [
   //   "1.2.840.10008.5.1.4.1.1.2",
   //   "1.2.840.10008.5.1.4.1.1.4"
   // ]
 
+  // The list of rejected Storage SOP classes.
+  // This configuration is only meaningful if
+  // "AcceptedSopClasses" is using regular expressions
+  // or if it has the default value.
+  // Each entry can contain wildcards ("?" or "*").
+  // (new in Orthanc 1.12.6)
+  // "RejectedSopClasses" : [
+  //   "1.2.840.10008.5.1.4.1.1.2",
+  //   "1.2.840.10008.5.1.4.1.1.4"
+  // ]
 
   // Set the timeout (in seconds) after which the DICOM associations
   // are closed by the Orthanc SCP (server) if no further DIMSE
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Thu Nov 21 12:17:47 2024 +0100
@@ -914,6 +914,59 @@
   }
 
 
+  static void SubmitGetScuJob(RestApiPostCall& call,
+                              bool allAnswers,
+                              size_t index)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    int timeout = -1;
+    Json::Value body;
+
+    if (call.ParseJsonRequest(body))
+    {
+      timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1);
+    }
+    
+    std::unique_ptr<DicomGetScuJob> job(new DicomGetScuJob(context));
+    job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short));
+    
+    {
+      QueryAccessor query(call);
+      job->SetRemoteModality(query.GetHandler().GetRemoteModality());
+
+      if (timeout >= 0)
+      {
+        // New in Orthanc 1.7.0
+        job->SetTimeout(static_cast<uint32_t>(timeout));
+      }
+      else if (query.GetHandler().HasTimeout())
+      {
+        // New in Orthanc 1.9.1
+        job->SetTimeout(query.GetHandler().GetTimeout());
+      }
+
+      LOG(WARNING) << "Driving C-Get SCU on remote modality "
+                   << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle();
+
+      if (allAnswers)
+      {
+        for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++)
+        {
+          job->AddFindAnswer(query.GetHandler(), i);
+        }
+      }
+      else
+      {
+        job->AddFindAnswer(query.GetHandler(), index);
+      }
+    }
+
+    OrthancRestApi::GetApi(call).SubmitCommandsJob
+      (call, job.release(), true /* synchronous by default */, body);
+  }
+
+
   static void SubmitRetrieveJob(RestApiPostCall& call,
                                 bool allAnswers,
                                 size_t index)
@@ -1007,7 +1060,7 @@
     {
       DocumentRetrieveShared(call);
       call.GetDocumentation()
-        .SetSummary("Retrieve one answer")
+        .SetSummary("Retrieve one answer with a C-MOVE SCU")
         .SetDescription("Start a C-MOVE SCU command as a job, in order to retrieve one answer associated with the "
                         "query/retrieve operation whose identifiers are provided in the URL: "
                         "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move")
@@ -1020,13 +1073,49 @@
   }
 
 
+  static void RetrieveOneAnswerWithGet(RestApiPostCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      DocumentRetrieveShared(call);
+      call.GetDocumentation()
+        .SetSummary("Retrieve one answer with a C-GET SCU")
+        .SetDescription("Start a C-GET SCU command as a job, in order to retrieve one answer associated with the "
+                        "query/retrieve operation whose identifiers are provided in the URL: "
+                        "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-get")  // TODO-GET: write doc
+        .SetUriArgument("index", "Index of the answer");
+      return;
+    }
+
+    size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
+    SubmitRetrieveJob(call, false, index);
+  }
+
+
+  static void RetrieveAllAnswersWithGet(RestApiPostCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      DocumentRetrieveShared(call);
+      call.GetDocumentation()
+        .SetSummary("Retrieve all answers with C-GET SCU")
+        .SetDescription("Start a C-GET SCU command as a job, in order to retrieve all the answers associated with the "
+                        "query/retrieve operation whose identifier is provided in the URL: "
+                        "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-get");
+      return;
+    }
+
+    SubmitGetScuJob(call, true, 0);
+  }
+
+
   static void RetrieveAllAnswers(RestApiPostCall& call)
   {
     if (call.IsDocumentation())
     {
       DocumentRetrieveShared(call);
       call.GetDocumentation()
-        .SetSummary("Retrieve all answers")
+        .SetSummary("Retrieve all answers with C-MOVE SCU")
         .SetDescription("Start a C-MOVE SCU command as a job, in order to retrieve all the answers associated with the "
                         "query/retrieve operation whose identifier is provided in the URL: "
                         "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move");
@@ -2638,6 +2727,7 @@
     Register("/queries/{id}/answers/{index}", ListQueryAnswerOperations);
     Register("/queries/{id}/answers/{index}/content", GetQueryOneAnswer);
     Register("/queries/{id}/answers/{index}/retrieve", RetrieveOneAnswer);
+    Register("/queries/{id}/answers/{index}/get", RetrieveOneAnswerWithGet);
     Register("/queries/{id}/answers/{index}/query-instances",
              QueryAnswerChildren<ResourceType_Instance>);
     Register("/queries/{id}/answers/{index}/query-series",
@@ -2648,6 +2738,7 @@
     Register("/queries/{id}/modality", GetQueryModality);
     Register("/queries/{id}/query", GetQueryArguments);
     Register("/queries/{id}/retrieve", RetrieveAllAnswers);
+    Register("/queries/{id}/get", RetrieveAllAnswersWithGet);
 
     Register("/peers", ListPeers);
     Register("/peers/{id}", ListPeerOperations);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Thu Nov 21 12:17:47 2024 +0100
@@ -466,6 +466,33 @@
     AnswerAcceptedTransferSyntaxes(call);
   }
 
+  static void GetAcceptedSopClasses(RestApiGetCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("System")
+        .SetSummary("Get accepted SOPClassUID")
+        .SetDescription("Get the list of SOP Class UIDs that are accepted "
+                        "by Orthanc C-STORE SCP. This corresponds to the configuration options "
+                        "`AcceptedSopClasses` and `RejectedSopClasses`.")
+        .AddAnswerType(MimeType_Json, "JSON array containing the SOP Class UIDs");
+      return;
+    }
+
+    std::set<std::string> sopClasses;
+    OrthancRestApi::GetContext(call).GetAcceptedSopClasses(sopClasses, 0);
+    
+    Json::Value json = Json::arrayValue;
+    for (std::set<std::string>::const_iterator
+           sop = sopClasses.begin(); sop != sopClasses.end(); ++sop)
+    {
+      json.append(*sop);
+    }
+    
+    call.GetOutput().AnswerJson(json);
+  }
+
 
   static void GetUnknownSopClassAccepted(RestApiGetCall& call)
   {
@@ -1188,5 +1215,8 @@
     Register("/tools/unknown-sop-class-accepted", SetUnknownSopClassAccepted);
 
     Register("/tools/labels", ListAllLabels);  // New in Orthanc 1.12.0
+
+    Register("/tools/accepted-sop-classes", GetAcceptedSopClasses);
+
   }
 }
--- a/OrthancServer/Sources/ServerContext.cpp	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Thu Nov 21 12:17:47 2024 +0100
@@ -51,6 +51,8 @@
 #include <dcmtk/dcmnet/dimse.h>
 #include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
 
+#include <boost/regex.hpp>
+
 #if HAVE_MALLOC_TRIM == 1
 #  include <malloc.h>
 #endif
@@ -487,7 +489,11 @@
 
         isUnknownSopClassAccepted_ = lock.GetConfiguration().GetBooleanParameter("UnknownSopClassAccepted", false);
 
-        lock.GetConfiguration().GetSetOfStringsParameter(acceptedSopClasses_, "AcceptedSopClasses");
+        std::list<std::string> acceptedSopClasses;
+        std::set<std::string> rejectedSopClasses;
+        lock.GetConfiguration().GetListOfStringsParameter(acceptedSopClasses, "AcceptedSopClasses");
+        lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, "RejectSopClasses");
+        SetAcceptedSopClasses(acceptedSopClasses, rejectedSopClasses);
       }
 
       jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
@@ -2110,49 +2116,114 @@
     }
   }
 
-  void ServerContext::SetAcceptedSopClasses(const std::set<std::string>& sopClasses)
+  void ServerContext::SetAcceptedSopClasses(const std::list<std::string>& acceptedSopClasses,
+                                            const std::set<std::string>& rejectedSopClasses)
   {
     boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
-    acceptedSopClasses_ = sopClasses;
+    acceptedSopClasses_.clear();
+
+    size_t count = 0;
+    std::set<std::string> allDcmtkSopClassUids;
+    std::set<std::string> shortDcmtkSopClassUids;
+
+    // we actually take a list of default 120 most common storage SOP classes defined in DCMTK
+    while (dcmLongSCUStorageSOPClassUIDs[count] != NULL)
+    {
+      shortDcmtkSopClassUids.insert(dcmLongSCUStorageSOPClassUIDs[count++]);
+    }
+
+    count = 0;
+    while (dcmAllStorageSOPClassUIDs[count] != NULL)
+    {
+      allDcmtkSopClassUids.insert(dcmAllStorageSOPClassUIDs[count++]);
+    }
+
+    if (acceptedSopClasses.size() == 0)
+    {
+      // by default, include the short list first and then all the others
+      for (std::set<std::string>::const_iterator it = shortDcmtkSopClassUids.begin(); it != shortDcmtkSopClassUids.end(); ++it)
+      {
+        acceptedSopClasses_.push_back(*it);
+      }
+
+      for (std::set<std::string>::const_iterator it = allDcmtkSopClassUids.begin(); it != allDcmtkSopClassUids.end(); ++it)
+      {
+        if (shortDcmtkSopClassUids.find(*it) == shortDcmtkSopClassUids.end()) // don't add the classes that we have already added
+        {
+          acceptedSopClasses_.push_back(*it);
+        }
+      }
+    }
+    else
+    {
+      std::set<std::string> addedSopClasses;
+
+      for (std::list<std::string>::const_iterator it = acceptedSopClasses.begin(); it != acceptedSopClasses.end(); ++it)
+      {
+        if (it->find('*') != std::string::npos || it->find('?') != std::string::npos)
+        {
+          // if it contains wildcard, add all the matching SOP classes known by DCMTK
+          boost::regex pattern(Toolbox::WildcardToRegularExpression(*it));
+
+          for (std::set<std::string>::const_iterator itall = allDcmtkSopClassUids.begin(); itall != allDcmtkSopClassUids.end(); ++itall)
+          {
+            if (regex_match(*itall, pattern) && addedSopClasses.find(*itall) == addedSopClasses.end())
+            {
+              acceptedSopClasses_.push_back(*itall);
+              addedSopClasses.insert(*itall);
+            }
+          }
+        }
+        else
+        {
+          // if it is a SOP Class UID, add it without checking if it is known by DCMTK
+          acceptedSopClasses_.push_back(*it);
+          addedSopClasses.insert(*it);
+        }
+      }
+    }
+    
+    // now remove all rejected syntaxes
+    if (rejectedSopClasses.size() > 0)
+    {
+      for (std::set<std::string>::const_iterator it = rejectedSopClasses.begin(); it != rejectedSopClasses.end(); ++it)
+      {
+        if (it->find('*') != std::string::npos || it->find('?') != std::string::npos)
+        {
+          // if it contains wildcard, get all the matching SOP classes known by DCMTK
+          boost::regex pattern(Toolbox::WildcardToRegularExpression(*it));
+
+          for (std::set<std::string>::const_iterator itall = allDcmtkSopClassUids.begin(); itall != allDcmtkSopClassUids.end(); ++itall)
+          {
+            if (regex_match(*itall, pattern))
+            {
+              acceptedSopClasses_.remove(*itall);
+            }
+          }
+        }
+        else
+        {
+          // if it is a SOP Class UID, remove it without checking if it is known by DCMTK
+          acceptedSopClasses_.remove(*it);
+        }
+      }
+    }
   }
 
   void ServerContext::GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) const
   {
+    sopClasses.clear();
+
     boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
 
-    if (acceptedSopClasses_.size() > 0)
-    {
-      size_t count = 0;
-      std::set<std::string>::const_iterator it = acceptedSopClasses_.begin();
-
-      while (it != acceptedSopClasses_.end() && (maxCount == 0 || count < maxCount))
-      {
-        sopClasses.insert(*it);
-        count++;
-      }
-    }
-    else
+    size_t count = 0;
+    std::list<std::string>::const_iterator it = acceptedSopClasses_.begin();
+
+    while (it != acceptedSopClasses_.end() && (maxCount == 0 || count < maxCount))
     {
-      if (maxCount != 0)
-      {
-        size_t count = 0;
-        // we actually take a list of default 120 most common storage SOP classes defined in DCMTK
-        while (dcmLongSCUStorageSOPClassUIDs[count] != NULL && count < maxCount)
-        {
-          sopClasses.insert(dcmAllStorageSOPClassUIDs[count]);
-          count++;
-        }
-      }
-      else
-      {
-        size_t count = 0;
-        // we actually take all known storage SOP classes defined in DCMTK
-        while (dcmAllStorageSOPClassUIDs[count] != NULL)
-        {
-          sopClasses.insert(dcmAllStorageSOPClassUIDs[count]);
-          count++;
-        }
-      }
+      sopClasses.insert(*it);
+      count++;
+      it++;
     }
   }
 
--- a/OrthancServer/Sources/ServerContext.h	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancServer/Sources/ServerContext.h	Thu Nov 21 12:17:47 2024 +0100
@@ -277,7 +277,7 @@
     mutable boost::mutex dynamicOptionsMutex_;
     bool isUnknownSopClassAccepted_;
     std::set<DicomTransferSyntax>  acceptedTransferSyntaxes_;
-    std::set<std::string>          acceptedSopClasses_;
+    std::list<std::string>         acceptedSopClasses_;  // ordered; the most 120 common ones first
 
     StoreResult StoreAfterTranscoding(std::string& resultPublicId,
                                       DicomInstanceToStore& dicom,
@@ -597,7 +597,8 @@
 
     void SetAcceptedTransferSyntaxes(const std::set<DicomTransferSyntax>& syntaxes);
 
-    void SetAcceptedSopClasses(const std::set<std::string>& sopClasses);
+    void SetAcceptedSopClasses(const std::list<std::string>& acceptedSopClasses,
+                               const std::set<std::string>& rejectedSopClasses);
 
     void GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) const;
 
--- a/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp	Thu Nov 21 12:17:47 2024 +0100
@@ -27,6 +27,7 @@
 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
 #include "../ServerContext.h"
 #include <dcmtk/dcmnet/dimse.h>
+#include <algorithm>
 
 static const char* const LOCAL_AET = "LocalAet";
 static const char* const QUERY = "Query";
@@ -112,32 +113,68 @@
   {
     if (connection_.get() == NULL)
     {
-      std::set<std::string> storageSopClassUids;
+      std::set<std::string> sopClassesToPropose;
+      std::set<std::string> sopClassesInStudy;
+      std::set<std::string> acceptedSopClasses;
       std::set<DicomTransferSyntax> storageAcceptedTransferSyntaxes;
 
-      if (findAnswer.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY))
+      if (findAnswer.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) &&
+          findAnswer.LookupStringValues(sopClassesInStudy, DICOM_TAG_SOP_CLASSES_IN_STUDY, false))
       {
-        throw OrthancException(ErrorCode_NotImplemented);
-        // TODO-GET
+        context_.GetAcceptedSopClasses(acceptedSopClasses, 0); 
+
+        // keep the sop classes from the resources to retrieve only if they are accepted by Orthanc
+        Toolbox::GetIntersection(sopClassesToPropose, sopClassesInStudy, acceptedSopClasses);
       }
       else
       {
-        // when we don't know what SOP Classes to use, we must limit to 120 SOP Classes because 
+        // when we don't know what SOP Classes to use, we include the 120 most common SOP Classes because 
         // there are only 128 presentation contexts available
-        context_.GetAcceptedSopClasses(storageSopClassUids, 120); 
+        context_.GetAcceptedSopClasses(sopClassesToPropose, 120); 
+      }
+
+      if (sopClassesToPropose.size() == 0)
+      {
+        throw OrthancException(ErrorCode_NoPresentationContext, "Cannot perform C-Get, no SOPClassUID have been accepted by Orthanc.");        
       }
 
       context_.GetAcceptedTransferSyntaxes(storageAcceptedTransferSyntaxes);
 
       connection_.reset(new DicomControlUserConnection(parameters_, 
                                                        ScuOperationFlags_Get, 
-                                                       storageSopClassUids,
+                                                       sopClassesToPropose,
                                                        storageAcceptedTransferSyntaxes));
     }
     
     connection_->Get(findAnswer, InstanceReceivedHandler, &context_);
   }
 
+  // this method is used to implement the retrieve part of a Q&R 
+  // it keeps only the main dicom tags from the C-Find answer
+  void DicomGetScuJob::AddFindAnswer(const DicomMap& answer)
+  {
+    DicomMap item;
+    item.CopyTagIfExists(answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+    item.CopyTagIfExists(answer, DICOM_TAG_PATIENT_ID);
+    item.CopyTagIfExists(answer, DICOM_TAG_STUDY_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_SERIES_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_SOP_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_ACCESSION_NUMBER);
+
+    query_.Add(item);
+    query_.GetAnswer(query_.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET); // Remove the "SpecificCharacterSet" (0008,0005) tag that is automatically added if creating a ParsedDicomFile object from a DicomMap
+
+    AddCommand(new Command(*this, answer));
+  }
+
+  void DicomGetScuJob::AddFindAnswer(QueryRetrieveHandler& query,
+                                     size_t i)
+  {
+    DicomMap answer;
+    query.GetAnswer(answer, i);
+    AddFindAnswer(answer);
+  }    
+
   void DicomGetScuJob::AddResourceToRetrieve(ResourceType level, const std::string& dicomId)
   {
     // TODO-GET: when retrieving a single series, one must provide the StudyInstanceUID too
@@ -218,7 +255,7 @@
   }
   
 
-  void DicomGetScuJob::SetQueryFormat(DicomToJsonFormat format)
+  void DicomGetScuJob::SetQueryFormat(DicomToJsonFormat format)  // TODO-GET: is this usefull ?
   {
     if (IsStarted())
     {
--- a/OrthancServer/Sources/ServerJobs/DicomGetScuJob.h	Wed Nov 20 09:58:12 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.h	Thu Nov 21 12:17:47 2024 +0100
@@ -59,12 +59,12 @@
     DicomGetScuJob(ServerContext& context,
                     const Json::Value& serialized);
 
-    // void AddFindAnswer(const DicomMap& answer);
+    void AddFindAnswer(const DicomMap& answer);
     
     // void AddQuery(const DicomMap& query);
 
-    // void AddFindAnswer(QueryRetrieveHandler& query,
-    //                    size_t i);
+    void AddFindAnswer(QueryRetrieveHandler& query,
+                       size_t i);
 
     void AddResourceToRetrieve(ResourceType level, const std::string& dicomId);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp	Thu Nov 21 12:17:47 2024 +0100
@@ -0,0 +1,150 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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.
+ * 
+ * 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/>.
+ **/
+
+
+#include "PrecompiledHeadersUnitTests.h"
+#include <gtest/gtest.h>
+
+#include "../../OrthancFramework/Sources/Compatibility.h"
+#include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
+#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h"
+#include "../../OrthancFramework/Sources/Logging.h"
+#include "../../OrthancFramework/Sources/SerializationToolbox.h"
+
+#include "../Sources/Database/SQLiteDatabaseWrapper.h"
+#include "../Sources/ServerContext.h"
+
+using namespace Orthanc;
+
+TEST(ServerConfig, AcceptedSopClasses)
+{
+  const std::string path = "UnitTestsStorage";
+
+  MemoryStorageArea storage;
+  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
+  db.Open();
+  ServerContext context(db, storage, true /* running unit tests */, 10);
+
+  { // default config -> all SOP Classes should be accepted
+    std::set<std::string> s;
+
+    context.GetAcceptedSopClasses(s, 0);
+    ASSERT_LE(100u, s.size());
+
+    ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+    ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") != s.end());
+
+    context.GetAcceptedSopClasses(s, 1);
+    ASSERT_EQ(1u, s.size());
+  }
+
+  {
+    std::list<std::string> acceptedStorageClasses;
+    std::set<std::string> rejectedStorageClasses;
+
+    std::set<std::string> s;
+    context.GetAcceptedSopClasses(s, 0);
+    size_t allSize = s.size();
+
+    { // default config but reject one class
+      acceptedStorageClasses.clear();
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.4");
+
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(allSize - 1, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") != s.end());
+
+      context.GetAcceptedSopClasses(s, 1);
+      ASSERT_EQ(1u, s.size());
+    }
+
+    { // default config but reject one regex
+      acceptedStorageClasses.clear();
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.*");
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+    }
+
+    { // accept a single - no rejection
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.1.4");
+      rejectedStorageClasses.clear();
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(1, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+    }
+
+    { // accept from regex - reject one
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.*");
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.12.1.1");
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_LE(10, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end());
+    }
+
+    { // accept from regex - reject from regex
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.*");
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.12.1.*");
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_LE(10, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end());
+    }
+
+    { // accept one that is unknown form DCMTK
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.3.4");
+      rejectedStorageClasses.clear();
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(1, s.size());
+      ASSERT_TRUE(s.find("1.2.3.4") != s.end());
+    }
+
+  }
+
+  context.Stop();
+  db.Close();
+}