changeset 5691:c352d762177c find-refactoring

integration mainline->find-refactoring
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 09 Jul 2024 18:06:21 +0200
parents 708952bd869c (diff) 13b1ff603b37 (current diff)
children 7c11a71927a9
files
diffstat 33 files changed, 5142 insertions(+), 347 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Jul 09 15:32:30 2024 +0200
+++ b/NEWS	Tue Jul 09 18:06:21 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/SQLite/Connection.h	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/Connection.h	Tue Jul 09 18:06:21 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	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementId.cpp	Tue Jul 09 18:06:21 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	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementId.h	Tue Jul 09 18:06:21 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	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/CMakeLists.txt	Tue Jul 09 18:06:21 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	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Tue Jul 09 18:06:21 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"
@@ -1278,6 +1279,35 @@
     {
       ListLabelsInternal(target, false, -1);
     }
+
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request,
+                             const Capabilities& capabilities) ORTHANC_OVERRIDE
+    {
+      // TODO-FIND
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+
+    virtual void ExecuteFind(std::list<std::string>& identifiers,
+                             const FindRequest& request,
+                             const Capabilities& capabilities) ORTHANC_OVERRIDE
+    {
+      // TODO-FIND
+      Compatibility::GenericFind find(*this);
+      find.ExecuteFind(identifiers, request, capabilities);
+    }
+
+
+    virtual void ExecuteExpand(FindResponse& response,
+                               const FindRequest& request,
+                               const std::string& identifier) ORTHANC_OVERRIDE
+    {
+      // TODO-FIND
+      Compatibility::GenericFind find(*this);
+      find.ExecuteExpand(response, request, identifier);
+    }
   };
 
 
@@ -1492,4 +1522,10 @@
       return dbCapabilities_;
     }
   }
+
+
+  bool OrthancPluginDatabaseV4::HasIntegratedFind() const
+  {
+    return false;  // TODO-FIND
+  }
 }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Tue Jul 09 18:06:21 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/Resources/Orthanc.doxygen	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Resources/Orthanc.doxygen	Tue Jul 09 18:06:21 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/Sources/Database/BaseDatabaseWrapper.cpp	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp	Tue Jul 09 18:06:21 2024 +0200
@@ -24,6 +24,7 @@
 #include "BaseDatabaseWrapper.h"
 
 #include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "Compatibility/GenericFind.h"
 
 namespace Orthanc
 {
@@ -46,6 +47,32 @@
   }
 
 
+  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 FindRequest& request,
+                                                         const Capabilities& capabilities)
+  {
+    Compatibility::GenericFind find(*this);
+    find.ExecuteFind(identifiers, request, capabilities);
+  }
+
+
+  void BaseDatabaseWrapper::BaseTransaction::ExecuteExpand(FindResponse& response,
+                                                           const FindRequest& request,
+                                                           const std::string& identifier)
+  {
+    Compatibility::GenericFind find(*this);
+    find.ExecuteExpand(response, request, identifier);
+  }
+
+
   uint64_t BaseDatabaseWrapper::MeasureLatency()
   {
     throw OrthancException(ErrorCode_NotImplemented);  // only implemented in V4
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.h	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h	Tue Jul 09 18:06:21 2024 +0200
@@ -47,8 +47,25 @@
                                           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 FindRequest& request,
+                               const Capabilities& capabilities) ORTHANC_OVERRIDE;
+
+      virtual void ExecuteExpand(FindResponse& response,
+                                 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	Tue Jul 09 18:06:21 2024 +0200
@@ -0,0 +1,590 @@
+/**
+ * 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 FindRequest& request,
+                                  const IDatabaseWrapper::Capabilities& capabilities)
+    {
+      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())
+      {
+        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 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 (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	Tue Jul 09 18:06:21 2024 +0200
@@ -0,0 +1,56 @@
+/**
+ * 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 FindRequest& request,
+                       const IDatabaseWrapper::Capabilities& capabilities);
+
+      void ExecuteExpand(FindResponse& response,
+                         const FindRequest& request,
+                         const std::string& identifier);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindRequest.cpp	Tue Jul 09 18:06:21 2024 +0200
@@ -0,0 +1,258 @@
+/**
+ * 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_))
+    {
+      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))
+    {
+      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	Tue Jul 09 18:06:21 2024 +0200
@@ -0,0 +1,432 @@
+/**
+ * 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:
+    /**
+
+       TO DISCUSS:
+
+       (1) ResponseContent_ChildInstanceId       = (1 << 6),     // When you need to access all tags from a patient/study/series, you might need to open the DICOM file of a child instance
+
+       if (requestedTags.size() > 0 && resourceType != ResourceType_Instance) // if we are requesting specific tags that might be outside of the MainDicomTags, we must get a childInstanceId too
+       {
+       responseContent = static_cast<FindRequest::ResponseContent>(responseContent | FindRequest::ResponseContent_ChildInstanceId);
+       }
+
+
+       (2) ResponseContent_IsStable              = (1 << 8),     // This is currently not saved in DB but it could be in the future.
+
+     **/
+
+
+    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)
+    std::deque<void*>   /* TODO-FIND */       metadataConstraints_;  // All metadata filters (note: the order is not important)
+    bool                                 hasLimits_;
+    uint64_t                             limitsSince_;
+    uint64_t                             limitsCount_;
+    std::set<std::string>                labels_;
+    LabelsConstraint                     labelsConstraint_;
+    std::deque<Ordering*>                ordering_;             // The ordering criteria (note: the order is 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 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_ ||
+              GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers());
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindResponse.cpp	Tue Jul 09 18:06:21 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]).IsRetrieveMainDicomTags())
+        {
+          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	Tue Jul 09 18:06:21 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	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Tue Jul 09 18:06:21 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>
@@ -351,6 +353,33 @@
                                           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 FindRequest& request,
+                               const Capabilities& capabilities) = 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 FindRequest& request,
+                                 const std::string& identifier) = 0;
     };
 
 
