changeset 5568:b0b5546f1b9f find-refactoring

find refactor: re-use existing code. /studies?expand is almost fully implemented with new code
author Alain Mazy <am@orthanc.team>
date Thu, 25 Apr 2024 09:22:07 +0200
parents f3562c1a150d
children 738f80622e91
files NEWS OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Sources/Database/Compatibility/GenericFind.cpp OrthancServer/Sources/Database/Compatibility/GenericFind.h OrthancServer/Sources/Database/FindRequest.cpp OrthancServer/Sources/Database/FindRequest.h OrthancServer/Sources/Database/FindResponse.cpp OrthancServer/Sources/Database/FindResponse.h OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/Search/DatabaseConstraint.cpp OrthancServer/Sources/Search/DatabaseConstraint.h OrthancServer/Sources/Search/ISqlLookupFormatter.h OrthancServer/Sources/Search/LabelsConstraint.h OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h
diffstat 22 files changed, 809 insertions(+), 366 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Apr 23 16:49:44 2024 +0200
+++ b/NEWS	Thu Apr 25 09:22:07 2024 +0200
@@ -16,6 +16,8 @@
 
 * API version upgraded to 24
 * Added "MaximumPatientCount" in /system
+* TODO-FIND: complete the list of updated routes:
+  /studies?expand and sibbling routes now also return "Metadata"
 
 Plugins
 -------
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Thu Apr 25 09:22:07 2024 +0200
@@ -1451,7 +1451,8 @@
 
 
     virtual void ExecuteFind(FindResponse& response,
-                             const FindRequest& request) ORTHANC_OVERRIDE
+                             const FindRequest& request, 
+                             const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE
     {
       Compatibility::GenericFind find(*this);
       find.Execute(response, request);
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp	Thu Apr 25 09:22:07 2024 +0200
@@ -1064,7 +1064,8 @@
 
 
     virtual void ExecuteFind(FindResponse& response,
-                             const FindRequest& request) ORTHANC_OVERRIDE
+                             const FindRequest& request, 
+                             const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE
     {
       Compatibility::GenericFind find(*this);
       find.Execute(response, request);
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp	Thu Apr 25 09:22:07 2024 +0200
@@ -1279,7 +1279,8 @@
 
 
     virtual void ExecuteFind(FindResponse& response,
-                             const FindRequest& request) ORTHANC_OVERRIDE
+                             const FindRequest& request, 
+                             const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE
     {
       Compatibility::GenericFind find(*this);
       find.Execute(response, request);
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Apr 25 09:22:07 2024 +0200
@@ -743,7 +743,7 @@
   } OrthancPluginResourceType;
 
 
-
+  
   /**
    * The supported types of changes that can be signaled to the change callback.
    * @ingroup Callbacks
--- a/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp	Thu Apr 25 09:22:07 2024 +0200
@@ -37,7 +37,8 @@
           !request.GetOrthancIdentifiers().HasStudyId() &&
           !request.GetOrthancIdentifiers().HasSeriesId() &&
           !request.GetOrthancIdentifiers().HasInstanceId() &&
-          request.GetFilterConstraintsCount() == 0 &&
+          request.GetDicomTagConstraintsCount() == 0 &&
+          request.GetMetadataConstraintsCount() == 0 &&
           !request.IsRetrieveTagsAtLevel(ResourceType_Patient) &&
           !request.IsRetrieveTagsAtLevel(ResourceType_Study) &&
           !request.IsRetrieveTagsAtLevel(ResourceType_Series) &&
--- a/OrthancServer/Sources/Database/Compatibility/GenericFind.h	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.h	Thu Apr 25 09:22:07 2024 +0200
@@ -28,6 +28,7 @@
 {
   namespace Compatibility
   {
+    // TODO-FIND: remove this class that only contains a temporary implementation
     class GenericFind : public boost::noncopyable
     {
     private:
--- a/OrthancServer/Sources/Database/FindRequest.cpp	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Database/FindRequest.cpp	Thu Apr 25 09:22:07 2024 +0200
@@ -73,11 +73,6 @@
 
   FindRequest::~FindRequest()
   {
-    for (std::deque<FilterConstraint*>::iterator it = filterConstraints_.begin(); it != filterConstraints_.end(); ++it)
-    {
-      assert(*it != NULL);
-      delete *it;
-    }
 
     for (std::deque<Ordering*>::iterator it = ordering_.begin(); it != ordering_.end(); ++it)
     {
@@ -86,30 +81,20 @@
     }
   }
 
-
-  void FindRequest::AddFilterConstraint(FilterConstraint* constraint /* takes ownership */)
+  void FindRequest::AddDicomTagConstraint(const DicomTagConstraint& constraint)
   {
-    if (constraint == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-    else
-    {
-      filterConstraints_.push_back(constraint);
-    }
+    dicomTagConstraints_.push_back(constraint);
   }
 
-
-  const FindRequest::FilterConstraint& FindRequest::GetFilterConstraint(size_t index) const
+  const DicomTagConstraint& FindRequest::GetDicomTagConstraint(size_t index) const
   {
-    if (index >= filterConstraints_.size())
+    if (index >= dicomTagConstraints_.size())
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
     else
     {
-      assert(filterConstraints_[index] != NULL);
-      return *filterConstraints_[index];
+      return dicomTagConstraints_[index];
     }
   }
 
--- a/OrthancServer/Sources/Database/FindRequest.h	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Database/FindRequest.h	Thu Apr 25 09:22:07 2024 +0200
@@ -25,7 +25,9 @@
 #include "../../../OrthancFramework/Sources/DicomFormat/DicomTag.h"
 #include "../ServerEnumerations.h"
 #include "OrthancIdentifiers.h"
-//#include "../Search/DatabaseConstraint.h"
+#include "../Search/DicomTagConstraint.h"
+#include "../Search/LabelsConstraint.h"
+#include "../Search/DatabaseConstraint.h"
 
 #include <deque>
 #include <map>
@@ -74,18 +76,18 @@
       OrderingDirection_Descending
     };
 
-    enum LabelsConstraint
-    {
-      LabelsConstraint_All,
-      LabelsConstraint_Any,
-      LabelsConstraint_None
-    };
 
     class Key
     {
       KeyType                       type_;
       boost::shared_ptr<DicomTag>   dicomTag_;
       MetadataType                  metadata_;
+      
+      // TODO-FIND: to execute the query, we actually need:
+      // ResourceType level_;
+      // DicomTagType dicomTagType_;
+      // these are however only populated in StatelessDatabaseOperations -> we had to add the normalized lookup arg to ExecuteFind
+
     public:
       Key(const DicomTag& dicomTag) :
         type_(KeyType_DicomTag),
@@ -153,165 +155,175 @@
       }
     };
 
