changeset 5554:12d8a1a266e9 find-refactoring

introduction of FindRequest and FindResponse
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 15 Apr 2024 16:13:24 +0200
parents dcbf0c776945
children def06a42e5ef
files OrthancServer/CMakeLists.txt OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp OrthancServer/Sources/Database/Compatibility/GenericFind.cpp OrthancServer/Sources/Database/Compatibility/GenericFind.h OrthancServer/Sources/Database/FindRequest.cpp OrthancServer/Sources/Database/FindRequest.h OrthancServer/Sources/Database/FindResponse.cpp OrthancServer/Sources/Database/FindResponse.h OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/OrthancIdentifiers.cpp OrthancServer/Sources/Database/OrthancIdentifiers.h OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp
diffstat 17 files changed, 1623 insertions(+), 29 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancServer/CMakeLists.txt	Fri Mar 29 23:23:01 2024 +0100
+++ b/OrthancServer/CMakeLists.txt	Mon Apr 15 16:13:24 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 Mar 29 23:23:01 2024 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Mon Apr 15 16:13:24 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,14 @@
     {
       throw OrthancException(ErrorCode_InternalError);  // Not supported
     }
+
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request) ORTHANC_OVERRIDE
+    {
+      Compatibility::GenericFind find(*this);
+      find.Execute(response, request);
+    }
   };
 
 
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Fri Mar 29 23:23:01 2024 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Mon Apr 15 16:13:24 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,14 @@
     {
       throw OrthancException(ErrorCode_InternalError);  // Not supported
     }
+
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request) ORTHANC_OVERRIDE
+    {
+      Compatibility::GenericFind find(*this);
+      find.Execute(response, request);
+    }
   };
 
   
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Fri Mar 29 23:23:01 2024 +0100
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Mon Apr 15 16:13:24 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,14 @@
     {
       ListLabelsInternal(target, false, -1);
     }
