Mercurial > hg > orthanc
changeset 5568:b0b5546f1b9f find-refactoring
find refactor: re-use existing code. /studies?expand is almost fully implemented with new code
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_;