-
-    class FilterConstraint : public boost::noncopyable
-    {
-      Key              key_;
+    // TODO-FIND: this class hierarchy actually adds complexity and is very redundant with DicomTagConstraint.
+    //       e.g, in this class hierarchy, it is difficult to implement an equivalent to DicomTagConstraint::ConvertToDatabaseConstraint
+    //       I have the feeling we can just have a MetadataConstraint in the same way as DicomTagConstraint
+    //       and both convert to a DatabaseConstraint in StatelessDatabaseOperations
+    // class FilterConstraint : public boost::noncopyable
+    // {
+    //   Key              key_;
     
-    protected:
-      FilterConstraint(const Key& key) :
-        key_(key)
-      {
-      }
+    // protected:
+    //   FilterConstraint(const Key& key) :
+    //     key_(key)
+    //   {
+    //   }
 
-    public:
-      virtual ~FilterConstraint()
-      {
-      }
-
-      virtual ConstraintType GetType() const = 0;
-      virtual bool IsCaseSensitive() const = 0;  // Needed for PN VR
-    };
+    // public:
+    //   virtual ~FilterConstraint()
+    //   {
+    //   }
 
+    //   const Key& GetKey() const
+    //   {
+    //     return key_;
+    //   }
 
-    class MandatoryConstraint : public FilterConstraint
-    {
-    public:
-      virtual ConstraintType GetType() const ORTHANC_OVERRIDE
-      {
-        return ConstraintType_Mandatory;
-      }
-    };
+    //   virtual ConstraintType GetType() const = 0;
+    //   virtual bool IsCaseSensitive() const = 0;  // Needed for PN VR
 
 
-    class StringConstraint : public FilterConstraint
-    {
-    private:
-      bool  caseSensitive_;
+    // };
+
 
-    public:
-      StringConstraint(Key key,
-                       bool caseSensitive) :
-        FilterConstraint(key),
-        caseSensitive_(caseSensitive)
-      {
-      }
-
-      bool IsCaseSensitive() const
-      {
-        return caseSensitive_;
-      }
-    };
+    // class MandatoryConstraint : public FilterConstraint
+    // {
+    // public:
+    //   virtual ConstraintType GetType() const ORTHANC_OVERRIDE
+    //   {
+    //     return ConstraintType_Mandatory;
+    //   }
+    // };
 
 
-    class EqualityConstraint : public StringConstraint
-    {
-    private:
-      std::string  value_;
+    // class StringConstraint : public FilterConstraint
+    // {
+    // private:
+    //   bool  caseSensitive_;
 
-    public:
-      explicit EqualityConstraint(Key key,
-                                  bool caseSensitive,
-                                  const std::string& value) :
-        StringConstraint(key, caseSensitive),
-        value_(value)
-      {
-      }
+    // public:
+    //   StringConstraint(Key key,
+    //                    bool caseSensitive) :
+    //     FilterConstraint(key),
+    //     caseSensitive_(caseSensitive)
+    //   {
+    //   }
 
-      virtual ConstraintType GetType() const ORTHANC_OVERRIDE
-      {
-        return ConstraintType_Equality;
-      }
-
-      const std::string& GetValue() const
-      {
-        return value_;
-      }
-    };
+    //   bool IsCaseSensitive() const
+    //   {
+    //     return caseSensitive_;
+    //   }
+    // };
 
 
-    class RangeConstraint : public StringConstraint
-    {
-    private:
-      std::string  start_;
-      std::string  end_;    // Inclusive
+    // class EqualityConstraint : public StringConstraint
+    // {
+    // private:
+    //   std::string  value_;
 
-    public:
-      RangeConstraint(Key key,
-                      bool caseSensitive,
-                      const std::string& start,
-                      const std::string& end) :
-        StringConstraint(key, caseSensitive),
-        start_(start),
-        end_(end)
-      {
-      }
+    // public:
+    //   explicit EqualityConstraint(Key key,
+    //                               bool caseSensitive,
+    //                               const std::string& value) :
+    //     StringConstraint(key, caseSensitive),
+    //     value_(value)
+    //   {
+    //   }
 
-      virtual ConstraintType GetType() const ORTHANC_OVERRIDE
-      {
-        return ConstraintType_Range;
-      }
+    //   virtual ConstraintType GetType() const ORTHANC_OVERRIDE
+    //   {
+    //     return ConstraintType_Equality;
+    //   }
 
-      const std::string& GetStart() const
-      {
-        return start_;
-      }
-
-      const std::string& GetEnd() const
-      {
-        return end_;
-      }
-    };
+    //   const std::string& GetValue() const
+    //   {
+    //     return value_;
+    //   }
+    // };
 
 
-    class WildcardConstraint : public StringConstraint
-    {
-    private:
-      std::string  value_;
+    // class RangeConstraint : public StringConstraint
+    // {
+    // private:
+    //   std::string  start_;
+    //   std::string  end_;    // Inclusive
 
-    public:
-      explicit WildcardConstraint(Key& key,
-                                  bool caseSensitive,
-                                  const std::string& value) :
-        StringConstraint(key, caseSensitive),
-        value_(value)
-      {
-      }
+    // public:
+    //   RangeConstraint(Key key,
+    //                   bool caseSensitive,
+    //                   const std::string& start,
+    //                   const std::string& end) :
+    //     StringConstraint(key, caseSensitive),
+    //     start_(start),
+    //     end_(end)
+    //   {
+    //   }
 
-      virtual ConstraintType GetType() const ORTHANC_OVERRIDE
-      {
-        return ConstraintType_Wildcard;
-      }
+    //   virtual ConstraintType GetType() const ORTHANC_OVERRIDE
+    //   {
+    //     return ConstraintType_Range;
+    //   }
 
-      const std::string& GetValue() const
-      {
-        return value_;
-      }
-    };
+    //   const std::string& GetStart() const
+    //   {
+    //     return start_;
+    //   }
+
+    //   const std::string& GetEnd() const
+    //   {
+    //     return end_;
+    //   }
+    // };
 
 
-    class ListConstraint : public StringConstraint
-    {
-    private:
-      std::set<std::string>  values_;
+    // class WildcardConstraint : public StringConstraint
+    // {
+    // private:
+    //   std::string  value_;
+
+    // public:
+    //   explicit WildcardConstraint(Key& key,
+    //                               bool caseSensitive,
+    //                               const std::string& value) :
+    //     StringConstraint(key, caseSensitive),
+    //     value_(value)
+    //   {
+    //   }
+
+    //   virtual ConstraintType GetType() const ORTHANC_OVERRIDE
+    //   {
+    //     return ConstraintType_Wildcard;
+    //   }
 
-    public:
-      ListConstraint(Key key,
-                     bool caseSensitive) :
-        StringConstraint(key, caseSensitive)
-      {
-      }
+    //   const std::string& GetValue() const
+    //   {
+    //     return value_;
+    //   }
+    // };
+
+
+    // class ListConstraint : public StringConstraint
+    // {
+    // private:
+    //   std::set<std::string>  values_;
 
-      virtual ConstraintType GetType() const ORTHANC_OVERRIDE
-      {
-        return ConstraintType_List;
-      }
+    // public:
+    //   ListConstraint(Key key,
+    //                  bool caseSensitive) :
+    //     StringConstraint(key, caseSensitive)
+    //   {
+    //   }
 
-      const std::set<std::string>& GetValues() const
-      {
-        return values_;
-      }
-    };
+    //   virtual ConstraintType GetType() const ORTHANC_OVERRIDE
+    //   {
+    //     return ConstraintType_List;
+    //   }
+
+    //   const std::set<std::string>& GetValues() const
+    //   {
+    //     return values_;
+    //   }
+    // };
 
 
   private:
