changeset 5577:9e74e761b108 find-refactoring

integration mainline->find-refactoring
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 26 Apr 2024 17:43:22 +0200
parents 5a13483d12c5 (diff) 3a6d6d35193c (current diff)
children 77570cce8855
files NEWS OrthancServer/CMakeLists.txt OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h
diffstat 28 files changed, 2441 insertions(+), 167 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Apr 26 17:41:40 2024 +0200
+++ b/NEWS	Fri Apr 26 17:43:22 2024 +0200
@@ -26,6 +26,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	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/Connection.h	Fri Apr 26 17:43:22 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	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementId.cpp	Fri Apr 26 17:43:22 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	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancFramework/Sources/SQLite/StatementId.h	Fri Apr 26 17:43:22 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	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/CMakeLists.txt	Fri Apr 26 17:43:22 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/OrthancPluginDatabase.cpp	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Fri Apr 26 17:43:22 2024 +0200
@@ -30,6 +30,7 @@
 
 #include "../../../OrthancFramework/Sources/Logging.h"
 #include "../../../OrthancFramework/Sources/OrthancException.h"
+#include "../../Sources/Database/Compatibility/GenericFind.h"
 #include "../../Sources/Database/Compatibility/ICreateInstance.h"
 #include "../../Sources/Database/Compatibility/IGetChildrenMetadata.h"
 #include "../../Sources/Database/Compatibility/ILookupResourceAndParent.h"
@@ -1447,6 +1448,15 @@
     {
       throw OrthancException(ErrorCode_InternalError);  // Not supported
     }
+
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request, 
+                             const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE
+    {
+      Compatibility::GenericFind find(*this);
+      find.Execute(response, request);
+    }
   };
 
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Fri Apr 26 17:43:22 2024 +0200
@@ -29,6 +29,7 @@
 
 #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 "PluginsEnumerations.h"
@@ -1060,6 +1061,15 @@
     {
       throw OrthancException(ErrorCode_InternalError);  // Not supported
     }
+
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request, 
+                             const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE
+    {
+      Compatibility::GenericFind find(*this);
+      find.Execute(response, request);
+    }
   };
 
   
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Fri Apr 26 17:43:22 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,15 @@
     {
       ListLabelsInternal(target, false, -1);
     }
+
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request, 
+                             const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE
+    {
+      Compatibility::GenericFind find(*this);
+      find.Execute(response, request);
+    }
   };
 
 
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Fri Apr 26 17:43:22 2024 +0200
@@ -744,7 +744,7 @@
   } OrthancPluginResourceType;
 
 