+
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request) ORTHANC_OVERRIDE
+    {
+      Compatibility::GenericFind find(*this);
+      find.Execute(response, request);
+    }
   };
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp	Mon Apr 15 16:13:24 2024 +0200
@@ -0,0 +1,106 @@
+/**
+ * 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.GetResponseType() == FindRequest::ResponseType_OrthancIdentifiers &&
+          !request.GetOrthancIdentifiers().HasPatientId() &&
+          !request.GetOrthancIdentifiers().HasStudyId() &&
+          !request.GetOrthancIdentifiers().HasSeriesId() &&
+          !request.GetOrthancIdentifiers().HasInstanceId() &&
+          request.GetTagConstraintsCount() == 0 &&
+          request.GetMetadataMode() == FindRequest::MetadataMode_None &&
+          !request.IsRetrieveTagsAtLevel(ResourceType_Patient) &&
+          !request.IsRetrieveTagsAtLevel(ResourceType_Study) &&
+          !request.IsRetrieveTagsAtLevel(ResourceType_Series) &&
+          !request.IsRetrieveTagsAtLevel(ResourceType_Instance) &&
+          request.GetTagOrdering().empty() &&
+          request.GetLabels().empty() &&
+          request.GetMetadataConstraints().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.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);
+        }
+
+        switch (request.GetResponseType())
+        {
+          case FindRequest::ResponseType_OrthancIdentifiers:
+            break;
+
+          case FindRequest::ResponseType_DicomMap:
+            if (!item.HasDicomMap())
+            {
+              throw OrthancException(ErrorCode_InternalError);
+            }
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
+        }
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.h	Mon Apr 15 16:13:24 2024 +0200
@@ -0,0 +1,46 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2024 Osimis S.A., Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../IDatabaseWrapper.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class GenericFind : public boost::noncopyable
+    {
+    private:
+      IDatabaseWrapper::ITransaction&  transaction_;
+
+    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	Mon Apr 15 16:13:24 2024 +0200
@@ -0,0 +1,243 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-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),
+    responseType_(ResponseType_OrthancIdentifiers),
+    hasLimits_(false),
+    limitsSince_(0),
+    limitsCount_(0),
+    metadataMode_(MetadataMode_None),
+    retrievePatientTags_(false),
+    retrieveStudyTags_(false),
+    retrieveSeriesTags_(false),
+    retrieveInstanceTags_(false)
+  {
+  }
+
+
+  FindRequest::~FindRequest()
+  {
+    for (std::deque<TagConstraint*>::iterator it = tagConstraints_.begin(); it != tagConstraints_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
+  }
+
+
+  void FindRequest::AddTagConstraint(TagConstraint* constraint /* takes ownership */)
+  {
+    if (constraint == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      tagConstraints_.push_back(constraint);
+    }
+  }
+
+
+  const FindRequest::TagConstraint& FindRequest::GetTagConstraint(size_t index) const
+  {
+    if (index >= tagConstraints_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(tagConstraints_[index] != NULL);
+      return *tagConstraints_[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::SetTagOrdering(DicomTag tag,
+                                   Ordering ordering)
+  {
+    switch (ordering)
+    {
+      case Ordering_None:
+        tagOrdering_.erase(tag);
+        break;
+
+      case Ordering_Ascending:
+        tagOrdering_[tag] = Ordering_Ascending;
+        break;
+
+      case Ordering_Descending:
+        tagOrdering_[tag] = Ordering_Descending;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void FindRequest::AddMetadataConstraint(MetadataType metadata,
+                                          const std::string& value)
+  {
+    if (metadataConstraints_.find(metadata) == metadataConstraints_.end())
+    {
+      metadataConstraints_[metadata] = value;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindRequest.h	Mon Apr 15 16:13:24 2024 +0200
@@ -0,0 +1,361 @@
+/**
+ * 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 <deque>
+#include <map>
+#include <set>
+
+
+namespace Orthanc
+{
+  class FindRequest : public boost::noncopyable
+  {
+  public:
+    enum ResponseType
+    {
+      ResponseType_OrthancIdentifiers,
+      ResponseType_DicomMap
+    };
+
+    enum ConstraintType
+    {
+      ConstraintType_Mandatory,
+      ConstraintType_Equality,
+      ConstraintType_Range,
+      ConstraintType_Wildcard,
+      ConstraintType_List
+    };
+
+    enum MetadataMode
+    {
+      MetadataMode_None,
+      MetadataMode_List,
+      MetadataMode_Retrieve
+    };
+
+    enum Ordering
+    {
+      Ordering_Ascending,
+      Ordering_Descending,
+      Ordering_None
+    };
+
+
+    class TagConstraint : public boost::noncopyable
+    {
+    private:
+      DicomTag  tag_;
+
+    public:
+      TagConstraint(DicomTag tag) :
+        tag_(tag)
+      {
+      }
+
+      virtual ~TagConstraint()
+      {
+      }
+
+      virtual DicomTag GetTag() const
+      {
+        return tag_;
+      }
+
+      virtual ConstraintType GetType() const = 0;
+
+      virtual bool IsCaseSensitive() const = 0;  // Needed for PN VR
+    };
+
+
+    class MandatoryConstraint : public TagConstraint
+    {
+    public:
+      virtual ConstraintType GetType() const ORTHANC_OVERRIDE
+      {
+        return ConstraintType_Mandatory;
+      }
+    };
+
+
+    class StringConstraint : public TagConstraint
+    {
+    private:
+      bool  caseSensitive_;
+
+    public:
+      StringConstraint(DicomTag tag,
+                       bool caseSensitive) :
+        TagConstraint(tag),
+        caseSensitive_(caseSensitive)
+      {
+      }
+
+      bool IsCaseSensitive() const
+      {
+        return caseSensitive_;
+      }
+    };
+
+
+    class EqualityConstraint : public StringConstraint
+    {
+    private:
+      std::string  value_;
+
+    public:
+      explicit EqualityConstraint(DicomTag tag,
+                                  bool caseSensitive,
+                                  const std::string& value) :
+        StringConstraint(tag, 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(DicomTag tag,
+                      bool caseSensitive,
+                      const std::string& start,
+                      const std::string& end) :
+        StringConstraint(tag, 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(DicomTag& tag,
+                                  bool caseSensitive,
+                                  const std::string& value) :
+        StringConstraint(tag, 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(DicomTag tag,
+                     bool caseSensitive) :
+        StringConstraint(tag, caseSensitive)
+      {
+      }
+
+      virtual ConstraintType GetType() const ORTHANC_OVERRIDE
+      {
+        return ConstraintType_List;
+      }
+
+      const std::set<std::string>& GetValues() const
+      {
+        return values_;
+      }
+    };
+
+
+  private:
+    ResourceType                         level_;
+    ResponseType                         responseType_;
+    OrthancIdentifiers                   orthancIdentifiers_;
+    std::deque<TagConstraint*>           tagConstraints_;
+    bool                                 hasLimits_;
+    uint64_t                             limitsSince_;
+    uint64_t                             limitsCount_;
+    MetadataMode                         metadataMode_;
+    bool                                 retrievePatientTags_;
+    bool                                 retrieveStudyTags_;
+    bool                                 retrieveSeriesTags_;
+    bool                                 retrieveInstanceTags_;
+    std::map<DicomTag, Ordering>         tagOrdering_;
+    std::set<std::string>                labels_;
+    std::map<MetadataType, std::string>  metadataConstraints_;
+
+    bool IsCompatibleLevel(ResourceType levelOfInterest) const;
+
+  public:
+    FindRequest(ResourceType level);
+
+    ~FindRequest();
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void SetResponseType(ResponseType type)
+    {
+      responseType_ = type;
+    }
+
+    ResponseType GetResponseType() const
+    {
+      return responseType_;
+    }
+
+    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 AddTagConstraint(TagConstraint* constraint /* takes ownership */);
+
+    size_t GetTagConstraintsCount() const
+    {
+      return tagConstraints_.size();
+    }
+
+    const TagConstraint& GetTagConstraint(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 SetMetadataMode(MetadataMode mode)
+    {
+      metadataMode_ = mode;
+    }
+
+    MetadataMode GetMetadataMode() const
+    {
+      return metadataMode_;
+    }
+
+    void SetRetrieveTagsAtLevel(ResourceType levelOfInterest,
+                                bool retrieve);
+
+    bool IsRetrieveTagsAtLevel(ResourceType levelOfInterest) const;
+
+    void SetTagOrdering(DicomTag tag,
+                        Ordering ordering);
+
+    const std::map<DicomTag, Ordering>& GetTagOrdering() const
+    {
+      return tagOrdering_;
+    }
+
+    void AddLabel(const std::string& label)
+    {
+      labels_.insert(label);
+    }
+
+    const std::set<std::string>& GetLabels() const
+    {
+      return labels_;
+    }
+
+    void AddMetadataConstraint(MetadataType metadata,
+                               const std::string& value);
+
+    const std::map<MetadataType, std::string>& GetMetadataConstraints() const
+    {
+      return metadataConstraints_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindResponse.cpp	Mon Apr 15 16:13:24 2024 +0200
@@ -0,0 +1,223 @@
+/**
+ * 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(ResourceType level,
+                           DicomMap* dicomMap /* takes ownership */) :
+    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];
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/FindResponse.h	Mon Apr 15 16:13:24 2024 +0200
@@ -0,0 +1,105 @@
+/**
+ * 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 "../ServerEnumerations.h"
+#include "OrthancIdentifiers.h"
+
+#include <boost/noncopyable.hpp>
+#include <deque>
+#include <map>
+#include <set>
+
+
+namespace Orthanc
+{
+  class FindResponse : public boost::noncopyable
+  {
+  public:
+    class Item : public boost::noncopyable
+    {
+    private:
+      ResourceType                         level_;
+      OrthancIdentifiers                   identifiers_;
+      std::map<MetadataType, std::string>  metadata_;
+      std::unique_ptr<DicomMap>            dicomMap_;
+
+    public:
+      Item(ResourceType level,
+           const OrthancIdentifiers& identifiers) :
+        level_(level),
+        identifiers_(identifiers)
+      {
+      }
+
+      Item(ResourceType level,
+           DicomMap* dicomMap /* takes ownership */);
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      const OrthancIdentifiers& GetIdentifiers() const
+      {
+        return identifiers_;
+      }
+
+      void AddMetadata(MetadataType metadata,
+                       const std::string& value);
+
+      bool HasMetadata(MetadataType metadata) const
+      {
+        return metadata_.find(metadata) != metadata_.end();
+      }
+
+      bool LookupMetadata(std::string& value,
+                          MetadataType metadata) const;
+
+      void ListMetadata(std::set<MetadataType> metadata) const;
+
+      bool HasDicomMap() const
+      {
+        return dicomMap_.get() != NULL;
+      }
+
+      const DicomMap& GetDicomMap() const;
+    };
+
+  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 Mar 29 23:23:01 2024 +0100
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Mon Apr 15 16:13:24 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,13 @@
                                           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) = 0;
     };
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/OrthancIdentifiers.cpp	Mon Apr 15 16:13:24 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	Mon Apr 15 16:13:24 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 Mar 29 23:23:01 2024 +0100
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Mon Apr 15 16:13:24 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,14 @@
         target.insert(s.ColumnString(0));
       }
     }