@@ -319,7 +331,9 @@
     // filter & ordering fields
     ResourceType                         level_;                // The level of the response (the filtering on tags, labels and metadata also happens at this level)
     OrthancIdentifiers                   orthancIdentifiers_;   // The response must belong to this Orthanc resources hierarchy
-    std::deque<FilterConstraint*>        filterConstraints_;    // All tags and metadata filters (note: the order is not important)
+    // std::deque<FilterConstraint*>        filterConstraints_;    // All tags and metadata filters (note: the order is not important)
+    std::vector<DicomTagConstraint>      dicomTagConstraints_;  // All tags filters (note: the order is not important)
+    std::deque<void*>   /* TODO-FIND */       metadataConstraints_;  // All metadata filters (note: the order is not important)
     bool                                 hasLimits_;
     uint64_t                             limitsSince_;
     uint64_t                             limitsCount_;
@@ -348,9 +362,6 @@
       return level_;
     }
 
-    // void GetDatabaseConstraints(std::vector<DatabaseConstraint>& target) const;  // conversion to DatabaseConstraint is required to feed to the LookupFormatter
-    // void GetOrdering(std::vector<Ordering>& target) const;
-
 
     void SetResponseContent(ResponseContent content)
     {
@@ -372,6 +383,11 @@
       return (responseContent_ & content) == content;
     }
 
+    bool IsResponseIdentifiersOnly() const
+    {
+      return responseContent_ == ResponseContent_IdentifiersOnly;
+    }
+
     void SetOrthancPatientId(const std::string& id)
     {
       orthancIdentifiers_.SetPatientId(id);
@@ -397,14 +413,20 @@
       return orthancIdentifiers_;
     }
 
-    void AddFilterConstraint(FilterConstraint* constraint /* takes ownership */);
+
+    void AddDicomTagConstraint(const DicomTagConstraint& constraint);
 
-    size_t GetFilterConstraintsCount() const
+    size_t GetDicomTagConstraintsCount() const
     {
-      return filterConstraints_.size();
+      return dicomTagConstraints_.size();
     }
 
-    const FilterConstraint& GetFilterConstraint(size_t index) const;
+    size_t GetMetadataConstraintsCount() const
+    {
+      return metadataConstraints_.size();
+    }
+
+    const DicomTagConstraint& GetDicomTagConstraint(size_t index) const;
 
     void SetLimits(uint64_t since,
                    uint64_t count);
@@ -442,5 +464,10 @@
     {
       return labels_;
     }
+
+    LabelsConstraint GetLabelsConstraint() const
+    {
+      return labelsContraint_;
+    }
   };
 }
--- a/OrthancServer/Sources/Database/FindResponse.cpp	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Database/FindResponse.cpp	Thu Apr 25 09:22:07 2024 +0200
@@ -133,8 +133,7 @@
 
 
   void FindResponse::Item::AddMetadata(MetadataType metadata,
-                                       const std::string& value,
-                                       int64_t revision)
+                                       const std::string& value)
   {
     if (metadata_.find(metadata) != metadata_.end())
     {
@@ -142,16 +141,15 @@
     }
     else
     {
-      metadata_[metadata] = StringWithRevision(value, revision);
+      metadata_[metadata] = value;
     }
   }
 
 
   bool FindResponse::Item::LookupMetadata(std::string& value,
-                                          int64_t revision,
                                           MetadataType metadata) const
   {
-    std::map<MetadataType, StringWithRevision>::const_iterator found = metadata_.find(metadata);
+    std::map<MetadataType, std::string>::const_iterator found = metadata_.find(metadata);
 
     if (found == metadata_.end())
     {
@@ -159,8 +157,7 @@
     }
     else
     {
-      value = found->second.GetValue();
-      revision = found->second.GetRevision();
+      value = found->second;
       return true;
     }
   }
@@ -170,7 +167,7 @@
   {
     target.clear();
 
-    for (std::map<MetadataType, StringWithRevision>::const_iterator it = metadata_.begin(); it != metadata_.end(); ++it)
+    for (std::map<MetadataType, std::string>::const_iterator it = metadata_.begin(); it != metadata_.end(); ++it)
     {
       target.insert(it->first);
     }
@@ -225,4 +222,21 @@
       return *items_[index];
     }
   }
+
+  void FindResponse::Item::AddDicomTag(uint16_t group, uint16_t element, const std::string& value, bool isBinary)
+  {
+    if (dicomMap_.get() == NULL)
+    {
+      dicomMap_.reset(new DicomMap());
+    }
+
+    dicomMap_->SetValue(group, element, value, isBinary);
+  }
+
+  void FindResponse::Item::AddChild(const std::string& childId)
+  {
+    children_.push_back(childId);
+  }
+
+
 }
