changeset 5747:796cb17db15c find-refactoring

merged default -> find-refactoring
author Alain Mazy <am@orthanc.team>
date Mon, 02 Sep 2024 17:17:22 +0200
parents 95a3802ad133 (diff) 8bb3f2fca242 (current diff)
children 4bc650d88463
files NEWS OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h
diffstat 44 files changed, 6304 insertions(+), 555 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Aug 29 13:46:49 2024 +0200
+++ b/NEWS	Mon Sep 02 17:17:22 2024 +0200
@@ -1,13 +1,15 @@
 Pending changes in the mainline
 ===============================
 
+* TODO-FIND: complete the list of updated routes:
+  /studies?expand and sibbling routes now also return "Metadata" (if the DB implements 'extended-api-v1')
+
 REST API
------------
+--------
 
 * Improved parsing of multiple numerical values in DICOM tags.
   https://discourse.orthanc-server.org/t/qido-includefield-with-sequences/4746/6
 
-
 Maintenance
 -----------
 
--- a/OrthancFramework/Sources/DicomFormat/DicomArray.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -95,7 +95,8 @@
       }
       else if (v.IsSequence())
       {
-        s = "(sequence)";
+        //s = "(sequence)";
+        s = "(sequence) " + v.GetSequenceContent().toStyledString();
       }
       else
       {
--- a/OrthancFramework/Sources/SQLite/Connection.h	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/Connection.h	Mon Sep 02 17:17:22 2024 +0200
@@ -57,6 +57,7 @@
 #endif
 
 #define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__)
+#define SQLITE_FROM_HERE_DYNAMIC(sql) ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__, sql)
 
 namespace Orthanc
 {
--- a/OrthancFramework/Sources/SQLite/StatementId.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementId.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -57,12 +57,24 @@
     {
     }
 
+    Orthanc::SQLite::StatementId::StatementId(const char *file,
+                                              int line,
+                                              const std::string& statement) :
+      file_(file),
+      line_(line),
+      statement_(statement)
+    {
+    }
+
     bool StatementId::operator< (const StatementId& other) const
     {
       if (line_ != other.line_)
         return line_ < other.line_;
 
-      return strcmp(file_, other.file_) < 0;
+      if (strcmp(file_, other.file_) < 0)
+        return true;
+
+      return statement_ < other.statement_;
     }
   }
 }
--- a/OrthancFramework/Sources/SQLite/StatementId.h	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementId.h	Mon Sep 02 17:17:22 2024 +0200
@@ -55,6 +55,7 @@
     private:
       const char* file_;
       int line_;
+      std::string statement_;
 
       StatementId(); // Forbidden
 
@@ -62,6 +63,10 @@
       StatementId(const char* file,
                   int line);
 
+      StatementId(const char* file,
+                  int line,
+                  const std::string& statement);
+
       bool operator< (const StatementId& other) const;
     };
   }
--- a/OrthancServer/CMakeLists.txt	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/CMakeLists.txt	Mon Sep 02 17:17:22 2024 +0200
@@ -90,11 +90,16 @@
 set(ORTHANC_SERVER_SOURCES
   ${CMAKE_SOURCE_DIR}/Sources/Database/BaseDatabaseWrapper.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/DatabaseLookup.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/GenericFind.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ICreateInstance.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/IGetChildrenMetadata.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ILookupResourceAndParent.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ILookupResources.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/SetOfResources.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/FindRequest.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/FindResponse.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/MainDicomTagsRegistry.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Database/OrthancIdentifiers.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/ResourcesContent.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/SQLiteDatabaseWrapper.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/StatelessDatabaseOperations.cpp
@@ -119,6 +124,7 @@
   ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestSystem.cpp
   ${CMAKE_SOURCE_DIR}/Sources/OrthancWebDav.cpp
   ${CMAKE_SOURCE_DIR}/Sources/QueryRetrieveHandler.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/ResourceFinder.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseConstraint.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseLookup.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/DicomTagConstraint.cpp
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -31,6 +31,7 @@
 #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../../OrthancFramework/Sources/Logging.h"
 #include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../Sources/Database/Compatibility/GenericFind.h"
 #include "../../Sources/Database/ResourcesContent.h"
 #include "../../Sources/Database/VoidDatabaseListener.h"
 #include "../../Sources/ServerToolbox.h"
@@ -134,6 +135,136 @@
   }
 
 
+  static void Convert(DatabasePluginMessages::DatabaseConstraint& target,
+                      const DatabaseConstraint& source)
+  {
+    target.set_level(Convert(source.GetLevel()));
+    target.set_tag_group(source.GetTag().GetGroup());
+    target.set_tag_element(source.GetTag().GetElement());
+    target.set_is_identifier_tag(source.IsIdentifier());
+    target.set_is_case_sensitive(source.IsCaseSensitive());
+    target.set_is_mandatory(source.IsMandatory());
+
+    target.mutable_values()->Reserve(source.GetValuesCount());
+    for (size_t j = 0; j < source.GetValuesCount(); j++)
+    {
+      target.add_values(source.GetValue(j));
+    }
+
+    switch (source.GetConstraintType())
+    {
+      case ConstraintType_Equal:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_EQUAL);
+        break;
+
+      case ConstraintType_SmallerOrEqual:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL);
+        break;
+
+      case ConstraintType_GreaterOrEqual:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL);
+        break;
+
+      case ConstraintType_Wildcard:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD);
+        break;
+
+      case ConstraintType_List:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_LIST);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static DatabasePluginMessages::LabelsConstraintType Convert(LabelsConstraint constraint)
+  {
+    switch (constraint)
+    {
+      case LabelsConstraint_All:
+        return DatabasePluginMessages::LABELS_CONSTRAINT_ALL;
+
+      case LabelsConstraint_Any:
+        return DatabasePluginMessages::LABELS_CONSTRAINT_ANY;
+
+      case LabelsConstraint_None:
+        return DatabasePluginMessages::LABELS_CONSTRAINT_NONE;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static void Convert(DatabasePluginMessages::Find_Request_ChildrenSpecification& target,
+                      const FindRequest::ChildrenSpecification& source)
+  {
+    target.set_retrieve_identifiers(source.IsRetrieveIdentifiers());
+
+    for (std::set<MetadataType>::const_iterator it = source.GetMetadata().begin(); it != source.GetMetadata().end(); ++it)
+    {
+      target.add_retrieve_metadata(*it);
+    }
+
+    for (std::set<DicomTag>::const_iterator it = source.GetMainDicomTags().begin(); it != source.GetMainDicomTags().end(); ++it)
+    {
+      DatabasePluginMessages::Find_Request_Tag* tag = target.add_retrieve_main_dicom_tags();
+      tag->set_group(it->GetGroup());
+      tag->set_element(it->GetElement());
+    }
+  }
+
+
+  static void Convert(FindResponse::Resource& target,
+                      ResourceType level,
+                      const DatabasePluginMessages::Find_Response_ResourceContent& source)
+  {
+    for (int i = 0; i < source.main_dicom_tags().size(); i++)
+    {
+      target.AddStringDicomTag(level, source.main_dicom_tags(i).group(),
+                               source.main_dicom_tags(i).element(), source.main_dicom_tags(i).value());
+    }
+
+    for (int i = 0; i < source.metadata().size(); i++)
+    {
+      target.AddMetadata(level, static_cast<MetadataType>(source.metadata(i).key()), source.metadata(i).value());
+    }
+  }
+
+
+  static void Convert(FindResponse::Resource& target,
+                      ResourceType level,
+                      const DatabasePluginMessages::Find_Response_ChildrenContent& source)
+  {
+    for (int i = 0; i < source.identifiers().size(); i++)
+    {
+      target.AddChildIdentifier(level, source.identifiers(i));
+    }
+
+    for (int i = 0; i < source.main_dicom_tags().size(); i++)
+    {
+      const DicomTag tag(source.main_dicom_tags(i).group(), source.main_dicom_tags(i).element());
+
+      for (int j = 0; j < source.main_dicom_tags(i).values().size(); j++)
+      {
+        target.AddChildrenMainDicomTagValue(level, tag, source.main_dicom_tags(i).values(j));
+      }
+    }
+
+    for (int i = 0; i < source.metadata().size(); i++)
+    {
+      MetadataType key = static_cast<MetadataType>(source.metadata(i).key());
+
+      for (int j = 0; j < source.metadata(i).values().size(); j++)
+      {
+        target.AddChildrenMetadataValue(level, key, source.metadata(i).values(j));
+      }
+    }
+  }
+
+
   static void Execute(DatabasePluginMessages::Response& response,
                       const OrthancPluginDatabaseV4& database,
                       const DatabasePluginMessages::Request& request)
@@ -972,7 +1103,7 @@
       return response.is_disk_size_above().result();
     }
 
-    
+
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId, // Can be NULL if not needed
                                       const DatabaseConstraints& lookup,
@@ -996,47 +1127,7 @@
       
       for (size_t i = 0; i < lookup.GetSize(); i++)
       {
-        const DatabaseConstraint& source = lookup.GetConstraint(i);
-
-        DatabasePluginMessages::DatabaseConstraint* target = request.mutable_lookup_resources()->add_lookup();
-        target->set_level(Convert(source.GetLevel()));
-        target->set_tag_group(source.GetTag().GetGroup());
-        target->set_tag_element(source.GetTag().GetElement());
-        target->set_is_identifier_tag(source.IsIdentifier());
-        target->set_is_case_sensitive(source.IsCaseSensitive());
-        target->set_is_mandatory(source.IsMandatory());
-
-        target->mutable_values()->Reserve(source.GetValuesCount());
-        for (size_t j = 0; j < source.GetValuesCount(); j++)
-        {
-          target->add_values(source.GetValue(j));
-        }
-
-        switch (source.GetConstraintType())
-        {
-          case ConstraintType_Equal:
-            target->set_type(DatabasePluginMessages::CONSTRAINT_EQUAL);
-            break;
-            
-          case ConstraintType_SmallerOrEqual:
-            target->set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL);
-            break;
-            
-          case ConstraintType_GreaterOrEqual:
-            target->set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL);
-            break;
-            
-          case ConstraintType_Wildcard:
-            target->set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD);
-            break;
-            
-          case ConstraintType_List:
-            target->set_type(DatabasePluginMessages::CONSTRAINT_LIST);
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
+        Convert(*request.mutable_lookup_resources()->add_lookup(), lookup.GetConstraint(i));
       }
 
       for (std::set<std::string>::const_iterator it = labels.begin(); it != labels.end(); ++it)
@@ -1044,23 +1135,7 @@
         request.mutable_lookup_resources()->add_labels(*it);
       }
 
-      switch (labelsConstraint)
-      {
-        case LabelsConstraint_All:
-          request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_ALL);
-          break;
-            
-        case LabelsConstraint_Any:
-          request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_ANY);
-          break;
-            
-        case LabelsConstraint_None:
-          request.mutable_lookup_resources()->set_labels_constraint(DatabasePluginMessages::LABELS_CONSTRAINT_NONE);
-          break;
-            
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
+      request.mutable_lookup_resources()->set_labels_constraint(Convert(labelsConstraint));
       
       DatabasePluginMessages::TransactionResponse response;
       ExecuteTransaction(response, DatabasePluginMessages::OPERATION_LOOKUP_RESOURCES, request);
@@ -1278,6 +1353,212 @@
     {
       ListLabelsInternal(target, false, -1);
     }
+
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request,
+                             const Capabilities& capabilities) ORTHANC_OVERRIDE
+    {
+      if (capabilities.HasFindSupport())
+      {
+        DatabasePluginMessages::TransactionRequest dbRequest;
+        dbRequest.mutable_find()->set_level(Convert(request.GetLevel()));
+
+        if (request.GetOrthancIdentifiers().HasPatientId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_patient(request.GetOrthancIdentifiers().GetPatientId());
+        }
+
+        if (request.GetOrthancIdentifiers().HasStudyId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_study(request.GetOrthancIdentifiers().GetStudyId());
+        }
+
+        if (request.GetOrthancIdentifiers().HasSeriesId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_series(request.GetOrthancIdentifiers().GetSeriesId());
+        }
+
+        if (request.GetOrthancIdentifiers().HasInstanceId())
+        {
+          dbRequest.mutable_find()->set_orthanc_id_instance(request.GetOrthancIdentifiers().GetInstanceId());
+        }
+
+        for (size_t i = 0; i < request.GetDicomTagConstraints().GetSize(); i++)
+        {
+          Convert(*dbRequest.mutable_find()->add_dicom_tag_constraints(), request.GetDicomTagConstraints().GetConstraint(i));
+        }
+
+        if (request.HasLimits())
+        {
+          dbRequest.mutable_find()->mutable_limits()->set_since(request.GetLimitsSince());
+          dbRequest.mutable_find()->mutable_limits()->set_count(request.GetLimitsCount());
+        }
+
+        for (std::set<std::string>::const_iterator it = request.GetLabels().begin(); it != request.GetLabels().end(); ++it)
+        {
+          dbRequest.mutable_find()->add_labels(*it);
+        }
+
+        dbRequest.mutable_find()->set_labels_constraint(Convert(request.GetLabelsConstraint()));
+
+        // TODO-FIND: ordering_
+        // TODO-FIND: metadataConstraints__
+
+        dbRequest.mutable_find()->set_retrieve_main_dicom_tags(request.IsRetrieveMainDicomTags());
+        dbRequest.mutable_find()->set_retrieve_metadata(request.IsRetrieveMetadata());
+        dbRequest.mutable_find()->set_retrieve_labels(request.IsRetrieveLabels());
+        dbRequest.mutable_find()->set_retrieve_attachments(request.IsRetrieveAttachments());
+        dbRequest.mutable_find()->set_retrieve_parent_identifier(request.IsRetrieveParentIdentifier());
+        dbRequest.mutable_find()->set_retrieve_at_least_one_instance(request.IsRetrieveOneInstanceIdentifier());
+
+        if (request.GetLevel() == ResourceType_Study ||
+            request.GetLevel() == ResourceType_Series ||
+            request.GetLevel() == ResourceType_Instance)
+        {
+          dbRequest.mutable_find()->mutable_parent_patient()->set_retrieve_main_dicom_tags(request.GetParentSpecification(ResourceType_Patient).IsRetrieveMainDicomTags());
+          dbRequest.mutable_find()->mutable_parent_patient()->set_retrieve_metadata(request.GetParentSpecification(ResourceType_Patient).IsRetrieveMetadata());
+        }
+
+        if (request.GetLevel() == ResourceType_Series ||
+            request.GetLevel() == ResourceType_Instance)
+        {
+          dbRequest.mutable_find()->mutable_parent_study()->set_retrieve_main_dicom_tags(request.GetParentSpecification(ResourceType_Study).IsRetrieveMainDicomTags());
+          dbRequest.mutable_find()->mutable_parent_study()->set_retrieve_metadata(request.GetParentSpecification(ResourceType_Study).IsRetrieveMetadata());
+        }
+
+        if (request.GetLevel() == ResourceType_Instance)
+        {
+          dbRequest.mutable_find()->mutable_parent_series()->set_retrieve_main_dicom_tags(request.GetParentSpecification(ResourceType_Series).IsRetrieveMainDicomTags());
+          dbRequest.mutable_find()->mutable_parent_series()->set_retrieve_metadata(request.GetParentSpecification(ResourceType_Series).IsRetrieveMetadata());
+        }
+
+        if (request.GetLevel() == ResourceType_Patient)
+        {
+          Convert(*dbRequest.mutable_find()->mutable_children_studies(), request.GetChildrenSpecification(ResourceType_Study));
+        }
+
+        if (request.GetLevel() == ResourceType_Patient ||
+            request.GetLevel() == ResourceType_Study)
+        {
+          Convert(*dbRequest.mutable_find()->mutable_children_series(), request.GetChildrenSpecification(ResourceType_Series));
+        }
+
+        if (request.GetLevel() == ResourceType_Patient ||
+            request.GetLevel() == ResourceType_Study ||
+            request.GetLevel() == ResourceType_Series)
+        {
+          Convert(*dbRequest.mutable_find()->mutable_children_instances(), request.GetChildrenSpecification(ResourceType_Instance));
+        }
+
+        DatabasePluginMessages::TransactionResponse dbResponse;
+        ExecuteTransaction(dbResponse, DatabasePluginMessages::OPERATION_FIND, dbRequest);
+
+        for (int i = 0; i < dbResponse.find().size(); i++)
+        {
+          const DatabasePluginMessages::Find_Response& source = dbResponse.find(i);
+
+          std::unique_ptr<FindResponse::Resource> target(
+            new FindResponse::Resource(request.GetLevel(), source.internal_id(), source.public_id()));
+
+          if (request.IsRetrieveParentIdentifier())
+          {
+            target->SetParentIdentifier(source.parent_public_id());
+          }
+
+          for (int i = 0; i < source.labels().size(); i++)
+          {
+            target->AddLabel(source.labels(i));
+          }
+
+          for (int i = 0; i < source.attachments().size(); i++)
+          {
+            target->AddAttachment(Convert(source.attachments(i)));
+          }
+
+          Convert(*target, ResourceType_Patient, source.patient_content());
+
+          if (request.GetLevel() == ResourceType_Study ||
+              request.GetLevel() == ResourceType_Series ||
+              request.GetLevel() == ResourceType_Instance)
+          {
+            Convert(*target, ResourceType_Study, source.study_content());
+          }
+
+          if (request.GetLevel() == ResourceType_Series ||
+              request.GetLevel() == ResourceType_Instance)
+          {
+            Convert(*target, ResourceType_Series, source.series_content());
+          }
+
+          if (request.GetLevel() == ResourceType_Instance)
+          {
+            Convert(*target, ResourceType_Instance, source.instance_content());
+          }
+
+          if (request.GetLevel() == ResourceType_Patient)
+          {
+            Convert(*target, ResourceType_Patient, source.children_studies_content());
+          }
+
+          if (request.GetLevel() == ResourceType_Patient ||
+              request.GetLevel() == ResourceType_Study)
+          {
+            Convert(*target, ResourceType_Study, source.children_series_content());
+          }
+
+          if (request.GetLevel() == ResourceType_Patient ||
+              request.GetLevel() == ResourceType_Study ||
+              request.GetLevel() == ResourceType_Series)
+          {
+            Convert(*target, ResourceType_Series, source.children_instances_content());
+          }
+
+          response.Add(target.release());
+        }
+
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
+
+    virtual void ExecuteFind(std::list<std::string>& identifiers,
+                             const Capabilities& capabilities,
+                             const FindRequest& request) ORTHANC_OVERRIDE
+    {
+      if (capabilities.HasFindSupport())
+      {
+        // The integrated version of "ExecuteFind()" should have been called
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        Compatibility::GenericFind find(*this);
+        find.ExecuteFind(identifiers, capabilities, request);
+      }
+    }
+
+
+    virtual void ExecuteExpand(FindResponse& response,
+                               const Capabilities& capabilities,
+                               const FindRequest& request,
+                               const std::string& identifier) ORTHANC_OVERRIDE
+    {
+      if (capabilities.HasFindSupport())
+      {
+        // The integrated version of "ExecuteFind()" should have been called
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        Compatibility::GenericFind find(*this);
+        find.ExecuteExpand(response, capabilities, request, identifier);
+      }
+    }
   };
 
 
@@ -1366,6 +1647,9 @@
       dbCapabilities_.SetAtomicIncrementGlobalProperty(systemInfo.supports_increment_global_property());
       dbCapabilities_.SetUpdateAndGetStatistics(systemInfo.has_update_and_get_statistics());
       dbCapabilities_.SetMeasureLatency(systemInfo.has_measure_latency());
+      dbCapabilities_.SetHasFindSupport(systemInfo.supports_find());
+
+      printf(">>> %d\n", dbCapabilities_.HasFindSupport());
     }
 
     open_ = true;
@@ -1492,4 +1776,10 @@
       return dbCapabilities_;
     }
   }
+
+
+  bool OrthancPluginDatabaseV4::HasIntegratedFind() const
+  {
+    return dbCapabilities_.HasFindSupport();
+  }
 }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Mon Sep 02 17:17:22 2024 +0200
@@ -93,6 +93,8 @@
     virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
 
     virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE;
+
+    virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE;
   };
 }
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Sep 02 17:17:22 2024 +0200
@@ -121,7 +121,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     12
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  4
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  5
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Mon Sep 02 17:17:22 2024 +0200
@@ -141,6 +141,7 @@
     bool supports_increment_global_property = 5;
     bool has_update_and_get_statistics = 6;
     bool has_measure_latency = 7;
+    bool supports_find = 8;         // New in Orthanc 1.12.5
   }
 }
 
@@ -288,11 +289,12 @@
   OPERATION_GET_CHILDREN_METADATA = 42;
   OPERATION_GET_LAST_CHANGE_INDEX = 43;
   OPERATION_LOOKUP_RESOURCE_AND_PARENT = 44;
-  OPERATION_ADD_LABEL = 45;        // New in Orthanc 1.12.0
-  OPERATION_REMOVE_LABEL = 46;     // New in Orthanc 1.12.0
-  OPERATION_LIST_LABELS = 47;      // New in Orthanc 1.12.0
-  OPERATION_INCREMENT_GLOBAL_PROPERTY = 48;      // New in Orthanc 1.12.3
-  OPERATION_UPDATE_AND_GET_STATISTICS = 49;      // New in Orthanc 1.12.3
+  OPERATION_ADD_LABEL = 45;                   // New in Orthanc 1.12.0
+  OPERATION_REMOVE_LABEL = 46;                // New in Orthanc 1.12.0
+  OPERATION_LIST_LABELS = 47;                 // New in Orthanc 1.12.0
+  OPERATION_INCREMENT_GLOBAL_PROPERTY = 48;   // New in Orthanc 1.12.3
+  OPERATION_UPDATE_AND_GET_STATISTICS = 49;   // New in Orthanc 1.12.3
+  OPERATION_FIND = 50;                        // New in Orthanc 1.12.5
 }
 
 message Rollback {
@@ -824,6 +826,99 @@
   }
 }
 
