changeset 5605:3f24eb4013d8 find-refactoring

integration mainline->find-refactoring
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 08 May 2024 10:30:57 +0200
parents e4e7ca3d206e (diff) c2a2fb8e868d (current diff)
children 6e2dad336446
files NEWS OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h
diffstat 29 files changed, 3490 insertions(+), 175 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed May 08 10:30:34 2024 +0200
+++ b/NEWS	Wed May 08 10:30:57 2024 +0200
@@ -10,6 +10,8 @@
   /patients|studies|series/instances/../reconstruct to speed up the reconstruction
   in case you just want to update the MainDicomTags of that resource level only 
   e.g. after you have updated the 'ExtraMainDicomTags' for this level.
+* TODO-FIND: complete the list of updated routes:
+  /studies?expand and sibbling routes now also return "Metadata" (if the DB implements 'extended-api-v1')
 
 Plugins
 -------
--- a/OrthancFramework/Sources/SQLite/Connection.h	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/Connection.h	Wed May 08 10:30:57 2024 +0200
@@ -56,6 +56,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	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementId.cpp	Wed May 08 10:30:57 2024 +0200
@@ -56,12 +56,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	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementId.h	Wed May 08 10:30:57 2024 +0200
@@ -54,6 +54,7 @@
     private:
       const char* file_;
       int line_;
+      std::string statement_;
 
       StatementId(); // Forbidden
 
@@ -61,6 +62,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	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/CMakeLists.txt	Wed May 08 10:30:57 2024 +0200
@@ -89,11 +89,15 @@
 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/OrthancIdentifiers.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/ResourcesContent.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/SQLiteDatabaseWrapper.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Database/StatelessDatabaseOperations.cpp
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Wed May 08 10:30:57 2024 +0200
@@ -30,6 +30,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"
@@ -1275,6 +1276,35 @@
     {
       ListLabelsInternal(target, false, -1);
     }
+
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request,
+                             const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE
+    {
+      // TODO-FIND
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+
+    virtual void ExecuteFind(std::list<std::string>& identifiers,
+                             const FindRequest& request,
+                             const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE
+    {
+      // TODO-FIND
+      Compatibility::GenericFind find(*this);
+      find.ExecuteFind(identifiers, request, normalized);
+    }
+
+
+    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);
+    }
   };
 
 
@@ -1489,4 +1519,10 @@
       return dbCapabilities_;
     }
   }
+
+
+  bool OrthancPluginDatabaseV4::HasIntegratedFind() const
+  {
+    return false;  // TODO-FIND
+  }
 }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h	Wed May 08 10:30:57 2024 +0200
@@ -92,6 +92,8 @@
     virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE;
 
     virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE;
+
+    virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE;
   };
 }
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed May 08 10:30:57 2024 +0200
@@ -744,7 +744,7 @@
   } OrthancPluginResourceType;
 
 
