Mercurial > hg > orthanc
changeset 5691:c352d762177c find-refactoring
integration mainline->find-refactoring
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 09 Jul 2024 18:06:21 +0200 |
parents | 708952bd869c (diff) 13b1ff603b37 (current diff) |
children | 7c11a71927a9 |
files | |
diffstat | 33 files changed, 5142 insertions(+), 347 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Tue Jul 09 15:32:30 2024 +0200 +++ b/NEWS Tue Jul 09 18:06:21 2024 +0200 @@ -1,13 +1,15 @@ Pending changes in the mainline =============================== +* TODO-FIND: complete the list of updated routes: + /studies?expand and sibbling routes now also return "Metadata" (if the DB implements 'extended-api-v1') + REST API ------------ +-------- * Improved parsing of multiple numerical values in DICOM tags. https://discourse.orthanc-server.org/t/qido-includefield-with-sequences/4746/6 - Maintenance -----------
--- a/OrthancFramework/Sources/SQLite/Connection.h Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/Connection.h Tue Jul 09 18:06:21 2024 +0200 @@ -57,6 +57,7 @@ #endif #define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__) +#define SQLITE_FROM_HERE_DYNAMIC(sql) ::Orthanc::SQLite::StatementId(__ORTHANC_FILE__, __LINE__, sql) namespace Orthanc {
--- a/OrthancFramework/Sources/SQLite/StatementId.cpp Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/StatementId.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -57,12 +57,24 @@ { } + Orthanc::SQLite::StatementId::StatementId(const char *file, + int line, + const std::string& statement) : + file_(file), + line_(line), + statement_(statement) + { + } + bool StatementId::operator< (const StatementId& other) const { if (line_ != other.line_) return line_ < other.line_; - return strcmp(file_, other.file_) < 0; + if (strcmp(file_, other.file_) < 0) + return true; + + return statement_ < other.statement_; } } }
--- a/OrthancFramework/Sources/SQLite/StatementId.h Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancFramework/Sources/SQLite/StatementId.h Tue Jul 09 18:06:21 2024 +0200 @@ -55,6 +55,7 @@ private: const char* file_; int line_; + std::string statement_; StatementId(); // Forbidden @@ -62,6 +63,10 @@ StatementId(const char* file, int line); + StatementId(const char* file, + int line, + const std::string& statement); + bool operator< (const StatementId& other) const; }; }
--- a/OrthancServer/CMakeLists.txt Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/CMakeLists.txt Tue Jul 09 18:06:21 2024 +0200 @@ -90,11 +90,16 @@ 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/MainDicomTagsRegistry.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 @@ -119,6 +124,7 @@ ${CMAKE_SOURCE_DIR}/Sources/OrthancRestApi/OrthancRestSystem.cpp ${CMAKE_SOURCE_DIR}/Sources/OrthancWebDav.cpp ${CMAKE_SOURCE_DIR}/Sources/QueryRetrieveHandler.cpp + ${CMAKE_SOURCE_DIR}/Sources/ResourceFinder.cpp ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseConstraint.cpp ${CMAKE_SOURCE_DIR}/Sources/Search/DatabaseLookup.cpp ${CMAKE_SOURCE_DIR}/Sources/Search/DicomTagConstraint.cpp
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -31,6 +31,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" @@ -1278,6 +1279,35 @@ { ListLabelsInternal(target, false, -1); } + + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const Capabilities& capabilities) ORTHANC_OVERRIDE + { + // TODO-FIND + throw OrthancException(ErrorCode_NotImplemented); + } + + + virtual void ExecuteFind(std::list<std::string>& identifiers, + const FindRequest& request, + const Capabilities& capabilities) ORTHANC_OVERRIDE + { + // TODO-FIND + Compatibility::GenericFind find(*this); + find.ExecuteFind(identifiers, request, capabilities); + } + + + 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); + } }; @@ -1492,4 +1522,10 @@ return dbCapabilities_; } } + + + bool OrthancPluginDatabaseV4::HasIntegratedFind() const + { + return false; // TODO-FIND + } }
--- a/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabaseV4.h Tue Jul 09 18:06:21 2024 +0200 @@ -93,6 +93,8 @@ virtual uint64_t MeasureLatency() ORTHANC_OVERRIDE; virtual const Capabilities GetDatabaseCapabilities() const ORTHANC_OVERRIDE; + + virtual bool HasIntegratedFind() const ORTHANC_OVERRIDE; }; }
--- a/OrthancServer/Resources/Orthanc.doxygen Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Resources/Orthanc.doxygen Tue Jul 09 18:06:21 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
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -24,6 +24,7 @@ #include "BaseDatabaseWrapper.h" #include "../../../OrthancFramework/Sources/OrthancException.h" +#include "Compatibility/GenericFind.h" namespace Orthanc { @@ -46,6 +47,32 @@ } + void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(FindResponse& response, + const FindRequest& request, + const Capabilities& capabilities) + { + throw OrthancException(ErrorCode_NotImplemented); // Not supported + } + + + void BaseDatabaseWrapper::BaseTransaction::ExecuteFind(std::list<std::string>& identifiers, + const FindRequest& request, + const Capabilities& capabilities) + { + Compatibility::GenericFind find(*this); + find.ExecuteFind(identifiers, request, capabilities); + } + + + 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
--- a/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/Database/BaseDatabaseWrapper.h Tue Jul 09 18:06:21 2024 +0200 @@ -47,8 +47,25 @@ int64_t& instancesCount, int64_t& compressedSize, int64_t& uncompressedSize) ORTHANC_OVERRIDE; + + virtual void ExecuteFind(FindResponse& response, + const FindRequest& request, + const Capabilities& capabilities) ORTHANC_OVERRIDE; + + virtual void ExecuteFind(std::list<std::string>& identifiers, + const FindRequest& request, + const Capabilities& capabilities) 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; + } }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -0,0 +1,590 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "GenericFind.h" + +#include "../../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../../../OrthancFramework/Sources/OrthancException.h" + +#include <stack> + + +namespace Orthanc +{ + namespace Compatibility + { + static bool IsRequestWithoutContraint(const FindRequest& request) + { + return (request.GetDicomTagConstraints().IsEmpty() && + request.GetMetadataConstraintsCount() == 0 && + request.GetLabels().empty() && + request.GetOrdering().empty()); + } + + static void GetChildren(std::list<int64_t>& target, + IDatabaseWrapper::ITransaction& transaction, + const std::list<int64_t>& resources) + { + target.clear(); + + for (std::list<int64_t>::const_iterator it = resources.begin(); it != resources.end(); ++it) + { + std::list<int64_t> tmp; + transaction.GetChildrenInternalId(tmp, *it); + target.splice(target.begin(), tmp); + } + } + + static void GetChildren(std::list<std::string>& target, + IDatabaseWrapper::ITransaction& transaction, + const std::list<int64_t>& resources) + { + target.clear(); + + for (std::list<int64_t>::const_iterator it = resources.begin(); it != resources.end(); ++it) + { + std::list<std::string> tmp; + transaction.GetChildrenPublicId(tmp, *it); + target.splice(target.begin(), tmp); + } + } + + static void GetChildrenIdentifiers(std::list<std::string>& children, + IDatabaseWrapper::ITransaction& transaction, + const OrthancIdentifiers& identifiers, + ResourceType topLevel, + ResourceType bottomLevel) + { + if (!IsResourceLevelAboveOrEqual(topLevel, bottomLevel) || + topLevel == bottomLevel) + { + throw OrthancException(ErrorCode_InternalError); + } + + std::list<int64_t> currentResources; + ResourceType currentLevel; + + { + int64_t id; + if (!transaction.LookupResource(id, currentLevel, identifiers.GetLevel(topLevel)) || + currentLevel != topLevel) + { + throw OrthancException(ErrorCode_InexistentItem); + } + + currentResources.push_back(id); + } + + while (currentLevel != bottomLevel) + { + ResourceType nextLevel = GetChildResourceType(currentLevel); + if (nextLevel == bottomLevel) + { + GetChildren(children, transaction, currentResources); + } + else + { + std::list<int64_t> nextResources; + GetChildren(nextResources, transaction, currentResources); + currentResources.swap(nextResources); + } + + currentLevel = nextLevel; + } + } + + void GenericFind::ExecuteFind(std::list<std::string>& identifiers, + const FindRequest& request, + const IDatabaseWrapper::Capabilities& capabilities) + { + if (!request.GetLabels().empty() && + !capabilities.HasLabelsSupport()) + { + throw OrthancException(ErrorCode_NotImplemented, "The database backend doesn't support labels"); + } + + if (IsRequestWithoutContraint(request) && + !request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + if (request.HasLimits()) + { + transaction_.GetAllPublicIds(identifiers, request.GetLevel(), request.GetLimitsSince(), request.GetLimitsCount()); + } + else + { + transaction_.GetAllPublicIds(identifiers, request.GetLevel()); + } + } + else if (IsRequestWithoutContraint(request) && + request.GetLevel() == ResourceType_Patient && + request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + // TODO-FIND: This is a trivial case for which no transaction is needed + identifiers.push_back(request.GetOrthancIdentifiers().GetPatientId()); + } + else if (IsRequestWithoutContraint(request) && + request.GetLevel() == ResourceType_Study && + !request.GetOrthancIdentifiers().HasPatientId() && + request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + // TODO-FIND: This is a trivial case for which no transaction is needed + identifiers.push_back(request.GetOrthancIdentifiers().GetStudyId()); + } + else if (IsRequestWithoutContraint(request) && + request.GetLevel() == ResourceType_Series && + !request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + // TODO-FIND: This is a trivial case for which no transaction is needed + identifiers.push_back(request.GetOrthancIdentifiers().GetSeriesId()); + } + else if (IsRequestWithoutContraint(request) && + request.GetLevel() == ResourceType_Instance && + !request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + request.GetOrthancIdentifiers().HasInstanceId()) + { + // TODO-FIND: This is a trivial case for which no transaction is needed + identifiers.push_back(request.GetOrthancIdentifiers().GetInstanceId()); + } + else if (IsRequestWithoutContraint(request) && + (request.GetLevel() == ResourceType_Study || + request.GetLevel() == ResourceType_Series || + request.GetLevel() == ResourceType_Instance) && + request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + GetChildrenIdentifiers(identifiers, transaction_, request.GetOrthancIdentifiers(), ResourceType_Patient, request.GetLevel()); + } + else if (IsRequestWithoutContraint(request) && + (request.GetLevel() == ResourceType_Series || + request.GetLevel() == ResourceType_Instance) && + !request.GetOrthancIdentifiers().HasPatientId() && + request.GetOrthancIdentifiers().HasStudyId() && + !request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + GetChildrenIdentifiers(identifiers, transaction_, request.GetOrthancIdentifiers(), ResourceType_Study, request.GetLevel()); + } + else if (IsRequestWithoutContraint(request) && + request.GetLevel() == ResourceType_Instance && + !request.GetOrthancIdentifiers().HasPatientId() && + !request.GetOrthancIdentifiers().HasStudyId() && + request.GetOrthancIdentifiers().HasSeriesId() && + !request.GetOrthancIdentifiers().HasInstanceId()) + { + GetChildrenIdentifiers(identifiers, transaction_, request.GetOrthancIdentifiers(), ResourceType_Series, request.GetLevel()); + } + else if (request.GetMetadataConstraintsCount() == 0 && + request.GetOrdering().empty()) + { + transaction_.ApplyLookupResources(identifiers, NULL /* TODO-FIND: Could the "instancesId" information be exploited? */, + request.GetDicomTagConstraints(), request.GetLevel(), request.GetLabels(), + request.GetLabelsConstraint(), request.HasLimits() ? request.GetLimitsCount() : 0); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + 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.GetParentSpecification(ResourceType_Patient).IsOfInterest()) + { + return ResourceType_Patient; + } + else + { + return ResourceType_Study; + } + + case ResourceType_Series: + if (request.GetParentSpecification(ResourceType_Patient).IsOfInterest()) + { + return ResourceType_Patient; + } + else if (request.GetParentSpecification(ResourceType_Study).IsOfInterest()) + { + return ResourceType_Study; + } + else + { + return ResourceType_Series; + } + + case ResourceType_Instance: + if (request.GetParentSpecification(ResourceType_Patient).IsOfInterest()) + { + return ResourceType_Patient; + } + else if (request.GetParentSpecification(ResourceType_Study).IsOfInterest()) + { + return ResourceType_Study; + } + else if (request.GetParentSpecification(ResourceType_Series).IsOfInterest()) + { + return ResourceType_Series; + } + else + { + return ResourceType_Instance; + } + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + static ResourceType GetBottomLevelOfInterest(const FindRequest& request) + { + switch (request.GetLevel()) + { + case ResourceType_Patient: + if (request.GetChildrenSpecification(ResourceType_Instance).IsOfInterest()) + { + return ResourceType_Instance; + } + else if (request.GetChildrenSpecification(ResourceType_Series).IsOfInterest()) + { + return ResourceType_Series; + } + else if (request.GetChildrenSpecification(ResourceType_Study).IsOfInterest()) + { + return ResourceType_Study; + } + else + { + return ResourceType_Patient; + } + + case ResourceType_Study: + if (request.GetChildrenSpecification(ResourceType_Instance).IsOfInterest()) + { + return ResourceType_Instance; + } + else if (request.GetChildrenSpecification(ResourceType_Series).IsOfInterest()) + { + return ResourceType_Series; + } + else + { + return ResourceType_Study; + } + + case ResourceType_Series: + if (request.GetChildrenSpecification(ResourceType_Instance).IsOfInterest()) + { + return ResourceType_Instance; + } + else + { + return ResourceType_Series; + } + + case ResourceType_Instance: + 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<FindResponse::Resource> resource(new FindResponse::Resource(request.GetLevel(), internalId, identifier)); + + if (request.IsRetrieveParentIdentifier()) + { + assert(!parent.empty()); + resource->SetParentIdentifier(parent); + } + + if (request.IsRetrieveMainDicomTags()) + { + RetrieveMainDicomTags(*resource, level, internalId); + } + + if (request.IsRetrieveMetadata()) + { + transaction_.GetAllMetadata(resource->GetMetadata(level), internalId); + } + + { + const ResourceType topLevel = GetTopLevelOfInterest(request); + + int64_t currentId = internalId; + ResourceType currentLevel = level; + + while (currentLevel != topLevel) + { + int64_t parentId; + if (transaction_.LookupParent(parentId, currentId)) + { + currentId = parentId; + currentLevel = GetParentResourceType(currentLevel); + } + else + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + + if (request.GetParentSpecification(currentLevel).IsRetrieveMainDicomTags()) + { + RetrieveMainDicomTags(*resource, currentLevel, currentId); + } + + if (request.GetParentSpecification(currentLevel).IsRetrieveMetadata()) + { + transaction_.GetAllMetadata(resource->GetMetadata(currentLevel), currentId); + } + } + } + + if (request.IsRetrieveLabels()) + { + transaction_.ListLabels(resource->GetLabels(), internalId); + } + + if (request.IsRetrieveAttachments()) + { + std::set<FileContentType> attachments; + transaction_.ListAvailableAttachments(attachments, internalId); + + for (std::set<FileContentType>::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); + } + } + } + + { + const ResourceType bottomLevel = GetBottomLevelOfInterest(request); + + std::list<int64_t> currentIds; + currentIds.push_back(internalId); + + ResourceType currentLevel = level; + + while (currentLevel != bottomLevel) + { + ResourceType childrenLevel = GetChildResourceType(currentLevel); + + if (request.GetChildrenSpecification(childrenLevel).IsRetrieveIdentifiers()) + { + for (std::list<int64_t>::const_iterator it = currentIds.begin(); it != currentIds.end(); ++it) + { + std::list<std::string> ids; + transaction_.GetChildrenPublicId(ids, *it); + + for (std::list<std::string>::const_iterator it2 = ids.begin(); it2 != ids.end(); ++it2) + { + resource->AddChildIdentifier(childrenLevel, *it2); + } + } + } + + const std::set<MetadataType>& metadata = request.GetChildrenSpecification(childrenLevel).GetMetadata(); + + for (std::set<MetadataType>::const_iterator it = metadata.begin(); it != metadata.end(); ++it) + { + for (std::list<int64_t>::const_iterator it2 = currentIds.begin(); it2 != currentIds.end(); ++it2) + { + std::list<std::string> values; + transaction_.GetChildrenMetadata(values, *it2, *it); + + for (std::list<std::string>::const_iterator it3 = values.begin(); it3 != values.end(); ++it3) + { + resource->AddChildrenMetadataValue(childrenLevel, *it, *it3); + } + } + } + + const std::set<DicomTag>& mainDicomTags = request.GetChildrenSpecification(childrenLevel).GetMainDicomTags(); + + if (childrenLevel != bottomLevel || + !mainDicomTags.empty()) + { + std::list<int64_t> childrenIds; + + for (std::list<int64_t>::const_iterator it = currentIds.begin(); it != currentIds.end(); ++it) + { + std::list<int64_t> tmp; + transaction_.GetChildrenInternalId(tmp, *it); + + childrenIds.splice(childrenIds.end(), tmp); + } + + if (!mainDicomTags.empty()) + { + for (std::list<int64_t>::const_iterator it = childrenIds.begin(); it != childrenIds.end(); ++it) + { + DicomMap m; + transaction_.GetMainDicomTags(m, *it); + + for (std::set<DicomTag>::const_iterator it2 = mainDicomTags.begin(); it2 != mainDicomTags.end(); ++it2) + { + std::string value; + if (m.LookupStringValue(value, *it2, false /* no binary allowed */)) + { + resource->AddChildrenMainDicomTagValue(childrenLevel, *it2, value); + } + } + } + } + + currentIds = childrenIds; + } + else + { + currentIds.clear(); + } + + currentLevel = childrenLevel; + } + } + + if (request.IsRetrieveOneInstanceIdentifier() && + !request.GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers()) + { + int64_t currentId = internalId; + ResourceType currentLevel = level; + + while (currentLevel != ResourceType_Instance) + { + std::list<int64_t> children; + transaction_.GetChildrenInternalId(children, currentId); + if (children.empty()) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else + { + currentId = children.front(); + currentLevel = GetChildResourceType(currentLevel); + } + } + + resource->AddChildIdentifier(ResourceType_Instance, transaction_.GetPublicId(currentId)); + } + + response.Add(resource.release()); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/Compatibility/GenericFind.h Tue Jul 09 18:06:21 2024 +0200 @@ -0,0 +1,56 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../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: + explicit GenericFind(IDatabaseWrapper::ITransaction& transaction) : + transaction_(transaction) + { + } + + void ExecuteFind(std::list<std::string>& identifiers, + const FindRequest& request, + const IDatabaseWrapper::Capabilities& capabilities); + + void ExecuteExpand(FindResponse& response, + const FindRequest& request, + const std::string& identifier); + }; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindRequest.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -0,0 +1,258 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "FindRequest.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + +#include "MainDicomTagsRegistry.h" + +#include <cassert> + + +namespace Orthanc +{ + FindRequest::ParentSpecification& FindRequest::GetParentSpecification(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level, level_)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + switch (level) + { + case ResourceType_Patient: + return retrieveParentPatient_; + + case ResourceType_Study: + return retrieveParentStudy_; + + case ResourceType_Series: + return retrieveParentSeries_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + FindRequest::ChildrenSpecification& FindRequest::GetChildrenSpecification(ResourceType level) + { + if (!IsResourceLevelAboveOrEqual(level_, level)) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + switch (level) + { + case ResourceType_Study: + return retrieveChildrenStudies_; + + case ResourceType_Series: + return retrieveChildrenSeries_; + + case ResourceType_Instance: + return retrieveChildrenInstances_; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + FindRequest::FindRequest(ResourceType level) : + level_(level), + hasLimits_(false), + limitsSince_(0), + limitsCount_(0), + labelsConstraint_(LabelsConstraint_All), + retrieveMainDicomTags_(false), + retrieveMetadata_(false), + retrieveLabels_(false), + retrieveAttachments_(false), + retrieveParentIdentifier_(false), + retrieveOneInstanceIdentifier_(false) + { + } + + + FindRequest::~FindRequest() + { + + for (std::deque<Ordering*>::iterator it = ordering_.begin(); it != ordering_.end(); ++it) + { + assert(*it != NULL); + delete *it; + } + } + + + void FindRequest::SetOrthancId(ResourceType level, + const std::string& id) + { + switch (level) + { + case ResourceType_Patient: + SetOrthancPatientId(id); + break; + + case ResourceType_Study: + SetOrthancStudyId(id); + break; + + case ResourceType_Series: + SetOrthancSeriesId(id); + break; + + case ResourceType_Instance: + SetOrthancInstanceId(id); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void FindRequest::SetOrthancPatientId(const std::string& id) + { + orthancIdentifiers_.SetPatientId(id); + } + + + void FindRequest::SetOrthancStudyId(const std::string& id) + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + orthancIdentifiers_.SetStudyId(id); + } + } + + + void FindRequest::SetOrthancSeriesId(const std::string& id) + { + if (level_ == ResourceType_Patient || + level_ == ResourceType_Study) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + orthancIdentifiers_.SetSeriesId(id); + } + } + + + void FindRequest::SetOrthancInstanceId(const std::string& id) + { + if (level_ == ResourceType_Patient || + level_ == ResourceType_Study || + level_ == ResourceType_Series) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + orthancIdentifiers_.SetInstanceId(id); + } + } + + + void FindRequest::SetLimits(uint64_t since, + uint64_t count) + { + 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::SetRetrieveParentIdentifier(bool retrieve) + { + if (level_ == ResourceType_Patient) + { + throw OrthancException(ErrorCode_BadParameterType); + } + else + { + retrieveParentIdentifier_ = retrieve; + } + } + + + void FindRequest::SetRetrieveOneInstanceIdentifier(bool retrieve) + { + if (level_ == ResourceType_Instance) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + retrieveOneInstanceIdentifier_ = retrieve; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindRequest.h Tue Jul 09 18:06:21 2024 +0200 @@ -0,0 +1,432 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomTag.h" +#include "../Search/DatabaseConstraint.h" +#include "../Search/DicomTagConstraint.h" +#include "../Search/ISqlLookupFormatter.h" +#include "../ServerEnumerations.h" +#include "OrthancIdentifiers.h" + +#include <deque> +#include <map> +#include <set> +#include <cassert> +#include <boost/shared_ptr.hpp> + +namespace Orthanc +{ + class MainDicomTagsRegistry; + + 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<FindRequest::ResponseContent>(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(); + } + }; + + + class ParentSpecification : public boost::noncopyable + { + private: + bool mainDicomTags_; + bool metadata_; + + public: + ParentSpecification() : + mainDicomTags_(false), + metadata_(false) + { + } + + void SetRetrieveMainDicomTags(bool retrieve) + { + mainDicomTags_ = retrieve; + } + + bool IsRetrieveMainDicomTags() const + { + return mainDicomTags_; + } + + void SetRetrieveMetadata(bool retrieve) + { + metadata_ = retrieve; + } + + bool IsRetrieveMetadata() const + { + return metadata_; + } + + bool IsOfInterest() const + { + return (mainDicomTags_ || metadata_); + } + }; + + + class ChildrenSpecification : public boost::noncopyable + { + private: + bool identifiers_; + std::set<MetadataType> metadata_; + std::set<DicomTag> mainDicomTags_; + + public: + ChildrenSpecification() : + identifiers_(false) + { + } + + void SetRetrieveIdentifiers(bool retrieve) + { + identifiers_ = retrieve; + } + + bool IsRetrieveIdentifiers() const + { + return identifiers_; + } + + void AddMetadata(MetadataType metadata) + { + metadata_.insert(metadata); + } + + const std::set<MetadataType>& GetMetadata() const + { + return metadata_; + } + + void AddMainDicomTag(const DicomTag& tag) + { + mainDicomTags_.insert(tag); + } + + const std::set<DicomTag>& GetMainDicomTags() const + { + return mainDicomTags_; + } + + bool IsOfInterest() const + { + return (identifiers_ || !metadata_.empty() || !mainDicomTags_.empty()); + } + }; + + + 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 + DatabaseConstraints dicomTagConstraints_; // All tags filters (note: the order is not important) + std::deque<void*> /* TODO-FIND */ metadataConstraints_; // All metadata filters (note: the order is not important) + bool hasLimits_; + uint64_t limitsSince_; + uint64_t limitsCount_; + std::set<std::string> labels_; + LabelsConstraint labelsConstraint_; + std::deque<Ordering*> ordering_; // The ordering criteria (note: the order is important !) + + bool retrieveMainDicomTags_; + bool retrieveMetadata_; + bool retrieveLabels_; + bool retrieveAttachments_; + bool retrieveParentIdentifier_; + ParentSpecification retrieveParentPatient_; + ParentSpecification retrieveParentStudy_; + ParentSpecification retrieveParentSeries_; + ChildrenSpecification retrieveChildrenStudies_; + ChildrenSpecification retrieveChildrenSeries_; + ChildrenSpecification retrieveChildrenInstances_; + bool retrieveOneInstanceIdentifier_; + + std::unique_ptr<MainDicomTagsRegistry> mainDicomTagsRegistry_; + + public: + explicit FindRequest(ResourceType level); + + ~FindRequest(); + + ResourceType GetLevel() const + { + return level_; + } + + void SetOrthancId(ResourceType level, + const std::string& id); + + void SetOrthancPatientId(const std::string& id); + + void SetOrthancStudyId(const std::string& id); + + void SetOrthancSeriesId(const std::string& id); + + void SetOrthancInstanceId(const std::string& id); + + const OrthancIdentifiers& GetOrthancIdentifiers() const + { + return orthancIdentifiers_; + } + + DatabaseConstraints& GetDicomTagConstraints() + { + return dicomTagConstraints_; + } + + const DatabaseConstraints& GetDicomTagConstraints() const + { + return dicomTagConstraints_; + } + + size_t GetMetadataConstraintsCount() const + { + return metadataConstraints_.size(); + } + + 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<Ordering*>& GetOrdering() const + { + return ordering_; + } + + void SetLabels(const std::set<std::string>& labels) + { + labels_ = labels; + } + + void AddLabel(const std::string& label) + { + labels_.insert(label); + } + + const std::set<std::string>& GetLabels() const + { + return labels_; + } + + LabelsConstraint GetLabelsConstraint() const + { + return labelsConstraint_; + } + + void SetLabelsConstraint(LabelsConstraint constraint) + { + labelsConstraint_ = constraint; + } + + void SetRetrieveMainDicomTags(bool retrieve) + { + retrieveMainDicomTags_ = retrieve; + } + + bool IsRetrieveMainDicomTags() const + { + return retrieveMainDicomTags_; + } + + void SetRetrieveMetadata(bool retrieve) + { + retrieveMetadata_ = retrieve; + } + + bool IsRetrieveMetadata() const + { + return retrieveMetadata_; + } + + 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_; + } + + ParentSpecification& GetParentSpecification(ResourceType level); + + const ParentSpecification& GetParentSpecification(ResourceType level) const + { + return const_cast<FindRequest&>(*this).GetParentSpecification(level); + } + + ChildrenSpecification& GetChildrenSpecification(ResourceType level); + + const ChildrenSpecification& GetChildrenSpecification(ResourceType level) const + { + return const_cast<FindRequest&>(*this).GetChildrenSpecification(level); + } + + void SetRetrieveOneInstanceIdentifier(bool retrieve); + + bool IsRetrieveOneInstanceIdentifier() const + { + return (retrieveOneInstanceIdentifier_ || + GetChildrenSpecification(ResourceType_Instance).IsRetrieveIdentifiers()); + } + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindResponse.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -0,0 +1,733 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "FindResponse.h" + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomArray.h" +#include "../../../OrthancFramework/Sources/OrthancException.h" +#include "../../../OrthancFramework/Sources/SerializationToolbox.h" + +#include <boost/lexical_cast.hpp> +#include <cassert> + + +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); + } + } + } + + + FindResponse::ChildrenInformation::~ChildrenInformation() + { + for (MetadataValues::iterator it = metadataValues_.begin(); it != metadataValues_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + + for (MainDicomTagValues::iterator it = mainDicomTagValues_.begin(); it != mainDicomTagValues_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void FindResponse::ChildrenInformation::AddIdentifier(const std::string& identifier) + { + if (identifiers_.find(identifier) == identifiers_.end()) + { + identifiers_.insert(identifier); + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void FindResponse::ChildrenInformation::AddMetadataValue(MetadataType metadata, + const std::string& value) + { + MetadataValues::iterator found = metadataValues_.find(metadata); + + if (found == metadataValues_.end()) + { + std::set<std::string> s; + s.insert(value); + metadataValues_[metadata] = new std::set(s); + } + else + { + assert(found->second != NULL); + found->second->insert(value); + } + } + + + void FindResponse::ChildrenInformation::GetMetadataValues(std::set<std::string>& values, + MetadataType metadata) const + { + MetadataValues::const_iterator found = metadataValues_.find(metadata); + + if (found == metadataValues_.end()) + { + values.clear(); + } + else + { + assert(found->second != NULL); + values = *found->second; + } + } + + + void FindResponse::ChildrenInformation::AddMainDicomTagValue(const DicomTag& tag, + const std::string& value) + { + MainDicomTagValues::iterator found = mainDicomTagValues_.find(tag); + + if (found == mainDicomTagValues_.end()) + { + std::set<std::string> s; + s.insert(value); + mainDicomTagValues_[tag] = new std::set(s); + } + else + { + assert(found->second != NULL); + found->second->insert(value); + } + } + + + void FindResponse::ChildrenInformation::GetMainDicomTagValues(std::set<std::string>& values, + const DicomTag& tag) const + { + MainDicomTagValues::const_iterator found = mainDicomTagValues_.find(tag); + + if (found == mainDicomTagValues_.end()) + { + values.clear(); + } + else + { + assert(found->second != NULL); + values = *found->second; + } + } + + + FindResponse::ChildrenInformation& FindResponse::Resource::GetChildrenInformation(ResourceType level) + { + switch (level) + { + case ResourceType_Study: + if (level_ == ResourceType_Patient) + { + return childrenStudiesInformation_; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + case ResourceType_Series: + if (level_ == ResourceType_Patient || + level_ == ResourceType_Study) + { + return childrenSeriesInformation_; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + case ResourceType_Instance: + if (level_ == ResourceType_Patient || + level_ == ResourceType_Study || + level_ == ResourceType_Series) + { + return childrenInstancesInformation_; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + 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::GetAllMainDicomTags(DicomMap& target) const + { + switch (level_) + { + // Don't reorder or add "break" below + case ResourceType_Instance: + mainDicomTagsInstance_.Export(target); + + case ResourceType_Series: + mainDicomTagsSeries_.Export(target); + + case ResourceType_Study: + mainDicomTagsStudy_.Export(target); + + case ResourceType_Patient: + mainDicomTagsPatient_.Export(target); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + void FindResponse::Resource::AddMetadata(ResourceType level, + MetadataType metadata, + const std::string& value) + { + std::map<MetadataType, std::string>& m = GetMetadata(level); + + if (m.find(metadata) != m.end()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); // Metadata already present + } + else + { + m[metadata] = value; + } + } + + + std::map<MetadataType, std::string>& 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<MetadataType, std::string>& m = GetMetadata(level); + + std::map<MetadataType, std::string>::const_iterator found = m.find(metadata); + + if (found == m.end()) + { + return false; + } + else + { + value = found->second; + return true; + } + } + + + 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)); + } + } + + + 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); + } + } + + + 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<FileContentType, FileInfo>::const_iterator it = attachments_.find(type); + if (it != attachments_.end()) + { + target = it->second; + return true; + } + else + { + return false; + } + } + + + const std::string& FindResponse::Resource::GetOneInstanceIdentifier() const + { + const std::set<std::string>& instances = GetChildrenInformation(ResourceType_Instance).GetIdentifiers(); + + if (instances.size() == 0) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); // HasOneInstanceIdentifier() should have been called + } + else + { + return *instances.begin(); + } + } + + + 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<MetadataType, std::string>& m) + { + target = Json::objectValue; + + for (std::map<MetadataType, std::string>::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<Json::UInt64>(info.GetUncompressedSize())); + target[EnumerationToString(info.GetContentType())] = u; + } + + + static void DebugSetOfStrings(Json::Value& target, + const std::set<std::string>& values) + { + target = Json::arrayValue; + for (std::set<std::string>::const_iterator it = values.begin(); it != values.end(); ++it) + { + target.append(*it); + } + } + + + 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()) + { + DicomMap m; + GetMainDicomTags(m, request.GetLevel()); + DebugDicomMap(target[EnumerationToString(GetLevel())]["MainDicomTags"], m); + } + + if (request.IsRetrieveMetadata()) + { + DebugMetadata(target[EnumerationToString(GetLevel())]["Metadata"], GetMetadata(request.GetLevel())); + } + + static const ResourceType levels[4] = { ResourceType_Patient, ResourceType_Study, ResourceType_Series, ResourceType_Instance }; + + for (size_t i = 0; i < 4; i++) + { + const char* level = EnumerationToString(levels[i]); + + if (levels[i] != request.GetLevel() && + IsResourceLevelAboveOrEqual(levels[i], request.GetLevel())) + { + if (request.GetParentSpecification(levels[i]).IsRetrieveMainDicomTags()) + { + DicomMap m; + GetMainDicomTags(m, levels[i]); + DebugDicomMap(target[level]["MainDicomTags"], m); + } + + if (request.GetParentSpecification(levels[i]).IsRetrieveMainDicomTags()) + { + DebugMetadata(target[level]["Metadata"], GetMetadata(levels[i])); + } + } + + if (levels[i] != request.GetLevel() && + IsResourceLevelAboveOrEqual(request.GetLevel(), levels[i])) + { + if (request.GetChildrenSpecification(levels[i]).IsRetrieveIdentifiers()) + { + DebugSetOfStrings(target[level]["Identifiers"], GetChildrenInformation(levels[i]).GetIdentifiers()); + } + + const std::set<MetadataType>& metadata = request.GetChildrenSpecification(levels[i]).GetMetadata(); + for (std::set<MetadataType>::const_iterator it = metadata.begin(); it != metadata.end(); ++it) + { + std::set<std::string> values; + GetChildrenInformation(levels[i]).GetMetadataValues(values, *it); + DebugSetOfStrings(target[level]["Metadata"][EnumerationToString(*it)], values); + } + + const std::set<DicomTag>& tags = request.GetChildrenSpecification(levels[i]).GetMainDicomTags(); + for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + std::set<std::string> values; + GetChildrenInformation(levels[i]).GetMainDicomTagValues(values, *it); + DebugSetOfStrings(target[level]["MainDicomTags"][it->Format()], values); + } + } + } + + if (request.IsRetrieveLabels()) + { + DebugSetOfStrings(target["Labels"], labels_); + } + + if (request.IsRetrieveAttachments()) + { + Json::Value v = Json::objectValue; + for (std::map<FileContentType, FileInfo>::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; + } + + if (request.IsRetrieveOneInstanceIdentifier()) + { + target["OneInstance"] = GetOneInstanceIdentifier(); + } + } + + + 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<Resource> 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::GetResourceByIndex(size_t index) const + { + if (index >= items_.size()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + else + { + assert(items_[index] != NULL); + return *items_[index]; + } + } + + + FindResponse::Resource& FindResponse::GetResourceByIdentifier(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; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/FindResponse.h Tue Jul 09 18:06:21 2024 +0200 @@ -0,0 +1,312 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" +#include "../../../OrthancFramework/Sources/Enumerations.h" +#include "../../../OrthancFramework/Sources/FileStorage/FileInfo.h" +#include "../ServerEnumerations.h" +#include "OrthancIdentifiers.h" +#include "FindRequest.h" + +#include <boost/noncopyable.hpp> +#include <deque> +#include <map> +#include <set> +#include <list> + + +namespace Orthanc +{ + class FindResponse : public boost::noncopyable + { + private: + class MainDicomTagsAtLevel : public boost::noncopyable + { + private: + class DicomValue; + + typedef std::map<DicomTag, DicomValue*> 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; + }; + + class ChildrenInformation : public boost::noncopyable + { + private: + typedef std::map<MetadataType, std::set<std::string>* > MetadataValues; + typedef std::map<DicomTag, std::set<std::string>* > MainDicomTagValues; + + std::set<std::string> identifiers_; + MetadataValues metadataValues_; + MainDicomTagValues mainDicomTagValues_; + + public: + ~ChildrenInformation(); + + void AddIdentifier(const std::string& identifier); + + const std::set<std::string>& GetIdentifiers() const + { + return identifiers_; + } + + void AddMetadataValue(MetadataType metadata, + const std::string& value); + + void GetMetadataValues(std::set<std::string>& values, + MetadataType metadata) const; + + void AddMainDicomTagValue(const DicomTag& tag, + const std::string& value); + + void GetMainDicomTagValues(std::set<std::string>& values, + const DicomTag& tag) const; + }; + + + public: + class Resource : public boost::noncopyable + { + private: + typedef std::map<MetadataType, std::list<std::string>*> ChildrenMetadata; + + ResourceType level_; + int64_t internalId_; // Internal ID of the resource in the database + std::string identifier_; + std::unique_ptr<std::string> parentIdentifier_; + MainDicomTagsAtLevel mainDicomTagsPatient_; + MainDicomTagsAtLevel mainDicomTagsStudy_; + MainDicomTagsAtLevel mainDicomTagsSeries_; + MainDicomTagsAtLevel mainDicomTagsInstance_; + std::map<MetadataType, std::string> metadataPatient_; + std::map<MetadataType, std::string> metadataStudy_; + std::map<MetadataType, std::string> metadataSeries_; + std::map<MetadataType, std::string> metadataInstance_; + ChildrenInformation childrenStudiesInformation_; + ChildrenInformation childrenSeriesInformation_; + ChildrenInformation childrenInstancesInformation_; + std::set<std::string> labels_; + std::map<FileContentType, FileInfo> attachments_; + + MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level); + + const MainDicomTagsAtLevel& GetMainDicomTagsAtLevel(ResourceType level) const + { + return const_cast<Resource&>(*this).GetMainDicomTagsAtLevel(level); + } + + ChildrenInformation& GetChildrenInformation(ResourceType level); + + const ChildrenInformation& GetChildrenInformation(ResourceType level) const + { + return const_cast<Resource&>(*this).GetChildrenInformation(level); + } + + public: + Resource(ResourceType level, + int64_t internalId, + const std::string& identifier) : + level_(level), + internalId_(internalId), + identifier_(identifier) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + int64_t GetInternalId() const + { + return internalId_; + } + + const std::string& GetIdentifier() const + { + return identifier_; + } + + void SetParentIdentifier(const std::string& id); + + const std::string& GetParentIdentifier() const; + + 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 GetAllMainDicomTags(DicomMap& target) const; + + void AddMetadata(ResourceType level, + MetadataType metadata, + const std::string& value); + + std::map<MetadataType, std::string>& GetMetadata(ResourceType level); + + const std::map<MetadataType, std::string>& GetMetadata(ResourceType level) const + { + return const_cast<Resource&>(*this).GetMetadata(level); + } + + bool LookupMetadata(std::string& value, + ResourceType level, + MetadataType metadata) const; + + void AddChildIdentifier(ResourceType level, + const std::string& childId) + { + GetChildrenInformation(level).AddIdentifier(childId); + } + + const std::set<std::string>& GetChildrenIdentifiers(ResourceType level) const + { + return GetChildrenInformation(level).GetIdentifiers(); + } + + void AddChildrenMetadataValue(ResourceType level, + MetadataType metadata, + const std::string& value) + { + GetChildrenInformation(level).AddMetadataValue(metadata, value); + } + + void GetChildrenMetadataValues(std::set<std::string>& values, + ResourceType level, + MetadataType metadata) const + { + GetChildrenInformation(level).GetMetadataValues(values, metadata); + } + + void AddChildrenMainDicomTagValue(ResourceType level, + const DicomTag& tag, + const std::string& value) + { + GetChildrenInformation(level).AddMainDicomTagValue(tag, value); + } + + void GetChildrenMainDicomTagValues(std::set<std::string>& values, + ResourceType level, + const DicomTag& tag) const + { + GetChildrenInformation(level).GetMainDicomTagValues(values, tag); + } + + void AddLabel(const std::string& label); + + std::set<std::string>& GetLabels() + { + return labels_; + } + + const std::set<std::string>& GetLabels() const + { + return labels_; + } + + void AddAttachment(const FileInfo& attachment); + + bool LookupAttachment(FileInfo& target, + FileContentType type) const; + + const std::map<FileContentType, FileInfo>& GetAttachments() const + { + return attachments_; + } + + const std::string& GetOneInstanceIdentifier() const; + + bool HasOneInstanceIdentifier() const + { + return !GetChildrenIdentifiers(ResourceType_Instance).empty(); + } + + void DebugExport(Json::Value& target, + const FindRequest& request) const; + }; + + private: + typedef std::map<std::string, Resource*> Index; + + std::deque<Resource*> items_; + Index index_; + + public: + ~FindResponse(); + + void Add(Resource* item /* takes ownership */); + + size_t GetSize() const + { + return items_.size(); + } + + const Resource& GetResourceByIndex(size_t index) const; + + Resource& GetResourceByIdentifier(const std::string& id); + + const Resource& GetResourceByIdentifier(const std::string& id) const + { + return const_cast<FindResponse&>(*this).GetResourceByIdentifier(id); + } + + bool HasResource(const std::string& id) const + { + return (index_.find(id) != index_.end()); + } + }; +}
--- a/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h Tue Jul 09 18:06:21 2024 +0200 @@ -29,6 +29,8 @@ #include "../ExportedResource.h" #include "../Search/ISqlLookupFormatter.h" #include "../ServerIndexChange.h" +#include "FindRequest.h" +#include "FindResponse.h" #include "IDatabaseListener.h" #include <list> @@ -351,6 +353,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 Capabilities& capabilities) = 0; + + // This is only implemented if "HasIntegratedFind()" is "false" + virtual void ExecuteFind(std::list<std::string>& identifiers, + const FindRequest& request, + const Capabilities& capabilities) = 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; }; @@ -375,5 +404,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; }; }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -0,0 +1,128 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "../PrecompiledHeadersServer.h" +#include "MainDicomTagsRegistry.h" + +#include "../ServerToolbox.h" + +namespace Orthanc +{ + void MainDicomTagsRegistry::LoadTags(ResourceType level) + { + { + const DicomTag* tags = NULL; + size_t size; + + ServerToolbox::LoadIdentifiers(tags, size, level); + + for (size_t i = 0; i < size; i++) + { + if (registry_.find(tags[i]) == registry_.end()) + { + registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier); + } + else + { + // These patient-level tags are copied in the study level + assert(level == ResourceType_Study && + (tags[i] == DICOM_TAG_PATIENT_ID || + tags[i] == DICOM_TAG_PATIENT_NAME || + tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE)); + } + } + } + + { + std::set<DicomTag> tags; + DicomMap::GetMainDicomTags(tags, level); + + for (std::set<DicomTag>::const_iterator + tag = tags.begin(); tag != tags.end(); ++tag) + { + if (registry_.find(*tag) == registry_.end()) + { + registry_[*tag] = TagInfo(level, DicomTagType_Main); + } + } + } + } + + + MainDicomTagsRegistry::MainDicomTagsRegistry() + { + LoadTags(ResourceType_Patient); + LoadTags(ResourceType_Study); + LoadTags(ResourceType_Series); + LoadTags(ResourceType_Instance); + } + + + void MainDicomTagsRegistry::LookupTag(ResourceType& level, + DicomTagType& type, + const DicomTag& tag) const + { + Registry::const_iterator it = registry_.find(tag); + + if (it == registry_.end()) + { + // Default values + level = ResourceType_Instance; + type = DicomTagType_Generic; + } + else + { + level = it->second.GetLevel(); + type = it->second.GetType(); + } + } + + + void MainDicomTagsRegistry::NormalizeLookup(DatabaseConstraints& target, + const DatabaseLookup& source, + ResourceType queryLevel) const + { + target.Clear(); + + for (size_t i = 0; i < source.GetConstraintsCount(); i++) + { + ResourceType level; + DicomTagType type; + + LookupTag(level, type, source.GetConstraint(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 && + queryLevel != ResourceType_Patient) + { + level = ResourceType_Study; + } + + target.AddConstraint(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type)); + } + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/MainDicomTagsRegistry.h Tue Jul 09 18:06:21 2024 +0200 @@ -0,0 +1,82 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../Search/DatabaseLookup.h" + +#include <boost/noncopyable.hpp> + + +namespace Orthanc +{ + class MainDicomTagsRegistry : public boost::noncopyable + { + private: + class TagInfo + { + private: + ResourceType level_; + DicomTagType type_; + + public: + TagInfo() + { + } + + TagInfo(ResourceType level, + DicomTagType type) : + level_(level), + type_(type) + { + } + + ResourceType GetLevel() const + { + return level_; + } + + DicomTagType GetType() const + { + return type_; + } + }; + + typedef std::map<DicomTag, TagInfo> Registry; + + Registry registry_; + + void LoadTags(ResourceType level); + + public: + MainDicomTagsRegistry(); + + void LookupTag(ResourceType& level, + DicomTagType& type, + const DicomTag& tag) const; + + void NormalizeLookup(DatabaseConstraints& target, + const DatabaseLookup& source, + ResourceType queryLevel) const; + }; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/OrthancIdentifiers.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -0,0 +1,243 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "FindRequest.h" + +#include "../../../OrthancFramework/Sources/OrthancException.h" + + +namespace Orthanc +{ + OrthancIdentifiers::OrthancIdentifiers(const OrthancIdentifiers& other) + { + if (other.HasPatientId()) + { + SetPatientId(other.GetPatientId()); + } + + if (other.HasStudyId()) + { + SetStudyId(other.GetStudyId()); + } + + if (other.HasSeriesId()) + { + SetSeriesId(other.GetSeriesId()); + } + + if (other.HasInstanceId()) + { + SetInstanceId(other.GetInstanceId()); + } + } + + + void OrthancIdentifiers::SetPatientId(const std::string& id) + { + if (HasPatientId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + patientId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetPatientId() const + { + if (HasPatientId()) + { + return *patientId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetStudyId(const std::string& id) + { + if (HasStudyId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + studyId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetStudyId() const + { + if (HasStudyId()) + { + return *studyId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetSeriesId(const std::string& id) + { + if (HasSeriesId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + seriesId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetSeriesId() const + { + if (HasSeriesId()) + { + return *seriesId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + void OrthancIdentifiers::SetInstanceId(const std::string& id) + { + if (HasInstanceId()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + instanceId_.reset(new std::string(id)); + } + } + + + const std::string& OrthancIdentifiers::GetInstanceId() const + { + if (HasInstanceId()) + { + return *instanceId_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + + ResourceType OrthancIdentifiers::DetectLevel() const + { + if (HasPatientId() && + !HasStudyId() && + !HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Patient; + } + else if (HasPatientId() && + HasStudyId() && + !HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Study; + } + else if (HasPatientId() && + HasStudyId() && + HasSeriesId() && + !HasInstanceId()) + { + return ResourceType_Series; + } + else if (HasPatientId() && + HasStudyId() && + HasSeriesId() && + HasInstanceId()) + { + return ResourceType_Instance; + } + else + { + throw OrthancException(ErrorCode_InexistentItem); + } + } + + + void OrthancIdentifiers::SetLevel(ResourceType level, + const std::string id) + { + switch (level) + { + case ResourceType_Patient: + SetPatientId(id); + break; + + case ResourceType_Study: + SetStudyId(id); + break; + + case ResourceType_Series: + SetSeriesId(id); + break; + + case ResourceType_Instance: + SetInstanceId(id); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + + std::string OrthancIdentifiers::GetLevel(ResourceType level) const + { + switch (level) + { + case ResourceType_Patient: + return GetPatientId(); + + case ResourceType_Study: + return GetStudyId(); + + case ResourceType_Series: + return GetSeriesId(); + + case ResourceType_Instance: + return GetInstanceId(); + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/Database/OrthancIdentifiers.h Tue Jul 09 18:06:21 2024 +0200 @@ -0,0 +1,93 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "../../../OrthancFramework/Sources/Compatibility.h" +#include "../../../OrthancFramework/Sources/Enumerations.h" + +#include <boost/noncopyable.hpp> +#include <string> + + +namespace Orthanc +{ + class OrthancIdentifiers : public boost::noncopyable + { + private: + std::unique_ptr<std::string> patientId_; + std::unique_ptr<std::string> studyId_; + std::unique_ptr<std::string> seriesId_; + std::unique_ptr<std::string> instanceId_; + + public: + OrthancIdentifiers() + { + } + + OrthancIdentifiers(const OrthancIdentifiers& other); + + void SetPatientId(const std::string& id); + + bool HasPatientId() const + { + return patientId_.get() != NULL; + } + + const std::string& GetPatientId() const; + + void SetStudyId(const std::string& id); + + bool HasStudyId() const + { + return studyId_.get() != NULL; + } + + const std::string& GetStudyId() const; + + void SetSeriesId(const std::string& id); + + bool HasSeriesId() const + { + return seriesId_.get() != NULL; + } + + const std::string& GetSeriesId() const; + + void SetInstanceId(const std::string& id); + + bool HasInstanceId() const + { + return instanceId_.get() != NULL; + } + + const std::string& GetInstanceId() const; + + ResourceType DetectLevel() const; + + void SetLevel(ResourceType level, + const std::string id); + + std::string GetLevel(ResourceType level) const; + }; +}
--- a/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -29,6 +29,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" @@ -1138,6 +1139,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<DatabaseConstraint>& normalized) ORTHANC_OVERRIDE + { +#if 0 + Compatibility::GenericFind find(*this); + find.Execute(response, request); +#else + { + SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS FilteredResourcesIds"); + s.Run(); + } + + { + + LookupFormatter formatter; + + std::string sqlLookup; + LookupFormatter::Apply(sqlLookup, + formatter, + normalized, + request.GetLevel(), + request.GetLabels(), + request.GetLabelsConstraint(), + (request.HasLimits() ? request.GetLimitsCount() : 0)); // TODO: handles since and count + + { + // 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<MetadataType>(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<FileContentType>(statement.ColumnInt(2)), + statement.ColumnInt64(3), + statement.ColumnString(6), + static_cast<CompressionType>(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 + };
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -300,141 +300,6 @@ } - class StatelessDatabaseOperations::MainDicomTagsRegistry : public boost::noncopyable - { - private: - class TagInfo - { - private: - ResourceType level_; - DicomTagType type_; - - public: - TagInfo() - { - } - - TagInfo(ResourceType level, - DicomTagType type) : - level_(level), - type_(type) - { - } - - ResourceType GetLevel() const - { - return level_; - } - - DicomTagType GetType() const - { - return type_; - } - }; - - typedef std::map<DicomTag, TagInfo> Registry; - - - Registry registry_; - - void LoadTags(ResourceType level) - { - { - const DicomTag* tags = NULL; - size_t size; - - ServerToolbox::LoadIdentifiers(tags, size, level); - - for (size_t i = 0; i < size; i++) - { - if (registry_.find(tags[i]) == registry_.end()) - { - registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier); - } - else - { - // These patient-level tags are copied in the study level - assert(level == ResourceType_Study && - (tags[i] == DICOM_TAG_PATIENT_ID || - tags[i] == DICOM_TAG_PATIENT_NAME || - tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE)); - } - } - } - - { - std::set<DicomTag> tags; - DicomMap::GetMainDicomTags(tags, level); - - for (std::set<DicomTag>::const_iterator - tag = tags.begin(); tag != tags.end(); ++tag) - { - if (registry_.find(*tag) == registry_.end()) - { - registry_[*tag] = TagInfo(level, DicomTagType_Main); - } - } - } - } - - void LookupTag(ResourceType& level, - DicomTagType& type, - const DicomTag& tag) const - { - Registry::const_iterator it = registry_.find(tag); - - if (it == registry_.end()) - { - // Default values - level = ResourceType_Instance; - type = DicomTagType_Generic; - } - else - { - level = it->second.GetLevel(); - type = it->second.GetType(); - } - } - - public: - MainDicomTagsRegistry() - { - LoadTags(ResourceType_Patient); - LoadTags(ResourceType_Study); - LoadTags(ResourceType_Series); - LoadTags(ResourceType_Instance); - } - - void NormalizeLookup(DatabaseConstraints& target, - const DatabaseLookup& source, - ResourceType queryLevel) const - { - target.Clear(); - - for (size_t i = 0; i < source.GetConstraintsCount(); i++) - { - ResourceType level; - DicomTagType type; - - LookupTag(level, type, source.GetConstraint(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 && - queryLevel != ResourceType_Patient) - { - level = ResourceType_Study; - } - - target.AddConstraint(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type)); - } - } - } - }; - - void StatelessDatabaseOperations::ReadWriteTransaction::LogChange(int64_t internalId, ChangeType changeType, ResourceType resourceType, @@ -902,7 +767,7 @@ Toolbox::GetMissingsFromSet(target.missingRequestedTags_, requestedTags, savedMainDicomTags); while ((target.missingRequestedTags_.size() > 0) - && currentLevel != ResourceType_Patient) + && currentLevel != ResourceType_Patient) { currentLevel = GetParentResourceType(currentLevel); @@ -2515,7 +2380,7 @@ catch (boost::bad_lexical_cast&) { LOG(ERROR) << "Cannot read the global sequence " - << boost::lexical_cast<std::string>(sequence_) << ", resetting it"; + << boost::lexical_cast<std::string>(sequence_) << ", resetting it"; oldValue = 0; } @@ -2814,8 +2679,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_)); @@ -2940,7 +2805,7 @@ } bool StatelessDatabaseOperations::ReadWriteTransaction::HasReachedMaxPatientCount(unsigned int maximumPatientCount, - const std::string& patientId) + const std::string& patientId) { if (maximumPatientCount != 0) { @@ -3042,7 +2907,7 @@ }; if (maximumStorageMode == MaxStorageMode_Recycle - && (maximumStorageSize != 0 || maximumPatientCount != 0)) + && (maximumStorageSize != 0 || maximumPatientCount != 0)) { Operations operations(maximumStorageSize, maximumPatientCount); Apply(operations); @@ -3106,9 +2971,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); @@ -3301,7 +3166,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(); } @@ -3330,7 +3195,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_) { @@ -3806,4 +3671,77 @@ boost::shared_lock<boost::shared_mutex> lock(mutex_); return db_.GetDatabaseCapabilities().HasLabelsSupport(); } + + + void StatelessDatabaseOperations::ExecuteFind(FindResponse& response, + const FindRequest& request) + { + class IntegratedFind : public ReadOnlyOperationsT3<FindResponse&, const FindRequest&, + const IDatabaseWrapper::Capabilities&> + { + 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<std::list<std::string>&, const FindRequest&, + const IDatabaseWrapper::Capabilities&> + { + 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<FindResponse&, const FindRequest&, const std::string&> + { + public: + virtual void ApplyTuple(ReadOnlyTransaction& transaction, + const Tuple& tuple) ORTHANC_OVERRIDE + { + transaction.ExecuteExpand(tuple.get<0>(), tuple.get<1>(), tuple.get<2>()); + } + }; + + IDatabaseWrapper::Capabilities capabilities = db_.GetDatabaseCapabilities(); + + 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, capabilities); + } + 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<std::string> identifiers; + + FindStage find; + find.Apply(*this, identifiers, request, capabilities); + + ExpandStage expand; + + for (std::list<std::string>::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); + } + } + } }
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h Tue Jul 09 18:06:21 2024 +0200 @@ -25,8 +25,9 @@ #include "../../../OrthancFramework/Sources/DicomFormat/DicomMap.h" +#include "../DicomInstanceOrigin.h" #include "IDatabaseWrapper.h" -#include "../DicomInstanceOrigin.h" +#include "MainDicomTagsRegistry.h" #include <boost/shared_ptr.hpp> #include <boost/thread/shared_mutex.hpp> @@ -80,7 +81,7 @@ indexInSeries_(0) { } - + void SetResource(ResourceType level, const std::string& id) { @@ -112,15 +113,26 @@ enum ExpandResourceFlags { ExpandResourceFlags_None = 0, + // used to fetch from DB and for output ExpandResourceFlags_IncludeMetadata = (1 << 0), ExpandResourceFlags_IncludeChildren = (1 << 1), ExpandResourceFlags_IncludeMainDicomTags = (1 << 2), ExpandResourceFlags_IncludeLabels = (1 << 3), - ExpandResourceFlags_Default = (ExpandResourceFlags_IncludeMetadata | - ExpandResourceFlags_IncludeChildren | - ExpandResourceFlags_IncludeMainDicomTags | - ExpandResourceFlags_IncludeLabels) + // only used for output + ExpandResourceFlags_IncludeAllMetadata = (1 << 4), // new in Orthanc 1.12.4 + ExpandResourceFlags_IncludeIsStable = (1 << 5), // new in Orthanc 1.12.4 + + ExpandResourceFlags_DefaultExtract = (ExpandResourceFlags_IncludeMetadata | + ExpandResourceFlags_IncludeChildren | + ExpandResourceFlags_IncludeMainDicomTags | + ExpandResourceFlags_IncludeLabels), + + ExpandResourceFlags_DefaultOutput = (ExpandResourceFlags_IncludeMetadata | + ExpandResourceFlags_IncludeChildren | + ExpandResourceFlags_IncludeMainDicomTags | + ExpandResourceFlags_IncludeLabels | + ExpandResourceFlags_IncludeIsStable) }; class StatelessDatabaseOperations : public boost::noncopyable @@ -378,6 +390,27 @@ { transaction_.ListAllLabels(target); } + + void ExecuteFind(FindResponse& response, + const FindRequest& request, + const IDatabaseWrapper::Capabilities& capabilities) + { + transaction_.ExecuteFind(response, request, capabilities); + } + + void ExecuteFind(std::list<std::string>& identifiers, + const FindRequest& request, + const IDatabaseWrapper::Capabilities& capabilities) + { + transaction_.ExecuteFind(identifiers, request, capabilities); + } + + void ExecuteExpand(FindResponse& response, + const FindRequest& request, + const std::string& identifier) + { + transaction_.ExecuteExpand(response, request, identifier); + } }; @@ -545,7 +578,6 @@ private: - class MainDicomTagsRegistry; class Transaction; IDatabaseWrapper& db_; @@ -798,5 +830,8 @@ const std::set<std::string>& labels); bool HasLabelsSupport(); + + void ExecuteFind(FindResponse& response, + const FindRequest& request); }; }
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -22,6 +22,8 @@ #include "../PrecompiledHeadersServer.h" +#include "../ResourceFinder.h" + #include "OrthancRestApi.h" #include "../../../OrthancFramework/Sources/Compression/GzipCompressor.h" @@ -129,7 +131,7 @@ // List all the patients, studies, series or instances ---------------------- - static void AnswerListOfResources(RestApiOutput& output, + static void AnswerListOfResources1(RestApiOutput& output, ServerContext& context, const std::list<std::string>& resources, const std::map<std::string, std::string>& instancesIds, // optional: the id of an instance for each found resource. @@ -183,7 +185,7 @@ } - static void AnswerListOfResources(RestApiOutput& output, + static void AnswerListOfResources2(RestApiOutput& output, ServerContext& context, const std::list<std::string>& resources, ResourceType level, @@ -196,7 +198,7 @@ std::map<std::string, boost::shared_ptr<DicomMap> > unusedResourcesMainDicomTags; std::map<std::string, boost::shared_ptr<Json::Value> > unusedResourcesDicomAsJson; - AnswerListOfResources(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess); + AnswerListOfResources1(output, context, resources, unusedInstancesIds, unusedResourcesMainDicomTags, unusedResourcesDicomAsJson, level, expand, format, requestedTags, allowStorageAccess); } @@ -226,41 +228,92 @@ ServerIndex& index = OrthancRestApi::GetIndex(call); ServerContext& context = OrthancRestApi::GetContext(call); - std::list<std::string> result; - - std::set<DicomTag> requestedTags; - OrthancRestApi::GetRequestedTags(requestedTags, call); - - if (call.HasArgument("limit") || - call.HasArgument("since")) + if (true) { - if (!call.HasArgument("limit")) + /** + * EXPERIMENTAL VERSION + **/ + + // TODO-FIND: include the FindRequest options parsing in a method (parse from get-arguments and from post payload) + // TODO-FIND: support other values for expand like expand=MainDicomTags,Labels,Parent,SeriesStatus + const bool expand = (call.HasArgument("expand") && + call.GetBooleanArgument("expand", true)); + + std::set<DicomTag> requestedTags; + OrthancRestApi::GetRequestedTags(requestedTags, call); + + ResourceFinder finder(resourceType, expand); + finder.AddRequestedTags(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<uint64_t>(call.GetArgument("since", "")); + uint64_t limit = boost::lexical_cast<uint64_t>(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<size_t>(call.GetArgument("since", "")); - size_t limit = boost::lexical_cast<size_t>(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.4 + **/ + + std::list<std::string> result; + + std::set<DicomTag> requestedTags; + OrthancRestApi::GetRequestedTags(requestedTags, call); + + if (call.HasArgument("limit") || + call.HasArgument("since")) + { + if (!call.HasArgument("limit")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"limit\" argument for GET request against: " + + call.FlattenUri()); + } + + if (!call.HasArgument("since")) + { + throw OrthancException(ErrorCode_BadRequest, + "Missing \"since\" argument for GET request against: " + + call.FlattenUri()); + } + + size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", "")); + size_t limit = boost::lexical_cast<size_t>(call.GetArgument("limit", "")); + index.GetAllUuids(result, resourceType, since, limit); + } + else + { + index.GetAllUuids(result, resourceType); + } + + AnswerListOfResources2(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true), + OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), + requestedTags, + true /* allowStorageAccess */); } - - AnswerListOfResources(call.GetOutput(), context, result, resourceType, call.HasArgument("expand") && call.GetBooleanArgument("expand", true), - OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), - requestedTags, - true /* allowStorageAccess */); } @@ -289,11 +342,35 @@ std::set<DicomTag> requestedTags; OrthancRestApi::GetRequestedTags(requestedTags, call); - Json::Value json; - if (OrthancRestApi::GetContext(call).ExpandResource( - json, call.GetUriComponent("id", ""), resourceType, format, requestedTags, true /* allowStorageAccess */)) + if (true) { - call.GetOutput().AnswerJson(json); + /** + * EXPERIMENTAL VERSION + **/ + + ResourceFinder finder(resourceType, true /* expand */); + finder.AddRequestedTags(requestedTags); + finder.SetFormat(OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human)); + finder.SetOrthancId(resourceType, call.GetUriComponent("id", "")); + + Json::Value json; + if (finder.ExecuteOneResource(json, OrthancRestApi::GetContext(call))) + { + call.GetOutput().AnswerJson(json); + } + } + else + { + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + + Json::Value json; + if (OrthancRestApi::GetContext(call).ExpandResource( + json, call.GetUriComponent("id", ""), resourceType, format, requestedTags, true /* allowStorageAccess */)) + { + call.GetOutput().AnswerJson(json); + } } } @@ -3140,7 +3217,7 @@ bool expand, const std::set<DicomTag>& requestedTags) const { - AnswerListOfResources(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags, IsStorageAccessAllowedForAnswers(findStorageAccessMode_)); + AnswerListOfResources1(output, context, resources_, instancesIds_, resourcesMainDicomTags_, resourcesDicomAsJson_, level, expand, format_, requestedTags, IsStorageAccessAllowedForAnswers(findStorageAccessMode_)); } }; } @@ -3252,8 +3329,143 @@ throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be an array of strings"); } + else if (true) + { + /** + * EXPERIMENTAL VERSION + **/ + + bool expand = false; + if (request.isMember(KEY_EXPAND)) + { + expand = request[KEY_EXPAND].asBool(); + } + + const ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString()); + + ResourceFinder finder(level, expand); + finder.SetDatabaseLimits(context.GetDatabaseLimits(level)); + finder.SetFormat(OrthancRestApi::GetDicomFormat(request, DicomToJsonFormat_Human)); + + size_t limit = 0; + if (request.isMember(KEY_LIMIT)) + { + int tmp = request[KEY_LIMIT].asInt(); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Field \"" + std::string(KEY_LIMIT) + "\" must be a positive integer"); + } + + limit = static_cast<size_t>(tmp); + } + + size_t since = 0; + if (request.isMember(KEY_SINCE)) + { + int tmp = request[KEY_SINCE].asInt(); + if (tmp < 0) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "Field \"" + std::string(KEY_SINCE) + "\" must be a positive integer"); + } + + since = static_cast<size_t>(tmp); + } + + if (request.isMember(KEY_LIMIT) || + request.isMember(KEY_SINCE)) + { + finder.SetLimits(since, limit); + } + + { + bool caseSensitive = false; + if (request.isMember(KEY_CASE_SENSITIVE)) + { + caseSensitive = request[KEY_CASE_SENSITIVE].asBool(); + } + + DatabaseLookup query; + + Json::Value::Members members = request[KEY_QUERY].getMemberNames(); + for (size_t i = 0; i < members.size(); i++) + { + if (request[KEY_QUERY][members[i]].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, + "Tag \"" + members[i] + "\" must be associated with a string"); + } + + const std::string value = request[KEY_QUERY][members[i]].asString(); + + if (!value.empty()) + { + // An empty string corresponds to an universal constraint, + // so we ignore it. This mimics the behavior of class + // "OrthancFindRequestHandler" + query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), + value, caseSensitive, true); + } + } + + finder.SetDatabaseLookup(query); + } + + if (request.isMember(KEY_REQUESTED_TAGS)) + { + std::set<DicomTag> requestedTags; + FromDcmtkBridge::ParseListOfTags(requestedTags, request[KEY_REQUESTED_TAGS]); + finder.AddRequestedTags(requestedTags); + } + + if (request.isMember(KEY_LABELS)) // New in Orthanc 1.12.0 + { + for (Json::Value::ArrayIndex i = 0; i < request[KEY_LABELS].size(); i++) + { + if (request[KEY_LABELS][i].type() != Json::stringValue) + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS) + "\" must contain strings"); + } + else + { + finder.AddLabel(request[KEY_LABELS][i].asString()); + } + } + } + + finder.SetLabelsConstraint(LabelsConstraint_All); + + if (request.isMember(KEY_LABELS_CONSTRAINT)) + { + const std::string& s = request[KEY_LABELS_CONSTRAINT].asString(); + if (s == "All") + { + finder.SetLabelsConstraint(LabelsConstraint_All); + } + else if (s == "Any") + { + finder.SetLabelsConstraint(LabelsConstraint_Any); + } + else if (s == "None") + { + finder.SetLabelsConstraint(LabelsConstraint_None); + } + else + { + throw OrthancException(ErrorCode_BadRequest, "Field \"" + std::string(KEY_LABELS_CONSTRAINT) + "\" must be \"All\", \"Any\", or \"None\""); + } + } + + Json::Value answer; + finder.Execute(answer, context); + call.GetOutput().AnswerJson(answer); + } else { + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ bool expand = false; if (request.isMember(KEY_EXPAND)) { @@ -3398,34 +3610,57 @@ ServerIndex& index = OrthancRestApi::GetIndex(call); ServerContext& context = OrthancRestApi::GetContext(call); + const bool expand = (!call.HasArgument("expand") || + // this "expand" is the only one to have a false default value to keep backward compatibility + call.GetBooleanArgument("expand", false)); + const DicomToJsonFormat format = OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human); + std::set<DicomTag> requestedTags; OrthancRestApi::GetRequestedTags(requestedTags, call); - std::list<std::string> a, b, c; - a.push_back(call.GetUriComponent("id", "")); - - ResourceType type = start; - while (type != end) + if (true) { - b.clear(); - - for (std::list<std::string>::const_iterator - it = a.begin(); it != a.end(); ++it) + /** + * EXPERIMENTAL VERSION + **/ + + ResourceFinder finder(end, expand); + finder.SetOrthancId(start, call.GetUriComponent("id", "")); + finder.AddRequestedTags(requestedTags); + finder.SetFormat(format); + + Json::Value answer; + finder.Execute(answer, context); + call.GetOutput().AnswerJson(answer); + } + else + { + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + std::list<std::string> a, b, c; + a.push_back(call.GetUriComponent("id", "")); + + ResourceType type = start; + while (type != end) { - index.GetChildren(c, *it); - b.splice(b.begin(), c); + b.clear(); + + for (std::list<std::string>::const_iterator + it = a.begin(); it != a.end(); ++it) + { + index.GetChildren(c, *it); + b.splice(b.begin(), c); + } + + type = GetChildResourceType(type); + + a.clear(); + a.splice(a.begin(), b); } - type = GetChildResourceType(type); - - a.clear(); - a.splice(a.begin(), b); + AnswerListOfResources2(call.GetOutput(), context, a, type, expand, format, requestedTags, true /* allowStorageAccess */); } - - 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 - OrthancRestApi::GetDicomFormat(call, DicomToJsonFormat_Human), - requestedTags, - true /* allowStorageAccess */); }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ResourceFinder.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -0,0 +1,1010 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#include "PrecompiledHeadersServer.h" +#include "ResourceFinder.h" + +#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" +#include "../../OrthancFramework/Sources/Logging.h" +#include "../../OrthancFramework/Sources/OrthancException.h" +#include "../../OrthancFramework/Sources/SerializationToolbox.h" +#include "OrthancConfiguration.h" +#include "Search/DatabaseLookup.h" +#include "ServerContext.h" +#include "ServerIndex.h" + + +namespace Orthanc +{ + static bool IsComputedTag(const DicomTag& tag) + { + return (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES || + tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES || + tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES || + tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES || + tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES || + tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES || + tag == DICOM_TAG_SOP_CLASSES_IN_STUDY || + tag == DICOM_TAG_MODALITIES_IN_STUDY || + tag == DICOM_TAG_INSTANCE_AVAILABILITY); + } + + void ResourceFinder::ConfigureChildrenCountComputedTag(DicomTag tag, + ResourceType parentLevel, + ResourceType childLevel) + { + if (request_.GetLevel() == parentLevel) + { + requestedComputedTags_.insert(tag); + hasRequestedTags_ = true; + request_.GetChildrenSpecification(childLevel).SetRetrieveIdentifiers(true); + } + } + + + void ResourceFinder::InjectChildrenCountComputedTag(DicomMap& requestedTags, + DicomTag tag, + const FindResponse::Resource& resource, + ResourceType level) const + { + if (IsRequestedComputedTag(tag)) + { + const std::set<std::string>& children = resource.GetChildrenIdentifiers(level); + requestedTags.SetValue(tag, boost::lexical_cast<std::string>(children.size()), false); + } + } + + + void ResourceFinder::InjectComputedTags(DicomMap& requestedTags, + const FindResponse::Resource& resource) const + { + switch (resource.GetLevel()) + { + case ResourceType_Patient: + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES, resource, ResourceType_Study); + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES, resource, ResourceType_Series); + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES, resource, ResourceType_Instance); + break; + + case ResourceType_Study: + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES, resource, ResourceType_Series); + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES, resource, ResourceType_Instance); + + if (IsRequestedComputedTag(DICOM_TAG_MODALITIES_IN_STUDY)) + { + std::set<std::string> modalities; + resource.GetChildrenMainDicomTagValues(modalities, ResourceType_Series, DICOM_TAG_MODALITY); + + std::string s; + Toolbox::JoinStrings(s, modalities, "\\"); + + requestedTags.SetValue(DICOM_TAG_MODALITIES_IN_STUDY, s, false); + } + + if (IsRequestedComputedTag(DICOM_TAG_SOP_CLASSES_IN_STUDY)) + { + std::set<std::string> classes; + resource.GetChildrenMetadataValues(classes, ResourceType_Instance, MetadataType_Instance_SopClassUid); + + std::string s; + Toolbox::JoinStrings(s, classes, "\\"); + + requestedTags.SetValue(DICOM_TAG_SOP_CLASSES_IN_STUDY, s, false); + } + + break; + + case ResourceType_Series: + InjectChildrenCountComputedTag(requestedTags, DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES, resource, ResourceType_Instance); + break; + + case ResourceType_Instance: + if (IsRequestedComputedTag(DICOM_TAG_INSTANCE_AVAILABILITY)) + { + requestedTags.SetValue(DICOM_TAG_INSTANCE_AVAILABILITY, "ONLINE", false); + } + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + + SeriesStatus ResourceFinder::GetSeriesStatus(uint32_t& expectedNumberOfInstances, + const FindResponse::Resource& resource) + { + if (resource.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::set<std::string> values; + resource.GetChildrenMetadataValues(values, ResourceType_Instance, MetadataType_Instance_IndexInSeries); + + std::set<int64_t> instances; + + for (std::set<std::string>::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<int64_t>(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<size_t>(expectedNumberOfInstances)) + { + return SeriesStatus_Complete; + } + else + { + return SeriesStatus_Missing; + } + } + + + void ResourceFinder::Expand(Json::Value& target, + const FindResponse::Resource& resource, + ServerIndex& index) const + { + /** + * This method closely follows "SerializeExpandedResource()" in + * "ServerContext.cpp" from Orthanc 1.12.4. + **/ + + 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<std::string>& children = resource.GetChildrenIdentifiers(GetChildResourceType(resource.GetLevel())); + + Json::Value c = Json::arrayValue; + for (std::set<std::string>::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); + + static const char* const EXPECTED_NUMBER_OF_INSTANCES = "ExpectedNumberOfInstances"; + + if (status == SeriesStatus_Unknown) + { + target[EXPECTED_NUMBER_OF_INSTANCES] = Json::nullValue; + } + else + { + target[EXPECTED_NUMBER_OF_INSTANCES] = expectedNumberOfInstances; + } + + break; + } + + case ResourceType_Instance: + { + FileInfo info; + if (resource.LookupAttachment(info, FileContentType_Dicom)) + { + target["FileSize"] = static_cast<Json::UInt64>(info.GetUncompressedSize()); + target["FileUuid"] = info.GetUuid(); + } + else + { + throw OrthancException(ErrorCode_InternalError); + } + + static const char* const INDEX_IN_SERIES = "IndexInSeries"; + + std::string s; + uint32_t index; + if (resource.LookupMetadata(s, ResourceType_Instance, MetadataType_Instance_IndexInSeries) && + SerializationToolbox::ParseUnsignedInteger32(index, s)) + { + target[INDEX_IN_SERIES] = index; + } + else + { + target[INDEX_IN_SERIES] = 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) + { + target["IsStable"] = !index.IsUnstableResource(resource.GetLevel(), resource.GetInternalId()); + + if (resource.LookupMetadata(s, resource.GetLevel(), MetadataType_LastUpdate)) + { + target["LastUpdate"] = s; + } + } + + { + DicomMap allMainDicomTags; + resource.GetMainDicomTags(allMainDicomTags, resource.GetLevel()); + + /** + * This section was part of "StatelessDatabaseOperations::ExpandResource()" + * in Orthanc <= 1.12.3 + **/ + + // read all main sequences from DB + std::string serializedSequences; + if (resource.LookupMetadata(serializedSequences, resource.GetLevel(), MetadataType_MainDicomSequences)) + { + Json::Value jsonMetadata; + Toolbox::ReadJson(jsonMetadata, serializedSequences); + + if (jsonMetadata["Version"].asInt() == 1) + { + allMainDicomTags.FromDicomAsJson(jsonMetadata["Sequences"], true /* append */, true /* parseSequences */); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + + /** + * End of section from StatelessDatabaseOperations + **/ + + + static const char* const MAIN_DICOM_TAGS = "MainDicomTags"; + static const char* const PATIENT_MAIN_DICOM_TAGS = "PatientMainDicomTags"; + + // TODO-FIND : Ignore "null" values + + DicomMap levelMainDicomTags; + allMainDicomTags.ExtractResourceInformation(levelMainDicomTags, resource.GetLevel()); + + target[MAIN_DICOM_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(target[MAIN_DICOM_TAGS], levelMainDicomTags, format_); + + if (resource.GetLevel() == ResourceType_Study) + { + DicomMap patientMainDicomTags; + allMainDicomTags.ExtractPatientInformation(patientMainDicomTags); + + target[PATIENT_MAIN_DICOM_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(target[PATIENT_MAIN_DICOM_TAGS], patientMainDicomTags, format_); + } + } + + { + Json::Value labels = Json::arrayValue; + + for (std::set<std::string>::const_iterator + it = resource.GetLabels().begin(); it != resource.GetLabels().end(); ++it) + { + labels.append(*it); + } + + target["Labels"] = labels; + } + + if (includeAllMetadata_) // new in Orthanc 1.12.4 + { + const std::map<MetadataType, std::string>& m = resource.GetMetadata(resource.GetLevel()); + + Json::Value metadata = Json::objectValue; + + for (std::map<MetadataType, std::string>::const_iterator it = m.begin(); it != m.end(); ++it) + { + metadata[EnumerationToString(it->first)] = it->second; + } + + target["Metadata"] = metadata; + } + } + + + void ResourceFinder::UpdateRequestLimits() + { + // By default, use manual paging + isDatabasePaging_ = false; + + if (databaseLimits_ != 0) + { + request_.SetLimits(0, databaseLimits_ + 1); + } + + if (lookup_.get() == NULL || + lookup_->HasOnlyMainDicomTags()) + { + // TODO-FIND: Understand why this doesn't work: + // $ ./Start.sh --force Orthanc.test_rest_find_limit Orthanc.test_resources_since_limit Orthanc.test_rest_find_limit + + /*if (hasLimits_) + { + isDatabasePaging_ = true; + request_.SetLimits(limitsSince_, limitsCount_); + }*/ + } + + // TODO-FIND: More cases could be added, depending on "GetDatabaseCapabilities()" + } + + + ResourceFinder::ResourceFinder(ResourceType level, + bool expand) : + request_(level), + databaseLimits_(0), + isDatabasePaging_(true), + hasLimits_(false), + limitsSince_(0), + limitsCount_(0), + expand_(expand), + format_(DicomToJsonFormat_Human), + allowStorageAccess_(true), + hasRequestedTags_(false), + includeAllMetadata_(false) + { + UpdateRequestLimits(); + + if (expand) + { + request_.SetRetrieveMainDicomTags(true); + request_.SetRetrieveMetadata(true); + request_.SetRetrieveLabels(true); + + switch (level) + { + case ResourceType_Patient: + request_.GetChildrenSpecification(ResourceType_Study).SetRetrieveIdentifiers(true); + break; + + case ResourceType_Study: + request_.GetChildrenSpecification(ResourceType_Series).SetRetrieveIdentifiers(true); + request_.SetRetrieveParentIdentifier(true); + break; + + case ResourceType_Series: + request_.GetChildrenSpecification(ResourceType_Instance).AddMetadata(MetadataType_Instance_IndexInSeries); // required for the SeriesStatus + request_.GetChildrenSpecification(ResourceType_Instance).SetRetrieveIdentifiers(true); + request_.SetRetrieveParentIdentifier(true); + break; + + case ResourceType_Instance: + request_.SetRetrieveAttachments(true); // for FileSize & FileUuid + request_.SetRetrieveParentIdentifier(true); + break; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + } + + + void ResourceFinder::SetDatabaseLimits(uint64_t limits) + { + databaseLimits_ = limits; + UpdateRequestLimits(); + } + + + void ResourceFinder::SetLimits(uint64_t since, + uint64_t count) + { + if (hasLimits_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + hasLimits_ = true; + limitsSince_ = since; + limitsCount_ = count; + UpdateRequestLimits(); + } + } + + + void ResourceFinder::SetDatabaseLookup(const DatabaseLookup& lookup) + { + lookup_.reset(lookup.Clone()); + UpdateRequestLimits(); + + for (size_t i = 0; i < lookup.GetConstraintsCount(); i++) + { + DicomTag tag = lookup.GetConstraint(i).GetTag(); + if (IsComputedTag(tag)) + { + AddRequestedTag(tag); + } + } + + MainDicomTagsRegistry registry; + registry.NormalizeLookup(request_.GetDicomTagConstraints(), lookup, request_.GetLevel()); + + // "request_.GetDicomTagConstraints()" only contains constraints on main DICOM tags + + for (size_t i = 0; i < request_.GetDicomTagConstraints().GetSize(); i++) + { + const DatabaseConstraint& constraint = request_.GetDicomTagConstraints().GetConstraint(i); + if (constraint.GetLevel() == request_.GetLevel()) + { + request_.SetRetrieveMainDicomTags(true); + } + else if (IsResourceLevelAboveOrEqual(constraint.GetLevel(), request_.GetLevel())) + { + request_.GetParentSpecification(constraint.GetLevel()).SetRetrieveMainDicomTags(true); + } + else + { + LOG(WARNING) << "Executing a database lookup at level " << EnumerationToString(request_.GetLevel()) + << " on main DICOM tag " << constraint.GetTag().Format() << " from an inferior level (" + << EnumerationToString(constraint.GetLevel()) << "), this will return no result"; + } + + if (IsComputedTag(constraint.GetTag())) + { + // Sanity check + throw OrthancException(ErrorCode_InternalError); + } + } + } + + + void ResourceFinder::AddRequestedTag(const DicomTag& tag) + { + if (DicomMap::IsMainDicomTag(tag, ResourceType_Patient)) + { + if (request_.GetLevel() == ResourceType_Patient) + { + request_.GetParentSpecification(ResourceType_Patient).SetRetrieveMainDicomTags(true); + request_.GetParentSpecification(ResourceType_Patient).SetRetrieveMetadata(true); + requestedPatientTags_.insert(tag); + } + else + { + /** + * This comes from the fact that patient-level tags are copied + * at the study level, as implemented by "ResourcesContent::AddResource()". + **/ + request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true); + request_.GetParentSpecification(ResourceType_Study).SetRetrieveMetadata(true); + requestedStudyTags_.insert(tag); + } + + hasRequestedTags_ = true; + } + else if (DicomMap::IsMainDicomTag(tag, ResourceType_Study)) + { + if (request_.GetLevel() == ResourceType_Patient) + { + LOG(WARNING) << "Requested tag " << tag.Format() + << " should only be read at the study, series, or instance level"; + requestedTagsFromFileStorage_.insert(tag); + request_.SetRetrieveOneInstanceIdentifier(true); + } + else + { + request_.GetParentSpecification(ResourceType_Study).SetRetrieveMainDicomTags(true); + request_.GetParentSpecification(ResourceType_Study).SetRetrieveMetadata(true); + requestedStudyTags_.insert(tag); + } + + hasRequestedTags_ = true; + } + else if (DicomMap::IsMainDicomTag(tag, ResourceType_Series)) + { + if (request_.GetLevel() == ResourceType_Patient || + request_.GetLevel() == ResourceType_Study) + { + LOG(WARNING) << "Requested tag " << tag.Format() + << " should only be read at the series or instance level"; + requestedTagsFromFileStorage_.insert(tag); + request_.SetRetrieveOneInstanceIdentifier(true); + } + else + { + request_.GetParentSpecification(ResourceType_Series).SetRetrieveMainDicomTags(true); + request_.GetParentSpecification(ResourceType_Series).SetRetrieveMetadata(true); + requestedSeriesTags_.insert(tag); + } + + hasRequestedTags_ = true; + } + else if (DicomMap::IsMainDicomTag(tag, ResourceType_Instance)) + { + if (request_.GetLevel() == ResourceType_Patient || + request_.GetLevel() == ResourceType_Study || + request_.GetLevel() == ResourceType_Series) + { + LOG(WARNING) << "Requested tag " << tag.Format() + << " should only be read at the instance level"; + requestedTagsFromFileStorage_.insert(tag); + request_.SetRetrieveOneInstanceIdentifier(true); + } + else + { + // Main DICOM tags from the instance level will be retrieved anyway + assert(request_.IsRetrieveMainDicomTags()); + assert(request_.IsRetrieveMetadata()); + requestedInstanceTags_.insert(tag); + } + + hasRequestedTags_ = true; + } + else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES) + { + ConfigureChildrenCountComputedTag(tag, ResourceType_Patient, ResourceType_Study); + } + else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES) + { + ConfigureChildrenCountComputedTag(tag, ResourceType_Patient, ResourceType_Series); + } + else if (tag == DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES) + { + ConfigureChildrenCountComputedTag(tag, ResourceType_Patient, ResourceType_Instance); + } + else if (tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES) + { + ConfigureChildrenCountComputedTag(tag, ResourceType_Study, ResourceType_Series); + } + else if (tag == DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES) + { + ConfigureChildrenCountComputedTag(tag, ResourceType_Study, ResourceType_Instance); + } + else if (tag == DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES) + { + ConfigureChildrenCountComputedTag(tag, ResourceType_Series, ResourceType_Instance); + } + else if (tag == DICOM_TAG_SOP_CLASSES_IN_STUDY) + { + requestedComputedTags_.insert(tag); + hasRequestedTags_ = true; + request_.GetChildrenSpecification(ResourceType_Instance).AddMetadata(MetadataType_Instance_SopClassUid); + } + else if (tag == DICOM_TAG_MODALITIES_IN_STUDY) + { + requestedComputedTags_.insert(tag); + hasRequestedTags_ = true; + request_.GetChildrenSpecification(ResourceType_Series).AddMainDicomTag(DICOM_TAG_MODALITY); + } + else if (tag == DICOM_TAG_INSTANCE_AVAILABILITY) + { + requestedComputedTags_.insert(tag); + hasRequestedTags_ = true; + } + else + { + // This is neither a main DICOM tag, nor a computed DICOM tag: + // We will be forced to access the DICOM file anyway + requestedTagsFromFileStorage_.insert(tag); + + if (request_.GetLevel() != ResourceType_Instance) + { + request_.SetRetrieveOneInstanceIdentifier(true); + } + + hasRequestedTags_ = true; + } + } + + + void ResourceFinder::AddRequestedTags(const std::set<DicomTag>& tags) + { + for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + AddRequestedTag(*it); + } + } + + + static void InjectRequestedTags(DicomMap& requestedTags, + std::set<DicomTag>& missingTags /* out */, + const FindResponse::Resource& resource, + ResourceType level, + const std::set<DicomTag>& tags) + { + if (!tags.empty()) + { + DicomMap m; + resource.GetMainDicomTags(m, level); + + for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) + { + std::string value; + if (m.LookupStringValue(value, *it, false /* not binary */)) + { + requestedTags.SetValue(*it, value, false /* not binary */); + } + else + { + // This is the case where the Housekeeper should be run + missingTags.insert(*it); + } + } + } + } + + + static void ReadMissingTagsFromStorageArea(DicomMap& requestedTags, + ServerContext& context, + const FindRequest& request, + const FindResponse::Resource& resource, + const std::set<DicomTag>& missingTags) + { + OrthancConfiguration::ReaderLock lock; + if (lock.GetConfiguration().IsWarningEnabled(Warnings_001_TagsBeingReadFromStorage)) + { + std::string missings; + FromDcmtkBridge::FormatListOfTags(missings, missingTags); + + LOG(WARNING) << "W001: Accessing Dicom tags from storage when accessing " + << Orthanc::GetResourceTypeText(resource.GetLevel(), false, false) + << ": " << missings; + } + + std::string instancePublicId; + + if (request.IsRetrieveOneInstanceIdentifier()) + { + instancePublicId = resource.GetOneInstanceIdentifier(); + } + else if (request.GetLevel() == ResourceType_Instance) + { + instancePublicId = resource.GetIdentifier(); + } + else + { + FindRequest requestDicomAttachment(request.GetLevel()); + requestDicomAttachment.SetOrthancId(request.GetLevel(), resource.GetIdentifier()); + requestDicomAttachment.SetRetrieveOneInstanceIdentifier(true); + + FindResponse responseDicomAttachment; + context.GetIndex().ExecuteFind(responseDicomAttachment, requestDicomAttachment); + + if (responseDicomAttachment.GetSize() != 1 || + !responseDicomAttachment.GetResourceByIndex(0).HasOneInstanceIdentifier()) + { + throw OrthancException(ErrorCode_InexistentFile); + } + else + { + instancePublicId = responseDicomAttachment.GetResourceByIndex(0).GetOneInstanceIdentifier(); + } + } + + LOG(INFO) << "Will retrieve missing DICOM tags from instance: " << instancePublicId; + + // TODO-FIND: What do we do if the DICOM has been removed since the request? + // Do we fail, or do we skip the resource? + + Json::Value tmpDicomAsJson; + context.ReadDicomAsJson(tmpDicomAsJson, instancePublicId, missingTags /* ignoreTagLength */); + + DicomMap tmpDicomMap; + tmpDicomMap.FromDicomAsJson(tmpDicomAsJson, false /* append */, true /* parseSequences*/); + + for (std::set<DicomTag>::const_iterator it = missingTags.begin(); it != missingTags.end(); ++it) + { + assert(!requestedTags.HasTag(*it)); + if (tmpDicomMap.HasTag(*it)) + { + requestedTags.SetValue(*it, tmpDicomMap.GetValue(*it)); + } + else + { + requestedTags.SetNullValue(*it); // TODO-FIND: Is this compatible with Orthanc <= 1.12.3? + } + } + } + + + void ResourceFinder::Execute(FindResponse& response, + ServerIndex& index) const + { + index.ExecuteFind(response, request_); + } + + + void ResourceFinder::Execute(IVisitor& visitor, + ServerContext& context) const + { + FindResponse response; + context.GetIndex().ExecuteFind(response, request_); + + bool complete; + if (isDatabasePaging_) + { + complete = true; + } + else + { + complete = (databaseLimits_ == 0 || + response.GetSize() <= databaseLimits_); + } + + if (lookup_.get() != NULL) + { + LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << response.GetSize(); + } + + size_t countResults = 0; + size_t skipped = 0; + + for (size_t i = 0; i < response.GetSize(); i++) + { + const FindResponse::Resource& resource = response.GetResourceByIndex(i); + + DicomMap requestedTags; + + if (hasRequestedTags_) + { + InjectComputedTags(requestedTags, resource); + + std::set<DicomTag> missingTags = requestedTagsFromFileStorage_; + InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Patient, requestedPatientTags_); + InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Study, requestedStudyTags_); + InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Series, requestedSeriesTags_); + InjectRequestedTags(requestedTags, missingTags, resource, ResourceType_Instance, requestedInstanceTags_); + + if (!missingTags.empty()) + { + if (!allowStorageAccess_) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "Cannot add missing requested tags, as access to file storage is disallowed"); + } + else + { + ReadMissingTagsFromStorageArea(requestedTags, context, request_, resource, missingTags); + } + } + } + + bool match = true; + + if (lookup_.get() != NULL) + { + DicomMap tags; + resource.GetAllMainDicomTags(tags); + tags.Merge(requestedTags); + match = lookup_->IsMatch(tags); + } + + if (match) + { + if (isDatabasePaging_) + { + visitor.Apply(resource, hasRequestedTags_, requestedTags); + } + else + { + if (hasLimits_ && + skipped < limitsSince_) + { + skipped++; + } + else if (hasLimits_ && + countResults >= limitsCount_) + { + // Too many results, don't mark as complete + complete = false; + break; + } + else + { + visitor.Apply(resource, hasRequestedTags_, requestedTags); + countResults++; + } + } + } + } + + if (complete) + { + visitor.MarkAsComplete(); + } + } + + + void ResourceFinder::Execute(Json::Value& target, + ServerContext& context) const + { + class Visitor : public IVisitor + { + private: + const ResourceFinder& that_; + ServerIndex& index_; + Json::Value& target_; + + public: + Visitor(const ResourceFinder& that, + ServerIndex& index, + Json::Value& target) : + that_(that), + index_(index), + target_(target) + { + } + + virtual void Apply(const FindResponse::Resource& resource, + bool hasRequestedTags, + const DicomMap& requestedTags) ORTHANC_OVERRIDE + { + if (that_.expand_) + { + Json::Value item; + that_.Expand(item, resource, index_); + + if (hasRequestedTags) + { + static const char* const REQUESTED_TAGS = "RequestedTags"; + item[REQUESTED_TAGS] = Json::objectValue; + FromDcmtkBridge::ToJson(item[REQUESTED_TAGS], requestedTags, that_.format_); + } + + target_.append(item); + } + else + { + target_.append(resource.GetIdentifier()); + } + } + + virtual void MarkAsComplete() ORTHANC_OVERRIDE + { + } + }; + + target = Json::arrayValue; + + Visitor visitor(*this, context.GetIndex(), target); + Execute(visitor, context); + } + + + bool ResourceFinder::ExecuteOneResource(Json::Value& target, + ServerContext& context) const + { + Json::Value answer; + Execute(answer, context); + + if (answer.type() != Json::arrayValue) + { + throw OrthancException(ErrorCode_InternalError); + } + else if (answer.size() > 1) + { + throw OrthancException(ErrorCode_DatabasePlugin); + } + else if (answer.empty()) + { + // Inexistent resource (or was deleted between the first and second phases) + return false; + } + else + { + target = answer[0]; + return true; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/Sources/ResourceFinder.h Tue Jul 09 18:06:21 2024 +0200 @@ -0,0 +1,171 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2023 Osimis S.A., Belgium + * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium + * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + **/ + + +#pragma once + +#include "Database/FindRequest.h" +#include "Database/FindResponse.h" + +namespace Orthanc +{ + class DatabaseLookup; + class ServerContext; + class ServerIndex; + + class ResourceFinder : public boost::noncopyable + { + public: + class IVisitor : public boost::noncopyable + { + public: + virtual ~IVisitor() + { + } + + virtual void Apply(const FindResponse::Resource& resource, + bool hasRequestedTags, + const DicomMap& requestedTags) = 0; + + virtual void MarkAsComplete() = 0; + }; + + private: + FindRequest request_; + uint64_t databaseLimits_; + std::unique_ptr<DatabaseLookup> lookup_; + bool isDatabasePaging_; + bool hasLimits_; + uint64_t limitsSince_; + uint64_t limitsCount_; + bool expand_; + DicomToJsonFormat format_; + bool allowStorageAccess_; + bool hasRequestedTags_; + std::set<DicomTag> requestedPatientTags_; + std::set<DicomTag> requestedStudyTags_; + std::set<DicomTag> requestedSeriesTags_; + std::set<DicomTag> requestedInstanceTags_; + std::set<DicomTag> requestedTagsFromFileStorage_; + std::set<DicomTag> requestedComputedTags_; + bool includeAllMetadata_; // Same as: ExpandResourceFlags_IncludeAllMetadata + + bool IsRequestedComputedTag(const DicomTag& tag) const + { + return requestedComputedTags_.find(tag) != requestedComputedTags_.end(); + } + + void ConfigureChildrenCountComputedTag(DicomTag tag, + ResourceType parentLevel, + ResourceType childLevel); + + void InjectChildrenCountComputedTag(DicomMap& requestedTags, + DicomTag tag, + const FindResponse::Resource& resource, + ResourceType level) const; + + static SeriesStatus GetSeriesStatus(uint32_t& expectedNumberOfInstances, + const FindResponse::Resource& resource); + + void InjectComputedTags(DicomMap& requestedTags, + const FindResponse::Resource& resource) const; + + void Expand(Json::Value& target, + const FindResponse::Resource& resource, + ServerIndex& index) const; + + void UpdateRequestLimits(); + + public: + ResourceFinder(ResourceType level, + bool expand); + + void SetDatabaseLimits(uint64_t limits); + + bool IsAllowStorageAccess() const + { + return allowStorageAccess_; + } + + void SetAllowStorageAccess(bool allow) + { + allowStorageAccess_ = allow; + } + + void SetOrthancId(ResourceType level, + const std::string& id) + { + request_.SetOrthancId(level, id); + } + + void SetFormat(DicomToJsonFormat format) + { + format_ = format; + } + + void SetLimits(uint64_t since, + uint64_t count); + + void SetDatabaseLookup(const DatabaseLookup& lookup); + + void SetIncludeAllMetadata(bool include) + { + includeAllMetadata_ = include; + } + + void AddRequestedTag(const DicomTag& tag); + + void AddRequestedTags(const std::set<DicomTag>& tags); + + void SetLabels(const std::set<std::string>& labels) + { + request_.SetLabels(labels); + } + + void AddLabel(const std::string& label) + { + request_.AddLabel(label); + } + + void SetLabelsConstraint(LabelsConstraint constraint) + { + request_.SetLabelsConstraint(constraint); + } + + void SetRetrieveOneInstanceIdentifier(bool retrieve) + { + request_.SetRetrieveOneInstanceIdentifier(retrieve); + } + + void Execute(FindResponse& target, + ServerIndex& index) const; + + void Execute(IVisitor& visitor, + ServerContext& context) const; + + void Execute(Json::Value& target, + ServerContext& context) const; + + bool ExecuteOneResource(Json::Value& target, + ServerContext& context) const; + }; +}
--- a/OrthancServer/Sources/Search/DatabaseConstraint.cpp Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/Search/DatabaseConstraint.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -37,6 +37,7 @@ # include <OrthancException.h> #endif +#include <boost/lexical_cast.hpp> #include <cassert> @@ -284,4 +285,64 @@ return *constraints_[index]; } } + + + std::string DatabaseConstraints::Format() const + { + std::string s; + + for (size_t i = 0; i < constraints_.size(); i++) + { + assert(constraints_[i] != NULL); + const DatabaseConstraint& constraint = *constraints_[i]; + s += "Constraint " + boost::lexical_cast<std::string>(i) + " at " + EnumerationToString(constraint.GetLevel()) + + ": " + constraint.GetTag().Format(); + + switch (constraint.GetConstraintType()) + { + case ConstraintType_Equal: + s += " == " + constraint.GetSingleValue(); + break; + + case ConstraintType_SmallerOrEqual: + s += " <= " + constraint.GetSingleValue(); + break; + + case ConstraintType_GreaterOrEqual: + s += " >= " + constraint.GetSingleValue(); + break; + + case ConstraintType_Wildcard: + s += " ~~ " + constraint.GetSingleValue(); + break; + + case ConstraintType_List: + { + s += " in [ "; + bool first = true; + for (size_t i = 0; i < constraint.GetValuesCount(); i++) + { + if (first) + { + first = false; + } + else + { + s += ", "; + } + s += constraint.GetValue(i); + } + s += "]"; + break; + } + + default: + throw OrthancException(ErrorCode_InternalError); + } + + s += "\n"; + } + + return s; + } }
--- a/OrthancServer/Sources/Search/DatabaseConstraint.h Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/Search/DatabaseConstraint.h Tue Jul 09 18:06:21 2024 +0200 @@ -178,5 +178,7 @@ } const DatabaseConstraint& GetConstraint(size_t index) const; + + std::string Format() const; }; }
--- a/OrthancServer/Sources/Search/DicomTagConstraint.cpp Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -215,6 +215,24 @@ } + static bool HasIntersection(const std::set<std::string>& expected, + const std::string& values) + { + std::vector<std::string> tokens; + Toolbox::TokenizeString(tokens, values, '\\'); + + for (size_t i = 0; i < tokens.size(); i++) + { + if (expected.find(tokens[i]) != expected.end()) + { + return true; + } + } + + return false; + } + + bool DicomTagConstraint::IsMatch(const std::string& value) const { NormalizedString source(value, caseSensitive_); @@ -224,7 +242,17 @@ case ConstraintType_Equal: { NormalizedString reference(GetValue(), caseSensitive_); - return source.GetValue() == reference.GetValue(); + + if (GetTag() == DICOM_TAG_MODALITIES_IN_STUDY) + { + std::set<std::string> expected; + expected.insert(reference.GetValue()); + return HasIntersection(expected, source.GetValue()); + } + else + { + return source.GetValue() == reference.GetValue(); + } } case ConstraintType_SmallerOrEqual: @@ -251,17 +279,16 @@ case ConstraintType_List: { + std::set<std::string> references; + for (std::set<std::string>::const_iterator it = values_.begin(); it != values_.end(); ++it) { NormalizedString reference(*it, caseSensitive_); - if (source.GetValue() == reference.GetValue()) - { - return true; - } + references.insert(reference.GetValue()); } - return false; + return HasIntersection(references, source.GetValue()); } default:
--- a/OrthancServer/Sources/ServerContext.cpp Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/ServerContext.cpp Tue Jul 09 18:06:21 2024 +0200 @@ -42,6 +42,7 @@ #include "OrthancConfiguration.h" #include "OrthancRestApi/OrthancRestApi.h" +#include "ResourceFinder.h" #include "Search/DatabaseLookup.h" #include "ServerJobs/OrthancJobUnserializer.h" #include "ServerToolbox.h" @@ -1538,8 +1539,7 @@ size_t since, size_t limit) { - unsigned int databaseLimit = (queryLevel == ResourceType_Instance ? - limitFindInstances_ : limitFindResults_); + const uint64_t databaseLimit = GetDatabaseLimits(queryLevel); std::vector<std::string> resources, instances; const DicomTagConstraint* dicomModalitiesConstraint = NULL; @@ -1556,7 +1556,50 @@ fastLookup->RemoveConstraint(DICOM_TAG_MODALITIES_IN_STUDY); } + if (true) { + /** + * EXPERIMENTAL VERSION + **/ + + ResourceFinder finder(queryLevel, false /* TODO-FIND: don't expand for now */); + finder.SetDatabaseLimits(databaseLimit); + finder.SetDatabaseLookup(lookup); + finder.SetLabels(labels); + finder.SetLabelsConstraint(labelsConstraint); + + if (queryLevel != ResourceType_Instance) + { + finder.SetRetrieveOneInstanceIdentifier(true); + } + + FindResponse response; + finder.Execute(response, GetIndex()); + + resources.resize(response.GetSize()); + instances.resize(response.GetSize()); + + for (size_t i = 0; i < response.GetSize(); i++) + { + const FindResponse::Resource& resource = response.GetResourceByIndex(i); + resources[i] = resource.GetIdentifier(); + + if (queryLevel == ResourceType_Instance) + { + instances[i] = resource.GetIdentifier(); + } + else + { + instances[i] = resource.GetOneInstanceIdentifier(); + } + } + } + else + { + /** + * VERSION IN ORTHANC <= 1.12.4 + **/ + const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1); GetIndex().ApplyLookupResources(resources, &instances, *fastLookup, queryLevel, labels, labelsConstraint, lookupLimit); } @@ -2127,124 +2170,137 @@ static void SerializeExpandedResource(Json::Value& target, const ExpandedResource& resource, DicomToJsonFormat format, - const std::set<DicomTag>& requestedTags) + const std::set<DicomTag>& requestedTags, + ExpandResourceFlags expandFlags) { target = Json::objectValue; target["Type"] = GetResourceTypeText(resource.GetLevel(), false, true); target["ID"] = resource.GetPublicId(); - switch (resource.GetLevel()) - { - case ResourceType_Patient: - break; - - case ResourceType_Study: - target["ParentPatient"] = resource.parentId_; - break; - - case ResourceType_Series: - target["ParentStudy"] = resource.parentId_; - break; - - case ResourceType_Instance: - target["ParentSeries"] = resource.parentId_; - break; - - default: - throw OrthancException(ErrorCode_InternalError); - } - - switch (resource.GetLevel()) + if (!resource.parentId_.empty()) { - case ResourceType_Patient: - case ResourceType_Study: - case ResourceType_Series: + switch (resource.GetLevel()) { - Json::Value c = Json::arrayValue; - - for (std::list<std::string>::const_iterator - it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it) - { - c.append(*it); - } - - if (resource.GetLevel() == ResourceType_Patient) - { - target["Studies"] = c; - } - else if (resource.GetLevel() == ResourceType_Study) - { - target["Series"] = c; - } - else - { - target["Instances"] = c; - } - break; + case ResourceType_Patient: + break; + + case ResourceType_Study: + target["ParentPatient"] = resource.parentId_; + break; + + case ResourceType_Series: + target["ParentStudy"] = resource.parentId_; + break; + + case ResourceType_Instance: + target["ParentSeries"] = resource.parentId_; + break; + + default: + throw OrthancException(ErrorCode_InternalError); } - - case ResourceType_Instance: - break; - - default: - throw OrthancException(ErrorCode_InternalError); } - switch (resource.GetLevel()) + if ((expandFlags & ExpandResourceFlags_IncludeChildren) != 0) { - case ResourceType_Patient: - case ResourceType_Study: - break; - - case ResourceType_Series: - if (resource.expectedNumberOfInstances_ < 0) + switch (resource.GetLevel()) + { + case ResourceType_Patient: + case ResourceType_Study: + case ResourceType_Series: { - target["ExpectedNumberOfInstances"] = Json::nullValue; + Json::Value c = Json::arrayValue; + + for (std::list<std::string>::const_iterator + it = resource.childrenIds_.begin(); it != resource.childrenIds_.end(); ++it) + { + c.append(*it); + } + + if (resource.GetLevel() == ResourceType_Patient) + { + target["Studies"] = c; + } + else if (resource.GetLevel() == ResourceType_Study) + { + target["Series"] = c; + } + else + { + target["Instances"] = c; + } + break; } - else - { - target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_; - } - target["Status"] = resource.status_; - break; - - case ResourceType_Instance: + + case ResourceType_Instance: + break; + + default: + throw OrthancException(ErrorCode_InternalError); + } + } + + if ((expandFlags & ExpandResourceFlags_IncludeMetadata) != 0) + { + switch (resource.GetLevel()) { - target["FileSize"] = static_cast<unsigned int>(resource.fileSize_); - target["FileUuid"] = resource.fileUuid_; - - if (resource.indexInSeries_ < 0) + case ResourceType_Patient: + case ResourceType_Study: + break; + + case ResourceType_Series: + if (resource.expectedNumberOfInstances_ < 0) + { + target["ExpectedNumberOfInstances"] = Json::nullValue; + } + else + { + target["ExpectedNumberOfInstances"] = resource.expectedNumberOfInstances_; + } + target["Status"] = resource.status_; + break; + + case ResourceType_Instance: { - target["IndexInSeries"] = Json::nullValue; + target["FileSize"] = static_cast<unsigned int>(resource.fileSize_); + target["FileUuid"] = resource.fileUuid_; + + if (resource.indexInSeries_ < 0) + { + target["IndexInSeries"] = Json::nullValue; + } + else + { + target["IndexInSeries"] = resource.indexInSeries_; + } + + break; } - else - { - target["IndexInSeries"] = resource.indexInSeries_; - } - - break; + + default: + throw OrthancException(ErrorCode_InternalError); } - - default: - throw OrthancException(ErrorCode_InternalError); - } - - if (!resource.anonymizedFrom_.empty()) - { - target["AnonymizedFrom"] = resource.anonymizedFrom_; - } - if (!resource.modifiedFrom_.empty()) - { - target["ModifiedFrom"] = resource.modifiedFrom_; + if (!resource.anonymizedFrom_.empty()) + { + target["AnonymizedFrom"] = resource.anonymizedFrom_; + } + + if (!resource.modifiedFrom_.empty()) + { + target["ModifiedFrom"] = resource.modifiedFrom_; + } } if (resource.GetLevel() == ResourceType_Patient || resource.GetLevel() == ResourceType_Study || resource.GetLevel() == ResourceType_Series) { - target["IsStable"] = resource.isStable_; + if ((expandFlags & ExpandResourceFlags_IncludeIsStable) != 0) + { + target["IsStable"] = resource.isStable_; + } if (!resource.lastUpdate_.empty()) { @@ -2252,38 +2308,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; @@ -2294,6 +2354,19 @@ target["Labels"] = labels; } + + // new in Orthanc 1.12.4 + if ((expandFlags & ExpandResourceFlags_IncludeAllMetadata) != 0) + { + Json::Value metadata = Json::objectValue; + + for (std::map<MetadataType, std::string>::const_iterator it = resource.metadata_.begin(); it != resource.metadata_.end(); ++it) + { + metadata[EnumerationToString(it->first)] = it->second; + } + + target["Metadata"] = metadata; + } } @@ -2539,9 +2612,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; } @@ -2694,5 +2767,4 @@ return elapsed.total_seconds(); } - }
--- a/OrthancServer/Sources/ServerContext.h Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/ServerContext.h Tue Jul 09 18:06:21 2024 +0200 @@ -442,6 +442,11 @@ void Stop(); + uint64_t GetDatabaseLimits(ResourceType level) const + { + return (level == ResourceType_Instance ? limitFindInstances_ : limitFindResults_); + } + void Apply(ILookupVisitor& visitor, const DatabaseLookup& lookup, ResourceType queryLevel,
--- a/OrthancServer/Sources/ServerIndex.h Tue Jul 09 15:32:30 2024 +0200 +++ b/OrthancServer/Sources/ServerIndex.h Tue Jul 09 18:06:21 2024 +0200 @@ -64,9 +64,6 @@ int64_t id, const std::string& publicId); - bool IsUnstableResource(ResourceType type, - int64_t id); - public: ServerIndex(ServerContext& context, IDatabaseWrapper& database, @@ -103,5 +100,8 @@ bool hasOldRevision, int64_t oldRevision, const std::string& oldMD5); + + bool IsUnstableResource(ResourceType type, + int64_t id); }; }