-
+  
   /**
    * The supported types of changes that can be signaled to the change callback.
    * @ingroup Callbacks
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp	Fri Apr 26 17:43:22 2024 +0200
@@ -0,0 +1,99 @@
+/**
+ * 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/OrthancException.h"
+
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    void GenericFind::Execute(FindResponse& response,
+                              const FindRequest& request)
+    {
+      if (request.GetResponseContent() == FindRequest::ResponseContent_IdentifiersOnly &&
+          !request.GetOrthancIdentifiers().HasPatientId() &&
+          !request.GetOrthancIdentifiers().HasStudyId() &&
+          !request.GetOrthancIdentifiers().HasSeriesId() &&
+          !request.GetOrthancIdentifiers().HasInstanceId() &&
+          request.GetDicomTagConstraintsCount() == 0 &&
+          request.GetMetadataConstraintsCount() == 0 &&
+          !request.IsRetrieveTagsAtLevel(ResourceType_Patient) &&
+          !request.IsRetrieveTagsAtLevel(ResourceType_Study) &&
+          !request.IsRetrieveTagsAtLevel(ResourceType_Series) &&
+          !request.IsRetrieveTagsAtLevel(ResourceType_Instance) &&
+          request.GetOrdering().empty() &&
+          request.GetLabels().empty())
+      {
+        std::list<std::string> ids;
+
+        if (request.HasLimits())
+        {
+          transaction_.GetAllPublicIds(ids, request.GetLevel(), request.GetLimitsSince(), request.GetLimitsCount());
+        }
+        else
+        {
+          transaction_.GetAllPublicIds(ids, request.GetLevel());
+        }
+
+        for (std::list<std::string>::const_iterator it = ids.begin(); it != ids.end(); ++it)
+        {
+          OrthancIdentifiers identifiers;
+          identifiers.SetLevel(request.GetLevel(), *it);
+
+          response.Add(new FindResponse::Item(request.GetResponseContent(),
+                                              request.GetLevel(), 
+                                              identifiers));
+        }
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+
+
+      /**
+       * Sanity checks
+       **/
+
+      for (size_t i = 0; i < response.GetSize(); i++)
+      {
+        const FindResponse::Item& item = response.GetItem(i);
+
+        if (item.GetLevel() != request.GetLevel())
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        if (request.HasResponseContent(FindRequest::ResponseContent_MainDicomTags)
+            && !item.HasDicomMap())
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        // TODO: other sanity checks
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.h	Fri Apr 26 17:43:22 2024 +0200
@@ -0,0 +1,47 @@
+/**
+ * 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
+  {
+    // TODO-FIND: remove this class that only contains a temporary implementation
+    class GenericFind : public boost::noncopyable
+    {
+    private:
+      IDatabaseWrapper::ITransaction&  transaction_;
+
+    public:
+      GenericFind(IDatabaseWrapper::ITransaction& transaction) :
+        transaction_(transaction)
+      {
+      }
+
+      void Execute(FindResponse& response,
+                   const FindRequest& request);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindRequest.cpp	Fri Apr 26 17:43:22 2024 +0200
@@ -0,0 +1,210 @@
+/**
+ * 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
+{
+  bool FindRequest::IsCompatibleLevel(ResourceType levelOfInterest) const
+  {
+    switch (level_)
+    {
+      case ResourceType_Patient:
+        return (levelOfInterest == ResourceType_Patient);
+
+      case ResourceType_Study:
+        return (levelOfInterest == ResourceType_Patient ||
+                levelOfInterest == ResourceType_Study);
+
+      case ResourceType_Series:
+        return (levelOfInterest == ResourceType_Patient ||
+                levelOfInterest == ResourceType_Study ||
+                levelOfInterest == ResourceType_Series);
+
+      case ResourceType_Instance:
+        return (levelOfInterest == ResourceType_Patient ||
+                levelOfInterest == ResourceType_Study ||
+                levelOfInterest == ResourceType_Series ||
+                levelOfInterest == ResourceType_Instance);
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  FindRequest::FindRequest(ResourceType level) :
+    level_(level),
+    hasLimits_(false),
+    limitsSince_(0),
+    limitsCount_(0),
+    responseContent_(ResponseContent_IdentifiersOnly),
+    retrievePatientTags_(false),
+    retrieveStudyTags_(false),
+    retrieveSeriesTags_(false),
+    retrieveInstanceTags_(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::SetRetrieveTagsAtLevel(ResourceType levelOfInterest,
+                                           bool retrieve)
+  {
+    if (!IsCompatibleLevel(levelOfInterest))
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (levelOfInterest)
+    {
+      case ResourceType_Patient:
+        retrievePatientTags_ = true;
+        break;
+
+      case ResourceType_Study:
+        retrieveStudyTags_ = true;
+        break;
+
+      case ResourceType_Series:
+        retrieveSeriesTags_ = true;
+        break;
+
+      case ResourceType_Instance:
+        retrieveInstanceTags_ = true;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool FindRequest::IsRetrieveTagsAtLevel(ResourceType levelOfInterest) const
+  {
+    switch (levelOfInterest)
+    {
+      case ResourceType_Patient:
+        return retrievePatientTags_;
+
+      case ResourceType_Study:
+        return retrieveStudyTags_;
+
+      case ResourceType_Series:
+        return retrieveSeriesTags_;
+
+      case ResourceType_Instance:
+        return retrieveInstanceTags_;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  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));
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindRequest.h	Fri Apr 26 17:43:22 2024 +0200
@@ -0,0 +1,474 @@
+/**
+ * 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 "../ServerEnumerations.h"
+#include "OrthancIdentifiers.h"
+#include "../Search/DicomTagConstraint.h"
+#include "../Search/LabelsConstraint.h"
+#include "../Search/DatabaseConstraint.h"
+
+#include <deque>
+#include <map>
+#include <set>
+#include <cassert>
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class FindRequest : public boost::noncopyable
+  {
+  public:
+    enum ResponseContent
+    {
+      ResponseContent_MainDicomTags         = (1 << 0),     // retrieve all tags from MainDicomTags and DicomIdentifiers
+      ResponseContent_Metadata              = (1 << 1),     // retrieve all metadata, their values and revision
+      ResponseContent_Labels                = (1 << 2),     // get all labels
+      ResponseContent_Attachments           = (1 << 3),     // retrieve all attachments, their values and revision
+      ResponseContent_Parent                = (1 << 4),     // get the id of the parent
+      ResponseContent_Children              = (1 << 5),     // retrieve the list of children ids
+      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
+      ResponseContent_ChildrenMetadata      = (1 << 7),     // That is actually required to compute the series status but could be usefull for other stuffs.
+      ResponseContent_IsStable              = (1 << 8),     // This is currently not saved in DB but it could be in the future.
+
+      ResponseContent_IdentifiersOnly       = 0,
+      ResponseContent_INTERNAL              = 0x7FFFFFFF
+    };
+
+    enum ConstraintType
+    {
+      ConstraintType_Mandatory,
+      ConstraintType_Equality,
+      ConstraintType_Range,
+      ConstraintType_Wildcard,
+      ConstraintType_List
+    };
+
+    enum KeyType  // used for ordering and filters
+    {
+      KeyType_DicomTag,
+      KeyType_Metadata
+    };
+
+    enum OrderingDirection
+    {
+      OrderingDirection_Ascending,
+      OrderingDirection_Descending
+    };
+
+
+    class Key
+    {
+      KeyType                       type_;
+      boost::shared_ptr<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:
+      Key(const DicomTag& dicomTag) :
+        type_(KeyType_DicomTag),
+        dicomTag_(new DicomTag(dicomTag)),
+        metadata_(MetadataType_EndUser)
+      {
+      }
+
+      Key(MetadataType metadata) :
+        type_(KeyType_Metadata),
+        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
+    {
+      OrderingDirection   direction_;
+      Key                 key_;
+
+    public:
+      Ordering(const Key& key,
+               OrderingDirection direction) :
+        direction_(direction),
+        key_(key)
+      {
+      }
+
+    public:
+      KeyType GetKeyType() const
+      {
+        return key_.GetType();
+      }
+
+      OrderingDirection GetDirection() const
+      {
+        return direction_;
+      }
+
+      MetadataType GetMetadataType() const
+      {
+        return key_.GetMetadataType();
+      }
+
+      DicomTag GetDicomTag() const
+      {
+        return key_.GetDicomTag();
+      }
+    };
+
+    // TODO-FIND: this class hierarchy actually adds complexity and is very redundant with DicomTagConstraint.
+    //       e.g, in this class hierarchy, it is difficult to implement an equivalent to DicomTagConstraint::ConvertToDatabaseConstraint
+    //       I have the feeling we can just have a MetadataConstraint in the same way as DicomTagConstraint
+    //       and both convert to a DatabaseConstraint in StatelessDatabaseOperations
+    // class FilterConstraint : public boost::noncopyable
+    // {
+    //   Key              key_;
+    
+    // protected:
+    //   FilterConstraint(const Key& key) :
+    //     key_(key)
+    //   {
+    //   }
+
+    // public:
+    //   virtual ~FilterConstraint()
+    //   {
+    //   }
+
+    //   const Key& GetKey() const
+    //   {
+    //     return key_;
+    //   }
+
+    //   virtual ConstraintType GetType() const = 0;
+    //   virtual bool IsCaseSensitive() const = 0;  // Needed for PN VR
+
+
+    // };
+
+
+    // class MandatoryConstraint : public FilterConstraint
+    // {
+    // public:
+    //   virtual ConstraintType GetType() const ORTHANC_OVERRIDE
+    //   {
+    //     return ConstraintType_Mandatory;
+    //   }
+    // };
+
+
+    // class StringConstraint : public FilterConstraint
+    // {
+    // private:
+    //   bool  caseSensitive_;
+
+    // public:
+    //   StringConstraint(Key key,
+    //                    bool caseSensitive) :
+    //     FilterConstraint(key),
+    //     caseSensitive_(caseSensitive)
+    //   {
+    //   }
+
+    //   bool IsCaseSensitive() const
+    //   {
+    //     return caseSensitive_;
+    //   }
+    // };
+
+
+    // class EqualityConstraint : public StringConstraint
+    // {
+    // private:
+    //   std::string  value_;
+
+    // public:
+    //   explicit EqualityConstraint(Key key,
+    //                               bool caseSensitive,
+    //                               const std::string& value) :
+    //     StringConstraint(key, caseSensitive),
+    //     value_(value)
+    //   {
+    //   }
+
+    //   virtual ConstraintType GetType() const ORTHANC_OVERRIDE
+    //   {
+    //     return ConstraintType_Equality;
+    //   }
+
+    //   const std::string& GetValue() const
+    //   {
+    //     return value_;
+    //   }
+    // };
+
+
+    // class RangeConstraint : public StringConstraint
+    // {
+    // private:
+    //   std::string  start_;
+    //   std::string  end_;    // Inclusive
+
+    // public:
+    //   RangeConstraint(Key key,
+    //                   bool caseSensitive,
+    //                   const std::string& start,
+    //                   const std::string& end) :
+    //     StringConstraint(key, caseSensitive),
+    //     start_(start),
+    //     end_(end)
+    //   {
+    //   }
+
+    //   virtual ConstraintType GetType() const ORTHANC_OVERRIDE
+    //   {
+    //     return ConstraintType_Range;
+    //   }
+
+    //   const std::string& GetStart() const
+    //   {
+    //     return start_;
+    //   }
+
+    //   const std::string& GetEnd() const
+    //   {
+    //     return end_;
+    //   }
+    // };
+
+
+    // class WildcardConstraint : public StringConstraint
+    // {
+    // private:
+    //   std::string  value_;
+
+    // public:
+    //   explicit WildcardConstraint(Key& key,
+    //                               bool caseSensitive,
+    //                               const std::string& value) :
+    //     StringConstraint(key, caseSensitive),
+    //     value_(value)
+    //   {
+    //   }
+
+    //   virtual ConstraintType GetType() const ORTHANC_OVERRIDE
+    //   {
+    //     return ConstraintType_Wildcard;
+    //   }
+
+    //   const std::string& GetValue() const
+    //   {
+    //     return value_;
+    //   }
+    // };
+
+
+    // class ListConstraint : public StringConstraint
+    // {
+    // private:
+    //   std::set<std::string>  values_;
+
+    // public:
+    //   ListConstraint(Key key,
+    //                  bool caseSensitive) :
+    //     StringConstraint(key, caseSensitive)
+    //   {
+    //   }
+
+    //   virtual ConstraintType GetType() const ORTHANC_OVERRIDE
+    //   {
+    //     return ConstraintType_List;
+    //   }
+
+    //   const std::set<std::string>& GetValues() const
+    //   {
+    //     return values_;
+    //   }
+    // };
+
+
+  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::deque<FilterConstraint*>        filterConstraints_;    // All tags and metadata filters (note: the order is not important)
+    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 !)
+
+    // response fields
+    ResponseContent                      responseContent_;
+    
+    // TODO: check if these 4 options are required.  We might just have a retrieveParentTags that could be part of the ResponseContent enum ?
+    bool                                 retrievePatientTags_;
+    bool                                 retrieveStudyTags_;
+    bool                                 retrieveSeriesTags_;
+    bool                                 retrieveInstanceTags_;
+
+    bool IsCompatibleLevel(ResourceType levelOfInterest) const;
+
+  public:
+    FindRequest(ResourceType level);
+
+    ~FindRequest();
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+
+    void SetResponseContent(ResponseContent content)
+    {
+      responseContent_ = content;
+    }
+
+    void AddResponseContent(ResponseContent content)
+    {
+      responseContent_ = static_cast<ResponseContent>(static_cast<uint32_t>(responseContent_) | content);
+    }
+
+    ResponseContent GetResponseContent() const
+    {
+      return responseContent_;
+    }
+
+    bool HasResponseContent(ResponseContent content) const
+    {
+      return (responseContent_ & content) == content;
+    }
+
+    bool IsResponseIdentifiersOnly() const
+    {
+      return responseContent_ == ResponseContent_IdentifiersOnly;
+    }
+
+    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 SetRetrieveTagsAtLevel(ResourceType levelOfInterest,
+                                bool retrieve);
+
+    bool IsRetrieveTagsAtLevel(ResourceType levelOfInterest) 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_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindResponse.cpp	Fri Apr 26 17:43:22 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 "FindResponse.h"
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h"
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+#include <cassert>
+
+
+namespace Orthanc
+{
+  static void ExtractOrthancIdentifiers(OrthancIdentifiers& identifiers,
+                                        ResourceType level,
+                                        const DicomMap& dicom)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+      {
+        std::string patientId;
+        if (!dicom.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false))
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        else
+        {
+          DicomInstanceHasher hasher(patientId, "", "", "");
+          identifiers.SetPatientId(hasher.HashPatient());
+        }
+        break;
+      }
+
+      case ResourceType_Study:
+      {
+        std::string patientId, studyInstanceUid;
+        if (!dicom.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) ||
+            !dicom.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false))
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        else
+        {
+          DicomInstanceHasher hasher(patientId, studyInstanceUid, "", "");
+          identifiers.SetPatientId(hasher.HashPatient());
+          identifiers.SetStudyId(hasher.HashStudy());
+        }
+        break;
+      }
+
+      case ResourceType_Series:
+      {
+        std::string patientId, studyInstanceUid, seriesInstanceUid;
+        if (!dicom.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) ||
+            !dicom.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) ||
+            !dicom.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false))
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        else
+        {
+          DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, "");
+          identifiers.SetPatientId(hasher.HashPatient());
+          identifiers.SetStudyId(hasher.HashStudy());
+          identifiers.SetSeriesId(hasher.HashSeries());
+        }
+        break;
+      }
+
+      case ResourceType_Instance:
+      {
+        std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid;
+        if (!dicom.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) ||
+            !dicom.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) ||
+            !dicom.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false) ||
+            !dicom.LookupStringValue(sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false))
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        else
+        {
+          DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
+          identifiers.SetPatientId(hasher.HashPatient());
+          identifiers.SetStudyId(hasher.HashStudy());
+          identifiers.SetSeriesId(hasher.HashSeries());
+          identifiers.SetInstanceId(hasher.HashInstance());
+        }
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  FindResponse::Item::Item(FindRequest::ResponseContent responseContent,
+                           ResourceType level,
+                           DicomMap* dicomMap /* takes ownership */) :
+    responseContent_(responseContent),
+    level_(level),
+    dicomMap_(dicomMap)
+  {
+    if (dicomMap == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      ExtractOrthancIdentifiers(identifiers_, level, *dicomMap);
+    }
+  }
+
+
+  void FindResponse::Item::AddMetadata(MetadataType metadata,
+                                       const std::string& value)
+  {
+    if (metadata_.find(metadata) != metadata_.end())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);  // Metadata already present
+    }
+    else
+    {
+      metadata_[metadata] = value;
+    }
+  }
+
+
+  bool FindResponse::Item::LookupMetadata(std::string& value,
+                                          MetadataType metadata) const
+  {
+    std::map<MetadataType, std::string>::const_iterator found = metadata_.find(metadata);
+
+    if (found == metadata_.end())
+    {
+      return false;
+    }
+    else
+    {
+      value = found->second;
+      return true;
+    }
+  }
+
+
+  void FindResponse::Item::ListMetadata(std::set<MetadataType>& target) const
+  {
+    target.clear();
+
+    for (std::map<MetadataType, std::string>::const_iterator it = metadata_.begin(); it != metadata_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
+
+
+  const DicomMap& FindResponse::Item::GetDicomMap() const
+  {
+    if (dicomMap_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *dicomMap_;
+    }
+  }
+
+
+  FindResponse::~FindResponse()
+  {
+    for (size_t i = 0; i < items_.size(); i++)
+    {
+      assert(items_[i] != NULL);
+      delete items_[i];
+    }
+  }
+
+
+  void FindResponse::Add(Item* item /* takes ownership */)
+  {
+    if (item == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      items_.push_back(item);
+    }
+  }
+
+
+  const FindResponse::Item& FindResponse::GetItem(size_t index) const
+  {
+    if (index >= items_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(items_[index] != NULL);
+      return *items_[index];
+    }
+  }
+
+  void FindResponse::Item::AddDicomTag(uint16_t group, uint16_t element, const std::string& value, bool isBinary)
+  {
+    if (dicomMap_.get() == NULL)
+    {
+      dicomMap_.reset(new DicomMap());
+    }
+
+    dicomMap_->SetValue(group, element, value, isBinary);
+  }
+
+  void FindResponse::Item::AddChild(const std::string& childId)
+  {
+    children_.push_back(childId);
+  }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindResponse.h	Fri Apr 26 17:43:22 2024 +0200
@@ -0,0 +1,240 @@
+/**
+ * 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
+  {
+  public:
+
+    // TODO-FIND: does it actually make sense to retrieve revisions for metadata and attachments ?
+    class StringWithRevision
+    {
+    private:
+      std::string         value_;
+      int64_t             revision_;
+    public:
+      StringWithRevision(const std::string& value,
+                          int64_t revision) :
+        value_(value),
+        revision_(revision)
+      {
+      }
+
+      StringWithRevision(const StringWithRevision& other) :
+        value_(other.value_),
+        revision_(other.revision_)
+      {
+      }
+
+      StringWithRevision() :
+        revision_(-1)
+      {
+      }
+
+      const std::string& GetValue() const
+      {
+        return value_;
+      }
+
+      int64_t GetRevision() const
+      {
+        return revision_;
+      }
+    };
+
+
+    class Item : public boost::noncopyable
+    {
+    private:
+      FindRequest::ResponseContent          responseContent_;    // what has been requested
+      ResourceType                          level_;
+      std::string                           resourceId_;
+      std::string                           parent_;
+      OrthancIdentifiers                    identifiers_;  // TODO-FIND: not convenient to use here.  A simple resourceId seems enough
+      std::unique_ptr<DicomMap>             dicomMap_;
+      std::list<std::string>                children_;
+      std::string                           childInstanceId_;
+      std::set<std::string>                 labels_;      
+      std::map<MetadataType, std::string>   metadata_;
+      std::map<FileContentType, FileInfo>   attachments_;
+
+    public:
+      Item(FindRequest::ResponseContent responseContent,
+           ResourceType level,
+           const OrthancIdentifiers& identifiers) :
+        responseContent_(responseContent),
+        level_(level),
+        identifiers_(identifiers)
+      {
+      }
+
+      Item(FindRequest::ResponseContent responseContent,
+           ResourceType level,
+           const std::string& resourceId) :
+        responseContent_(responseContent),
+        level_(level),
+        resourceId_(resourceId)
+      {
+      }
+
+      Item(FindRequest::ResponseContent responseContent,
+           ResourceType level,
+           DicomMap* dicomMap /* takes ownership */);
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      const std::string& GetResourceId() const
+      {
+        return resourceId_;
+      }
+
+      const OrthancIdentifiers& GetIdentifiers() const
+      {
+        return identifiers_;
+      }
+
+      FindRequest::ResponseContent GetResponseContent() const
+      {
+        return responseContent_;
+      }
+
+      bool HasResponseContent(FindRequest::ResponseContent content) const
+      {
+        return (responseContent_ & content) == content;
+      }
+
+      void AddDicomTag(uint16_t group, uint16_t element, const std::string& value, bool isBinary);
+
+      void AddMetadata(MetadataType metadata,
+                       const std::string& value);
+                       //int64_t revision);
+
+      const std::map<MetadataType, std::string>& GetMetadata() const
+      {
+        return metadata_;
+      }
+
+      bool HasMetadata(MetadataType metadata) const
+      {
+        return metadata_.find(metadata) != metadata_.end();
+      }
+
+      bool LookupMetadata(std::string& value, /* int64_t revision, */
+                          MetadataType metadata) const;
+
+      void ListMetadata(std::set<MetadataType>& metadata) const;
+
+      bool HasDicomMap() const
+      {
+        return dicomMap_.get() != NULL;
+      }
+
+      const DicomMap& GetDicomMap() const;
+
+      void AddChild(const std::string& childId);
+
+      const std::list<std::string>& GetChildren() const
+      {
+        return children_;
+      }
+
+      void SetParent(const std::string& parent)
+      {
+        parent_ = parent;
+      }
+
+      const std::string& GetParent() const
+      {
+        return parent_;
+      }
+
+      void AddLabel(const std::string& label)
+      {
+        labels_.insert(label);
+      }
+
+      const std::set<std::string>& GetLabels() const
+      {
+        return labels_;
+      }
+
+      void AddAttachment(const FileInfo& attachment)
+      {
+        attachments_[attachment.GetContentType()] = attachment;
+      }
+
+      const std::map<FileContentType, FileInfo>& GetAttachments() const
+      {
+        return attachments_;
+      }
+
+      bool 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;
+        }
+
+        return false;
+      }
+
+      // TODO-FIND: add other getters and setters
+    };
+
+  private:
+    std::deque<Item*>  items_;
+
+  public:
+    ~FindResponse();
+
+    void Add(Item* item /* takes ownership */);
+
+    size_t GetSize() const
+    {
+      return items_.size();
+    }
+
+    const Item& GetItem(size_t index) const;
+  };
+}
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Fri Apr 26 17:43:22 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,14 @@
                                           int64_t& instancesCount,
                                           int64_t& compressedSize,
                                           int64_t& uncompressedSize) = 0;