+message Find {        // New in Orthanc 1.12.5
+  message Request {   // This corresponds to "FindRequest" in C++
+    message Tag {
+      uint32 group = 1;
+      uint32 element = 2;
+    }
+    message Limits {
+      uint64 since = 1;
+      uint64 count = 2;
+    }
+    message ParentSpecification {
+      bool retrieve_main_dicom_tags = 1;
+      bool retrieve_metadata = 2;
+    }
+    message ChildrenSpecification {
+      bool retrieve_identifiers = 1;
+      repeated int32 retrieve_metadata = 2;
+      repeated Tag retrieve_main_dicom_tags = 3;
+    }
+
+    // Part 1 of the request: Constraints
+    ResourceType level = 1;
+    string orthanc_id_patient = 2;   // optional - GetOrthancIdentifiers().GetPatientId();
+    string orthanc_id_study = 3;     // optional - GetOrthancIdentifiers().GetStudyId();
+    string orthanc_id_series = 4;    // optional - GetOrthancIdentifiers().GetSeriesId();
+    string orthanc_id_instance = 5;  // optional - GetOrthancIdentifiers().GetInstanceId();
+    repeated DatabaseConstraint dicom_tag_constraints = 6;
+    Limits limits = 7;               // optional
+    repeated string labels = 8;
+    LabelsConstraintType labels_constraint = 9;
+
+    // TODO-FIND: ordering_
+    // TODO-FIND: metadataConstraints_
+
+    // Part 2 of the request: What is to be retrieved
+    bool retrieve_main_dicom_tags = 100;
+    bool retrieve_metadata = 101;
+    bool retrieve_labels = 102;
+    bool retrieve_attachments = 103;
+    bool retrieve_parent_identifier = 104;
+    bool retrieve_at_least_one_instance = 105;
+    ParentSpecification parent_patient = 106;
+    ParentSpecification parent_study = 107;
+    ParentSpecification parent_series = 108;
+    ChildrenSpecification children_studies = 109;
+    ChildrenSpecification children_series = 110;
+    ChildrenSpecification children_instances = 111;
+  }
+
+  message Response {  // This corresponds to "FindResponse" in C++
+    message Tag {
+      uint32 group = 1;
+      uint32 element = 2;
+      string value = 3;
+    }
+    message Metadata {
+      int32 key = 1;
+      string value = 2;
+    }
+    message MultipleTags {
+      uint32 group = 1;
+      uint32 element = 2;
+      repeated string values = 3;
+    }
+    message MultipleMetadata {
+      int32 key = 1;
+      repeated string values = 2;
+    }
+    message ResourceContent {
+      repeated Tag main_dicom_tags = 1;
+      repeated Metadata metadata = 2;
+    }
+    message ChildrenContent {
+      repeated string identifiers = 1;
+      repeated MultipleTags main_dicom_tags = 2;
+      repeated MultipleMetadata metadata = 3;
+    }
+
+    int64 internal_id = 1;
+    string public_id = 2;
+    string parent_public_id = 3;   // optional
+    repeated string labels = 4;
+    repeated FileInfo attachments = 5;
+    ResourceContent patient_content = 6;
+    ResourceContent study_content = 7;
+    ResourceContent series_content = 8;
+    ResourceContent instance_content = 9;
+    ChildrenContent children_studies_content = 10;
+    ChildrenContent children_series_content = 11;
+    ChildrenContent children_instances_content = 12;
+  }
+}
+
 message TransactionRequest {
   sfixed64              transaction = 1;
   TransactionOperation  operation = 2;
@@ -878,6 +973,7 @@
   ListLabels.Request                      list_labels = 147;
   IncrementGlobalProperty.Request         increment_global_property = 148;
   UpdateAndGetStatistics.Request          update_and_get_statistics = 149;
+  Find.Request                            find = 150;
 }
 
 message TransactionResponse {
@@ -931,6 +1027,7 @@
   ListLabels.Response                      list_labels = 147;
   IncrementGlobalProperty.Response         increment_global_property = 148;
   UpdateAndGetStatistics.Response          update_and_get_statistics = 149;
+  repeated Find.Response                   find = 150;   // One message per found resources
 }
 
 enum RequestType {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Graveyard/FindRefactoringForSQLite.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,167 @@
+#if 0
+    // TODO-FIND: Remove this implementation, as it should be done by
+    // the compatibility mode implemented by "GenericFind"
+    
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request, 
+                             const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE
+    {
+#if 0
+      Compatibility::GenericFind find(*this);
+      find.Execute(response, request);
+#else
+      {
+        SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS FilteredResourcesIds");
+        s.Run();
+      }
+
+      {
+
+        LookupFormatter formatter;
+
+        std::string sqlLookup;
+        LookupFormatter::Apply(sqlLookup, 
+                               formatter, 
+                               normalized, 
+                               request.GetLevel(),
+                               request.GetLabels(),
+                               request.GetLabelsConstraint(),
+                               (request.HasLimits() ? request.GetLimitsCount() : 0));  // TODO: handles since and count
+
+        {
+          // first create a temporary table that with the filtered and ordered results
+          sqlLookup = "CREATE TEMPORARY TABLE FilteredResourcesIds AS " + sqlLookup;
+
+          SQLite::Statement statement(db_, SQLITE_FROM_HERE_DYNAMIC(sqlLookup), sqlLookup);
+          formatter.Bind(statement);
+          statement.Run();
+        }
+
+        {
+          // create the response item with the public ids only
+          SQLite::Statement statement(db_, SQLITE_FROM_HERE, "SELECT publicId FROM FilteredResourcesIds");
+          formatter.Bind(statement);
+
+          while (statement.Step())
+          {
+            const std::string resourceId = statement.ColumnString(0);
+            response.Add(new FindResponse::Resource(request.GetLevel(), resourceId));
+          }
+        }
+
+        // request Each response content through INNER JOIN with the temporary table
+        if (request.IsRetrieveMainDicomTags())
+        {
+          // TODO-FIND: handle the case where we request tags from multiple levels
+          SQLite::Statement statement(db_, SQLITE_FROM_HERE, 
+                                      "SELECT publicId, tagGroup, tagElement, value FROM MainDicomTags AS tags "
+                                      "  INNER JOIN FilteredResourcesIds  ON tags.id = FilteredResourcesIds.internalId");
+          formatter.Bind(statement);
+
+          while (statement.Step())
+          {
+            const std::string& resourceId = statement.ColumnString(0);
+            assert(response.HasResource(resourceId));
+            response.GetResource(resourceId).AddStringDicomTag(statement.ColumnInt(1),
+                                                               statement.ColumnInt(2),
+                                                               statement.ColumnString(3));
+          }
+        }
+
+        if (request.IsRetrieveChildrenIdentifiers())
+        {
+          SQLite::Statement statement(db_, SQLITE_FROM_HERE, 
+                                      "SELECT filtered.publicId, childLevel.publicId AS childPublicId "
+                                      "FROM Resources as currentLevel "
+                                      "    INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = currentLevel.internalId "
+                                      "    INNER JOIN Resources childLevel ON childLevel.parentId = currentLevel.internalId");
+          formatter.Bind(statement);
+
+          while (statement.Step())
+          {
+            const std::string& resourceId = statement.ColumnString(0);
+            assert(response.HasResource(resourceId));
+            response.GetResource(resourceId).AddChildIdentifier(GetChildResourceType(request.GetLevel()), statement.ColumnString(1));
+          }
+        }
+
+        if (request.IsRetrieveParentIdentifier())
+        {
+          SQLite::Statement statement(db_, SQLITE_FROM_HERE, 
+                                      "SELECT filtered.publicId, parentLevel.publicId AS parentPublicId "
+                                      "FROM Resources as currentLevel "
+                                      "    INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = currentLevel.internalId "
+                                      "    INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId");
+
+          while (statement.Step())
+          {
+            const std::string& resourceId = statement.ColumnString(0);
+            const std::string& parentId = statement.ColumnString(1);
+            assert(response.HasResource(resourceId));
+            response.GetResource(resourceId).SetParentIdentifier(parentId);
+          }
+        }
+
+        if (request.IsRetrieveMetadata())
+        {
+          SQLite::Statement statement(db_, SQLITE_FROM_HERE, 
+                                      "SELECT filtered.publicId, metadata.type, metadata.value "
+                                      "FROM Metadata "
+                                      "  INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = Metadata.id");
+
+          while (statement.Step())
+          {
+            const std::string& resourceId = statement.ColumnString(0);
+            assert(response.HasResource(resourceId));
+            response.GetResource(resourceId).AddMetadata(static_cast<MetadataType>(statement.ColumnInt(1)),
+                                                         statement.ColumnString(2));
+          }
+        }
+
+        if (request.IsRetrieveLabels())
+        {
+          SQLite::Statement statement(db_, SQLITE_FROM_HERE, 
+                                      "SELECT filtered.publicId, label "
+                                      "FROM Labels "
+                                      "  INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = Labels.id");
+
+          while (statement.Step())
+          {
+            const std::string& resourceId = statement.ColumnString(0);
+            assert(response.HasResource(resourceId));
+            response.GetResource(resourceId).AddLabel(statement.ColumnString(1));
+          }
+        }
+
+        if (request.IsRetrieveAttachments())
+        {
+          SQLite::Statement statement(db_, SQLITE_FROM_HERE, 
+                                      "SELECT filtered.publicId, uuid, fileType, uncompressedSize, compressionType, compressedSize, "
+                                      "       uncompressedMD5, compressedMD5 "
+                                      "FROM AttachedFiles "
+                                      "  INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = AttachedFiles.id");
+
+          while (statement.Step())
+          {
+            const std::string& resourceId = statement.ColumnString(0);
+            FileInfo attachment = FileInfo(statement.ColumnString(1),
+                                           static_cast<FileContentType>(statement.ColumnInt(2)),
+                                           statement.ColumnInt64(3),
+                                           statement.ColumnString(6),
+                                           static_cast<CompressionType>(statement.ColumnInt(4)),
+                                           statement.ColumnInt64(5),
+                                           statement.ColumnString(7));
+
+            assert(response.HasResource(resourceId));
+            response.GetResource(resourceId).AddAttachment(attachment);
+          };
+        }
+
+        // TODO-FIND: implement other responseContent: ResponseContent_ChildInstanceId, ResponseContent_ChildrenMetadata (later: ResponseContent_IsStable)
+
+      }
+
+#endif
+    }
+#endif
+
--- a/OrthancServer/Resources/Orthanc.doxygen	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Resources/Orthanc.doxygen	Mon Sep 02 17:17:22 2024 +0200
@@ -755,6 +755,7 @@
 # Note: If this tag is empty the current directory is searched.
 
 INPUT                  = @CMAKE_SOURCE_DIR@/../OrthancFramework/Sources \
+                         @CMAKE_SOURCE_DIR@/Plugins/Engine \
                          @CMAKE_SOURCE_DIR@/Sources
 
 # This tag can be used to specify the character encoding of the source files
--- a/OrthancServer/Resources/RunCppCheck.sh	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Resources/RunCppCheck.sh	Mon Sep 02 17:17:22 2024 +0200
@@ -16,8 +16,8 @@
 stlFindInsert:../../OrthancFramework/Sources/DicomFormat/DicomMap.cpp:1477
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:166
 stlFindInsert:../../OrthancFramework/Sources/RestApi/RestApiCallDocumentation.cpp:74
-stlFindInsert:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:374
-stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:378
+stlFindInsert:../../OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp:65
+stlFindInsert:../../OrthancServer/Sources/OrthancWebDav.cpp:480
 stlFindInsert:../../OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp:41
 stlFindInsert:../../OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp:191
 stlFindInsert:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:361
@@ -34,9 +34,9 @@
 useInitializationList:../../OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp:275
 assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:277
 assertWithSideEffect:../../OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp:1026
-assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:290
-assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:389
-assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3663
+assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:292
+assertWithSideEffect:../../OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp:391
+assertWithSideEffect:../../OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp:3527
 assertWithSideEffect:../../OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp:286
 assertWithSideEffect:../../OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp:454
 EOF
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -24,6 +24,7 @@
 #include "BaseDatabaseWrapper.h"
 
 #include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "Compatibility/GenericFind.h"
 
 namespace Orthanc
 {
@@ -46,6 +47,33 @@
   }
 
 
+  void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(FindResponse& response,
+                                                         const FindRequest& request,
+                                                         const Capabilities& capabilities)
+  {
+    throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+  }
+
+
+  void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(std::list<std::string>& identifiers,
+                                                         const Capabilities& capabilities,
+                                                         const FindRequest& request)
+  {
+    Compatibility::GenericFind find(*this);
+    find.ExecuteFind(identifiers, capabilities, request);
+  }
+
+
+  void BaseDatabaseWrapper::BaseTransaction::ExecuteExpand(FindResponse& response,
+                                                           const Capabilities& capabilities,
+                                                           const FindRequest& request,
+                                                           const std::string& identifier)
+  {
+    Compatibility::GenericFind find(*this);
+    find.ExecuteExpand(response, capabilities, request, identifier);
+  }
+
+
   uint64_t BaseDatabaseWrapper::MeasureLatency()
   {
     throw OrthancException(ErrorCode_NotImplemented);  // only implemented in V4
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.h	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h	Mon Sep 02 17:17:22 2024 +0200
@@ -47,8 +47,26 @@
                                           int64_t& instancesCount,
                                           int64_t& compressedSize,
                                           int64_t& uncompressedSize) ORTHANC_OVERRIDE;
+
+      virtual void ExecuteFind(FindResponse& response,
+                               const FindRequest& request,
+                               const Capabilities& capabilities) ORTHANC_OVERRIDE;
+
+      virtual void ExecuteFind(std::list<std::string>& identifiers,
+                               const Capabilities& capabilities,
+                               const FindRequest& request) ORTHANC_OVERRIDE;
+
+      virtual void ExecuteExpand(FindResponse& response,
+                                 const Capabilities& capabilities,
+                                 const FindRequest& request,
+                                 const std::string& identifier) ORTHANC_OVERRIDE;
     };
 
     virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