--- a/OrthancServer/Sources/Database/FindResponse.h	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Database/FindResponse.h	Thu Apr 25 09:22:07 2024 +0200
@@ -39,6 +39,8 @@
   class FindResponse : public boost::noncopyable
   {
   public:
+
+    // TODO-FIND: does it actually make sense to retrieve revisions for metadata and attachments ?
     class StringWithRevision
     {
     private:
@@ -80,13 +82,15 @@
     private:
       FindRequest::ResponseContent          responseContent_;    // what has been requested
       ResourceType                          level_;
-      OrthancIdentifiers                    identifiers_;
+      std::string                           resourceId_;
+      std::string                           parent_;
+      OrthancIdentifiers                    identifiers_;  // TODO-FIND: not convenient to use here.  A simple resourceId seems enough
       std::unique_ptr<DicomMap>             dicomMap_;
       std::list<std::string>                children_;
       std::string                           childInstanceId_;
-      std::list<std::string>                labels_;      
-      std::map<MetadataType, StringWithRevision>    metadata_;
-      std::map<uint16_t, StringWithRevision>        attachments_;
+      std::set<std::string>                 labels_;      
+      std::map<MetadataType, std::string>   metadata_;
+      std::map<uint16_t, std::string>       attachments_;
 
     public:
       Item(FindRequest::ResponseContent responseContent,
@@ -100,6 +104,15 @@
 
       Item(FindRequest::ResponseContent responseContent,
            ResourceType level,
+           const std::string& resourceId) :
+        responseContent_(responseContent),
+        level_(level),
+        resourceId_(resourceId)
+      {
+      }
+
+      Item(FindRequest::ResponseContent responseContent,
+           ResourceType level,
            DicomMap* dicomMap /* takes ownership */);
 
       ResourceType GetLevel() const
@@ -107,21 +120,43 @@
         return level_;
       }
 
+      const std::string& GetResourceId() const
+      {
+        return resourceId_;
+      }
+
       const OrthancIdentifiers& GetIdentifiers() const
       {
         return identifiers_;
       }
 
+      FindRequest::ResponseContent GetResponseContent() const
+      {
+        return responseContent_;
+      }
+
+      bool HasResponseContent(FindRequest::ResponseContent content) const
+      {
+        return (responseContent_ & content) == content;
+      }
+
+      void AddDicomTag(uint16_t group, uint16_t element, const std::string& value, bool isBinary);
+
       void AddMetadata(MetadataType metadata,
-                       const std::string& value,
-                       int64_t revision);
+                       const std::string& value);
+                       //int64_t revision);
+
+      const std::map<MetadataType, std::string>& GetMetadata() const
+      {
+        return metadata_;
+      }
 
       bool HasMetadata(MetadataType metadata) const
       {
         return metadata_.find(metadata) != metadata_.end();
       }
 
-      bool LookupMetadata(std::string& value, int64_t revision,
+      bool LookupMetadata(std::string& value, /* int64_t revision, */
                           MetadataType metadata) const;
 
       void ListMetadata(std::set<MetadataType>& metadata) const;
@@ -133,8 +168,33 @@
 
       const DicomMap& GetDicomMap() const;
 
+      void AddChild(const std::string& childId);
 
-      // TODO: add other getters and setters
+      const std::list<std::string>& GetChildren() const
+      {
+        return children_;
+      }
+
+      void SetParent(const std::string& parent)
+      {
+        parent_ = parent;
+      }
+
+      const std::string& GetParent() const
+      {
+        return parent_;
+      }
+
+      void AddLabel(const std::string& label)
+      {
+        labels_.insert(label);
+      }
+
+      const std::set<std::string>& GetLabels() const
+      {
+        return labels_;
+      }
+      // TODO-FIND: add other getters and setters
     };
 
   private:
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Thu Apr 25 09:22:07 2024 +0200
@@ -358,7 +358,8 @@
        **/
 
       virtual void ExecuteFind(FindResponse& response,
-                               const FindRequest& request) = 0;
+                               const FindRequest& request,
+                               const std::vector<DatabaseConstraint>& normalized) = 0;
     };
 
 
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Thu Apr 25 09:22:07 2024 +0200
@@ -1141,9 +1141,10 @@
 
 
     virtual void ExecuteFind(FindResponse& response,
-                             const FindRequest& request) ORTHANC_OVERRIDE
+                             const FindRequest& request, 
+                             const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE
     {
-#if 1
+#if 0
       Compatibility::GenericFind find(*this);
       find.Execute(response, request);
 #else
@@ -1153,11 +1154,142 @@
       }
 
       {
-        std::string sql;
-        // sql = "CREATE TEMPORARY TABLE FilteredResourcesIds AS ";
-        sql = "..";
-        SQLite::Statement s(db_, SQLITE_FROM_HERE_DYNAMIC(sql), sql);
+
+        LookupFormatter formatter;
+
+        std::string sqlLookup;
+        LookupFormatter::Apply(sqlLookup, 
+                               formatter, 
+                               normalized, 
+                               request.GetLevel(),
+                               request.GetLabels(),
+                               request.GetLabelsConstraint(),
+                               (request.HasLimits() ? request.GetLimitsCount() : 0));  // TODO: handles since and count
+
+        if (request.IsResponseIdentifiersOnly())
+        {
+          SQLite::Statement statement(db_, SQLITE_FROM_HERE_DYNAMIC(sqlLookup), sqlLookup);
+          formatter.Bind(statement);
+
+          while (statement.Step())
+          {
+            FindResponse::Item* item = new FindResponse::Item(request.GetResponseContent(),
+                                                              request.GetLevel(),
+                                                              statement.ColumnString(0));
+            response.Add(item);
+          }
+        }
+        else
+        {
+          std::map<std::string, FindResponse::Item*> items;  // cache to the response items
+
+          {// first create a temporary table that with the filtered and ordered results
+            sqlLookup = "CREATE TEMPORARY TABLE FilteredResourcesIds AS " + sqlLookup;
+
+            SQLite::Statement statement(db_, SQLITE_FROM_HERE_DYNAMIC(sqlLookup), sqlLookup);
+            formatter.Bind(statement);
+            statement.Run();
+          }
+
+          {
+            // create the response item with the public ids only
+            SQLite::Statement statement(db_, SQLITE_FROM_HERE, "SELECT publicId FROM FilteredResourcesIds");
+            formatter.Bind(statement);
+
+            while (statement.Step())
+            {
+              const std::string& resourceId = statement.ColumnString(0);
+              FindResponse::Item* item = new FindResponse::Item(request.GetResponseContent(),
+                                                                request.GetLevel(),
+                                                                resourceId);
+              items[resourceId] = item;
+              response.Add(item);
+            }
+          }
+
+          // request Each response content through INNER JOIN with the temporary table
+          if (request.HasResponseContent(FindRequest::ResponseContent_MainDicomTags))
+          {
+            // TODO-FIND: handle the case where we request tags from multiple levels
+            SQLite::Statement statement(db_, SQLITE_FROM_HERE, 
+                                        "SELECT publicId, tagGroup, tagElement, value FROM MainDicomTags AS tags "
+                                        "  INNER JOIN FilteredResourcesIds  ON tags.id = FilteredResourcesIds.internalId");
+            formatter.Bind(statement);
+
+            while (statement.Step())
+            {
+              const std::string& resourceId = statement.ColumnString(0);
+              items[resourceId]->AddDicomTag(statement.ColumnInt(1),
+                                             statement.ColumnInt(2),
+                                             statement.ColumnString(3), false);
+            }
+          }
+
+          if (request.HasResponseContent(FindRequest::ResponseContent_Children))
+          {
+            SQLite::Statement statement(db_, SQLITE_FROM_HERE, 
+                "SELECT filtered.publicId, childLevel.publicId AS childPublicId "
+                "FROM Resources as currentLevel "
+                "    INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = currentLevel.internalId "
+                "    INNER JOIN Resources childLevel ON childLevel.parentId = currentLevel.internalId");
+            formatter.Bind(statement);
+
+            while (statement.Step())
+            {
+              const std::string& resourceId = statement.ColumnString(0);
+              items[resourceId]->AddChild(statement.ColumnString(1));
+            }
+          }
+
+          if (request.HasResponseContent(FindRequest::ResponseContent_Parent))
+          {
+            SQLite::Statement statement(db_, SQLITE_FROM_HERE, 
+                "SELECT filtered.publicId, parentLevel.publicId AS parentPublicId "
+                "FROM Resources as currentLevel "
+                "    INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = currentLevel.internalId "
+                "    INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId");
+
+            while (statement.Step())
+            {
+              const std::string& resourceId = statement.ColumnString(0);
+              items[resourceId]->SetParent(statement.ColumnString(1));
+            }
+          }
+
+          if (request.HasResponseContent(FindRequest::ResponseContent_Metadata))
+          {
+            SQLite::Statement statement(db_, SQLITE_FROM_HERE, 
+                "SELECT filtered.publicId, metadata.type, metadata.value "
+                "FROM Metadata "
+                "  INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = Metadata.id");
+
+            while (statement.Step())
+            {
+              const std::string& resourceId = statement.ColumnString(0);
+              items[resourceId]->AddMetadata(static_cast<MetadataType>(statement.ColumnInt(1)),
+                                             statement.ColumnString(2));
+            }
+          }
+
+          if (request.HasResponseContent(FindRequest::ResponseContent_Labels))
+          {
+            SQLite::Statement statement(db_, SQLITE_FROM_HERE, 
+                "SELECT filtered.publicId, label "
+                "FROM Labels "
+                "  INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = Labels.id");
+
+            while (statement.Step())
+            {
+              const std::string& resourceId = statement.ColumnString(0);
+              items[resourceId]->AddLabel(statement.ColumnString(1));
+            }
+          }
+
+          // TODO-FIND: implement other responseContent: ResponseContent_ChildInstanceId, ResponseContent_Attachments, (later: ResponseContent_IsStable)
+
+        }
       }