+
+      /**
+       * Primitives introduced in Orthanc 1.12.4
+       **/
+
+      virtual void ExecuteFind(FindResponse& response,
+                               const FindRequest& request,
+                               const std::vector<DatabaseConstraint>& normalized) = 0;
     };
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/OrthancIdentifiers.cpp	Fri Apr 26 17:43:22 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	Fri Apr 26 17:43:22 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	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Fri Apr 26 17:43:22 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,182 @@
         target.insert(s.ColumnString(0));
       }
     }
+
+
+    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
+
+        if (request.IsResponseIdentifiersOnly())
+        {
+          SQLite::Statement statement(db_, SQLITE_FROM_HERE_DYNAMIC(sqlLookup), sqlLookup);
+          formatter.Bind(statement);
+
+          while (statement.Step())
+          {
+            FindResponse::Item* item = new FindResponse::Item(request.GetResponseContent(),
+                                                              request.GetLevel(),
+                                                              statement.ColumnString(0));
+            response.Add(item);
+          }
+        }
+        else
+        {
+          std::map<std::string, FindResponse::Item*> items;  // cache to the response items
+
+          {// 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);
+              FindResponse::Item* item = new FindResponse::Item(request.GetResponseContent(),
+                                                                request.GetLevel(),
+                                                                resourceId);
+              items[resourceId] = item;
+              response.Add(item);
+            }
+          }
+
+          // request Each response content through INNER JOIN with the temporary table
+          if (request.HasResponseContent(FindRequest::ResponseContent_MainDicomTags))
+          {
+            // 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);
+              items[resourceId]->AddDicomTag(statement.ColumnInt(1),
+                                             statement.ColumnInt(2),
+                                             statement.ColumnString(3), false);
+            }
+          }
+
+          if (request.HasResponseContent(FindRequest::ResponseContent_Children))
+          {
+            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);
+              items[resourceId]->AddChild(statement.ColumnString(1));
+            }
+          }
+
+          if (request.HasResponseContent(FindRequest::ResponseContent_Parent))
+          {
+            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);
+              items[resourceId]->SetParent(statement.ColumnString(1));
+            }
+          }
+
+          if (request.HasResponseContent(FindRequest::ResponseContent_Metadata))
+          {
+            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);
+              items[resourceId]->AddMetadata(static_cast<MetadataType>(statement.ColumnInt(1)),
+                                             statement.ColumnString(2));
+            }
+          }
+
+          if (request.HasResponseContent(FindRequest::ResponseContent_Labels))
+          {
+            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);
+              items[resourceId]->AddLabel(statement.ColumnString(1));
+            }
+          }
+
+          if (request.HasResponseContent(FindRequest::ResponseContent_Attachments))
+          {
+            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));
+              items[resourceId]->AddAttachment(attachment);
+            };
+          }
+
+          // TODO-FIND: implement other responseContent: ResponseContent_ChildInstanceId, ResponseContent_ChildrenMetadata (later: ResponseContent_IsStable)
+
+        }
+      }
+
+#endif
+    }
   };
 
 
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Apr 26 17:43:22 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
   {
@@ -3807,4 +3839,111 @@
     boost::shared_lock<boost::shared_mutex> lock(mutex_);
     return db_.GetDatabaseCapabilities().HasLabelsSupport();
   }
