# HG changeset patch # User Sebastien Jodogne # Date 1715157057 -7200 # Node ID 3f24eb4013d8b5fcb1352f24d3d86064622cd34e # Parent e4e7ca3d206e64585b5e33673268970414a45c62# Parent c2a2fb8e868d6fd21a3f5af57dccd6a776b18e76 integration mainline->find-refactoring diff -r c2a2fb8e868d -r 3f24eb4013d8 NEWS --- a/NEWS Wed May 08 10:30:34 2024 +0200 +++ b/NEWS Wed May 08 10:30:57 2024 +0200 @@ -10,6 +10,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 ------- diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancFramework/Sources/SQLite/Connection.h --- a/OrthancFramework/Sources/SQLite/Connection.h Wed May 08 10:30:34 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/Connection.h Wed May 08 10:30:57 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 { diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancFramework/Sources/SQLite/StatementId.cpp --- a/OrthancFramework/Sources/SQLite/StatementId.cpp Wed May 08 10:30:34 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/StatementId.cpp Wed May 08 10:30:57 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_; } } } diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancFramework/Sources/SQLite/StatementId.h --- a/OrthancFramework/Sources/SQLite/StatementId.h Wed May 08 10:30:34 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/StatementId.h Wed May 08 10:30:57 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; }; } diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/CMakeLists.txt --- a/OrthancServer/CMakeLists.txt Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/CMakeLists.txt Wed May 08 10:30:57 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 diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Plugins/Engine/OrthancPluginDatabaseV3.cpp diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Wed May 08 10:30:57 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,35 @@ { ListLabelsInternal(target, false, -1); } + + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector& normalized) ORTHANC_OVERRIDE + { + // TODO-FIND + throw OrthancException(ErrorCode_NotImplemented); + } + + + virtual void ExecuteFind(std::list& identifiers, + const FindRequest& request, + const std::vector& normalized) ORTHANC_OVERRIDE + { + // TODO-FIND + Compatibility::GenericFind find(*this); + find.ExecuteFind(identifiers, request, normalized); + } + + + virtual void ExecuteExpand(FindResponse& response, + const FindRequest& request, + const std::string& identifier) ORTHANC_OVERRIDE + { + // TODO-FIND + Compatibility::GenericFind find(*this); + find.ExecuteExpand(response, request, identifier); + } }; @@ -1489,4 +1519,10 @@ return dbCapabilities_; } } + + + bool OrthancPluginDatabaseV4::HasIntegratedFind() const + { + return false; // TODO-FIND + } } diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h --- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Wed May 08 10:30:57 2024 +0200 @@ -92,6 +92,8 @@ virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE; + + virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE; }; } diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h --- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h Wed May 08 10:30:57 2024 +0200 @@ -744,7 +744,7 @@ } OrthancPluginResourceType; - + /** * The supported types of changes that can be signaled to the change callback. * @ingroup Callbacks diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Resources/Orthanc.doxygen --- a/OrthancServer/Resources/Orthanc.doxygen Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Resources/Orthanc.doxygen Wed May 08 10:30:57 2024 +0200 @@ -755,6 +755,7 @@ # Note: If this tag is empty the current directory is searched. INPUT = @CMAKE_SOURCE_DIR@/../OrthancFramework/Sources \ + @CMAKE_SOURCE_DIR@/Plugins/Engine \ @CMAKE_SOURCE_DIR@/Sources # This tag can be used to specify the character encoding of the source files diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp --- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Wed May 08 10:30:57 2024 +0200 @@ -23,6 +23,7 @@ #include "BaseDatabaseWrapper.h" #include "../../../OrthancFramework/Sources/OrthancException.h" +#include "Compatibility/GenericFind.h" namespace Orthanc { @@ -45,6 +46,32 @@ } + void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector& normalized) + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + + void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(std::list& identifiers, + const FindRequest& request, + const std::vector& normalized) + { + Compatibility::GenericFind find(*this); + find.ExecuteFind(identifiers, request, normalized); + } + + + void BaseDatabaseWrapper::BaseTransaction::ExecuteExpand(FindResponse& response, + const FindRequest& request, + const std::string& identifier) + { + Compatibility::GenericFind find(*this); + find.ExecuteExpand(response, request, identifier); + } + + uint64_t BaseDatabaseWrapper::MeasureLatency() { throw OrthancException(ErrorCode_NotImplemented); // only implemented in V4 diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/BaseDatabaseWrapper.h --- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Wed May 08 10:30:57 2024 +0200 @@ -46,8 +46,25 @@ int64_t& instancesCount, int64_t& compressedSize, int64_t& uncompressedSize) ORTHANC_OVERRIDE; + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector& normalized) ORTHANC_OVERRIDE; + + virtual void ExecuteFind(std::list& identifiers, + const FindRequest& request, + const std::vector& normalized) ORTHANC_OVERRIDE; + + virtual void ExecuteExpand(FindResponse& response, + const FindRequest& request, + const std::string& identifier) ORTHANC_OVERRIDE; }; virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE; + + virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE + { + return false; + } }; } diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/Compatibility/GenericFind.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp Wed May 08 10:30:57 2024 +0200 @@ -0,0 +1,329 @@ +/** + * 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 . + **/ + + +#include "GenericFind.h" + +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../../../OrthancFramework/Sources/OrthancException.h" + +#include + + +namespace Orthanc +{ + namespace Compatibility + { + void GenericFind::ExecuteFind(std::list& identifiers, + const FindRequest& request, + const std::vector& normalized) + { + if (!request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId() && + request.GetDicomTagConstraintsCount() == 0 && + request.GetMetadataConstraintsCount() == 0 && + request.GetLabels().empty() && + request.GetOrdering().empty()) + { + if (request.HasLimits()) + { + transaction_.GetAllPublicIds(identifiers, request.GetLevel(), request.GetLimitsSince(), request.GetLimitsCount()); + } + else + { + transaction_.GetAllPublicIds(identifiers, request.GetLevel()); + } + } + else + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + } + + + void GenericFind::RetrieveMainDicomTags(FindResponse::Resource& target, + ResourceType level, + int64_t internalId) + { + DicomMap m; + transaction_.GetMainDicomTags(m, internalId); + + DicomArray a(m); + for (size_t i = 0; i < a.GetSize(); i++) + { + const DicomElement& element = a.GetElement(i); + if (element.GetValue().IsString()) + { + target.AddStringDicomTag(level, element.GetTag().GetGroup(), + element.GetTag().GetElement(), element.GetValue().GetContent()); + } + else + { + throw OrthancException(ErrorCode_BadParameterType); + } + } + } + + + static ResourceType GetTopLevelOfInterest(const FindRequest& request) + { + switch (request.GetLevel()) + { + case ResourceType_Patient: + return ResourceType_Patient; + + case ResourceType_Study: + if (request.IsRetrieveMainDicomTags(ResourceType_Patient) || + request.IsRetrieveMetadata(ResourceType_Patient)) + { + return ResourceType_Patient; + } + else + { + return ResourceType_Study; + } + + case ResourceType_Series: + if (request.IsRetrieveMainDicomTags(ResourceType_Patient) || + request.IsRetrieveMetadata(ResourceType_Patient)) + { + return ResourceType_Patient; + } + else if (request.IsRetrieveMainDicomTags(ResourceType_Study) || + request.IsRetrieveMetadata(ResourceType_Study)) + { + return ResourceType_Study; + } + else + { + return ResourceType_Series; + } + + case ResourceType_Instance: + if (request.IsRetrieveMainDicomTags(ResourceType_Patient) || + request.IsRetrieveMetadata(ResourceType_Patient)) + { + return ResourceType_Patient; + } + else if (request.IsRetrieveMainDicomTags(ResourceType_Study) || + request.IsRetrieveMetadata(ResourceType_Study)) + { + return ResourceType_Study; + } + else if (request.IsRetrieveMainDicomTags(ResourceType_Series) || + request.IsRetrieveMetadata(ResourceType_Series)) + { + return ResourceType_Series; + } + else + { + return ResourceType_Instance; + } + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void GenericFind::ExecuteExpand(FindResponse& response, + const FindRequest& request, + const std::string& identifier) + { + int64_t internalId; + ResourceType level; + std::string parent; + + if (request.IsRetrieveParentIdentifier()) + { + if (!transaction_.LookupResourceAndParent(internalId, level, parent, identifier)) + { + return; // The resource is not available anymore + } + + if (level == ResourceType_Patient) + { + if (!parent.empty()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + else + { + if (parent.empty()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + } + else + { + if (!transaction_.LookupResource(internalId, level, identifier)) + { + return; // The resource is not available anymore + } + } + + if (level != request.GetLevel()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + std::unique_ptr resource(new FindResponse::Resource(request.GetLevel(), identifier)); + + if (request.IsRetrieveParentIdentifier()) + { + assert(!parent.empty()); + resource->SetParentIdentifier(parent); + } + + { + int64_t currentId = internalId; + ResourceType currentLevel = level; + const ResourceType topLevel = GetTopLevelOfInterest(request); + + for (;;) + { + if (request.IsRetrieveMainDicomTags(currentLevel)) + { + RetrieveMainDicomTags(*resource, currentLevel, currentId); + } + + if (request.IsRetrieveMetadata(currentLevel)) + { + transaction_.GetAllMetadata(resource->GetMetadata(currentLevel), currentId); + } + + if (currentLevel == topLevel) + { + break; + } + else + { + int64_t parentId; + if (transaction_.LookupParent(parentId, currentId)) + { + currentId = parentId; + currentLevel = GetParentResourceType(currentLevel); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + } + } + + if (request.IsRetrieveLabels()) + { + transaction_.ListLabels(resource->GetLabels(), internalId); + } + + if (request.IsRetrieveAttachments()) + { + std::set attachments; + transaction_.ListAvailableAttachments(attachments, internalId); + + for (std::set::const_iterator it = attachments.begin(); it != attachments.end(); ++it) + { + FileInfo info; + int64_t revision; + if (transaction_.LookupAttachment(info, revision, internalId, *it) && + info.GetContentType() == *it) + { + resource->AddAttachment(info); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + } + + if (request.IsRetrieveChildrenIdentifiers()) + { + std::list children; + transaction_.GetChildrenPublicId(children, internalId); + + for (std::list::const_iterator it = children.begin(); it != children.end(); ++it) + { + resource->AddChildIdentifier(*it); + } + } + + for (std::set::const_iterator it = request.GetRetrieveChildrenMetadata().begin(); + it != request.GetRetrieveChildrenMetadata().end(); ++it) + { + std::list values; + transaction_.GetChildrenMetadata(values, internalId, *it); + resource->AddChildrenMetadata(*it, values); + } + + if (!request.GetRetrieveAttachmentOfOneInstance().empty()) + { + std::set todo = request.GetRetrieveAttachmentOfOneInstance(); + std::stack< std::pair > candidates; + candidates.push(std::make_pair(level, internalId)); + + while (!todo.empty() && + !candidates.empty()) + { + std::pair top = candidates.top(); + candidates.pop(); + + if (top.first == ResourceType_Instance) + { + std::set nextTodo; + + for (std::set::const_iterator it = todo.begin(); it != todo.end(); ++it) + { + FileInfo attachment; + int64_t revision; + if (transaction_.LookupAttachment(attachment, revision, top.second, *it)) + { + resource->AddAttachmentOfOneInstance(attachment); + } + else + { + nextTodo.insert(*it); + } + } + + todo = nextTodo; + } + else + { + std::list children; + transaction_.GetChildrenInternalId(children, top.second); + for (std::list::const_iterator it = children.begin(); it != children.end(); ++it) + { + candidates.push(std::make_pair(GetChildResourceType(top.first), *it)); + } + } + } + } + + response.Add(resource.release()); + } + } +} diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/Compatibility/GenericFind.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.h Wed May 08 10:30:57 2024 +0200 @@ -0,0 +1,55 @@ +/** + * 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 . + **/ + + +#pragma once + +#include "../IDatabaseWrapper.h" + +namespace Orthanc +{ + namespace Compatibility + { + class GenericFind : public boost::noncopyable + { + private: + IDatabaseWrapper::ITransaction& transaction_; + + void RetrieveMainDicomTags(FindResponse::Resource& target, + ResourceType level, + int64_t internalId); + + public: + GenericFind(IDatabaseWrapper::ITransaction& transaction) : + transaction_(transaction) + { + } + + void ExecuteFind(std::list& identifiers, + const FindRequest& request, + const std::vector& normalized); + + void ExecuteExpand(FindResponse& response, + const FindRequest& request, + const std::string& identifier); + }; + } +} diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/FindRequest.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindRequest.cpp Wed May 08 10:30:57 2024 +0200 @@ -0,0 +1,305 @@ +/** + * 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 . + **/ + + +#include "FindRequest.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + + +#include + +namespace Orthanc +{ + FindRequest::FindRequest(ResourceType level) : + level_(level), + hasLimits_(false), + limitsSince_(0), + limitsCount_(0), + retrieveMainDicomTagsPatients_(false), + retrieveMainDicomTagsStudies_(false), + retrieveMainDicomTagsSeries_(false), + retrieveMainDicomTagsInstances_(false), + retrieveMetadataPatients_(false), + retrieveMetadataStudies_(false), + retrieveMetadataSeries_(false), + retrieveMetadataInstances_(false), + retrieveLabels_(false), + retrieveAttachments_(false), + retrieveParentIdentifier_(false), + retrieveChildrenIdentifiers_(false) + { + } + + + FindRequest::~FindRequest() + { + + for (std::deque::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::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)); + } + + + void FindRequest::SetRetrieveMainDicomTags(ResourceType level, + bool retrieve) + { + if (!IsResourceLevelAboveOrEqual(level, level_)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + switch (level) + { + case ResourceType_Patient: + retrieveMainDicomTagsPatients_ = retrieve; + break; + + case ResourceType_Study: + retrieveMainDicomTagsStudies_ = retrieve; + break; + + case ResourceType_Series: + retrieveMainDicomTagsSeries_ = retrieve; + break; + + case ResourceType_Instance: + retrieveMainDicomTagsInstances_ = retrieve; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + bool FindRequest::IsRetrieveMainDicomTags(ResourceType level) const + { + if (!IsResourceLevelAboveOrEqual(level, level_)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + switch (level) + { + case ResourceType_Patient: + return retrieveMainDicomTagsPatients_; + + case ResourceType_Study: + return retrieveMainDicomTagsStudies_; + + case ResourceType_Series: + return retrieveMainDicomTagsSeries_; + + case ResourceType_Instance: + return retrieveMainDicomTagsInstances_; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + void FindRequest::SetRetrieveMetadata(ResourceType level, + bool retrieve) + { + if (!IsResourceLevelAboveOrEqual(level, level_)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + switch (level) + { + case ResourceType_Patient: + retrieveMetadataPatients_ = retrieve; + break; + + case ResourceType_Study: + retrieveMetadataStudies_ = retrieve; + break; + + case ResourceType_Series: + retrieveMetadataSeries_ = retrieve; + break; + + case ResourceType_Instance: + retrieveMetadataInstances_ = retrieve; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + bool FindRequest::IsRetrieveMetadata(ResourceType level) const + { + if (!IsResourceLevelAboveOrEqual(level, level_)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + switch (level) + { + case ResourceType_Patient: + return retrieveMetadataPatients_; + + case ResourceType_Study: + return retrieveMetadataStudies_; + + case ResourceType_Series: + return retrieveMetadataSeries_; + + case ResourceType_Instance: + return retrieveMetadataInstances_; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + void FindRequest::SetRetrieveParentIdentifier(bool retrieve) + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + retrieveParentIdentifier_ = retrieve; + } + } + + + void FindRequest::SetRetrieveChildrenIdentifiers(bool retrieve) + { + if (level_ == ResourceType_Instance) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + retrieveChildrenIdentifiers_ = retrieve; + } + } + + + void FindRequest::AddRetrieveChildrenMetadata(MetadataType metadata) + { + if (IsRetrieveChildrenMetadata(metadata)) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + retrieveChildrenMetadata_.insert(metadata); + } + } + + + void FindRequest::AddRetrieveAttachmentOfOneInstance(FileContentType type) + { + if (retrieveAttachmentOfOneInstance_.find(type) == retrieveAttachmentOfOneInstance_.end()) + { + retrieveAttachmentOfOneInstance_.insert(type); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } +} diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/FindRequest.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindRequest.h Wed May 08 10:30:57 2024 +0200 @@ -0,0 +1,332 @@ +/** + * 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 . + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomTag.h" +#include "../Search/DatabaseConstraint.h" +#include "../Search/DicomTagConstraint.h" +#include "../Search/ISqlLookupFormatter.h" +#include "../ServerEnumerations.h" +#include "OrthancIdentifiers.h" + +#include +#include +#include +#include +#include + +namespace Orthanc +{ + class FindRequest : public boost::noncopyable + { + public: + /** + + TO DISCUSS: + + (1) 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 + + 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(responseContent | FindRequest::ResponseContent_ChildInstanceId); + } + + + (2) ResponseContent_IsStable = (1 << 8), // This is currently not saved in DB but it could be in the future. + + **/ + + + enum KeyType // used for ordering and filters + { + KeyType_DicomTag, + KeyType_Metadata + }; + + + enum OrderingDirection + { + OrderingDirection_Ascending, + OrderingDirection_Descending + }; + + + class Key + { + private: + KeyType type_; + 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: + explicit Key(const DicomTag& dicomTag) : + type_(KeyType_DicomTag), + dicomTag_(dicomTag), + metadata_(MetadataType_EndUser) + { + } + + explicit Key(MetadataType metadata) : + type_(KeyType_Metadata), + dicomTag_(0, 0), + 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 + { + private: + OrderingDirection direction_; + Key key_; + + public: + Ordering(const Key& key, + OrderingDirection direction) : + direction_(direction), + key_(key) + { + } + + KeyType GetKeyType() const + { + return key_.GetType(); + } + + OrderingDirection GetDirection() const + { + return direction_; + } + + MetadataType GetMetadataType() const + { + return key_.GetMetadataType(); + } + + DicomTag GetDicomTag() const + { + return key_.GetDicomTag(); + } + }; + + + 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::vector dicomTagConstraints_; // All tags filters (note: the order is not important) + std::deque /* TODO-FIND */ metadataConstraints_; // All metadata filters (note: the order is not important) + bool hasLimits_; + uint64_t limitsSince_; + uint64_t limitsCount_; + std::set labels_; + LabelsConstraint labelsContraint_; + std::deque ordering_; // The ordering criteria (note: the order is important !) + + bool retrieveMainDicomTagsPatients_; + bool retrieveMainDicomTagsStudies_; + bool retrieveMainDicomTagsSeries_; + bool retrieveMainDicomTagsInstances_; + bool retrieveMetadataPatients_; + bool retrieveMetadataStudies_; + bool retrieveMetadataSeries_; + bool retrieveMetadataInstances_; + bool retrieveLabels_; + bool retrieveAttachments_; + bool retrieveParentIdentifier_; + bool retrieveChildrenIdentifiers_; + std::set retrieveChildrenMetadata_; + std::set retrieveAttachmentOfOneInstance_; + + public: + explicit FindRequest(ResourceType level); + + ~FindRequest(); + + ResourceType GetLevel() const + { + return level_; + } + + 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 AddOrdering(const DicomTag& tag, OrderingDirection direction); + + void AddOrdering(MetadataType metadataType, OrderingDirection direction); + + const std::deque& GetOrdering() const + { + return ordering_; + } + + void AddLabel(const std::string& label) + { + labels_.insert(label); + } + + const std::set& GetLabels() const + { + return labels_; + } + + LabelsConstraint GetLabelsConstraint() const + { + return labelsContraint_; + } + + void SetRetrieveMainDicomTags(ResourceType level, + bool retrieve); + + bool IsRetrieveMainDicomTags(ResourceType level) const; + + void SetRetrieveMetadata(ResourceType level, + bool retrieve); + + bool IsRetrieveMetadata(ResourceType level) const; + + void SetRetrieveLabels(bool retrieve) + { + retrieveLabels_ = retrieve; + } + + bool IsRetrieveLabels() const + { + return retrieveLabels_; + } + + void SetRetrieveAttachments(bool retrieve) + { + retrieveAttachments_ = retrieve; + } + + bool IsRetrieveAttachments() const + { + return retrieveAttachments_; + } + + void SetRetrieveParentIdentifier(bool retrieve); + + bool IsRetrieveParentIdentifier() const + { + return retrieveParentIdentifier_; + } + + void SetRetrieveChildrenIdentifiers(bool retrieve); + + bool IsRetrieveChildrenIdentifiers() const + { + return retrieveChildrenIdentifiers_; + } + + void AddRetrieveChildrenMetadata(MetadataType metadata); + + bool IsRetrieveChildrenMetadata(MetadataType metadata) const + { + return retrieveChildrenMetadata_.find(metadata) != retrieveChildrenMetadata_.end(); + } + + const std::set& GetRetrieveChildrenMetadata() const + { + return retrieveChildrenMetadata_; + } + + void AddRetrieveAttachmentOfOneInstance(FileContentType type); + + const std::set& GetRetrieveAttachmentOfOneInstance() const + { + return retrieveAttachmentOfOneInstance_; + } + }; +} diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/FindResponse.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindResponse.cpp Wed May 08 10:30:57 2024 +0200 @@ -0,0 +1,673 @@ +/** + * 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 . + **/ + + +#include "FindResponse.h" + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../OrthancFramework/Sources/SerializationToolbox.h" + +#include +#include + + +namespace Orthanc +{ + class FindResponse::MainDicomTagsAtLevel::DicomValue : public boost::noncopyable + { + public: + enum ValueType + { + ValueType_String, + ValueType_Null + }; + + private: + ValueType type_; + std::string value_; + + public: + DicomValue(ValueType type, + const std::string& value) : + type_(type), + value_(value) + { + } + + ValueType GetType() const + { + return type_; + } + + const std::string& GetValue() const + { + switch (type_) + { + case ValueType_Null: + throw OrthancException(ErrorCode_BadSequenceOfCalls); + + case ValueType_String: + return value_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + }; + + + FindResponse::MainDicomTagsAtLevel::~MainDicomTagsAtLevel() + { + for (MainDicomTags::iterator it = mainDicomTags_.begin(); it != mainDicomTags_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void FindResponse::MainDicomTagsAtLevel::AddNullDicomTag(uint16_t group, + uint16_t element) + { + const DicomTag tag(group, element); + + if (mainDicomTags_.find(tag) == mainDicomTags_.end()) + { + mainDicomTags_[tag] = new DicomValue(DicomValue::ValueType_Null, ""); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::MainDicomTagsAtLevel::AddStringDicomTag(uint16_t group, + uint16_t element, + const std::string& value) + { + const DicomTag tag(group, element); + + if (mainDicomTags_.find(tag) == mainDicomTags_.end()) + { + mainDicomTags_[tag] = new DicomValue(DicomValue::ValueType_String, value); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::MainDicomTagsAtLevel::Export(DicomMap& target) const + { + for (MainDicomTags::const_iterator it = mainDicomTags_.begin(); it != mainDicomTags_.end(); ++it) + { + assert(it->second != NULL); + + switch (it->second->GetType()) + { + case DicomValue::ValueType_String: + target.SetValue(it->first, it->second->GetValue(), false /* not binary */); + break; + + case DicomValue::ValueType_Null: + target.SetNullValue(it->first); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + void FindResponse::Resource::AddChildIdentifier(const std::string& identifier) + { + if (childrenIdentifiers_.find(identifier) == childrenIdentifiers_.end()) + { + childrenIdentifiers_.insert(identifier); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + FindResponse::MainDicomTagsAtLevel& FindResponse::Resource::GetMainDicomTagsAtLevel(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level, level_)) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + switch (level) + { + case ResourceType_Patient: + return mainDicomTagsPatient_; + + case ResourceType_Study: + return mainDicomTagsStudy_; + + case ResourceType_Series: + return mainDicomTagsSeries_; + + case ResourceType_Instance: + return mainDicomTagsInstance_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void FindResponse::Resource::AddMetadata(ResourceType level, + MetadataType metadata, + const std::string& value) + { + std::map& m = GetMetadata(level); + + if (m.find(metadata) != m.end()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); // Metadata already present + } + else + { + m[metadata] = value; + } + } + + + std::map& FindResponse::Resource::GetMetadata(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level, level_)) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + switch (level) + { + case ResourceType_Patient: + return metadataPatient_; + + case ResourceType_Study: + return metadataStudy_; + + case ResourceType_Series: + return metadataSeries_; + + case ResourceType_Instance: + return metadataInstance_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + bool FindResponse::Resource::LookupMetadata(std::string& value, + ResourceType level, + MetadataType metadata) const + { + const std::map& m = GetMetadata(level); + + std::map::const_iterator found = m.find(metadata); + + if (found == m.end()) + { + return false; + } + else + { + value = found->second; + return true; + } + } + + + const std::string& FindResponse::Resource::GetParentIdentifier() const + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else if (HasParentIdentifier()) + { + return *parentIdentifier_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + FindResponse::Resource::~Resource() + { + for (ChildrenMetadata::iterator it = childrenMetadata_.begin(); it != childrenMetadata_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void FindResponse::Resource::SetParentIdentifier(const std::string& id) + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else if (HasParentIdentifier()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + parentIdentifier_.reset(new std::string(id)); + } + } + + + bool FindResponse::Resource::HasParentIdentifier() const + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + return parentIdentifier_.get() != NULL; + } + } + + + void FindResponse::Resource::AddLabel(const std::string& label) + { + if (labels_.find(label) == labels_.end()) + { + labels_.insert(label); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::Resource::AddAttachment(const FileInfo& attachment) + { + if (attachments_.find(attachment.GetContentType()) == attachments_.end()) + { + attachments_[attachment.GetContentType()] = attachment; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + bool FindResponse::Resource::LookupAttachment(FileInfo& target, FileContentType type) const + { + std::map::const_iterator it = attachments_.find(type); + if (it != attachments_.end()) + { + target = it->second; + return true; + } + else + { + return false; + } + } + + + void FindResponse::Resource::AddChildrenMetadata(MetadataType metadata, + const std::list& values) + { + if (childrenMetadata_.find(metadata) == childrenMetadata_.end()) + { + childrenMetadata_[metadata] = new std::list(values); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + bool FindResponse::Resource::LookupChildrenMetadata(std::list& values, + MetadataType metadata) const + { + ChildrenMetadata::const_iterator found = childrenMetadata_.find(metadata); + if (found == childrenMetadata_.end()) + { + return false; + } + else + { + assert(found->second != NULL); + values = *found->second; + return true; + } + } + + + void FindResponse::Resource::AddAttachmentOfOneInstance(const FileInfo& info) + { + if (attachmentOfOneInstance_.find(info.GetContentType()) == attachmentOfOneInstance_.end()) + { + attachmentOfOneInstance_[info.GetContentType()] = info; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + bool FindResponse::Resource::LookupAttachmentOfOneInstance(FileInfo& target, + FileContentType type) const + { + std::map::const_iterator found = attachmentOfOneInstance_.find(type); + + if (found == attachmentOfOneInstance_.end()) + { + return false; + } + else + { + target = found->second; + return true; + } + } + + + static void DebugDicomMap(Json::Value& target, + const DicomMap& m) + { + DicomArray a(m); + for (size_t i = 0; i < a.GetSize(); i++) + { + if (a.GetElement(i).GetValue().IsNull()) + { + target[a.GetElement(i).GetTag().Format()] = Json::nullValue; + } + else if (a.GetElement(i).GetValue().IsString()) + { + target[a.GetElement(i).GetTag().Format()] = a.GetElement(i).GetValue().GetContent(); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + static void DebugMetadata(Json::Value& target, + const std::map& m) + { + target = Json::objectValue; + + for (std::map::const_iterator it = m.begin(); it != m.end(); ++it) + { + target[EnumerationToString(it->first)] = it->second; + } + } + + + static void DebugAddAttachment(Json::Value& target, + const FileInfo& info) + { + Json::Value u = Json::arrayValue; + u.append(info.GetUuid()); + u.append(static_cast(info.GetUncompressedSize())); + target[EnumerationToString(info.GetContentType())] = u; + } + + void FindResponse::Resource::DebugExport(Json::Value& target, + const FindRequest& request) const + { + target = Json::objectValue; + + target["Level"] = EnumerationToString(GetLevel()); + target["ID"] = GetIdentifier(); + + if (request.IsRetrieveParentIdentifier()) + { + target["ParentID"] = GetParentIdentifier(); + } + + if (request.IsRetrieveMainDicomTags(ResourceType_Patient)) + { + DicomMap m; + GetMainDicomTags(m, ResourceType_Patient); + DebugDicomMap(target["Patient"]["MainDicomTags"], m); + } + + if (request.IsRetrieveMetadata(ResourceType_Patient)) + { + DebugMetadata(target["Patient"]["Metadata"], GetMetadata(ResourceType_Patient)); + } + + if (request.GetLevel() != ResourceType_Patient) + { + if (request.IsRetrieveMainDicomTags(ResourceType_Study)) + { + DicomMap m; + GetMainDicomTags(m, ResourceType_Study); + DebugDicomMap(target["Study"]["MainDicomTags"], m); + } + + if (request.IsRetrieveMetadata(ResourceType_Study)) + { + DebugMetadata(target["Study"]["Metadata"], GetMetadata(ResourceType_Study)); + } + } + + if (request.GetLevel() != ResourceType_Patient && + request.GetLevel() != ResourceType_Study) + { + if (request.IsRetrieveMainDicomTags(ResourceType_Series)) + { + DicomMap m; + GetMainDicomTags(m, ResourceType_Series); + DebugDicomMap(target["Series"]["MainDicomTags"], m); + } + + if (request.IsRetrieveMetadata(ResourceType_Series)) + { + DebugMetadata(target["Series"]["Metadata"], GetMetadata(ResourceType_Series)); + } + } + + if (request.GetLevel() != ResourceType_Patient && + request.GetLevel() != ResourceType_Study && + request.GetLevel() != ResourceType_Series) + { + if (request.IsRetrieveMainDicomTags(ResourceType_Instance)) + { + DicomMap m; + GetMainDicomTags(m, ResourceType_Instance); + DebugDicomMap(target["Instance"]["MainDicomTags"], m); + } + + if (request.IsRetrieveMetadata(ResourceType_Instance)) + { + DebugMetadata(target["Instance"]["Metadata"], GetMetadata(ResourceType_Instance)); + } + } + + if (request.IsRetrieveChildrenIdentifiers()) + { + Json::Value v = Json::arrayValue; + for (std::set::const_iterator it = childrenIdentifiers_.begin(); + it != childrenIdentifiers_.end(); ++it) + { + v.append(*it); + } + target["Children"] = v; + } + + if (request.IsRetrieveLabels()) + { + Json::Value v = Json::arrayValue; + for (std::set::const_iterator it = labels_.begin(); + it != labels_.end(); ++it) + { + v.append(*it); + } + target["Labels"] = v; + } + + if (request.IsRetrieveAttachments()) + { + Json::Value v = Json::objectValue; + for (std::map::const_iterator it = attachments_.begin(); + it != attachments_.end(); ++it) + { + if (it->first != it->second.GetContentType()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + DebugAddAttachment(v, it->second); + } + } + target["Attachments"] = v; + } + + for (std::set::const_iterator it = request.GetRetrieveChildrenMetadata().begin(); + it != request.GetRetrieveChildrenMetadata().end(); ++it) + { + std::list l; + if (LookupChildrenMetadata(l, *it)) + { + Json::Value v = Json::arrayValue; + for (std::list::const_iterator it2 = l.begin(); it2 != l.end(); ++it2) + { + v.append(*it2); + } + target["ChildrenMetadata"][EnumerationToString(*it)] = v; + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + + for (std::set::const_iterator it = request.GetRetrieveAttachmentOfOneInstance().begin(); + it != request.GetRetrieveAttachmentOfOneInstance().end(); ++it) + { + FileInfo info; + if (LookupAttachmentOfOneInstance(info, *it)) + { + if (info.GetContentType() == *it) + { + DebugAddAttachment(target["AttachmentOfOneInstance"], info); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + } + } + + + FindResponse::~FindResponse() + { + for (size_t i = 0; i < items_.size(); i++) + { + assert(items_[i] != NULL); + delete items_[i]; + } + } + + + void FindResponse::Add(Resource* item /* takes ownership */) + { + std::unique_ptr protection(item); + + if (item == NULL) + { + throw OrthancException(ErrorCode_NullPointer); + } + else if (!items_.empty() && + items_[0]->GetLevel() != item->GetLevel()) + { + throw OrthancException(ErrorCode_BadParameterType, "A find response must only contain resources of the same type"); + } + else + { + const std::string& id = item->GetIdentifier(); + + if (index_.find(id) == index_.end()) + { + items_.push_back(protection.release()); + index_[id] = item; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "This resource has already been added: " + id); + } + } + } + + + const FindResponse::Resource& FindResponse::GetResource(size_t index) const + { + if (index >= items_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(items_[index] != NULL); + return *items_[index]; + } + } + + + FindResponse::Resource& FindResponse::GetResource(const std::string& id) + { + Index::const_iterator found = index_.find(id); + + if (found == index_.end()) + { + throw OrthancException(ErrorCode_InexistentItem); + } + else + { + assert(found->second != NULL); + return *found->second; + } + } +} diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/FindResponse.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindResponse.h Wed May 08 10:30:57 2024 +0200 @@ -0,0 +1,235 @@ +/** + * 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 . + **/ + + +#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 +#include +#include +#include +#include + + +namespace Orthanc +{ + class FindResponse : public boost::noncopyable + { + private: + class MainDicomTagsAtLevel : public boost::noncopyable + { + private: + class DicomValue; + + typedef std::map MainDicomTags; + + MainDicomTags mainDicomTags_; + + public: + ~MainDicomTagsAtLevel(); + + void AddStringDicomTag(uint16_t group, + uint16_t element, + const std::string& value); + + // The "Null" value could be used in the future to indicate a + // value that is not available, typically a new "ExtraMainDicomTag" + void AddNullDicomTag(uint16_t group, + uint16_t element); + + void Export(DicomMap& target) const; + }; + + + public: + class Resource : public boost::noncopyable + { + private: + typedef std::map*> ChildrenMetadata; + + ResourceType level_; + std::string identifier_; + std::unique_ptr parentIdentifier_; + MainDicomTagsAtLevel mainDicomTagsPatient_; + MainDicomTagsAtLevel mainDicomTagsStudy_; + MainDicomTagsAtLevel mainDicomTagsSeries_; + MainDicomTagsAtLevel mainDicomTagsInstance_; + std::map metadataPatient_; + std::map metadataStudy_; + std::map metadataSeries_; + std::map metadataInstance_; + std::set childrenIdentifiers_; + std::set labels_; + std::map attachments_; + ChildrenMetadata childrenMetadata_; + std::map attachmentOfOneInstance_; + + MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level); + + const MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level) const + { + return const_cast(*this).GetMainDicomTagsAtLevel(level); + } + + public: + Resource(ResourceType level, + const std::string& identifier) : + level_(level), + identifier_(identifier) + { + } + + ~Resource(); + + ResourceType GetLevel() const + { + return level_; + } + + const std::string& GetIdentifier() const + { + return identifier_; + } + + const std::string& GetParentIdentifier() const; + + void SetParentIdentifier(const std::string& id); + + bool HasParentIdentifier() const; + + void AddStringDicomTag(ResourceType level, + uint16_t group, + uint16_t element, + const std::string& value) + { + GetMainDicomTagsAtLevel(level).AddStringDicomTag(group, element, value); + } + + void AddNullDicomTag(ResourceType level, + uint16_t group, + uint16_t element) + { + GetMainDicomTagsAtLevel(level).AddNullDicomTag(group, element); + } + + void GetMainDicomTags(DicomMap& target, + ResourceType level) const + { + GetMainDicomTagsAtLevel(level).Export(target); + } + + void AddMetadata(ResourceType level, + MetadataType metadata, + const std::string& value); + + std::map& GetMetadata(ResourceType level); + + const std::map& GetMetadata(ResourceType level) const + { + return const_cast(*this).GetMetadata(level); + } + + bool LookupMetadata(std::string& value, + ResourceType level, + MetadataType metadata) const; + + void AddChildIdentifier(const std::string& childId); + + const std::set& GetChildrenIdentifiers() const + { + return childrenIdentifiers_; + } + + void AddLabel(const std::string& label); + + std::set& GetLabels() + { + return labels_; + } + + const std::set& GetLabels() const + { + return labels_; + } + + void AddAttachment(const FileInfo& attachment); + + bool LookupAttachment(FileInfo& target, + FileContentType type) const; + + const std::map& GetAttachments() const + { + return attachments_; + } + + void AddChildrenMetadata(MetadataType metadata, + const std::list& values); + + bool LookupChildrenMetadata(std::list& values, + MetadataType metadata) const; + + void AddAttachmentOfOneInstance(const FileInfo& info); + + bool LookupAttachmentOfOneInstance(FileInfo& target, + FileContentType type) const; + + void DebugExport(Json::Value& target, + const FindRequest& request) const; + }; + + private: + typedef std::map Index; + + std::deque items_; + Index index_; + + public: + ~FindResponse(); + + void Add(Resource* item /* takes ownership */); + + size_t GetSize() const + { + return items_.size(); + } + + const Resource& GetResource(size_t index) const; + + Resource& GetResource(const std::string& id); + + const Resource& GetResource(const std::string& id) const + { + return const_cast(*this).GetResource(id); + } + + bool HasResource(const std::string& id) const + { + return (index_.find(id) != index_.end()); + } + }; +} diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/IDatabaseWrapper.h --- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Wed May 08 10:30:57 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 @@ -350,6 +352,33 @@ int64_t& instancesCount, int64_t& compressedSize, int64_t& uncompressedSize) = 0; + + /** + * Primitives introduced in Orthanc 1.12.4 + **/ + + // This is only implemented if "HasIntegratedFind()" is "true" + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector& normalized) = 0; + + // This is only implemented if "HasIntegratedFind()" is "false" + virtual void ExecuteFind(std::list& identifiers, + const FindRequest& request, + const std::vector& normalized) = 0; + + /** + * This is only implemented if "HasIntegratedFind()" is + * "false". In this flavor, the resource of interest might have + * been deleted, as the expansion is not done in the same + * transaction as the "ExecuteFind()". In such cases, the + * wrapper should not throw an exception, but simply ignore the + * request to expand the resource (i.e., "response" must not be + * modified). + **/ + virtual void ExecuteExpand(FindResponse& response, + const FindRequest& request, + const std::string& identifier) = 0; }; @@ -374,5 +403,9 @@ virtual const Capabilities GetDatabaseCapabilities() const = 0; virtual uint64_t MeasureLatency() = 0; + + // Returns "true" iff. the database engine supports the + // simultaneous find and expansion of resources. + virtual bool HasIntegratedFind() const = 0; }; } diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/OrthancIdentifiers.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/OrthancIdentifiers.cpp Wed May 08 10:30:57 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 . + **/ + + +#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); + } + } +} diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/OrthancIdentifiers.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/OrthancIdentifiers.h Wed May 08 10:30:57 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 . + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../OrthancFramework/Sources/Enumerations.h" + +#include +#include + + +namespace Orthanc +{ + class OrthancIdentifiers : public boost::noncopyable + { + private: + std::unique_ptr patientId_; + std::unique_ptr studyId_; + std::unique_ptr seriesId_; + std::unique_ptr 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; + }; +} diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp --- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Wed May 08 10:30:57 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,175 @@ target.insert(s.ColumnString(0)); } } + + +#if 0 + // TODO-FIND: Remove this implementation, as it should be done by + // the compatibility mode implemented by "GenericFind" + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector& 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 + + { + // 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); + response.Add(new FindResponse::Resource(request.GetLevel(), resourceId)); + } + } + + // request Each response content through INNER JOIN with the temporary table + if (request.IsRetrieveMainDicomTags()) + { + // 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); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddStringDicomTag(statement.ColumnInt(1), + statement.ColumnInt(2), + statement.ColumnString(3)); + } + } + + if (request.IsRetrieveChildrenIdentifiers()) + { + 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); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddChildIdentifier(GetChildResourceType(request.GetLevel()), statement.ColumnString(1)); + } + } + + if (request.IsRetrieveParentIdentifier()) + { + 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); + const std::string& parentId = statement.ColumnString(1); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).SetParentIdentifier(parentId); + } + } + + if (request.IsRetrieveMetadata()) + { + 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); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddMetadata(static_cast(statement.ColumnInt(1)), + statement.ColumnString(2)); + } + } + + if (request.IsRetrieveLabels()) + { + 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); + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddLabel(statement.ColumnString(1)); + } + } + + if (request.IsRetrieveAttachments()) + { + 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(statement.ColumnInt(2)), + statement.ColumnInt64(3), + statement.ColumnString(6), + static_cast(statement.ColumnInt(4)), + statement.ColumnInt64(5), + statement.ColumnString(7)); + + assert(response.HasResource(resourceId)); + response.GetResource(resourceId).AddAttachment(attachment); + }; + } + + // TODO-FIND: implement other responseContent: ResponseContent_ChildInstanceId, ResponseContent_ChildrenMetadata (later: ResponseContent_IsStable) + + } + +#endif + } +#endif + }; diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp --- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Wed May 08 10:30:57 2024 +0200 @@ -501,6 +501,38 @@ } } + void StatelessDatabaseOperations::NormalizeLookup(std::vector& 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 { @@ -905,7 +937,7 @@ Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags); while ((target.missingRequestedTags_.size() > 0) - && currentLevel != ResourceType_Patient) + && currentLevel != ResourceType_Patient) { currentLevel = GetParentResourceType(currentLevel); @@ -2516,7 +2548,7 @@ catch (boost::bad_lexical_cast&) { LOG(ERROR) << "Cannot read the global sequence " - << boost::lexical_cast(sequence_) << ", resetting it"; + << boost::lexical_cast(sequence_) << ", resetting it"; oldValue = 0; } @@ -2815,8 +2847,8 @@ public: explicit Operations(const ParsedDicomFile& dicom, bool limitToThisLevelDicomTags, ResourceType limitToLevel) - : limitToThisLevelDicomTags_(limitToThisLevelDicomTags), - limitToLevel_(limitToLevel) + : limitToThisLevelDicomTags_(limitToThisLevelDicomTags), + limitToLevel_(limitToLevel) { OrthancConfiguration::DefaultExtractDicomSummary(summary_, dicom); hasher_.reset(new DicomInstanceHasher(summary_)); @@ -2941,7 +2973,7 @@ } bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount, - const std::string& patientId) + const std::string& patientId) { if (maximumPatientCount != 0) { @@ -3043,7 +3075,7 @@ }; if (maximumStorageMode == MaxStorageMode_Recycle - && (maximumStorageSize != 0 || maximumPatientCount != 0)) + && (maximumStorageSize != 0 || maximumPatientCount != 0)) { Operations operations(maximumStorageSize, maximumPatientCount); Apply(operations); @@ -3107,9 +3139,9 @@ } static void SetMainDicomSequenceMetadata(ResourcesContent& content, - int64_t resource, - const DicomMap& dicomSummary, - ResourceType level) + int64_t resource, + const DicomMap& dicomSummary, + ResourceType level) { std::string serialized; GetMainDicomSequenceMetadataContent(serialized, dicomSummary, level); @@ -3302,7 +3334,7 @@ // Ensure there is enough room in the storage for the new instance uint64_t instanceSize = 0; for (Attachments::const_iterator it = attachments_.begin(); - it != attachments_.end(); ++it) + it != attachments_.end(); ++it) { instanceSize += it->GetCompressedSize(); } @@ -3331,7 +3363,7 @@ // Attach the files to the newly created instance for (Attachments::const_iterator it = attachments_.begin(); - it != attachments_.end(); ++it) + it != attachments_.end(); ++it) { if (isReconstruct_) { @@ -3348,7 +3380,7 @@ // Attach the user-specified metadata (in case of reconstruction, metadata_ contains all past metadata, including the system ones we want to keep) for (MetadataMap::const_iterator - it = metadata_.begin(); it != metadata_.end(); ++it) + it = metadata_.begin(); it != metadata_.end(); ++it) { switch (it->first.first) { @@ -3807,4 +3839,172 @@ boost::shared_lock lock(mutex_); return db_.GetDatabaseCapabilities().HasLabelsSupport(); } + + + void StatelessDatabaseOperations::ExecuteFind(FindResponse& response, + const FindRequest& request) + { + class IntegratedFind : public ReadOnlyOperationsT3&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + class FindStage : public ReadOnlyOperationsT3&, const FindRequest&, const std::vector&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteFind(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + class ExpandStage : public ReadOnlyOperationsT3 + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteExpand(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + std::vector normalized; + NormalizeLookup(normalized, request); + + if (db_.HasIntegratedFind()) + { + /** + * In this flavor, the "find" and the "expand" phases are + * executed in one single transaction. + **/ + IntegratedFind operations; + operations.Apply(*this, response, request, normalized); + } + else + { + /** + * In this flavor, the "find" and the "expand" phases for each + * found resource are executed in distinct transactions. This is + * the compatibility mode equivalent to Orthanc <= 1.12.3. + **/ + std::list identifiers; + + FindStage find; + find.Apply(*this, identifiers, request, normalized); + + ExpandStage expand; + + for (std::list::const_iterator it = identifiers.begin(); it != identifiers.end(); ++it) + { + /** + * Not that the resource might have been deleted (as we are in + * another transaction). The database engine must ignore such + * error cases. + **/ + expand.Apply(*this, response, request, *it); + } + } + } + + // 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 FindRequest& request, + const FindResponse::Resource& resource) : + id_(resource.GetIdentifier()), + level_(request.GetLevel()), + isStable_(false), + expectedNumberOfInstances_(0), + fileSize_(0), + indexInSeries_(0) + { + if (request.GetLevel() != resource.GetLevel()) + { + throw OrthancException(ErrorCode_InternalError); + } + + if (request.IsRetrieveMainDicomTags(request.GetLevel())) + { + resource.GetMainDicomTags(tags_, request.GetLevel()); + } + + if (request.IsRetrieveChildrenIdentifiers()) + { + const std::set& children = resource.GetChildrenIdentifiers(); + for (std::set::const_iterator it = children.begin(); it != children.end(); ++it) + { + childrenIds_.push_back(*it); + } + } + + if (request.IsRetrieveParentIdentifier()) + { + parentId_ = resource.GetParentIdentifier(); + } + + if (request.IsRetrieveMetadata(request.GetLevel())) + { + metadata_ = resource.GetMetadata(request.GetLevel()); + std::string value; + if (resource.LookupMetadata(value, request.GetLevel(), MetadataType_MainDicomTagsSignature)) + { + mainDicomTagsSignature_ = value; + } + if (resource.LookupMetadata(value, request.GetLevel(), MetadataType_AnonymizedFrom)) + { + anonymizedFrom_ = value; + } + if (resource.LookupMetadata(value, request.GetLevel(), MetadataType_ModifiedFrom)) + { + modifiedFrom_ = value; + } + if (resource.LookupMetadata(value, request.GetLevel(), MetadataType_LastUpdate)) + { + lastUpdate_ = value; + } + if (request.GetLevel() == ResourceType_Series) + { + if (resource.LookupMetadata(value, request.GetLevel(), MetadataType_Series_ExpectedNumberOfInstances)) + { + expectedNumberOfInstances_ = boost::lexical_cast(value); + } + } + if (request.GetLevel() == ResourceType_Instance) + { + if (resource.LookupMetadata(value, request.GetLevel(), MetadataType_Instance_IndexInSeries)) + { + indexInSeries_ = boost::lexical_cast(value); + } + } + } + + if (request.IsRetrieveLabels()) + { + labels_ = resource.GetLabels(); + } + + if (request.IsRetrieveAttachments()) + { + FileInfo attachment; + if (resource.LookupAttachment(attachment, FileContentType_Dicom)) + { + fileSize_ = attachment.GetUncompressedSize(); + fileUuid_ = attachment.GetUuid(); + } + } + + //if (request.IsRetrieveChildrenMetadata()) + { + // 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_ + } } diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Database/StatelessDatabaseOperations.h --- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Wed May 08 10:30:57 2024 +0200 @@ -80,6 +80,9 @@ { } + ExpandedResource(const FindRequest& request, + const FindResponse::Resource& resource); + void SetResource(ResourceType level, const std::string& id) { @@ -111,15 +114,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 @@ -377,6 +391,27 @@ { transaction_.ListAllLabels(target); } + + void ExecuteFind(FindResponse& response, + const FindRequest& request, + const std::vector& normalized) + { + transaction_.ExecuteFind(response, request, normalized); + } + + void ExecuteFind(std::list& identifiers, + const FindRequest& request, + const std::vector& normalized) + { + transaction_.ExecuteFind(identifiers, request, normalized); + } + + void ExecuteExpand(FindResponse& response, + const FindRequest& request, + const std::string& identifier) + { + transaction_.ExecuteExpand(response, request, identifier); + } }; @@ -559,6 +594,9 @@ const DatabaseLookup& source, ResourceType level) const; + void NormalizeLookup(std::vector& target, + const FindRequest& findRequest) const; + void ApplyInternal(IReadOnlyOperations* readOperations, IReadWriteOperations* writeOperations); @@ -801,5 +839,8 @@ const std::set& labels); bool HasLabelsSupport(); + + void ExecuteFind(FindResponse& response, + const FindRequest& request); }; } diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Wed May 08 10:30:57 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& resources, const std::map& 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& resources, ResourceType level, @@ -194,10 +194,375 @@ std::map > unusedResourcesMainDicomTags; std::map > 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); } + class ResourceFinder : public boost::noncopyable + { + private: + FindRequest request_; + bool expand_; + std::set requestedTags_; + DicomToJsonFormat format_; + bool includeAllMetadata_; // Same as: ExpandResourceFlags_IncludeAllMetadata + + SeriesStatus GetSeriesStatus(uint32_t& expectedNumberOfInstances, + const FindResponse::Resource& resource) const + { + if (request_.GetLevel() != ResourceType_Series) + { + throw OrthancException(ErrorCode_BadParameterType); + } + + std::string s; + if (!resource.LookupMetadata(s, ResourceType_Series, MetadataType_Series_ExpectedNumberOfInstances) || + !SerializationToolbox::ParseUnsignedInteger32(expectedNumberOfInstances, s)) + { + return SeriesStatus_Unknown; + } + + std::list values; + if (!resource.LookupChildrenMetadata(values, MetadataType_Instance_IndexInSeries)) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + std::set instances; + + for (std::list::const_iterator + it = values.begin(); it != values.end(); ++it) + { + int64_t index; + + if (!SerializationToolbox::ParseInteger64(index, *it)) + { + return SeriesStatus_Unknown; + } + + if (index <= 0 || + index > static_cast(expectedNumberOfInstances)) + { + // Out-of-range instance index + return SeriesStatus_Inconsistent; + } + + if (instances.find(index) != instances.end()) + { + // Twice the same instance index + return SeriesStatus_Inconsistent; + } + + instances.insert(index); + } + + if (instances.size() == static_cast(expectedNumberOfInstances)) + { + return SeriesStatus_Complete; + } + else + { + return SeriesStatus_Missing; + } + } + + + void Expand(Json::Value& target, + const FindResponse::Resource& resource) const + { + /** + + TODO-FIND: + + - Metadata / Series / ExpectedNumberOfInstances + + - Metadata / Series / Status + + - Metadata / Instance / FileSize + + - Metadata / Instance / FileUuid + + - Metadata / Instance / IndexInSeries + + - Metadata / AnonymizedFrom + + - Metadata / ModifiedFrom + + **/ + + /** + * This method closely follows "SerializeExpandedResource()" in + * "ServerContext.cpp" from Orthanc 1.12.3. + **/ + + if (resource.GetLevel() != request_.GetLevel()) + { + throw OrthancException(ErrorCode_InternalError); + } + + target = Json::objectValue; + + target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true); + target["ID"] = resource.GetIdentifier(); + + switch (resource.GetLevel()) + { + case ResourceType_Patient: + break; + + case ResourceType_Study: + target["ParentPatient"] = resource.GetParentIdentifier(); + break; + + case ResourceType_Series: + target["ParentStudy"] = resource.GetParentIdentifier(); + break; + + case ResourceType_Instance: + target["ParentSeries"] = resource.GetParentIdentifier(); + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + + if (resource.GetLevel() != ResourceType_Instance) + { + const std::set& children = resource.GetChildrenIdentifiers(); + + Json::Value c = Json::arrayValue; + for (std::set::const_iterator + it = children.begin(); it != children.end(); ++it) + { + c.append(*it); + } + + switch (resource.GetLevel()) + { + case ResourceType_Patient: + target["Studies"] = c; + break; + + case ResourceType_Study: + target["Series"] = c; + break; + + case ResourceType_Series: + target["Instances"] = c; + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + switch (resource.GetLevel()) + { + case ResourceType_Patient: + case ResourceType_Study: + break; + + case ResourceType_Series: + { + uint32_t expectedNumberOfInstances; + SeriesStatus status = GetSeriesStatus(expectedNumberOfInstances, resource); + + target["Status"] = EnumerationToString(status); + + if (status == SeriesStatus_Unknown) + { + target["ExpectedNumberOfInstances"] = Json::nullValue; + } + else + { + target["ExpectedNumberOfInstances"] = expectedNumberOfInstances; + } + + break; + } + + case ResourceType_Instance: + { + FileInfo info; + if (resource.LookupAttachment(info, FileContentType_Dicom)) + { + target["FileSize"] = static_cast(info.GetUncompressedSize()); + target["FileUuid"] = info.GetUuid(); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + + std::string s; + uint32_t index; + if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_IndexInSeries) && + SerializationToolbox::ParseUnsignedInteger32(index, s)) + { + target["IndexInSeries"] = index; + } + else + { + target["IndexInSeries"] = Json::nullValue; + } + + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + std::string s; + if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_AnonymizedFrom)) + { + target["AnonymizedFrom"] = s; + } + + if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_ModifiedFrom)) + { + target["ModifiedFrom"] = s; + } + + if (resource.GetLevel() == ResourceType_Patient || + resource.GetLevel() == ResourceType_Study || + resource.GetLevel() == ResourceType_Series) + { + // TODO-FIND: Stable + + /* + if (resource.IsStable()) + { + target["IsStable"] = true; + } + */ + + if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_LastUpdate)) + { + target["LastUpdate"] = s; + } + } + + { + Json::Value labels = Json::arrayValue; + + for (std::set::const_iterator + it = resource.GetLabels().begin(); it != resource.GetLabels().end(); ++it) + { + labels.append(*it); + } + + target["Labels"] = labels; + } + + if (includeAllMetadata_) + { + const std::map& m = resource.GetMetadata(resource.GetLevel()); + + Json::Value metadata = Json::objectValue; + + for (std::map::const_iterator it = m.begin(); it != m.end(); ++it) + { + metadata[EnumerationToString(it->first)] = it->second; + } + + target["Metadata"] = metadata; + } + } + + + public: + ResourceFinder(ResourceType level, + bool expand) : + request_(level), + expand_(expand), + format_(DicomToJsonFormat_Human), + includeAllMetadata_(false) + { + if (expand) + { + request_.SetRetrieveMainDicomTags(level, true); + request_.SetRetrieveMetadata(level, true); + request_.SetRetrieveLabels(true); + + if (level == ResourceType_Series) + { + request_.AddRetrieveChildrenMetadata(MetadataType_Instance_IndexInSeries); // required for the SeriesStatus + } + + if (level == ResourceType_Instance) + { + request_.SetRetrieveAttachments(true); // for FileSize & FileUuid + } + else + { + request_.SetRetrieveChildrenIdentifiers(true); + } + + if (level != ResourceType_Patient) + { + request_.SetRetrieveParentIdentifier(true); + } + } + } + + void SetRequestedTags(const std::set& tags) + { + requestedTags_ = tags; + } + + void SetFormat(DicomToJsonFormat format) + { + format_ = format; + } + + void SetLimits(uint64_t since, + uint64_t count) + { + request_.SetLimits(since, count); + } + + void SetIncludeAllMetadata(bool include) + { + includeAllMetadata_ = include; + } + + void Execute(Json::Value& target, + ServerContext& context) + { + FindResponse response; + context.GetIndex().ExecuteFind(response, request_); + + target = Json::arrayValue; + + if (expand_) + { + for (size_t i = 0; i < response.GetSize(); i++) + { + Json::Value item; + Expand(item, response.GetResource(i)); + +#if 0 + target.append(item); +#else + context.AppendFindResponse(target, request_, response.GetResource(i), format_, + requestedTags_, true /* allowStorageAccess */); + std::cout << "+++ Expected: " << target[target.size() - 1].toStyledString(); + std::cout << "--- Actual: " << item.toStyledString(); +#endif + } + } + else + { + for (size_t i = 0; i < response.GetSize(); i++) + { + target.append(response.GetResource(i).GetIdentifier()); + } + } + } + }; + + template static void ListResources(RestApiGetCall& call) { @@ -224,41 +589,92 @@ ServerIndex& index = OrthancRestApi::GetIndex(call); ServerContext& context = OrthancRestApi::GetContext(call); - std::list result; - - std::set 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 requestedTags; + OrthancRestApi::GetRequestedTags(requestedTags, call); + + ResourceFinder finder(resourceType, expand); + finder.SetRequestedTags(requestedTags); + finder.SetFormat(OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human)); + + if (call.HasArgument("limit") || + call.HasArgument("since")) { - throw OrthancException(ErrorCode_BadRequest, - "Missing \"limit\" argument for GET request against: " + - call.FlattenUri()); + if (!call.HasArgument("limit")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"limit\" argument for GET request against: " + + call.FlattenUri()); + } + + if (!call.HasArgument("since")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"since\" argument for GET request against: " + + call.FlattenUri()); + } + + uint64_t since = boost::lexical_cast(call.GetArgument("since", "")); + uint64_t limit = boost::lexical_cast(call.GetArgument("limit", "")); + finder.SetLimits(since, limit); } - if (!call.HasArgument("since")) - { - throw OrthancException(ErrorCode_BadRequest, - "Missing \"since\" argument for GET request against: " + - call.FlattenUri()); - } - - size_t since = boost::lexical_cast(call.GetArgument("since", "")); - size_t limit = boost::lexical_cast(call.GetArgument("limit", "")); - index.GetAllUuids(result, resourceType, since, limit); + Json::Value answer; + finder.Execute(answer, context); + call.GetOutput().AnswerJson(answer); } else { - index.GetAllUuids(result, resourceType); + /** + * VERSION IN ORTHANC <= 1.12.3 + **/ + + std::list result; + + std::set 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(call.GetArgument("since", "")); + size_t limit = boost::lexical_cast(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 +3523,7 @@ bool expand, const std::set& 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 +3803,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 */); diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Search/DatabaseConstraint.cpp --- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Sources/Search/DatabaseConstraint.cpp Wed May 08 10:30:57 2024 +0200 @@ -152,6 +152,7 @@ const std::vector& values, bool caseSensitive, bool mandatory) : + keyType_(DatabaseConstraint::KeyType_DicomTag), level_(level), tag_(tag), isIdentifier_(isIdentifier), diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Search/DatabaseConstraint.h --- a/OrthancServer/Sources/Search/DatabaseConstraint.h Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Sources/Search/DatabaseConstraint.h Wed May 08 10:30:57 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 values_; diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/Search/ISqlLookupFormatter.h --- a/OrthancServer/Sources/Search/ISqlLookupFormatter.h Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h Wed May 08 10:30:57 2024 +0200 @@ -34,7 +34,7 @@ namespace Orthanc { class DatabaseConstraint; - + enum LabelsConstraint { LabelsConstraint_All, diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/ServerContext.cpp --- a/OrthancServer/Sources/ServerContext.cpp Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Wed May 08 10:30:57 2024 +0200 @@ -2125,124 +2125,137 @@ static void SerializeExpandedResource(Json::Value& target, const ExpandedResource& resource, DicomToJsonFormat format, - const std::set& requestedTags) + const std::set& 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::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::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(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(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::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,42 @@ return elapsed.total_seconds(); } + void ServerContext::AppendFindResponse(Json::Value& target, + const FindRequest& request, + const FindResponse::Resource& item, + DicomToJsonFormat format, + const std::set& 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(request, item); + + ExpandResourceFlags expandFlags = ExpandResourceFlags_None; + if (request.IsRetrieveChildrenIdentifiers()) + { + expandFlags = static_cast(expandFlags | ExpandResourceFlags_IncludeChildren); + } + if (request.IsRetrieveMetadata(request.GetLevel())) + { + expandFlags = static_cast(expandFlags | ExpandResourceFlags_IncludeAllMetadata | ExpandResourceFlags_IncludeMetadata ); + } + if (request.IsRetrieveMainDicomTags(request.GetLevel())) + { + expandFlags = static_cast(expandFlags | ExpandResourceFlags_IncludeMainDicomTags); + } + if (true /* request.HasResponseContent(FindRequest::ResponseContent_IsStable) */) // TODO-FIND: Is this correct? + { + expandFlags = static_cast(expandFlags | ExpandResourceFlags_IncludeIsStable); + } + if (request.IsRetrieveLabels()) + { + expandFlags = static_cast(expandFlags | ExpandResourceFlags_IncludeLabels); + } + + Json::Value jsonItem; + SerializeExpandedResource(jsonItem, resource, format, requestedTags, expandFlags); + target.append(jsonItem); + } + + } diff -r c2a2fb8e868d -r 3f24eb4013d8 OrthancServer/Sources/ServerContext.h --- a/OrthancServer/Sources/ServerContext.h Wed May 08 10:30:34 2024 +0200 +++ b/OrthancServer/Sources/ServerContext.h Wed May 08 10:30:57 2024 +0200 @@ -607,6 +607,13 @@ ExpandResourceFlags expandFlags, bool allowStorageAccess); + void AppendFindResponse(Json::Value& target, + const FindRequest& request, + const FindResponse::Resource& resource, + DicomToJsonFormat format, + const std::set& requestedTags, + bool allowStorageAccess); + FindStorageAccessMode GetFindStorageAccessMode() const { return findStorageAccessMode_;