+
+    virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,596 @@
+/**
+ * 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 "GenericFind.h"
+
+#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
+#include "../../../../OrthancFramework/Sources/OrthancException.h"
+
+#include <stack>
+
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    static bool IsRequestWithoutContraint(const FindRequest& request)
+    {
+      return (request.GetDicomTagConstraints().IsEmpty() &&
+              request.GetMetadataConstraintsCount() == 0 &&
+              request.GetLabels().empty() &&
+              request.GetOrdering().empty());
+    }
+
+    static void GetChildren(std::list<int64_t>& target,
+                            IDatabaseWrapper::ITransaction& transaction,
+                            const std::list<int64_t>& resources)
+    {
+      target.clear();
+
+      for (std::list<int64_t>::const_iterator it = resources.begin(); it != resources.end(); ++it)
+      {
+        std::list<int64_t> tmp;
+        transaction.GetChildrenInternalId(tmp, *it);
+        target.splice(target.begin(), tmp);
+      }
+    }
+
+    static void GetChildren(std::list<std::string>& target,
+                            IDatabaseWrapper::ITransaction& transaction,
+                            const std::list<int64_t>& resources)
+    {
+      target.clear();
+
+      for (std::list<int64_t>::const_iterator it = resources.begin(); it != resources.end(); ++it)
+      {
+        std::list<std::string> tmp;
+        transaction.GetChildrenPublicId(tmp, *it);
+        target.splice(target.begin(), tmp);
+      }
+    }
+
+    static void GetChildrenIdentifiers(std::list<std::string>& children,
+                                       IDatabaseWrapper::ITransaction& transaction,
+                                       const OrthancIdentifiers& identifiers,
+                                       ResourceType topLevel,
+                                       ResourceType bottomLevel)
+    {
+      if (!IsResourceLevelAboveOrEqual(topLevel, bottomLevel) ||
+          topLevel == bottomLevel)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      std::list<int64_t> currentResources;
+      ResourceType currentLevel;
+
+      {
+        int64_t id;
+        if (!transaction.LookupResource(id, currentLevel, identifiers.GetLevel(topLevel)) ||
+            currentLevel != topLevel)
+        {
+          throw OrthancException(ErrorCode_InexistentItem);
+        }
+
+        currentResources.push_back(id);
+      }
+
+      while (currentLevel != bottomLevel)
+      {
+        ResourceType nextLevel = GetChildResourceType(currentLevel);
+        if (nextLevel == bottomLevel)
+        {
+          GetChildren(children, transaction, currentResources);
+        }
+        else
+        {
+          std::list<int64_t> nextResources;
+          GetChildren(nextResources, transaction, currentResources);
+          currentResources.swap(nextResources);
+        }
+
+        currentLevel = nextLevel;
+      }
+    }
+
+    void GenericFind::ExecuteFind(std::list<std::string>& identifiers,
+                                  const IDatabaseWrapper::Capabilities& capabilities,
+                                  const FindRequest& request)
+    {
+      if (!request.GetLabels().empty() &&
+          !capabilities.HasLabelsSupport())
+      {
+        throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels");
+      }
+
+      if (IsRequestWithoutContraint(request) &&
+          !request.GetOrthancIdentifiers().HasPatientId() &&
+          !request.GetOrthancIdentifiers().HasStudyId() &&
+          !request.GetOrthancIdentifiers().HasSeriesId() &&
+          !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        if (request.HasLimits())
+        {
+          transaction_.GetAllPublicIds(identifiers, request.GetLevel(), request.GetLimitsSince(), request.GetLimitsCount());
+        }
+        else
+        {
+          transaction_.GetAllPublicIds(identifiers, request.GetLevel());
+        }
+      }
+      else if (IsRequestWithoutContraint(request) &&
+               request.GetLevel() == ResourceType_Patient &&
+               request.GetOrthancIdentifiers().HasPatientId() &&
+               !request.GetOrthancIdentifiers().HasStudyId() &&
+               !request.GetOrthancIdentifiers().HasSeriesId() &&
+               !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        // TODO-FIND: This is a trivial case for which no transaction is needed
+        identifiers.push_back(request.GetOrthancIdentifiers().GetPatientId());
+      }
+      else if (IsRequestWithoutContraint(request) &&
+               request.GetLevel() == ResourceType_Study &&
+               !request.GetOrthancIdentifiers().HasPatientId() &&
+               request.GetOrthancIdentifiers().HasStudyId() &&
+               !request.GetOrthancIdentifiers().HasSeriesId() &&
+               !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        // TODO-FIND: This is a trivial case for which no transaction is needed
+        identifiers.push_back(request.GetOrthancIdentifiers().GetStudyId());
+      }
+      else if (IsRequestWithoutContraint(request) &&
+               request.GetLevel() == ResourceType_Series &&
+               !request.GetOrthancIdentifiers().HasPatientId() &&
+               !request.GetOrthancIdentifiers().HasStudyId() &&
+               request.GetOrthancIdentifiers().HasSeriesId() &&
+               !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        // TODO-FIND: This is a trivial case for which no transaction is needed
+        identifiers.push_back(request.GetOrthancIdentifiers().GetSeriesId());
+      }
+      else if (IsRequestWithoutContraint(request) &&
+               request.GetLevel() == ResourceType_Instance &&
+               !request.GetOrthancIdentifiers().HasPatientId() &&
+               !request.GetOrthancIdentifiers().HasStudyId() &&
+               !request.GetOrthancIdentifiers().HasSeriesId() &&
+               request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        // TODO-FIND: This is a trivial case for which no transaction is needed
+        identifiers.push_back(request.GetOrthancIdentifiers().GetInstanceId());
+      }
+      else if (IsRequestWithoutContraint(request) &&
+               (request.GetLevel() == ResourceType_Study ||
+                request.GetLevel() == ResourceType_Series ||
+                request.GetLevel() == ResourceType_Instance) &&
+               request.GetOrthancIdentifiers().HasPatientId() &&
+               !request.GetOrthancIdentifiers().HasStudyId() &&
+               !request.GetOrthancIdentifiers().HasSeriesId() &&
+               !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        GetChildrenIdentifiers(identifiers, transaction_, request.GetOrthancIdentifiers(), ResourceType_Patient, request.GetLevel());
+      }
+      else if (IsRequestWithoutContraint(request) &&
+               (request.GetLevel() == ResourceType_Series ||
+                request.GetLevel() == ResourceType_Instance) &&
+               !request.GetOrthancIdentifiers().HasPatientId() &&
+               request.GetOrthancIdentifiers().HasStudyId() &&
+               !request.GetOrthancIdentifiers().HasSeriesId() &&
+               !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        GetChildrenIdentifiers(identifiers, transaction_, request.GetOrthancIdentifiers(), ResourceType_Study, request.GetLevel());
+      }
+      else if (IsRequestWithoutContraint(request) &&
+               request.GetLevel() == ResourceType_Instance &&
+               !request.GetOrthancIdentifiers().HasPatientId() &&
+               !request.GetOrthancIdentifiers().HasStudyId() &&
+               request.GetOrthancIdentifiers().HasSeriesId() &&
+               !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        GetChildrenIdentifiers(identifiers, transaction_, request.GetOrthancIdentifiers(), ResourceType_Series, request.GetLevel());
+      }
+      else if (request.GetMetadataConstraintsCount() == 0 &&
+               request.GetOrdering().empty() &&
+               !request.GetOrthancIdentifiers().HasPatientId() &&
+               !request.GetOrthancIdentifiers().HasStudyId() &&
+               !request.GetOrthancIdentifiers().HasSeriesId() &&
+               !request.GetOrthancIdentifiers().HasInstanceId())
+      {
+        transaction_.ApplyLookupResources(identifiers, NULL /* TODO-FIND: Could the "instancesId" information be exploited? */,
+                                          request.GetDicomTagConstraints(), request.GetLevel(), request.GetLabels(),
+                                          request.GetLabelsConstraint(), request.HasLimits() ? request.GetLimitsCount() : 0);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
+
+    void GenericFind::RetrieveMainDicomTags(FindResponse::Resource& target,
+                                            ResourceType level,
+                                            int64_t internalId)
+    {
+      DicomMap m;
+      transaction_.GetMainDicomTags(m, internalId);
+
+      DicomArray a(m);
+      for (size_t i = 0; i < a.GetSize(); i++)
+      {
+        const DicomElement& element = a.GetElement(i);
+        if (element.GetValue().IsString())
+        {
+          target.AddStringDicomTag(level, element.GetTag().GetGroup(),
+                                   element.GetTag().GetElement(), element.GetValue().GetContent());
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadParameterType);
+        }
+      }
+    }
+
+
+    static ResourceType GetTopLevelOfInterest(const FindRequest& request)
+    {
+      switch (request.GetLevel())
+      {
+        case ResourceType_Patient:
+          return ResourceType_Patient;
+
+        case ResourceType_Study:
+          if (request.GetParentSpecification(ResourceType_Patient).IsOfInterest())
+          {
+            return ResourceType_Patient;
+          }
+          else
+          {
+            return ResourceType_Study;
+          }
+
+        case ResourceType_Series:
+          if (request.GetParentSpecification(ResourceType_Patient).IsOfInterest())
+          {
+            return ResourceType_Patient;
+          }
+          else if (request.GetParentSpecification(ResourceType_Study).IsOfInterest())
+          {
+            return ResourceType_Study;
+          }
+          else
+          {
+            return ResourceType_Series;
+          }
+
+        case ResourceType_Instance:
+          if (request.GetParentSpecification(ResourceType_Patient).IsOfInterest())
+          {
+            return ResourceType_Patient;
+          }
+          else if (request.GetParentSpecification(ResourceType_Study).IsOfInterest())
+          {
+            return ResourceType_Study;
+          }
+          else if (request.GetParentSpecification(ResourceType_Series).IsOfInterest())
+          {
+            return ResourceType_Series;
+          }
+          else
+          {
+            return ResourceType_Instance;
+          }
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    static ResourceType GetBottomLevelOfInterest(const FindRequest& request)
+    {
+      switch (request.GetLevel())
+      {
+        case ResourceType_Patient:
+          if (request.GetChildrenSpecification(ResourceType_Instance).IsOfInterest())
+          {
+            return ResourceType_Instance;
+          }
+          else if (request.GetChildrenSpecification(ResourceType_Series).IsOfInterest())
+          {
+            return ResourceType_Series;
+          }
+          else if (request.GetChildrenSpecification(ResourceType_Study).IsOfInterest())
+          {
+            return ResourceType_Study;
+          }
+          else
+          {
+            return ResourceType_Patient;
+          }
+
+        case ResourceType_Study:
+          if (request.GetChildrenSpecification(ResourceType_Instance).IsOfInterest())
+          {
+            return ResourceType_Instance;
+          }
+          else if (request.GetChildrenSpecification(ResourceType_Series).IsOfInterest())
+          {
+            return ResourceType_Series;
+          }
+          else
+          {
+            return ResourceType_Study;
+          }
+
+        case ResourceType_Series:
+          if (request.GetChildrenSpecification(ResourceType_Instance).IsOfInterest())
+          {
+            return ResourceType_Instance;
+          }
+          else
+          {
+            return ResourceType_Series;
+          }
+
+        case ResourceType_Instance:
+          return ResourceType_Instance;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    void GenericFind::ExecuteExpand(FindResponse& response,
+                                    const IDatabaseWrapper::Capabilities& capabilities,
+                                    const FindRequest& request,
+                                    const std::string& identifier)
+    {
+      int64_t internalId;
+      ResourceType level;
+      std::string parent;
+
+      if (request.IsRetrieveParentIdentifier())
+      {
+        if (!transaction_.LookupResourceAndParent(internalId, level, parent, identifier))
+        {
+          return;  // The resource is not available anymore
+        }
+
+        if (level == ResourceType_Patient)
+        {
+          if (!parent.empty())
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+        }
+        else
+        {
+          if (parent.empty())
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+        }
+      }
+      else
+      {
+        if (!transaction_.LookupResource(internalId, level, identifier))
+        {
+          return;  // The resource is not available anymore
+        }
+      }
+
+      if (level != request.GetLevel())
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+
+      std::unique_ptr<FindResponse::Resource> resource(new FindResponse::Resource(request.GetLevel(), internalId, identifier));
+
+      if (request.IsRetrieveParentIdentifier())
+      {
+        assert(!parent.empty());
+        resource->SetParentIdentifier(parent);
+      }
+
+      if (request.IsRetrieveMainDicomTags())
+      {
+        RetrieveMainDicomTags(*resource, level, internalId);
+      }
+
+      if (request.IsRetrieveMetadata())
+      {
+        transaction_.GetAllMetadata(resource->GetMetadata(level), internalId);
+      }
+
+      {
+        const ResourceType topLevel = GetTopLevelOfInterest(request);
+
+        int64_t currentId = internalId;
+        ResourceType currentLevel = level;
+
+        while (currentLevel != topLevel)
+        {
+          int64_t parentId;
+          if (transaction_.LookupParent(parentId, currentId))
+          {
+            currentId = parentId;
+            currentLevel = GetParentResourceType(currentLevel);
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+
+          if (request.GetParentSpecification(currentLevel).IsRetrieveMainDicomTags())
+          {
+            RetrieveMainDicomTags(*resource, currentLevel, currentId);
+          }
+
+          if (request.GetParentSpecification(currentLevel).IsRetrieveMetadata())
+          {
+            transaction_.GetAllMetadata(resource->GetMetadata(currentLevel), currentId);
+          }
+        }
+      }
+
+      if (capabilities.HasLabelsSupport() &&
+          request.IsRetrieveLabels())
+      {
+        transaction_.ListLabels(resource->GetLabels(), internalId);
+      }
+
+      if (request.IsRetrieveAttachments())
+      {
+        std::set<FileContentType> attachments;
+        transaction_.ListAvailableAttachments(attachments, internalId);
+
+        for (std::set<FileContentType>::const_iterator it = attachments.begin(); it != attachments.end(); ++it)
+        {
+          FileInfo info;
+          int64_t revision;
+          if (transaction_.LookupAttachment(info, revision, internalId, *it) &&
+              info.GetContentType() == *it)
+          {
+            resource->AddAttachment(info);
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+        }
+      }
+
+      {
+        const ResourceType bottomLevel = GetBottomLevelOfInterest(request);
+
+        std::list<int64_t> currentIds;
+        currentIds.push_back(internalId);
+
+        ResourceType currentLevel = level;
+
+        while (currentLevel != bottomLevel)
+        {
+          ResourceType childrenLevel = GetChildResourceType(currentLevel);
+
+          if (request.GetChildrenSpecification(childrenLevel).IsRetrieveIdentifiers())
+          {
+            for (std::list<int64_t>::const_iterator it = currentIds.begin(); it != currentIds.end(); ++it)
+            {
+              std::list<std::string> ids;
+              transaction_.GetChildrenPublicId(ids, *it);
+
+              for (std::list<std::string>::const_iterator it2 = ids.begin(); it2 != ids.end(); ++it2)
+              {
+                resource->AddChildIdentifier(childrenLevel, *it2);
+              }
+            }
+          }
+
+          const std::set<MetadataType>& metadata = request.GetChildrenSpecification(childrenLevel).GetMetadata();
+
+          for (std::set<MetadataType>::const_iterator it = metadata.begin(); it != metadata.end(); ++it)
+          {
+            for (std::list<int64_t>::const_iterator it2 = currentIds.begin(); it2 != currentIds.end(); ++it2)
+            {
+              std::list<std::string> values;
+              transaction_.GetChildrenMetadata(values, *it2, *it);
+
+              for (std::list<std::string>::const_iterator it3 = values.begin(); it3 != values.end(); ++it3)
+              {
+                resource->AddChildrenMetadataValue(childrenLevel, *it, *it3);
+              }
+            }
+          }
+
+          const std::set<DicomTag>& mainDicomTags = request.GetChildrenSpecification(childrenLevel).GetMainDicomTags();
+
+          if (childrenLevel != bottomLevel ||
+              !mainDicomTags.empty())
+          {
+            std::list<int64_t> childrenIds;
+
+            for (std::list<int64_t>::const_iterator it = currentIds.begin(); it != currentIds.end(); ++it)
+            {
+              std::list<int64_t> tmp;
+              transaction_.GetChildrenInternalId(tmp, *it);
+
+              childrenIds.splice(childrenIds.end(), tmp);
+            }
+
+            if (!mainDicomTags.empty())
+            {
+              for (std::list<int64_t>::const_iterator it = childrenIds.begin(); it != childrenIds.end(); ++it)
+              {
+                DicomMap m;
+                transaction_.GetMainDicomTags(m, *it);
+
+                for (std::set<DicomTag>::const_iterator it2 = mainDicomTags.begin(); it2 != mainDicomTags.end(); ++it2)
+                {
+                  std::string value;
+                  if (m.LookupStringValue(value, *it2, false /* no binary allowed */))
+                  {
+                    resource->AddChildrenMainDicomTagValue(childrenLevel, *it2, value);
+                  }
+                }
+              }
+            }
+
+            currentIds = childrenIds;
+          }
+          else
+          {
+            currentIds.clear();
+          }
+
+          currentLevel = childrenLevel;
+        }
+      }
+
+      if (request.IsRetrieveOneInstanceIdentifier() &&
+          !request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers())
+      {
+        int64_t currentId = internalId;
+        ResourceType currentLevel = level;
+
+        while (currentLevel != ResourceType_Instance)
+        {
+          std::list<int64_t> children;
+          transaction_.GetChildrenInternalId(children, currentId);
+          if (children.empty())
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+          else
+          {
+            currentId = children.front();
+            currentLevel = GetChildResourceType(currentLevel);
+          }
+        }
+
+        resource->AddChildIdentifier(ResourceType_Instance, transaction_.GetPublicId(currentId));
+      }
+
+      response.Add(resource.release());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.h	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,57 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "../IDatabaseWrapper.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class GenericFind : public boost::noncopyable
+    {
+    private:
+      IDatabaseWrapper::ITransaction&  transaction_;
+
+      void RetrieveMainDicomTags(FindResponse::Resource& target,
+                                 ResourceType level,
+                                 int64_t internalId);
+
+    public:
+      explicit GenericFind(IDatabaseWrapper::ITransaction& transaction) :
+        transaction_(transaction)
+      {
+      }
+
+      void ExecuteFind(std::list<std::string>& identifiers,
+                       const IDatabaseWrapper::Capabilities& capabilities,
+                       const FindRequest& request);
+
+      void ExecuteExpand(FindResponse& response,
+                         const IDatabaseWrapper::Capabilities& capabilities,
+                         const FindRequest& request,
+                         const std::string& identifier);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindRequest.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,260 @@
+/**
+ * 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 "FindRequest.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+#include "MainDicomTagsRegistry.h"
+
+#include <cassert>
+
+
+namespace Orthanc
+{
+  FindRequest::ParentSpecification& FindRequest::GetParentSpecification(ResourceType level)
+  {
+    if (!IsResourceLevelAboveOrEqual(level, level_) ||
+        level == level_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return retrieveParentPatient_;
+
+      case ResourceType_Study:
+        return retrieveParentStudy_;
+
+      case ResourceType_Series:
+        return retrieveParentSeries_;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  FindRequest::ChildrenSpecification& FindRequest::GetChildrenSpecification(ResourceType level)
+  {
+    if (!IsResourceLevelAboveOrEqual(level_, level) ||
+        level == level_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Study:
+        return retrieveChildrenStudies_;
+
+      case ResourceType_Series:
+        return retrieveChildrenSeries_;
+
+      case ResourceType_Instance:
+        return retrieveChildrenInstances_;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  FindRequest::FindRequest(ResourceType level) :
+    level_(level),
+    hasLimits_(false),
+    limitsSince_(0),
+    limitsCount_(0),
+    labelsConstraint_(LabelsConstraint_All),
+    retrieveMainDicomTags_(false),
+    retrieveMetadata_(false),
+    retrieveLabels_(false),
+    retrieveAttachments_(false),
+    retrieveParentIdentifier_(false),
+    retrieveOneInstanceIdentifier_(false)
+  {
+  }
+
+
+  FindRequest::~FindRequest()
+  {
+
+    for (std::deque<Ordering*>::iterator it = ordering_.begin(); it != ordering_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
+  }
+
+
+  void FindRequest::SetOrthancId(ResourceType level,
+                                 const std::string& id)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        SetOrthancPatientId(id);
+        break;
+
+      case ResourceType_Study:
+        SetOrthancStudyId(id);
+        break;
+
+      case ResourceType_Series:
+        SetOrthancSeriesId(id);
+        break;
+
+      case ResourceType_Instance:
+        SetOrthancInstanceId(id);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void FindRequest::SetOrthancPatientId(const std::string& id)
+  {
+    orthancIdentifiers_.SetPatientId(id);
+  }
+
+
+  void FindRequest::SetOrthancStudyId(const std::string& id)
+  {
+    if (level_ == ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      orthancIdentifiers_.SetStudyId(id);
+    }
+  }
+
+
+  void FindRequest::SetOrthancSeriesId(const std::string& id)
+  {
+    if (level_ == ResourceType_Patient ||
+        level_ == ResourceType_Study)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      orthancIdentifiers_.SetSeriesId(id);
+    }
+  }
+
+
+  void FindRequest::SetOrthancInstanceId(const std::string& id)
+  {
+    if (level_ == ResourceType_Patient ||
+        level_ == ResourceType_Study ||
+        level_ == ResourceType_Series)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      orthancIdentifiers_.SetInstanceId(id);
+    }
+  }
+
+
+  void FindRequest::SetLimits(uint64_t since,
+                              uint64_t count)
+  {
+    hasLimits_ = true;
+    limitsSince_ = since;
+    limitsCount_ = count;
+  }
+
+
+  uint64_t FindRequest::GetLimitsSince() const
+  {
+    if (hasLimits_)
+    {
+      return limitsSince_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  uint64_t FindRequest::GetLimitsCount() const
+  {
+    if (hasLimits_)
+    {
+      return limitsCount_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FindRequest::AddOrdering(const DicomTag& tag,
+                                OrderingDirection direction)
+  {
+    ordering_.push_back(new Ordering(Key(tag), direction));
+  }
+
+
+  void FindRequest::AddOrdering(MetadataType metadataType, 
+                                OrderingDirection direction)
+  {
+    ordering_.push_back(new Ordering(Key(metadataType), direction));
+  }
+
+
+  void FindRequest::SetRetrieveParentIdentifier(bool retrieve)
+  {
+    if (level_ == ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      retrieveParentIdentifier_ = retrieve;
+    }
+  }
+
+
+  void FindRequest::SetRetrieveOneInstanceIdentifier(bool retrieve)
+  {
+    if (level_ == ResourceType_Instance)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      retrieveOneInstanceIdentifier_ = retrieve;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindRequest.h	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,425 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomTag.h"
+#include "../Search/DatabaseConstraint.h"
+#include "../Search/DicomTagConstraint.h"
+#include "../Search/ISqlLookupFormatter.h"
+#include "../ServerEnumerations.h"
+#include "OrthancIdentifiers.h"
+
+#include <deque>
+#include <map>
+#include <set>
+#include <cassert>
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class MainDicomTagsRegistry;
+
+  class FindRequest : public boost::noncopyable
+  {
+  public:
+    enum KeyType  // used for ordering and filters
+    {
+      KeyType_DicomTag,
+      KeyType_Metadata
+    };
+
+
+    enum OrderingDirection
+    {
+      OrderingDirection_Ascending,
+      OrderingDirection_Descending
+    };
+
+
+    class Key
+    {
+    private:
+      KeyType       type_;
+      DicomTag      dicomTag_;
+      MetadataType  metadata_;
+      
+      // TODO-FIND: to execute the query, we actually need:
+      // ResourceType level_;
+      // DicomTagType dicomTagType_;
+      // these are however only populated in StatelessDatabaseOperations -> we had to add the normalized lookup arg to ExecuteFind
+
+    public:
+      explicit Key(const DicomTag& dicomTag) :
+        type_(KeyType_DicomTag),
+        dicomTag_(dicomTag),
+        metadata_(MetadataType_EndUser)
+      {
+      }
+
+      explicit Key(MetadataType metadata) :
+        type_(KeyType_Metadata),
+        dicomTag_(0, 0),
+        metadata_(metadata)
+      {
+      }
+
+      KeyType GetType() const
+      {
+        return type_;
+      }
+
+      const DicomTag& GetDicomTag() const
+      {
+        assert(GetType() == KeyType_DicomTag);
+        return dicomTag_;
+      }
+
+      MetadataType GetMetadataType() const
+      {
+        assert(GetType() == KeyType_Metadata);
+        return metadata_;
+      }
+    };
+
+    class Ordering : public boost::noncopyable
+    {
+    private:
+      OrderingDirection   direction_;
+      Key                 key_;
+
+    public:
+      Ordering(const Key& key,
+               OrderingDirection direction) :
+        direction_(direction),
+        key_(key)
+      {
+      }
+
+      KeyType GetKeyType() const
+      {
+        return key_.GetType();
+      }
+
+      OrderingDirection GetDirection() const
+      {
+        return direction_;
+      }
+
+      MetadataType GetMetadataType() const
+      {
+        return key_.GetMetadataType();
+      }
+
+      DicomTag GetDicomTag() const
+      {
+        return key_.GetDicomTag();
+      }
+    };
+
+
+    class ParentSpecification : public boost::noncopyable
+    {
+    private:
+      bool  mainDicomTags_;
+      bool  metadata_;
+
+    public:
+      ParentSpecification() :
+        mainDicomTags_(false),
+        metadata_(false)
+      {
+      }
+
+      void SetRetrieveMainDicomTags(bool retrieve)
+      {
+        mainDicomTags_ = retrieve;
+      }
+
+      bool IsRetrieveMainDicomTags() const
+      {
+        return mainDicomTags_;
+      }
+
+      void SetRetrieveMetadata(bool retrieve)
+      {
+        metadata_ = retrieve;
+      }
+
+      bool IsRetrieveMetadata() const
+      {
+        return metadata_;
+      }
+
+      bool IsOfInterest() const
+      {
+        return (mainDicomTags_ || metadata_);
+      }
+    };
+
+
+    class ChildrenSpecification : public boost::noncopyable
+    {
+    private:
+      bool                    identifiers_;
+      std::set<MetadataType>  metadata_;
+      std::set<DicomTag>      mainDicomTags_;
+
+    public:
+      ChildrenSpecification() :
+        identifiers_(false)
+      {
+      }
+
+      void SetRetrieveIdentifiers(bool retrieve)
+      {
+        identifiers_ = retrieve;
+      }
+
+      bool IsRetrieveIdentifiers() const
+      {
+        return identifiers_;
+      }
+
+      void AddMetadata(MetadataType metadata)
+      {
+        metadata_.insert(metadata);
+      }
+
+      const std::set<MetadataType>& GetMetadata() const
+      {
+        return metadata_;
+      }
+
+      void AddMainDicomTag(const DicomTag& tag)
+      {
+        mainDicomTags_.insert(tag);
+      }
+
+      const std::set<DicomTag>& GetMainDicomTags() const
+      {
+        return mainDicomTags_;
+      }
+
+      bool IsOfInterest() const
+      {
+        return (identifiers_ || !metadata_.empty() || !mainDicomTags_.empty());
+      }
+    };
+
+
+  private:
+    // filter & ordering fields
+    ResourceType                         level_;                // The level of the response (the filtering on tags, labels and metadata also happens at this level)
+    OrthancIdentifiers                   orthancIdentifiers_;   // The response must belong to this Orthanc resources hierarchy
+    DatabaseConstraints                  dicomTagConstraints_;  // All tags filters (note: the order is not important)
+    bool                                 hasLimits_;
+    uint64_t                             limitsSince_;
+    uint64_t                             limitsCount_;
+    std::set<std::string>                labels_;
+    LabelsConstraint                     labelsConstraint_;
+
+    // TODO-FIND
+    std::deque<Ordering*>                ordering_;             // The ordering criteria (note: the order is important !)
+    std::deque<void*>   /* TODO-FIND */       metadataConstraints_;  // All metadata filters (note: the order is not important)
+
+    bool                                 retrieveMainDicomTags_;
+    bool                                 retrieveMetadata_;
+    bool                                 retrieveLabels_;
+    bool                                 retrieveAttachments_;
+    bool                                 retrieveParentIdentifier_;
+    ParentSpecification                  retrieveParentPatient_;
+    ParentSpecification                  retrieveParentStudy_;
+    ParentSpecification                  retrieveParentSeries_;
+    ChildrenSpecification                retrieveChildrenStudies_;
+    ChildrenSpecification                retrieveChildrenSeries_;
+    ChildrenSpecification                retrieveChildrenInstances_;
+    bool                                 retrieveOneInstanceIdentifier_;
+
+    std::unique_ptr<MainDicomTagsRegistry>  mainDicomTagsRegistry_;
+
+  public:
+    explicit FindRequest(ResourceType level);
+
+    ~FindRequest();
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void SetOrthancId(ResourceType level,
+                      const std::string& id);
+
+    void SetOrthancPatientId(const std::string& id);
+
+    void SetOrthancStudyId(const std::string& id);
+
+    void SetOrthancSeriesId(const std::string& id);
+
+    void SetOrthancInstanceId(const std::string& id);
+
+    const OrthancIdentifiers& GetOrthancIdentifiers() const
+    {
+      return orthancIdentifiers_;
+    }
+
+    DatabaseConstraints& GetDicomTagConstraints()
+    {
+      return dicomTagConstraints_;
+    }
+
+    const DatabaseConstraints& GetDicomTagConstraints() const
+    {
+      return dicomTagConstraints_;
+    }
+
+    size_t GetMetadataConstraintsCount() const
+    {
+      return metadataConstraints_.size();
+    }
+
+    void ClearLimits()
+    {
+      hasLimits_ = false;
+    }
+
+    void SetLimits(uint64_t since,
+                   uint64_t count);
+
+    bool HasLimits() const
+    {
+      return hasLimits_;
+    }
+
+    uint64_t GetLimitsSince() const;
+
+    uint64_t GetLimitsCount() const;
+
+    void AddOrdering(const DicomTag& tag,
+                     OrderingDirection direction);
+
+    void AddOrdering(MetadataType metadataType,
+                     OrderingDirection direction);
+
+    const std::deque<Ordering*>& GetOrdering() const
+    {
+      return ordering_;
+    }
+
+    void SetLabels(const std::set<std::string>& labels)
+    {
+      labels_ = labels;
+    }
+
+    void AddLabel(const std::string& label)
+    {
+      labels_.insert(label);
+    }
+
+    const std::set<std::string>& GetLabels() const
+    {
+      return labels_;
+    }
+
+    LabelsConstraint GetLabelsConstraint() const
+    {
+      return labelsConstraint_;
+    }
+
+    void SetLabelsConstraint(LabelsConstraint constraint)
+    {
+      labelsConstraint_ = constraint;
+    }
+
+    void SetRetrieveMainDicomTags(bool retrieve)
+    {
+      retrieveMainDicomTags_ = retrieve;
+    }
+
+    bool IsRetrieveMainDicomTags() const
+    {
+      return retrieveMainDicomTags_;
+    }
+
+    void SetRetrieveMetadata(bool retrieve)
+    {
+      retrieveMetadata_ = retrieve;
+    }
+
+    bool IsRetrieveMetadata() const
+    {
+      return retrieveMetadata_;
+    }
+
+    void SetRetrieveLabels(bool retrieve)
+    {
+      retrieveLabels_ = retrieve;
+    }
+
+    bool IsRetrieveLabels() const
+    {
+      return retrieveLabels_;
+    }
+
+    void SetRetrieveAttachments(bool retrieve)
+    {
+      retrieveAttachments_ = retrieve;
+    }
+
+    bool IsRetrieveAttachments() const
+    {
+      return retrieveAttachments_;
+    }
+
+    void SetRetrieveParentIdentifier(bool retrieve);
+
+    bool IsRetrieveParentIdentifier() const
+    {
+      return retrieveParentIdentifier_;
+    }
+
+    ParentSpecification& GetParentSpecification(ResourceType level);
+
+    const ParentSpecification& GetParentSpecification(ResourceType level) const
+    {
+      return const_cast<FindRequest&>(*this).GetParentSpecification(level);
+    }
+
+    ChildrenSpecification& GetChildrenSpecification(ResourceType level);
+
+    const ChildrenSpecification& GetChildrenSpecification(ResourceType level) const
+    {
+      return const_cast<FindRequest&>(*this).GetChildrenSpecification(level);
+    }
+
+    void SetRetrieveOneInstanceIdentifier(bool retrieve);
+
+    bool IsRetrieveOneInstanceIdentifier() const
+    {
+      return (retrieveOneInstanceIdentifier_ ||
+              (level_ != ResourceType_Instance &&
+               GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers()));
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindResponse.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,733 @@
+/**
+ * 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 "FindResponse.h"
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../../OrthancFramework/Sources/SerializationToolbox.h"
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+
+namespace Orthanc
+{
+  class FindResponse::MainDicomTagsAtLevel::DicomValue : public boost::noncopyable
+  {
+  public:
+    enum ValueType
+    {
+      ValueType_String,
+      ValueType_Null
+    };
+
+  private:
+    ValueType     type_;
+    std::string   value_;
+
+  public:
+    DicomValue(ValueType type,
+               const std::string& value) :
+      type_(type),
+      value_(value)
+    {
+    }
+
+    ValueType GetType() const
+    {
+      return type_;
+    }
+
+    const std::string& GetValue() const
+    {
+      switch (type_)
+      {
+        case ValueType_Null:
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+
+        case ValueType_String:
+          return value_;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+  };
+
+
+  FindResponse::MainDicomTagsAtLevel::~MainDicomTagsAtLevel()
+  {
+    for (MainDicomTags::iterator it = mainDicomTags_.begin(); it != mainDicomTags_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  void FindResponse::MainDicomTagsAtLevel::AddNullDicomTag(uint16_t group,
+                                                           uint16_t element)
+  {
+    const DicomTag tag(group, element);
+
+    if (mainDicomTags_.find(tag) == mainDicomTags_.end())
+    {
+      mainDicomTags_[tag] = new DicomValue(DicomValue::ValueType_Null, "");
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FindResponse::MainDicomTagsAtLevel::AddStringDicomTag(uint16_t group,
+                                                             uint16_t element,
+                                                             const std::string& value)
+  {
+    const DicomTag tag(group, element);
+
+    if (mainDicomTags_.find(tag) == mainDicomTags_.end())
+    {
+      mainDicomTags_[tag] = new DicomValue(DicomValue::ValueType_String, value);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FindResponse::MainDicomTagsAtLevel::Export(DicomMap& target) const
+  {
+    for (MainDicomTags::const_iterator it = mainDicomTags_.begin(); it != mainDicomTags_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      switch (it->second->GetType())
+      {
+        case DicomValue::ValueType_String:
+          target.SetValue(it->first, it->second->GetValue(), false /* not binary */);
+          break;
+
+        case DicomValue::ValueType_Null:
+          target.SetNullValue(it->first);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  FindResponse::ChildrenInformation::~ChildrenInformation()
+  {
+    for (MetadataValues::iterator it = metadataValues_.begin(); it != metadataValues_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+
+    for (MainDicomTagValues::iterator it = mainDicomTagValues_.begin(); it != mainDicomTagValues_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  void FindResponse::ChildrenInformation::AddIdentifier(const std::string& identifier)
+  {
+    if (identifiers_.find(identifier) == identifiers_.end())
+    {
+      identifiers_.insert(identifier);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FindResponse::ChildrenInformation::AddMetadataValue(MetadataType metadata,
+                                                           const std::string& value)
+  {
+    MetadataValues::iterator found = metadataValues_.find(metadata);
+
+    if (found == metadataValues_.end())
+    {
+      std::set<std::string> s;
+      s.insert(value);
+      metadataValues_[metadata] = new std::set(s);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      found->second->insert(value);
+    }
+  }
+
+
+  void FindResponse::ChildrenInformation::GetMetadataValues(std::set<std::string>& values,
+                                                            MetadataType metadata) const
+  {
+    MetadataValues::const_iterator found = metadataValues_.find(metadata);
+
+    if (found == metadataValues_.end())
+    {
+      values.clear();
+    }
+    else
+    {
+      assert(found->second != NULL);
+      values = *found->second;
+    }
+  }
+
+
+  void FindResponse::ChildrenInformation::AddMainDicomTagValue(const DicomTag& tag,
+                                                               const std::string& value)
+  {
+    MainDicomTagValues::iterator found = mainDicomTagValues_.find(tag);
+
+    if (found == mainDicomTagValues_.end())
+    {
+      std::set<std::string> s;
+      s.insert(value);
+      mainDicomTagValues_[tag] = new std::set(s);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      found->second->insert(value);
+    }
+  }
+
+
+  void FindResponse::ChildrenInformation::GetMainDicomTagValues(std::set<std::string>& values,
+                                                                const DicomTag& tag) const
+  {
+    MainDicomTagValues::const_iterator found = mainDicomTagValues_.find(tag);
+
+    if (found == mainDicomTagValues_.end())
+    {
+      values.clear();
+    }
+    else
+    {
+      assert(found->second != NULL);
+      values = *found->second;
+    }
+  }
+
+
+  FindResponse::ChildrenInformation& FindResponse::Resource::GetChildrenInformation(ResourceType level)
+  {
+    switch (level)
+    {
+      case ResourceType_Study:
+        if (level_ == ResourceType_Patient)
+        {
+          return childrenStudiesInformation_;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+
+      case ResourceType_Series:
+        if (level_ == ResourceType_Patient ||
+            level_ == ResourceType_Study)
+        {
+          return childrenSeriesInformation_;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+
+      case ResourceType_Instance:
+        if (level_ == ResourceType_Patient ||
+            level_ == ResourceType_Study ||
+            level_ == ResourceType_Series)
+        {
+          return childrenInstancesInformation_;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  FindResponse::MainDicomTagsAtLevel& FindResponse::Resource::GetMainDicomTagsAtLevel(ResourceType level)
+  {
+    if (!IsResourceLevelAboveOrEqual(level, level_))
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return mainDicomTagsPatient_;
+
+      case ResourceType_Study:
+        return mainDicomTagsStudy_;
+
+      case ResourceType_Series:
+        return mainDicomTagsSeries_;
+
+      case ResourceType_Instance:
+        return mainDicomTagsInstance_;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void FindResponse::Resource::GetAllMainDicomTags(DicomMap& target) const
+  {
+    switch (level_)
+    {
+      // Don't reorder or add "break" below
+      case ResourceType_Instance:
+        mainDicomTagsInstance_.Export(target);
+
+      case ResourceType_Series:
+        mainDicomTagsSeries_.Export(target);
+
+      case ResourceType_Study:
+        mainDicomTagsStudy_.Export(target);
+
+      case ResourceType_Patient:
+        mainDicomTagsPatient_.Export(target);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void FindResponse::Resource::AddMetadata(ResourceType level,
+                                           MetadataType metadata,
+                                           const std::string& value)
+  {
+    std::map<MetadataType, std::string>& m = GetMetadata(level);
+
+    if (m.find(metadata) != m.end())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);  // Metadata already present
+    }
+    else
+    {
+      m[metadata] = value;
+    }
+  }
+
+
+  std::map<MetadataType, std::string>& FindResponse::Resource::GetMetadata(ResourceType level)
+  {
+    if (!IsResourceLevelAboveOrEqual(level, level_))
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return metadataPatient_;
+
+      case ResourceType_Study:
+        return metadataStudy_;
+
+      case ResourceType_Series:
+        return metadataSeries_;
+
+      case ResourceType_Instance:
+        return metadataInstance_;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool FindResponse::Resource::LookupMetadata(std::string& value,
+                                              ResourceType level,
+                                              MetadataType metadata) const
+  {
+    const std::map<MetadataType, std::string>& m = GetMetadata(level);
+
+    std::map<MetadataType, std::string>::const_iterator found = m.find(metadata);
+
+    if (found == m.end())
+    {
+      return false;
+    }
+    else
+    {
+      value = found->second;
+      return true;
+    }
+  }
+
+
+  void FindResponse::Resource::SetParentIdentifier(const std::string& id)
+  {
+    if (level_ == ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else if (HasParentIdentifier())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parentIdentifier_.reset(new std::string(id));
+    }
+  }
+
+
+  const std::string& FindResponse::Resource::GetParentIdentifier() const
+  {
+    if (level_ == ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else if (HasParentIdentifier())
+    {
+      return *parentIdentifier_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  bool FindResponse::Resource::HasParentIdentifier() const
+  {
+    if (level_ == ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      return parentIdentifier_.get() != NULL;
+    }
+  }
+
+
+  void FindResponse::Resource::AddLabel(const std::string& label)
+  {
+    if (labels_.find(label) == labels_.end())
+    {
+      labels_.insert(label);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void FindResponse::Resource::AddAttachment(const FileInfo& attachment)
+  {
+    if (attachments_.find(attachment.GetContentType()) == attachments_.end())
+    {
+      attachments_[attachment.GetContentType()] = attachment;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  bool FindResponse::Resource::LookupAttachment(FileInfo& target, FileContentType type) const
+  {
+    std::map<FileContentType, FileInfo>::const_iterator it = attachments_.find(type);
+    if (it != attachments_.end())
+    {
+      target = it->second;
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  const std::string& FindResponse::Resource::GetOneInstanceIdentifier() const
+  {
+    const std::set<std::string>& instances = GetChildrenInformation(ResourceType_Instance).GetIdentifiers();
+
+    if (instances.size() == 0)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);  // HasOneInstanceIdentifier() should have been called
+    }
+    else
+    {
+      return *instances.begin();
+    }
+  }
+
+
+  static void DebugDicomMap(Json::Value& target,
+                            const DicomMap& m)
+  {
+    DicomArray a(m);
+    for (size_t i = 0; i < a.GetSize(); i++)
+    {
+      if (a.GetElement(i).GetValue().IsNull())
+      {
+        target[a.GetElement(i).GetTag().Format()] = Json::nullValue;
+      }
+      else if (a.GetElement(i).GetValue().IsString())
+      {
+        target[a.GetElement(i).GetTag().Format()] = a.GetElement(i).GetValue().GetContent();
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  static void DebugMetadata(Json::Value& target,
+                            const std::map<MetadataType, std::string>& m)
+  {
+    target = Json::objectValue;
+
+    for (std::map<MetadataType, std::string>::const_iterator it = m.begin(); it != m.end(); ++it)
+    {
+      target[EnumerationToString(it->first)] = it->second;
+    }
+  }
+
+
+  static void DebugAddAttachment(Json::Value& target,
+                                 const FileInfo& info)
+  {
+    Json::Value u = Json::arrayValue;
+    u.append(info.GetUuid());
+    u.append(static_cast<Json::UInt64>(info.GetUncompressedSize()));
+    target[EnumerationToString(info.GetContentType())] = u;
+  }
+
+
+  static void DebugSetOfStrings(Json::Value& target,
+                                const std::set<std::string>& values)
+  {
+    target = Json::arrayValue;
+    for (std::set<std::string>::const_iterator it = values.begin(); it != values.end(); ++it)
+    {
+      target.append(*it);
+    }
+  }
+
+
+  void FindResponse::Resource::DebugExport(Json::Value& target,
+                                           const FindRequest& request) const
+  {
+    target = Json::objectValue;
+
+    target["Level"] = EnumerationToString(GetLevel());
+    target["ID"] = GetIdentifier();
+
+    if (request.IsRetrieveParentIdentifier())
+    {
+      target["ParentID"] = GetParentIdentifier();
+    }
+
+    if (request.IsRetrieveMainDicomTags())
+    {
+      DicomMap m;
+      GetMainDicomTags(m, request.GetLevel());
+      DebugDicomMap(target[EnumerationToString(GetLevel())]["MainDicomTags"], m);
+    }
+
+    if (request.IsRetrieveMetadata())
+    {
+      DebugMetadata(target[EnumerationToString(GetLevel())]["Metadata"], GetMetadata(request.GetLevel()));
+    }
+
+    static const ResourceType levels[4] = { ResourceType_Patient, ResourceType_Study, ResourceType_Series, ResourceType_Instance };
+
+    for (size_t i = 0; i < 4; i++)
+    {
+      const char* level = EnumerationToString(levels[i]);
+
+      if (levels[i] != request.GetLevel() &&
+          IsResourceLevelAboveOrEqual(levels[i], request.GetLevel()))
+      {
+        if (request.GetParentSpecification(levels[i]).IsRetrieveMainDicomTags())
+        {
+          DicomMap m;
+          GetMainDicomTags(m, levels[i]);
+          DebugDicomMap(target[level]["MainDicomTags"], m);
+        }
+
+        if (request.GetParentSpecification(levels[i]).IsRetrieveMetadata())
+        {
+          DebugMetadata(target[level]["Metadata"], GetMetadata(levels[i]));
+        }
+      }
+
+      if (levels[i] != request.GetLevel() &&
+          IsResourceLevelAboveOrEqual(request.GetLevel(), levels[i]))
+      {
+        if (request.GetChildrenSpecification(levels[i]).IsRetrieveIdentifiers())
+        {
+          DebugSetOfStrings(target[level]["Identifiers"], GetChildrenInformation(levels[i]).GetIdentifiers());
+        }
+
+        const std::set<MetadataType>& metadata = request.GetChildrenSpecification(levels[i]).GetMetadata();
+        for (std::set<MetadataType>::const_iterator it = metadata.begin(); it != metadata.end(); ++it)
+        {
+          std::set<std::string> values;
+          GetChildrenInformation(levels[i]).GetMetadataValues(values, *it);
+          DebugSetOfStrings(target[level]["Metadata"][EnumerationToString(*it)], values);
+        }
+
+        const std::set<DicomTag>& tags = request.GetChildrenSpecification(levels[i]).GetMainDicomTags();
+        for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+        {
+          std::set<std::string> values;
+          GetChildrenInformation(levels[i]).GetMainDicomTagValues(values, *it);
+          DebugSetOfStrings(target[level]["MainDicomTags"][it->Format()], values);
+        }
+      }
+    }
+
+    if (request.IsRetrieveLabels())
+    {
+      DebugSetOfStrings(target["Labels"], labels_);
+    }
+
+    if (request.IsRetrieveAttachments())
+    {
+      Json::Value v = Json::objectValue;
+      for (std::map<FileContentType, FileInfo>::const_iterator it = attachments_.begin();
+           it != attachments_.end(); ++it)
+      {
+        if (it->first != it->second.GetContentType())
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+        else
+        {
+          DebugAddAttachment(v, it->second);
+        }
+      }
+      target["Attachments"] = v;
+    }
+
+    if (request.IsRetrieveOneInstanceIdentifier())
+    {
+      target["OneInstance"] = GetOneInstanceIdentifier();
+    }
+  }
+
+
+  FindResponse::~FindResponse()
+  {
+    for (size_t i = 0; i < items_.size(); i++)
+    {
+      assert(items_[i] != NULL);
+      delete items_[i];
+    }
+  }
+
+
+  void FindResponse::Add(Resource* item /* takes ownership */)
+  {
+    std::unique_ptr<Resource> protection(item);
+
+    if (item == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (!items_.empty() &&
+             items_[0]->GetLevel() != item->GetLevel())
+    {
+      throw OrthancException(ErrorCode_BadParameterType, "A find response must only contain resources of the same type");
+    }
+    else
+    {
+      const std::string& id = item->GetIdentifier();
+
+      if (index_.find(id) == index_.end())
+      {
+        items_.push_back(protection.release());
+        index_[id] = item;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls, "This resource has already been added: " + id);
+      }
+    }
+  }
+
+
+  const FindResponse::Resource& FindResponse::GetResourceByIndex(size_t index) const
+  {
+    if (index >= items_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(items_[index] != NULL);
+      return *items_[index];
+    }
+  }
+
+
+  FindResponse::Resource& FindResponse::GetResourceByIdentifier(const std::string& id)
+  {
+    Index::const_iterator found = index_.find(id);
+
+    if (found == index_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      return *found->second;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindResponse.h	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,312 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
+#include "../../../OrthancFramework/Sources/Enumerations.h"
+#include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h"
+#include "../ServerEnumerations.h"
+#include "OrthancIdentifiers.h"
+#include "FindRequest.h"
+
+#include <boost/noncopyable.hpp>
+#include <deque>
+#include <map>
+#include <set>
+#include <list>
+
+
+namespace Orthanc
+{
+  class FindResponse : public boost::noncopyable
+  {
+  private:
+    class MainDicomTagsAtLevel : public boost::noncopyable
+    {
+    private:
+      class DicomValue;
+
+      typedef std::map<DicomTag, DicomValue*>  MainDicomTags;
+
+      MainDicomTags  mainDicomTags_;
+
+    public:
+      ~MainDicomTagsAtLevel();
+
+      void AddStringDicomTag(uint16_t group,
+                             uint16_t element,
+                             const std::string& value);
+
+      // The "Null" value could be used in the future to indicate a
+      // value that is not available, typically a new "ExtraMainDicomTag"
+      void AddNullDicomTag(uint16_t group,
+                           uint16_t element);
+
+      void Export(DicomMap& target) const;
+    };
+
+    class ChildrenInformation : public boost::noncopyable
+    {
+    private:
+      typedef std::map<MetadataType, std::set<std::string>* >  MetadataValues;
+      typedef std::map<DicomTag, std::set<std::string>* >      MainDicomTagValues;
+
+      std::set<std::string>  identifiers_;
+      MetadataValues         metadataValues_;
+      MainDicomTagValues     mainDicomTagValues_;
+
+    public:
+      ~ChildrenInformation();
+
+      void AddIdentifier(const std::string& identifier);
+
+      const std::set<std::string>& GetIdentifiers() const
+      {
+        return identifiers_;
+      }
+
+      void AddMetadataValue(MetadataType metadata,
+                            const std::string& value);
+
+      void GetMetadataValues(std::set<std::string>& values,
+                             MetadataType metadata) const;
+
+      void AddMainDicomTagValue(const DicomTag& tag,
+                                const std::string& value);
+
+      void GetMainDicomTagValues(std::set<std::string>& values,
+                                 const DicomTag& tag) const;
+    };
+
+
+  public:
+    class Resource : public boost::noncopyable
+    {
+    private:
+      typedef std::map<MetadataType, std::list<std::string>*>  ChildrenMetadata;
+
+      ResourceType                          level_;
+      int64_t                               internalId_;   // Internal ID of the resource in the database
+      std::string                           identifier_;
+      std::unique_ptr<std::string>          parentIdentifier_;
+      MainDicomTagsAtLevel                  mainDicomTagsPatient_;
+      MainDicomTagsAtLevel                  mainDicomTagsStudy_;
+      MainDicomTagsAtLevel                  mainDicomTagsSeries_;
+      MainDicomTagsAtLevel                  mainDicomTagsInstance_;
+      std::map<MetadataType, std::string>   metadataPatient_;
+      std::map<MetadataType, std::string>   metadataStudy_;
+      std::map<MetadataType, std::string>   metadataSeries_;
+      std::map<MetadataType, std::string>   metadataInstance_;
+      ChildrenInformation                   childrenStudiesInformation_;
+      ChildrenInformation                   childrenSeriesInformation_;
+      ChildrenInformation                   childrenInstancesInformation_;
+      std::set<std::string>                 labels_;
+      std::map<FileContentType, FileInfo>   attachments_;
+
+      MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level);
+
+      const MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level) const
+      {
+        return const_cast<Resource&>(*this).GetMainDicomTagsAtLevel(level);
+      }
+
+      ChildrenInformation& GetChildrenInformation(ResourceType level);
+
+      const ChildrenInformation& GetChildrenInformation(ResourceType level) const
+      {
+        return const_cast<Resource&>(*this).GetChildrenInformation(level);
+      }
+
+    public:
+      Resource(ResourceType level,
+               int64_t internalId,
+               const std::string& identifier) :
+        level_(level),
+        internalId_(internalId),
+        identifier_(identifier)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      int64_t GetInternalId() const
+      {
+        return internalId_;
+      }
+
+      const std::string& GetIdentifier() const
+      {
+        return identifier_;
+      }
+
+      void SetParentIdentifier(const std::string& id);
+
+      const std::string& GetParentIdentifier() const;
+
+      bool HasParentIdentifier() const;
+
+      void AddStringDicomTag(ResourceType level,
+                             uint16_t group,
+                             uint16_t element,
+                             const std::string& value)
+      {
+        GetMainDicomTagsAtLevel(level).AddStringDicomTag(group, element, value);
+      }
+
+      void AddNullDicomTag(ResourceType level,
+                           uint16_t group,
+                           uint16_t element)
+      {
+        GetMainDicomTagsAtLevel(level).AddNullDicomTag(group, element);
+      }
+
+      void GetMainDicomTags(DicomMap& target,
+                            ResourceType level) const
+      {
+        GetMainDicomTagsAtLevel(level).Export(target);
+      }
+
+      void GetAllMainDicomTags(DicomMap& target) const;
+
+      void AddMetadata(ResourceType level,
+                       MetadataType metadata,
+                       const std::string& value);
+
+      std::map<MetadataType, std::string>& GetMetadata(ResourceType level);
+
+      const std::map<MetadataType, std::string>& GetMetadata(ResourceType level) const
+      {
+        return const_cast<Resource&>(*this).GetMetadata(level);
+      }
+
+      bool LookupMetadata(std::string& value,
+                          ResourceType level,
+                          MetadataType metadata) const;
+
+      void AddChildIdentifier(ResourceType level,
+                              const std::string& childId)
+      {
+        GetChildrenInformation(level).AddIdentifier(childId);
+      }
+
+      const std::set<std::string>& GetChildrenIdentifiers(ResourceType level) const
+      {
+        return GetChildrenInformation(level).GetIdentifiers();
+      }
+
+      void AddChildrenMetadataValue(ResourceType level,
+                                    MetadataType metadata,
+                                    const std::string& value)
+      {
+        GetChildrenInformation(level).AddMetadataValue(metadata, value);
+      }
+
+      void GetChildrenMetadataValues(std::set<std::string>& values,
+                                     ResourceType level,
+                                     MetadataType metadata) const
+      {
+        GetChildrenInformation(level).GetMetadataValues(values, metadata);
+      }
+
+      void AddChildrenMainDicomTagValue(ResourceType level,
+                                        const DicomTag& tag,
+                                        const std::string& value)
+      {
+        GetChildrenInformation(level).AddMainDicomTagValue(tag, value);
+      }
+
+      void GetChildrenMainDicomTagValues(std::set<std::string>& values,
+                                         ResourceType level,
+                                         const DicomTag& tag) const
+      {
+        GetChildrenInformation(level).GetMainDicomTagValues(values, tag);
+      }
+
+      void AddLabel(const std::string& label);
+
+      std::set<std::string>& GetLabels()
+      {
+        return labels_;
+      }
+
+      const std::set<std::string>& GetLabels() const
+      {
+        return labels_;
+      }
+
+      void AddAttachment(const FileInfo& attachment);
+
+      bool LookupAttachment(FileInfo& target,
+                            FileContentType type) const;
+
+      const std::map<FileContentType, FileInfo>& GetAttachments() const
+      {
+        return attachments_;
+      }
+
+      const std::string& GetOneInstanceIdentifier() const;
+
+      bool HasOneInstanceIdentifier() const
+      {
+        return !GetChildrenIdentifiers(ResourceType_Instance).empty();
+      }
+
+      void DebugExport(Json::Value& target,
+                       const FindRequest& request) const;
+    };
+
+  private:
+    typedef std::map<std::string, Resource*>  Index;
+
+    std::deque<Resource*>  items_;
+    Index                  index_;
+
+  public:
+    ~FindResponse();
+
+    void Add(Resource* item /* takes ownership */);
+
+    size_t GetSize() const
+    {
+      return items_.size();
+    }
+
+    const Resource& GetResourceByIndex(size_t index) const;
+
+    Resource& GetResourceByIdentifier(const std::string& id);
+
+    const Resource& GetResourceByIdentifier(const std::string& id) const
+    {
+      return const_cast<FindResponse&>(*this).GetResourceByIdentifier(id);
+    }
+
+    bool HasResource(const std::string& id) const
+    {
+      return (index_.find(id) != index_.end());
+    }
+  };
+}
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Mon Sep 02 17:17:22 2024 +0200
@@ -29,6 +29,8 @@
 #include "../ExportedResource.h"
 #include "../Search/ISqlLookupFormatter.h"
 #include "../ServerIndexChange.h"
+#include "FindRequest.h"
+#include "FindResponse.h"
 #include "IDatabaseListener.h"
 
 #include <list>
@@ -52,6 +54,7 @@
       bool hasAtomicIncrementGlobalProperty_;
       bool hasUpdateAndGetStatistics_;
       bool hasMeasureLatency_;
+      bool hasFindSupport_;
 
     public:
       Capabilities() :
@@ -60,7 +63,8 @@
         hasLabelsSupport_(false),
         hasAtomicIncrementGlobalProperty_(false),
         hasUpdateAndGetStatistics_(false),
-        hasMeasureLatency_(false)
+        hasMeasureLatency_(false),
+        hasFindSupport_(false)
       {
       }
 
@@ -123,6 +127,16 @@
       {
         return hasMeasureLatency_;
       }
+
+      void SetHasFindSupport(bool value)
+      {
+        hasFindSupport_ = value;
+      }
+
+      bool HasFindSupport() const
+      {
+        return hasFindSupport_;
+      }
     };
 
 
@@ -351,6 +365,34 @@
                                           int64_t& instancesCount,
                                           int64_t& compressedSize,
                                           int64_t& uncompressedSize) = 0;
+
+      /**
+       * Primitives introduced in Orthanc 1.12.4
+       **/
+
+      // This is only implemented if "HasIntegratedFind()" is "true"
+      virtual void ExecuteFind(FindResponse& response,
+                               const FindRequest& request,
+                               const Capabilities& capabilities) = 0;
+
+      // This is only implemented if "HasIntegratedFind()" is "false"
+      virtual void ExecuteFind(std::list<std::string>& identifiers,
+                               const Capabilities& capabilities,
+                               const FindRequest& request) = 0;
+
+      /**
+       * This is only implemented if "HasIntegratedFind()" is
+       * "false". In this flavor, the resource of interest might have
+       * been deleted, as the expansion is not done in the same
+       * transaction as the "ExecuteFind()". In such cases, the
+       * wrapper should not throw an exception, but simply ignore the
+       * request to expand the resource (i.e., "response" must not be
+       * modified).
+       **/
+      virtual void ExecuteExpand(FindResponse& response,
+                                 const Capabilities& capabilities,
+                                 const FindRequest& request,
+                                 const std::string& identifier) = 0;
     };
 
 
@@ -375,5 +417,9 @@
     virtual const Capabilities GetDatabaseCapabilities() const = 0;
 
     virtual uint64_t MeasureLatency() = 0;
+
+    // Returns "true" iff. the database engine supports the
+    // simultaneous find and expansion of resources.
+    virtual bool HasIntegratedFind() const = 0;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,142 @@
+/**
+ * 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 "../PrecompiledHeadersServer.h"
+#include "MainDicomTagsRegistry.h"
+
+#include "../ServerToolbox.h"
+
+namespace Orthanc
+{
+  void MainDicomTagsRegistry::LoadTags(ResourceType level)
+  {
+    {
+      const DicomTag* tags = NULL;
+      size_t size;
+
+      ServerToolbox::LoadIdentifiers(tags, size, level);
+
+      for (size_t i = 0; i < size; i++)
+      {
+        if (registry_.find(tags[i]) == registry_.end())
+        {
+          registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier);
+        }
+        else
+        {
+          // These patient-level tags are copied in the study level
+          assert(level == ResourceType_Study &&
+                 (tags[i] == DICOM_TAG_PATIENT_ID ||
+                  tags[i] == DICOM_TAG_PATIENT_NAME ||
+                  tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
+        }
+      }
+    }
+
+    {
+      std::set<DicomTag> tags;
+      DicomMap::GetMainDicomTags(tags, level);
+
+      for (std::set<DicomTag>::const_iterator
+             tag = tags.begin(); tag != tags.end(); ++tag)
+      {
+        if (registry_.find(*tag) == registry_.end())
+        {
+          registry_[*tag] = TagInfo(level, DicomTagType_Main);
+        }
+      }
+    }
+  }
+
+
+  MainDicomTagsRegistry::MainDicomTagsRegistry()
+  {
+    LoadTags(ResourceType_Patient);
+    LoadTags(ResourceType_Study);
+    LoadTags(ResourceType_Series);
+    LoadTags(ResourceType_Instance);
+  }
+
+
+  void MainDicomTagsRegistry::LookupTag(ResourceType& level,
+                                        DicomTagType& type,
+                                        const DicomTag& tag) const
+  {
+    Registry::const_iterator it = registry_.find(tag);
+
+    if (it == registry_.end())
+    {
+      // Default values
+      level = ResourceType_Instance;
+      type = DicomTagType_Generic;
+    }
+    else
+    {
+      level = it->second.GetLevel();
+      type = it->second.GetType();
+    }
+  }
+
+
+  bool MainDicomTagsRegistry::NormalizeLookup(DatabaseConstraints& target,
+                                              const DatabaseLookup& source,
+                                              ResourceType queryLevel) const
+  {
+    bool isEquivalentLookup = true;
+
+    target.Clear();
+
+    for (size_t i = 0; i < source.GetConstraintsCount(); i++)
+    {
+      ResourceType level;
+      DicomTagType type;
+
+      LookupTag(level, type, source.GetConstraint(i).GetTag());
+
+      if (type == DicomTagType_Identifier ||
+          type == DicomTagType_Main)
+      {
+        // Use the fact that patient-level tags are copied at the study level
+        if (level == ResourceType_Patient &&
+            queryLevel != ResourceType_Patient)
+        {
+          level = ResourceType_Study;
+        }
+
+        bool isEquivalentConstraint;
+        target.AddConstraint(source.GetConstraint(i).ConvertToDatabaseConstraint(isEquivalentConstraint, level, type));
+
+        if (!isEquivalentConstraint)
+        {
+          isEquivalentLookup = false;
+        }
+      }
+      else
+      {
+        isEquivalentLookup = false;
+      }
+    }
+
+    return isEquivalentLookup;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.h	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,88 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "../Search/DatabaseLookup.h"
+
+#include <boost/noncopyable.hpp>
+
+
+namespace Orthanc
+{
+  class MainDicomTagsRegistry : public boost::noncopyable
+  {
+  private:
+    class TagInfo
+    {
+    private:
+      ResourceType  level_;
+      DicomTagType  type_;
+
+    public:
+      TagInfo()
+      {
+      }
+
+      TagInfo(ResourceType level,
+              DicomTagType type) :
+        level_(level),
+        type_(type)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      DicomTagType GetType() const
+      {
+        return type_;
+      }
+    };
+
+    typedef std::map<DicomTag, TagInfo>   Registry;
+
+    Registry  registry_;
+
+    void LoadTags(ResourceType level);
+
+  public:
+    MainDicomTagsRegistry();
+
+    void LookupTag(ResourceType& level,
+                   DicomTagType& type,
+                   const DicomTag& tag) const;
+
+    /**
+     * Returns "true" iff. the normalized lookup is the same as the
+     * original DatabaseLookup. If "false" is returned, the target
+     * constraints are less strict than the original DatabaseLookup,
+     * so more resources will match them.
+     **/
+    bool NormalizeLookup(DatabaseConstraints& target,
+                         const DatabaseLookup& source,
+                         ResourceType queryLevel) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/OrthancIdentifiers.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,243 @@
+/**
+ * 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 "FindRequest.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+
+namespace Orthanc
+{
+  OrthancIdentifiers::OrthancIdentifiers(const OrthancIdentifiers& other)
+  {
+    if (other.HasPatientId())
+    {
+      SetPatientId(other.GetPatientId());
+    }
+
+    if (other.HasStudyId())
+    {
+      SetStudyId(other.GetStudyId());
+    }
+
+    if (other.HasSeriesId())
+    {
+      SetSeriesId(other.GetSeriesId());
+    }
+
+    if (other.HasInstanceId())
+    {
+      SetInstanceId(other.GetInstanceId());
+    }
+  }
+
+
+  void OrthancIdentifiers::SetPatientId(const std::string& id)
+  {
+    if (HasPatientId())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      patientId_.reset(new std::string(id));
+    }
+  }
+
+
+  const std::string& OrthancIdentifiers::GetPatientId() const
+  {
+    if (HasPatientId())
+    {
+      return *patientId_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void OrthancIdentifiers::SetStudyId(const std::string& id)
+  {
+    if (HasStudyId())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      studyId_.reset(new std::string(id));
+    }
+  }
+
+
+  const std::string& OrthancIdentifiers::GetStudyId() const
+  {
+    if (HasStudyId())
+    {
+      return *studyId_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void OrthancIdentifiers::SetSeriesId(const std::string& id)
+  {
+    if (HasSeriesId())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      seriesId_.reset(new std::string(id));
+    }
+  }
+
+
+  const std::string& OrthancIdentifiers::GetSeriesId() const
+  {
+    if (HasSeriesId())
+    {
+      return *seriesId_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void OrthancIdentifiers::SetInstanceId(const std::string& id)
+  {
+    if (HasInstanceId())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      instanceId_.reset(new std::string(id));
+    }
+  }
+
+
+  const std::string& OrthancIdentifiers::GetInstanceId() const
+  {
+    if (HasInstanceId())
+    {
+      return *instanceId_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  ResourceType OrthancIdentifiers::DetectLevel() const
+  {
+    if (HasPatientId() &&
+        !HasStudyId() &&
+        !HasSeriesId() &&
+        !HasInstanceId())
+    {
+      return ResourceType_Patient;
+    }
+    else if (HasPatientId() &&
+             HasStudyId() &&
+             !HasSeriesId() &&
+             !HasInstanceId())
+    {
+      return ResourceType_Study;
+    }
+    else if (HasPatientId() &&
+             HasStudyId() &&
+             HasSeriesId() &&
+             !HasInstanceId())
+    {
+      return ResourceType_Series;
+    }
+    else if (HasPatientId() &&
+             HasStudyId() &&
+             HasSeriesId() &&
+             HasInstanceId())
+    {
+      return ResourceType_Instance;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+  }
+
+
+  void OrthancIdentifiers::SetLevel(ResourceType level,
+                                    const std::string& id)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        SetPatientId(id);
+        break;
+
+      case ResourceType_Study:
+        SetStudyId(id);
+        break;
+
+      case ResourceType_Series:
+        SetSeriesId(id);
+        break;
+
+      case ResourceType_Instance:
+        SetInstanceId(id);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  std::string OrthancIdentifiers::GetLevel(ResourceType level) const
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return GetPatientId();
+
+      case ResourceType_Study:
+        return GetStudyId();
+
+      case ResourceType_Series:
+        return GetSeriesId();
+
+      case ResourceType_Instance:
+        return GetInstanceId();
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/OrthancIdentifiers.h	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,93 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../OrthancFramework/Sources/Enumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <string>
+
+
+namespace Orthanc
+{
+  class OrthancIdentifiers : public boost::noncopyable
+  {
+  private:
+    std::unique_ptr<std::string>  patientId_;
+    std::unique_ptr<std::string>  studyId_;
+    std::unique_ptr<std::string>  seriesId_;
+    std::unique_ptr<std::string>  instanceId_;
+
+  public:
+    OrthancIdentifiers()
+    {
+    }
+
+    OrthancIdentifiers(const OrthancIdentifiers& other);
+
+    void SetPatientId(const std::string& id);
+
+    bool HasPatientId() const
+    {
+      return patientId_.get() != NULL;
+    }
+
+    const std::string& GetPatientId() const;
+
+    void SetStudyId(const std::string& id);
+
+    bool HasStudyId() const
+    {
+      return studyId_.get() != NULL;
+    }
+
+    const std::string& GetStudyId() const;
+
+    void SetSeriesId(const std::string& id);
+
+    bool HasSeriesId() const
+    {
+      return seriesId_.get() != NULL;
+    }
+
+    const std::string& GetSeriesId() const;
+
+    void SetInstanceId(const std::string& id);
+
+    bool HasInstanceId() const
+    {
+      return instanceId_.get() != NULL;
+    }
+
+    const std::string& GetInstanceId() const;
+
+    ResourceType DetectLevel() const;
+
+    void SetLevel(ResourceType level,
+                  const std::string& id);
+
+    std::string GetLevel(ResourceType level) const;
+  };
+}
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -29,6 +29,7 @@
 #include "../../../OrthancFramework/Sources/SQLite/Transaction.h"
 #include "../Search/ISqlLookupFormatter.h"
 #include "../ServerToolbox.h"
+#include "Compatibility/GenericFind.h"
 #include "Compatibility/ICreateInstance.h"
 #include "Compatibility/IGetChildrenMetadata.h"
 #include "Compatibility/ILookupResourceAndParent.h"
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -300,141 +300,6 @@
   }
 
 
-  class StatelessDatabaseOperations::MainDicomTagsRegistry : public boost::noncopyable
-  {
-  private:
-    class TagInfo
-    {
-    private:
-      ResourceType  level_;
-      DicomTagType  type_;
-
-    public:
-      TagInfo()
-      {
-      }
-
-      TagInfo(ResourceType level,
-              DicomTagType type) :
-        level_(level),
-        type_(type)
-      {
-      }
-
-      ResourceType GetLevel() const
-      {
-        return level_;
-      }
-
-      DicomTagType GetType() const
-      {
-        return type_;
-      }
-    };
-      
-    typedef std::map<DicomTag, TagInfo>   Registry;
-
-
-    Registry  registry_;
-      
-    void LoadTags(ResourceType level)
-    {
-      {
-        const DicomTag* tags = NULL;
-        size_t size;
-  
-        ServerToolbox::LoadIdentifiers(tags, size, level);
-  
-        for (size_t i = 0; i < size; i++)
-        {
-          if (registry_.find(tags[i]) == registry_.end())
-          {
-            registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier);
-          }
-          else
-          {
-            // These patient-level tags are copied in the study level
-            assert(level == ResourceType_Study &&
-                   (tags[i] == DICOM_TAG_PATIENT_ID ||
-                    tags[i] == DICOM_TAG_PATIENT_NAME ||
-                    tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
-          }
-        }
-      }
-
-      {
-        std::set<DicomTag> tags;
-        DicomMap::GetMainDicomTags(tags, level);
-
-        for (std::set<DicomTag>::const_iterator
-               tag = tags.begin(); tag != tags.end(); ++tag)
-        {
-          if (registry_.find(*tag) == registry_.end())
-          {
-            registry_[*tag] = TagInfo(level, DicomTagType_Main);
-          }
-        }
-      }
-    }
-
-    void LookupTag(ResourceType& level,
-                   DicomTagType& type,
-                   const DicomTag& tag) const
-    {
-      Registry::const_iterator it = registry_.find(tag);
-
-      if (it == registry_.end())
-      {
-        // Default values
-        level = ResourceType_Instance;
-        type = DicomTagType_Generic;
-      }
-      else
-      {
-        level = it->second.GetLevel();
-        type = it->second.GetType();
-      }
-    }
-
-  public:
-    MainDicomTagsRegistry()
-    {
-      LoadTags(ResourceType_Patient);
-      LoadTags(ResourceType_Study);
-      LoadTags(ResourceType_Series);
-      LoadTags(ResourceType_Instance);
-    }
-
-    void NormalizeLookup(DatabaseConstraints& target,
-                         const DatabaseLookup& source,
-                         ResourceType queryLevel) const
-    {
-      target.Clear();
-
-      for (size_t i = 0; i < source.GetConstraintsCount(); i++)
-      {
-        ResourceType level;
-        DicomTagType type;
-
-        LookupTag(level, type, source.GetConstraint(i).GetTag());
-
-        if (type == DicomTagType_Identifier ||
-            type == DicomTagType_Main)
-        {
-          // Use the fact that patient-level tags are copied at the study level
-          if (level == ResourceType_Patient &&
-              queryLevel != ResourceType_Patient)
-          {
-            level = ResourceType_Study;
-          }
-
-          target.AddConstraint(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type));
-        }
-      }
-    }
-  };
-
-
   void StatelessDatabaseOperations::ReadWriteTransaction::LogChange(int64_t internalId,
                                                                     ChangeType changeType,
                                                                     ResourceType resourceType,
@@ -902,7 +767,7 @@
               Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags);
 
               while ((target.missingRequestedTags_.size() > 0)
-                    && currentLevel != ResourceType_Patient)
+                     && currentLevel != ResourceType_Patient)
               {
                 currentLevel = GetParentResourceType(currentLevel);
 
@@ -1687,7 +1552,8 @@
     DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true);
 
     DatabaseConstraints query;
-    query.AddConstraint(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+    bool isIdentical;  // unused
+    query.AddConstraint(c.ConvertToDatabaseConstraint(isIdentical, level, DicomTagType_Identifier));
 
 
     class Operations : public IReadOnlyOperations
@@ -2515,7 +2381,7 @@
             catch (boost::bad_lexical_cast&)
             {
               LOG(ERROR) << "Cannot read the global sequence "
-                        << boost::lexical_cast<std::string>(sequence_) << ", resetting it";
+                         << boost::lexical_cast<std::string>(sequence_) << ", resetting it";
               oldValue = 0;
             }
 
@@ -2814,8 +2680,8 @@
 
     public:
       explicit Operations(const ParsedDicomFile& dicom, bool limitToThisLevelDicomTags, ResourceType limitToLevel)
-      : limitToThisLevelDicomTags_(limitToThisLevelDicomTags),
-        limitToLevel_(limitToLevel)
+        : limitToThisLevelDicomTags_(limitToThisLevelDicomTags),
+          limitToLevel_(limitToLevel)
       {
         OrthancConfiguration::DefaultExtractDicomSummary(summary_, dicom);
         hasher_.reset(new DicomInstanceHasher(summary_));
@@ -2940,7 +2806,7 @@
   }                                                                           
 
   bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount,
-                                                                                   const std::string& patientId)
+                                                                                    const std::string& patientId)
   {
     if (maximumPatientCount != 0)
     {
@@ -3042,7 +2908,7 @@
     };
 
     if (maximumStorageMode == MaxStorageMode_Recycle 
-      && (maximumStorageSize != 0 || maximumPatientCount != 0))
+        && (maximumStorageSize != 0 || maximumPatientCount != 0))
     {
       Operations operations(maximumStorageSize, maximumPatientCount);
       Apply(operations);
@@ -3106,9 +2972,9 @@
       }
 
       static void SetMainDicomSequenceMetadata(ResourcesContent& content,
-                                                int64_t resource,
-                                                const DicomMap& dicomSummary,
-                                                ResourceType level)
+                                               int64_t resource,
+                                               const DicomMap& dicomSummary,
+                                               ResourceType level)
       {
         std::string serialized;
         GetMainDicomSequenceMetadataContent(serialized, dicomSummary, level);
@@ -3301,7 +3167,7 @@
         // Ensure there is enough room in the storage for the new instance
         uint64_t instanceSize = 0;
         for (Attachments::const_iterator it = attachments_.begin();
-              it != attachments_.end(); ++it)
+             it != attachments_.end(); ++it)
         {
           instanceSize += it->GetCompressedSize();
         }
@@ -3330,7 +3196,7 @@
     
         // Attach the files to the newly created instance
         for (Attachments::const_iterator it = attachments_.begin();
-              it != attachments_.end(); ++it)
+             it != attachments_.end(); ++it)
         {
           if (isReconstruct_)
           {
@@ -3806,4 +3672,76 @@
     boost::shared_lock<boost::shared_mutex> lock(mutex_);
     return db_.GetDatabaseCapabilities().HasLabelsSupport();
   }
+
+
+  void StatelessDatabaseOperations::ExecuteFind(FindResponse& response,
+                                                const FindRequest& request)
+  {
+    class IntegratedFind : public ReadOnlyOperationsT3<FindResponse&, const FindRequest&,
+                                                       const IDatabaseWrapper::Capabilities&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>(), tuple.get<2>());
+      }
+    };
+
+    class FindStage : public ReadOnlyOperationsT3<std::list<std::string>&, const IDatabaseWrapper::Capabilities&, const FindRequest& >
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>(), tuple.get<2>());
+      }
+    };
+
+    class ExpandStage : public ReadOnlyOperationsT4<FindResponse&, const IDatabaseWrapper::Capabilities&, const FindRequest&, const std::string&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.ExecuteExpand(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
+      }
+    };
+
+    IDatabaseWrapper::Capabilities capabilities = db_.GetDatabaseCapabilities();
+
+    if (db_.HasIntegratedFind())
+    {
+      /**
+       * In this flavor, the "find" and the "expand" phases are
+       * executed in one single transaction.
+       **/
+      IntegratedFind operations;
+      operations.Apply(*this, response, request, capabilities);
+    }
+    else
+    {
+      /**
+       * In this flavor, the "find" and the "expand" phases for each
+       * found resource are executed in distinct transactions. This is
+       * the compatibility mode equivalent to Orthanc <= 1.12.3.
+       **/
+      std::list<std::string> identifiers;
+
+      FindStage find;
+      find.Apply(*this, identifiers, capabilities, request);
+
+      ExpandStage expand;
+
+      for (std::list<std::string>::const_iterator it = identifiers.begin(); it != identifiers.end(); ++it)
+      {
+        /**
+         * Not that the resource might have been deleted (as we are in
+         * another transaction). The database engine must ignore such
+         * error cases.
+         **/
+        expand.Apply(*this, response, capabilities, request, *it);
+      }
+    }
+  }
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Mon Sep 02 17:17:22 2024 +0200
@@ -25,8 +25,9 @@
 
 #include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
 
+#include "../DicomInstanceOrigin.h"
 #include "IDatabaseWrapper.h"
-#include "../DicomInstanceOrigin.h"
+#include "MainDicomTagsRegistry.h"
 
 #include <boost/shared_ptr.hpp>
 #include <boost/thread/shared_mutex.hpp>
@@ -80,7 +81,7 @@
       indexInSeries_(0)
     {
     }
-    
+
     void SetResource(ResourceType level,
                      const std::string& id)
     {
@@ -112,15 +113,26 @@
   enum ExpandResourceFlags
   {
     ExpandResourceFlags_None                    = 0,
+    // used to fetch from DB and for output
     ExpandResourceFlags_IncludeMetadata         = (1 << 0),
     ExpandResourceFlags_IncludeChildren         = (1 << 1),
     ExpandResourceFlags_IncludeMainDicomTags    = (1 << 2),
     ExpandResourceFlags_IncludeLabels           = (1 << 3),
 
-    ExpandResourceFlags_Default = (ExpandResourceFlags_IncludeMetadata |
-                                     ExpandResourceFlags_IncludeChildren |
-                                     ExpandResourceFlags_IncludeMainDicomTags |
-                                     ExpandResourceFlags_IncludeLabels)
+    // only used for output
+    ExpandResourceFlags_IncludeAllMetadata      = (1 << 4),  // new in Orthanc 1.12.4
+    ExpandResourceFlags_IncludeIsStable         = (1 << 5),  // new in Orthanc 1.12.4
+
+    ExpandResourceFlags_DefaultExtract = (ExpandResourceFlags_IncludeMetadata |
+                                          ExpandResourceFlags_IncludeChildren |
+                                          ExpandResourceFlags_IncludeMainDicomTags |
+                                          ExpandResourceFlags_IncludeLabels),
+
+    ExpandResourceFlags_DefaultOutput = (ExpandResourceFlags_IncludeMetadata |
+                                         ExpandResourceFlags_IncludeChildren |
+                                         ExpandResourceFlags_IncludeMainDicomTags |
+                                         ExpandResourceFlags_IncludeLabels |
+                                         ExpandResourceFlags_IncludeIsStable)
   };
 
   class StatelessDatabaseOperations : public boost::noncopyable
@@ -378,6 +390,28 @@
       {
         transaction_.ListAllLabels(target);
       }
+
+      void ExecuteFind(FindResponse& response,
+                       const FindRequest& request,
+                       const IDatabaseWrapper::Capabilities& capabilities)
+      {
+        transaction_.ExecuteFind(response, request, capabilities);
+      }
+
+      void ExecuteFind(std::list<std::string>& identifiers,
+                       const IDatabaseWrapper::Capabilities& capabilities,
+                       const FindRequest& request)
+      {
+        transaction_.ExecuteFind(identifiers, capabilities, request);
+      }
+
+      void ExecuteExpand(FindResponse& response,
+                         const IDatabaseWrapper::Capabilities& capabilities,
+                         const FindRequest& request,
+                         const std::string& identifier)
+      {
+        transaction_.ExecuteExpand(response, capabilities, request, identifier);
+      }
     };
 
 
@@ -545,7 +579,6 @@
     
 
   private:
-    class MainDicomTagsRegistry;
     class Transaction;
 
     IDatabaseWrapper&                            db_;
@@ -798,5 +831,8 @@
                    const std::set<std::string>& labels);
 
     bool HasLabelsSupport();
+
+    void ExecuteFind(FindResponse& response,
+                     const FindRequest& request);
   };
 }
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -30,6 +30,7 @@
 #include "../../OrthancFramework/Sources/Lua/LuaFunctionCall.h"
 #include "../../OrthancFramework/Sources/MetricsRegistry.h"
 #include "OrthancConfiguration.h"
+#include "ResourceFinder.cpp"
 #include "Search/DatabaseLookup.h"
 #include "ServerContext.h"
 #include "ServerToolbox.h"
@@ -39,6 +40,48 @@
 
 namespace Orthanc
 {
+  static void CopySequence(ParsedDicomFile& dicom,
+                           const DicomTag& tag,
+                           const Json::Value& source,
+                           const std::string& defaultPrivateCreator,
+                           const std::map<uint16_t, std::string>& privateCreators)
+  {
+    if (source.type() == Json::objectValue &&
+        source.isMember("Type") &&
+        source.isMember("Value") &&
+        source["Type"].asString() == "Sequence" &&
+        source["Value"].type() == Json::arrayValue)
+    {
+      Json::Value content = Json::arrayValue;
+
+      for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++)
+      {
+        Json::Value item;
+        Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short);
+        content.append(item);
+      }
+
+      if (tag.IsPrivate())
+      {
+        std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(tag.GetGroup());
+
+        if (found != privateCreators.end())
+        {
+          dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str());
+        }
+        else
+        {
+          dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator);
+        }
+      }
+      else
+      {
+        dicom.Replace(tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */);
+      }
+    }
+  }
+
+
   static void AddAnswer(DicomFindAnswers& answers,
                         ServerContext& context,
                         const std::string& publicId,
@@ -126,39 +169,7 @@
         assert(dicomAsJson != NULL);
         const Json::Value& source = (*dicomAsJson) [tag->Format()];
 
-        if (source.type() == Json::objectValue &&
-            source.isMember("Type") &&
-            source.isMember("Value") &&
-            source["Type"].asString() == "Sequence" &&
-            source["Value"].type() == Json::arrayValue)
-        {
-          Json::Value content = Json::arrayValue;
-
-          for (Json::Value::ArrayIndex i = 0; i < source["Value"].size(); i++)
-          {
-            Json::Value item;
-            Toolbox::SimplifyDicomAsJson(item, source["Value"][i], DicomToJsonFormat_Short);
-            content.append(item);
-          }
-
-          if (tag->IsPrivate())
-          {
-            std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(tag->GetGroup());
-            
-            if (found != privateCreators.end())
-            {
-              dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str());
-            }
-            else
-            {
-              dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator);
-            }
-          }
-          else
-          {
-            dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */);
-          }
-        }
+        CopySequence(dicom, *tag, source, defaultPrivateCreator, privateCreators);
       }
 
       answers.Add(dicom);
@@ -319,6 +330,124 @@
   };
 
 
+  namespace
+  {
+    class LookupVisitorV2 : public ResourceFinder::IVisitor
+    {
+    private:
+      DicomFindAnswers&           answers_;
+      DicomArray                  queryAsArray_;
+      const std::list<DicomTag>&  sequencesToReturn_;
+      std::string                 defaultPrivateCreator_;       // the private creator to use if the group is not defined in the query itself
+      const std::map<uint16_t, std::string>& privateCreators_;  // the private creators defined in the query itself
+      std::string                 retrieveAet_;
+
+    public:
+      LookupVisitorV2(DicomFindAnswers& answers,
+                      const DicomMap& query,
+                      const std::list<DicomTag>& sequencesToReturn,
+                      const std::map<uint16_t, std::string>& privateCreators) :
+        answers_(answers),
+        queryAsArray_(query),
+        sequencesToReturn_(sequencesToReturn),
+        privateCreators_(privateCreators)
+      {
+        answers_.SetComplete(false);
+
+        {
+          OrthancConfiguration::ReaderLock lock;
+          defaultPrivateCreator_ = lock.GetConfiguration().GetDefaultPrivateCreator();
+          retrieveAet_ = lock.GetConfiguration().GetOrthancAET();
+        }
+      }
+
+      virtual void Apply(const FindResponse::Resource& resource,
+                         const DicomMap& requestedTags) ORTHANC_OVERRIDE
+      {
+        DicomMap resourceTags;
+        resource.GetAllMainDicomTags(resourceTags);
+        resourceTags.Merge(requestedTags);
+
+        DicomMap result;
+
+        /**
+         * Add the mandatory "Retrieve AE Title (0008,0054)" tag, which was missing in Orthanc <= 1.7.2.
+         * http://dicom.nema.org/medical/dicom/current/output/html/part04.html#sect_C.4.1.1.3.2
+         * https://groups.google.com/g/orthanc-users/c/-7zNTKR_PMU/m/kfjwzEVNAgAJ
+         **/
+        result.SetValue(DICOM_TAG_RETRIEVE_AE_TITLE, retrieveAet_, false /* not binary */);
+
+        for (size_t i = 0; i < queryAsArray_.GetSize(); i++)
+        {
+          const DicomTag tag = queryAsArray_.GetElement(i).GetTag();
+
+          if (tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL)
+          {
+            // Fix issue 30 on Google Code (QR response missing "Query/Retrieve Level" (008,0052))
+            result.SetValue(tag, queryAsArray_.GetElement(i).GetValue());
+          }
+          else if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
+          {
+            // Do not include the encoding, this is handled by class ParsedDicomFile
+          }
+          else
+          {
+            const DicomValue* value = resourceTags.TestAndGetValue(tag);
+
+            if (value == NULL ||
+                value->IsNull() ||
+                value->IsBinary())
+            {
+              result.SetValue(tag, "", false);
+            }
+            else
+            {
+              result.SetValue(tag, value->GetContent(), false);
+            }
+          }
+        }
+
+        if (result.GetSize() == 0 &&
+            sequencesToReturn_.empty())
+        {
+          CLOG(WARNING, DICOM) << "The C-FIND request does not return any DICOM tag";
+        }
+        else if (sequencesToReturn_.empty())
+        {
+          answers_.Add(result);
+        }
+        else
+        {
+          ParsedDicomFile dicom(result, GetDefaultDicomEncoding(),
+                                true /* be permissive, cf. issue #136 */, defaultPrivateCreator_, privateCreators_);
+
+          for (std::list<DicomTag>::const_iterator tag = sequencesToReturn_.begin();
+               tag != sequencesToReturn_.end(); ++tag)
+          {
+            const DicomValue* value = resourceTags.TestAndGetValue(*tag);
+            if (value != NULL &&
+                value->IsSequence())
+            {
+              CopySequence(dicom, *tag, value->GetSequenceContent(), defaultPrivateCreator_, privateCreators_);
+            }
+            else
+            {
+              dicom.Replace(*tag, std::string(""), false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator_);
+            }
+          }
+
+          answers_.Add(dicom);
+        }
+      }
+
+      virtual void MarkAsComplete() ORTHANC_OVERRIDE
+      {
+        answers_.SetComplete(true);
+      }
+    };
+  }
+
+
   void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
                                          const DicomMap& input,
                                          const std::list<DicomTag>& sequencesToReturn,
@@ -396,7 +525,6 @@
       throw OrthancException(ErrorCode_NotImplemented);
     }
 
-
     DicomArray query(*filteredInput);
     CLOG(INFO, DICOM) << "DICOM C-Find request at level: " << EnumerationToString(level);
 
@@ -410,9 +538,12 @@
       }
     }
 
+    std::set<DicomTag> requestedTags;
+
     for (std::list<DicomTag>::const_iterator it = sequencesToReturn.begin();
          it != sequencesToReturn.end(); ++it)
     {
+      requestedTags.insert(*it);
       CLOG(INFO, DICOM) << "  (" << it->Format()
                         << ")  " << FromDcmtkBridge::GetTagName(*it, "")
                         << " : sequence tag whose content will be copied";
@@ -441,18 +572,26 @@
       const DicomTag tag = element.GetTag();
 
       // remove tags that are not used for matching
-      if (element.GetValue().IsNull() ||
-          tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
+      if (tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
           tag == DICOM_TAG_SPECIFIC_CHARACTER_SET ||
           tag == DICOM_TAG_TIMEZONE_OFFSET_FROM_UTC)  // time zone is not directly used for matching.  Once we support "Timezone query adjustment", we may use it to adjust date-time filters but for now, just ignore it 
       {
         continue;
       }
 
+      requestedTags.insert(tag);
+
+      if (element.GetValue().IsNull())
+      {
+        // There is no constraint on this tag
+        continue;
+      }
+
       std::string value = element.GetValue().GetContent();
       if (value.size() == 0)
       {
         // An empty string corresponds to an universal constraint, so we ignore it
+        requestedTags.insert(tag);
         continue;
       }
 
@@ -486,8 +625,28 @@
     size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_;
 
 
-    LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn, privateCreators, context_.GetFindStorageAccessMode());
-    context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit);
+    if (true)
+    {
+      /**
+       * EXPERIMENTAL VERSION
+       **/
+
+      ResourceFinder finder(level, false /* don't expand */);
+      finder.SetDatabaseLookup(lookup);
+      finder.AddRequestedTags(requestedTags);
+
+      LookupVisitorV2 visitor(answers, *filteredInput, sequencesToReturn, privateCreators);
+      finder.Execute(visitor, context_);
+    }
+    else
+    {
+      /**
+       * VERSION IN ORTHANC <= 1.12.4
+       **/
+
+      LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn, privateCreators, context_.GetFindStorageAccessMode());
+      context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit);
+    }
   }
 
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -22,6 +22,8 @@
 
 
 #include "../PrecompiledHeadersServer.h"
+#include "../ResourceFinder.h"
+
 #include "OrthancRestApi.h"
 
 #include "../../../OrthancFramework/Sources/Compression/GzipCompressor.h"
@@ -127,9 +129,24 @@
   }
 
 
+  static bool ExpandResource(Json::Value& target,
+                             ServerContext& context,
+                             ResourceType level,
+                             const std::string& identifier,
+                             DicomToJsonFormat format,
+                             bool retrieveMetadata)
+  {
+    ResourceFinder finder(level, true /* expand */);
+    finder.SetOrthancId(level, identifier);
+    finder.SetRetrieveMetadata(retrieveMetadata);
+
+    return finder.ExecuteOneResource(target, context, format, retrieveMetadata);
+  }
+
+
   // List all the patients, studies, series or instances ----------------------
  
-  static void AnswerListOfResources(RestApiOutput& output,
+  static void AnswerListOfResources1(RestApiOutput& output,
                                     ServerContext& context,
                                     const std::list<std::string>& resources,
                                     const std::map<std::string, std::string>& instancesIds, // optional: the id of an instance for each found resource.
@@ -183,7 +200,7 @@
   }
 
 
-  static void AnswerListOfResources(RestApiOutput& output,
+  static void AnswerListOfResources2(RestApiOutput& output,
                                     ServerContext& context,
                                     const std::list<std::string>& resources,
                                     ResourceType level,
@@ -196,7 +213,7 @@
     std::map<std::string, boost::shared_ptr<DicomMap> > unusedResourcesMainDicomTags;
     std::map<std::string, boost::shared_ptr<Json::Value> > unusedResourcesDicomAsJson;
 
-    AnswerListOfResources(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess);
+    AnswerListOfResources1(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess);
   }
 
 
@@ -226,41 +243,92 @@
     ServerIndex& index = OrthancRestApi::GetIndex(call);
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    std::list<std::string> result;
-
-    std::set<DicomTag> requestedTags;
-    OrthancRestApi::GetRequestedTags(requestedTags, call);
-
-    if (call.HasArgument("limit") ||
-        call.HasArgument("since"))
+    if (true)
     {
-      if (!call.HasArgument("limit"))
+      /**
+       * EXPERIMENTAL VERSION
+       **/
+
+      // TODO-FIND: include the FindRequest options parsing in a method (parse from get-arguments and from post payload)
+      // TODO-FIND: support other values for expand like expand=MainDicomTags,Labels,Parent,SeriesStatus
+      const bool expand = (call.HasArgument("expand") &&
+                           call.GetBooleanArgument("expand", true));
+
+      std::set<DicomTag> requestedTags;
+      OrthancRestApi::GetRequestedTags(requestedTags, call);
+
+      ResourceFinder finder(resourceType, expand);
+      finder.AddRequestedTags(requestedTags);
+
+      if (call.HasArgument("limit") ||
+          call.HasArgument("since"))
       {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "Missing \"limit\" argument for GET request against: " +
-                               call.FlattenUri());
+        if (!call.HasArgument("limit"))
+        {
+          throw OrthancException(ErrorCode_BadRequest,
+                                 "Missing \"limit\" argument for GET request against: " +
+                                 call.FlattenUri());
+        }
+
+        if (!call.HasArgument("since"))
+        {
+          throw OrthancException(ErrorCode_BadRequest,
+                                 "Missing \"since\" argument for GET request against: " +
+                                 call.FlattenUri());
+        }
+
+        uint64_t since = boost::lexical_cast<uint64_t>(call.GetArgument("since", ""));
+        uint64_t limit = boost::lexical_cast<uint64_t>(call.GetArgument("limit", ""));
+        finder.SetLimitsSince(since);
+        finder.SetLimitsCount(limit);
       }
 
-      if (!call.HasArgument("since"))
-      {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "Missing \"since\" argument for GET request against: " +
-                               call.FlattenUri());
-      }
-
-      size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", ""));
-      size_t limit = boost::lexical_cast<size_t>(call.GetArgument("limit", ""));
-      index.GetAllUuids(result, resourceType, since, limit);
+      Json::Value answer;
+      finder.Execute(answer, context, OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), false /* no "Metadata" field */);
+      call.GetOutput().AnswerJson(answer);
     }
     else
     {
-      index.GetAllUuids(result, resourceType);
+      /**
+       * VERSION IN ORTHANC <= 1.12.4
+       **/
+
+      std::list<std::string> result;
+
+      std::set<DicomTag> requestedTags;
+      OrthancRestApi::GetRequestedTags(requestedTags, call);
+
+      if (call.HasArgument("limit") ||
+          call.HasArgument("since"))
+      {
+        if (!call.HasArgument("limit"))
+        {
+          throw OrthancException(ErrorCode_BadRequest,
+                                 "Missing \"limit\" argument for GET request against: " +
+                                 call.FlattenUri());
+        }
+
+        if (!call.HasArgument("since"))
+        {
+          throw OrthancException(ErrorCode_BadRequest,
+                                 "Missing \"since\" argument for GET request against: " +
+                                 call.FlattenUri());
+        }
+
+        size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", ""));
+        size_t limit = boost::lexical_cast<size_t>(call.GetArgument("limit", ""));
+        index.GetAllUuids(result, resourceType, since, limit);
+      }
+      else
+      {
+        index.GetAllUuids(result, resourceType);
+      }
+
+      AnswerListOfResources2(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true),
+                            OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human),
+                            requestedTags,
+                            true /* allowStorageAccess */);
     }
-
-    AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true),
-                          OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human),
-                          requestedTags,
-                          true /* allowStorageAccess */);
   }
 
 
@@ -289,11 +357,34 @@
     std::set<DicomTag> requestedTags;
     OrthancRestApi::GetRequestedTags(requestedTags, call);
 
-    Json::Value json;
-    if (OrthancRestApi::GetContext(call).ExpandResource(
-          json, call.GetUriComponent("id", ""), resourceType, format, requestedTags, true /* allowStorageAccess */))
+    if (true)
     {
-      call.GetOutput().AnswerJson(json);
+      /**
+       * EXPERIMENTAL VERSION
+       **/
+
+      ResourceFinder finder(resourceType, true /* expand */);
+      finder.AddRequestedTags(requestedTags);
+      finder.SetOrthancId(resourceType, call.GetUriComponent("id", ""));
+
+      Json::Value json;
+      if (finder.ExecuteOneResource(json, OrthancRestApi::GetContext(call), format, false /* no "Metadata" field */))
+      {
+        call.GetOutput().AnswerJson(json);
+      }
+    }
+    else
+    {
+      /**
+       * VERSION IN ORTHANC <= 1.12.4
+       **/
+
+      Json::Value json;
+      if (OrthancRestApi::GetContext(call).ExpandResource(
+            json, call.GetUriComponent("id", ""), resourceType, format, requestedTags, true /* allowStorageAccess */))
+      {
+        call.GetOutput().AnswerJson(json);
+      }
     }
   }
 