+
+
+  void StatelessDatabaseOperations::ExecuteFind(FindResponse& response,
+                                                const FindRequest& request)
+  {
+    class Operations : 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>());
+      }
+    };
+
+    std::vector<DatabaseConstraint> normalized;
+    NormalizeLookup(normalized, request);
+
+    Operations operations;
+    operations.Apply(*this, response, request, normalized);
+  }
+
+  // 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 FindResponse::Item& item) :
+    id_(item.GetResourceId()),
+    level_(item.GetLevel()),
+    isStable_(false),
+    expectedNumberOfInstances_(0),
+    fileSize_(0),
+    indexInSeries_(0)
+  {
+    if (item.HasResponseContent(FindRequest::ResponseContent_MainDicomTags))
+    {
+      tags_.Assign(item.GetDicomMap());
+    }
+
+    if (item.HasResponseContent(FindRequest::ResponseContent_Children))
+    {
+      childrenIds_ = item.GetChildren();
+    }
+
+    if (item.HasResponseContent(FindRequest::ResponseContent_Parent))
+    {
+      parentId_ = item.GetParent();
+    }
+
+    if (item.HasResponseContent(FindRequest::ResponseContent_Metadata))
+    {
+      metadata_ = item.GetMetadata();
+      std::string value;
+      if (item.LookupMetadata(value, MetadataType_MainDicomTagsSignature))
+      {
+        mainDicomTagsSignature_ = value;
+      }
+      if (item.LookupMetadata(value, MetadataType_AnonymizedFrom))
+      {
+        anonymizedFrom_ = value;
+      }
+      if (item.LookupMetadata(value, MetadataType_ModifiedFrom))
+      {
+        modifiedFrom_ = value;
+      }
+      if (item.LookupMetadata(value, MetadataType_LastUpdate))
+      {
+        lastUpdate_ = value;
+      }
+      if (item.GetLevel() == ResourceType_Series)
+      {
+        if (item.LookupMetadata(value, MetadataType_Series_ExpectedNumberOfInstances))
+        {
+          expectedNumberOfInstances_ = boost::lexical_cast<int>(value);
+        }
+      }
+      if (item.GetLevel() == ResourceType_Instance)
+      {
+        if (item.LookupMetadata(value, MetadataType_Instance_IndexInSeries))
+        {
+          indexInSeries_ = boost::lexical_cast<int>(value);
+        }
+      }
+    }
+
+    if (item.HasResponseContent(FindRequest::ResponseContent_Labels))
+    {
+      labels_ = item.GetLabels();
+    }
+
+    if (item.HasResponseContent(FindRequest::ResponseContent_Attachments))
+    {
+      FileInfo attachment;
+      if (item.LookupAttachment(attachment, FileContentType_Dicom))
+      {
+        fileSize_ = attachment.GetUncompressedSize();
+        fileUuid_ = attachment.GetUuid();
+      }
+    }
+
+    if (item.HasResponseContent(FindRequest::ResponseContent_ChildrenMetadata))
+    {
+      // 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	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri Apr 26 17:43:22 2024 +0200
@@ -80,6 +80,8 @@
     {
     }
     
+    ExpandedResource(const FindResponse::Item& item);
+
     void SetResource(ResourceType level,
                      const std::string& id)
     {
@@ -111,15 +113,26 @@
   enum ExpandResourceFlags
   {
     ExpandResourceFlags_None                    = 0,
+    // used to fetch from DB and for output
     ExpandResourceFlags_IncludeMetadata         = (1 << 0),
     ExpandResourceFlags_IncludeChildren         = (1 << 1),
     ExpandResourceFlags_IncludeMainDicomTags    = (1 << 2),
     ExpandResourceFlags_IncludeLabels           = (1 << 3),
 
-    ExpandResourceFlags_Default = (ExpandResourceFlags_IncludeMetadata |
-                                     ExpandResourceFlags_IncludeChildren |
-                                     ExpandResourceFlags_IncludeMainDicomTags |
-                                     ExpandResourceFlags_IncludeLabels)
+    // only used for output
+    ExpandResourceFlags_IncludeAllMetadata      = (1 << 4),  // new in Orthanc 1.12.4
+    ExpandResourceFlags_IncludeIsStable         = (1 << 5),  // new in Orthanc 1.12.4
+
+    ExpandResourceFlags_DefaultExtract = (ExpandResourceFlags_IncludeMetadata |
+                                          ExpandResourceFlags_IncludeChildren |
+                                          ExpandResourceFlags_IncludeMainDicomTags |
+                                          ExpandResourceFlags_IncludeLabels),
+
+    ExpandResourceFlags_DefaultOutput = (ExpandResourceFlags_IncludeMetadata |
+                                         ExpandResourceFlags_IncludeChildren |
+                                         ExpandResourceFlags_IncludeMainDicomTags |
+                                         ExpandResourceFlags_IncludeLabels |
+                                         ExpandResourceFlags_IncludeIsStable)
   };
 
   class StatelessDatabaseOperations : public boost::noncopyable
@@ -376,6 +389,13 @@
       {
         transaction_.ListAllLabels(target);
       }
+
+      void ExecuteFind(FindResponse& response,
+                       const FindRequest& request, 
+                       const std::vector<DatabaseConstraint>& normalized)
+      {
+        transaction_.ExecuteFind(response, request, normalized);
+      }
     };
 
 
@@ -558,6 +578,9 @@
                          const DatabaseLookup& source,
                          ResourceType level) const;
 
+    void NormalizeLookup(std::vector<DatabaseConstraint>& target,
+                         const FindRequest& findRequest) const;
+
     void ApplyInternal(IReadOnlyOperations* readOperations,
                        IReadWriteOperations* writeOperations);
 
@@ -800,5 +823,8 @@
                    const std::set<std::string>& labels);
 
     bool HasLabelsSupport();