+
 #endif
     }
   };
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Thu Apr 25 09:22:07 2024 +0200
@@ -501,6 +501,38 @@
     }
   }
 
+  void StatelessDatabaseOperations::NormalizeLookup(std::vector<DatabaseConstraint>& target,
+                                                    const FindRequest& findRequest) const
+  {
+    assert(mainDicomTagsRegistry_.get() != NULL);
+
+    target.clear();
+    target.reserve(findRequest.GetDicomTagConstraintsCount());
+
+    for (size_t i = 0; i < findRequest.GetDicomTagConstraintsCount(); i++)
+    {
+      ResourceType level;
+      DicomTagType type;
+      
+      mainDicomTagsRegistry_->LookupTag(level, type, findRequest.GetDicomTagConstraint(i).GetTag());
+
+      if (type == DicomTagType_Identifier ||
+          type == DicomTagType_Main)
+      {
+        // Use the fact that patient-level tags are copied at the study level
+        if (level == ResourceType_Patient &&
+            findRequest.GetLevel() != ResourceType_Patient)
+        {
+          level = ResourceType_Study;
+        }
+        
+        target.push_back(findRequest.GetDicomTagConstraint(i).ConvertToDatabaseConstraint(level, type));
+      }
+    }
+
+    // TODO-FIND: add metadata constraints
+  }
+
 
   class StatelessDatabaseOperations::Transaction : public boost::noncopyable
   {
@@ -3780,17 +3812,88 @@
   void StatelessDatabaseOperations::ExecuteFind(FindResponse& response,
                                                 const FindRequest& request)
   {
-    class Operations : public ReadOnlyOperationsT2<FindResponse&, const FindRequest&>
+    class Operations : public ReadOnlyOperationsT3<FindResponse&, const FindRequest&, const std::vector<DatabaseConstraint>&>
     {
     public:
       virtual void ApplyTuple(ReadOnlyTransaction& transaction,
                               const Tuple& tuple) ORTHANC_OVERRIDE
       {
-        transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>());
+        transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>(), tuple.get<2>());
       }
     };
 
+    std::vector<DatabaseConstraint> normalized;
+    NormalizeLookup(normalized, request);
+
     Operations operations;
-    operations.Apply(*this, response, request);
+    operations.Apply(*this, response, request, normalized);
+  }
+
+  // TODO-FIND: we reuse the ExpandedResource class to reuse Serialization code from ExpandedResource
+  // But, finally, we might just get rid of ExpandedResource and replace it by FindResponse
+  ExpandedResource::ExpandedResource(const FindResponse::Item& item) :
+    id_(item.GetResourceId()),
+    level_(item.GetLevel()),
+    isStable_(false),
+    expectedNumberOfInstances_(0),
+    fileSize_(0),
+    indexInSeries_(0)
+  {
+    if (item.HasResponseContent(FindRequest::ResponseContent_MainDicomTags))
+    {
+      tags_.Assign(item.GetDicomMap());
+    }
+
+    if (item.HasResponseContent(FindRequest::ResponseContent_Children))
+    {
+      childrenIds_ = item.GetChildren();
+    }
+
+    if (item.HasResponseContent(FindRequest::ResponseContent_Parent))
+    {
+      parentId_ = item.GetParent();
+    }
+
+    if (item.HasResponseContent(FindRequest::ResponseContent_Metadata))
+    {
+      metadata_ = item.GetMetadata();
+      std::string value;
+      if (item.LookupMetadata(value, MetadataType_MainDicomTagsSignature))
+      {
+        mainDicomTagsSignature_ = value;
+      }
+      if (item.LookupMetadata(value, MetadataType_AnonymizedFrom))
+      {
+        anonymizedFrom_ = value;
+      }
+      if (item.LookupMetadata(value, MetadataType_ModifiedFrom))
+      {
+        modifiedFrom_ = value;
+      }
+      if (item.LookupMetadata(value, MetadataType_LastUpdate))
+      {
+        lastUpdate_ = value;
+      }
+      if (item.GetLevel() == ResourceType_Series)
+      {
+        if (item.LookupMetadata(value, MetadataType_Series_ExpectedNumberOfInstances))
+        {
+          expectedNumberOfInstances_ = boost::lexical_cast<int>(value);
+        }
+      }
+      if (item.GetLevel() == ResourceType_Instance)
+      {
+        if (item.LookupMetadata(value, MetadataType_Instance_IndexInSeries))
+        {
+          indexInSeries_ = boost::lexical_cast<int>(value);
+        }
+      }
+    }
+
+    if (item.HasResponseContent(FindRequest::ResponseContent_Labels))
+    {
+      labels_ = item.GetLabels();
+    }
+    // TODO-FIND: continue: isStable_, satus_, fileSize_, fileUuid_
   }
 }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Thu Apr 25 09:22:07 2024 +0200