@@ -3140,7 +3231,7 @@
                   bool expand,
                   const std::set<DicomTag>& requestedTags) const
       {
-        AnswerListOfResources(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags, IsStorageAccessAllowedForAnswers(findStorageAccessMode_));
+        AnswerListOfResources1(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags, IsStorageAccessAllowedForAnswers(findStorageAccessMode_));
       }
     };
   }
@@ -3252,8 +3343,140 @@
       throw OrthancException(ErrorCode_BadRequest, 
                              "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be an array of strings");
     }
+    else if (true)
+    {
+      /**
+       * EXPERIMENTAL VERSION
+       **/
+
+      bool expand = false;
+      if (request.isMember(KEY_EXPAND))
+      {
+        expand = request[KEY_EXPAND].asBool();
+      }
+
+      const ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
+
+      ResourceFinder finder(level, expand);
+      finder.SetDatabaseLimits(context.GetDatabaseLimits(level));
+
+      const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human);
+
+      if (request.isMember(KEY_LIMIT))
+      {
+        int64_t tmp = request[KEY_LIMIT].asInt64();
+        if (tmp < 0)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer");
+        }
+        else if (tmp != 0)  // This is for compatibility with Orthanc 1.12.4
+        {
+          finder.SetLimitsCount(static_cast<uint64_t>(tmp));
+        }
+      }
+
+      if (request.isMember(KEY_SINCE))
+      {
+        int64_t tmp = request[KEY_SINCE].asInt64();
+        if (tmp < 0)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer");
+        }
+        else
+        {
+          finder.SetLimitsSince(static_cast<uint64_t>(tmp));
+        }
+      }
+
+      {
+        bool caseSensitive = false;
+        if (request.isMember(KEY_CASE_SENSITIVE))
+        {
+          caseSensitive = request[KEY_CASE_SENSITIVE].asBool();
+        }
+
+        DatabaseLookup query;
+
+        Json::Value::Members members = request[KEY_QUERY].getMemberNames();
+        for (size_t i = 0; i < members.size(); i++)
+        {
+          if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
+          {
+            throw OrthancException(ErrorCode_BadRequest,
+                                   "Tag \"" + members[i] + "\" must be associated with a string");
+          }
+
+          const std::string value = request[KEY_QUERY][members[i]].asString();
+
+          if (!value.empty())
+          {
+            // An empty string corresponds to an universal constraint,
+            // so we ignore it. This mimics the behavior of class
+            // "OrthancFindRequestHandler"
+            query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]),
+                                    value, caseSensitive, true);
+          }
+        }
+
+        finder.SetDatabaseLookup(query);
+      }
+
+      if (request.isMember(KEY_REQUESTED_TAGS))
+      {
+        std::set<DicomTag> requestedTags;
+        FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]);
+        finder.AddRequestedTags(requestedTags);
+      }
+
+      if (request.isMember(KEY_LABELS))  // New in Orthanc 1.12.0
+      {
+        for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++)
+        {
+          if (request[KEY_LABELS][i].type() != Json::stringValue)
+          {
+            throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS) + "\" must contain strings");
+          }
+          else
+          {
+            finder.AddLabel(request[KEY_LABELS][i].asString());
+          }
+        }
+      }
+
+      finder.SetLabelsConstraint(LabelsConstraint_All);
+
+      if (request.isMember(KEY_LABELS_CONSTRAINT))
+      {
+        const std::string& s = request[KEY_LABELS_CONSTRAINT].asString();
+        if (s == "All")
+        {
+          finder.SetLabelsConstraint(LabelsConstraint_All);
+        }
+        else if (s == "Any")
+        {
+          finder.SetLabelsConstraint(LabelsConstraint_Any);
+        }
+        else if (s == "None")
+        {
+          finder.SetLabelsConstraint(LabelsConstraint_None);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\"");
+        }
+      }
+
+      Json::Value answer;
+      finder.Execute(answer, context, format, false /* no "Metadata" field */);
+      call.GetOutput().AnswerJson(answer);
+    }
     else
     {
+      /**
+       * VERSION IN ORTHANC <= 1.12.4
+       **/
       bool expand = false;
       if (request.isMember(KEY_EXPAND))
       {
@@ -3398,34 +3621,56 @@
     ServerIndex& index = OrthancRestApi::GetIndex(call);
     ServerContext& context = OrthancRestApi::GetContext(call);
 
+    const bool expand = (!call.HasArgument("expand") ||
+                         // this "expand" is the only one to have a false default value to keep backward compatibility
+                         call.GetBooleanArgument("expand", false));
+    const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
+
     std::set<DicomTag> requestedTags;
     OrthancRestApi::GetRequestedTags(requestedTags, call);
 
-    std::list<std::string> a, b, c;
-    a.push_back(call.GetUriComponent("id", ""));
-
-    ResourceType type = start;
-    while (type != end)
+    if (true)
     {
-      b.clear();
-
-      for (std::list<std::string>::const_iterator
-             it = a.begin(); it != a.end(); ++it)
+      /**
+       * EXPERIMENTAL VERSION
+       **/
+
+      ResourceFinder finder(end, expand);
+      finder.SetOrthancId(start, call.GetUriComponent("id", ""));
+      finder.AddRequestedTags(requestedTags);
+
+      Json::Value answer;
+      finder.Execute(answer, context, format, false /* no "Metadata" field */);
+      call.GetOutput().AnswerJson(answer);
+    }
+    else
+    {
+      /**
+       * VERSION IN ORTHANC <= 1.12.4
+       **/
+      std::list<std::string> a, b, c;
+      a.push_back(call.GetUriComponent("id", ""));
+
+      ResourceType type = start;
+      while (type != end)
       {
-        index.GetChildren(c, *it);
-        b.splice(b.begin(), c);
+        b.clear();
+
+        for (std::list<std::string>::const_iterator
+               it = a.begin(); it != a.end(); ++it)
+        {
+          index.GetChildren(c, *it);
+          b.splice(b.begin(), c);
+        }
+
+        type = GetChildResourceType(type);
+
+        a.clear();
+        a.splice(a.begin(), b);
       }
 
-      type = GetChildResourceType(type);
-
-      a.clear();
-      a.splice(a.begin(), b);
+      AnswerListOfResources2(call.GetOutput(), context, a, type, expand, format, requestedTags, true /* allowStorageAccess */);
     }
-
-    AnswerListOfResources(call.GetOutput(), context, a, type, !call.HasArgument("expand") || call.GetBooleanArgument("expand", false),  // this "expand" is the only one to have a false default value to keep backward compatibility
-                          OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human),
-                          requestedTags,
-                          true /* allowStorageAccess */);
   }
 
 