+
+    void ExecuteFind(FindResponse& response,
+                     const FindRequest& request);
   };
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Apr 26 17:43:22 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,7 +194,7 @@
     std::map<std::string, boost::shared_ptr<DicomMap> > unusedResourcesMainDicomTags;
     std::map<std::string, boost::shared_ptr<Json::Value> > unusedResourcesDicomAsJson;
 
-    AnswerListOfResources(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess);
+    AnswerListOfResources1(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess);
   }
 
 
@@ -224,41 +224,153 @@
     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);
+
+      const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
+
+      FindRequest request(resourceType);
+
+      if (expand)
       {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "Missing \"limit\" argument for GET request against: " +
-                               call.FlattenUri());
+        // compatibility with default expand option
+        FindRequest::ResponseContent responseContent = static_cast<FindRequest::ResponseContent>(FindRequest::ResponseContent_MainDicomTags |
+                                   FindRequest::ResponseContent_Metadata |
+                                   FindRequest::ResponseContent_Labels);
+        
+        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);
+        }
+        if (resourceType == ResourceType_Series)
+        {
+          responseContent = static_cast<FindRequest::ResponseContent>(responseContent | FindRequest::ResponseContent_ChildrenMetadata); // required for the SeriesStatus
+        }
+        if (resourceType != ResourceType_Instance)
+        {
+          responseContent = static_cast<FindRequest::ResponseContent>(responseContent | FindRequest::ResponseContent_Children);
+        }
+        if (resourceType == ResourceType_Instance)
+        {
+          responseContent = static_cast<FindRequest::ResponseContent>(responseContent | FindRequest::ResponseContent_Attachments); // for FileSize & FileUuid
+        }
+        if (resourceType != ResourceType_Patient)
+        {
+          responseContent = static_cast<FindRequest::ResponseContent>(responseContent | FindRequest::ResponseContent_Parent);
+        }
+
+        request.SetResponseContent(responseContent);
+        request.SetRetrieveTagsAtLevel(resourceType, true);
+
+        if (resourceType == ResourceType_Study)
+        {
+          request.SetRetrieveTagsAtLevel(ResourceType_Patient, true);
+        }
       }
