changeset 5834:79a497908b04 attach-custom-data

merged find-refactoring -> attach-custom-data
author Alain Mazy <am@orthanc.team>
date Wed, 09 Oct 2024 11:06:20 +0200
parents 79ac3924eff8 (current diff) dd2af8692cbc (diff)
children
files NEWS OrthancServer/CMakeLists.txt OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/ServerEnumerations.h OrthancServer/UnitTestsSources/ServerIndexTests.cpp
diffstat 43 files changed, 1559 insertions(+), 654 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed Oct 02 11:41:01 2024 +0200
+++ b/NEWS	Wed Oct 09 11:06:20 2024 +0200
@@ -42,12 +42,20 @@
 * in /system, added a new field "Capabilities" with new values:
   - "HasExtendedChanges" for DB backend that provides this feature (the default SQLite DB
     or PostgreSQL vX.X, MySQL vX.X, ODBC vX.X).
-  - "HasExendedFind" for DB backend that provides this feature (the default SQLite DB
+  - "HasExtendedFind" for DB backend that provides this feature (the default SQLite DB
     or PostgreSQL vX.X, MySQL vX.X, ODBC vX.X).
 * With DB backend with "HasExtendedChanges" support, /changes now supports 2 more options: 
   - 'type' to filter the changes returned by the query 
   - 'to' to potentially cycle through changes in reverse order.
   example: /changes?type=StableStudy&to=7584&limit=100
+* With DB backend with "HasExtendedFind" support, /tools/find now supports new options:
+  - 'OrderBy' to order by DICOM Tag or metadata value
+  - 'ParentPatient', 'ParentStudy', 'ParentSeries' to retrieve only descendants of an
+    Orthanc resource.
+  - 'QueryMetadata' to filter results based on metadata values.
+  - 'ResponseContent' to define what shall be included in the response for each returned
+    resource (e.g: Metadata, Children, ...)
+
 
 Maintenance
 -----------
@@ -67,6 +75,7 @@
   - added 2 metrics: orthanc_storage_cache_miss_count & orthanc_storage_cache_hit_count 
 * Upgraded dependencies for static builds:
   - curl 8.9.0
+  - SQLite 3.46
 * Added a new fallback when trying to decode a frame: transcode the file using the plugin
   before decoding the frame.  This solves some issues with JP2K Lossy compression:
   https://discourse.orthanc-server.org/t/decoding-displaying-jpeg2000-lossy-images/5117
--- a/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake	Wed Oct 09 11:06:20 2024 +0200
@@ -37,11 +37,11 @@
 
 
 if (SQLITE_STATIC)
-  SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3270100)
-  SET(SQLITE_MD5 "16717b26358ba81f0bfdac07addc77da")
-  SET(SQLITE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/sqlite-amalgamation-3270100.zip")
+  SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3460100)
+  SET(SQLITE_MD5 "1fb0f7ebbee45752098cf453b6dffff3")
+  SET(SQLITE_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/sqlite-amalgamation-3460100.zip")
 
-  set(ORTHANC_SQLITE_VERSION 3027001)
+  set(ORTHANC_SQLITE_VERSION 3046001)
 
   DownloadPackage(${SQLITE_MD5} ${SQLITE_URL} "${SQLITE_SOURCES_DIR}")
 
--- a/OrthancServer/CMakeLists.txt	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/CMakeLists.txt	Wed Oct 09 11:06:20 2024 +0200
@@ -126,8 +126,9 @@
   ${CMAKE_SOURCE_DIR}/Sources/OrthancWebDav.cpp
   ${CMAKE_SOURCE_DIR}/Sources/QueryRetrieveHandler.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ResourceFinder.cpp
-  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseConstraint.cpp
-  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseConstraints.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseDicomTagConstraint.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseDicomTagConstraints.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseMetadataConstraint.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseLookup.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/DicomTagConstraint.cpp
   ${CMAKE_SOURCE_DIR}/Sources/Search/HierarchicalMatcher.cpp
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -566,7 +566,7 @@
     
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
-                                      const DatabaseConstraints& lookup,
+                                      const DatabaseDicomTagConstraints& lookup,
                                       ResourceType queryLevel,
                                       const std::set<std::string>& labels,
                                       LabelsConstraint labelsConstraint,
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -800,7 +800,7 @@
     
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId, // Can be NULL if not needed
-                                      const DatabaseConstraints& lookup,
+                                      const DatabaseDicomTagConstraints& lookup,
                                       ResourceType queryLevel,
                                       const std::set<std::string>& labels,
                                       LabelsConstraint labelsConstraint,
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -36,6 +36,7 @@
 #include "../../Sources/Database/VoidDatabaseListener.h"
 #include "../../Sources/ServerToolbox.h"
 #include "PluginsEnumerations.h"
+#include "../../Sources/Database/MainDicomTagsRegistry.h"
 
 #include "OrthancDatabasePlugin.pb.h"  // Auto-generated file
 
@@ -137,7 +138,7 @@
 
 
   static void Convert(DatabasePluginMessages::DatabaseConstraint& target,
-                      const DatabaseConstraint& source)
+                      const DatabaseDicomTagConstraint& source)
   {
     target.set_level(Convert(source.GetLevel()));
     target.set_tag_group(source.GetTag().GetGroup());
@@ -180,6 +181,94 @@
   }
 
 