@@ -80,6 +80,8 @@
     {
     }
     
+    ExpandedResource(const FindResponse::Item& item);
+
     void SetResource(ResourceType level,
                      const std::string& id)
     {
@@ -111,15 +113,26 @@
   enum ExpandResourceFlags
   {
     ExpandResourceFlags_None                    = 0,
+    // used to fetch from DB and for output
     ExpandResourceFlags_IncludeMetadata         = (1 << 0),
     ExpandResourceFlags_IncludeChildren         = (1 << 1),
     ExpandResourceFlags_IncludeMainDicomTags    = (1 << 2),
     ExpandResourceFlags_IncludeLabels           = (1 << 3),
 
-    ExpandResourceFlags_Default = (ExpandResourceFlags_IncludeMetadata |
-                                     ExpandResourceFlags_IncludeChildren |
-                                     ExpandResourceFlags_IncludeMainDicomTags |
-                                     ExpandResourceFlags_IncludeLabels)
+    // only used for output
+    ExpandResourceFlags_IncludeAllMetadata      = (1 << 4),  // new in Orthanc 1.12.4
+    ExpandResourceFlags_IncludeIsStable         = (1 << 5),  // new in Orthanc 1.12.4
+
+    ExpandResourceFlags_DefaultExtract = (ExpandResourceFlags_IncludeMetadata |
+                                          ExpandResourceFlags_IncludeChildren |
+                                          ExpandResourceFlags_IncludeMainDicomTags |
+                                          ExpandResourceFlags_IncludeLabels),
+
+    ExpandResourceFlags_DefaultOutput = (ExpandResourceFlags_IncludeMetadata |
+                                         ExpandResourceFlags_IncludeChildren |
+                                         ExpandResourceFlags_IncludeMainDicomTags |
+                                         ExpandResourceFlags_IncludeLabels |
+                                         ExpandResourceFlags_IncludeIsStable)
   };
 
   class StatelessDatabaseOperations : public boost::noncopyable
@@ -378,9 +391,10 @@
       }
 
       void ExecuteFind(FindResponse& response,
-                       const FindRequest& request)
+                       const FindRequest& request, 
+                       const std::vector<DatabaseConstraint>& normalized)
       {
-        transaction_.ExecuteFind(response, request);
+        transaction_.ExecuteFind(response, request, normalized);
       }
     };
 
@@ -564,6 +578,9 @@
                          const DatabaseLookup& source,
                          ResourceType level) const;
 
+    void NormalizeLookup(std::vector<DatabaseConstraint>& target,
+                         const FindRequest& findRequest) const;
+
     void ApplyInternal(IReadOnlyOperations* readOperations,
                        IReadWriteOperations* writeOperations);
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Apr 25 09:22:07 2024 +0200
@@ -234,16 +234,14 @@
 
       FindRequest request(resourceType);
 
-#if 0
-      // TODO - This version should be executed if no disk access is needed
       if (expand)
       {
-        request.SetResponseContent(FindRequest::ResponseContent_MainDicomTags |
+        request.SetResponseContent(static_cast<FindRequest::ResponseContent>(FindRequest::ResponseContent_MainDicomTags |
                                    FindRequest::ResponseContent_Metadata |
                                    FindRequest::ResponseContent_Labels |
                                    FindRequest::ResponseContent_Attachments |
                                    FindRequest::ResponseContent_Parent |
-                                   FindRequest::ResponseContent_Children)
+                                   FindRequest::ResponseContent_Children));
 
         request.SetRetrieveTagsAtLevel(resourceType, true);
 
@@ -256,9 +254,6 @@
       {
         request.SetResponseContent(FindRequest::ResponseContent_IdentifiersOnly);
       }
-#else
-      request.SetResponseContent(FindRequest::ResponseContent_IdentifiersOnly);
-#endif
 
       if (call.HasArgument("limit") ||
           call.HasArgument("since"))
@@ -285,35 +280,27 @@
       FindResponse response;
       index.ExecuteFind(response, request);
 
-      std::set<DicomTag> requestedTags;
-      OrthancRestApi::GetRequestedTags(requestedTags, call);
-
-      const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
-
+      // TODO-FIND: put this in an AnswerFindResponse method !
       Json::Value answer = Json::arrayValue;
 
-      for (size_t i = 0; i < response.GetSize(); i++)
+      if (request.IsResponseIdentifiersOnly())
       {
-        std::string resourceId = response.GetItem(i).GetIdentifiers().GetLevel(resourceType);
-
-        if (expand)
+        for (size_t i = 0; i < response.GetSize(); i++)
         {
-          Json::Value expanded;
-
-          context.ExpandResource(expanded, resourceId, resourceType, format, requestedTags, true /* allowStorageAccess */);
-
-          if (expanded.type() == Json::objectValue)
-          {
-            answer.append(expanded);
-          }
-          else
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
+          std::string resourceId = response.GetItem(i).GetResourceId();
+          answer.append(resourceId);
         }
-        else
+      }
+      else
+      {
+        std::set<DicomTag> requestedTags;
+        OrthancRestApi::GetRequestedTags(requestedTags, call);
+
+        const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human);
+
+        for (size_t i = 0; i < response.GetSize(); i++)
         {
-          answer.append(resourceId);
+          context.AppendFindResponse(answer, response.GetItem(i), format, requestedTags, true /* allowStorageAccess */);
         }
       }
 
--- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Thu Apr 25 09:22:07 2024 +0200
@@ -152,6 +152,7 @@
                                          const std::vector<std::string>& values,
                                          bool caseSensitive,
                                          bool mandatory) :
+    keyType_(DatabaseConstraint::KeyType_DicomTag),
     level_(level),
     tag_(tag),
     isIdentifier_(isIdentifier),
--- a/OrthancServer/Sources/Search/DatabaseConstraint.h	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.h	Thu Apr 25 09:22:07 2024 +0200
@@ -79,9 +79,18 @@
   // This class is also used by the "orthanc-databases" project
   class DatabaseConstraint
   {
+  public:
+    enum KeyType  // used for ordering and filters
+    {
+      KeyType_DicomTag,
+      KeyType_Metadata
+    };
+
   private:
+    KeyType                   keyType_;
     ResourceType              level_;
     DicomTag                  tag_;
+    uint32_t                  metadataType_;  // TODO: implement
     bool                      isIdentifier_;
     ConstraintType            constraintType_;
     std::vector<std::string>  values_;
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Thu Apr 25 09:22:07 2024 +0200
@@ -24,6 +24,7 @@
 
 #if ORTHANC_BUILDING_SERVER_LIBRARY == 1
 #  include "../../../OrthancFramework/Sources/Enumerations.h"
+#  include "../Search/LabelsConstraint.h"
 #else
 #  include <Enumerations.h>
 #endif
@@ -35,13 +36,6 @@
 {
   class DatabaseConstraint;
   
-  enum LabelsConstraint
-  {
-    LabelsConstraint_All,
-    LabelsConstraint_Any,
-    LabelsConstraint_None
-  };
-
   // This class is also used by the "orthanc-databases" project
   class ISqlLookupFormatter : public boost::noncopyable
   {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/LabelsConstraint.h	Thu Apr 25 09:22:07 2024 +0200
@@ -0,0 +1,33 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2024 Osimis S.A., Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+namespace Orthanc
+{
+  enum LabelsConstraint
+  {
+    LabelsConstraint_All,
+    LabelsConstraint_Any,
+    LabelsConstraint_None
+  };
+}
\ No newline at end of file
--- a/OrthancServer/Sources/ServerContext.cpp	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.cpp	Thu Apr 25 09:22:07 2024 +0200
@@ -2125,124 +2125,137 @@
   static void SerializeExpandedResource(Json::Value& target,
                                         const ExpandedResource& resource,
                                         DicomToJsonFormat format,
-                                        const std::set<DicomTag>& requestedTags)
+                                        const std::set<DicomTag>& requestedTags,
+                                        ExpandResourceFlags expandFlags)
   {
     target = Json::objectValue;
 
     target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true);
     target["ID"] = resource.GetPublicId();
 
-    switch (resource.GetLevel())
-    {
-      case ResourceType_Patient:
-        break;
-
-      case ResourceType_Study:
-        target["ParentPatient"] = resource.parentId_;
-        break;
-
-      case ResourceType_Series:
-        target["ParentStudy"] = resource.parentId_;
-        break;
-
-      case ResourceType_Instance:
-        target["ParentSeries"] = resource.parentId_;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    switch (resource.GetLevel())
+    if (!resource.parentId_.empty())
     {
-      case ResourceType_Patient:
-      case ResourceType_Study:
-      case ResourceType_Series:
+      switch (resource.GetLevel())
       {
-        Json::Value c = Json::arrayValue;
-
-        for (std::list<std::string>::const_iterator
-                it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it)
-        {
-          c.append(*it);
-        }
-
-        if (resource.GetLevel() == ResourceType_Patient)
-        {
-          target["Studies"] = c;
-        }
-        else if (resource.GetLevel() == ResourceType_Study)
-        {
-          target["Series"] = c;
-        }
-        else
-        {
-          target["Instances"] = c;
-        }
-        break;
+        case ResourceType_Patient:
+          break;
+
+        case ResourceType_Study:
+          target["ParentPatient"] = resource.parentId_;
+          break;
+
+        case ResourceType_Series:
+          target["ParentStudy"] = resource.parentId_;
+          break;
+
+        case ResourceType_Instance:
+          target["ParentSeries"] = resource.parentId_;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
       }
-
-      case ResourceType_Instance:
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
     }
 
-    switch (resource.GetLevel())
+    if ((expandFlags & ExpandResourceFlags_IncludeChildren) != 0)
     {
-      case ResourceType_Patient:
-      case ResourceType_Study:
-        break;
-
-      case ResourceType_Series:
-        if (resource.expectedNumberOfInstances_ < 0)
+      switch (resource.GetLevel())
+      {
+        case ResourceType_Patient:
+        case ResourceType_Study:
+        case ResourceType_Series:
         {
-          target["ExpectedNumberOfInstances"] = Json::nullValue;
+          Json::Value c = Json::arrayValue;
+
+          for (std::list<std::string>::const_iterator
+                  it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it)
+          {
+            c.append(*it);
+          }
+
+          if (resource.GetLevel() == ResourceType_Patient)
+          {
+            target["Studies"] = c;
+          }
+          else if (resource.GetLevel() == ResourceType_Study)
+          {
+            target["Series"] = c;
+          }
+          else
+          {
+            target["Instances"] = c;
+          }
+          break;
         }
-        else
-        {
-          target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_;
-        }
-        target["Status"] = resource.status_;
-        break;
-
-      case ResourceType_Instance:
+
+        case ResourceType_Instance:
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    if ((expandFlags & ExpandResourceFlags_IncludeMetadata) != 0)
+    {
+      switch (resource.GetLevel())
       {
-        target["FileSize"] = static_cast<unsigned int>(resource.fileSize_);
-        target["FileUuid"] = resource.fileUuid_;
-
-        if (resource.indexInSeries_ < 0)
+        case ResourceType_Patient:
+        case ResourceType_Study:
+          break;
+
+        case ResourceType_Series:
+          if (resource.expectedNumberOfInstances_ < 0)
+          {
+            target["ExpectedNumberOfInstances"] = Json::nullValue;
+          }
+          else
+          {
+            target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_;
+          }
+          target["Status"] = resource.status_;
+          break;
+
+        case ResourceType_Instance:
         {
-          target["IndexInSeries"] = Json::nullValue;
+          target["FileSize"] = static_cast<unsigned int>(resource.fileSize_);
+          target["FileUuid"] = resource.fileUuid_;
+
+          if (resource.indexInSeries_ < 0)
+          {
+            target["IndexInSeries"] = Json::nullValue;
+          }
+          else
+          {
+            target["IndexInSeries"] = resource.indexInSeries_;
+          }
+
+          break;
         }
-        else
-        {
-          target["IndexInSeries"] = resource.indexInSeries_;
-        }
-
-        break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
       }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (!resource.anonymizedFrom_.empty())
-    {
-      target["AnonymizedFrom"] = resource.anonymizedFrom_;
-    }
     
-    if (!resource.modifiedFrom_.empty())
-    {
-      target["ModifiedFrom"] = resource.modifiedFrom_;
+      if (!resource.anonymizedFrom_.empty())
+      {
+        target["AnonymizedFrom"] = resource.anonymizedFrom_;
+      }
+      
+      if (!resource.modifiedFrom_.empty())
+      {
+        target["ModifiedFrom"] = resource.modifiedFrom_;
+      }
     }
 
     if (resource.GetLevel() == ResourceType_Patient ||
         resource.GetLevel() == ResourceType_Study ||
         resource.GetLevel() == ResourceType_Series)
     {
-      target["IsStable"] = resource.isStable_;
+      if ((expandFlags & ExpandResourceFlags_IncludeIsStable) != 0)
+      {
+        target["IsStable"] = resource.isStable_;
+      }
 
       if (!resource.lastUpdate_.empty())
       {
@@ -2250,38 +2263,42 @@
       }
     }
 
-    // serialize tags
-
-    static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
-    static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
-
-    DicomMap mainDicomTags;
-    resource.GetMainDicomTags().ExtractResourceInformation(mainDicomTags, resource.GetLevel());
-
-    target[MAIN_DICOM_TAGS] = Json::objectValue;
-    FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format);
-    
-    if (resource.GetLevel() == ResourceType_Study)
+    if ((expandFlags & ExpandResourceFlags_IncludeMainDicomTags) != 0)
     {
-      DicomMap patientMainDicomTags;
-      resource.GetMainDicomTags().ExtractPatientInformation(patientMainDicomTags);
-
-      target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format);
+      // serialize tags
+
+      static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
+      static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags";
+
+      DicomMap mainDicomTags;
+      resource.GetMainDicomTags().ExtractResourceInformation(mainDicomTags, resource.GetLevel());
+
+      target[MAIN_DICOM_TAGS] = Json::objectValue;
+      FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format);
+      
+      if (resource.GetLevel() == ResourceType_Study)
+      {
+        DicomMap patientMainDicomTags;
+        resource.GetMainDicomTags().ExtractPatientInformation(patientMainDicomTags);
+
+        target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue;
+        FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format);
+      }
+
+      if (requestedTags.size() > 0)
+      {
+        static const char* const REQUESTED_TAGS = "RequestedTags";
+
+        DicomMap tags;
+        resource.GetMainDicomTags().ExtractTags(tags, requestedTags);
+
+        target[REQUESTED_TAGS] = Json::objectValue;
+        FromDcmtkBridge::ToJson(target[REQUESTED_TAGS], tags, format);
+
+      }
     }
 
-    if (requestedTags.size() > 0)
-    {
-      static const char* const REQUESTED_TAGS = "RequestedTags";
-
-      DicomMap tags;
-      resource.GetMainDicomTags().ExtractTags(tags, requestedTags);
-
-      target[REQUESTED_TAGS] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target[REQUESTED_TAGS], tags, format);
-
-    }
-
+    if ((expandFlags & ExpandResourceFlags_IncludeLabels) != 0)
     {
       Json::Value labels = Json::arrayValue;
 
@@ -2292,6 +2309,19 @@
 
       target["Labels"] = labels;
     }
+
+    // new in Orthanc 1.12.4
+    if ((expandFlags & ExpandResourceFlags_IncludeAllMetadata) != 0)
+    {
+      Json::Value metadata = Json::objectValue;
+
+      for (std::map<MetadataType, std::string>::const_iterator it = resource.metadata_.begin(); it != resource.metadata_.end(); ++it)
+      {
+        metadata[EnumerationToString(it->first)] = it->second;
+      }
+
+      target["Metadata"] = metadata;
+    }
   }
 
 