-
-      if (!call.HasArgument("since"))
+      else
+      {
+        request.SetResponseContent(FindRequest::ResponseContent_IdentifiersOnly);
+      }
+
+      if (call.HasArgument("limit") ||
+          call.HasArgument("since"))
       {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "Missing \"since\" 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", ""));
+        request.SetLimits(since, limit);
       }
 
-      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);
+      FindResponse response;
+      index.ExecuteFind(response, request);
+
+      // TODO-FIND: put this in an AnswerFindResponse method !
+      Json::Value answer = Json::arrayValue;
+
+      if (request.IsResponseIdentifiersOnly())
+      {
+        for (size_t i = 0; i < response.GetSize(); i++)
+        {
+          std::string resourceId = response.GetItem(i).GetResourceId();
+          answer.append(resourceId);
+        }
+      }
+      else
+      {
+        for (size_t i = 0; i < response.GetSize(); i++)
+        {
+          context.AppendFindResponse(answer, response.GetItem(i), format, requestedTags, true /* allowStorageAccess */);
+        }
+      }
+
+      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 +3219,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 +3499,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	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Fri Apr 26 17:43:22 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	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.h	Fri Apr 26 17:43:22 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	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Fri Apr 26 17:43:22 2024 +0200
@@ -24,6 +24,7 @@
 
 #if ORTHANC_BUILDING_SERVER_LIBRARY == 1
 #  include "../../../OrthancFramework/Sources/Enumerations.h"