-
+  
   /**
    * The supported types of changes that can be signaled to the change callback.
    * @ingroup Callbacks
--- a/OrthancServer/Resources/Orthanc.doxygen	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Resources/Orthanc.doxygen	Wed May 08 10:30:57 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	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp	Wed May 08 10:30:57 2024 +0200
@@ -23,6 +23,7 @@
 #include "BaseDatabaseWrapper.h"
 
 #include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "Compatibility/GenericFind.h"
 
 namespace Orthanc
 {
@@ -45,6 +46,32 @@
   }
 
 
+  void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(FindResponse& response,
+                                                         const FindRequest& request,
+                                                         const std::vector<DatabaseConstraint>& normalized)
+  {
+    throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+  }
+
+
+  void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(std::list<std::string>& identifiers,
+                                                         const FindRequest& request,
+                                                         const std::vector<DatabaseConstraint>& normalized)
+  {
+    Compatibility::GenericFind find(*this);
+    find.ExecuteFind(identifiers, request, normalized);
+  }
+
+
+  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	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h	Wed May 08 10:30:57 2024 +0200
@@ -46,8 +46,25 @@
                                           int64_t& instancesCount,
                                           int64_t& compressedSize,
                                           int64_t& uncompressedSize) ORTHANC_OVERRIDE;
+
+      virtual void ExecuteFind(FindResponse& response,
+                               const FindRequest& request,
+                               const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE;
+
+      virtual void ExecuteFind(std::list<std::string>& identifiers,
+                               const FindRequest& request,
+                               const std::vector<DatabaseConstraint>& normalized) 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	Wed May 08 10:30:57 2024 +0200
@@ -0,0 +1,329 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2024 Osimis S.A., 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
+  {
+    void GenericFind::ExecuteFind(std::list<std::string>& identifiers,
+                                  const FindRequest& request,
+                                  const std::vector<DatabaseConstraint>& normalized)
+    {
+      if (!request.GetOrthancIdentifiers().HasPatientId() &&
+          !request.GetOrthancIdentifiers().HasStudyId() &&
+          !request.GetOrthancIdentifiers().HasSeriesId() &&
+          !request.GetOrthancIdentifiers().HasInstanceId() &&
+          request.GetDicomTagConstraintsCount() == 0 &&
+          request.GetMetadataConstraintsCount() == 0 &&
+          request.GetLabels().empty() &&
+          request.GetOrdering().empty())
+      {
+        if (request.HasLimits())
+        {
+          transaction_.GetAllPublicIds(identifiers, request.GetLevel(), request.GetLimitsSince(), request.GetLimitsCount());
+        }
+        else
+        {
+          transaction_.GetAllPublicIds(identifiers, request.GetLevel());
+        }
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotImplemented);  // Not supported
+      }
+    }
+
+
+    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.IsRetrieveMainDicomTags(ResourceType_Patient) ||
+              request.IsRetrieveMetadata(ResourceType_Patient))
+          {
+            return ResourceType_Patient;
+          }
+          else
+          {
+            return ResourceType_Study;
+          }
+
+        case ResourceType_Series:
+          if (request.IsRetrieveMainDicomTags(ResourceType_Patient) ||
+              request.IsRetrieveMetadata(ResourceType_Patient))
+          {
+            return ResourceType_Patient;
+          }
+          else if (request.IsRetrieveMainDicomTags(ResourceType_Study) ||
+                   request.IsRetrieveMetadata(ResourceType_Study))
+          {
+            return ResourceType_Study;
+          }
+          else
+          {
+            return ResourceType_Series;
+          }
+
+        case ResourceType_Instance:
+          if (request.IsRetrieveMainDicomTags(ResourceType_Patient) ||
+              request.IsRetrieveMetadata(ResourceType_Patient))
+          {
+            return ResourceType_Patient;
+          }
+          else if (request.IsRetrieveMainDicomTags(ResourceType_Study) ||
+                   request.IsRetrieveMetadata(ResourceType_Study))
+          {
+            return ResourceType_Study;
+          }
+          else if (request.IsRetrieveMainDicomTags(ResourceType_Series) ||
+                   request.IsRetrieveMetadata(ResourceType_Series))
+          {
+            return ResourceType_Series;
+          }
+          else
+          {
+            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(), identifier));
+
+      if (request.IsRetrieveParentIdentifier())
+      {
+        assert(!parent.empty());
+        resource->SetParentIdentifier(parent);
+      }
+
+      {
+        int64_t currentId = internalId;
+        ResourceType currentLevel = level;
+        const ResourceType topLevel = GetTopLevelOfInterest(request);
+
+        for (;;)
+        {
+          if (request.IsRetrieveMainDicomTags(currentLevel))
+          {
+            RetrieveMainDicomTags(*resource, currentLevel, currentId);
+          }
+
+          if (request.IsRetrieveMetadata(currentLevel))
+          {
+            transaction_.GetAllMetadata(resource->GetMetadata(currentLevel), currentId);
+          }
+
+          if (currentLevel == topLevel)
+          {
+            break;
+          }
+          else
+          {
+            int64_t parentId;
+            if (transaction_.LookupParent(parentId, currentId))
+            {
+              currentId = parentId;
+              currentLevel = GetParentResourceType(currentLevel);
+            }
+            else
+            {
+              throw OrthancException(ErrorCode_DatabasePlugin);
+            }
+          }
+        }
+      }
+
+      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);
+          }
+        }
+      }
+
+      if (request.IsRetrieveChildrenIdentifiers())
+      {
+        std::list<std::string> children;
+        transaction_.GetChildrenPublicId(children, internalId);
+
+        for (std::list<std::string>::const_iterator it = children.begin(); it != children.end(); ++it)
+        {
+          resource->AddChildIdentifier(*it);
+        }
+      }
+
+      for (std::set<MetadataType>::const_iterator it = request.GetRetrieveChildrenMetadata().begin();
+           it != request.GetRetrieveChildrenMetadata().end(); ++it)
+      {
+        std::list<std::string> values;
+        transaction_.GetChildrenMetadata(values, internalId, *it);
+        resource->AddChildrenMetadata(*it, values);
+      }
+
+      if (!request.GetRetrieveAttachmentOfOneInstance().empty())
+      {
+        std::set<FileContentType> todo = request.GetRetrieveAttachmentOfOneInstance();
+        std::stack< std::pair<ResourceType, int64_t> > candidates;
+        candidates.push(std::make_pair(level, internalId));
+
+        while (!todo.empty() &&
+               !candidates.empty())
+        {
+          std::pair<ResourceType, int64_t> top = candidates.top();
+          candidates.pop();
+
+          if (top.first == ResourceType_Instance)
+          {
+            std::set<FileContentType> nextTodo;
+
+            for (std::set<FileContentType>::const_iterator it = todo.begin(); it != todo.end(); ++it)
+            {
+              FileInfo attachment;
+              int64_t revision;
+              if (transaction_.LookupAttachment(attachment, revision, top.second, *it))
+              {
+                resource->AddAttachmentOfOneInstance(attachment);
+              }
+              else
+              {
+                nextTodo.insert(*it);
+              }
+            }
+
+            todo = nextTodo;
+          }
+          else
+          {
+            std::list<int64_t> children;
+            transaction_.GetChildrenInternalId(children, top.second);
+            for (std::list<int64_t>::const_iterator it = children.begin(); it != children.end(); ++it)
+            {
+              candidates.push(std::make_pair(GetChildResourceType(top.first), *it));
+            }
+          }
+        }
+      }
+
+      response.Add(resource.release());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.h	Wed May 08 10:30:57 2024 +0200
@@ -0,0 +1,55 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2024 Osimis S.A., 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:
+      GenericFind(IDatabaseWrapper::ITransaction& transaction) :
+        transaction_(transaction)
+      {
+      }
+
+      void ExecuteFind(std::list<std::string>& identifiers,
+                       const FindRequest& request,
+                       const std::vector<DatabaseConstraint>& normalized);
+
+      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	Wed May 08 10:30:57 2024 +0200
@@ -0,0 +1,305 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2024 Osimis S.A., 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 <cassert>
+
+namespace Orthanc
+{
+  FindRequest::FindRequest(ResourceType level) :
+    level_(level),
+    hasLimits_(false),
+    limitsSince_(0),
+    limitsCount_(0),
+    retrieveMainDicomTagsPatients_(false),
+    retrieveMainDicomTagsStudies_(false),
+    retrieveMainDicomTagsSeries_(false),
+    retrieveMainDicomTagsInstances_(false),
+    retrieveMetadataPatients_(false),
+    retrieveMetadataStudies_(false),
+    retrieveMetadataSeries_(false),
+    retrieveMetadataInstances_(false),
+    retrieveLabels_(false),
+    retrieveAttachments_(false),
+    retrieveParentIdentifier_(false),
+    retrieveChildrenIdentifiers_(false)
+  {
+  }
+
+
+  FindRequest::~FindRequest()
+  {
+
+    for (std::deque<Ordering*>::iterator it = ordering_.begin(); it != ordering_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
+  }
+
+  void FindRequest::AddDicomTagConstraint(const DicomTagConstraint& constraint)
+  {
+    dicomTagConstraints_.push_back(constraint);
+  }
+
+  const DicomTagConstraint& FindRequest::GetDicomTagConstraint(size_t index) const
+  {
+    if (index >= dicomTagConstraints_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return dicomTagConstraints_[index];
+    }
+  }
+
+
+  void FindRequest::SetLimits(uint64_t since,
+                              uint64_t count)
+  {
+    if (hasLimits_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      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::SetRetrieveMainDicomTags(ResourceType level,
+                                             bool retrieve)
+  {
+    if (!IsResourceLevelAboveOrEqual(level, level_))
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        retrieveMainDicomTagsPatients_ = retrieve;
+        break;
+
+      case ResourceType_Study:
+        retrieveMainDicomTagsStudies_ = retrieve;
+        break;
+
+      case ResourceType_Series:
+        retrieveMainDicomTagsSeries_ = retrieve;
+        break;
+
+      case ResourceType_Instance:
+        retrieveMainDicomTagsInstances_ = retrieve;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  bool FindRequest::IsRetrieveMainDicomTags(ResourceType level) const
+  {
+    if (!IsResourceLevelAboveOrEqual(level, level_))
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return retrieveMainDicomTagsPatients_;
+
+      case ResourceType_Study:
+        return retrieveMainDicomTagsStudies_;
+
+      case ResourceType_Series:
+        return retrieveMainDicomTagsSeries_;
+
+      case ResourceType_Instance:
+        return retrieveMainDicomTagsInstances_;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  void FindRequest::SetRetrieveMetadata(ResourceType level,
+                                        bool retrieve)
+  {
+    if (!IsResourceLevelAboveOrEqual(level, level_))
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        retrieveMetadataPatients_ = retrieve;
+        break;
+
+      case ResourceType_Study:
+        retrieveMetadataStudies_ = retrieve;
+        break;
+
+      case ResourceType_Series:
+        retrieveMetadataSeries_ = retrieve;
+        break;
+
+      case ResourceType_Instance:
+        retrieveMetadataInstances_ = retrieve;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  bool FindRequest::IsRetrieveMetadata(ResourceType level) const
+  {
+    if (!IsResourceLevelAboveOrEqual(level, level_))
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return retrieveMetadataPatients_;
+
+      case ResourceType_Study:
+        return retrieveMetadataStudies_;
+
+      case ResourceType_Series:
+        return retrieveMetadataSeries_;
+
+      case ResourceType_Instance:
+        return retrieveMetadataInstances_;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  void FindRequest::SetRetrieveParentIdentifier(bool retrieve)
+  {
+    if (level_ == ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      retrieveParentIdentifier_ = retrieve;
+    }
+  }
+
+
+  void FindRequest::SetRetrieveChildrenIdentifiers(bool retrieve)
+  {
+    if (level_ == ResourceType_Instance)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      retrieveChildrenIdentifiers_ = retrieve;
+    }
+  }
+
+
+  void FindRequest::AddRetrieveChildrenMetadata(MetadataType metadata)
+  {
+    if (IsRetrieveChildrenMetadata(metadata))
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      retrieveChildrenMetadata_.insert(metadata);
+    }
+  }
+
+
+  void FindRequest::AddRetrieveAttachmentOfOneInstance(FileContentType type)
+  {
+    if (retrieveAttachmentOfOneInstance_.find(type) == retrieveAttachmentOfOneInstance_.end())
+    {
+      retrieveAttachmentOfOneInstance_.insert(type);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindRequest.h	Wed May 08 10:30:57 2024 +0200
@@ -0,0 +1,332 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2024 Osimis S.A., 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 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();
+      }
+    };
+
+
+  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
+    std::vector<DicomTagConstraint>      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                     labelsContraint_;
+    std::deque<Ordering*>                ordering_;             // The ordering criteria (note: the order is important !)
+
+    bool                                 retrieveMainDicomTagsPatients_;
+    bool                                 retrieveMainDicomTagsStudies_;
+    bool                                 retrieveMainDicomTagsSeries_;
+    bool                                 retrieveMainDicomTagsInstances_;
+    bool                                 retrieveMetadataPatients_;
+    bool                                 retrieveMetadataStudies_;
+    bool                                 retrieveMetadataSeries_;
+    bool                                 retrieveMetadataInstances_;
+    bool                                 retrieveLabels_;
+    bool                                 retrieveAttachments_;
+    bool                                 retrieveParentIdentifier_;
+    bool                                 retrieveChildrenIdentifiers_;
+    std::set<MetadataType>               retrieveChildrenMetadata_;
+    std::set<FileContentType>            retrieveAttachmentOfOneInstance_;
+
+  public:
+    explicit FindRequest(ResourceType level);
+
+    ~FindRequest();
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void SetOrthancPatientId(const std::string& id)
+    {
+      orthancIdentifiers_.SetPatientId(id);
+    }
+
+    void SetOrthancStudyId(const std::string& id)
+    {
+      orthancIdentifiers_.SetStudyId(id);
+    }
+
+    void SetOrthancSeriesId(const std::string& id)
+    {
+      orthancIdentifiers_.SetSeriesId(id);
+    }
+
+    void SetOrthancInstanceId(const std::string& id)
+    {
+      orthancIdentifiers_.SetInstanceId(id);
+    }
+
+    const OrthancIdentifiers& GetOrthancIdentifiers() const
+    {
+      return orthancIdentifiers_;
+    }
+
+    void AddDicomTagConstraint(const DicomTagConstraint& constraint);
+
+    size_t GetDicomTagConstraintsCount() const
+    {
+      return dicomTagConstraints_.size();
+    }
+
+    size_t GetMetadataConstraintsCount() const
+    {
+      return metadataConstraints_.size();
+    }
+
+    const DicomTagConstraint& GetDicomTagConstraint(size_t index) const;
+
+    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 AddLabel(const std::string& label)
+    {
+      labels_.insert(label);
+    }
+
+    const std::set<std::string>& GetLabels() const
+    {
+      return labels_;
+    }
+
+    LabelsConstraint GetLabelsConstraint() const
+    {
+      return labelsContraint_;
+    }
+
+    void SetRetrieveMainDicomTags(ResourceType level,
+                                  bool retrieve);
+
+    bool IsRetrieveMainDicomTags(ResourceType level) const;
+
+    void SetRetrieveMetadata(ResourceType level,
+                             bool retrieve);
+
+    bool IsRetrieveMetadata(ResourceType level) const;
+
+    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_;
+    }
+
+    void SetRetrieveChildrenIdentifiers(bool retrieve);
+
+    bool IsRetrieveChildrenIdentifiers() const
+    {
+      return retrieveChildrenIdentifiers_;
+    }
+
+    void AddRetrieveChildrenMetadata(MetadataType metadata);
+
+    bool IsRetrieveChildrenMetadata(MetadataType metadata) const
+    {
+      return retrieveChildrenMetadata_.find(metadata) != retrieveChildrenMetadata_.end();
+    }
+
+    const std::set<MetadataType>& GetRetrieveChildrenMetadata() const
+    {
+      return retrieveChildrenMetadata_;
+    }
+
+    void AddRetrieveAttachmentOfOneInstance(FileContentType type);
+
+    const std::set<FileContentType>& GetRetrieveAttachmentOfOneInstance() const
+    {
+      return retrieveAttachmentOfOneInstance_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindResponse.cpp	Wed May 08 10:30:57 2024 +0200
@@ -0,0 +1,673 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2024 Osimis S.A., 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);
+      }
+    }
+  }
+
+
+  void FindResponse::Resource::AddChildIdentifier(const std::string& identifier)
+  {
+    if (childrenIdentifiers_.find(identifier) == childrenIdentifiers_.end())
+    {
+      childrenIdentifiers_.insert(identifier);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  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::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;
+    }
+  }
+
+
+  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);
+    }
+  }
+
+
+  FindResponse::Resource::~Resource()
+  {
+    for (ChildrenMetadata::iterator it = childrenMetadata_.begin(); it != childrenMetadata_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  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));
+    }
+  }
+
+
+  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;
+    }
+  }
+
+
+  void FindResponse::Resource::AddChildrenMetadata(MetadataType metadata,
+                                                   const std::list<std::string>& values)
+  {
+    if (childrenMetadata_.find(metadata) == childrenMetadata_.end())
+    {
+      childrenMetadata_[metadata] = new std::list<std::string>(values);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  bool FindResponse::Resource::LookupChildrenMetadata(std::list<std::string>& values,
+                                                      MetadataType metadata) const
+  {
+    ChildrenMetadata::const_iterator found = childrenMetadata_.find(metadata);
+    if (found == childrenMetadata_.end())
+    {
+      return false;
+    }
+    else
+    {
+      assert(found->second != NULL);
+      values = *found->second;
+      return true;
+    }
+  }
+
+
+  void FindResponse::Resource::AddAttachmentOfOneInstance(const FileInfo& info)
+  {
+    if (attachmentOfOneInstance_.find(info.GetContentType()) == attachmentOfOneInstance_.end())
+    {
+      attachmentOfOneInstance_[info.GetContentType()] = info;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  bool FindResponse::Resource::LookupAttachmentOfOneInstance(FileInfo& target,
+                                                             FileContentType type) const
+  {
+    std::map<FileContentType, FileInfo>::const_iterator found = attachmentOfOneInstance_.find(type);
+
+    if (found == attachmentOfOneInstance_.end())
+    {
+      return false;
+    }
+    else
+    {
+      target = found->second;
+      return true;
+    }
+  }
+
+
+  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;
+  }
+
+  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(ResourceType_Patient))
+    {
+      DicomMap m;
+      GetMainDicomTags(m, ResourceType_Patient);
+      DebugDicomMap(target["Patient"]["MainDicomTags"], m);
+    }
+
+    if (request.IsRetrieveMetadata(ResourceType_Patient))
+    {
+      DebugMetadata(target["Patient"]["Metadata"], GetMetadata(ResourceType_Patient));
+    }
+
+    if (request.GetLevel() != ResourceType_Patient)
+    {
+      if (request.IsRetrieveMainDicomTags(ResourceType_Study))
+      {
+        DicomMap m;
+        GetMainDicomTags(m, ResourceType_Study);
+        DebugDicomMap(target["Study"]["MainDicomTags"], m);
+      }
+
+      if (request.IsRetrieveMetadata(ResourceType_Study))
+      {
+        DebugMetadata(target["Study"]["Metadata"], GetMetadata(ResourceType_Study));
+      }
+    }
+
+    if (request.GetLevel() != ResourceType_Patient &&
+        request.GetLevel() != ResourceType_Study)
+    {
+      if (request.IsRetrieveMainDicomTags(ResourceType_Series))
+      {
+        DicomMap m;
+        GetMainDicomTags(m, ResourceType_Series);
+        DebugDicomMap(target["Series"]["MainDicomTags"], m);
+      }
+
+      if (request.IsRetrieveMetadata(ResourceType_Series))
+      {
+        DebugMetadata(target["Series"]["Metadata"], GetMetadata(ResourceType_Series));
+      }
+    }
+
+    if (request.GetLevel() != ResourceType_Patient &&
+        request.GetLevel() != ResourceType_Study &&
+        request.GetLevel() != ResourceType_Series)
+    {
+      if (request.IsRetrieveMainDicomTags(ResourceType_Instance))
+      {
+        DicomMap m;
+        GetMainDicomTags(m, ResourceType_Instance);
+        DebugDicomMap(target["Instance"]["MainDicomTags"], m);
+      }
+
+      if (request.IsRetrieveMetadata(ResourceType_Instance))
+      {
+        DebugMetadata(target["Instance"]["Metadata"], GetMetadata(ResourceType_Instance));
+      }
+    }
+
+    if (request.IsRetrieveChildrenIdentifiers())
+    {
+      Json::Value v = Json::arrayValue;
+      for (std::set<std::string>::const_iterator it = childrenIdentifiers_.begin();
+           it != childrenIdentifiers_.end(); ++it)
+      {
+        v.append(*it);
+      }
+      target["Children"] = v;
+    }
+
+    if (request.IsRetrieveLabels())
+    {
+      Json::Value v = Json::arrayValue;
+      for (std::set<std::string>::const_iterator it = labels_.begin();
+           it != labels_.end(); ++it)
+      {
+        v.append(*it);
+      }
+      target["Labels"] = v;
+    }
+
+    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;
+    }
+
+    for (std::set<MetadataType>::const_iterator it = request.GetRetrieveChildrenMetadata().begin();
+         it != request.GetRetrieveChildrenMetadata().end(); ++it)
+    {
+      std::list<std::string> l;
+      if (LookupChildrenMetadata(l, *it))
+      {
+        Json::Value v = Json::arrayValue;
+        for (std::list<std::string>::const_iterator it2 = l.begin(); it2 != l.end(); ++it2)
+        {
+          v.append(*it2);
+        }
+        target["ChildrenMetadata"][EnumerationToString(*it)] = v;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+
+    for (std::set<FileContentType>::const_iterator it = request.GetRetrieveAttachmentOfOneInstance().begin();
+         it != request.GetRetrieveAttachmentOfOneInstance().end(); ++it)
+    {
+      FileInfo info;
+      if (LookupAttachmentOfOneInstance(info, *it))
+      {
+        if (info.GetContentType() == *it)
+        {
+          DebugAddAttachment(target["AttachmentOfOneInstance"], info);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+  }
+
+
+  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::GetResource(size_t index) const
+  {
+    if (index >= items_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(items_[index] != NULL);
+      return *items_[index];
+    }
+  }
+
+
+  FindResponse::Resource& FindResponse::GetResource(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	Wed May 08 10:30:57 2024 +0200
@@ -0,0 +1,235 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2024 Osimis S.A., 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;
+    };
+
+
+  public:
+    class Resource : public boost::noncopyable
+    {
+    private:
+      typedef std::map<MetadataType, std::list<std::string>*>  ChildrenMetadata;
+
+      ResourceType                          level_;
+      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_;
+      std::set<std::string>                 childrenIdentifiers_;
+      std::set<std::string>                 labels_;      
+      std::map<FileContentType, FileInfo>   attachments_;
+      ChildrenMetadata                      childrenMetadata_;
+      std::map<FileContentType, FileInfo>   attachmentOfOneInstance_;
+
+      MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level);
+
+      const MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level) const
+      {
+        return const_cast<Resource&>(*this).GetMainDicomTagsAtLevel(level);
+      }
+
+    public:
+      Resource(ResourceType level,
+               const std::string& identifier) :
+        level_(level),
+        identifier_(identifier)
+      {
+      }
+
+      ~Resource();
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      const std::string& GetIdentifier() const
+      {
+        return identifier_;
+      }
+
+      const std::string& GetParentIdentifier() const;
+
+      void SetParentIdentifier(const std::string& id);
+
+      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 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(const std::string& childId);
+
+      const std::set<std::string>& GetChildrenIdentifiers() const
+      {
+        return childrenIdentifiers_;
+      }
+
+      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_;
+      }
+
+      void AddChildrenMetadata(MetadataType metadata,
+                               const std::list<std::string>& values);
+
+      bool LookupChildrenMetadata(std::list<std::string>& values,
+                                  MetadataType metadata) const;
+
+      void AddAttachmentOfOneInstance(const FileInfo& info);
+
+      bool LookupAttachmentOfOneInstance(FileInfo& target,
+                                         FileContentType type) const;
+
+      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& GetResource(size_t index) const;
+
+    Resource& GetResource(const std::string& id);
+
+    const Resource& GetResource(const std::string& id) const
+    {
+      return const_cast<FindResponse&>(*this).GetResource(id);
+    }
+
+    bool HasResource(const std::string& id) const
+    {
+      return (index_.find(id) != index_.end());
+    }
+  };
+}
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Wed May 08 10:30:57 2024 +0200
@@ -28,6 +28,8 @@
 #include "../ExportedResource.h"
 #include "../Search/ISqlLookupFormatter.h"
 #include "../ServerIndexChange.h"
+#include "FindRequest.h"
+#include "FindResponse.h"
 #include "IDatabaseListener.h"
 
 #include <list>
@@ -350,6 +352,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 std::vector<DatabaseConstraint>& normalized) = 0;
+
+      // This is only implemented if "HasIntegratedFind()" is "false"
+      virtual void ExecuteFind(std::list<std::string>& identifiers,
+                               const FindRequest& request,
+                               const std::vector<DatabaseConstraint>& normalized) = 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;
     };
 
 
@@ -374,5 +403,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/OrthancIdentifiers.cpp	Wed May 08 10:30:57 2024 +0200
@@ -0,0 +1,242 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2024 Osimis S.A., 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	Wed May 08 10:30:57 2024 +0200
@@ -0,0 +1,92 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2024 Osimis S.A., 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	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Wed May 08 10:30:57 2024 +0200
@@ -28,6 +28,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"
@@ -1137,6 +1138,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	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Wed May 08 10:30:57 2024 +0200
@@ -501,6 +501,38 @@
     }
   }
 
+  void StatelessDatabaseOperations::NormalizeLookup(std::vector<DatabaseConstraint>& target,
+                                                    const FindRequest& findRequest) const
+  {
+    assert(mainDicomTagsRegistry_.get() != NULL);
+
+    target.clear();
+    target.reserve(findRequest.GetDicomTagConstraintsCount());
+
+    for (size_t i = 0; i < findRequest.GetDicomTagConstraintsCount(); i++)
+    {
+      ResourceType level;
+      DicomTagType type;
+      
+      mainDicomTagsRegistry_->LookupTag(level, type, findRequest.GetDicomTagConstraint(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 &&
+            findRequest.GetLevel() != ResourceType_Patient)
+        {
+          level = ResourceType_Study;
+        }
+        
+        target.push_back(findRequest.GetDicomTagConstraint(i).ConvertToDatabaseConstraint(level, type));
+      }
+    }
+
+    // TODO-FIND: add metadata constraints
+  }
+
 
   class StatelessDatabaseOperations::Transaction : public boost::noncopyable
   {
@@ -905,7 +937,7 @@
               Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags);
 
               while ((target.missingRequestedTags_.size() > 0)
-                    && currentLevel != ResourceType_Patient)
+                     && currentLevel != ResourceType_Patient)
               {
                 currentLevel = GetParentResourceType(currentLevel);
 
@@ -2516,7 +2548,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;
             }
 
@@ -2815,8 +2847,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_));
@@ -2941,7 +2973,7 @@
   }                                                                           
 
   bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount,
-                                                                                   const std::string& patientId)
+                                                                                    const std::string& patientId)
   {
     if (maximumPatientCount != 0)
     {
@@ -3043,7 +3075,7 @@
     };
 
     if (maximumStorageMode == MaxStorageMode_Recycle 
-      && (maximumStorageSize != 0 || maximumPatientCount != 0))
+        && (maximumStorageSize != 0 || maximumPatientCount != 0))
     {
       Operations operations(maximumStorageSize, maximumPatientCount);
       Apply(operations);
@@ -3107,9 +3139,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);
@@ -3302,7 +3334,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();
         }
@@ -3331,7 +3363,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_)
           {
@@ -3348,7 +3380,7 @@
 
           // Attach the user-specified metadata (in case of reconstruction, metadata_ contains all past metadata, including the system ones we want to keep)
           for (MetadataMap::const_iterator 
-                  it = metadata_.begin(); it != metadata_.end(); ++it)
+                 it = metadata_.begin(); it != metadata_.end(); ++it)
           {
             switch (it->first.first)
             {
@@ -3807,4 +3839,172 @@
     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 std::vector<DatabaseConstraint>&>
+    {
+    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 std::vector<DatabaseConstraint>&>
+    {
+    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>());
+      }
+    };
+
+    std::vector<DatabaseConstraint> normalized;
+    NormalizeLookup(normalized, request);
+
+    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, normalized);
+    }
+    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, normalized);
+
+      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);
+      }
+    }
+  }
+
+  // TODO-FIND: we reuse the ExpandedResource class to reuse Serialization code from ExpandedResource
+  // But, finally, we might just get rid of ExpandedResource and replace it by FindResponse
+  ExpandedResource::ExpandedResource(const FindRequest& request,
+                                     const FindResponse::Resource& resource) :
+    id_(resource.GetIdentifier()),
+    level_(request.GetLevel()),
+    isStable_(false),
+    expectedNumberOfInstances_(0),
+    fileSize_(0),
+    indexInSeries_(0)
+  {
+    if (request.GetLevel() != resource.GetLevel())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (request.IsRetrieveMainDicomTags(request.GetLevel()))
+    {
+      resource.GetMainDicomTags(tags_, request.GetLevel());
+    }
+
+    if (request.IsRetrieveChildrenIdentifiers())
+    {
+      const std::set<std::string>& children = resource.GetChildrenIdentifiers();
+      for (std::set<std::string>::const_iterator it = children.begin(); it != children.end(); ++it)
+      {
+        childrenIds_.push_back(*it);
+      }
+    }
+
+    if (request.IsRetrieveParentIdentifier())
+    {
+      parentId_ = resource.GetParentIdentifier();
+    }
+
+    if (request.IsRetrieveMetadata(request.GetLevel()))
+    {
+      metadata_ = resource.GetMetadata(request.GetLevel());
+      std::string value;
+      if (resource.LookupMetadata(value, request.GetLevel(), MetadataType_MainDicomTagsSignature))
+      {
+        mainDicomTagsSignature_ = value;
+      }
+      if (resource.LookupMetadata(value, request.GetLevel(), MetadataType_AnonymizedFrom))
+      {
+        anonymizedFrom_ = value;
+      }
+      if (resource.LookupMetadata(value, request.GetLevel(), MetadataType_ModifiedFrom))
+      {
+        modifiedFrom_ = value;
+      }
+      if (resource.LookupMetadata(value, request.GetLevel(), MetadataType_LastUpdate))
+      {
+        lastUpdate_ = value;
+      }
+      if (request.GetLevel() == ResourceType_Series)
+      {
+        if (resource.LookupMetadata(value, request.GetLevel(), MetadataType_Series_ExpectedNumberOfInstances))
+        {
+          expectedNumberOfInstances_ = boost::lexical_cast<int>(value);
+        }
+      }
+      if (request.GetLevel() == ResourceType_Instance)
+      {
+        if (resource.LookupMetadata(value, request.GetLevel(), MetadataType_Instance_IndexInSeries))
+        {
+          indexInSeries_ = boost::lexical_cast<int>(value);
+        }
+      }
+    }
+
+    if (request.IsRetrieveLabels())
+    {
+      labels_ = resource.GetLabels();
+    }
+
+    if (request.IsRetrieveAttachments())
+    {
+      FileInfo attachment;
+      if (resource.LookupAttachment(attachment, FileContentType_Dicom))
+      {
+        fileSize_ = attachment.GetUncompressedSize();
+        fileUuid_ = attachment.GetUuid();
+      }
+    }
+
+    //if (request.IsRetrieveChildrenMetadata())
+    {
+      // TODO-FIND: the status_ is normally obtained from transaction.GetSeriesStatus(internalId, i)
+      // but, that's an heavy operation for something that is rarely used -> we should have dedicated SQL code 
+      // to compute it AFAP and we should compute it only if the user request it !
+    }
+
+    // TODO-FIND: continue: isStable_, status_
+  }
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Wed May 08 10:30:57 2024 +0200
@@ -80,6 +80,9 @@
     {
     }
     
+    ExpandedResource(const FindRequest& request,
+                     const FindResponse::Resource& resource);
+
     void SetResource(ResourceType level,
                      const std::string& id)
     {
@@ -111,15 +114,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
@@ -377,6 +391,27 @@
       {
         transaction_.ListAllLabels(target);
       }
+
+      void ExecuteFind(FindResponse& response,
+                       const FindRequest& request, 
+                       const std::vector<DatabaseConstraint>& normalized)
+      {
+        transaction_.ExecuteFind(response, request, normalized);
+      }
+
+      void ExecuteFind(std::list<std::string>& identifiers,
+                       const FindRequest& request,
+                       const std::vector<DatabaseConstraint>& normalized)
+      {
+        transaction_.ExecuteFind(identifiers, request, normalized);
+      }
+
+      void ExecuteExpand(FindResponse& response,
+                         const FindRequest& request,
+                         const std::string& identifier)
+      {
+        transaction_.ExecuteExpand(response, request, identifier);
+      }
     };
 
 
@@ -559,6 +594,9 @@
                          const DatabaseLookup& source,
                          ResourceType level) const;
 
+    void NormalizeLookup(std::vector<DatabaseConstraint>& target,
+                         const FindRequest& findRequest) const;
+
     void ApplyInternal(IReadOnlyOperations* readOperations,
                        IReadWriteOperations* writeOperations);
 
@@ -801,5 +839,8 @@
                    const std::set<std::string>& labels);
 
     bool HasLabelsSupport();
+
+    void ExecuteFind(FindResponse& response,
+                     const FindRequest& request);
   };
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed May 08 10:30:57 2024 +0200
@@ -127,7 +127,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.
@@ -181,7 +181,7 @@
   }
 
 
-  static void AnswerListOfResources(RestApiOutput& output,
+  static void AnswerListOfResources2(RestApiOutput& output,
                                     ServerContext& context,
                                     const std::list<std::string>& resources,
                                     ResourceType level,
@@ -194,10 +194,375 @@
     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);
   }
 
 
+  class ResourceFinder : public boost::noncopyable
+  {
+  private:
+    FindRequest        request_;
+    bool               expand_;
+    std::set<DicomTag> requestedTags_;
+    DicomToJsonFormat  format_;
+    bool               includeAllMetadata_;   // Same as: ExpandResourceFlags_IncludeAllMetadata
+
+    SeriesStatus GetSeriesStatus(uint32_t& expectedNumberOfInstances,
+                                 const FindResponse::Resource& resource) const
+    {
+      if (request_.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::list<std::string> values;
+      if (!resource.LookupChildrenMetadata(values, MetadataType_Instance_IndexInSeries))
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      std::set<int64_t> instances;
+
+      for (std::list<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 Expand(Json::Value& target,
+                const FindResponse::Resource& resource) const
+    {
+      /**
+
+         TODO-FIND:
+
+         - Metadata / Series / ExpectedNumberOfInstances
+
+         - Metadata / Series / Status
+
+         - Metadata / Instance / FileSize
+
+         - Metadata / Instance / FileUuid
+
+         - Metadata / Instance / IndexInSeries
+
+         - Metadata / AnonymizedFrom
+
+         - Metadata / ModifiedFrom
+
+      **/
+
+      /**
+       * This method closely follows "SerializeExpandedResource()" in
+       * "ServerContext.cpp" from Orthanc 1.12.3.
+       **/
+
+      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();
+
+        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);
+
+          if (status == SeriesStatus_Unknown)
+          {
+            target["ExpectedNumberOfInstances"] = Json::nullValue;
+          }
+          else
+          {
+            target["ExpectedNumberOfInstances"] = 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);
+          }
+
+          std::string s;
+          uint32_t index;
+          if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_IndexInSeries) &&
+              SerializationToolbox::ParseUnsignedInteger32(index, s))
+          {
+            target["IndexInSeries"] = index;
+          }
+          else
+          {
+            target["IndexInSeries"] = 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)
+      {
+        // TODO-FIND: Stable
+
+        /*
+          if (resource.IsStable())
+        {
+          target["IsStable"] = true;
+        }
+        */
+
+        if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_LastUpdate))
+        {
+          target["LastUpdate"] = s;
+        }
+      }
+
+      {
+        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_)
+      {
+        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;
+      }
+    }
+
+
+  public:
+    ResourceFinder(ResourceType level,
+                   bool expand) :
+      request_(level),
+      expand_(expand),
+      format_(DicomToJsonFormat_Human),
+      includeAllMetadata_(false)
+    {
+      if (expand)
+      {
+        request_.SetRetrieveMainDicomTags(level, true);
+        request_.SetRetrieveMetadata(level, true);
+        request_.SetRetrieveLabels(true);
+
+        if (level == ResourceType_Series)
+        {
+          request_.AddRetrieveChildrenMetadata(MetadataType_Instance_IndexInSeries); // required for the SeriesStatus
+        }
+
+        if (level == ResourceType_Instance)
+        {
+          request_.SetRetrieveAttachments(true); // for FileSize & FileUuid
+        }
+        else
+        {
+          request_.SetRetrieveChildrenIdentifiers(true);
+        }
+
+        if (level != ResourceType_Patient)
+        {
+          request_.SetRetrieveParentIdentifier(true);
+        }
+      }
+    }
+
+    void SetRequestedTags(const std::set<DicomTag>& tags)
+    {
+      requestedTags_ = tags;
+    }
+
+    void SetFormat(DicomToJsonFormat format)
+    {
+      format_ = format;
+    }
+
+    void SetLimits(uint64_t since,
+                   uint64_t count)
+    {
+      request_.SetLimits(since, count);
+    }
+
+    void SetIncludeAllMetadata(bool include)
+    {
+      includeAllMetadata_ = include;
+    }
+
+    void Execute(Json::Value& target,
+                 ServerContext& context)
+    {
+      FindResponse response;
+      context.GetIndex().ExecuteFind(response, request_);
+
+      target = Json::arrayValue;
+
+      if (expand_)
+      {
+        for (size_t i = 0; i < response.GetSize(); i++)
+        {
+          Json::Value item;
+          Expand(item, response.GetResource(i));
+
+#if 0
+          target.append(item);
+#else
+          context.AppendFindResponse(target, request_, response.GetResource(i), format_,
+                                     requestedTags_, true /* allowStorageAccess */);
+          std::cout << "+++ Expected: " << target[target.size() - 1].toStyledString();
+          std::cout << "--- Actual: " << item.toStyledString();
+#endif
+        }
+      }
+      else
+      {
+        for (size_t i = 0; i < response.GetSize(); i++)
+        {
+          target.append(response.GetResource(i).GetIdentifier());
+        }
+      }
+    }
+  };
+
+
   template <enum ResourceType resourceType>
   static void ListResources(RestApiGetCall& call)
   {
@@ -224,41 +589,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.SetRequestedTags(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.3
+       **/
+
+      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 */);
   }
 
 