@@ -375,5 +404,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	Tue Jul 09 18:06:21 2024 +0200
@@ -0,0 +1,128 @@
+/**
+ * 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();
+    }
+  }
+
+
+  void MainDicomTagsRegistry::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));
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.h	Tue Jul 09 18:06:21 2024 +0200
@@ -0,0 +1,82 @@
+/**
+ * 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;
+
+    void 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	Tue Jul 09 18:06:21 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	Tue Jul 09 18:06:21 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	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Tue Jul 09 18:06:21 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"
@@ -1138,6 +1139,175 @@
         target.insert(s.ColumnString(0));
       }
     }
+
+
+#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/Sources/Database/StatelessDatabaseOperations.cpp	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Tue Jul 09 18:06:21 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);
 
@@ -2515,7 +2380,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 +2679,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 +2805,7 @@
   }                                                                           
 
   bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount,
-                                                                                   const std::string& patientId)
+                                                                                    const std::string& patientId)
   {
     if (maximumPatientCount != 0)
     {
@@ -3042,7 +2907,7 @@
     };
 
     if (maximumStorageMode == MaxStorageMode_Recycle 
-      && (maximumStorageSize != 0 || maximumPatientCount != 0))
+        && (maximumStorageSize != 0 || maximumPatientCount != 0))
     {
       Operations operations(maximumStorageSize, maximumPatientCount);
       Apply(operations);
@@ -3106,9 +2971,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 +3166,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 +3195,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 +3671,77 @@
     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 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 ExpandStage : public ReadOnlyOperationsT3<FindResponse&, 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>());
+      }
+    };
+
+    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, request, capabilities);
+
+      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, request, *it);
+      }
+    }
+  }
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Tue Jul 09 18:06:21 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,27 @@
       {
         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 FindRequest& request,
+                       const IDatabaseWrapper::Capabilities& capabilities)
+      {
+        transaction_.ExecuteFind(identifiers, request, capabilities);
+      }
+
+      void ExecuteExpand(FindResponse& response,
+                         const FindRequest& request,
+                         const std::string& identifier)
+      {
+        transaction_.ExecuteExpand(response, request, identifier);
+      }
     };
 
 
@@ -545,7 +578,6 @@
     
 
   private:
-    class MainDicomTagsRegistry;
     class Transaction;
 
     IDatabaseWrapper&                            db_;
@@ -798,5 +830,8 @@
                    const std::set<std::string>& labels);
 
     bool HasLabelsSupport();
+
+    void ExecuteFind(FindResponse& response,
+                     const FindRequest& request);
   };
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Jul 09 18:06:21 2024 +0200
@@ -22,6 +22,8 @@
 
 
 #include "../PrecompiledHeadersServer.h"
+#include "../ResourceFinder.h"
+
 #include "OrthancRestApi.h"
 
 #include "../../../OrthancFramework/Sources/Compression/GzipCompressor.h"
@@ -129,7 +131,7 @@
 
   // 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 +185,7 @@
   }
 
 
-  static void AnswerListOfResources(RestApiOutput& output,
+  static void AnswerListOfResources2(RestApiOutput& output,
                                     ServerContext& context,
                                     const std::list<std::string>& resources,
                                     ResourceType level,
@@ -196,7 +198,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 +228,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);
+      finder.SetFormat(OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human));
+
+      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.SetLimits(since, 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);
+      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 +342,35 @@
     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.SetFormat(OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human));
+      finder.SetOrthancId(resourceType, call.GetUriComponent("id", ""));
+
+      Json::Value json;
+      if (finder.ExecuteOneResource(json, OrthancRestApi::GetContext(call)))
+      {
+        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 +3217,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 +3329,143 @@
       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));
+      finder.SetFormat(OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human));
+
+      size_t limit = 0;
+      if (request.isMember(KEY_LIMIT))
+      {
+        int tmp = request[KEY_LIMIT].asInt();
+        if (tmp < 0)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer");
+        }
+
+        limit = static_cast<size_t>(tmp);
+      }
+
+      size_t since = 0;
+      if (request.isMember(KEY_SINCE))
+      {
+        int tmp = request[KEY_SINCE].asInt();
+        if (tmp < 0)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer");
+        }
+
+        since = static_cast<size_t>(tmp);
+      }
+
+      if (request.isMember(KEY_LIMIT) ||
+          request.isMember(KEY_SINCE))
+      {
+        finder.SetLimits(since, limit);
+      }
+
+      {
+        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);
+      call.GetOutput().AnswerJson(answer);
+    }
     else
     {
+      /**
+       * VERSION IN ORTHANC <= 1.12.4
+       **/
       bool expand = false;
       if (request.isMember(KEY_EXPAND))
       {
@@ -3398,34 +3610,57 @@
     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);