@@ -2537,9 +2567,9 @@
   {
     ExpandedResource resource;
 
-    if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceFlags_Default, allowStorageAccess))
+    if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceFlags_DefaultExtract, allowStorageAccess))
     {
-      SerializeExpandedResource(target, resource, format, requestedTags);
+      SerializeExpandedResource(target, resource, format, requestedTags, ExpandResourceFlags_DefaultOutput);
       return true;
     }
 
@@ -2687,4 +2717,41 @@
     return elapsed.total_seconds();
   }
 
+  void ServerContext::AppendFindResponse(Json::Value& target,
+                                         const FindResponse::Item& item,
+                                         DicomToJsonFormat format,
+                                         const std::set<DicomTag>& requestedTags,
+                                         bool allowStorageAccess)
+  {
+    // convert to ExpandedResource to re-use the serialization code TODO-FIND: check if this is the right way to do.  shouldn't we copy the code and finally get rid of ExpandedResource ? 
+    ExpandedResource resource(item);
+
+    ExpandResourceFlags expandFlags = ExpandResourceFlags_None;
+    if (item.HasResponseContent(FindRequest::ResponseContent_Children))
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeChildren);
+    }
+    if (item.HasResponseContent(FindRequest::ResponseContent_Metadata))
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeAllMetadata | ExpandResourceFlags_IncludeMetadata );
+    }
+    if (item.HasResponseContent(FindRequest::ResponseContent_MainDicomTags))
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeMainDicomTags);
+    }
+    if (item.HasResponseContent(FindRequest::ResponseContent_IsStable))
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeIsStable);
+    }
+    if (item.HasResponseContent(FindRequest::ResponseContent_Labels))
+    {
+      expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeLabels);
+    }
+
+    Json::Value jsonItem;
+    SerializeExpandedResource(jsonItem, resource, format, requestedTags, expandFlags);
+    target.append(jsonItem);
+  }
+
+
 }
--- a/OrthancServer/Sources/ServerContext.h	Tue Apr 23 16:49:44 2024 +0200
+++ b/OrthancServer/Sources/ServerContext.h	Thu Apr 25 09:22:07 2024 +0200
@@ -607,6 +607,12 @@
                         ExpandResourceFlags expandFlags,
                         bool allowStorageAccess);
 
+    void AppendFindResponse(Json::Value& target,
+                            const FindResponse::Item& item,
+                            DicomToJsonFormat format,
+                            const std::set<DicomTag>& requestedTags,
+                            bool allowStorageAccess);
+
     FindStorageAccessMode GetFindStorageAccessMode() const
     {
       return findStorageAccessMode_;