+
+
+    virtual void ExecuteFind(FindResponse& response,
+                             const FindRequest& request) ORTHANC_OVERRIDE
+    {
+      Compatibility::GenericFind find(*this);
+      find.Execute(response, request);
+    }
   };
 
 
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Fri Mar 29 23:23:01 2024 +0100
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Mon Apr 15 16:13:24 2024 +0200
@@ -3775,4 +3775,22 @@
     boost::shared_lock<boost::shared_mutex> lock(mutex_);
     return db_.GetDatabaseCapabilities().HasLabelsSupport();
   }
+
+
+  void StatelessDatabaseOperations::ExecuteFind(FindResponse& response,
+                                                const FindRequest& request)
+  {
+    class Operations : public ReadOnlyOperationsT2<FindResponse&, const FindRequest&>
+    {
+    public:
+      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
+                              const Tuple& tuple) ORTHANC_OVERRIDE
+      {
+        transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>());
+      }
+    };
+
+    Operations operations;
+    operations.Apply(*this, response, request);
+  }
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Fri Mar 29 23:23:01 2024 +0100
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Mon Apr 15 16:13:24 2024 +0200
@@ -376,6 +376,12 @@
       {
         transaction_.ListAllLabels(target);
       }
+
+      void ExecuteFind(FindResponse& response,
+                       const FindRequest& request)
+      {
+        transaction_.ExecuteFind(response, request);
+      }
     };
 
 