@@ -3538,9 +3783,26 @@
     const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
 
     Json::Value resource;
-    if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format, requestedTags, true /* allowStorageAccess */))
+
+    if (true)
     {
-      call.GetOutput().AnswerJson(resource);
+      /**
+       * EXPERIMENTAL VERSION
+       **/
+      if (ExpandResource(resource, OrthancRestApi::GetContext(call), currentType, current, format, false))
+      {
+        call.GetOutput().AnswerJson(resource);
+      }
+    }
+    else
+    {
+      /**
+       * VERSION IN ORTHANC <= 1.12.4
+       **/
+      if (OrthancRestApi::GetContext(call).ExpandResource(resource, current, end, format, requestedTags, true /* allowStorageAccess */))
+      {
+        call.GetOutput().AnswerJson(resource);
+      }
     }
   }
 
@@ -3971,17 +4233,34 @@
         for (std::set<std::string>::const_iterator
                it = interest.begin(); it != interest.end(); ++it)
         {
-          Json::Value item;
-          std::set<DicomTag> emptyRequestedTags;  // not supported for bulk content
-
-          if (OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */))
+          if (true)
+          {
+            /**
+             * EXPERIMENTAL VERSION
+             **/
+            Json::Value item;
+            if (ExpandResource(item, OrthancRestApi::GetContext(call), level, *it, format, metadata))
+            {
+              answer.append(item);
+            }
+          }
+          else
           {
-            if (metadata)
+            /**
+             * VERSION IN ORTHANC <= 1.12.4
+             **/
+            Json::Value item;
+            std::set<DicomTag> emptyRequestedTags;  // not supported for bulk content
+
+            if (OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */))
             {
-              AddMetadata(item[METADATA], index, *it, level);
+              if (metadata)
+              {
+                AddMetadata(item[METADATA], index, *it, level);
+              }
+
+              answer.append(item);
             }