+#  include "../Search/LabelsConstraint.h"
 #else
 #  include <Enumerations.h>
 #endif
@@ -35,13 +36,6 @@
 {
   class DatabaseConstraint;
   
-  enum LabelsConstraint
-  {
-    LabelsConstraint_All,
-    LabelsConstraint_Any,
-    LabelsConstraint_None
-  };
-
   // This class is also used by the "orthanc-databases" project
   class ISqlLookupFormatter : public boost::noncopyable
   {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/LabelsConstraint.h	Fri Apr 26 17:43:22 2024 +0200
@@ -0,0 +1,33 @@
+/**
+ * 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
+
+namespace Orthanc
+{
+  enum LabelsConstraint
+  {
+    LabelsConstraint_All,
+    LabelsConstraint_Any,
+    LabelsConstraint_None
+  };
+}
\ No newline at end of file
--- a/OrthancServer/Sources/ServerContext.cpp	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Fri Apr 26 17:43:22 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,41 @@
     return elapsed.total_seconds();
   }
 
+  void ServerContext::AppendFindResponse(Json::Value& target,
+                                         const FindResponse::Item& 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(item);
+
+    ExpandResourceFlags expandFlags = ExpandResourceFlags_None;
+    if (item.HasResponseContent(FindRequest::ResponseContent_Children))
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeChildren);
+    }
+    if (item.HasResponseContent(FindRequest::ResponseContent_Metadata))
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeAllMetadata | ExpandResourceFlags_IncludeMetadata );
+    }
+    if (item.HasResponseContent(FindRequest::ResponseContent_MainDicomTags))
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeMainDicomTags);
+    }
+    if (item.HasResponseContent(FindRequest::ResponseContent_IsStable))
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeIsStable);
+    }
+    if (item.HasResponseContent(FindRequest::ResponseContent_Labels))
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeLabels);
+    }
+
+    Json::Value jsonItem;
+    SerializeExpandedResource(jsonItem, resource, format, requestedTags, expandFlags);
+    target.append(jsonItem);
+  }
+
+
 }
--- a/OrthancServer/Sources/ServerContext.h	Fri Apr 26 17:41:40 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Fri Apr 26 17:43:22 2024 +0200
@@ -607,6 +607,12 @@
                         ExpandResourceFlags expandFlags,
                         bool allowStorageAccess);
 
+    void AppendFindResponse(Json::Value& target,
+                            const FindResponse::Item& item,
+                            DicomToJsonFormat format,
+                            const std::set<DicomTag>& requestedTags,
+                            bool allowStorageAccess);
+
     FindStorageAccessMode GetFindStorageAccessMode() const
     {
       return findStorageAccessMode_;