@@ -3107,7 +3523,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_));
       }
     };
   }
@@ -3387,7 +3803,7 @@
       a.splice(a.begin(), b);
     }
 
-    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
+    AnswerListOfResources2(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 */);
--- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Wed May 08 10:30:57 2024 +0200
@@ -152,6 +152,7 @@
                                          const std::vector<std::string>& values,
                                          bool caseSensitive,
                                          bool mandatory) :
+    keyType_(DatabaseConstraint::KeyType_DicomTag),
     level_(level),
     tag_(tag),
     isIdentifier_(isIdentifier),
--- a/OrthancServer/Sources/Search/DatabaseConstraint.h	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.h	Wed May 08 10:30:57 2024 +0200
@@ -79,9 +79,18 @@
   // This class is also used by the "orthanc-databases" project
   class DatabaseConstraint
   {
+  public:
+    enum KeyType  // used for ordering and filters
+    {
+      KeyType_DicomTag,
+      KeyType_Metadata
+    };
+
   private:
+    KeyType                   keyType_;
     ResourceType              level_;
     DicomTag                  tag_;
+    uint32_t                  metadataType_;  // TODO: implement
     bool                      isIdentifier_;
     ConstraintType            constraintType_;
     std::vector<std::string>  values_;
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Wed May 08 10:30:57 2024 +0200
@@ -34,7 +34,7 @@
 namespace Orthanc
 {
   class DatabaseConstraint;
-  
+
   enum LabelsConstraint
   {
     LabelsConstraint_All,
--- a/OrthancServer/Sources/ServerContext.cpp	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Wed May 08 10:30:57 2024 +0200
@@ -2125,124 +2125,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())
       {
@@ -2250,38 +2263,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;
 
@@ -2292,6 +2309,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;
+    }
   }
 
 
@@ -2537,9 +2567,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;
     }
 