+  static void Convert(DatabasePluginMessages::DatabaseMetadataConstraint& target,
+                      const DatabaseMetadataConstraint& source)
+  {
+    target.set_metadata(source.GetMetadata());
+    target.set_is_case_sensitive(source.IsCaseSensitive());
+    target.set_is_mandatory(source.IsMandatory());
+
+    target.mutable_values()->Reserve(source.GetValuesCount());
+    for (size_t j = 0; j < source.GetValuesCount(); j++)
+    {
+      target.add_values(source.GetValue(j));
+    }
+
+    switch (source.GetConstraintType())
+    {
+      case ConstraintType_Equal:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_EQUAL);
+        break;
+
+      case ConstraintType_SmallerOrEqual:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_SMALLER_OR_EQUAL);
+        break;
+
+      case ConstraintType_GreaterOrEqual:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_GREATER_OR_EQUAL);
+        break;
+
+      case ConstraintType_Wildcard:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_WILDCARD);
+        break;
+
+      case ConstraintType_List:
+        target.set_type(DatabasePluginMessages::CONSTRAINT_LIST);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static void Convert(DatabasePluginMessages::Find_Request_Ordering& target,
+                      const FindRequest::Ordering& source)
+  {
+    switch (source.GetKeyType())
+    {
+      case FindRequest::KeyType_DicomTag:
+      {
+        ResourceType tagLevel;
+        DicomTagType tagType;
+        MainDicomTagsRegistry registry;
+
+        registry.LookupTag(tagLevel, tagType, source.GetDicomTag());
+
+        target.set_key_type(DatabasePluginMessages::ORDERING_KEY_TYPE_DICOM_TAG);
+        target.set_tag_group(source.GetDicomTag().GetGroup());
+        target.set_tag_element(source.GetDicomTag().GetElement());
+        target.set_is_identifier_tag(tagType == DicomTagType_Identifier);
+        target.set_tag_level(Convert(tagLevel));
+
+      }; break;
+
+      case FindRequest::KeyType_Metadata:
+        target.set_key_type(DatabasePluginMessages::ORDERING_KEY_TYPE_METADATA);
+        target.set_metadata(source.GetMetadataType());
+
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    switch (source.GetDirection())
+    {
+      case FindRequest::OrderingDirection_Ascending:
+        target.set_direction(DatabasePluginMessages::ORDERING_DIRECTION_ASC);
+        break;
+
+      case FindRequest::OrderingDirection_Descending:
+        target.set_direction(DatabasePluginMessages::ORDERING_DIRECTION_DESC);
+
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
   static DatabasePluginMessages::LabelsConstraintType Convert(LabelsConstraint constraint)
   {
     switch (constraint)
@@ -1139,7 +1228,7 @@
 
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId, // Can be NULL if not needed
-                                      const DatabaseConstraints& lookup,
+                                      const DatabaseDicomTagConstraints& lookup,
                                       ResourceType queryLevel,
                                       const std::set<std::string>& labels,
                                       LabelsConstraint labelsConstraint,
@@ -1422,6 +1511,16 @@
           Convert(*dbRequest.mutable_find()->add_dicom_tag_constraints(), request.GetDicomTagConstraints().GetConstraint(i));
         }
 
+        for (std::deque<DatabaseMetadataConstraint*>::const_iterator it = request.GetMetadataConstraint().begin(); it != request.GetMetadataConstraint().end(); ++it)
+        {
+          Convert(*dbRequest.mutable_find()->add_metadata_constraints(), *(*it)); 
+        }
+
+        for (std::deque<FindRequest::Ordering*>::const_iterator it = request.GetOrdering().begin(); it != request.GetOrdering().end(); ++it)
+        {
+          Convert(*dbRequest.mutable_find()->add_ordering(), *(*it)); 
+        }
+
         if (request.HasLimits())
         {
           dbRequest.mutable_find()->mutable_limits()->set_since(request.GetLimitsSince());
@@ -1435,9 +1534,6 @@
 
         dbRequest.mutable_find()->set_labels_constraint(Convert(request.GetLabelsConstraint()));
 
-        // TODO-FIND: ordering_
-        // TODO-FIND: metadataConstraints__
-
         dbRequest.mutable_find()->set_retrieve_main_dicom_tags(request.IsRetrieveMainDicomTags());
         dbRequest.mutable_find()->set_retrieve_metadata(request.IsRetrieveMetadata());
         dbRequest.mutable_find()->set_retrieve_labels(request.IsRetrieveLabels());
--- a/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -59,7 +59,7 @@
 #include "../../Sources/Database/VoidDatabaseListener.h"
 #include "../../Sources/OrthancConfiguration.h"
 #include "../../Sources/OrthancFindRequestHandler.h"
-#include "../../Sources/Search/DatabaseConstraint.h"
+#include "../../Sources/Search/IDatabaseConstraint.h"
 #include "../../Sources/Search/HierarchicalMatcher.h"
 #include "../../Sources/ServerContext.h"
 #include "../../Sources/ServerToolbox.h"
--- a/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancDatabasePlugin.proto	Wed Oct 09 11:06:20 2024 +0200
@@ -79,6 +79,16 @@
   LABELS_CONSTRAINT_NONE = 2;
 }
 
+enum OrderingKeyType {
+  ORDERING_KEY_TYPE_DICOM_TAG = 0;
+  ORDERING_KEY_TYPE_METADATA = 1;
+}
+
+enum OrderingDirection {
+  ORDERING_DIRECTION_ASC = 0;
+  ORDERING_DIRECTION_DESC = 1;
+}
+
 message ServerIndexChange {
   int64         seq = 1;
   int32         change_type = 2;   // opaque "ChangeType" in Orthanc
@@ -110,6 +120,13 @@
   repeated string  values = 8;
 }
 
+message DatabaseMetadataConstraint {
+  int32            metadata = 1;
+  bool             is_case_sensitive = 2;
+  bool             is_mandatory = 3;
+  ConstraintType   type = 4;
+  repeated string  values = 5;
+}
 
 /**
  * Database-level operations.
@@ -861,6 +878,15 @@
       repeated int32 retrieve_metadata = 2;
       repeated Tag retrieve_main_dicom_tags = 3;
     }
+    message Ordering {
+      OrderingKeyType key_type = 1;
+      OrderingDirection direction = 2;
+      uint32 tag_group = 3;
+      uint32 tag_element = 4;
+      bool is_identifier_tag = 5;
+      ResourceType tag_level = 6;
+      int32 metadata = 7;
+    }
 
     // Part 1 of the request: Constraints
     ResourceType level = 1;
@@ -872,9 +898,9 @@
     Limits limits = 7;               // optional
     repeated string labels = 8;
     LabelsConstraintType labels_constraint = 9;
+    repeated Ordering ordering = 10;
+    repeated DatabaseMetadataConstraint metadata_constraints = 11;
 
-    // TODO-FIND: ordering_
-    // TODO-FIND: metadataConstraints_
 
     // Part 2 of the request: What is to be retrieved
     bool retrieve_main_dicom_tags = 100;
--- a/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -74,7 +74,7 @@
           }
         }
         
-        void Add(const DatabaseConstraint& constraint)
+        void Add(const DatabaseDicomTagConstraint& constraint)
         {
           constraints_.push_back(new DicomTagConstraint(constraint));
         }          
@@ -84,7 +84,7 @@
     
     static void ApplyIdentifierConstraint(SetOfResources& candidates,
                                           ILookupResources& compatibility,
-                                          const DatabaseConstraint& constraint,
+                                          const DatabaseDicomTagConstraint& constraint,
                                           ResourceType level)
     {
       std::list<int64_t> matches;
@@ -134,8 +134,8 @@
     
     static void ApplyIdentifierRange(SetOfResources& candidates,
                                      ILookupResources& compatibility,
-                                     const DatabaseConstraint& smaller,
-                                     const DatabaseConstraint& greater,
+                                     const DatabaseDicomTagConstraint& smaller,
+                                     const DatabaseDicomTagConstraint& greater,
                                      ResourceType level)
     {
       assert(smaller.GetConstraintType() == ConstraintType_SmallerOrEqual &&
@@ -153,10 +153,10 @@
     static void ApplyLevel(SetOfResources& candidates,
                            IDatabaseWrapper::ITransaction& transaction,
                            ILookupResources& compatibility,
-                           const DatabaseConstraints& lookup,
+                           const DatabaseDicomTagConstraints& lookup,
                            ResourceType level)
     {
-      typedef std::set<const DatabaseConstraint*>  SetOfConstraints;
+      typedef std::set<const DatabaseDicomTagConstraint*>  SetOfConstraints;
       typedef std::map<DicomTag, SetOfConstraints> Identifiers;
 
       // (1) Select which constraints apply to this level, and split
@@ -168,7 +168,7 @@
       
       for (size_t i = 0; i < lookup.GetSize(); i++)
       {
-        const DatabaseConstraint& constraint = lookup.GetConstraint(i);
+        const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i);
 
         if (constraint.GetLevel() == level)
         {
@@ -191,8 +191,8 @@
       {
         // Check whether some range constraint over identifiers is
         // present at this level
-        const DatabaseConstraint* smaller = NULL;
-        const DatabaseConstraint* greater = NULL;
+        const DatabaseDicomTagConstraint* smaller = NULL;
+        const DatabaseDicomTagConstraint* greater = NULL;
         
         for (SetOfConstraints::const_iterator it2 = it->second.begin();
              it2 != it->second.end(); ++it2)
@@ -308,7 +308,7 @@
 
     void DatabaseLookup::ApplyLookupResources(std::list<std::string>& resourcesId,
                                               std::list<std::string>* instancesId,
-                                              const DatabaseConstraints& lookup,
+                                              const DatabaseDicomTagConstraints& lookup,
                                               ResourceType queryLevel,
                                               size_t limit)
     {
--- a/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h	Wed Oct 09 11:06:20 2024 +0200
@@ -46,7 +46,7 @@
 
       void ApplyLookupResources(std::list<std::string>& resourcesId,
                                 std::list<std::string>* instancesId,
-                                const DatabaseConstraints& lookup,
+                                const DatabaseDicomTagConstraints& lookup,
                                 ResourceType queryLevel,
                                 size_t limit);
     };
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -35,7 +35,7 @@
       ILookupResources& compatibility,
       std::list<std::string>& resourcesId,
       std::list<std::string>* instancesId,
-      const DatabaseConstraints& lookup,
+      const DatabaseDicomTagConstraints& lookup,
       ResourceType queryLevel,
       size_t limit)
     {
--- a/OrthancServer/Sources/Database/Compatibility/ILookupResources.h	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.h	Wed Oct 09 11:06:20 2024 +0200
@@ -60,7 +60,7 @@
                         ILookupResources& compatibility,
                         std::list<std::string>& resourcesId,
                         std::list<std::string>* instancesId,
-                        const DatabaseConstraints& lookup,
+                        const DatabaseDicomTagConstraints& lookup,
                         ResourceType queryLevel,
                         size_t limit);
     };
--- a/OrthancServer/Sources/Database/FindRequest.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Database/FindRequest.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -100,8 +100,13 @@
 
   FindRequest::~FindRequest()
   {
+    for (std::deque<Ordering*>::iterator it = ordering_.begin(); it != ordering_.end(); ++it)
+    {
+      assert(*it != NULL);
+      delete *it;
+    }
 
-    for (std::deque<Ordering*>::iterator it = ordering_.begin(); it != ordering_.end(); ++it)
+    for (std::deque<DatabaseMetadataConstraint*>::iterator it = metadataConstraints_.begin(); it != metadataConstraints_.end(); ++it)
     {
       assert(*it != NULL);
       delete *it;
@@ -233,6 +238,12 @@
   }
 
 
+  void FindRequest::AddMetadataConstraint(DatabaseMetadataConstraint* constraint)
+  {
+    metadataConstraints_.push_back(constraint);
+  }
+
+
   void FindRequest::SetRetrieveParentIdentifier(bool retrieve)
   {
     if (level_ == ResourceType_Patient)
--- a/OrthancServer/Sources/Database/FindRequest.h	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Database/FindRequest.h	Wed Oct 09 11:06:20 2024 +0200
@@ -24,7 +24,8 @@
 #pragma once
 
 #include "../../../OrthancFramework/Sources/DicomFormat/DicomTag.h"
-#include "../Search/DatabaseConstraints.h"
+#include "../Search/DatabaseDicomTagConstraints.h"
+#include "../Search/DatabaseMetadataConstraint.h"
 #include "../Search/DicomTagConstraint.h"
 #include "../Search/ISqlLookupFormatter.h"
 #include "../ServerEnumerations.h"
@@ -232,16 +233,15 @@
     // filter & ordering fields
     ResourceType                         level_;                // The level of the response (the filtering on tags, labels and metadata also happens at this level)
     OrthancIdentifiers                   orthancIdentifiers_;   // The response must belong to this Orthanc resources hierarchy
-    DatabaseConstraints                  dicomTagConstraints_;  // All tags filters (note: the order is not important)
+    DatabaseDicomTagConstraints          dicomTagConstraints_;  // All tags filters (note: the order is not important)
     bool                                 hasLimits_;
     uint64_t                             limitsSince_;
     uint64_t                             limitsCount_;
     std::set<std::string>                labels_;
     LabelsConstraint                     labelsConstraint_;
 
-    // TODO-FIND
     std::deque<Ordering*>                ordering_;             // The ordering criteria (note: the order is important !)
-    std::deque<void*>   /* TODO-FIND */       metadataConstraints_;  // All metadata filters (note: the order is not important)
+    std::deque<DatabaseMetadataConstraint*>  metadataConstraints_;  // All metadata filters (note: the order is not important)
 
     bool                                 retrieveMainDicomTags_;
     bool                                 retrieveMetadata_;
@@ -284,12 +284,12 @@
       return orthancIdentifiers_;
     }
 
-    DatabaseConstraints& GetDicomTagConstraints()
+    DatabaseDicomTagConstraints& GetDicomTagConstraints()
     {
       return dicomTagConstraints_;
     }
 
-    const DatabaseConstraints& GetDicomTagConstraints() const
+    const DatabaseDicomTagConstraints& GetDicomTagConstraints() const
     {
       return dicomTagConstraints_;
     }
@@ -327,6 +327,13 @@
       return ordering_;
     }
 
+    void AddMetadataConstraint(DatabaseMetadataConstraint* constraint);
+
+    const std::deque<DatabaseMetadataConstraint*>& GetMetadataConstraint() const
+    {
+      return metadataConstraints_;
+    }
+
     void SetLabels(const std::set<std::string>& labels)
     {
       labels_ = labels;
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Wed Oct 09 11:06:20 2024 +0200
@@ -39,7 +39,7 @@
 
 namespace Orthanc
 {
-  class DatabaseConstraints;
+  class DatabaseDicomTagConstraints;
   class ResourcesContent;
 
   class IDatabaseWrapper : public boost::noncopyable
@@ -320,7 +320,7 @@
     
       virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                         std::list<std::string>* instancesId, // Can be NULL if not needed
-                                        const DatabaseConstraints& lookup,
+                                        const DatabaseDicomTagConstraints& lookup,
                                         ResourceType queryLevel,
                                         const std::set<std::string>& labels,
                                         LabelsConstraint labelsConstraint,
--- a/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -98,7 +98,7 @@
   }
 
 
-  bool MainDicomTagsRegistry::NormalizeLookup(DatabaseConstraints& target,
+  bool MainDicomTagsRegistry::NormalizeLookup(DatabaseDicomTagConstraints& target,
                                               const DatabaseLookup& source,
                                               ResourceType queryLevel) const
   {
--- a/OrthancServer/Sources/Database/MainDicomTagsRegistry.h	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.h	Wed Oct 09 11:06:20 2024 +0200
@@ -24,7 +24,7 @@
 #pragma once
 
 #include "../Search/DatabaseLookup.h"
-#include "../Search/DatabaseConstraints.h"
+#include "../Search/DatabaseDicomTagConstraints.h"
 
 #include <boost/noncopyable.hpp>
 
@@ -82,7 +82,7 @@
      * constraints are less strict than the original DatabaseLookup,
      * so more resources will match them.
      **/
-    bool NormalizeLookup(DatabaseConstraints& target,
+    bool NormalizeLookup(DatabaseDicomTagConstraints& target,
                          const DatabaseLookup& source,
                          ResourceType queryLevel) const;
   };
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -430,7 +430,7 @@
 
     virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
                                       std::list<std::string>* instancesId,
-                                      const DatabaseConstraints& lookup,
+                                      const DatabaseDicomTagConstraints& lookup,
                                       ResourceType queryLevel,
                                       const std::set<std::string>& labels,
                                       LabelsConstraint labelsConstraint,
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -1593,7 +1593,7 @@
 
     DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true);
 
-    DatabaseConstraints query;
+    DatabaseDicomTagConstraints query;
     bool isIdentical;  // unused
     query.AddConstraint(c.ConvertToDatabaseConstraint(isIdentical, level, DicomTagType_Identifier));
 
@@ -1602,12 +1602,12 @@
     {
     private:
       std::vector<std::string>&   result_;
-      const DatabaseConstraints&  query_;
+      const DatabaseDicomTagConstraints&  query_;
       ResourceType                level_;
       
     public:
       Operations(std::vector<std::string>& result,
-                 const DatabaseConstraints& query,
+                 const DatabaseDicomTagConstraints& query,
                  ResourceType level) :
         result_(result),
         query_(query),
@@ -1893,7 +1893,7 @@
                                                          LabelsConstraint labelsConstraint,
                                                          uint32_t limit)
   {
-    class Operations : public ReadOnlyOperationsT6<bool, const DatabaseConstraints&, ResourceType,
+    class Operations : public ReadOnlyOperationsT6<bool, const DatabaseDicomTagConstraints&, ResourceType,
                                                    const std::set<std::string>&, LabelsConstraint, size_t>
     {
     private:
@@ -1939,7 +1939,7 @@
       ServerToolbox::CheckValidLabel(*it);
     }
 
-    DatabaseConstraints normalized;
+    DatabaseDicomTagConstraints normalized;
 
     assert(mainDicomTagsRegistry_.get() != NULL);
     mainDicomTagsRegistry_->NormalizeLookup(normalized, lookup, queryLevel);
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Wed Oct 09 11:06:20 2024 +0200
@@ -221,7 +221,7 @@
 
       void ApplyLookupResources(std::list<std::string>& resourcesId,
                                 std::list<std::string>* instancesId, // Can be NULL if not needed
-                                const DatabaseConstraints& lookup,
+                                const DatabaseDicomTagConstraints& lookup,
                                 ResourceType queryLevel,
                                 const std::set<std::string>& labels,  // New in Orthanc 1.12.0
                                 LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
--- a/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -631,7 +631,7 @@
        * EXPERIMENTAL VERSION
        **/
 
-      ResourceFinder finder(level, false /* don't expand */);
+      ResourceFinder finder(level, ResponseContentFlags_ID);
       finder.SetDatabaseLookup(lookup);
       finder.AddRequestedTags(requestedTags);
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -41,6 +41,7 @@
 
 #include "../OrthancConfiguration.h"
 #include "../Search/DatabaseLookup.h"
+#include "../Search/DatabaseMetadataConstraint.h"
 #include "../ServerContext.h"
 #include "../ServerToolbox.h"
 #include "../SliceOrdering.h"
@@ -136,7 +137,14 @@
                              DicomToJsonFormat format,
                              bool retrieveMetadata)
   {
-    ResourceFinder finder(level, true /* expand */);
+    ResponseContentFlags responseContent = ResponseContentFlags_ExpandTrue;
+    
+    if (retrieveMetadata)
+    {
+      responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | ResponseContentFlags_Metadata);
+    }
+
+    ResourceFinder finder(level, responseContent);
     finder.SetOrthancId(level, identifier);
     finder.SetRetrieveMetadata(retrieveMetadata);
 
@@ -257,7 +265,7 @@
       std::set<DicomTag> requestedTags;
       OrthancRestApi::GetRequestedTags(requestedTags, call);
 
-      ResourceFinder finder(resourceType, expand);
+      ResourceFinder finder(resourceType, (expand ? ResponseContentFlags_ExpandTrue : ResponseContentFlags_ID));
       finder.AddRequestedTags(requestedTags);
 
       if (call.HasArgument("limit") ||
@@ -363,7 +371,7 @@
        * EXPERIMENTAL VERSION
        **/
 
-      ResourceFinder finder(resourceType, true /* expand */);
+      ResourceFinder finder(resourceType, ResponseContentFlags_ExpandTrue);
       finder.AddRequestedTags(requestedTags);
       finder.SetOrthancId(resourceType, call.GetUriComponent("id", ""));
 
@@ -3250,6 +3258,15 @@
     static const char* const KEY_SINCE = "Since";
     static const char* const KEY_LABELS = "Labels";                       // New in Orthanc 1.12.0
     static const char* const KEY_LABELS_CONSTRAINT = "LabelsConstraint";  // New in Orthanc 1.12.0
+    static const char* const KEY_ORDER_BY = "OrderBy";                    // New in Orthanc 1.12.5
+    static const char* const KEY_ORDER_BY_KEY = "Key";                    // New in Orthanc 1.12.5
+    static const char* const KEY_ORDER_BY_TYPE = "Type";                  // New in Orthanc 1.12.5
+    static const char* const KEY_ORDER_BY_DIRECTION = "Direction";        // New in Orthanc 1.12.5
+    static const char* const KEY_PARENT_PATIENT = "ParentPatient";        // New in Orthanc 1.12.5
+    static const char* const KEY_PARENT_STUDY = "ParentStudy";            // New in Orthanc 1.12.5
+    static const char* const KEY_PARENT_SERIES = "ParentSeries";          // New in Orthanc 1.12.5
+    static const char* const KEY_QUERY_METADATA = "QueryMetadata";        // New in Orthanc 1.12.5
+    static const char* const KEY_RESPONSE_CONTENT = "ResponseContent";    // New in Orthanc 1.12.5
 
     if (call.IsDocumentation())
     {
@@ -3283,6 +3300,20 @@
                          "List of strings specifying which labels to look for in the resources (new in Orthanc 1.12.0)", true)
         .SetRequestField(KEY_LABELS_CONSTRAINT, RestApiCallDocumentation::Type_String,
                          "Constraint on the labels, can be `All`, `Any`, or `None` (defaults to `All`, new in Orthanc 1.12.0)", true)
+        .SetRequestField(KEY_ORDER_BY, RestApiCallDocumentation::Type_JsonListOfObjects,
+                         "Array of associative arrays containing the requested ordering (new in Orthanc 1.12.5)", true)
+        .SetRequestField(KEY_PARENT_PATIENT, RestApiCallDocumentation::Type_String,
+                         "Limit the reported resources to descendants of this patient (new in Orthanc 1.12.5)", true)
+        .SetRequestField(KEY_PARENT_STUDY, RestApiCallDocumentation::Type_String,
+                         "Limit the reported resources to descendants of this study (new in Orthanc 1.12.5)", true)
+        .SetRequestField(KEY_PARENT_SERIES, RestApiCallDocumentation::Type_String,
+                         "Limit the reported resources to descendants of this series (new in Orthanc 1.12.5)", true)
+        .SetRequestField(KEY_QUERY_METADATA, RestApiCallDocumentation::Type_JsonObject,
+                         "Associative array containing the filter on the values of the metadata (new in Orthanc 1.12.5)", true)
+        .SetRequestField(KEY_RESPONSE_CONTENT, RestApiCallDocumentation::Type_JsonListOfStrings,
+                         "Defines the content of response for each returned resource.  Allowed values are `MainDicomTags`, "
+                         "`Metadata`, `Children`, `Parent`, `Labels`, `Status`, `IsStable`, `Attachments`.  "
+                         "(new in Orthanc 1.12.5)", true)
         .AddAnswerType(MimeType_Json, "JSON array containing either the Orthanc identifiers, or detailed information "
                        "about the reported resources (if `Expand` argument is `true`)");
       return;
@@ -3333,6 +3364,12 @@
       throw OrthancException(ErrorCode_BadRequest, 
                              "Field \"" + std::string(KEY_REQUESTED_TAGS) + "\" must be an array");
     }
+    else if (request.isMember(KEY_RESPONSE_CONTENT) &&
+             request[KEY_RESPONSE_CONTENT].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_RESPONSE_CONTENT) + "\" must be an array");
+    }
     else if (request.isMember(KEY_LABELS) &&
              request[KEY_LABELS].type() != Json::arrayValue)
     {
@@ -3345,21 +3382,61 @@
       throw OrthancException(ErrorCode_BadRequest, 
                              "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be an array of strings");
     }
+    else if (request.isMember(KEY_ORDER_BY) &&
+             request[KEY_ORDER_BY].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_ORDER_BY) + "\" must be an array");
+    }
+    else if (request.isMember(KEY_QUERY_METADATA) &&
+             request[KEY_QUERY_METADATA].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_QUERY_METADATA) + "\" must be an JSON object");
+    }
+    else if (request.isMember(KEY_PARENT_PATIENT) &&
+             request[KEY_PARENT_PATIENT].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_PARENT_PATIENT) + "\" must be a string");
+    }
+    else if (request.isMember(KEY_PARENT_STUDY) &&
+             request[KEY_PARENT_STUDY].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_PARENT_STUDY) + "\" must be a string");
+    }
+    else if (request.isMember(KEY_PARENT_SERIES) &&
+             request[KEY_PARENT_SERIES].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_PARENT_SERIES) + "\" must be a string");
+    }
     else if (true)
     {
       /**
        * EXPERIMENTAL VERSION
        **/
 
-      bool expand = false;
-      if (request.isMember(KEY_EXPAND))
+      ResponseContentFlags responseContent = ResponseContentFlags_ID;
+      
+      if (request.isMember(KEY_RESPONSE_CONTENT))
       {
-        expand = request[KEY_EXPAND].asBool();
+        responseContent = ResponseContentFlags_Default;
+
+        for (Json::ArrayIndex i = 0; i < request[KEY_RESPONSE_CONTENT].size(); ++i)
+        {
+          responseContent = static_cast<ResponseContentFlags>(static_cast<uint32_t>(responseContent) | StringToResponseContent(request[KEY_RESPONSE_CONTENT][i].asString()));
+        }
+      }
+      else if (request.isMember(KEY_EXPAND) && request[KEY_EXPAND].asBool())
+      {
+        responseContent = ResponseContentFlags_ExpandTrue;
       }
 
       const ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
 
-      ResourceFinder finder(level, expand);
+      ResourceFinder finder(level, responseContent);
       finder.SetDatabaseLimits(context.GetDatabaseLimits(level));
 
       const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human);