+      finder.SetFormat(format);
+
+      Json::Value answer;
+      finder.Execute(answer, context);
+      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 */);
   }
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ResourceFinder.cpp	Tue Jul 09 18:06:21 2024 +0200
@@ -0,0 +1,1010 @@
+/**
+ * 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) const
+  {
+    /**
+     * This method closely follows "SerializeExpandedResource()" in
+     * "ServerContext.cpp" from Orthanc 1.12.4.
+     **/
+
+    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 index;
+        if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_IndexInSeries) &&
+            SerializationToolbox::ParseUnsignedInteger32(index, s))
+        {
+          target[INDEX_IN_SERIES] = index;
+        }
+        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
+    isDatabasePaging_ = false;
+
+    if (databaseLimits_ != 0)
+    {
+      request_.SetLimits(0, databaseLimits_ + 1);
+    }
+
+    if (lookup_.get() == NULL ||
+        lookup_->HasOnlyMainDicomTags())
+    {
+      // TODO-FIND: Understand why this doesn't work:
+      // $ ./Start.sh --force Orthanc.test_rest_find_limit Orthanc.test_resources_since_limit Orthanc.test_rest_find_limit
+
+      /*if (hasLimits_)
+      {
+        isDatabasePaging_ = true;
+        request_.SetLimits(limitsSince_, limitsCount_);
+        }*/
+    }
+
+    // TODO-FIND: More cases could be added, depending on "GetDatabaseCapabilities()"
+  }
+
+
+  ResourceFinder::ResourceFinder(ResourceType level,
+                                 bool expand) :
+    request_(level),
+    databaseLimits_(0),
+    isDatabasePaging_(true),
+    hasLimits_(false),
+    limitsSince_(0),
+    limitsCount_(0),
+    expand_(expand),
+    format_(DicomToJsonFormat_Human),
+    allowStorageAccess_(true),
+    hasRequestedTags_(false),
+    includeAllMetadata_(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::SetLimits(uint64_t since,
+                                 uint64_t count)
+  {
+    if (hasLimits_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      hasLimits_ = true;
+      limitsSince_ = since;
+      limitsCount_ = count;
+      UpdateRequestLimits();
+    }
+  }
+
+
+  void ResourceFinder::SetDatabaseLookup(const DatabaseLookup& lookup)
+  {
+    lookup_.reset(lookup.Clone());
+    UpdateRequestLimits();
+
+    for (size_t i = 0; i < lookup.GetConstraintsCount(); i++)
+    {
+      DicomTag tag = lookup.GetConstraint(i).GetTag();
+      if (IsComputedTag(tag))
+      {
+        AddRequestedTag(tag);
+      }
+    }
+
+    MainDicomTagsRegistry registry;
+    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);
+      }
+    }
+  }
+
+
+  void ResourceFinder::AddRequestedTag(const DicomTag& tag)
+  {
+    if (DicomMap::IsMainDicomTag(tag, ResourceType_Patient))
+    {
+      if (request_.GetLevel() == ResourceType_Patient)
+      {
+        request_.GetParentSpecification(ResourceType_Patient).SetRetrieveMainDicomTags(true);
+        request_.GetParentSpecification(ResourceType_Patient).SetRetrieveMetadata(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()".
+         **/
+        request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true);
+        request_.GetParentSpecification(ResourceType_Study).SetRetrieveMetadata(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
+      {
+        request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true);
+        request_.GetParentSpecification(ResourceType_Study).SetRetrieveMetadata(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
+      {
+        request_.GetParentSpecification(ResourceType_Series).SetRetrieveMainDicomTags(true);
+        request_.GetParentSpecification(ResourceType_Series).SetRetrieveMetadata(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
+      {
+        // Main DICOM tags from the instance level will be retrieved anyway
+        assert(request_.IsRetrieveMainDicomTags());
+        assert(request_.IsRetrieveMetadata());
+        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(FindResponse& response,
+                               ServerIndex& index) const
+  {
+    index.ExecuteFind(response, request_);
+  }
+
+  
+  void ResourceFinder::Execute(IVisitor& visitor,
+                               ServerContext& context) const
+  {
+    FindResponse response;
+    context.GetIndex().ExecuteFind(response, request_);
+
+    bool complete;
+    if (isDatabasePaging_)
+    {
+      complete = true;
+    }
+    else
+    {
+      complete = (databaseLimits_ == 0 ||
+                  response.GetSize() <= databaseLimits_);
+    }
+
+    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 (isDatabasePaging_)
+        {
+          visitor.Apply(resource, hasRequestedTags_, requestedTags);
+        }
+        else
+        {
+          if (hasLimits_ &&
+              skipped < limitsSince_)
+          {
+            skipped++;
+          }
+          else if (hasLimits_ &&
+                   countResults >= limitsCount_)
+          {
+            // Too many results, don't mark as complete
+            complete = false;
+            break;
+          }
+          else
+          {
+            visitor.Apply(resource, hasRequestedTags_, requestedTags);
+            countResults++;
+          }
+        }
+      }
+    }
+
+    if (complete)
+    {
+      visitor.MarkAsComplete();
+    }
+  }
+
+
+  void ResourceFinder::Execute(Json::Value& target,
+                               ServerContext& context) const
+  {
+    class Visitor : public IVisitor
+    {
+    private:
+      const ResourceFinder&  that_;
+      ServerIndex& index_;
+      Json::Value& target_;
+
+    public:
+      Visitor(const ResourceFinder& that,
+              ServerIndex& index,
+              Json::Value& target) :
+        that_(that),
+        index_(index),
+        target_(target)
+      {
+      }
+
+      virtual void Apply(const FindResponse::Resource& resource,
+                         bool hasRequestedTags,
+                         const DicomMap& requestedTags) ORTHANC_OVERRIDE
+      {
+        if (that_.expand_)
+        {
+          Json::Value item;
+          that_.Expand(item, resource, index_);
+
+          if (hasRequestedTags)
+          {
+            static const char* const REQUESTED_TAGS = "RequestedTags";
+            item[REQUESTED_TAGS] = Json::objectValue;
+            FromDcmtkBridge::ToJson(item[REQUESTED_TAGS], requestedTags, that_.format_);
+          }
+
+          target_.append(item);
+        }
+        else
+        {
+          target_.append(resource.GetIdentifier());
+        }
+      }
+
+      virtual void MarkAsComplete() ORTHANC_OVERRIDE
+      {
+      }
+    };
+
+    target = Json::arrayValue;
+
+    Visitor visitor(*this, context.GetIndex(), target);
+    Execute(visitor, context);
+  }
+
+
+  bool ResourceFinder::ExecuteOneResource(Json::Value& target,
+                                          ServerContext& context) const
+  {
+    Json::Value answer;
+    Execute(answer, context);
+
+    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	Tue Jul 09 18:06:21 2024 +0200
@@ -0,0 +1,171 @@
+/**
+ * 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,
+                         bool hasRequestedTags,
+                         const DicomMap& requestedTags) = 0;
+
+      virtual void MarkAsComplete() = 0;
+    };
+
+  private:
+    FindRequest                      request_;
+    uint64_t                         databaseLimits_;
+    std::unique_ptr<DatabaseLookup>  lookup_;
+    bool                             isDatabasePaging_;
+    bool                             hasLimits_;
+    uint64_t                         limitsSince_;
+    uint64_t                         limitsCount_;
+    bool                             expand_;
+    DicomToJsonFormat                format_;
+    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                             includeAllMetadata_;   // Same as: ExpandResourceFlags_IncludeAllMetadata
+
+    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 Expand(Json::Value& target,
+                const FindResponse::Resource& resource,
+                ServerIndex& index) 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 SetFormat(DicomToJsonFormat format)
+    {
+      format_ = format;
+    }
+
+    void SetLimits(uint64_t since,
+                   uint64_t count);
+
+    void SetDatabaseLookup(const DatabaseLookup& lookup);
+
+    void SetIncludeAllMetadata(bool include)
+    {
+      includeAllMetadata_ = include;
+    }
+
+    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 Execute(FindResponse& target,
+                 ServerIndex& index) const;
+
+    void Execute(IVisitor& visitor,
+                 ServerContext& context) const;
+
+    void Execute(Json::Value& target,
+                 ServerContext& context) const;
+
+    bool ExecuteOneResource(Json::Value& target,
+                            ServerContext& context) const;
+  };
+}
--- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Tue Jul 09 18:06:21 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 i = 0; i < constraint.GetValuesCount(); i++)
+          {
+            if (first)
+            {
+              first = false;
+            }
+            else
+            {
+              s += ", ";
+            }
+            s += constraint.GetValue(i);
+          }
+          s += "]";
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      s += "\n";
+    }
+
+    return s;
+  }
 }
--- a/OrthancServer/Sources/Search/DatabaseConstraint.h	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.h	Tue Jul 09 18:06:21 2024 +0200
@@ -178,5 +178,7 @@
     }
 
     const DatabaseConstraint& GetConstraint(size_t index) const;
+
+    std::string Format() const;
   };
 }
--- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Tue Jul 09 18:06:21 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:
--- a/OrthancServer/Sources/ServerContext.cpp	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Tue Jul 09 18:06:21 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"
@@ -1538,8 +1539,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;
@@ -1556,7 +1556,50 @@
       fastLookup->RemoveConstraint(DICOM_TAG_MODALITIES_IN_STUDY);
     }
 
+    if (true)
     {
+      /**
+       * EXPERIMENTAL VERSION
+       **/
+
+      ResourceFinder finder(queryLevel, false /* TODO-FIND: don't expand for now */);
+      finder.SetDatabaseLimits(databaseLimit);
+      finder.SetDatabaseLookup(lookup);
+      finder.SetLabels(labels);
+      finder.SetLabelsConstraint(labelsConstraint);
+
+      if (queryLevel != ResourceType_Instance)
+      {
+        finder.SetRetrieveOneInstanceIdentifier(true);
+      }
+
+      FindResponse response;
+      finder.Execute(response, GetIndex());
+
+      resources.resize(response.GetSize());
+      instances.resize(response.GetSize());
+
+      for (size_t i = 0; i < response.GetSize(); i++)
+      {
+        const FindResponse::Resource& resource = response.GetResourceByIndex(i);
+        resources[i] = resource.GetIdentifier();
+
+        if (queryLevel == ResourceType_Instance)
+        {
+          instances[i] = resource.GetIdentifier();
+        }
+        else
+        {
+          instances[i] = resource.GetOneInstanceIdentifier();
+        }
+      }
+    }
+    else
+    {
+      /**
+       * VERSION IN ORTHANC <= 1.12.4
+       **/
+
       const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);
       GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, labels, labelsConstraint, lookupLimit);
     }
@@ -2127,124 +2170,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())
       {
@@ -2252,38 +2308,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;
 
@@ -2294,6 +2354,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;
+    }
   }
 
 
@@ -2539,9 +2612,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;
     }
 
@@ -2694,5 +2767,4 @@
 
     return elapsed.total_seconds();
   }
-
 }
--- a/OrthancServer/Sources/ServerContext.h	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Tue Jul 09 18:06:21 2024 +0200
@@ -442,6 +442,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	Tue Jul 09 15:32:30 2024 +0200
+++ b/OrthancServer/Sources/ServerIndex.h	Tue Jul 09 18:06:21 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);
   };
 }