-
-            answer.append(item);
           }
         }
       }
@@ -3998,19 +4277,36 @@
           Json::Value item;
           std::set<DicomTag> emptyRequestedTags;  // not supported for bulk content
 
-          if (index.LookupResourceType(level, *it) &&
-              OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */))
+          if (true)
           {
-            if (metadata)
+            /**
+             * EXPERIMENTAL VERSION
+             **/
+            if (index.LookupResourceType(level, *it) &&
+                ExpandResource(item, OrthancRestApi::GetContext(call), level, *it, format, metadata))
             {
-              AddMetadata(item[METADATA], index, *it, level);
+              answer.append(item);
             }
-
-            answer.append(item);
           }
           else
           {
-            CLOG(INFO, HTTP) << "Unknown resource during a bulk content retrieval: " << *it;
+            /**
+             * VERSION IN ORTHANC <= 1.12.4
+             **/
+            if (index.LookupResourceType(level, *it) &&
+                OrthancRestApi::GetContext(call).ExpandResource(item, *it, level, format, emptyRequestedTags, true /* allowStorageAccess */))
+            {
+              if (metadata)
+              {
+                AddMetadata(item[METADATA], index, *it, level);
+              }
+
+              answer.append(item);
+            }
+            else
+            {
+              CLOG(INFO, HTTP) << "Unknown resource during a bulk content retrieval: " << *it;
+            }
           }
         }
       }
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -28,6 +28,7 @@
 #include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
 #include "../../OrthancFramework/Sources/HttpServer/WebDavStorage.h"
 #include "../../OrthancFramework/Sources/Logging.h"
+#include "ResourceFinder.h"
 #include "Search/DatabaseLookup.h"
 #include "ServerContext.h"
 
@@ -50,6 +51,20 @@
   {
     return boost::posix_time::second_clock::universal_time();
   }
+
+
+  static void ParseTime(boost::posix_time::ptime& target,
+                        const std::string& value)
+  {
+    try
+    {
+      target = boost::posix_time::from_iso_string(value);
+    }
+    catch (std::exception& e)
+    {
+      target = GetNow();
+    }
+  }
   
 
   static void LookupTime(boost::posix_time::ptime& target,
@@ -62,17 +77,12 @@
     int64_t revision;  // Ignored
     if (context.GetIndex().LookupMetadata(value, revision, publicId, level, metadata))
     {
-      try
-      {
-        target = boost::posix_time::from_iso_string(value);
-        return;
-      }
-      catch (std::exception& e)
-      {
-      }
+      ParseTime(target, value);
     }
-
-    target = GetNow();
+    else
+    {
+      target = GetNow();
+    }
   }
 
   
@@ -169,6 +179,98 @@
   };
 
   
+  class OrthancWebDav::DicomIdentifiersVisitorV2 : public ResourceFinder::IVisitor
+  {
+  private:
+    bool         isComplete_;
+    Collection&  target_;
+
+  public:
+    explicit DicomIdentifiersVisitorV2(Collection& target) :
+      isComplete_(false),
+      target_(target)
+    {
+    }
+
+    virtual void MarkAsComplete() ORTHANC_OVERRIDE
+    {
+      isComplete_ = true;  // TODO
+    }
+
+    virtual void Apply(const FindResponse::Resource& resource,
+                       const DicomMap& requestedTags)  ORTHANC_OVERRIDE
+    {
+      DicomMap resourceTags;
+      resource.GetMainDicomTags(resourceTags, resource.GetLevel());
+
+      std::string uid;
+      bool hasUid;
+
+      std::string time;
+      bool hasTime;
+
+      switch (resource.GetLevel())
+      {
+        case ResourceType_Study:
+          hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_STUDY_INSTANCE_UID, false);
+          hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_LastUpdate);
+          break;
+
+        case ResourceType_Series:
+          hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_SERIES_INSTANCE_UID, false);
+          hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_LastUpdate);
+          break;
+
+        case ResourceType_Instance:
+          hasUid = resourceTags.LookupStringValue(uid, DICOM_TAG_SOP_INSTANCE_UID, false);
+          hasTime = resource.LookupMetadata(time, resource.GetLevel(), MetadataType_Instance_ReceptionDate);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (hasUid &&
+          !uid.empty())
+      {
+        std::unique_ptr<Resource> item;
+
+        if (resource.GetLevel() == ResourceType_Instance)
+        {
+          FileInfo info;
+          if (resource.LookupAttachment(info, FileContentType_Dicom))
+          {
+            std::unique_ptr<File> f(new File(uid + ".dcm"));
+            f->SetMimeType(MimeType_Dicom);
+            f->SetContentLength(info.GetUncompressedSize());
+            item.reset(f.release());
+          }
+        }
+        else
+        {
+          item.reset(new Folder(uid));
+        }
+
+        if (item.get() != NULL)
+        {
+          if (hasTime)
+          {
+            boost::posix_time::ptime t;
+            ParseTime(t, time);
+            item->SetCreationTime(t);
+          }
+          else
+          {
+            item->SetCreationTime(GetNow());
+          }
+
+          target_.AddResource(item.release());
+        }
+      }
+    }
+  };
+
+
   class OrthancWebDav::DicomFileVisitor : public ServerContext::ILookupVisitor
   {
   private:
@@ -221,6 +323,60 @@
   };
   
 
+  class OrthancWebDav::DicomFileVisitorV2 : public ResourceFinder::IVisitor
+  {
+  private:
+    ServerContext&  context_;
+    bool            success_;
+    std::string&    target_;
+    boost::posix_time::ptime&  time_;
+
+  public:
+    DicomFileVisitorV2(ServerContext& context,
+                       std::string& target,
+                       boost::posix_time::ptime& time) :
+      context_(context),
+      success_(false),
+      target_(target),
+      time_(time)
+    {
+    }
+
+    bool IsSuccess() const
+    {
+      return success_;
+    }
+
+    virtual void MarkAsComplete() ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void Apply(const FindResponse::Resource& resource,
+                       const DicomMap& requestedTags) ORTHANC_OVERRIDE
+    {
+      if (success_)
+      {
+        success_ = false;  // Two matches => Error
+      }
+      else
+      {
+        std::string s;
+        if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_ReceptionDate))
+        {
+          ParseTime(time_, s);
+        }
+        else
+        {
+          time_ = GetNow();
+        }
+
+        context_.ReadDicom(target_, resource.GetIdentifier());
+        success_ = true;
+      }
+    }
+  };
+
+
   class OrthancWebDav::OrthancJsonVisitor : public ServerContext::ILookupVisitor
   {
   private:
@@ -955,7 +1111,7 @@
     std::string  year_;
     std::string  month_;
 
-    class Visitor : public ServerContext::ILookupVisitor
+    class Visitor : public ResourceFinder::IVisitor
     {
     private:
       std::list<std::string>&  resources_;
@@ -966,21 +1122,14 @@
       {
       }
 
-      virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-      {
-        return false;   // (*)
-      }
-      
       virtual void MarkAsComplete() ORTHANC_OVERRIDE
       {
       }
 
-      virtual void Visit(const std::string& publicId,
-                         const std::string& instanceId   /* unused     */,
-                         const DicomMap& mainDicomTags,
-                         const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+      virtual void Apply(const FindResponse::Resource& resource,
+                         const DicomMap& requestedTags) ORTHANC_OVERRIDE
       {
-        resources_.push_back(publicId);
+        resources_.push_back(resource.GetIdentifier());
       }
     };
     
@@ -992,7 +1141,10 @@
                               true /* case sensitive */, true /* mandatory tag */);
 
       Visitor visitor(resources);
-      GetContext().Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
+
+      ResourceFinder finder(ResourceType_Study, false /* no expand */);
+      finder.SetDatabaseLookup(query);
+      finder.Execute(visitor, GetContext());
     }
 
     virtual INode* CreateResourceNode(const std::string& resource) ORTHANC_OVERRIDE
@@ -1025,7 +1177,7 @@
     std::string       year_;
     const Templates&  templates_;
 
-    class Visitor : public ServerContext::ILookupVisitor
+    class Visitor : public ResourceFinder::IVisitor
     {
     private:
       std::set<std::string> months_;
@@ -1036,20 +1188,16 @@
         return months_;
       }
       
-      virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-      {
-        return false;   // (*)
-      }
-      
       virtual void MarkAsComplete() ORTHANC_OVERRIDE
       {
       }
 
-      virtual void Visit(const std::string& publicId,
-                         const std::string& instanceId   /* unused     */,
-                         const DicomMap& mainDicomTags,
-                         const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+      virtual void Apply(const FindResponse::Resource& resource,
+                         const DicomMap& requestedTags) ORTHANC_OVERRIDE
       {
+        DicomMap mainDicomTags;
+        resource.GetMainDicomTags(mainDicomTags, ResourceType_Study);
+
         std::string s;
         if (mainDicomTags.LookupStringValue(s, DICOM_TAG_STUDY_DATE, false) &&
             s.size() == 8)
@@ -1071,7 +1219,10 @@
                               true /* case sensitive */, true /* mandatory tag */);
 
       Visitor visitor;
-      context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
+
+      ResourceFinder finder(ResourceType_Study, false /* no expand */);
+      finder.SetDatabaseLookup(query);
+      finder.Execute(visitor, context_);
 
       for (std::set<std::string>::const_iterator it = visitor.GetMonths().begin();
            it != visitor.GetMonths().end(); ++it)
@@ -1165,7 +1316,7 @@
   };
 
 
-  class OrthancWebDav::DicomDeleteVisitor : public ServerContext::ILookupVisitor
+  class OrthancWebDav::DicomDeleteVisitor : public ResourceFinder::IVisitor
   {
   private:
     ServerContext&  context_;
@@ -1179,22 +1330,15 @@
     {
     }
 
-    virtual bool IsDicomAsJsonNeeded() const ORTHANC_OVERRIDE
-    {
-      return false;   // (*)
-    }
-      
     virtual void MarkAsComplete() ORTHANC_OVERRIDE
     {
     }
 
-    virtual void Visit(const std::string& publicId,
-                       const std::string& instanceId   /* unused     */,
-                       const DicomMap& mainDicomTags   /* unused     */,
-                       const Json::Value* dicomAsJson  /* unused (*) */)  ORTHANC_OVERRIDE
+    virtual void Apply(const FindResponse::Resource& resource,
+                       const DicomMap& requestedTags) ORTHANC_OVERRIDE
     {
       Json::Value info;
-      context_.DeleteResource(info, publicId, level_);
+      context_.DeleteResource(info, resource.GetIdentifier(), level_);
     }
   };
   
@@ -1455,9 +1599,48 @@
         return false;
       }
 
-      DicomIdentifiersVisitor visitor(context_, collection, level);
-      context_.Apply(visitor, query, level, 0 /* since */, limit);
-      
+      if (true)
+      {
+        /**
+         * EXPERIMENTAL VERSION
+         **/
+
+        ResourceFinder finder(level, false /* don't expand */);
+        finder.SetDatabaseLookup(query);
+        finder.SetRetrieveMetadata(true);
+
+        switch (level)
+        {
+          case ResourceType_Study:
+            finder.AddRequestedTag(DICOM_TAG_STUDY_INSTANCE_UID);
+            break;
+
+          case ResourceType_Series:
+            finder.AddRequestedTag(DICOM_TAG_SERIES_INSTANCE_UID);
+            break;
+
+          case ResourceType_Instance:
+            finder.AddRequestedTag(DICOM_TAG_SOP_INSTANCE_UID);
+            finder.SetRetrieveAttachments(true);
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+
+        DicomIdentifiersVisitorV2 visitor(collection);
+        finder.Execute(visitor, context_);
+      }
+      else
+      {
+        /**
+         * VERSION IN ORTHANC <= 1.12.4
+         **/
+
+        DicomIdentifiersVisitor visitor(context_, collection, level);
+        context_.Apply(visitor, query, level, 0 /* since */, limit);
+      }
+
       return true;
     }
     else if (path[0] == BY_PATIENTS ||
@@ -1478,6 +1661,33 @@
   }
 
   
+  static bool GetOrthancJson(std::string& target,
+                             ServerContext& context,
+                             ResourceType level,
+                             const DatabaseLookup& query)
+  {
+    ResourceFinder finder(level, true /* expand */);
+    finder.SetDatabaseLookup(query);
+
+    Json::Value expanded;
+    finder.Execute(expanded, context, DicomToJsonFormat_Human, false /* don't add "Metadata" */);
+
+    if (expanded.size() != 1)
+    {
+      return false;
+    }
+    else
+    {
+      target = expanded[0].toStyledString();
+
+      // Replace UNIX newlines with DOS newlines
+      boost::replace_all(target, "\n", "\r\n");
+
+      return true;
+    }
+  }
+
+
   bool OrthancWebDav::GetFileContent(MimeType& mime,
                                      std::string& content,
                                      boost::posix_time::ptime& modificationTime, 
@@ -1495,12 +1705,25 @@
         DatabaseLookup query;
         query.AddRestConstraint(DICOM_TAG_STUDY_INSTANCE_UID, path[1],
                                 true /* case sensitive */, true /* mandatory tag */);
-      
-        OrthancJsonVisitor visitor(context_, content, ResourceType_Study);
-        context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
 
         mime = MimeType_Json;
-        return visitor.IsSuccess();
+
+        if (true)
+        {
+          /**
+           * EXPERIMENTAL VERSION
+           **/
+          return GetOrthancJson(content, context_, ResourceType_Study, query);
+        }
+        else
+        {
+          /**
+           * VERSION IN ORTHANC <= 1.12.4
+           **/
+          OrthancJsonVisitor visitor(context_, content, ResourceType_Study);
+          context_.Apply(visitor, query, ResourceType_Study, 0 /* since */, 0 /* no limit */);
+          return visitor.IsSuccess();
+        }
       }
       else if (path.size() == 4 &&
                path[3] == SERIES_INFO)
@@ -1511,11 +1734,24 @@
         query.AddRestConstraint(DICOM_TAG_SERIES_INSTANCE_UID, path[2],
                                 true /* case sensitive */, true /* mandatory tag */);
       
-        OrthancJsonVisitor visitor(context_, content, ResourceType_Series);
-        context_.Apply(visitor, query, ResourceType_Series, 0 /* since */, 0 /* no limit */);
+        mime = MimeType_Json;
 
-        mime = MimeType_Json;
-        return visitor.IsSuccess();
+        if (true)
+        {
+          /**
+           * EXPERIMENTAL VERSION
+           **/
+          return GetOrthancJson(content, context_, ResourceType_Series, query);
+        }
+        else
+        {
+          /**
+           * VERSION IN ORTHANC <= 1.12.4
+           **/
+          OrthancJsonVisitor visitor(context_, content, ResourceType_Series);
+          context_.Apply(visitor, query, ResourceType_Series, 0 /* since */, 0 /* no limit */);
+          return visitor.IsSuccess();
+        }
       }
       else if (path.size() == 4 &&
                boost::ends_with(path[3], ".dcm"))
@@ -1530,11 +1766,32 @@
         query.AddRestConstraint(DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUid,
                                 true /* case sensitive */, true /* mandatory tag */);
       
-        DicomFileVisitor visitor(context_, content, modificationTime);
-        context_.Apply(visitor, query, ResourceType_Instance, 0 /* since */, 0 /* no limit */);
-        
         mime = MimeType_Dicom;
-        return visitor.IsSuccess();
+
+        if (true)
+        {
+          /**
+           * EXPERIMENTAL VERSION
+           **/
+          ResourceFinder finder(ResourceType_Instance, false /* no expand */);
+          finder.SetDatabaseLookup(query);
+          finder.SetRetrieveMetadata(true);
+          finder.SetRetrieveAttachments(true);
+
+          DicomFileVisitorV2 visitor(context_, content, modificationTime);
+          finder.Execute(visitor, context_);
+
+          return visitor.IsSuccess();
+        }
+        else
+        {
+          /**
+           * VERSION IN ORTHANC <= 1.12.4
+           **/
+          DicomFileVisitor visitor(context_, content, modificationTime);
+          context_.Apply(visitor, query, ResourceType_Instance, 0 /* since */, 0 /* no limit */);
+          return visitor.IsSuccess();
+        }
       }
       else
       {
@@ -1655,7 +1912,10 @@
         }
 
         DicomDeleteVisitor visitor(context_, level);
-        context_.Apply(visitor, query, level, 0 /* since */, 0 /* no limit */);
+
+        ResourceFinder finder(level, false /* no expand */);
+        finder.SetDatabaseLookup(query);
+        finder.Execute(visitor, context_);
         return true;
       }
       else
--- a/OrthancServer/Sources/OrthancWebDav.h	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.h	Mon Sep 02 17:17:22 2024 +0200
@@ -39,7 +39,9 @@
 
     class DicomDeleteVisitor;
     class DicomFileVisitor;
+    class DicomFileVisitorV2;
     class DicomIdentifiersVisitor;  
+    class DicomIdentifiersVisitorV2;
     class InstancesOfSeries;
     class InternalNode;
     class ListOfResources;
@@ -47,6 +49,7 @@
     class ListOfStudiesByMonth;
     class ListOfStudiesByYear;
     class OrthancJsonVisitor;
+    class OrthancJsonVisitorV2;
     class ResourcesIndex;
     class RootNode;
     class SingleDicomResource;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ResourceFinder.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,1095 @@