@@ -3399,30 +3476,66 @@
           caseSensitive = request[KEY_CASE_SENSITIVE].asBool();
         }
 
-        DatabaseLookup query;
-
-        Json::Value::Members members = request[KEY_QUERY].getMemberNames();
-        for (size_t i = 0; i < members.size(); i++)
-        {
-          if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
+        { // DICOM Tag query
+          DatabaseLookup dicomTagLookup;
+
+          Json::Value::Members members = request[KEY_QUERY].getMemberNames();
+          for (size_t i = 0; i < members.size(); i++)
           {
-            throw OrthancException(ErrorCode_BadRequest,
-                                   "Tag \"" + members[i] + "\" must be associated with a string");
+            if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
+            {
+              throw OrthancException(ErrorCode_BadRequest,
+                                    "Tag \"" + members[i] + "\" must be associated with a string");
+            }
+
+            const std::string value = request[KEY_QUERY][members[i]].asString();
+
+            if (!value.empty())
+            {
+              // An empty string corresponds to an universal constraint,
+              // so we ignore it. This mimics the behavior of class
+              // "OrthancFindRequestHandler"
+              dicomTagLookup.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]),
+                                      value, caseSensitive, true);
+            }
           }
 
-          const std::string value = request[KEY_QUERY][members[i]].asString();
-
-          if (!value.empty())
+          finder.SetDatabaseLookup(dicomTagLookup);
+        }
+
+        { // Metadata query
+          Json::Value::Members members = request[KEY_QUERY_METADATA].getMemberNames();
+          for (size_t i = 0; i < members.size(); i++)
           {
-            // An empty string corresponds to an universal constraint,
-            // so we ignore it. This mimics the behavior of class
-            // "OrthancFindRequestHandler"
-            query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]),
-                                    value, caseSensitive, true);
+            if (request[KEY_QUERY_METADATA][members[i]].type() != Json::stringValue)
+            {
+              throw OrthancException(ErrorCode_BadRequest,
+                                    "Tag \"" + members[i] + "\" must be associated with a string");
+            }
+            MetadataType metadata = StringToMetadata(members[i]);
+
+            const std::string value = request[KEY_QUERY_METADATA][members[i]].asString();
+
+            if (!value.empty())
+            {
+              if (value.find('\\') != std::string::npos)
+              {
+                std::vector<std::string> items;
+                Toolbox::TokenizeString(items, value, '\\');
+                
+                finder.AddMetadataConstraint(new DatabaseMetadataConstraint(metadata, ConstraintType_List, items, caseSensitive));
+              }
+              else if (value.find('*') != std::string::npos || value.find('?') != std::string::npos)
+              {
+                finder.AddMetadataConstraint(new DatabaseMetadataConstraint(metadata, ConstraintType_Wildcard, value, caseSensitive));
+              }
+              else
+              {
+                finder.AddMetadataConstraint(new DatabaseMetadataConstraint(metadata, ConstraintType_Equal, value, caseSensitive));
+              }
+            }
           }
         }
-
-        finder.SetDatabaseLookup(query);
       }
 
       if (request.isMember(KEY_REQUESTED_TAGS))
@@ -3470,6 +3583,82 @@
         }
       }
 