@@ -2687,4 +2717,42 @@
     return elapsed.total_seconds();
   }
 
+  void ServerContext::AppendFindResponse(Json::Value& target,
+                                         const FindRequest& request,
+                                         const FindResponse::Resource& item,
+                                         DicomToJsonFormat format,
+                                         const std::set<DicomTag>& requestedTags,
+                                         bool allowStorageAccess)
+  {
+    // convert to ExpandedResource to re-use the serialization code TODO-FIND: check if this is the right way to do.  shouldn't we copy the code and finally get rid of ExpandedResource ? 
+    ExpandedResource resource(request, item);
+
+    ExpandResourceFlags expandFlags = ExpandResourceFlags_None;
+    if (request.IsRetrieveChildrenIdentifiers())
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeChildren);
+    }
+    if (request.IsRetrieveMetadata(request.GetLevel()))
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeAllMetadata | ExpandResourceFlags_IncludeMetadata );
+    }
+    if (request.IsRetrieveMainDicomTags(request.GetLevel()))
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeMainDicomTags);
+    }
+    if (true /* request.HasResponseContent(FindRequest::ResponseContent_IsStable) */)  // TODO-FIND: Is this correct?
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeIsStable);
+    }
+    if (request.IsRetrieveLabels())
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeLabels);
+    }
+
+    Json::Value jsonItem;
+    SerializeExpandedResource(jsonItem, resource, format, requestedTags, expandFlags);
+    target.append(jsonItem);
+  }
+
+
 }
--- a/OrthancServer/Sources/ServerContext.h	Wed May 08 10:30:34 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Wed May 08 10:30:57 2024 +0200
@@ -607,6 +607,13 @@
                         ExpandResourceFlags expandFlags,
                         bool allowStorageAccess);
 
+    void AppendFindResponse(Json::Value& target,
+                            const FindRequest& request,
+                            const FindResponse::Resource& resource,
+                            DicomToJsonFormat format,
+                            const std::set<DicomTag>& requestedTags,
+                            bool allowStorageAccess);
+
     FindStorageAccessMode GetFindStorageAccessMode() const
     {
       return findStorageAccessMode_;