@@ -798,5 +804,8 @@
                    const std::set<std::string>& labels);
 
     bool HasLabelsSupport();
+
+    void ExecuteFind(FindResponse& response,
+                     const FindRequest& request);
   };
 }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Fri Mar 29 23:23:01 2024 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Mon Apr 15 16:13:24 2024 +0200
@@ -126,7 +126,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.
@@ -180,7 +180,7 @@
   }
 
 
-  static void AnswerListOfResources(RestApiOutput& output,
+  static void AnswerListOfResources2(RestApiOutput& output,
                                     ServerContext& context,
                                     const std::list<std::string>& resources,
                                     ResourceType level,
@@ -193,7 +193,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);
   }
 
 
@@ -223,41 +223,141 @@
     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
+       **/
+
+      const bool expand = (call.HasArgument("expand") &&
+                           call.GetBooleanArgument("expand", true));
+
+      FindRequest request(resourceType);
+
+#if 0
+      // TODO - This version should be executed if no disk access is needed
+      if (expand)
+      {
+        request.SetResponseType(FindRequest::ResponseType_DicomMap);
+        request.SetMetadataMode(FindRequest::MetadataMode_Retrieve);
+        request.SetRetrieveTagsAtLevel(resourceType, true);
+
+        if (resourceType == ResourceType_Study)
+        {
+          request.SetRetrieveTagsAtLevel(ResourceType_Patient, true);
+        }
+      }
+      else
+      {
+        request.SetResponseType(FindRequest::ResponseType_OrthancIdentifiers);
+        request.SetMetadataMode(FindRequest::MetadataMode_None);
+      }
+#else
+      request.SetResponseType(FindRequest::ResponseType_OrthancIdentifiers);
+      request.SetMetadataMode(FindRequest::MetadataMode_None);
+#endif
+
+      if (call.HasArgument("limit") ||
+          call.HasArgument("since"))
       {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "Missing \"limit\" argument for GET request against: " +
-                               call.FlattenUri());
+        if (!call.HasArgument("limit"))
+        {
+          throw OrthancException(ErrorCode_BadRequest,
+                                 "Missing \"limit\" argument for GET request against: " +
+                                 call.FlattenUri());
+        }
+
+        if (!call.HasArgument("since"))
+        {
+          throw OrthancException(ErrorCode_BadRequest,
+                                 "Missing \"since\" argument for GET request against: " +
+                                 call.FlattenUri());
+        }
+
+        uint64_t since = boost::lexical_cast<uint64_t>(call.GetArgument("since", ""));
+        uint64_t limit = boost::lexical_cast<uint64_t>(call.GetArgument("limit", ""));
+        request.SetLimits(since, limit);
       }
 
-      if (!call.HasArgument("since"))
+      FindResponse response;
+      index.ExecuteFind(response, request);
+
+      std::set<DicomTag> requestedTags;
+      OrthancRestApi::GetRequestedTags(requestedTags, call);
+
+      const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
+
+      Json::Value answer = Json::arrayValue;
+
+      for (size_t i = 0; i < response.GetSize(); i++)
       {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "Missing \"since\" argument for GET request against: " +
-                               call.FlattenUri());
+        std::string resourceId = response.GetItem(i).GetIdentifiers().GetLevel(resourceType);
+
+        if (expand)
+        {
+          Json::Value expanded;
+
+          context.ExpandResource(expanded, resourceId, resourceType, format, requestedTags, true /* allowStorageAccess */);
+
+          if (expanded.type() == Json::objectValue)
+          {
+            answer.append(expanded);
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+        }
+        else
+        {
+          answer.append(resourceId);
+        }
       }
 
-      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);
+      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 */);
   }
 
 
@@ -3106,7 +3206,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_));
       }
     };
   }
@@ -3386,7 +3486,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 */);