+      if (request.isMember(KEY_PARENT_PATIENT)) // New in Orthanc 1.12.5
+      {
+        finder.SetOrthancId(ResourceType_Patient, request[KEY_PARENT_PATIENT].asString());
+      }
+      else if (request.isMember(KEY_PARENT_STUDY))
+      {
+        finder.SetOrthancId(ResourceType_Study, request[KEY_PARENT_STUDY].asString());
+      }
+      else if (request.isMember(KEY_PARENT_SERIES))
+      {
+        finder.SetOrthancId(ResourceType_Series, request[KEY_PARENT_SERIES].asString());
+     }
+
+      if (request.isMember(KEY_ORDER_BY))  // New in Orthanc 1.12.5
+      {
+        for (Json::Value::ArrayIndex i = 0; i < request[KEY_ORDER_BY].size(); i++)
+        {
+          if (request[KEY_ORDER_BY][i].type() != Json::objectValue)
+          {
+            throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY) + "\" must contain objects");
+          }
+          else
+          {
+            const Json::Value& order = request[KEY_ORDER_BY][i];
+            FindRequest::OrderingDirection direction;
+            std::string directionString;
+            std::string typeString;
+
+            if (!order.isMember(KEY_ORDER_BY_KEY) || order[KEY_ORDER_BY_KEY].type() != Json::stringValue)
+            {
+              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_KEY) + "\" must be a string");
+            }
+
+            if (!order.isMember(KEY_ORDER_BY_DIRECTION) || order[KEY_ORDER_BY_DIRECTION].type() != Json::stringValue)
+            {
+              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\"");
+            }
+
+            Toolbox::ToLowerCase(directionString,  order[KEY_ORDER_BY_DIRECTION].asString());
+            if (directionString == "asc")
+            {
+              direction = FindRequest::OrderingDirection_Ascending;
+            }
+            else if (directionString == "desc")
+            {
+              direction = FindRequest::OrderingDirection_Descending;
+            }
+            else
+            {
+              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_DIRECTION) + "\" must be \"ASC\" or \"DESC\"");
+            }
+
+            if (!order.isMember(KEY_ORDER_BY_TYPE) || order[KEY_ORDER_BY_TYPE].type() != Json::stringValue)
+            {
+              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\"");
+            }
+
+            Toolbox::ToLowerCase(typeString, order[KEY_ORDER_BY_TYPE].asString());
+            if (typeString == "dicomtag")
+            {
+              DicomTag tag = FromDcmtkBridge::ParseTag(order[KEY_ORDER_BY_KEY].asString());
+              finder.AddOrdering(tag, direction);
+            }
+            else if (typeString == "metadata")
+            {
+              MetadataType metadata = StringToMetadata(order[KEY_ORDER_BY_KEY].asString());
+              finder.AddOrdering(metadata, direction);
+            }
+            else
+            {
+              throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_ORDER_BY_TYPE) + "\" must be \"DicomTag\" or \"Metadata\"");
+            }
+          }
+        }
+      }
+
       Json::Value answer;
       finder.Execute(answer, context, format, false /* no "Metadata" field */);
       call.GetOutput().AnswerJson(answer);
@@ -3637,7 +3826,7 @@
        * EXPERIMENTAL VERSION
        **/
 
-      ResourceFinder finder(end, expand);
+      ResourceFinder finder(end, (expand ? ResponseContentFlags_ExpandTrue : ResponseContentFlags_ID));
       finder.SetOrthancId(start, call.GetUriComponent("id", ""));
       finder.AddRequestedTags(requestedTags);
 
--- a/OrthancServer/Sources/OrthancWebDav.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/OrthancWebDav.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -1142,7 +1142,7 @@
 
       Visitor visitor(resources);
 
-      ResourceFinder finder(ResourceType_Study, false /* no expand */);
+      ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID);
       finder.SetDatabaseLookup(query);
       finder.Execute(visitor, GetContext());
     }
@@ -1220,7 +1220,7 @@
 
       Visitor visitor;
 
-      ResourceFinder finder(ResourceType_Study, false /* no expand */);
+      ResourceFinder finder(ResourceType_Study, ResponseContentFlags_ID);
       finder.SetDatabaseLookup(query);
       finder.Execute(visitor, context_);
 
@@ -1605,7 +1605,7 @@
          * EXPERIMENTAL VERSION
          **/
 
-        ResourceFinder finder(level, false /* don't expand */);
+        ResourceFinder finder(level, ResponseContentFlags_ID);
         finder.SetDatabaseLookup(query);
         finder.SetRetrieveMetadata(true);
 
@@ -1666,7 +1666,7 @@
                              ResourceType level,
                              const DatabaseLookup& query)
   {
-    ResourceFinder finder(level, true /* expand */);
+    ResourceFinder finder(level, ResponseContentFlags_ExpandTrue);
     finder.SetDatabaseLookup(query);
 
     Json::Value expanded;
@@ -1773,7 +1773,7 @@
           /**
            * EXPERIMENTAL VERSION
            **/
-          ResourceFinder finder(ResourceType_Instance, false /* no expand */);
+          ResourceFinder finder(ResourceType_Instance, ResponseContentFlags_ID);
           finder.SetDatabaseLookup(query);
           finder.SetRetrieveMetadata(true);
           finder.SetRetrieveAttachments(true);
@@ -1913,7 +1913,7 @@
 
         DicomDeleteVisitor visitor(context_, level);
 
-        ResourceFinder finder(level, false /* no expand */);
+        ResourceFinder finder(level, ResponseContentFlags_ID);
         finder.SetDatabaseLookup(query);
         finder.Execute(visitor, context_);
         return true;
--- a/OrthancServer/Sources/ResourceFinder.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/ResourceFinder.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -210,15 +210,14 @@
   void ResourceFinder::Expand(Json::Value& target,
                               const FindResponse::Resource& resource,
                               ServerIndex& index,
-                              DicomToJsonFormat format,
-                              bool includeAllMetadata) const
+                              DicomToJsonFormat format) const
   {
     /**
      * This method closely follows "SerializeExpandedResource()" in
      * "ServerContext.cpp" from Orthanc 1.12.4.
      **/
 
-    if (!expand_)
+    if (responseContent_ == ResponseContentFlags_ID)
     {
       throw OrthancException(ErrorCode_BadSequenceOfCalls);
     }
@@ -233,28 +232,31 @@
     target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true);
     target["ID"] = resource.GetIdentifier();
 
-    switch (resource.GetLevel())
+    if (responseContent_ & ResponseContentFlags_Parent)
     {
-      case ResourceType_Patient:
-        break;
+      switch (resource.GetLevel())
+      {
+        case ResourceType_Patient:
+          break;
 
-      case ResourceType_Study:
-        target["ParentPatient"] = resource.GetParentIdentifier();
-        break;
+        case ResourceType_Study:
+          target["ParentPatient"] = resource.GetParentIdentifier();
+          break;
 
-      case ResourceType_Series:
-        target["ParentStudy"] = resource.GetParentIdentifier();
-        break;
+        case ResourceType_Series:
+          target["ParentStudy"] = resource.GetParentIdentifier();
+          break;
 
-      case ResourceType_Instance:
-        target["ParentSeries"] = resource.GetParentIdentifier();
-        break;
+        case ResourceType_Instance:
+          target["ParentSeries"] = resource.GetParentIdentifier();
+          break;
 
-      default:
-        throw OrthancException(ErrorCode_InternalError);
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
     }
 
-    if (resource.GetLevel() != ResourceType_Instance)
+    if ((responseContent_ & ResponseContentFlags_Children) && (resource.GetLevel() != ResourceType_Instance))
     {
       const std::set<std::string>& children = resource.GetChildrenIdentifiers(GetChildResourceType(resource.GetLevel()));
 
@@ -292,52 +294,65 @@
 
       case ResourceType_Series:
       {
-        uint32_t expectedNumberOfInstances;
-        SeriesStatus status = GetSeriesStatus(expectedNumberOfInstances, resource);
-
-        target["Status"] = EnumerationToString(status);
-
-        static const char* const EXPECTED_NUMBER_OF_INSTANCES = "ExpectedNumberOfInstances";
+        if ((responseContent_ & ResponseContentFlags_Status) || (responseContent_ & ResponseContentFlags_MetadataLegacy) )
+        {
+          uint32_t expectedNumberOfInstances;
+          SeriesStatus status = GetSeriesStatus(expectedNumberOfInstances, resource);
+          
+          if (responseContent_ & ResponseContentFlags_Status )
+          {
+            target["Status"] = EnumerationToString(status);
+          }
 
-        if (status == SeriesStatus_Unknown)
-        {
-          target[EXPECTED_NUMBER_OF_INSTANCES] = Json::nullValue;
+          if (responseContent_ & ResponseContentFlags_MetadataLegacy)
+          {
+            static const char* const EXPECTED_NUMBER_OF_INSTANCES = "ExpectedNumberOfInstances";
+
+            if (status == SeriesStatus_Unknown)
+            {
+              target[EXPECTED_NUMBER_OF_INSTANCES] = Json::nullValue;
+            }
+            else
+            {
+              target[EXPECTED_NUMBER_OF_INSTANCES] = expectedNumberOfInstances;
+            }
+          }
         }
-        else
-        {
-          target[EXPECTED_NUMBER_OF_INSTANCES] = expectedNumberOfInstances;
-        }
-
         break;
       }
 
       case ResourceType_Instance:
       {
-        FileInfo info;
-        if (resource.LookupAttachment(info, FileContentType_Dicom))
+        if (responseContent_ & ResponseContentFlags_AttachmentsLegacy)
         {
-          target["FileSize"] = static_cast<Json::UInt64>(info.GetUncompressedSize());
-          target["FileUuid"] = info.GetUuid();
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_InternalError);
+          FileInfo info;
+          if (resource.LookupAttachment(info, FileContentType_Dicom))
+          {
+            target["FileSize"] = static_cast<Json::UInt64>(info.GetUncompressedSize());
+            target["FileUuid"] = info.GetUuid();
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
         }
 
-        static const char* const INDEX_IN_SERIES = "IndexInSeries";
-
-        std::string s;
-        uint32_t indexInSeries;
-        if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_IndexInSeries) &&
-            SerializationToolbox::ParseUnsignedInteger32(indexInSeries, s))
+        if (responseContent_ & ResponseContentFlags_MetadataLegacy)
         {
-          target[INDEX_IN_SERIES] = indexInSeries;
+          static const char* const INDEX_IN_SERIES = "IndexInSeries";
+
+          std::string s;
+          uint32_t indexInSeries;
+          if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_IndexInSeries) &&
+              SerializationToolbox::ParseUnsignedInteger32(indexInSeries, s))
+          {
+            target[INDEX_IN_SERIES] = indexInSeries;
+          }
+          else
+          {
+            target[INDEX_IN_SERIES] = Json::nullValue;
+          }
         }
-        else
-        {
-          target[INDEX_IN_SERIES] = Json::nullValue;
-        }
-
         break;
       }
 
@@ -346,28 +361,40 @@
     }
 
     std::string s;
-    if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_AnonymizedFrom))
+    if (responseContent_ & ResponseContentFlags_MetadataLegacy)
     {
-      target["AnonymizedFrom"] = s;
-    }
-
-    if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_ModifiedFrom))
-    {
-      target["ModifiedFrom"] = s;
-    }
+      if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_AnonymizedFrom))
+      {
+        target["AnonymizedFrom"] = s;
+      }
 
-    if (resource.GetLevel() == ResourceType_Patient ||
-        resource.GetLevel() == ResourceType_Study ||
-        resource.GetLevel() == ResourceType_Series)
-    {
-      target["IsStable"] = !index.IsUnstableResource(resource.GetLevel(), resource.GetInternalId());
+      if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_ModifiedFrom))
+      {
+        target["ModifiedFrom"] = s;
+      }
 
-      if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_LastUpdate))
+      if (resource.GetLevel() == ResourceType_Patient ||
+          resource.GetLevel() == ResourceType_Study ||
+          resource.GetLevel() == ResourceType_Series)
       {
-        target["LastUpdate"] = s;
+        if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_LastUpdate))
+        {
+          target["LastUpdate"] = s;
+        }
       }
     }
 
+    if (responseContent_ & ResponseContentFlags_IsStable)
+    {
+      if (resource.GetLevel() == ResourceType_Patient ||
+          resource.GetLevel() == ResourceType_Study ||
+          resource.GetLevel() == ResourceType_Series)
+      {
+        target["IsStable"] = !index.IsUnstableResource(resource.GetLevel(), resource.GetInternalId());
+      }
+    }
+
+    if (responseContent_ & ResponseContentFlags_MainDicomTags)
     {
       DicomMap allMainDicomTags;
       resource.GetMainDicomTags(allMainDicomTags, resource.GetLevel());
@@ -396,6 +423,7 @@
       }
     }
 
+    if (responseContent_ & ResponseContentFlags_Labels)
     {
       Json::Value labels = Json::arrayValue;
 
@@ -408,7 +436,7 @@
       target["Labels"] = labels;
     }
 