+/**
+ * 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 "PrecompiledHeadersServer.h"
+#include "ResourceFinder.h"
+
+#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../OrthancFramework/Sources/Logging.h"
+#include "../../OrthancFramework/Sources/OrthancException.h"
+#include "../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "OrthancConfiguration.h"
+#include "Search/DatabaseLookup.h"
+#include "ServerContext.h"
+#include "ServerIndex.h"
+
+
+namespace Orthanc
+{
+  static bool IsComputedTag(const DicomTag& tag)
+  {
+    return (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES ||
+            tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES ||
+            tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES ||
+            tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES ||
+            tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES ||
+            tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES ||
+            tag == DICOM_TAG_SOP_CLASSES_IN_STUDY ||
+            tag == DICOM_TAG_MODALITIES_IN_STUDY ||
+            tag == DICOM_TAG_INSTANCE_AVAILABILITY);
+  }
+
+  void ResourceFinder::ConfigureChildrenCountComputedTag(DicomTag tag,
+                                                         ResourceType parentLevel,
+                                                         ResourceType childLevel)
+  {
+    if (request_.GetLevel() == parentLevel)
+    {
+      requestedComputedTags_.insert(tag);
+      hasRequestedTags_ = true;
+      request_.GetChildrenSpecification(childLevel).SetRetrieveIdentifiers(true);
+    }
+  }
+
+
+  void ResourceFinder::InjectChildrenCountComputedTag(DicomMap& requestedTags,
+                                                      DicomTag tag,
+                                                      const FindResponse::Resource& resource,
+                                                      ResourceType level) const
+  {
+    if (IsRequestedComputedTag(tag))
+    {
+      const std::set<std::string>& children = resource.GetChildrenIdentifiers(level);
+      requestedTags.SetValue(tag, boost::lexical_cast<std::string>(children.size()), false);
+    }
+  }
+
+
+  void ResourceFinder::InjectComputedTags(DicomMap& requestedTags,
+                                          const FindResponse::Resource& resource) const
+  {
+    switch (resource.GetLevel())
+    {
+      case ResourceType_Patient:
+        InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, resource, ResourceType_Study);
+        InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, resource, ResourceType_Series);
+        InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, resource, ResourceType_Instance);
+        break;
+
+      case ResourceType_Study:
+        InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, resource, ResourceType_Series);
+        InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, resource, ResourceType_Instance);
+
+        if (IsRequestedComputedTag(DICOM_TAG_MODALITIES_IN_STUDY))
+        {
+          std::set<std::string> modalities;
+          resource.GetChildrenMainDicomTagValues(modalities, ResourceType_Series, DICOM_TAG_MODALITY);
+
+          std::string s;
+          Toolbox::JoinStrings(s, modalities, "\\");
+
+          requestedTags.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, s, false);
+        }
+
+        if (IsRequestedComputedTag(DICOM_TAG_SOP_CLASSES_IN_STUDY))
+        {
+          std::set<std::string> classes;
+          resource.GetChildrenMetadataValues(classes, ResourceType_Instance, MetadataType_Instance_SopClassUid);
+
+          std::string s;
+          Toolbox::JoinStrings(s, classes, "\\");
+
+          requestedTags.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, s, false);
+        }
+
+        break;
+
+      case ResourceType_Series:
+        InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, resource, ResourceType_Instance);
+        break;
+
+      case ResourceType_Instance:
+        if (IsRequestedComputedTag(DICOM_TAG_INSTANCE_AVAILABILITY))
+        {
+          requestedTags.SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false);
+        }
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  SeriesStatus ResourceFinder::GetSeriesStatus(uint32_t& expectedNumberOfInstances,
+                                               const FindResponse::Resource& resource)
+  {
+    if (resource.GetLevel() != ResourceType_Series)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    std::string s;
+    if (!resource.LookupMetadata(s, ResourceType_Series, MetadataType_Series_ExpectedNumberOfInstances) ||
+        !SerializationToolbox::ParseUnsignedInteger32(expectedNumberOfInstances, s))
+    {
+      return SeriesStatus_Unknown;
+    }
+
+    std::set<std::string> values;
+    resource.GetChildrenMetadataValues(values, ResourceType_Instance, MetadataType_Instance_IndexInSeries);
+
+    std::set<int64_t> instances;
+
+    for (std::set<std::string>::const_iterator
+           it = values.begin(); it != values.end(); ++it)
+    {
+      int64_t index;
+
+      if (!SerializationToolbox::ParseInteger64(index, *it))
+      {
+        return SeriesStatus_Unknown;
+      }
+
+      if (index <= 0 ||
+          index > static_cast<int64_t>(expectedNumberOfInstances))
+      {
+        // Out-of-range instance index
+        return SeriesStatus_Inconsistent;
+      }
+
+      if (instances.find(index) != instances.end())
+      {
+        // Twice the same instance index
+        return SeriesStatus_Inconsistent;
+      }
+
+      instances.insert(index);
+    }
+
+    if (instances.size() == static_cast<size_t>(expectedNumberOfInstances))
+    {
+      return SeriesStatus_Complete;
+    }
+    else
+    {
+      return SeriesStatus_Missing;
+    }
+  }
+
+
+  void ResourceFinder::Expand(Json::Value& target,
+                              const FindResponse::Resource& resource,
+                              ServerIndex& index,
+                              DicomToJsonFormat format,
+                              bool includeAllMetadata) const
+  {
+    /**
+     * This method closely follows "SerializeExpandedResource()" in
+     * "ServerContext.cpp" from Orthanc 1.12.4.
+     **/
+
+    if (!expand_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (resource.GetLevel() != request_.GetLevel())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    target = Json::objectValue;
+
+    target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true);
+    target["ID"] = resource.GetIdentifier();
+
+    switch (resource.GetLevel())
+    {
+      case ResourceType_Patient:
+        break;
+
+      case ResourceType_Study:
+        target["ParentPatient"] = resource.GetParentIdentifier();
+        break;
+
+      case ResourceType_Series:
+        target["ParentStudy"] = resource.GetParentIdentifier();
+        break;
+
+      case ResourceType_Instance:
+        target["ParentSeries"] = resource.GetParentIdentifier();
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (resource.GetLevel() != ResourceType_Instance)
+    {
+      const std::set<std::string>& children = resource.GetChildrenIdentifiers(GetChildResourceType(resource.GetLevel()));
+
+      Json::Value c = Json::arrayValue;
+      for (std::set<std::string>::const_iterator
+             it = children.begin(); it != children.end(); ++it)
+      {
+        c.append(*it);
+      }
+
+      switch (resource.GetLevel())
+      {
+        case ResourceType_Patient:
+          target["Studies"] = c;
+          break;
+
+        case ResourceType_Study:
+          target["Series"] = c;
+          break;
+
+        case ResourceType_Series:
+          target["Instances"] = c;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    switch (resource.GetLevel())
+    {
+      case ResourceType_Patient:
+      case ResourceType_Study:
+        break;
+
+      case ResourceType_Series:
+      {
+        uint32_t expectedNumberOfInstances;
+        SeriesStatus status = GetSeriesStatus(expectedNumberOfInstances, resource);
+
+        target["Status"] = EnumerationToString(status);
+
+        static const char* const EXPECTED_NUMBER_OF_INSTANCES = "ExpectedNumberOfInstances";
+
+        if (status == SeriesStatus_Unknown)
+        {
+          target[EXPECTED_NUMBER_OF_INSTANCES] = Json::nullValue;
+        }
+        else
+        {
+          target[EXPECTED_NUMBER_OF_INSTANCES] = expectedNumberOfInstances;
+        }
+
+        break;
+      }
+
+      case ResourceType_Instance:
+      {
+        FileInfo info;
+        if (resource.LookupAttachment(info, FileContentType_Dicom))
+        {
+          target["FileSize"] = static_cast<Json::UInt64>(info.GetUncompressedSize());
+          target["FileUuid"] = info.GetUuid();
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        static const char* const INDEX_IN_SERIES = "IndexInSeries";
+
+        std::string s;
+        uint32_t indexInSeries;
+        if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_IndexInSeries) &&
+            SerializationToolbox::ParseUnsignedInteger32(indexInSeries, s))
+        {
+          target[INDEX_IN_SERIES] = indexInSeries;
+        }
+        else
+        {
+          target[INDEX_IN_SERIES] = Json::nullValue;
+        }
+
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    std::string s;
+    if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_AnonymizedFrom))
+    {
+      target["AnonymizedFrom"] = s;
+    }
+
+    if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_ModifiedFrom))
+    {
+      target["ModifiedFrom"] = s;
+    }
+
+    if (resource.GetLevel() == ResourceType_Patient ||
+        resource.GetLevel() == ResourceType_Study ||
+        resource.GetLevel() == ResourceType_Series)
+    {
+      target["IsStable"] = !index.IsUnstableResource(resource.GetLevel(), resource.GetInternalId());
+
+      if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_LastUpdate))
+      {
+        target["LastUpdate"] = s;
+      }
+    }
+
+    {
+      DicomMap allMainDicomTags;
+      resource.GetMainDicomTags(allMainDicomTags, resource.GetLevel());
+
+      /**
+       * This section was part of "StatelessDatabaseOperations::ExpandResource()"
+       * in Orthanc <= 1.12.3
+       **/
+
+      // read all main sequences from DB
+      std::string serializedSequences;
+      if (resource.LookupMetadata(serializedSequences, resource.GetLevel(), MetadataType_MainDicomSequences))
+      {
+        Json::Value jsonMetadata;
+        Toolbox::ReadJson(jsonMetadata, serializedSequences);
+
+        if (jsonMetadata["Version"].asInt() == 1)
+        {
+          allMainDicomTags.FromDicomAsJson(jsonMetadata["Sequences"], true /* append */, true /* parseSequences */);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_NotImplemented);
+        }
+      }
+
+      /**
+       * End of section from StatelessDatabaseOperations
+       **/
+
+
+      static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
+      static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
+
+      // TODO-FIND : Ignore "null" values
+
+      DicomMap levelMainDicomTags;
+      allMainDicomTags.ExtractResourceInformation(levelMainDicomTags, resource.GetLevel());
+
+      target[MAIN_DICOM_TAGS] = Json::objectValue;
+      FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], levelMainDicomTags, format);
+
+      if (resource.GetLevel() == ResourceType_Study)
+      {
+        DicomMap patientMainDicomTags;
+        allMainDicomTags.ExtractPatientInformation(patientMainDicomTags);
+
+        target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue;
+        FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format);
+      }
+    }
+
+    {
+      Json::Value labels = Json::arrayValue;
+
+      for (std::set<std::string>::const_iterator
+             it = resource.GetLabels().begin(); it != resource.GetLabels().end(); ++it)
+      {
+        labels.append(*it);
+      }
+
+      target["Labels"] = labels;
+    }
+
+    if (includeAllMetadata)  // new in Orthanc 1.12.4
+    {
+      const std::map<MetadataType, std::string>& m = resource.GetMetadata(resource.GetLevel());
+
+      Json::Value metadata = Json::objectValue;
+
+      for (std::map<MetadataType, std::string>::const_iterator it = m.begin(); it != m.end(); ++it)
+      {
+        metadata[EnumerationToString(it->first)] = it->second;
+      }
+
+      target["Metadata"] = metadata;
+    }
+  }
+
+
+  void ResourceFinder::UpdateRequestLimits()
+  {
+    // By default, use manual paging
+    pagingMode_ = PagingMode_FullManual;
+
+    if (databaseLimits_ != 0)
+    {
+      request_.SetLimits(0, databaseLimits_ + 1);
+    }
+    else
+    {
+      request_.ClearLimits();
+    }
+
+    if (lookup_.get() == NULL &&
+        (hasLimitsSince_ || hasLimitsCount_))
+    {
+      pagingMode_ = PagingMode_FullDatabase;
+      request_.SetLimits(limitsSince_, limitsCount_);
+    }
+
+    if (lookup_.get() != NULL &&
+        isSimpleLookup_ &&
+        (hasLimitsSince_ || hasLimitsCount_))
+    {
+      /**
+       * TODO-FIND: "IDatabaseWrapper::ApplyLookupResources()" only
+       * accept the "limit" argument.  The "since" must be implemented
+       * manually.
+       **/
+
+      if (hasLimitsSince_ &&
+          limitsSince_ != 0)
+      {
+        pagingMode_ = PagingMode_ManualSkip;
+        request_.SetLimits(0, limitsCount_ + limitsSince_);
+      }
+      else
+      {
+        pagingMode_ = PagingMode_FullDatabase;
+        request_.SetLimits(0, limitsCount_);
+      }
+    }
+
+    // TODO-FIND: More cases could be added, depending on "GetDatabaseCapabilities()"
+  }
+
+
+  ResourceFinder::ResourceFinder(ResourceType level,
+                                 bool expand) :
+    request_(level),
+    databaseLimits_(0),
+    isSimpleLookup_(true),
+    pagingMode_(PagingMode_FullManual),
+    hasLimitsSince_(false),
+    hasLimitsCount_(false),
+    limitsSince_(0),
+    limitsCount_(0),
+    expand_(expand),
+    allowStorageAccess_(true),
+    hasRequestedTags_(false)
+  {
+    UpdateRequestLimits();
+
+    if (expand)
+    {
+      request_.SetRetrieveMainDicomTags(true);
+      request_.SetRetrieveMetadata(true);
+      request_.SetRetrieveLabels(true);
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          request_.GetChildrenSpecification(ResourceType_Study).SetRetrieveIdentifiers(true);
+          break;
+
+        case ResourceType_Study:
+          request_.GetChildrenSpecification(ResourceType_Series).SetRetrieveIdentifiers(true);
+          request_.SetRetrieveParentIdentifier(true);
+          break;
+
+        case ResourceType_Series:
+          request_.GetChildrenSpecification(ResourceType_Instance).AddMetadata(MetadataType_Instance_IndexInSeries); // required for the SeriesStatus
+          request_.GetChildrenSpecification(ResourceType_Instance).SetRetrieveIdentifiers(true);
+          request_.SetRetrieveParentIdentifier(true);
+          break;
+
+        case ResourceType_Instance:
+          request_.SetRetrieveAttachments(true); // for FileSize & FileUuid
+          request_.SetRetrieveParentIdentifier(true);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+  }
+
+
+  void ResourceFinder::SetDatabaseLimits(uint64_t limits)
+  {
+    databaseLimits_ = limits;
+    UpdateRequestLimits();
+  }
+
+
+  void ResourceFinder::SetLimitsSince(uint64_t since)
+  {
+    if (hasLimitsSince_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      hasLimitsSince_ = true;
+      limitsSince_ = since;
+      UpdateRequestLimits();
+    }
+  }
+
+
+  void ResourceFinder::SetLimitsCount(uint64_t count)
+  {
+    if (hasLimitsCount_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      hasLimitsCount_ = true;
+      limitsCount_ = count;
+      UpdateRequestLimits();
+    }
+  }
+
+
+  void ResourceFinder::SetDatabaseLookup(const DatabaseLookup& lookup)
+  {
+    MainDicomTagsRegistry registry;
+
+    lookup_.reset(lookup.Clone());
+
+    for (size_t i = 0; i < lookup.GetConstraintsCount(); i++)
+    {
+      DicomTag tag = lookup.GetConstraint(i).GetTag();
+      if (IsComputedTag(tag))
+      {
+        AddRequestedTag(tag);
+      }
+      else
+      {
+        ResourceType level;
+        DicomTagType tagType;
+        registry.LookupTag(level, tagType, tag);
+        if (tagType == DicomTagType_Generic)
+        {
+          AddRequestedTag(tag);
+        }
+      }
+    }
+
+    isSimpleLookup_ = registry.NormalizeLookup(request_.GetDicomTagConstraints(), lookup, request_.GetLevel());
+
+    // "request_.GetDicomTagConstraints()" only contains constraints on main DICOM tags
+
+    for (size_t i = 0; i < request_.GetDicomTagConstraints().GetSize(); i++)
+    {
+      const DatabaseConstraint& constraint = request_.GetDicomTagConstraints().GetConstraint(i);
+      if (constraint.GetLevel() == request_.GetLevel())
+      {
+        request_.SetRetrieveMainDicomTags(true);
+      }
+      else if (IsResourceLevelAboveOrEqual(constraint.GetLevel(), request_.GetLevel()))
+      {
+        request_.GetParentSpecification(constraint.GetLevel()).SetRetrieveMainDicomTags(true);
+      }
+      else
+      {
+        LOG(WARNING) << "Executing a database lookup at level " << EnumerationToString(request_.GetLevel())
+                     << " on main DICOM tag " << constraint.GetTag().Format() << " from an inferior level ("
+                     << EnumerationToString(constraint.GetLevel()) << "), this will return no result";
+      }
+
+      if (IsComputedTag(constraint.GetTag()))
+      {
+        // Sanity check
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    UpdateRequestLimits();
+  }
+
+
+  void ResourceFinder::AddRequestedTag(const DicomTag& tag)
+  {
+    if (DicomMap::IsMainDicomTag(tag, ResourceType_Patient))
+    {
+      if (request_.GetLevel() == ResourceType_Patient)
+      {
+        request_.SetRetrieveMainDicomTags(true);
+        requestedPatientTags_.insert(tag);
+      }
+      else
+      {
+        /**
+         * This comes from the fact that patient-level tags are copied
+         * at the study level, as implemented by "ResourcesContent::AddResource()".
+         **/
+        requestedStudyTags_.insert(tag);
+
+        if (request_.GetLevel() == ResourceType_Study)
+        {
+          request_.SetRetrieveMainDicomTags(true);
+        }
+        else
+        {
+          request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true);
+        }
+
+        requestedStudyTags_.insert(tag);
+      }
+
+      hasRequestedTags_ = true;
+    }
+    else if (DicomMap::IsMainDicomTag(tag, ResourceType_Study))
+    {
+      if (request_.GetLevel() == ResourceType_Patient)
+      {
+        LOG(WARNING) << "Requested tag " << tag.Format()
+                     << " should only be read at the study, series, or instance level";
+        requestedTagsFromFileStorage_.insert(tag);
+        request_.SetRetrieveOneInstanceIdentifier(true);
+      }
+      else
+      {
+        if (request_.GetLevel() == ResourceType_Study)
+        {
+          request_.SetRetrieveMainDicomTags(true);
+        }
+        else
+        {
+          request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true);
+        }
+
+        requestedStudyTags_.insert(tag);
+      }
+
+      hasRequestedTags_ = true;
+    }
+    else if (DicomMap::IsMainDicomTag(tag, ResourceType_Series))
+    {
+      if (request_.GetLevel() == ResourceType_Patient ||
+          request_.GetLevel() == ResourceType_Study)
+      {
+        LOG(WARNING) << "Requested tag " << tag.Format()
+                     << " should only be read at the series or instance level";
+        requestedTagsFromFileStorage_.insert(tag);
+        request_.SetRetrieveOneInstanceIdentifier(true);
+      }
+      else
+      {
+        if (request_.GetLevel() == ResourceType_Series)
+        {
+          request_.SetRetrieveMainDicomTags(true);
+        }
+        else
+        {
+          request_.GetParentSpecification(ResourceType_Series).SetRetrieveMainDicomTags(true);
+        }
+
+        requestedSeriesTags_.insert(tag);
+      }
+
+      hasRequestedTags_ = true;
+    }
+    else if (DicomMap::IsMainDicomTag(tag, ResourceType_Instance))
+    {
+      if (request_.GetLevel() == ResourceType_Patient ||
+          request_.GetLevel() == ResourceType_Study ||
+          request_.GetLevel() == ResourceType_Series)
+      {
+        LOG(WARNING) << "Requested tag " << tag.Format()
+                     << " should only be read at the instance level";
+        requestedTagsFromFileStorage_.insert(tag);
+        request_.SetRetrieveOneInstanceIdentifier(true);
+      }
+      else
+      {
+        request_.SetRetrieveMainDicomTags(true);
+        requestedInstanceTags_.insert(tag);
+      }
+
+      hasRequestedTags_ = true;
+    }
+    else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES)
+    {
+      ConfigureChildrenCountComputedTag(tag, ResourceType_Patient, ResourceType_Study);
+    }
+    else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES)
+    {
+      ConfigureChildrenCountComputedTag(tag, ResourceType_Patient, ResourceType_Series);
+    }
+    else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES)
+    {
+      ConfigureChildrenCountComputedTag(tag, ResourceType_Patient, ResourceType_Instance);
+    }
+    else if (tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES)
+    {
+      ConfigureChildrenCountComputedTag(tag, ResourceType_Study, ResourceType_Series);
+    }
+    else if (tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES)
+    {
+      ConfigureChildrenCountComputedTag(tag, ResourceType_Study, ResourceType_Instance);
+    }
+    else if (tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES)
+    {
+      ConfigureChildrenCountComputedTag(tag, ResourceType_Series, ResourceType_Instance);
+    }
+    else if (tag == DICOM_TAG_SOP_CLASSES_IN_STUDY)
+    {
+      requestedComputedTags_.insert(tag);
+      hasRequestedTags_ = true;
+      request_.GetChildrenSpecification(ResourceType_Instance).AddMetadata(MetadataType_Instance_SopClassUid);
+    }
+    else if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
+    {
+      requestedComputedTags_.insert(tag);
+      hasRequestedTags_ = true;
+      request_.GetChildrenSpecification(ResourceType_Series).AddMainDicomTag(DICOM_TAG_MODALITY);
+    }
+    else if (tag == DICOM_TAG_INSTANCE_AVAILABILITY)
+    {
+      requestedComputedTags_.insert(tag);
+      hasRequestedTags_ = true;
+    }
+    else
+    {
+      // This is neither a main DICOM tag, nor a computed DICOM tag:
+      // We will be forced to access the DICOM file anyway
+      requestedTagsFromFileStorage_.insert(tag);
+
+      if (request_.GetLevel() != ResourceType_Instance)
+      {
+        request_.SetRetrieveOneInstanceIdentifier(true);
+      }
+
+      hasRequestedTags_ = true;
+    }
+  }
+
+
+  void ResourceFinder::AddRequestedTags(const std::set<DicomTag>& tags)
+  {
+    for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      AddRequestedTag(*it);
+    }
+  }
+
+
+  static void InjectRequestedTags(DicomMap& requestedTags,
+                                  std::set<DicomTag>& missingTags /* out */,
+                                  const FindResponse::Resource& resource,
+                                  ResourceType level,
+                                  const std::set<DicomTag>& tags)
+  {
+    if (!tags.empty())
+    {
+      DicomMap m;
+      resource.GetMainDicomTags(m, level);
+
+      for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+      {
+        std::string value;
+        if (m.LookupStringValue(value, *it, false /* not binary */))
+        {
+          requestedTags.SetValue(*it, value, false /* not binary */);
+        }
+        else
+        {
+          // This is the case where the Housekeeper should be run
+          missingTags.insert(*it);
+        }
+      }
+    }
+  }
+
+
+  static void ReadMissingTagsFromStorageArea(DicomMap& requestedTags,
+                                             ServerContext& context,
+                                             const FindRequest& request,
+                                             const FindResponse::Resource& resource,
+                                             const std::set<DicomTag>& missingTags)
+  {
+    OrthancConfiguration::ReaderLock lock;
+    if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage))
+    {
+      std::string missings;
+      FromDcmtkBridge::FormatListOfTags(missings, missingTags);
+
+      LOG(WARNING) << "W001: Accessing DICOM tags from storage when accessing "
+                   << Orthanc::GetResourceTypeText(resource.GetLevel(), false, false)
+                   << ": " << missings;
+    }
+
+    std::string instancePublicId;
+
+    if (request.IsRetrieveOneInstanceIdentifier())
+    {
+      instancePublicId = resource.GetOneInstanceIdentifier();
+    }
+    else if (request.GetLevel() == ResourceType_Instance)
+    {
+      instancePublicId = resource.GetIdentifier();
+    }
+    else
+    {
+      FindRequest requestDicomAttachment(request.GetLevel());
+      requestDicomAttachment.SetOrthancId(request.GetLevel(), resource.GetIdentifier());
+      requestDicomAttachment.SetRetrieveOneInstanceIdentifier(true);
+
+      FindResponse responseDicomAttachment;
+      context.GetIndex().ExecuteFind(responseDicomAttachment, requestDicomAttachment);
+
+      if (responseDicomAttachment.GetSize() != 1 ||
+          !responseDicomAttachment.GetResourceByIndex(0).HasOneInstanceIdentifier())
+      {
+        throw OrthancException(ErrorCode_InexistentFile);
+      }
+      else
+      {
+        instancePublicId = responseDicomAttachment.GetResourceByIndex(0).GetOneInstanceIdentifier();
+      }
+    }
+
+    LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << instancePublicId;
+
+    // TODO-FIND: What do we do if the DICOM has been removed since the request?
+    // Do we fail, or do we skip the resource?
+
+    Json::Value tmpDicomAsJson;
+    context.ReadDicomAsJson(tmpDicomAsJson, instancePublicId, missingTags /* ignoreTagLength */);
+
+    DicomMap tmpDicomMap;
+    tmpDicomMap.FromDicomAsJson(tmpDicomAsJson, false /* append */, true /* parseSequences*/);
+
+    for (std::set<DicomTag>::const_iterator it = missingTags.begin(); it != missingTags.end(); ++it)
+    {
+      assert(!requestedTags.HasTag(*it));
+      if (tmpDicomMap.HasTag(*it))
+      {
+        requestedTags.SetValue(*it, tmpDicomMap.GetValue(*it));
+      }
+      else
+      {
+        requestedTags.SetNullValue(*it);  // TODO-FIND: Is this compatible with Orthanc <= 1.12.3?
+      }
+    }
+  }
+
+
+  void ResourceFinder::Execute(IVisitor& visitor,
+                               ServerContext& context) const
+  {
+    FindResponse response;
+    context.GetIndex().ExecuteFind(response, request_);
+
+    bool complete;
+
+    switch (pagingMode_)
+    {
+      case PagingMode_FullDatabase:
+      case PagingMode_ManualSkip:
+        complete = true;
+        break;
+
+      case PagingMode_FullManual:
+        complete = (databaseLimits_ == 0 ||
+                    response.GetSize() <= databaseLimits_);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (lookup_.get() != NULL)
+    {
+      LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << response.GetSize();
+    }
+
+    size_t countResults = 0;
+    size_t skipped = 0;
+
+    for (size_t i = 0; i < response.GetSize(); i++)
+    {
+      const FindResponse::Resource& resource = response.GetResourceByIndex(i);
+
+      DicomMap requestedTags;
+
+      if (hasRequestedTags_)
+      {
+        InjectComputedTags(requestedTags, resource);
+
+        std::set<DicomTag> missingTags = requestedTagsFromFileStorage_;
+        InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Patient, requestedPatientTags_);
+        InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Study, requestedStudyTags_);
+        InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Series, requestedSeriesTags_);
+        InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Instance, requestedInstanceTags_);
+
+        if (!missingTags.empty())
+        {
+          if (!allowStorageAccess_)
+          {
+            throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                                   "Cannot add missing requested tags, as access to file storage is disallowed");
+          }
+          else
+          {
+            ReadMissingTagsFromStorageArea(requestedTags, context, request_, resource, missingTags);
+          }
+        }
+      }
+
+      bool match = true;
+
+      if (lookup_.get() != NULL)
+      {
+        DicomMap tags;
+        resource.GetAllMainDicomTags(tags);
+        tags.Merge(requestedTags);
+        match = lookup_->IsMatch(tags);
+      }
+
+      if (match)
+      {
+        if (pagingMode_ == PagingMode_FullDatabase)
+        {
+          visitor.Apply(resource, requestedTags);
+        }
+        else
+        {
+          if (hasLimitsSince_ &&
+              skipped < limitsSince_)
+          {
+            skipped++;
+          }
+          else if (hasLimitsCount_ &&
+                   countResults >= limitsCount_)
+          {
+            // Too many results, don't mark as complete
+            complete = false;
+            break;
+          }
+          else
+          {
+            visitor.Apply(resource, requestedTags);
+            countResults++;
+          }
+        }
+      }
+    }
+
+    if (complete)
+    {
+      visitor.MarkAsComplete();
+    }
+  }
+
+
+  void ResourceFinder::Execute(Json::Value& target,
+                               ServerContext& context,
+                               DicomToJsonFormat format,
+                               bool includeAllMetadata) const
+  {
+    class Visitor : public IVisitor
+    {
+    private:
+      const ResourceFinder&  that_;
+      ServerIndex&           index_;
+      Json::Value&           target_;
+      DicomToJsonFormat      format_;
+      bool                   hasRequestedTags_;
+      bool                   includeAllMetadata_;
+
+    public:
+      Visitor(const ResourceFinder& that,
+              ServerIndex& index,
+              Json::Value& target,
+              DicomToJsonFormat format,
+              bool hasRequestedTags,
+              bool includeAllMetadata) :
+        that_(that),
+        index_(index),
+        target_(target),
+        format_(format),
+        hasRequestedTags_(hasRequestedTags),
+        includeAllMetadata_(includeAllMetadata)
+      {
+      }
+
+      virtual void Apply(const FindResponse::Resource& resource,
+                         const DicomMap& requestedTags) ORTHANC_OVERRIDE
+      {
+        if (that_.expand_)
+        {
+          Json::Value item;
+          that_.Expand(item, resource, index_, format_, includeAllMetadata_);
+
+          if (hasRequestedTags_)
+          {
+            static const char* const REQUESTED_TAGS = "RequestedTags";
+            item[REQUESTED_TAGS] = Json::objectValue;
+            FromDcmtkBridge::ToJson(item[REQUESTED_TAGS], requestedTags, format_);
+          }
+
+          target_.append(item);
+        }
+        else
+        {
+          target_.append(resource.GetIdentifier());
+        }
+      }
+
+      virtual void MarkAsComplete() ORTHANC_OVERRIDE
+      {
+      }
+    };
+
+    target = Json::arrayValue;
+
+    Visitor visitor(*this, context.GetIndex(), target, format, hasRequestedTags_, includeAllMetadata);
+    Execute(visitor, context);
+  }
+
+
+  bool ResourceFinder::ExecuteOneResource(Json::Value& target,
+                                          ServerContext& context,
+                                          DicomToJsonFormat format,
+                                          bool includeAllMetadata) const
+  {
+    Json::Value answer;
+    Execute(answer, context, format, includeAllMetadata);
+
+    if (answer.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else if (answer.size() > 1)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+    else if (answer.empty())
+    {
+      // Inexistent resource (or was deleted between the first and second phases)
+      return false;
+    }
+    else
+    {
+      target = answer[0];
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ResourceFinder.h	Mon Sep 02 17:17:22 2024 +0200
@@ -0,0 +1,182 @@
+/**
+ * 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/>.
+ **/
+
+
+#pragma once
+
+#include "Database/FindRequest.h"
+#include "Database/FindResponse.h"
+
+namespace Orthanc
+{
+  class DatabaseLookup;
+  class ServerContext;
+  class ServerIndex;
+
+  class ResourceFinder : public boost::noncopyable
+  {
+  public:
+    class IVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~IVisitor()
+      {
+      }
+
+      virtual void Apply(const FindResponse::Resource& resource,
+                         const DicomMap& requestedTags) = 0;
+
+      virtual void MarkAsComplete() = 0;
+    };
+
+  private:
+    enum PagingMode
+    {
+      PagingMode_FullDatabase,
+      PagingMode_FullManual,
+      PagingMode_ManualSkip
+    };
+
+    FindRequest                      request_;
+    uint64_t                         databaseLimits_;
+    std::unique_ptr<DatabaseLookup>  lookup_;
+    bool                             isSimpleLookup_;
+    PagingMode                       pagingMode_;
+    bool                             hasLimitsSince_;
+    bool                             hasLimitsCount_;
+    uint64_t                         limitsSince_;
+    uint64_t                         limitsCount_;
+    bool                             expand_;
+    bool                             allowStorageAccess_;
+    bool                             hasRequestedTags_;
+    std::set<DicomTag>               requestedPatientTags_;
+    std::set<DicomTag>               requestedStudyTags_;
+    std::set<DicomTag>               requestedSeriesTags_;
+    std::set<DicomTag>               requestedInstanceTags_;
+    std::set<DicomTag>               requestedTagsFromFileStorage_;
+    std::set<DicomTag>               requestedComputedTags_;
+
+    bool IsRequestedComputedTag(const DicomTag& tag) const
+    {
+      return requestedComputedTags_.find(tag) != requestedComputedTags_.end();
+    }
+
+    void ConfigureChildrenCountComputedTag(DicomTag tag,
+                                           ResourceType parentLevel,
+                                           ResourceType childLevel);
+
+    void InjectChildrenCountComputedTag(DicomMap& requestedTags,
+                                        DicomTag tag,
+                                        const FindResponse::Resource& resource,
+                                        ResourceType level) const;
+
+    static SeriesStatus GetSeriesStatus(uint32_t& expectedNumberOfInstances,
+                                        const FindResponse::Resource& resource);
+
+    void InjectComputedTags(DicomMap& requestedTags,
+                            const FindResponse::Resource& resource) const;
+
+    void UpdateRequestLimits();
+
+  public:
+    ResourceFinder(ResourceType level,
+                   bool expand);
+
+    void SetDatabaseLimits(uint64_t limits);
+
+    bool IsAllowStorageAccess() const
+    {
+      return allowStorageAccess_;
+    }
+
+    void SetAllowStorageAccess(bool allow)
+    {
+      allowStorageAccess_ = allow;
+    }
+
+    void SetOrthancId(ResourceType level,
+                      const std::string& id)
+    {
+      request_.SetOrthancId(level, id);
+    }
+
+    void SetLimitsSince(uint64_t since);
+
+    void SetLimitsCount(uint64_t count);
+
+    void SetDatabaseLookup(const DatabaseLookup& lookup);
+
+    void AddRequestedTag(const DicomTag& tag);
+
+    void AddRequestedTags(const std::set<DicomTag>& tags);
+
+    void SetLabels(const std::set<std::string>& labels)
+    {
+      request_.SetLabels(labels);
+    }
+
+    void AddLabel(const std::string& label)
+    {
+      request_.AddLabel(label);
+    }
+
+    void SetLabelsConstraint(LabelsConstraint constraint)
+    {
+      request_.SetLabelsConstraint(constraint);
+    }
+
+    void SetRetrieveOneInstanceIdentifier(bool retrieve)
+    {
+      request_.SetRetrieveOneInstanceIdentifier(retrieve);
+    }
+
+    void SetRetrieveMetadata(bool retrieve)
+    {
+      request_.SetRetrieveMetadata(retrieve);
+    }
+
+    void SetRetrieveAttachments(bool retrieve)
+    {
+      request_.SetRetrieveAttachments(retrieve);
+    }
+
+    // NB: "index" is only used in this method to fill the "IsStable" information
+    void Expand(Json::Value& target,
+                const FindResponse::Resource& resource,
+                ServerIndex& index,
+                DicomToJsonFormat format,
+                bool includeAllMetadata /* Same as: ExpandResourceFlags_IncludeAllMetadata */) const;
+
+    void Execute(IVisitor& visitor,
+                 ServerContext& context) const;
+
+    void Execute(Json::Value& target,
+                 ServerContext& context,
+                 DicomToJsonFormat format,
+                 bool includeAllMetadata) const;
+
+    bool ExecuteOneResource(Json::Value& target,
+                            ServerContext& context,
+                            DicomToJsonFormat format,
+                            bool includeAllMetadata) const;
+  };
+}
--- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -37,6 +37,7 @@
 #  include <OrthancException.h>
 #endif
 
+#include <boost/lexical_cast.hpp>
 #include <cassert>
 
 
@@ -284,4 +285,64 @@
       return *constraints_[index];
     }
   }
