Mercurial > hg > orthanc
changeset 5577:9e74e761b108 find-refactoring
integration mainline->find-refactoring
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 26 Apr 2024 17:43:22 +0200 |
parents | 5a13483d12c5 (diff) 3a6d6d35193c (current diff) |
children | 77570cce8855 |
files | NEWS OrthancServer/CMakeLists.txt OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h |
diffstat | 28 files changed, 2441 insertions(+), 167 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Fri Apr 26 17:41:40 2024 +0200 +++ b/NEWS Fri Apr 26 17:43:22 2024 +0200 @@ -26,6 +26,8 @@ /patients|studies|series/instances/../reconstruct to speed up the reconstruction in case you just want to update the MainDicomTags of that resource level only e.g. after you have updated the 'ExtraMainDicomTags' for this level. +* TODO-FIND: complete the list of updated routes: + /studies?expand and sibbling routes now also return "Metadata" (if the DB implements 'extended-api-v1') Plugins -------
--- a/OrthancFramework/Sources/SQLite/Connection.h Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/Connection.h Fri Apr 26 17:43:22 2024 +0200 @@ -56,6 +56,7 @@ #endif #define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__) +#define SQLITE_FROM_HERE_DYNAMIC(sql) ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__, sql) namespace Orthanc {
--- a/OrthancFramework/Sources/SQLite/StatementId.cpp Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/StatementId.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -56,12 +56,24 @@ { } + Orthanc::SQLite::StatementId::StatementId(const char *file, + int line, + const std::string& statement) : + file_(file), + line_(line), + statement_(statement) + { + } + bool StatementId::operator< (const StatementId& other) const { if (line_ != other.line_) return line_ < other.line_; - return strcmp(file_, other.file_) < 0; + if (strcmp(file_, other.file_) < 0) + return true; + + return statement_ < other.statement_; } } }
--- a/OrthancFramework/Sources/SQLite/StatementId.h Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/StatementId.h Fri Apr 26 17:43:22 2024 +0200 @@ -54,6 +54,7 @@ private: const char* file_; int line_; + std::string statement_; StatementId(); // Forbidden @@ -61,6 +62,10 @@ StatementId(const char* file, int line); + StatementId(const char* file, + int line, + const std::string& statement); + bool operator< (const StatementId& other) const; }; }
--- a/OrthancServer/CMakeLists.txt Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/CMakeLists.txt Fri Apr 26 17:43:22 2024 +0200 @@ -89,11 +89,15 @@ set(ORTHANC_SERVER_SOURCES ${CMAKE_SOURCE_DIR}/Sources/Database/BaseDatabaseWrapper.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/DatabaseLookup.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/GenericFind.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ICreateInstance.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/IGetChildrenMetadata.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ILookupResourceAndParent.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/ILookupResources.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/Compatibility/SetOfResources.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/FindRequest.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/FindResponse.cpp + ${CMAKE_SOURCE_DIR}/Sources/Database/OrthancIdentifiers.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/ResourcesContent.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/SQLiteDatabaseWrapper.cpp ${CMAKE_SOURCE_DIR}/Sources/Database/StatelessDatabaseOperations.cpp
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -30,6 +30,7 @@ #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../Sources/Database/Compatibility/GenericFind.h" #include "../../Sources/Database/Compatibility/ICreateInstance.h" #include "../../Sources/Database/Compatibility/IGetChildrenMetadata.h" #include "../../Sources/Database/Compatibility/ILookupResourceAndParent.h" @@ -1447,6 +1448,15 @@ { throw OrthancException(ErrorCode_InternalError); // Not supported } + + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE + { + Compatibility::GenericFind find(*this); + find.Execute(response, request); + } };
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -29,6 +29,7 @@ #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../Sources/Database/Compatibility/GenericFind.h" #include "../../Sources/Database/ResourcesContent.h" #include "../../Sources/Database/VoidDatabaseListener.h" #include "PluginsEnumerations.h" @@ -1060,6 +1061,15 @@ { throw OrthancException(ErrorCode_InternalError); // Not supported } + + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE + { + Compatibility::GenericFind find(*this); + find.Execute(response, request); + } };
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -30,6 +30,7 @@ #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" #include "../../../OrthancFramework/Sources/Logging.h" #include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../Sources/Database/Compatibility/GenericFind.h" #include "../../Sources/Database/ResourcesContent.h" #include "../../Sources/Database/VoidDatabaseListener.h" #include "../../Sources/ServerToolbox.h" @@ -1275,6 +1276,15 @@ { ListLabelsInternal(target, false, -1); } + + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE + { + Compatibility::GenericFind find(*this); + find.Execute(response, request); + } };
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Fri Apr 26 17:43:22 2024 +0200 @@ -744,7 +744,7 @@ } OrthancPluginResourceType; - + /** * The supported types of changes that can be signaled to the change callback. * @ingroup Callbacks
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -0,0 +1,99 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2024 Osimis S.A., Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "GenericFind.h" + +#include "../../../../OrthancFramework/Sources/OrthancException.h" + + +namespace Orthanc +{ + namespace Compatibility + { + void GenericFind::Execute(FindResponse& response, + const FindRequest& request) + { + if (request.GetResponseContent() == FindRequest::ResponseContent_IdentifiersOnly && + !request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId() && + request.GetDicomTagConstraintsCount() == 0 && + request.GetMetadataConstraintsCount() == 0 && + !request.IsRetrieveTagsAtLevel(ResourceType_Patient) && + !request.IsRetrieveTagsAtLevel(ResourceType_Study) && + !request.IsRetrieveTagsAtLevel(ResourceType_Series) && + !request.IsRetrieveTagsAtLevel(ResourceType_Instance) && + request.GetOrdering().empty() && + request.GetLabels().empty()) + { + std::list<std::string> ids; + + if (request.HasLimits()) + { + transaction_.GetAllPublicIds(ids, request.GetLevel(), request.GetLimitsSince(), request.GetLimitsCount()); + } + else + { + transaction_.GetAllPublicIds(ids, request.GetLevel()); + } + + for (std::list<std::string>::const_iterator it = ids.begin(); it != ids.end(); ++it) + { + OrthancIdentifiers identifiers; + identifiers.SetLevel(request.GetLevel(), *it); + + response.Add(new FindResponse::Item(request.GetResponseContent(), + request.GetLevel(), + identifiers)); + } + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + + + /** + * Sanity checks + **/ + + for (size_t i = 0; i < response.GetSize(); i++) + { + const FindResponse::Item& item = response.GetItem(i); + + if (item.GetLevel() != request.GetLevel()) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (request.HasResponseContent(FindRequest::ResponseContent_MainDicomTags) + && !item.HasDicomMap()) + { + throw OrthancException(ErrorCode_InternalError); + } + + // TODO: other sanity checks + } + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.h Fri Apr 26 17:43:22 2024 +0200 @@ -0,0 +1,47 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2024 Osimis S.A., Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../IDatabaseWrapper.h" + +namespace Orthanc +{ + namespace Compatibility + { + // TODO-FIND: remove this class that only contains a temporary implementation + class GenericFind : public boost::noncopyable + { + private: + IDatabaseWrapper::ITransaction& transaction_; + + public: + GenericFind(IDatabaseWrapper::ITransaction& transaction) : + transaction_(transaction) + { + } + + void Execute(FindResponse& response, + const FindRequest& request); + }; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindRequest.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -0,0 +1,210 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2024 Osimis S.A., Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "FindRequest.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + + +#include <cassert> + +namespace Orthanc +{ + bool FindRequest::IsCompatibleLevel(ResourceType levelOfInterest) const + { + switch (level_) + { + case ResourceType_Patient: + return (levelOfInterest == ResourceType_Patient); + + case ResourceType_Study: + return (levelOfInterest == ResourceType_Patient || + levelOfInterest == ResourceType_Study); + + case ResourceType_Series: + return (levelOfInterest == ResourceType_Patient || + levelOfInterest == ResourceType_Study || + levelOfInterest == ResourceType_Series); + + case ResourceType_Instance: + return (levelOfInterest == ResourceType_Patient || + levelOfInterest == ResourceType_Study || + levelOfInterest == ResourceType_Series || + levelOfInterest == ResourceType_Instance); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + FindRequest::FindRequest(ResourceType level) : + level_(level), + hasLimits_(false), + limitsSince_(0), + limitsCount_(0), + responseContent_(ResponseContent_IdentifiersOnly), + retrievePatientTags_(false), + retrieveStudyTags_(false), + retrieveSeriesTags_(false), + retrieveInstanceTags_(false) + { + } + + + FindRequest::~FindRequest() + { + + for (std::deque<Ordering*>::iterator it = ordering_.begin(); it != ordering_.end(); ++it) + { + assert(*it != NULL); + delete *it; + } + } + + void FindRequest::AddDicomTagConstraint(const DicomTagConstraint& constraint) + { + dicomTagConstraints_.push_back(constraint); + } + + const DicomTagConstraint& FindRequest::GetDicomTagConstraint(size_t index) const + { + if (index >= dicomTagConstraints_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + return dicomTagConstraints_[index]; + } + } + + + void FindRequest::SetLimits(uint64_t since, + uint64_t count) + { + if (hasLimits_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + hasLimits_ = true; + limitsSince_ = since; + limitsCount_ = count; + } + } + + + uint64_t FindRequest::GetLimitsSince() const + { + if (hasLimits_) + { + return limitsSince_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + uint64_t FindRequest::GetLimitsCount() const + { + if (hasLimits_) + { + return limitsCount_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindRequest::SetRetrieveTagsAtLevel(ResourceType levelOfInterest, + bool retrieve) + { + if (!IsCompatibleLevel(levelOfInterest)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + switch (levelOfInterest) + { + case ResourceType_Patient: + retrievePatientTags_ = true; + break; + + case ResourceType_Study: + retrieveStudyTags_ = true; + break; + + case ResourceType_Series: + retrieveSeriesTags_ = true; + break; + + case ResourceType_Instance: + retrieveInstanceTags_ = true; + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool FindRequest::IsRetrieveTagsAtLevel(ResourceType levelOfInterest) const + { + switch (levelOfInterest) + { + case ResourceType_Patient: + return retrievePatientTags_; + + case ResourceType_Study: + return retrieveStudyTags_; + + case ResourceType_Series: + return retrieveSeriesTags_; + + case ResourceType_Instance: + return retrieveInstanceTags_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void FindRequest::AddOrdering(const DicomTag& tag, + OrderingDirection direction) + { + ordering_.push_back(new Ordering(Key(tag), direction)); + } + + void FindRequest::AddOrdering(MetadataType metadataType, + OrderingDirection direction) + { + ordering_.push_back(new Ordering(Key(metadataType), direction)); + } + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindRequest.h Fri Apr 26 17:43:22 2024 +0200 @@ -0,0 +1,474 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2024 Osimis S.A., Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomTag.h" +#include "../ServerEnumerations.h" +#include "OrthancIdentifiers.h" +#include "../Search/DicomTagConstraint.h" +#include "../Search/LabelsConstraint.h" +#include "../Search/DatabaseConstraint.h" + +#include <deque> +#include <map> +#include <set> +#include <cassert> +#include <boost/shared_ptr.hpp> + +namespace Orthanc +{ + class FindRequest : public boost::noncopyable + { + public: + enum ResponseContent + { + ResponseContent_MainDicomTags = (1 << 0), // retrieve all tags from MainDicomTags and DicomIdentifiers + ResponseContent_Metadata = (1 << 1), // retrieve all metadata, their values and revision + ResponseContent_Labels = (1 << 2), // get all labels + ResponseContent_Attachments = (1 << 3), // retrieve all attachments, their values and revision + ResponseContent_Parent = (1 << 4), // get the id of the parent + ResponseContent_Children = (1 << 5), // retrieve the list of children ids + ResponseContent_ChildInstanceId = (1 << 6), // When you need to access all tags from a patient/study/series, you might need to open the DICOM file of a child instance + ResponseContent_ChildrenMetadata = (1 << 7), // That is actually required to compute the series status but could be usefull for other stuffs. + ResponseContent_IsStable = (1 << 8), // This is currently not saved in DB but it could be in the future. + + ResponseContent_IdentifiersOnly = 0, + ResponseContent_INTERNAL = 0x7FFFFFFF + }; + + enum ConstraintType + { + ConstraintType_Mandatory, + ConstraintType_Equality, + ConstraintType_Range, + ConstraintType_Wildcard, + ConstraintType_List + }; + + enum KeyType // used for ordering and filters + { + KeyType_DicomTag, + KeyType_Metadata + }; + + enum OrderingDirection + { + OrderingDirection_Ascending, + OrderingDirection_Descending + }; + + + class Key + { + KeyType type_; + boost::shared_ptr<DicomTag> dicomTag_; + MetadataType metadata_; + + // TODO-FIND: to execute the query, we actually need: + // ResourceType level_; + // DicomTagType dicomTagType_; + // these are however only populated in StatelessDatabaseOperations -> we had to add the normalized lookup arg to ExecuteFind + + public: + Key(const DicomTag& dicomTag) : + type_(KeyType_DicomTag), + dicomTag_(new DicomTag(dicomTag)), + metadata_(MetadataType_EndUser) + { + } + + Key(MetadataType metadata) : + type_(KeyType_Metadata), + metadata_(metadata) + { + } + + KeyType GetType() const + { + return type_; + } + + const DicomTag& GetDicomTag() const + { + assert(GetType() == KeyType_DicomTag); + return *dicomTag_; + } + + MetadataType GetMetadataType() const + { + assert(GetType() == KeyType_Metadata); + return metadata_; + } + }; + + class Ordering : public boost::noncopyable + { + OrderingDirection direction_; + Key key_; + + public: + Ordering(const Key& key, + OrderingDirection direction) : + direction_(direction), + key_(key) + { + } + + public: + KeyType GetKeyType() const + { + return key_.GetType(); + } + + OrderingDirection GetDirection() const + { + return direction_; + } + + MetadataType GetMetadataType() const + { + return key_.GetMetadataType(); + } + + DicomTag GetDicomTag() const + { + return key_.GetDicomTag(); + } + }; + + // TODO-FIND: this class hierarchy actually adds complexity and is very redundant with DicomTagConstraint. + // e.g, in this class hierarchy, it is difficult to implement an equivalent to DicomTagConstraint::ConvertToDatabaseConstraint + // I have the feeling we can just have a MetadataConstraint in the same way as DicomTagConstraint + // and both convert to a DatabaseConstraint in StatelessDatabaseOperations + // class FilterConstraint : public boost::noncopyable + // { + // Key key_; + + // protected: + // FilterConstraint(const Key& key) : + // key_(key) + // { + // } + + // public: + // virtual ~FilterConstraint() + // { + // } + + // const Key& GetKey() const + // { + // return key_; + // } + + // virtual ConstraintType GetType() const = 0; + // virtual bool IsCaseSensitive() const = 0; // Needed for PN VR + + + // }; + + + // class MandatoryConstraint : public FilterConstraint + // { + // public: + // virtual ConstraintType GetType() const ORTHANC_OVERRIDE + // { + // return ConstraintType_Mandatory; + // } + // }; + + + // class StringConstraint : public FilterConstraint + // { + // private: + // bool caseSensitive_; + + // public: + // StringConstraint(Key key, + // bool caseSensitive) : + // FilterConstraint(key), + // caseSensitive_(caseSensitive) + // { + // } + + // bool IsCaseSensitive() const + // { + // return caseSensitive_; + // } + // }; + + + // class EqualityConstraint : public StringConstraint + // { + // private: + // std::string value_; + + // public: + // explicit EqualityConstraint(Key key, + // bool caseSensitive, + // const std::string& value) : + // StringConstraint(key, caseSensitive), + // value_(value) + // { + // } + + // virtual ConstraintType GetType() const ORTHANC_OVERRIDE + // { + // return ConstraintType_Equality; + // } + + // const std::string& GetValue() const + // { + // return value_; + // } + // }; + + + // class RangeConstraint : public StringConstraint + // { + // private: + // std::string start_; + // std::string end_; // Inclusive + + // public: + // RangeConstraint(Key key, + // bool caseSensitive, + // const std::string& start, + // const std::string& end) : + // StringConstraint(key, caseSensitive), + // start_(start), + // end_(end) + // { + // } + + // virtual ConstraintType GetType() const ORTHANC_OVERRIDE + // { + // return ConstraintType_Range; + // } + + // const std::string& GetStart() const + // { + // return start_; + // } + + // const std::string& GetEnd() const + // { + // return end_; + // } + // }; + + + // class WildcardConstraint : public StringConstraint + // { + // private: + // std::string value_; + + // public: + // explicit WildcardConstraint(Key& key, + // bool caseSensitive, + // const std::string& value) : + // StringConstraint(key, caseSensitive), + // value_(value) + // { + // } + + // virtual ConstraintType GetType() const ORTHANC_OVERRIDE + // { + // return ConstraintType_Wildcard; + // } + + // const std::string& GetValue() const + // { + // return value_; + // } + // }; + + + // class ListConstraint : public StringConstraint + // { + // private: + // std::set<std::string> values_; + + // public: + // ListConstraint(Key key, + // bool caseSensitive) : + // StringConstraint(key, caseSensitive) + // { + // } + + // virtual ConstraintType GetType() const ORTHANC_OVERRIDE + // { + // return ConstraintType_List; + // } + + // const std::set<std::string>& GetValues() const + // { + // return values_; + // } + // }; + + + private: + + // filter & ordering fields + ResourceType level_; // The level of the response (the filtering on tags, labels and metadata also happens at this level) + OrthancIdentifiers orthancIdentifiers_; // The response must belong to this Orthanc resources hierarchy + // std::deque<FilterConstraint*> filterConstraints_; // All tags and metadata filters (note: the order is not important) + std::vector<DicomTagConstraint> dicomTagConstraints_; // All tags filters (note: the order is not important) + std::deque<void*> /* TODO-FIND */ metadataConstraints_; // All metadata filters (note: the order is not important) + bool hasLimits_; + uint64_t limitsSince_; + uint64_t limitsCount_; + std::set<std::string> labels_; + LabelsConstraint labelsContraint_; + std::deque<Ordering*> ordering_; // The ordering criteria (note: the order is important !) + + // response fields + ResponseContent responseContent_; + + // TODO: check if these 4 options are required. We might just have a retrieveParentTags that could be part of the ResponseContent enum ? + bool retrievePatientTags_; + bool retrieveStudyTags_; + bool retrieveSeriesTags_; + bool retrieveInstanceTags_; + + bool IsCompatibleLevel(ResourceType levelOfInterest) const; + + public: + FindRequest(ResourceType level); + + ~FindRequest(); + + ResourceType GetLevel() const + { + return level_; + } + + + void SetResponseContent(ResponseContent content) + { + responseContent_ = content; + } + + void AddResponseContent(ResponseContent content) + { + responseContent_ = static_cast<ResponseContent>(static_cast<uint32_t>(responseContent_) | content); + } + + ResponseContent GetResponseContent() const + { + return responseContent_; + } + + bool HasResponseContent(ResponseContent content) const + { + return (responseContent_ & content) == content; + } + + bool IsResponseIdentifiersOnly() const + { + return responseContent_ == ResponseContent_IdentifiersOnly; + } + + void SetOrthancPatientId(const std::string& id) + { + orthancIdentifiers_.SetPatientId(id); + } + + void SetOrthancStudyId(const std::string& id) + { + orthancIdentifiers_.SetStudyId(id); + } + + void SetOrthancSeriesId(const std::string& id) + { + orthancIdentifiers_.SetSeriesId(id); + } + + void SetOrthancInstanceId(const std::string& id) + { + orthancIdentifiers_.SetInstanceId(id); + } + + const OrthancIdentifiers& GetOrthancIdentifiers() const + { + return orthancIdentifiers_; + } + + + void AddDicomTagConstraint(const DicomTagConstraint& constraint); + + size_t GetDicomTagConstraintsCount() const + { + return dicomTagConstraints_.size(); + } + + size_t GetMetadataConstraintsCount() const + { + return metadataConstraints_.size(); + } + + const DicomTagConstraint& GetDicomTagConstraint(size_t index) const; + + void SetLimits(uint64_t since, + uint64_t count); + + bool HasLimits() const + { + return hasLimits_; + } + + uint64_t GetLimitsSince() const; + + uint64_t GetLimitsCount() const; + + + void SetRetrieveTagsAtLevel(ResourceType levelOfInterest, + bool retrieve); + + bool IsRetrieveTagsAtLevel(ResourceType levelOfInterest) const; + + void AddOrdering(const DicomTag& tag, OrderingDirection direction); + + void AddOrdering(MetadataType metadataType, OrderingDirection direction); + + const std::deque<Ordering*>& GetOrdering() const + { + return ordering_; + } + + void AddLabel(const std::string& label) + { + labels_.insert(label); + } + + const std::set<std::string>& GetLabels() const + { + return labels_; + } + + LabelsConstraint GetLabelsConstraint() const + { + return labelsContraint_; + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindResponse.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -0,0 +1,242 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2024 Osimis S.A., Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "FindResponse.h" + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h" +#include "../../../OrthancFramework/Sources/OrthancException.h" + +#include <cassert> + + +namespace Orthanc +{ + static void ExtractOrthancIdentifiers(OrthancIdentifiers& identifiers, + ResourceType level, + const DicomMap& dicom) + { + switch (level) + { + case ResourceType_Patient: + { + std::string patientId; + if (!dicom.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + DicomInstanceHasher hasher(patientId, "", "", ""); + identifiers.SetPatientId(hasher.HashPatient()); + } + break; + } + + case ResourceType_Study: + { + std::string patientId, studyInstanceUid; + if (!dicom.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) || + !dicom.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + DicomInstanceHasher hasher(patientId, studyInstanceUid, "", ""); + identifiers.SetPatientId(hasher.HashPatient()); + identifiers.SetStudyId(hasher.HashStudy()); + } + break; + } + + case ResourceType_Series: + { + std::string patientId, studyInstanceUid, seriesInstanceUid; + if (!dicom.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) || + !dicom.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) || + !dicom.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, ""); + identifiers.SetPatientId(hasher.HashPatient()); + identifiers.SetStudyId(hasher.HashStudy()); + identifiers.SetSeriesId(hasher.HashSeries()); + } + break; + } + + case ResourceType_Instance: + { + std::string patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid; + if (!dicom.LookupStringValue(patientId, Orthanc::DICOM_TAG_PATIENT_ID, false) || + !dicom.LookupStringValue(studyInstanceUid, Orthanc::DICOM_TAG_STUDY_INSTANCE_UID, false) || + !dicom.LookupStringValue(seriesInstanceUid, Orthanc::DICOM_TAG_SERIES_INSTANCE_UID, false) || + !dicom.LookupStringValue(sopInstanceUid, Orthanc::DICOM_TAG_SOP_INSTANCE_UID, false)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + DicomInstanceHasher hasher(patientId, studyInstanceUid, seriesInstanceUid, sopInstanceUid); + identifiers.SetPatientId(hasher.HashPatient()); + identifiers.SetStudyId(hasher.HashStudy()); + identifiers.SetSeriesId(hasher.HashSeries()); + identifiers.SetInstanceId(hasher.HashInstance()); + } + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + FindResponse::Item::Item(FindRequest::ResponseContent responseContent, + ResourceType level, + DicomMap* dicomMap /* takes ownership */) : + responseContent_(responseContent), + level_(level), + dicomMap_(dicomMap) + { + if (dicomMap == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + ExtractOrthancIdentifiers(identifiers_, level, *dicomMap); + } + } + + + void FindResponse::Item::AddMetadata(MetadataType metadata, + const std::string& value) + { + if (metadata_.find(metadata) != metadata_.end()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); // Metadata already present + } + else + { + metadata_[metadata] = value; + } + } + + + bool FindResponse::Item::LookupMetadata(std::string& value, + MetadataType metadata) const + { + std::map<MetadataType, std::string>::const_iterator found = metadata_.find(metadata); + + if (found == metadata_.end()) + { + return false; + } + else + { + value = found->second; + return true; + } + } + + + void FindResponse::Item::ListMetadata(std::set<MetadataType>& target) const + { + target.clear(); + + for (std::map<MetadataType, std::string>::const_iterator it = metadata_.begin(); it != metadata_.end(); ++it) + { + target.insert(it->first); + } + } + + + const DicomMap& FindResponse::Item::GetDicomMap() const + { + if (dicomMap_.get() == NULL) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + return *dicomMap_; + } + } + + + FindResponse::~FindResponse() + { + for (size_t i = 0; i < items_.size(); i++) + { + assert(items_[i] != NULL); + delete items_[i]; + } + } + + + void FindResponse::Add(Item* item /* takes ownership */) + { + if (item == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else + { + items_.push_back(item); + } + } + + + const FindResponse::Item& FindResponse::GetItem(size_t index) const + { + if (index >= items_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(items_[index] != NULL); + return *items_[index]; + } + } + + void FindResponse::Item::AddDicomTag(uint16_t group, uint16_t element, const std::string& value, bool isBinary) + { + if (dicomMap_.get() == NULL) + { + dicomMap_.reset(new DicomMap()); + } + + dicomMap_->SetValue(group, element, value, isBinary); + } + + void FindResponse::Item::AddChild(const std::string& childId) + { + children_.push_back(childId); + } + + +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindResponse.h Fri Apr 26 17:43:22 2024 +0200 @@ -0,0 +1,240 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2024 Osimis S.A., Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" +#include "../../../OrthancFramework/Sources/Enumerations.h" +#include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h" +#include "../ServerEnumerations.h" +#include "OrthancIdentifiers.h" +#include "FindRequest.h" + +#include <boost/noncopyable.hpp> +#include <deque> +#include <map> +#include <set> +#include <list> + + +namespace Orthanc +{ + class FindResponse : public boost::noncopyable + { + public: + + // TODO-FIND: does it actually make sense to retrieve revisions for metadata and attachments ? + class StringWithRevision + { + private: + std::string value_; + int64_t revision_; + public: + StringWithRevision(const std::string& value, + int64_t revision) : + value_(value), + revision_(revision) + { + } + + StringWithRevision(const StringWithRevision& other) : + value_(other.value_), + revision_(other.revision_) + { + } + + StringWithRevision() : + revision_(-1) + { + } + + const std::string& GetValue() const + { + return value_; + } + + int64_t GetRevision() const + { + return revision_; + } + }; + + + class Item : public boost::noncopyable + { + private: + FindRequest::ResponseContent responseContent_; // what has been requested + ResourceType level_; + std::string resourceId_; + std::string parent_; + OrthancIdentifiers identifiers_; // TODO-FIND: not convenient to use here. A simple resourceId seems enough + std::unique_ptr<DicomMap> dicomMap_; + std::list<std::string> children_; + std::string childInstanceId_; + std::set<std::string> labels_; + std::map<MetadataType, std::string> metadata_; + std::map<FileContentType, FileInfo> attachments_; + + public: + Item(FindRequest::ResponseContent responseContent, + ResourceType level, + const OrthancIdentifiers& identifiers) : + responseContent_(responseContent), + level_(level), + identifiers_(identifiers) + { + } + + Item(FindRequest::ResponseContent responseContent, + ResourceType level, + const std::string& resourceId) : + responseContent_(responseContent), + level_(level), + resourceId_(resourceId) + { + } + + Item(FindRequest::ResponseContent responseContent, + ResourceType level, + DicomMap* dicomMap /* takes ownership */); + + ResourceType GetLevel() const + { + return level_; + } + + const std::string& GetResourceId() const + { + return resourceId_; + } + + const OrthancIdentifiers& GetIdentifiers() const + { + return identifiers_; + } + + FindRequest::ResponseContent GetResponseContent() const + { + return responseContent_; + } + + bool HasResponseContent(FindRequest::ResponseContent content) const + { + return (responseContent_ & content) == content; + } + + void AddDicomTag(uint16_t group, uint16_t element, const std::string& value, bool isBinary); + + void AddMetadata(MetadataType metadata, + const std::string& value); + //int64_t revision); + + const std::map<MetadataType, std::string>& GetMetadata() const + { + return metadata_; + } + + bool HasMetadata(MetadataType metadata) const + { + return metadata_.find(metadata) != metadata_.end(); + } + + bool LookupMetadata(std::string& value, /* int64_t revision, */ + MetadataType metadata) const; + + void ListMetadata(std::set<MetadataType>& metadata) const; + + bool HasDicomMap() const + { + return dicomMap_.get() != NULL; + } + + const DicomMap& GetDicomMap() const; + + void AddChild(const std::string& childId); + + const std::list<std::string>& GetChildren() const + { + return children_; + } + + void SetParent(const std::string& parent) + { + parent_ = parent; + } + + const std::string& GetParent() const + { + return parent_; + } + + void AddLabel(const std::string& label) + { + labels_.insert(label); + } + + const std::set<std::string>& GetLabels() const + { + return labels_; + } + + void AddAttachment(const FileInfo& attachment) + { + attachments_[attachment.GetContentType()] = attachment; + } + + const std::map<FileContentType, FileInfo>& GetAttachments() const + { + return attachments_; + } + + bool LookupAttachment(FileInfo& target, FileContentType type) const + { + std::map<FileContentType, FileInfo>::const_iterator it = attachments_.find(type); + if (it != attachments_.end()) + { + target = it->second; + return true; + } + + return false; + } + + // TODO-FIND: add other getters and setters + }; + + private: + std::deque<Item*> items_; + + public: + ~FindResponse(); + + void Add(Item* item /* takes ownership */); + + size_t GetSize() const + { + return items_.size(); + } + + const Item& GetItem(size_t index) const; + }; +}
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Fri Apr 26 17:43:22 2024 +0200 @@ -28,6 +28,8 @@ #include "../ExportedResource.h" #include "../Search/ISqlLookupFormatter.h" #include "../ServerIndexChange.h" +#include "FindRequest.h" +#include "FindResponse.h" #include "IDatabaseListener.h" #include <list> @@ -350,6 +352,14 @@ int64_t& instancesCount, int64_t& compressedSize, int64_t& uncompressedSize) = 0; + + /** + * Primitives introduced in Orthanc 1.12.4 + **/ + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector<DatabaseConstraint>& normalized) = 0; };
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/OrthancIdentifiers.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -0,0 +1,242 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2024 Osimis S.A., Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "FindRequest.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + + +namespace Orthanc +{ + OrthancIdentifiers::OrthancIdentifiers(const OrthancIdentifiers& other) + { + if (other.HasPatientId()) + { + SetPatientId(other.GetPatientId()); + } + + if (other.HasStudyId()) + { + SetStudyId(other.GetStudyId()); + } + + if (other.HasSeriesId()) + { + SetSeriesId(other.GetSeriesId()); + } + + if (other.HasInstanceId()) + { + SetInstanceId(other.GetInstanceId()); + } + } + + + void OrthancIdentifiers::SetPatientId(const std::string& id) + { + if (HasPatientId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + patientId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetPatientId() const + { + if (HasPatientId()) + { + return *patientId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetStudyId(const std::string& id) + { + if (HasStudyId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + studyId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetStudyId() const + { + if (HasStudyId()) + { + return *studyId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetSeriesId(const std::string& id) + { + if (HasSeriesId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + seriesId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetSeriesId() const + { + if (HasSeriesId()) + { + return *seriesId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetInstanceId(const std::string& id) + { + if (HasInstanceId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + instanceId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetInstanceId() const + { + if (HasInstanceId()) + { + return *instanceId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + ResourceType OrthancIdentifiers::DetectLevel() const + { + if (HasPatientId() && + !HasStudyId() && + !HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Patient; + } + else if (HasPatientId() && + HasStudyId() && + !HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Study; + } + else if (HasPatientId() && + HasStudyId() && + HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Series; + } + else if (HasPatientId() && + HasStudyId() && + HasSeriesId() && + HasInstanceId()) + { + return ResourceType_Instance; + } + else + { + throw OrthancException(ErrorCode_InexistentItem); + } + } + + + void OrthancIdentifiers::SetLevel(ResourceType level, + const std::string id) + { + switch (level) + { + case ResourceType_Patient: + SetPatientId(id); + break; + + case ResourceType_Study: + SetStudyId(id); + break; + + case ResourceType_Series: + SetSeriesId(id); + break; + + case ResourceType_Instance: + SetInstanceId(id); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + std::string OrthancIdentifiers::GetLevel(ResourceType level) const + { + switch (level) + { + case ResourceType_Patient: + return GetPatientId(); + + case ResourceType_Study: + return GetStudyId(); + + case ResourceType_Series: + return GetSeriesId(); + + case ResourceType_Instance: + return GetInstanceId(); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/OrthancIdentifiers.h Fri Apr 26 17:43:22 2024 +0200 @@ -0,0 +1,92 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2024 Osimis S.A., Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../OrthancFramework/Sources/Enumerations.h" + +#include <boost/noncopyable.hpp> +#include <string> + + +namespace Orthanc +{ + class OrthancIdentifiers : public boost::noncopyable + { + private: + std::unique_ptr<std::string> patientId_; + std::unique_ptr<std::string> studyId_; + std::unique_ptr<std::string> seriesId_; + std::unique_ptr<std::string> instanceId_; + + public: + OrthancIdentifiers() + { + } + + OrthancIdentifiers(const OrthancIdentifiers& other); + + void SetPatientId(const std::string& id); + + bool HasPatientId() const + { + return patientId_.get() != NULL; + } + + const std::string& GetPatientId() const; + + void SetStudyId(const std::string& id); + + bool HasStudyId() const + { + return studyId_.get() != NULL; + } + + const std::string& GetStudyId() const; + + void SetSeriesId(const std::string& id); + + bool HasSeriesId() const + { + return seriesId_.get() != NULL; + } + + const std::string& GetSeriesId() const; + + void SetInstanceId(const std::string& id); + + bool HasInstanceId() const + { + return instanceId_.get() != NULL; + } + + const std::string& GetInstanceId() const; + + ResourceType DetectLevel() const; + + void SetLevel(ResourceType level, + const std::string id); + + std::string GetLevel(ResourceType level) const; + }; +}
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -28,6 +28,7 @@ #include "../../../OrthancFramework/Sources/SQLite/Transaction.h" #include "../Search/ISqlLookupFormatter.h" #include "../ServerToolbox.h" +#include "Compatibility/GenericFind.h" #include "Compatibility/ICreateInstance.h" #include "Compatibility/IGetChildrenMetadata.h" #include "Compatibility/ILookupResourceAndParent.h" @@ -1137,6 +1138,182 @@ target.insert(s.ColumnString(0)); } } + + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE + { +#if 0 + Compatibility::GenericFind find(*this); + find.Execute(response, request); +#else + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS FilteredResourcesIds"); + s.Run(); + } + + { + + LookupFormatter formatter; + + std::string sqlLookup; + LookupFormatter::Apply(sqlLookup, + formatter, + normalized, + request.GetLevel(), + request.GetLabels(), + request.GetLabelsConstraint(), + (request.HasLimits() ? request.GetLimitsCount() : 0)); // TODO: handles since and count + + if (request.IsResponseIdentifiersOnly()) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE_DYNAMIC(sqlLookup), sqlLookup); + formatter.Bind(statement); + + while (statement.Step()) + { + FindResponse::Item* item = new FindResponse::Item(request.GetResponseContent(), + request.GetLevel(), + statement.ColumnString(0)); + response.Add(item); + } + } + else + { + std::map<std::string, FindResponse::Item*> items; // cache to the response items + + {// first create a temporary table that with the filtered and ordered results + sqlLookup = "CREATE TEMPORARY TABLE FilteredResourcesIds AS " + sqlLookup; + + SQLite::Statement statement(db_, SQLITE_FROM_HERE_DYNAMIC(sqlLookup), sqlLookup); + formatter.Bind(statement); + statement.Run(); + } + + { + // create the response item with the public ids only + SQLite::Statement statement(db_, SQLITE_FROM_HERE, "SELECT publicId FROM FilteredResourcesIds"); + formatter.Bind(statement); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + FindResponse::Item* item = new FindResponse::Item(request.GetResponseContent(), + request.GetLevel(), + resourceId); + items[resourceId] = item; + response.Add(item); + } + } + + // request Each response content through INNER JOIN with the temporary table + if (request.HasResponseContent(FindRequest::ResponseContent_MainDicomTags)) + { + // TODO-FIND: handle the case where we request tags from multiple levels + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT publicId, tagGroup, tagElement, value FROM MainDicomTags AS tags " + " INNER JOIN FilteredResourcesIds ON tags.id = FilteredResourcesIds.internalId"); + formatter.Bind(statement); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + items[resourceId]->AddDicomTag(statement.ColumnInt(1), + statement.ColumnInt(2), + statement.ColumnString(3), false); + } + } + + if (request.HasResponseContent(FindRequest::ResponseContent_Children)) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, childLevel.publicId AS childPublicId " + "FROM Resources as currentLevel " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = currentLevel.internalId " + " INNER JOIN Resources childLevel ON childLevel.parentId = currentLevel.internalId"); + formatter.Bind(statement); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + items[resourceId]->AddChild(statement.ColumnString(1)); + } + } + + if (request.HasResponseContent(FindRequest::ResponseContent_Parent)) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, parentLevel.publicId AS parentPublicId " + "FROM Resources as currentLevel " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = currentLevel.internalId " + " INNER JOIN Resources parentLevel ON currentLevel.parentId = parentLevel.internalId"); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + items[resourceId]->SetParent(statement.ColumnString(1)); + } + } + + if (request.HasResponseContent(FindRequest::ResponseContent_Metadata)) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, metadata.type, metadata.value " + "FROM Metadata " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = Metadata.id"); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + items[resourceId]->AddMetadata(static_cast<MetadataType>(statement.ColumnInt(1)), + statement.ColumnString(2)); + } + } + + if (request.HasResponseContent(FindRequest::ResponseContent_Labels)) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, label " + "FROM Labels " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = Labels.id"); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + items[resourceId]->AddLabel(statement.ColumnString(1)); + } + } + + if (request.HasResponseContent(FindRequest::ResponseContent_Attachments)) + { + SQLite::Statement statement(db_, SQLITE_FROM_HERE, + "SELECT filtered.publicId, uuid, fileType, uncompressedSize, compressionType, compressedSize, " + " uncompressedMD5, compressedMD5 " + "FROM AttachedFiles " + " INNER JOIN FilteredResourcesIds filtered ON filtered.internalId = AttachedFiles.id"); + + while (statement.Step()) + { + const std::string& resourceId = statement.ColumnString(0); + FileInfo attachment = FileInfo(statement.ColumnString(1), + static_cast<FileContentType>(statement.ColumnInt(2)), + statement.ColumnInt64(3), + statement.ColumnString(6), + static_cast<CompressionType>(statement.ColumnInt(4)), + statement.ColumnInt64(5), + statement.ColumnString(7)); + items[resourceId]->AddAttachment(attachment); + }; + } + + // TODO-FIND: implement other responseContent: ResponseContent_ChildInstanceId, ResponseContent_ChildrenMetadata (later: ResponseContent_IsStable) + + } + } + +#endif + } };
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -501,6 +501,38 @@ } } + void StatelessDatabaseOperations::NormalizeLookup(std::vector<DatabaseConstraint>& target, + const FindRequest& findRequest) const + { + assert(mainDicomTagsRegistry_.get() != NULL); + + target.clear(); + target.reserve(findRequest.GetDicomTagConstraintsCount()); + + for (size_t i = 0; i < findRequest.GetDicomTagConstraintsCount(); i++) + { + ResourceType level; + DicomTagType type; + + mainDicomTagsRegistry_->LookupTag(level, type, findRequest.GetDicomTagConstraint(i).GetTag()); + + if (type == DicomTagType_Identifier || + type == DicomTagType_Main) + { + // Use the fact that patient-level tags are copied at the study level + if (level == ResourceType_Patient && + findRequest.GetLevel() != ResourceType_Patient) + { + level = ResourceType_Study; + } + + target.push_back(findRequest.GetDicomTagConstraint(i).ConvertToDatabaseConstraint(level, type)); + } + } + + // TODO-FIND: add metadata constraints + } + class StatelessDatabaseOperations::Transaction : public boost::noncopyable { @@ -3807,4 +3839,111 @@ boost::shared_lock<boost::shared_mutex> lock(mutex_); return db_.GetDatabaseCapabilities().HasLabelsSupport(); } + + + void StatelessDatabaseOperations::ExecuteFind(FindResponse& response, + const FindRequest& request) + { + class Operations : public ReadOnlyOperationsT3<FindResponse&, const FindRequest&, const std::vector<DatabaseConstraint>&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + std::vector<DatabaseConstraint> normalized; + NormalizeLookup(normalized, request); + + Operations operations; + operations.Apply(*this, response, request, normalized); + } + + // TODO-FIND: we reuse the ExpandedResource class to reuse Serialization code from ExpandedResource + // But, finally, we might just get rid of ExpandedResource and replace it by FindResponse + ExpandedResource::ExpandedResource(const FindResponse::Item& item) : + id_(item.GetResourceId()), + level_(item.GetLevel()), + isStable_(false), + expectedNumberOfInstances_(0), + fileSize_(0), + indexInSeries_(0) + { + if (item.HasResponseContent(FindRequest::ResponseContent_MainDicomTags)) + { + tags_.Assign(item.GetDicomMap()); + } + + if (item.HasResponseContent(FindRequest::ResponseContent_Children)) + { + childrenIds_ = item.GetChildren(); + } + + if (item.HasResponseContent(FindRequest::ResponseContent_Parent)) + { + parentId_ = item.GetParent(); + } + + if (item.HasResponseContent(FindRequest::ResponseContent_Metadata)) + { + metadata_ = item.GetMetadata(); + std::string value; + if (item.LookupMetadata(value, MetadataType_MainDicomTagsSignature)) + { + mainDicomTagsSignature_ = value; + } + if (item.LookupMetadata(value, MetadataType_AnonymizedFrom)) + { + anonymizedFrom_ = value; + } + if (item.LookupMetadata(value, MetadataType_ModifiedFrom)) + { + modifiedFrom_ = value; + } + if (item.LookupMetadata(value, MetadataType_LastUpdate)) + { + lastUpdate_ = value; + } + if (item.GetLevel() == ResourceType_Series) + { + if (item.LookupMetadata(value, MetadataType_Series_ExpectedNumberOfInstances)) + { + expectedNumberOfInstances_ = boost::lexical_cast<int>(value); + } + } + if (item.GetLevel() == ResourceType_Instance) + { + if (item.LookupMetadata(value, MetadataType_Instance_IndexInSeries)) + { + indexInSeries_ = boost::lexical_cast<int>(value); + } + } + } + + if (item.HasResponseContent(FindRequest::ResponseContent_Labels)) + { + labels_ = item.GetLabels(); + } + + if (item.HasResponseContent(FindRequest::ResponseContent_Attachments)) + { + FileInfo attachment; + if (item.LookupAttachment(attachment, FileContentType_Dicom)) + { + fileSize_ = attachment.GetUncompressedSize(); + fileUuid_ = attachment.GetUuid(); + } + } + + if (item.HasResponseContent(FindRequest::ResponseContent_ChildrenMetadata)) + { + // TODO-FIND: the status_ is normally obtained from transaction.GetSeriesStatus(internalId, i) + // but, that's an heavy operation for something that is rarely used -> we should have dedicated SQL code + // to compute it AFAP and we should compute it only if the user request it ! + } + + // TODO-FIND: continue: isStable_, status_ + } }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Fri Apr 26 17:43:22 2024 +0200 @@ -80,6 +80,8 @@ { } + ExpandedResource(const FindResponse::Item& item); + void SetResource(ResourceType level, const std::string& id) { @@ -111,15 +113,26 @@ enum ExpandResourceFlags { ExpandResourceFlags_None = 0, + // used to fetch from DB and for output ExpandResourceFlags_IncludeMetadata = (1 << 0), ExpandResourceFlags_IncludeChildren = (1 << 1), ExpandResourceFlags_IncludeMainDicomTags = (1 << 2), ExpandResourceFlags_IncludeLabels = (1 << 3), - ExpandResourceFlags_Default = (ExpandResourceFlags_IncludeMetadata | - ExpandResourceFlags_IncludeChildren | - ExpandResourceFlags_IncludeMainDicomTags | - ExpandResourceFlags_IncludeLabels) + // only used for output + ExpandResourceFlags_IncludeAllMetadata = (1 << 4), // new in Orthanc 1.12.4 + ExpandResourceFlags_IncludeIsStable = (1 << 5), // new in Orthanc 1.12.4 + + ExpandResourceFlags_DefaultExtract = (ExpandResourceFlags_IncludeMetadata | + ExpandResourceFlags_IncludeChildren | + ExpandResourceFlags_IncludeMainDicomTags | + ExpandResourceFlags_IncludeLabels), + + ExpandResourceFlags_DefaultOutput = (ExpandResourceFlags_IncludeMetadata | + ExpandResourceFlags_IncludeChildren | + ExpandResourceFlags_IncludeMainDicomTags | + ExpandResourceFlags_IncludeLabels | + ExpandResourceFlags_IncludeIsStable) }; class StatelessDatabaseOperations : public boost::noncopyable @@ -376,6 +389,13 @@ { transaction_.ListAllLabels(target); } + + void ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector<DatabaseConstraint>& normalized) + { + transaction_.ExecuteFind(response, request, normalized); + } }; @@ -558,6 +578,9 @@ const DatabaseLookup& source, ResourceType level) const; + void NormalizeLookup(std::vector<DatabaseConstraint>& target, + const FindRequest& findRequest) const; + void ApplyInternal(IReadOnlyOperations* readOperations, IReadWriteOperations* writeOperations); @@ -800,5 +823,8 @@ const std::set<std::string>& labels); bool HasLabelsSupport(); + + void ExecuteFind(FindResponse& response, + const FindRequest& request); }; }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -127,7 +127,7 @@ // List all the patients, studies, series or instances ---------------------- - static void AnswerListOfResources(RestApiOutput& output, + static void AnswerListOfResources1(RestApiOutput& output, ServerContext& context, const std::list<std::string>& resources, const std::map<std::string, std::string>& instancesIds, // optional: the id of an instance for each found resource. @@ -181,7 +181,7 @@ } - static void AnswerListOfResources(RestApiOutput& output, + static void AnswerListOfResources2(RestApiOutput& output, ServerContext& context, const std::list<std::string>& resources, ResourceType level, @@ -194,7 +194,7 @@ std::map<std::string, boost::shared_ptr<DicomMap> > unusedResourcesMainDicomTags; std::map<std::string, boost::shared_ptr<Json::Value> > unusedResourcesDicomAsJson; - AnswerListOfResources(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess); + AnswerListOfResources1(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess); } @@ -224,41 +224,153 @@ ServerIndex& index = OrthancRestApi::GetIndex(call); ServerContext& context = OrthancRestApi::GetContext(call); - std::list<std::string> result; - - std::set<DicomTag> requestedTags; - OrthancRestApi::GetRequestedTags(requestedTags, call); - - if (call.HasArgument("limit") || - call.HasArgument("since")) + if (true) { - if (!call.HasArgument("limit")) + /** + * EXPERIMENTAL VERSION + **/ + + // TODO-FIND: include the FindRequest options parsing in a method (parse from get-arguments and from post payload) + // TODO-FIND: support other values for expand like expand=MainDicomTags,Labels,Parent,SeriesStatus + const bool expand = (call.HasArgument("expand") && + call.GetBooleanArgument("expand", true)); + + std::set<DicomTag> requestedTags; + OrthancRestApi::GetRequestedTags(requestedTags, call); + + const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); + + FindRequest request(resourceType); + + if (expand) { - throw OrthancException(ErrorCode_BadRequest, - "Missing \"limit\" argument for GET request against: " + - call.FlattenUri()); + // compatibility with default expand option + FindRequest::ResponseContent responseContent = static_cast<FindRequest::ResponseContent>(FindRequest::ResponseContent_MainDicomTags | + FindRequest::ResponseContent_Metadata | + FindRequest::ResponseContent_Labels); + + if (requestedTags.size() > 0 && resourceType != ResourceType_Instance) // if we are requesting specific tags that might be outside of the MainDicomTags, we must get a childInstanceId too + { + responseContent = static_cast<FindRequest::ResponseContent>(responseContent | FindRequest::ResponseContent_ChildInstanceId); + } + if (resourceType == ResourceType_Series) + { + responseContent = static_cast<FindRequest::ResponseContent>(responseContent | FindRequest::ResponseContent_ChildrenMetadata); // required for the SeriesStatus + } + if (resourceType != ResourceType_Instance) + { + responseContent = static_cast<FindRequest::ResponseContent>(responseContent | FindRequest::ResponseContent_Children); + } + if (resourceType == ResourceType_Instance) + { + responseContent = static_cast<FindRequest::ResponseContent>(responseContent | FindRequest::ResponseContent_Attachments); // for FileSize & FileUuid + } + if (resourceType != ResourceType_Patient) + { + responseContent = static_cast<FindRequest::ResponseContent>(responseContent | FindRequest::ResponseContent_Parent); + } + + request.SetResponseContent(responseContent); + request.SetRetrieveTagsAtLevel(resourceType, true); + + if (resourceType == ResourceType_Study) + { + request.SetRetrieveTagsAtLevel(ResourceType_Patient, true); + } } - - if (!call.HasArgument("since")) + else + { + request.SetResponseContent(FindRequest::ResponseContent_IdentifiersOnly); + } + + if (call.HasArgument("limit") || + call.HasArgument("since")) { - throw OrthancException(ErrorCode_BadRequest, - "Missing \"since\" argument for GET request against: " + - call.FlattenUri()); + if (!call.HasArgument("limit")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"limit\" argument for GET request against: " + + call.FlattenUri()); + } + + if (!call.HasArgument("since")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"since\" argument for GET request against: " + + call.FlattenUri()); + } + + uint64_t since = boost::lexical_cast<uint64_t>(call.GetArgument("since", "")); + uint64_t limit = boost::lexical_cast<uint64_t>(call.GetArgument("limit", "")); + request.SetLimits(since, limit); } - size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", "")); - size_t limit = boost::lexical_cast<size_t>(call.GetArgument("limit", "")); - index.GetAllUuids(result, resourceType, since, limit); + FindResponse response; + index.ExecuteFind(response, request); + + // TODO-FIND: put this in an AnswerFindResponse method ! + Json::Value answer = Json::arrayValue; + + if (request.IsResponseIdentifiersOnly()) + { + for (size_t i = 0; i < response.GetSize(); i++) + { + std::string resourceId = response.GetItem(i).GetResourceId(); + answer.append(resourceId); + } + } + else + { + for (size_t i = 0; i < response.GetSize(); i++) + { + context.AppendFindResponse(answer, response.GetItem(i), format, requestedTags, true /* allowStorageAccess */); + } + } + + call.GetOutput().AnswerJson(answer); } else { - index.GetAllUuids(result, resourceType); + /** + * VERSION IN ORTHANC <= 1.12.3 + **/ + + std::list<std::string> result; + + std::set<DicomTag> requestedTags; + OrthancRestApi::GetRequestedTags(requestedTags, call); + + if (call.HasArgument("limit") || + call.HasArgument("since")) + { + if (!call.HasArgument("limit")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"limit\" argument for GET request against: " + + call.FlattenUri()); + } + + if (!call.HasArgument("since")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"since\" argument for GET request against: " + + call.FlattenUri()); + } + + size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", "")); + size_t limit = boost::lexical_cast<size_t>(call.GetArgument("limit", "")); + index.GetAllUuids(result, resourceType, since, limit); + } + else + { + index.GetAllUuids(result, resourceType); + } + + AnswerListOfResources2(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true), + OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), + requestedTags, + true /* allowStorageAccess */); } - - AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true), - OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), - requestedTags, - true /* allowStorageAccess */); } @@ -3107,7 +3219,7 @@ bool expand, const std::set<DicomTag>& requestedTags) const { - AnswerListOfResources(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags, IsStorageAccessAllowedForAnswers(findStorageAccessMode_)); + AnswerListOfResources1(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags, IsStorageAccessAllowedForAnswers(findStorageAccessMode_)); } }; } @@ -3387,7 +3499,7 @@ a.splice(a.begin(), b); } - AnswerListOfResources(call.GetOutput(), context, a, type, !call.HasArgument("expand") || call.GetBooleanArgument("expand", false), // this "expand" is the only one to have a false default value to keep backward compatibility + AnswerListOfResources2(call.GetOutput(), context, a, type, !call.HasArgument("expand") || call.GetBooleanArgument("expand", false), // this "expand" is the only one to have a false default value to keep backward compatibility OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), requestedTags, true /* allowStorageAccess */);
--- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Sources/Search/DatabaseConstraint.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -152,6 +152,7 @@ const std::vector<std::string>& values, bool caseSensitive, bool mandatory) : + keyType_(DatabaseConstraint::KeyType_DicomTag), level_(level), tag_(tag), isIdentifier_(isIdentifier),
--- a/OrthancServer/Sources/Search/DatabaseConstraint.h Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Sources/Search/DatabaseConstraint.h Fri Apr 26 17:43:22 2024 +0200 @@ -79,9 +79,18 @@ // This class is also used by the "orthanc-databases" project class DatabaseConstraint { + public: + enum KeyType // used for ordering and filters + { + KeyType_DicomTag, + KeyType_Metadata + }; + private: + KeyType keyType_; ResourceType level_; DicomTag tag_; + uint32_t metadataType_; // TODO: implement bool isIdentifier_; ConstraintType constraintType_; std::vector<std::string> values_;
--- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h Fri Apr 26 17:43:22 2024 +0200 @@ -24,6 +24,7 @@ #if ORTHANC_BUILDING_SERVER_LIBRARY == 1 # include "../../../OrthancFramework/Sources/Enumerations.h" +# include "../Search/LabelsConstraint.h" #else # include <Enumerations.h> #endif @@ -35,13 +36,6 @@ { class DatabaseConstraint; - enum LabelsConstraint - { - LabelsConstraint_All, - LabelsConstraint_Any, - LabelsConstraint_None - }; - // This class is also used by the "orthanc-databases" project class ISqlLookupFormatter : public boost::noncopyable {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Search/LabelsConstraint.h Fri Apr 26 17:43:22 2024 +0200 @@ -0,0 +1,33 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2024 Osimis S.A., Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +namespace Orthanc +{ + enum LabelsConstraint + { + LabelsConstraint_All, + LabelsConstraint_Any, + LabelsConstraint_None + }; +} \ No newline at end of file
--- a/OrthancServer/Sources/ServerContext.cpp Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Fri Apr 26 17:43:22 2024 +0200 @@ -2125,124 +2125,137 @@ static void SerializeExpandedResource(Json::Value& target, const ExpandedResource& resource, DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags) + const std::set<DicomTag>& requestedTags, + ExpandResourceFlags expandFlags) { target = Json::objectValue; target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true); target["ID"] = resource.GetPublicId(); - switch (resource.GetLevel()) - { - case ResourceType_Patient: - break; - - case ResourceType_Study: - target["ParentPatient"] = resource.parentId_; - break; - - case ResourceType_Series: - target["ParentStudy"] = resource.parentId_; - break; - - case ResourceType_Instance: - target["ParentSeries"] = resource.parentId_; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - switch (resource.GetLevel()) + if (!resource.parentId_.empty()) { - case ResourceType_Patient: - case ResourceType_Study: - case ResourceType_Series: + switch (resource.GetLevel()) { - Json::Value c = Json::arrayValue; - - for (std::list<std::string>::const_iterator - it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it) - { - c.append(*it); - } - - if (resource.GetLevel() == ResourceType_Patient) - { - target["Studies"] = c; - } - else if (resource.GetLevel() == ResourceType_Study) - { - target["Series"] = c; - } - else - { - target["Instances"] = c; - } - break; + case ResourceType_Patient: + break; + + case ResourceType_Study: + target["ParentPatient"] = resource.parentId_; + break; + + case ResourceType_Series: + target["ParentStudy"] = resource.parentId_; + break; + + case ResourceType_Instance: + target["ParentSeries"] = resource.parentId_; + break; + + default: + throw OrthancException(ErrorCode_InternalError); } - - case ResourceType_Instance: - break; - - default: - throw OrthancException(ErrorCode_InternalError); } - switch (resource.GetLevel()) + if ((expandFlags & ExpandResourceFlags_IncludeChildren) != 0) { - case ResourceType_Patient: - case ResourceType_Study: - break; - - case ResourceType_Series: - if (resource.expectedNumberOfInstances_ < 0) + switch (resource.GetLevel()) + { + case ResourceType_Patient: + case ResourceType_Study: + case ResourceType_Series: { - target["ExpectedNumberOfInstances"] = Json::nullValue; + Json::Value c = Json::arrayValue; + + for (std::list<std::string>::const_iterator + it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it) + { + c.append(*it); + } + + if (resource.GetLevel() == ResourceType_Patient) + { + target["Studies"] = c; + } + else if (resource.GetLevel() == ResourceType_Study) + { + target["Series"] = c; + } + else + { + target["Instances"] = c; + } + break; } - else - { - target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_; - } - target["Status"] = resource.status_; - break; - - case ResourceType_Instance: + + case ResourceType_Instance: + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + if ((expandFlags & ExpandResourceFlags_IncludeMetadata) != 0) + { + switch (resource.GetLevel()) { - target["FileSize"] = static_cast<unsigned int>(resource.fileSize_); - target["FileUuid"] = resource.fileUuid_; - - if (resource.indexInSeries_ < 0) + case ResourceType_Patient: + case ResourceType_Study: + break; + + case ResourceType_Series: + if (resource.expectedNumberOfInstances_ < 0) + { + target["ExpectedNumberOfInstances"] = Json::nullValue; + } + else + { + target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_; + } + target["Status"] = resource.status_; + break; + + case ResourceType_Instance: { - target["IndexInSeries"] = Json::nullValue; + target["FileSize"] = static_cast<unsigned int>(resource.fileSize_); + target["FileUuid"] = resource.fileUuid_; + + if (resource.indexInSeries_ < 0) + { + target["IndexInSeries"] = Json::nullValue; + } + else + { + target["IndexInSeries"] = resource.indexInSeries_; + } + + break; } - else - { - target["IndexInSeries"] = resource.indexInSeries_; - } - - break; + + default: + throw OrthancException(ErrorCode_InternalError); } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - if (!resource.anonymizedFrom_.empty()) - { - target["AnonymizedFrom"] = resource.anonymizedFrom_; - } - if (!resource.modifiedFrom_.empty()) - { - target["ModifiedFrom"] = resource.modifiedFrom_; + if (!resource.anonymizedFrom_.empty()) + { + target["AnonymizedFrom"] = resource.anonymizedFrom_; + } + + if (!resource.modifiedFrom_.empty()) + { + target["ModifiedFrom"] = resource.modifiedFrom_; + } } if (resource.GetLevel() == ResourceType_Patient || resource.GetLevel() == ResourceType_Study || resource.GetLevel() == ResourceType_Series) { - target["IsStable"] = resource.isStable_; + if ((expandFlags & ExpandResourceFlags_IncludeIsStable) != 0) + { + target["IsStable"] = resource.isStable_; + } if (!resource.lastUpdate_.empty()) { @@ -2250,38 +2263,42 @@ } } - // serialize tags - - static const char* const MAIN_DICOM_TAGS = "MainDicomTags"; - static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags"; - - DicomMap mainDicomTags; - resource.GetMainDicomTags().ExtractResourceInformation(mainDicomTags, resource.GetLevel()); - - target[MAIN_DICOM_TAGS] = Json::objectValue; - FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format); - - if (resource.GetLevel() == ResourceType_Study) + if ((expandFlags & ExpandResourceFlags_IncludeMainDicomTags) != 0) { - DicomMap patientMainDicomTags; - resource.GetMainDicomTags().ExtractPatientInformation(patientMainDicomTags); - - target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue; - FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format); + // serialize tags + + static const char* const MAIN_DICOM_TAGS = "MainDicomTags"; + static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags"; + + DicomMap mainDicomTags; + resource.GetMainDicomTags().ExtractResourceInformation(mainDicomTags, resource.GetLevel()); + + target[MAIN_DICOM_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], mainDicomTags, format); + + if (resource.GetLevel() == ResourceType_Study) + { + DicomMap patientMainDicomTags; + resource.GetMainDicomTags().ExtractPatientInformation(patientMainDicomTags); + + target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format); + } + + if (requestedTags.size() > 0) + { + static const char* const REQUESTED_TAGS = "RequestedTags"; + + DicomMap tags; + resource.GetMainDicomTags().ExtractTags(tags, requestedTags); + + target[REQUESTED_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(target[REQUESTED_TAGS], tags, format); + + } } - if (requestedTags.size() > 0) - { - static const char* const REQUESTED_TAGS = "RequestedTags"; - - DicomMap tags; - resource.GetMainDicomTags().ExtractTags(tags, requestedTags); - - target[REQUESTED_TAGS] = Json::objectValue; - FromDcmtkBridge::ToJson(target[REQUESTED_TAGS], tags, format); - - } - + if ((expandFlags & ExpandResourceFlags_IncludeLabels) != 0) { Json::Value labels = Json::arrayValue; @@ -2292,6 +2309,19 @@ target["Labels"] = labels; } + + // new in Orthanc 1.12.4 + if ((expandFlags & ExpandResourceFlags_IncludeAllMetadata) != 0) + { + Json::Value metadata = Json::objectValue; + + for (std::map<MetadataType, std::string>::const_iterator it = resource.metadata_.begin(); it != resource.metadata_.end(); ++it) + { + metadata[EnumerationToString(it->first)] = it->second; + } + + target["Metadata"] = metadata; + } } @@ -2537,9 +2567,9 @@ { ExpandedResource resource; - if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceFlags_Default, allowStorageAccess)) + if (ExpandResource(resource, publicId, mainDicomTags, instanceId, dicomAsJson, level, requestedTags, ExpandResourceFlags_DefaultExtract, allowStorageAccess)) { - SerializeExpandedResource(target, resource, format, requestedTags); + SerializeExpandedResource(target, resource, format, requestedTags, ExpandResourceFlags_DefaultOutput); return true; } @@ -2687,4 +2717,41 @@ return elapsed.total_seconds(); } + void ServerContext::AppendFindResponse(Json::Value& target, + const FindResponse::Item& item, + DicomToJsonFormat format, + const std::set<DicomTag>& requestedTags, + bool allowStorageAccess) + { + // convert to ExpandedResource to re-use the serialization code TODO-FIND: check if this is the right way to do. shouldn't we copy the code and finally get rid of ExpandedResource ? + ExpandedResource resource(item); + + ExpandResourceFlags expandFlags = ExpandResourceFlags_None; + if (item.HasResponseContent(FindRequest::ResponseContent_Children)) + { + expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeChildren); + } + if (item.HasResponseContent(FindRequest::ResponseContent_Metadata)) + { + expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeAllMetadata | ExpandResourceFlags_IncludeMetadata ); + } + if (item.HasResponseContent(FindRequest::ResponseContent_MainDicomTags)) + { + expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeMainDicomTags); + } + if (item.HasResponseContent(FindRequest::ResponseContent_IsStable)) + { + expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeIsStable); + } + if (item.HasResponseContent(FindRequest::ResponseContent_Labels)) + { + expandFlags = static_cast<ExpandResourceFlags>(expandFlags | ExpandResourceFlags_IncludeLabels); + } + + Json::Value jsonItem; + SerializeExpandedResource(jsonItem, resource, format, requestedTags, expandFlags); + target.append(jsonItem); + } + + }
--- a/OrthancServer/Sources/ServerContext.h Fri Apr 26 17:41:40 2024 +0200 +++ b/OrthancServer/Sources/ServerContext.h Fri Apr 26 17:43:22 2024 +0200 @@ -607,6 +607,12 @@ ExpandResourceFlags expandFlags, bool allowStorageAccess); + void AppendFindResponse(Json::Value& target, + const FindResponse::Item& item, + DicomToJsonFormat format, + const std::set<DicomTag>& requestedTags, + bool allowStorageAccess); + FindStorageAccessMode GetFindStorageAccessMode() const { return findStorageAccessMode_;