-    if (includeAllMetadata)  // new in Orthanc 1.12.4
+    if (responseContent_ & ResponseContentFlags_Metadata)  // new in Orthanc 1.12.4
     {
       const std::map<MetadataType, std::string>& m = resource.GetMetadata(resource.GetLevel());
 
@@ -421,6 +449,26 @@
 
       target["Metadata"] = metadata;
     }
+
+    if (responseContent_ & ResponseContentFlags_Attachments)  // new in Orthanc 1.12.5
+    {
+      const std::map<FileContentType, FileInfo>& attachments = resource.GetAttachments();
+
+      target["Attachments"] = Json::arrayValue;
+
+      for (std::map<FileContentType, FileInfo>::const_iterator it = attachments.begin(); it != attachments.end(); ++it)
+      {
+        Json::Value attachment = Json::objectValue;    
+        attachment["Uuid"] = it->second.GetUuid();
+        attachment["ContentType"] = it->second.GetContentType();
+        attachment["UncompressedSize"] = Json::Value::UInt64(it->second.GetUncompressedSize());
+        attachment["CompressedSize"] = Json::Value::UInt64(it->second.GetCompressedSize());
+        attachment["UncompressedMD5"] = it->second.GetUncompressedMD5();
+        attachment["CompressedMD5"] = it->second.GetCompressedMD5();
+
+        target["Attachments"].append(attachment);
+      }
+    }
   }
 
 
@@ -473,7 +521,7 @@
 
 
   ResourceFinder::ResourceFinder(ResourceType level,
-                                 bool expand) :
+                                 ResponseContentFlags responseContent) :
     request_(level),
     databaseLimits_(0),
     isSimpleLookup_(true),
@@ -482,7 +530,7 @@
     hasLimitsCount_(false),
     limitsSince_(0),
     limitsCount_(0),
-    expand_(expand),
+    responseContent_(responseContent),
     allowStorageAccess_(true),
     isWarning002Enabled_(false),
     isWarning004Enabled_(false),
@@ -495,40 +543,43 @@
       isWarning005Enabled_ = lock.GetConfiguration().IsWarningEnabled(Warnings_005_RequestingTagFromLowerResourceLevel);
     }
 
-
     UpdateRequestLimits();
 