+
+
+  std::string DatabaseConstraints::Format() const
+  {
+    std::string s;
+
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      const DatabaseConstraint& constraint = *constraints_[i];
+      s += "Constraint " + boost::lexical_cast<std::string>(i) + " at " + EnumerationToString(constraint.GetLevel()) +
+        ": " + constraint.GetTag().Format();
+
+      switch (constraint.GetConstraintType())
+      {
+        case ConstraintType_Equal:
+          s += " == " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_SmallerOrEqual:
+          s += " <= " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_GreaterOrEqual:
+          s += " >= " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_Wildcard:
+          s += " ~~ " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_List:
+        {
+          s += " in [ ";
+          bool first = true;
+          for (size_t j = 0; j < constraint.GetValuesCount(); j++)
+          {
+            if (first)
+            {
+              first = false;
+            }
+            else
+            {
+              s += ", ";
+            }
+            s += constraint.GetValue(j);
+          }
+          s += "]";
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      s += "\n";
+    }
+
+    return s;
+  }
 }
--- a/OrthancServer/Sources/Search/DatabaseConstraint.h	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.h	Mon Sep 02 17:17:22 2024 +0200
@@ -178,5 +178,7 @@
     }
 
     const DatabaseConstraint& GetConstraint(size_t index) const;
+
+    std::string Format() const;
   };
 }
--- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -215,6 +215,24 @@
   }
 
 
+  static bool HasIntersection(const std::set<std::string>& expected,
+                              const std::string& values)
+  {
+    std::vector<std::string> tokens;
+    Toolbox::TokenizeString(tokens, values, '\\');
+
+    for (size_t i = 0; i < tokens.size(); i++)
+    {
+      if (expected.find(tokens[i]) != expected.end())
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
   bool DicomTagConstraint::IsMatch(const std::string& value) const
   {
     NormalizedString source(value, caseSensitive_);
@@ -224,7 +242,17 @@
       case ConstraintType_Equal:
       {
         NormalizedString reference(GetValue(), caseSensitive_);
-        return source.GetValue() == reference.GetValue();
+
+        if (GetTag() == DICOM_TAG_MODALITIES_IN_STUDY)
+        {
+          std::set<std::string> expected;
+          expected.insert(reference.GetValue());
+          return HasIntersection(expected, source.GetValue());
+        }
+        else
+        {
+          return source.GetValue() == reference.GetValue();
+        }
       }
 
       case ConstraintType_SmallerOrEqual:
@@ -251,17 +279,16 @@
 
       case ConstraintType_List:
       {
+        std::set<std::string> references;
+
         for (std::set<std::string>::const_iterator
                it = values_.begin(); it != values_.end(); ++it)
         {
           NormalizedString reference(*it, caseSensitive_);
-          if (source.GetValue() == reference.GetValue())
-          {
-            return true;
-          }
+          references.insert(reference.GetValue());
         }
 
-        return false;
+        return HasIntersection(references, source.GetValue());
       }
 
       default:
@@ -342,7 +369,8 @@
   }
 
 
-  DatabaseConstraint* DicomTagConstraint::ConvertToDatabaseConstraint(ResourceType level,
+  DatabaseConstraint* DicomTagConstraint::ConvertToDatabaseConstraint(bool& isIdentical,
+                                                                      ResourceType level,
                                                                       DicomTagType tagType) const
   {
     bool isIdentifier, caseSensitive;
@@ -365,13 +393,21 @@
 
     std::vector<std::string> values;
     values.reserve(values_.size());
-      
+
+    isIdentical = true;
+
     for (std::set<std::string>::const_iterator
            it = values_.begin(); it != values_.end(); ++it)
     {
       if (isIdentifier)
       {
-        values.push_back(ServerToolbox::NormalizeIdentifier(*it));
+        std::string normalized = ServerToolbox::NormalizeIdentifier(*it);
+        values.push_back(normalized);
+
+        if (normalized != *it)
+        {
+          isIdentical = false;
+        }
       }
       else
       {
--- a/OrthancServer/Sources/Search/DicomTagConstraint.h	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.h	Mon Sep 02 17:17:22 2024 +0200
@@ -109,7 +109,8 @@
 
     std::string Format() const;
 
-    DatabaseConstraint* ConvertToDatabaseConstraint(ResourceType level,
+    DatabaseConstraint* ConvertToDatabaseConstraint(bool& isIdentical /* out */,
+                                                    ResourceType level,
                                                     DicomTagType tagType) const;
   };
 }
--- a/OrthancServer/Sources/ServerContext.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -42,6 +42,7 @@
 
 #include "OrthancConfiguration.h"
 #include "OrthancRestApi/OrthancRestApi.h"
+#include "ResourceFinder.h"
 #include "Search/DatabaseLookup.h"
 #include "ServerJobs/OrthancJobUnserializer.h"
 #include "ServerToolbox.h"
@@ -1549,8 +1550,7 @@
                             size_t since,
                             size_t limit)
   {    
-    unsigned int databaseLimit = (queryLevel == ResourceType_Instance ?
-                                  limitFindInstances_ : limitFindResults_);
+    const uint64_t databaseLimit = GetDatabaseLimits(queryLevel);
       
     std::vector<std::string> resources, instances;
     const DicomTagConstraint* dicomModalitiesConstraint = NULL;
@@ -1567,10 +1567,8 @@
       fastLookup->RemoveConstraint(DICOM_TAG_MODALITIES_IN_STUDY);
     }
 
-    {
-      const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);
-      GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, labels, labelsConstraint, lookupLimit);
-    }
+    const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);
+    GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, labels, labelsConstraint, lookupLimit);
 
     bool complete = (databaseLimit == 0 ||
                      resources.size() <= databaseLimit);
@@ -2137,124 +2135,137 @@
   static void SerializeExpandedResource(Json::Value& target,
                                         const ExpandedResource& resource,
                                         DicomToJsonFormat format,
-                                        const std::set<DicomTag>& requestedTags)
+                                        const std::set<DicomTag>& requestedTags,
+                                        ExpandResourceFlags expandFlags)
   {
     target = Json::objectValue;
 
     target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true);
     target["ID"] = resource.GetPublicId();
 
-    switch (resource.GetLevel())
-    {
-      case ResourceType_Patient:
-        break;
-
-      case ResourceType_Study:
-        target["ParentPatient"] = resource.parentId_;
-        break;
-
-      case ResourceType_Series:
-        target["ParentStudy"] = resource.parentId_;
-        break;
-
-      case ResourceType_Instance:
-        target["ParentSeries"] = resource.parentId_;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    switch (resource.GetLevel())
+    if (!resource.parentId_.empty())
     {
-      case ResourceType_Patient:
-      case ResourceType_Study:
-      case ResourceType_Series:
+      switch (resource.GetLevel())
       {
-        Json::Value c = Json::arrayValue;
-
-        for (std::list<std::string>::const_iterator
-                it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it)
-        {
-          c.append(*it);
-        }
-
-        if (resource.GetLevel() == ResourceType_Patient)
-        {
-          target["Studies"] = c;
-        }
-        else if (resource.GetLevel() == ResourceType_Study)
-        {
-          target["Series"] = c;
-        }
-        else
-        {
-          target["Instances"] = c;
-        }
-        break;
+        case ResourceType_Patient:
+          break;
+
+        case ResourceType_Study:
+          target["ParentPatient"] = resource.parentId_;
+          break;
+
+        case ResourceType_Series:
+          target["ParentStudy"] = resource.parentId_;
+          break;
+
+        case ResourceType_Instance:
+          target["ParentSeries"] = resource.parentId_;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
       }
-
-      case ResourceType_Instance:
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
     }
 
-    switch (resource.GetLevel())
+    if ((expandFlags & ExpandResourceFlags_IncludeChildren) != 0)
     {
-      case ResourceType_Patient:
-      case ResourceType_Study:
-        break;
-
-      case ResourceType_Series:
-        if (resource.expectedNumberOfInstances_ < 0)
+      switch (resource.GetLevel())
+      {
+        case ResourceType_Patient:
+        case ResourceType_Study:
+        case ResourceType_Series:
         {
-          target["ExpectedNumberOfInstances"] = Json::nullValue;
+          Json::Value c = Json::arrayValue;
+
+          for (std::list<std::string>::const_iterator
+                  it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it)
+          {
+            c.append(*it);
+          }
+
+          if (resource.GetLevel() == ResourceType_Patient)
+          {
+            target["Studies"] = c;
+          }
+          else if (resource.GetLevel() == ResourceType_Study)
+          {
+            target["Series"] = c;
+          }
+          else
+          {
+            target["Instances"] = c;
+          }
+          break;
         }
-        else
-        {
-          target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_;
-        }
-        target["Status"] = resource.status_;
-        break;
-
-      case ResourceType_Instance:
+
+        case ResourceType_Instance:
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    if ((expandFlags & ExpandResourceFlags_IncludeMetadata) != 0)
+    {
+      switch (resource.GetLevel())
       {
-        target["FileSize"] = static_cast<unsigned int>(resource.fileSize_);
-        target["FileUuid"] = resource.fileUuid_;
-
-        if (resource.indexInSeries_ < 0)
+        case ResourceType_Patient:
+        case ResourceType_Study:
+          break;
+
+        case ResourceType_Series:
+          if (resource.expectedNumberOfInstances_ < 0)
+          {
+            target["ExpectedNumberOfInstances"] = Json::nullValue;
+          }
+          else
+          {
+            target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_;
+          }
+          target["Status"] = resource.status_;
+          break;
+
+        case ResourceType_Instance:
         {
-          target["IndexInSeries"] = Json::nullValue;
+          target["FileSize"] = static_cast<unsigned int>(resource.fileSize_);
+          target["FileUuid"] = resource.fileUuid_;
+
+          if (resource.indexInSeries_ < 0)
+          {
+            target["IndexInSeries"] = Json::nullValue;
+          }
+          else
+          {
+            target["IndexInSeries"] = resource.indexInSeries_;
+          }
+
+          break;
         }
-        else
-        {
-          target["IndexInSeries"] = resource.indexInSeries_;
-        }
-
-        break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
       }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (!resource.anonymizedFrom_.empty())
-    {
-      target["AnonymizedFrom"] = resource.anonymizedFrom_;
-    }
     
-    if (!resource.modifiedFrom_.empty())
-    {
-      target["ModifiedFrom"] = resource.modifiedFrom_;
+      if (!resource.anonymizedFrom_.empty())
+      {
+        target["AnonymizedFrom"] = resource.anonymizedFrom_;
+      }
+      
+      if (!resource.modifiedFrom_.empty())
+      {
+        target["ModifiedFrom"] = resource.modifiedFrom_;
+      }
     }
 
     if (resource.GetLevel() == ResourceType_Patient ||
         resource.GetLevel() == ResourceType_Study ||
         resource.GetLevel() == ResourceType_Series)
     {
-      target["IsStable"] = resource.isStable_;
+      if ((expandFlags & ExpandResourceFlags_IncludeIsStable) != 0)
+      {
+        target["IsStable"] = resource.isStable_;
+      }
 
       if (!resource.lastUpdate_.empty())
       {
@@ -2262,38 +2273,42 @@
       }
     }
 
-    // serialize tags
-
-    static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
-    static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
-
-    DicomMap mainDicomTags;
-    resource.GetMainDicomTags().ExtractResourceInformation(mainDicomTags, resource.GetLevel());
-
-    target[MAIN_DICOM_TAGS] = Json::objectValue;
-    FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format);
-    
-    if (resource.GetLevel() == ResourceType_Study)
+    if ((expandFlags & ExpandResourceFlags_IncludeMainDicomTags) != 0)
     {
-      DicomMap patientMainDicomTags;
-      resource.GetMainDicomTags().ExtractPatientInformation(patientMainDicomTags);
-
-      target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format);
+      // serialize tags
+
+      static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
+      static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
+
+      DicomMap mainDicomTags;
+      resource.GetMainDicomTags().ExtractResourceInformation(mainDicomTags, resource.GetLevel());
+
+      target[MAIN_DICOM_TAGS] = Json::objectValue;
+      FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format);
+      
+      if (resource.GetLevel() == ResourceType_Study)
+      {
+        DicomMap patientMainDicomTags;
+        resource.GetMainDicomTags().ExtractPatientInformation(patientMainDicomTags);
+
+        target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue;
+        FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format);
+      }
+
+      if (requestedTags.size() > 0)
+      {
+        static const char* const REQUESTED_TAGS = "RequestedTags";
+
+        DicomMap tags;
+        resource.GetMainDicomTags().ExtractTags(tags, requestedTags);
+
+        target[REQUESTED_TAGS] = Json::objectValue;
+        FromDcmtkBridge::ToJson(target[REQUESTED_TAGS], tags, format);
+
+      }
     }
 
-    if (requestedTags.size() > 0)
-    {
-      static const char* const REQUESTED_TAGS = "RequestedTags";
-
-      DicomMap tags;
-      resource.GetMainDicomTags().ExtractTags(tags, requestedTags);
-
-      target[REQUESTED_TAGS] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target[REQUESTED_TAGS], tags, format);
-
-    }
-
+    if ((expandFlags & ExpandResourceFlags_IncludeLabels) != 0)
     {
       Json::Value labels = Json::arrayValue;
 
@@ -2304,6 +2319,19 @@
 
       target["Labels"] = labels;
     }
+
+    // new in Orthanc 1.12.4
+    if ((expandFlags & ExpandResourceFlags_IncludeAllMetadata) != 0)
+    {
+      Json::Value metadata = Json::objectValue;
+
+      for (std::map<MetadataType, std::string>::const_iterator it = resource.metadata_.begin(); it != resource.metadata_.end(); ++it)
+      {
+        metadata[EnumerationToString(it->first)] = it->second;
+      }
+
+      target["Metadata"] = metadata;
+    }
   }
 
 
@@ -2549,9 +2577,9 @@
   {
     ExpandedResource resource;
 
-    if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceFlags_Default, allowStorageAccess))
+    if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceFlags_DefaultExtract, allowStorageAccess))
     {
-      SerializeExpandedResource(target, resource, format, requestedTags);
+      SerializeExpandedResource(target, resource, format, requestedTags, ExpandResourceFlags_DefaultOutput);
       return true;
     }
 
@@ -2704,5 +2732,4 @@
 
     return elapsed.total_seconds();
   }
-
 }
--- a/OrthancServer/Sources/ServerContext.h	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Mon Sep 02 17:17:22 2024 +0200
@@ -445,6 +445,11 @@
 
     void Stop();
 
+    uint64_t GetDatabaseLimits(ResourceType level) const
+    {
+      return (level == ResourceType_Instance ? limitFindInstances_ : limitFindResults_);
+    }
+
     void Apply(ILookupVisitor& visitor,
                const DatabaseLookup& lookup,
                ResourceType queryLevel,
--- a/OrthancServer/Sources/ServerIndex.h	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/ServerIndex.h	Mon Sep 02 17:17:22 2024 +0200
@@ -64,9 +64,6 @@
                         int64_t id,
                         const std::string& publicId);
 
-    bool IsUnstableResource(ResourceType type,
-                            int64_t id);
-
   public:
     ServerIndex(ServerContext& context,
                 IDatabaseWrapper& database,
@@ -103,5 +100,8 @@
                               bool hasOldRevision,
                               int64_t oldRevision,
                               const std::string& oldMD5);
+
+    bool IsUnstableResource(ResourceType type,
+                            int64_t id);
   };
 }
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -744,10 +744,18 @@
         }
         else
         {
-          ExpandedResource originalStudy;
-          if (GetContext().GetIndex().ExpandResource(originalStudy, *studyId, ResourceType_Study, emptyRequestedTags, ExpandResourceFlags_IncludeMainDicomTags))
+          FindRequest request(ResourceType_Study);
+          request.SetOrthancStudyId(*studyId);
+          request.SetRetrieveMainDicomTags(true);
+
+          FindResponse response;
+          GetContext().GetIndex().ExecuteFind(response, request);
+
+          if (response.GetSize() == 1)
           {
-            targetPatientId = originalStudy.GetMainDicomTags().GetStringValue(DICOM_TAG_PATIENT_ID, "", false);
+            DicomMap tags;
+            response.GetResourceByIndex(0).GetMainDicomTags(tags, ResourceType_Study);
+            targetPatientId = tags.GetStringValue(DICOM_TAG_PATIENT_ID, "", false);
           }
           else
           {
@@ -762,22 +770,34 @@
         // if the patient exists, check how many child studies it has.
         if (lookupPatientResult.size() >= 1)
         {
-          ExpandedResource targetPatient;
-          
-          if (GetContext().GetIndex().ExpandResource(targetPatient, lookupPatientResult[0], ResourceType_Patient, emptyRequestedTags, static_cast<ExpandResourceFlags>(ExpandResourceFlags_IncludeMainDicomTags | ExpandResourceFlags_IncludeChildren)))
+          FindRequest request(ResourceType_Patient);
+          request.SetOrthancPatientId(lookupPatientResult[0]);
+          request.SetRetrieveMainDicomTags(true);
+          request.GetChildrenSpecification(ResourceType_Study).SetRetrieveIdentifiers(true);
+
+          FindResponse response;
+          GetContext().GetIndex().ExecuteFind(response, request);
+
+          if (response.GetSize() == 1)
           {
-            const std::list<std::string> childrenIds = targetPatient.childrenIds_;
+            const FindResponse::Resource& targetPatient = response.GetResourceByIndex(0);
+
+            const std::set<std::string>& childrenIds = targetPatient.GetChildrenIdentifiers(ResourceType_Study);
+
             bool targetPatientHasOtherStudies = childrenIds.size() > 1;
             if (childrenIds.size() == 1)
             {
-              targetPatientHasOtherStudies = std::find(childrenIds.begin(), childrenIds.end(), *studyId) == childrenIds.end();  // if the patient has one study that is not the one being modified
+              targetPatientHasOtherStudies = (childrenIds.find(*studyId) == childrenIds.end());  // if the patient has one study that is not the one being modified
             }
 
             if (targetPatientHasOtherStudies)
             {
+              DicomMap mainDicomTags;
+              targetPatient.GetMainDicomTags(mainDicomTags, ResourceType_Patient);
+
               // this is allowed if all patient replacedTags do match the target patient tags
               DicomMap targetPatientTags;
-              targetPatient.GetMainDicomTags().ExtractPatientInformation(targetPatientTags);
+              mainDicomTags.ExtractPatientInformation(targetPatientTags);
 
               std::set<DicomTag> mainPatientTags;
               DicomMap::GetMainDicomTags(mainPatientTags, ResourceType_Patient);
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Thu Aug 29 13:46:49 2024 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Mon Sep 02 17:17:22 2024 +0200
@@ -167,7 +167,8 @@
       DicomTagConstraint c(tag, type, value, true, true);
       
       DatabaseConstraints lookup;
-      lookup.AddConstraint(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+      bool isEquivalent;  // unused
+      lookup.AddConstraint(c.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier));
 
       std::set<std::string> noLabel;
       transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */);
@@ -187,8 +188,9 @@
       DicomTagConstraint c2(tag, type2, value2, true, true);
 
       DatabaseConstraints lookup;
-      lookup.AddConstraint(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
-      lookup.AddConstraint(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+      bool isEquivalent;  // unused
+      lookup.AddConstraint(c1.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier));
+      lookup.AddConstraint(c2.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier));
       
       std::set<std::string> noLabel;
       transaction_->ApplyLookupResources(result, NULL, lookup, level, noLabel, LabelsConstraint_All, 0 /* no limit */);