-    if (expand)
-    {
-      request_.SetRetrieveMainDicomTags(true);
-      request_.SetRetrieveMetadata(true);
-      request_.SetRetrieveLabels(true);
+    request_.SetRetrieveMainDicomTags(responseContent_ & ResponseContentFlags_MainDicomTags);
+    request_.SetRetrieveMetadata((responseContent_ & ResponseContentFlags_Metadata) || (responseContent_ & ResponseContentFlags_MetadataLegacy));
+    request_.SetRetrieveLabels(responseContent_ & ResponseContentFlags_Labels);
 
-      switch (level)
-      {
-        case ResourceType_Patient:
-          request_.GetChildrenSpecification(ResourceType_Study).SetRetrieveIdentifiers(true);
-          break;
+    switch (level)
+    {
+      case ResourceType_Patient:
+        request_.GetChildrenSpecification(ResourceType_Study).SetRetrieveIdentifiers(responseContent_ & ResponseContentFlags_Children);
+        request_.SetRetrieveAttachments(responseContent_ & ResponseContentFlags_Attachments); 
+        break;
+
+      case ResourceType_Study:
+        request_.GetChildrenSpecification(ResourceType_Series).SetRetrieveIdentifiers(responseContent_ & ResponseContentFlags_Children);
+        request_.SetRetrieveParentIdentifier(responseContent_ & ResponseContentFlags_Parent);
+        request_.SetRetrieveAttachments(responseContent_ & ResponseContentFlags_Attachments); 
+        break;
 
-        case ResourceType_Study:
-          request_.GetChildrenSpecification(ResourceType_Series).SetRetrieveIdentifiers(true);
-          request_.SetRetrieveParentIdentifier(true);
-          break;
-
-        case ResourceType_Series:
+      case ResourceType_Series:
+        if (responseContent_ & ResponseContentFlags_Status)
+        {
           request_.GetChildrenSpecification(ResourceType_Instance).AddMetadata(MetadataType_Instance_IndexInSeries); // required for the SeriesStatus
-          request_.GetChildrenSpecification(ResourceType_Instance).SetRetrieveIdentifiers(true);
-          request_.SetRetrieveParentIdentifier(true);
-          break;
+        }
+        request_.GetChildrenSpecification(ResourceType_Instance).SetRetrieveIdentifiers(responseContent_ & ResponseContentFlags_Children);
+        request_.SetRetrieveParentIdentifier(responseContent_ & ResponseContentFlags_Parent);
+        request_.SetRetrieveAttachments(responseContent_ & ResponseContentFlags_Attachments); 
+        break;
 
-        case ResourceType_Instance:
-          request_.SetRetrieveAttachments(true); // for FileSize & FileUuid
-          request_.SetRetrieveParentIdentifier(true);
-          break;
+      case ResourceType_Instance:
+        request_.SetRetrieveAttachments((responseContent_ & ResponseContentFlags_AttachmentsLegacy) // for FileSize & FileUuid
+                                        || (responseContent_ & ResponseContentFlags_Attachments)); 
+        request_.SetRetrieveParentIdentifier(true);
+        break;
 
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
 
@@ -601,7 +652,7 @@
 
     for (size_t i = 0; i < request_.GetDicomTagConstraints().GetSize(); i++)
     {
-      const DatabaseConstraint& constraint = request_.GetDicomTagConstraints().GetConstraint(i);
+      const DatabaseDicomTagConstraint& constraint = request_.GetDicomTagConstraints().GetConstraint(i);
       if (constraint.GetLevel() == request_.GetLevel())
       {
         request_.SetRetrieveMainDicomTags(true);
@@ -1126,10 +1177,10 @@
       virtual void Apply(const FindResponse::Resource& resource,
                          const DicomMap& requestedTags) ORTHANC_OVERRIDE
       {
-        if (that_.expand_)
+        if (that_.responseContent_ != ResponseContentFlags_ID)
         {
           Json::Value item;
-          that_.Expand(item, resource, index_, format_, includeAllMetadata_);
+          that_.Expand(item, resource, index_, format_);
 
           if (hasRequestedTags_)
           {
--- a/OrthancServer/Sources/ResourceFinder.h	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/ResourceFinder.h	Wed Oct 09 11:06:20 2024 +0200
@@ -65,7 +65,7 @@
     bool                             hasLimitsCount_;
     uint64_t                         limitsSince_;
     uint64_t                         limitsCount_;
-    bool                             expand_;
+    ResponseContentFlags             responseContent_;
     bool                             allowStorageAccess_;
     std::set<DicomTag>               requestedTags_;
     std::set<DicomTag>               requestedComputedTags_;
@@ -103,7 +103,7 @@
 
   public:
     ResourceFinder(ResourceType level,
-                   bool expand);
+                   ResponseContentFlags responseContent);
 
     void SetDatabaseLimits(uint64_t limits);
 
@@ -133,6 +133,23 @@
 
     void AddRequestedTags(const std::set<DicomTag>& tags);
 
+    void AddOrdering(const DicomTag& tag,
+                     FindRequest::OrderingDirection direction)
+    {
+      request_.AddOrdering(tag, direction);
+    }
+
+    void AddOrdering(MetadataType metadataType,
+                     FindRequest::OrderingDirection direction)
+    {
+      request_.AddOrdering(metadataType, direction);
+    }
+
+    void AddMetadataConstraint(DatabaseMetadataConstraint* constraint)
+    {
+      request_.AddMetadataConstraint(constraint);
+    }
+
     void SetLabels(const std::set<std::string>& labels)
     {
       request_.SetLabels(labels);
@@ -167,8 +184,7 @@
     void Expand(Json::Value& target,
                 const FindResponse::Resource& resource,
                 ServerIndex& index,
-                DicomToJsonFormat format,
-                bool includeAllMetadata /* Same as: ExpandResourceFlags_IncludeAllMetadata */) const;
+                DicomToJsonFormat format) const;
 
     void Execute(IVisitor& visitor,
                  ServerContext& context) const;
--- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,112 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "DatabaseConstraint.h"
-
-#include "../../../OrthancFramework/Sources/OrthancException.h"
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-#  include "../../Plugins/Engine/PluginsEnumerations.h"
-#endif
-
-#include <boost/lexical_cast.hpp>
-#include <cassert>
-
-
-namespace Orthanc
-{
-  DatabaseConstraint::DatabaseConstraint(ResourceType level,
-                                         const DicomTag& tag,
-                                         bool isIdentifier,
-                                         ConstraintType type,
-                                         const std::vector<std::string>& values,
-                                         bool caseSensitive,
-                                         bool mandatory) :
-    level_(level),
-    tag_(tag),
-    isIdentifier_(isIdentifier),
-    constraintType_(type),
-    values_(values),
-    caseSensitive_(caseSensitive),
-    mandatory_(mandatory)
-  {
-    if (type != ConstraintType_List &&
-        values_.size() != 1)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }      
-
-    
-  const std::string& DatabaseConstraint::GetValue(size_t index) const
-  {
-    if (index >= values_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      return values_[index];
-    }
-  }
-
-
-  const std::string& DatabaseConstraint::GetSingleValue() const
-  {
-    if (values_.size() != 1)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return values_[0];
-    }
-  }
-
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-  void DatabaseConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
-                                            std::vector<const char*>& tmpValues) const
-  {
-    memset(&constraint, 0, sizeof(constraint));
-    
-    tmpValues.resize(values_.size());
-
-    for (size_t i = 0; i < values_.size(); i++)
-    {
-      tmpValues[i] = values_[i].c_str();
-    }
-
-    constraint.level = Plugins::Convert(level_);
-    constraint.tagGroup = tag_.GetGroup();
-    constraint.tagElement = tag_.GetElement();
-    constraint.isIdentifierTag = isIdentifier_;
-    constraint.isCaseSensitive = caseSensitive_;
-    constraint.isMandatory = mandatory_;
-    constraint.type = Plugins::Convert(constraintType_);
-    constraint.valuesCount = values_.size();
-    constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]);
-  }
-#endif    
-}
--- a/OrthancServer/Sources/Search/DatabaseConstraint.h	Wed Oct 02 11:41:01 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
-#include "../ServerEnumerations.h"
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-#  include "../../Plugins/Include/orthanc/OrthancCDatabasePlugin.h"
-#endif
-
-namespace Orthanc
-{
-  class DatabaseConstraint : public boost::noncopyable
-  {
-  private:
-    ResourceType              level_;
-    DicomTag                  tag_;
-    bool                      isIdentifier_;
-    ConstraintType            constraintType_;
-    std::vector<std::string>  values_;
-    bool                      caseSensitive_;
-    bool                      mandatory_;
-
-  public:
-    DatabaseConstraint(ResourceType level,
-                       const DicomTag& tag,
-                       bool isIdentifier,
-                       ConstraintType type,
-                       const std::vector<std::string>& values,
-                       bool caseSensitive,
-                       bool mandatory);
-    
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    const DicomTag& GetTag() const
-    {
-      return tag_;
-    }
-
-    bool IsIdentifier() const
-    {
-      return isIdentifier_;
-    }
-
-    ConstraintType GetConstraintType() const
-    {
-      return constraintType_;
-    }
-
-    size_t GetValuesCount() const
-    {
-      return values_.size();
-    }
-
-    const std::string& GetValue(size_t index) const;
-
-    const std::string& GetSingleValue() const;
-
-    bool IsCaseSensitive() const
-    {
-      return caseSensitive_;
-    }
-
-    bool IsMandatory() const
-    {
-      return mandatory_;
-    }
-
-    bool IsMatch(const DicomMap& dicom) const;
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
-                          std::vector<const char*>& tmpValues) const;
-#endif    
-  };
-}
--- a/OrthancServer/Sources/Search/DatabaseConstraints.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,132 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "../PrecompiledHeadersServer.h"
-#include "DatabaseConstraints.h"
-
-#include "../../../OrthancFramework/Sources/OrthancException.h"
-
-#include <boost/lexical_cast.hpp>
-#include <cassert>
-
-
-namespace Orthanc
-{
-  void DatabaseConstraints::Clear()
-  {
-    for (size_t i = 0; i < constraints_.size(); i++)
-    {
-      assert(constraints_[i] != NULL);
-      delete constraints_[i];
-    }
-
-    constraints_.clear();
-  }
-
-
-  void DatabaseConstraints::AddConstraint(DatabaseConstraint* constraint)
-  {
-    if (constraint == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-    else
-    {
-      constraints_.push_back(constraint);
-    }
-  }
-
-
-  const DatabaseConstraint& DatabaseConstraints::GetConstraint(size_t index) const
-  {
-    if (index >= constraints_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      assert(constraints_[index] != NULL);
-      return *constraints_[index];
-    }
-  }
-
-
-  std::string DatabaseConstraints::Format() const
-  {
-    std::string s;
-
-    for (size_t i = 0; i < constraints_.size(); i++)
-    {
-      assert(constraints_[i] != NULL);
-      const DatabaseConstraint& constraint = *constraints_[i];
-      s += "Constraint " + boost::lexical_cast<std::string>(i) + " at " + EnumerationToString(constraint.GetLevel()) +
-        ": " + constraint.GetTag().Format();
-
-      switch (constraint.GetConstraintType())
-      {
-        case ConstraintType_Equal:
-          s += " == " + constraint.GetSingleValue();
-          break;
-
-        case ConstraintType_SmallerOrEqual:
-          s += " <= " + constraint.GetSingleValue();
-          break;
-
-        case ConstraintType_GreaterOrEqual:
-          s += " >= " + constraint.GetSingleValue();
-          break;
-
-        case ConstraintType_Wildcard:
-          s += " ~~ " + constraint.GetSingleValue();
-          break;
-
-        case ConstraintType_List:
-        {
-          s += " in [ ";
-          bool first = true;
-          for (size_t j = 0; j < constraint.GetValuesCount(); j++)
-          {
-            if (first)
-            {
-              first = false;
-            }
-            else
-            {
-              s += ", ";
-            }
-            s += constraint.GetValue(j);
-          }
-          s += "]";
-          break;
-        }
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
-      s += "\n";
-    }
-
-    return s;
-  }
-}
--- a/OrthancServer/Sources/Search/DatabaseConstraints.h	Wed Oct 02 11:41:01 2024 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2023 Osimis S.A., Belgium
- * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
- * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#pragma once
-
-#include "DatabaseConstraint.h"
-
-#include <deque>
-
-namespace Orthanc
-{
-  class DatabaseConstraints : public boost::noncopyable
-  {
-  private:
-    std::deque<DatabaseConstraint*>  constraints_;
-
-  public:
-    ~DatabaseConstraints()
-    {
-      Clear();
-    }
-
-    void Clear();
-
-    void AddConstraint(DatabaseConstraint* constraint);  // Takes ownership
-
-    bool IsEmpty() const
-    {
-      return constraints_.empty();
-    }
-
-    size_t GetSize() const
-    {
-      return constraints_.size();
-    }
-
-    const DatabaseConstraint& GetConstraint(size_t index) const;
-
-    std::string Format() const;
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraint.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -0,0 +1,112 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "DatabaseDicomTagConstraint.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  include "../../Plugins/Engine/PluginsEnumerations.h"
+#endif
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+
+namespace Orthanc
+{
+  DatabaseDicomTagConstraint::DatabaseDicomTagConstraint(ResourceType level,
+                                                         const DicomTag& tag,
+                                                         bool isIdentifier,
+                                                         ConstraintType type,
+                                                         const std::vector<std::string>& values,
+                                                         bool caseSensitive,
+                                                         bool mandatory) :
+    level_(level),
+    tag_(tag),
+    isIdentifier_(isIdentifier),
+    constraintType_(type),
+    values_(values),
+    caseSensitive_(caseSensitive),
+    mandatory_(mandatory)
+  {
+    if (type != ConstraintType_List &&
+        values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }      
+
+    
+  const std::string& DatabaseDicomTagConstraint::GetValue(size_t index) const
+  {
+    if (index >= values_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return values_[index];
+    }
+  }
+
+
+  const std::string& DatabaseDicomTagConstraint::GetSingleValue() const
+  {
+    if (values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return values_[0];
+    }
+  }
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+  void DatabaseDicomTagConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
+                                                    std::vector<const char*>& tmpValues) const
+  {
+    memset(&constraint, 0, sizeof(constraint));
+    
+    tmpValues.resize(values_.size());
+
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      tmpValues[i] = values_[i].c_str();
+    }
+
+    constraint.level = Plugins::Convert(level_);
+    constraint.tagGroup = tag_.GetGroup();
+    constraint.tagElement = tag_.GetElement();
+    constraint.isIdentifierTag = isIdentifier_;
+    constraint.isCaseSensitive = caseSensitive_;
+    constraint.isMandatory = mandatory_;
+    constraint.type = Plugins::Convert(constraintType_);
+    constraint.valuesCount = values_.size();
+    constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]);
+  }
+#endif    
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraint.h	Wed Oct 09 11:06:20 2024 +0200
@@ -0,0 +1,101 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
+#include "../ServerEnumerations.h"
+#include "IDatabaseConstraint.h"
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  include "../../Plugins/Include/orthanc/OrthancCDatabasePlugin.h"
+#endif
+
+namespace Orthanc
+{
+  class DatabaseDicomTagConstraint : public IDatabaseConstraint
+  {
+  private:
+    ResourceType              level_;
+    DicomTag                  tag_;
+    bool                      isIdentifier_;
+    ConstraintType            constraintType_;
+    std::vector<std::string>  values_;
+    bool                      caseSensitive_;
+    bool                      mandatory_;
+
+  public:
+    DatabaseDicomTagConstraint(ResourceType level,
+                               const DicomTag& tag,
+                               bool isIdentifier,
+                               ConstraintType type,
+                               const std::vector<std::string>& values,
+                               bool caseSensitive,
+                               bool mandatory);
+    
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    const DicomTag& GetTag() const
+    {
+      return tag_;
+    }
+
+    bool IsIdentifier() const
+    {
+      return isIdentifier_;
+    }
+
+    virtual ConstraintType GetConstraintType() const ORTHANC_OVERRIDE
+    {
+      return constraintType_;
+    }
+
+    virtual size_t GetValuesCount() const ORTHANC_OVERRIDE
+    {
+      return values_.size();
+    }
+
+    virtual const std::string& GetValue(size_t index) const ORTHANC_OVERRIDE;
+
+    virtual const std::string& GetSingleValue() const ORTHANC_OVERRIDE;
+
+    virtual bool IsCaseSensitive() const ORTHANC_OVERRIDE
+    {
+      return caseSensitive_;
+    }
+
+    virtual bool IsMandatory() const ORTHANC_OVERRIDE
+    {
+      return mandatory_;
+    }
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
+                          std::vector<const char*>& tmpValues) const;
+#endif    
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraints.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -0,0 +1,132 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "DatabaseDicomTagConstraints.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+
+namespace Orthanc
+{
+  void DatabaseDicomTagConstraints::Clear()
+  {
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      delete constraints_[i];
+    }
+
+    constraints_.clear();
+  }
+
+
+  void DatabaseDicomTagConstraints::AddConstraint(DatabaseDicomTagConstraint* constraint)
+  {
+    if (constraint == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      constraints_.push_back(constraint);
+    }
+  }
+
+
+  const DatabaseDicomTagConstraint& DatabaseDicomTagConstraints::GetConstraint(size_t index) const
+  {
+    if (index >= constraints_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(constraints_[index] != NULL);
+      return *constraints_[index];
+    }
+  }
+
+
+  std::string DatabaseDicomTagConstraints::Format() const
+  {
+    std::string s;
+
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      const DatabaseDicomTagConstraint& constraint = *constraints_[i];
+      s += "Constraint " + boost::lexical_cast<std::string>(i) + " at " + EnumerationToString(constraint.GetLevel()) +
+        ": " + constraint.GetTag().Format();
+
+      switch (constraint.GetConstraintType())
+      {
+        case ConstraintType_Equal:
+          s += " == " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_SmallerOrEqual:
+          s += " <= " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_GreaterOrEqual:
+          s += " >= " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_Wildcard:
+          s += " ~~ " + constraint.GetSingleValue();
+          break;
+
+        case ConstraintType_List:
+        {
+          s += " in [ ";
+          bool first = true;
+          for (size_t j = 0; j < constraint.GetValuesCount(); j++)
+          {
+            if (first)
+            {
+              first = false;
+            }
+            else
+            {
+              s += ", ";
+            }
+            s += constraint.GetValue(j);
+          }
+          s += "]";
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      s += "\n";
+    }
+
+    return s;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseDicomTagConstraints.h	Wed Oct 09 11:06:20 2024 +0200
@@ -0,0 +1,61 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "DatabaseDicomTagConstraint.h"
+
+#include <deque>
+
+namespace Orthanc
+{
+  class DatabaseDicomTagConstraints : public boost::noncopyable
+  {
+  private:
+    std::deque<DatabaseDicomTagConstraint*>  constraints_;
+
+  public:
+    ~DatabaseDicomTagConstraints()
+    {
+      Clear();
+    }
+
+    void Clear();
+
+    void AddConstraint(DatabaseDicomTagConstraint* constraint);  // Takes ownership
+
+    bool IsEmpty() const
+    {
+      return constraints_.empty();
+    }
+
+    size_t GetSize() const
+    {
+      return constraints_.size();
+    }
+
+    const DatabaseDicomTagConstraint& GetConstraint(size_t index) const;
+
+    std::string Format() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseMetadataConstraint.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -0,0 +1,93 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "DatabaseMetadataConstraint.h"
+
+#include "../../../OrthancFramework/Sources/OrthancException.h"
+
+#include <boost/lexical_cast.hpp>
+#include <cassert>
+
+
+namespace Orthanc
+{
+  DatabaseMetadataConstraint::DatabaseMetadataConstraint(MetadataType metadata,
+                                                         ConstraintType type,
+                                                         const std::vector<std::string>& values,
+                                                         bool caseSensitive) :
+    metadata_(metadata),
+    constraintType_(type),
+    values_(values),
+    caseSensitive_(caseSensitive)
+  {
+    if (type != ConstraintType_List &&
+        values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }      
+
+
+  DatabaseMetadataConstraint::DatabaseMetadataConstraint(MetadataType metadata,
+                                                         ConstraintType type,
+                                                         const std::string& value,
+                                                         bool caseSensitive) :
+    metadata_(metadata),
+    constraintType_(type),
+    caseSensitive_(caseSensitive)
+  {
+    if (type == ConstraintType_List)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    values_.push_back(value);
+  }      
+
+  const std::string& DatabaseMetadataConstraint::GetValue(size_t index) const
+  {
+    if (index >= values_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return values_[index];
+    }
+  }
+
+
+  const std::string& DatabaseMetadataConstraint::GetSingleValue() const
+  {
+    if (values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return values_[0];
+    }
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseMetadataConstraint.h	Wed Oct 09 11:06:20 2024 +0200
@@ -0,0 +1,81 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
+#include "../ServerEnumerations.h"
+#include "IDatabaseConstraint.h"
+
+namespace Orthanc
+{
+  class DatabaseMetadataConstraint : public IDatabaseConstraint
+  {
+  private:
+    MetadataType              metadata_;
+    ConstraintType            constraintType_;
+    std::vector<std::string>  values_;
+    bool                      caseSensitive_;
+
+  public:
+    DatabaseMetadataConstraint(MetadataType metadata,
+                               ConstraintType type,
+                               const std::string& value,
+                               bool caseSensitive);
+
+    DatabaseMetadataConstraint(MetadataType metadata,
+                               ConstraintType type,
+                               const std::vector<std::string>& values,
+                               bool caseSensitive);
+
+    const MetadataType& GetMetadata() const
+    {
+      return metadata_;
+    }
+
+    virtual ConstraintType GetConstraintType() const ORTHANC_OVERRIDE
+    {
+      return constraintType_;
+    }
+
+    virtual size_t GetValuesCount() const ORTHANC_OVERRIDE
+    {
+      return values_.size();
+    }
+
+    virtual const std::string& GetValue(size_t index) const ORTHANC_OVERRIDE;
+
+    virtual const std::string& GetSingleValue() const ORTHANC_OVERRIDE;
+
+    virtual bool IsCaseSensitive() const ORTHANC_OVERRIDE
+    {
+      return caseSensitive_;
+    }
+
+    virtual bool IsMandatory() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+  };
+}
--- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -30,7 +30,7 @@
 
 #include "../../../OrthancFramework/Sources/OrthancException.h"
 #include "../../../OrthancFramework/Sources/Toolbox.h"
-#include "DatabaseConstraint.h"
+#include "DatabaseDicomTagConstraint.h"
 
 #include <boost/regex.hpp>
 
@@ -154,7 +154,7 @@
   }
     
 
-  DicomTagConstraint::DicomTagConstraint(const DatabaseConstraint& constraint) :
+  DicomTagConstraint::DicomTagConstraint(const DatabaseDicomTagConstraint& constraint) :
     tag_(constraint.GetTag()),
     constraintType_(constraint.GetConstraintType()),
     caseSensitive_(constraint.IsCaseSensitive()),
@@ -369,9 +369,9 @@
   }
 
 
-  DatabaseConstraint* DicomTagConstraint::ConvertToDatabaseConstraint(bool& isIdentical,
-                                                                      ResourceType level,
-                                                                      DicomTagType tagType) const
+  DatabaseDicomTagConstraint* DicomTagConstraint::ConvertToDatabaseConstraint(bool& isIdentical,
+                                                                              ResourceType level,
+                                                                              DicomTagType tagType) const
   {
     bool isIdentifier, caseSensitive;
     
@@ -415,7 +415,7 @@
       }
     }
 
-    return new DatabaseConstraint(level, tag_, isIdentifier, constraintType_,
-                                  values, caseSensitive, mandatory_);
+    return new DatabaseDicomTagConstraint(level, tag_, isIdentifier, constraintType_,
+                                          values, caseSensitive, mandatory_);
   }  
 }
--- a/OrthancServer/Sources/Search/DicomTagConstraint.h	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.h	Wed Oct 09 11:06:20 2024 +0200
@@ -25,7 +25,7 @@
 
 #include "../ServerEnumerations.h"
 #include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
-#include "DatabaseConstraint.h"
+#include "DatabaseDicomTagConstraint.h"
 
 #include <boost/shared_ptr.hpp>
 
@@ -62,7 +62,7 @@
 
     explicit DicomTagConstraint(const DicomTagConstraint& other);
     
-    explicit DicomTagConstraint(const DatabaseConstraint& constraint);
+    explicit DicomTagConstraint(const DatabaseDicomTagConstraint& constraint);
 
     const DicomTag& GetTag() const
     {
@@ -109,8 +109,8 @@
 
     std::string Format() const;
 
-    DatabaseConstraint* ConvertToDatabaseConstraint(bool& isIdentical /* out */,
-                                                    ResourceType level,
-                                                    DicomTagType tagType) const;
+    DatabaseDicomTagConstraint* ConvertToDatabaseConstraint(bool& isIdentical /* out */,
+                                                            ResourceType level,
+                                                            DicomTagType tagType) const;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/IDatabaseConstraint.h	Wed Oct 09 11:06:20 2024 +0200
@@ -0,0 +1,50 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../ServerEnumerations.h"
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IDatabaseConstraint : public boost::noncopyable
+  {
+  public:
+    virtual ~IDatabaseConstraint()
+    {
+    }
+    
+    virtual ConstraintType GetConstraintType() const = 0;
+
+    virtual size_t GetValuesCount() const = 0;
+
+    virtual const std::string& GetValue(size_t index) const = 0;
+
+    virtual const std::string& GetSingleValue() const = 0;
+
+    virtual bool IsCaseSensitive() const  = 0;
+
+    virtual bool IsMandatory() const  = 0;
+  };
+}
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -27,7 +27,8 @@
 #include "../../../OrthancFramework/Sources/OrthancException.h"
 #include "../../../OrthancFramework/Sources/Toolbox.h"
 #include "../Database/FindRequest.h"
-#include "DatabaseConstraint.h"
+#include "DatabaseDicomTagConstraint.h"
+#include "../Database/MainDicomTagsRegistry.h"
 
 #include <cassert>
 #include <boost/lexical_cast.hpp>
@@ -56,11 +57,32 @@
         throw OrthancException(ErrorCode_InternalError);
     }
   }      
-  
+
+  static std::string FormatLevel(const char* prefix, ResourceType level)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return std::string(prefix) + "patients";
+        
+      case ResourceType_Study:
+        return std::string(prefix) + "studies";
+        
+      case ResourceType_Series:
+        return std::string(prefix) + "series";
+        
+      case ResourceType_Instance:
+        return std::string(prefix) + "instances";
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }      
+
 
   static bool FormatComparison(std::string& target,
                                ISqlLookupFormatter& formatter,
-                               const DatabaseConstraint& constraint,
+                               const IDatabaseConstraint& constraint,
                                size_t index,
                                bool escapeBrackets)
   {
@@ -232,7 +254,7 @@
 
 
   static void FormatJoin(std::string& target,
-                         const DatabaseConstraint& constraint,
+                         const DatabaseDicomTagConstraint& constraint,
                          size_t index)
   {
     std::string tag = "t" + boost::lexical_cast<std::string>(index);
@@ -262,6 +284,99 @@
                boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
   }
 
+  static void FormatJoin(std::string& target,
+                         const DatabaseMetadataConstraint& constraint,
+                         ResourceType level,
+                         size_t index)
+  {
+    std::string tag = "t" + boost::lexical_cast<std::string>(index);
+
+    if (constraint.IsMandatory())
+    {
+      target = " INNER JOIN ";
+    }
+    else
+    {
+      target = " LEFT JOIN ";
+    }
+
+    target += "Metadata ";
+
+    target += tag + " ON " + tag + ".id = " + FormatLevel(level) +
+               ".internalId AND " + tag + ".type = " +
+               boost::lexical_cast<std::string>(constraint.GetMetadata());
+  }
+
+
+  static void FormatJoinForOrdering(std::string& target,
+                                    const DicomTag& tag,
+                                    size_t index,
+                                    ResourceType requestLevel)
+  {
+    std::string orderArg = "order" + boost::lexical_cast<std::string>(index);
+
+    target.clear();
+
+    ResourceType tagLevel;
+    DicomTagType tagType;
+    MainDicomTagsRegistry registry;
+
+    registry.LookupTag(tagLevel, tagType, tag);
+
+    if (tagLevel == ResourceType_Patient && requestLevel == ResourceType_Study)
+    { // Patient tags are copied at study level
+      tagLevel = ResourceType_Study;
+    }
+
+    std::string tagTable;
+    if (tagType == DicomTagType_Identifier)
+    {
+      tagTable = "DicomIdentifiers ";
+    }
+    else
+    {
+      tagTable = "MainDicomTags ";
+    }
+
+    std::string tagFilter = orderArg + ".tagGroup = " + boost::lexical_cast<std::string>(tag.GetGroup()) + " AND " + orderArg + ".tagElement = " + boost::lexical_cast<std::string>(tag.GetElement());
+
+    if (tagLevel == requestLevel)
+    {
+      target = " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + FormatLevel(requestLevel) +
+                ".internalId AND " + tagFilter;
+    }
+    else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 1)
+    {
+      target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId "
+               " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "parent.internalId AND " + tagFilter;
+    }
+    else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 2)
+    {
+      target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId "
+               " INNER JOIN Resources " + orderArg + "grandparent ON " + orderArg + "grandparent.internalId = " + orderArg + "parent.parentId "
+               " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "grandparent.internalId AND " + tagFilter;
+    }
+    else if (static_cast<int32_t>(requestLevel) - static_cast<int32_t>(tagLevel) == 3)
+    {
+      target = " INNER JOIN Resources " + orderArg + "parent ON " + orderArg + "parent.internalId = " + FormatLevel(requestLevel) + ".parentId "
+               " INNER JOIN Resources " + orderArg + "grandparent ON " + orderArg + "grandparent.internalId = " + orderArg + "parent.parentId "
+               " INNER JOIN Resources " + orderArg + "grandgrandparent ON " + orderArg + "grandgrandparent.internalId = " + orderArg + "grandparent.parentId "
+               " LEFT JOIN " + tagTable + " " + orderArg + " ON " + orderArg + ".id = " + orderArg + "grandgrandparent.internalId AND " + tagFilter;
+    }
+  }
+
+  static void FormatJoinForOrdering(std::string& target,
+                                    const MetadataType& metadata,
+                                    size_t index,
+                                    ResourceType requestLevel)
+  {
+    std::string arg = "order" + boost::lexical_cast<std::string>(index);
+
+
+    target = " INNER JOIN Metadata " + arg + " ON " + arg + ".id = " + FormatLevel(requestLevel) +
+             ".internalId AND " + arg + ".type = " +
+             boost::lexical_cast<std::string>(metadata);
+  }
 
   static std::string Join(const std::list<std::string>& values,
                           const std::string& prefix,
@@ -296,7 +411,7 @@
 
   static bool FormatComparison2(std::string& target,
                                 ISqlLookupFormatter& formatter,
-                                const DatabaseConstraint& constraint,
+                                const DatabaseDicomTagConstraint& constraint,
                                 bool escapeBrackets)
   {
     std::string comparison;
@@ -466,7 +581,7 @@
   void ISqlLookupFormatter::GetLookupLevels(ResourceType& lowerLevel,
                                             ResourceType& upperLevel,
                                             const ResourceType& queryLevel,
-                                            const DatabaseConstraints& lookup)
+                                            const DatabaseDicomTagConstraints& lookup)
   {
     assert(ResourceType_Patient < ResourceType_Study &&
            ResourceType_Study < ResourceType_Series &&
@@ -494,12 +609,13 @@
 
   void ISqlLookupFormatter::Apply(std::string& sql,
                                   ISqlLookupFormatter& formatter,
-                                  const DatabaseConstraints& lookup,
+                                  const DatabaseDicomTagConstraints& lookup,
                                   ResourceType queryLevel,
                                   const std::set<std::string>& labels,
                                   LabelsConstraint labelsConstraint,
                                   size_t limit)
   {
+    // get the limit levels of the DICOM Tags lookup
     ResourceType lowerLevel, upperLevel;
     GetLookupLevels(lowerLevel, upperLevel, queryLevel, lookup);
 
@@ -514,7 +630,7 @@
     
     for (size_t i = 0; i < lookup.GetSize(); i++)
     {
-      const DatabaseConstraint& constraint = lookup.GetConstraint(i);
+      const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i);
 
       std::string comparison;
       
@@ -619,8 +735,64 @@
     assert(upperLevel <= queryLevel &&
            queryLevel <= lowerLevel);
 
-    std::string ordering = "row_number() over (order by " + strQueryLevel + ".publicId) as rowNumber";  // we need a default ordering in order to make default queries repeatable when using since&limit
+    std::string ordering;
+    std::string orderingJoins;
+
+    if (request.GetOrdering().size() > 0)
+    {
+      int counter = 0;
+      std::vector<std::string> orderByFields;
+      for (std::deque<FindRequest::Ordering*>::const_iterator it = request.GetOrdering().begin(); it != request.GetOrdering().end(); ++it)
+      {
+        std::string orderingJoin;
+
+        switch ((*it)->GetKeyType())
+        {
+          case FindRequest::KeyType_DicomTag:
+            FormatJoinForOrdering(orderingJoin, (*it)->GetDicomTag(), counter, request.GetLevel());
+            break;
+          case FindRequest::KeyType_Metadata:
+            FormatJoinForOrdering(orderingJoin, (*it)->GetMetadataType(), counter, request.GetLevel());
+            break;
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+        orderingJoins += orderingJoin;
+        
+        std::string orderByField;
 
+#if ORTHANC_SQLITE_VERSION < 3030001
+        // this is a way to push NULL values at the end before "NULLS LAST" was introduced:
+        // first filter by 0/1 and then by the column value itself
+        orderByField += "order" + boost::lexical_cast<std::string>(counter) + ".value IS NULL, ";
+#endif
+        orderByField += "order" + boost::lexical_cast<std::string>(counter) + ".value";
+
+        if ((*it)->GetDirection() == FindRequest::OrderingDirection_Ascending)
+        {
+          orderByField += " ASC";
+        }
+        else
+        {
+          orderByField += " DESC";
+        }
+        orderByFields.push_back(orderByField);
+        ++counter;
+      }
+
+      std::string orderByFieldsString;
+      Toolbox::JoinStrings(orderByFieldsString, orderByFields, ", ");
+
+      ordering = "ROW_NUMBER() OVER (ORDER BY " + orderByFieldsString;
+#if ORTHANC_SQLITE_VERSION >= 3030001
+      ordering += " NULLS LAST";
+#endif
+      ordering += ") AS rowNumber";
+    }
+    else
+    {
+      ordering = "ROW_NUMBER() OVER (ORDER BY " + strQueryLevel + ".publicId) AS rowNumber";  // we need a default ordering in order to make default queries repeatable when using since&limit
+    }
 
     sql = ("SELECT " +
            strQueryLevel + ".publicId, " +
@@ -631,50 +803,76 @@
 
     std::string joins, comparisons;
 
+    // handle parent constraints
     if (request.GetOrthancIdentifiers().IsDefined() && request.GetOrthancIdentifiers().DetectLevel() <= queryLevel)
     {
-      // single child resource matching, there should not be other constraints (at least for now)
-      assert(request.GetDicomTagConstraints().GetSize() == 0);
-      assert(request.GetLabels().size() == 0);
-      assert(request.HasLimits() == false);
+      ResourceType topParentLevel = request.GetOrthancIdentifiers().DetectLevel();
 
-      ResourceType topParentLevel = request.GetOrthancIdentifiers().DetectLevel();
-      const std::string& strTopParentLevel = FormatLevel(topParentLevel);
+      if (topParentLevel == queryLevel)
+      {
+        comparisons += " AND " + FormatLevel(topParentLevel) + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(topParentLevel));
+      }
+      else
+      {
+        comparisons += " AND " + FormatLevel("parent", topParentLevel) + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(topParentLevel));
 
-      comparisons = " AND " + strTopParentLevel + ".publicId = " + formatter.GenerateParameter(request.GetOrthancIdentifiers().GetLevel(topParentLevel));
-
-      for (int level = queryLevel; level > topParentLevel; level--)
-      {
-        sql += (" INNER JOIN Resources " +
-                FormatLevel(static_cast<ResourceType>(level - 1)) + " ON " +
-                FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" +
-                FormatLevel(static_cast<ResourceType>(level)) + ".parentId");
+        for (int level = queryLevel; level > topParentLevel; level--)
+        {
+          joins += " INNER JOIN Resources " +
+                  FormatLevel("parent", static_cast<ResourceType>(level - 1)) + " ON " +
+                  FormatLevel("parent", static_cast<ResourceType>(level - 1)) + ".internalId = ";
+          if (level == queryLevel)
+          {
+            joins += FormatLevel(static_cast<ResourceType>(level)) + ".parentId";
+          }
+          else
+          {
+            joins += FormatLevel("parent", static_cast<ResourceType>(level)) + ".parentId";
+          }
+        }
       }
     }
-    else
+
+    size_t count = 0;
+    
+    const DatabaseDicomTagConstraints& dicomTagsConstraints = request.GetDicomTagConstraints();
+    for (size_t i = 0; i < dicomTagsConstraints.GetSize(); i++)
     {
-      size_t count = 0;
+      const DatabaseDicomTagConstraint& constraint = dicomTagsConstraints.GetConstraint(i);
+
+      std::string comparison;
       
-      const DatabaseConstraints& dicomTagsConstraints = request.GetDicomTagConstraints();
-      for (size_t i = 0; i < dicomTagsConstraints.GetSize(); i++)
+      if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
       {
-        const DatabaseConstraint& constraint = dicomTagsConstraints.GetConstraint(i);
+        std::string join;
+        FormatJoin(join, constraint, count);
+        joins += join;
 
-        std::string comparison;
+        if (!comparison.empty())
+        {
+          comparisons += " AND " + comparison;
+        }
         
-        if (FormatComparison(comparison, formatter, constraint, count, escapeBrackets))
-        {
-          std::string join;
-          FormatJoin(join, constraint, count);
-          joins += join;
+        count ++;
+      }
+    }
 
-          if (!comparison.empty())
-          {
-            comparisons += " AND " + comparison;
-          }
-          
-          count ++;
+    for (std::deque<DatabaseMetadataConstraint*>::const_iterator it = request.GetMetadataConstraint().begin(); it != request.GetMetadataConstraint().end(); ++it)
+    {
+      std::string comparison;
+      
+      if (FormatComparison(comparison, formatter, *(*it), count, escapeBrackets))
+      {
+        std::string join;
+        FormatJoin(join, *(*it), request.GetLevel(), count);
+        joins += join;
+
+        if (!comparison.empty())
+        {
+          comparisons += " AND " + comparison;
         }
+        
+        count ++;
       }
     }
 
@@ -738,7 +936,7 @@
                       ".internalId AND selectedLabels.label IN (" + Join(formattedLabels, "", ", ") + ")) " + condition);
     }
 
-    sql += joins + Join(where, " WHERE ", " AND ");
+    sql += joins + orderingJoins + Join(where, " WHERE ", " AND ");
 
     if (request.HasLimits())
     {
@@ -749,7 +947,7 @@
 
   void ISqlLookupFormatter::ApplySingleLevel(std::string& sql,
                                              ISqlLookupFormatter& formatter,
-                                             const DatabaseConstraints& lookup,
+                                             const DatabaseDicomTagConstraints& lookup,
                                              ResourceType queryLevel,
                                              const std::set<std::string>& labels,
                                              LabelsConstraint labelsConstraint,
@@ -768,7 +966,7 @@
 
     for (size_t i = 0; i < lookup.GetSize(); i++)
     {
-      const DatabaseConstraint& constraint = lookup.GetConstraint(i);
+      const DatabaseDicomTagConstraint& constraint = lookup.GetConstraint(i);
 
       std::string comparison;
       
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Wed Oct 09 11:06:20 2024 +0200
@@ -30,7 +30,7 @@
 
 namespace Orthanc
 {
-  class DatabaseConstraints;
+  class DatabaseDicomTagConstraints;
   class FindRequest;
 
   enum LabelsConstraint
@@ -65,11 +65,11 @@
     static void GetLookupLevels(ResourceType& lowerLevel,
                                 ResourceType& upperLevel,
                                 const ResourceType& queryLevel,
-                                const DatabaseConstraints& lookup);
+                                const DatabaseDicomTagConstraints& lookup);
 
     static void Apply(std::string& sql,
                       ISqlLookupFormatter& formatter,
-                      const DatabaseConstraints& lookup,
+                      const DatabaseDicomTagConstraints& lookup,
                       ResourceType queryLevel,
                       const std::set<std::string>& labels,  // New in Orthanc 1.12.0
                       LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
@@ -77,7 +77,7 @@
 
     static void ApplySingleLevel(std::string& sql,
                                  ISqlLookupFormatter& formatter,
-                                 const DatabaseConstraints& lookup,
+                                 const DatabaseDicomTagConstraints& lookup,
                                  ResourceType queryLevel,
                                  const std::set<std::string>& labels,  // New in Orthanc 1.12.0
                                  LabelsConstraint labelsConstraint,    // New in Orthanc 1.12.0
--- a/OrthancServer/Sources/ServerEnumerations.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -616,4 +616,45 @@
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
   }
-}
+
+  ResponseContentFlags StringToResponseContent(const std::string& value)
+  {
+    if (value == "MainDicomTags")
+    {
+      return ResponseContentFlags_MainDicomTags;
+    }
+    else if (value == "Metadata")
+    {
+      return ResponseContentFlags_Metadata;
+    }
+    else if (value == "Status")
+    {
+      return ResponseContentFlags_Status;
+    }
+    else if (value == "Parent")
+    {
+      return ResponseContentFlags_Parent;
+    }
+    else if (value == "Children")
+    {
+      return ResponseContentFlags_Children;
+    }
+    else if (value == "Labels")
+    {
+      return ResponseContentFlags_Labels;
+    }
+    else if (value == "Attachments")
+    {
+      return ResponseContentFlags_Attachments;
+    }
+    else if (value == "IsStable")
+    {
+      return ResponseContentFlags_IsStable;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Unrecognized value for \"ResponseContent\": " + value);
+    }    
+  }
+}
\ No newline at end of file
--- a/OrthancServer/Sources/ServerEnumerations.h	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/Sources/ServerEnumerations.h	Wed Oct 09 11:06:20 2024 +0200
@@ -121,6 +121,40 @@
     ConstraintType_List
   };
 
+  enum ResponseContentFlags
+  {
+    ResponseContentFlags_ID                   = (1 << 0),
+    ResponseContentFlags_Type                 = (1 << 1),
+    ResponseContentFlags_RequestedTags        = (1 << 2),
+    ResponseContentFlags_MainDicomTags        = (1 << 3),
+    ResponseContentFlags_MetadataLegacy       = (1 << 4),    // when "Expand": true -> all metadata are included at root level
+    ResponseContentFlags_AttachmentsLegacy    = (1 << 5),    // when "Expand": true -> include attachments info at instance level
+    ResponseContentFlags_Metadata             = (1 << 6),    // all metadata are listed in a "Metadata" field
+    ResponseContentFlags_Attachments          = (1 << 7),    // all attachments are listed in a "Attachments" field
+    ResponseContentFlags_Status               = (1 << 8),
+    ResponseContentFlags_Parent               = (1 << 9),
+    ResponseContentFlags_Children             = (1 << 10),
+    ResponseContentFlags_Labels               = (1 << 11),
+    ResponseContentFlags_IsStable             = (1 << 12),
+
+    // Some predefined combinations
+    ResponseContentFlags_ExpandTrue  = (ResponseContentFlags_ID |
+                                        ResponseContentFlags_Type |
+                                        ResponseContentFlags_RequestedTags |
+                                        ResponseContentFlags_MainDicomTags |
+                                        ResponseContentFlags_MetadataLegacy |
+                                        ResponseContentFlags_AttachmentsLegacy | 
+                                        ResponseContentFlags_Status | 
+                                        ResponseContentFlags_Parent | 
+                                        ResponseContentFlags_Children | 
+                                        ResponseContentFlags_Labels |
+                                        ResponseContentFlags_IsStable),  // equivalent to "Expand": true
+    
+    ResponseContentFlags_Default = (ResponseContentFlags_ID |
+                                    ResponseContentFlags_Type |
+                                    ResponseContentFlags_RequestedTags) // minimal content as soon as you have a "ResponseContent"
+    
+  };
 
   /**
    * WARNING: Do not change the explicit values in the enumerations
@@ -251,6 +285,8 @@
 
   Verbosity StringToVerbosity(const std::string& str);
 
+  ResponseContentFlags StringToResponseContent(const std::string& str);
+
   std::string EnumerationToString(FileContentType type);
 
   std::string GetFileContentMime(FileContentType type);
--- a/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Wed Oct 02 11:41:01 2024 +0200
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Wed Oct 09 11:06:20 2024 +0200
@@ -166,7 +166,7 @@
       
       DicomTagConstraint c(tag, type, value, true, true);
       
-      DatabaseConstraints lookup;
+      DatabaseDicomTagConstraints lookup;
       bool isEquivalent;  // unused
       lookup.AddConstraint(c.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier));
 
@@ -187,7 +187,7 @@
       DicomTagConstraint c1(tag, type1, value1, true, true);
       DicomTagConstraint c2(tag, type2, value2, true, true);
 
-      DatabaseConstraints lookup;
+      DatabaseDicomTagConstraints lookup;
       bool isEquivalent;  // unused
       lookup.AddConstraint(c1.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier));
       lookup.AddConstraint(c2.ConvertToDatabaseConstraint(isEquivalent, level, DicomTagType_Identifier));